Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ jobs:
submodules: 'recursive'

- name: Download test report
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: test-report-file

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v6
-
name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916
uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
yaml-file: .github/labels.yml
Expand Down
6 changes: 3 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# documentation root, use Path(...).absolute() to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
from pathlib import Path
sys.path.insert(0, str(Path('..').absolute()))


# -- Project information -----------------------------------------------------
Expand Down
8 changes: 4 additions & 4 deletions linode_api4/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from dataclasses import dataclass
from pathlib import Path

from linode_api4.objects import JSONObject

Expand Down Expand Up @@ -47,9 +47,9 @@ def load_and_validate_keys(authorized_keys):
ret.append(k)
else:
# it doesn't appear to be a key.. is it a path to the key?
k = os.path.expanduser(k)
if os.path.isfile(k):
with open(k) as f:
k_path = Path(k).expanduser()
if k_path.is_file():
with open(k_path) as f:
ret.append(f.read().rstrip())
else:
raise ValueError(
Expand Down
7 changes: 4 additions & 3 deletions linode_api4/groups/linode.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import base64
import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

from linode_api4.common import load_and_validate_keys
Expand Down Expand Up @@ -457,8 +457,9 @@ def stackscript_create(
script_body = script
if not script.startswith("#!"):
# it doesn't look like a stackscript body, let's see if it's a file
if os.path.isfile(script):
with open(script) as f:
script_path = Path(script)
if script_path.is_file():
with open(script_path) as f:
script_body = f.read()
else:
raise ValueError(
Expand Down
8 changes: 4 additions & 4 deletions linode_api4/groups/profile.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from datetime import datetime
from pathlib import Path

from linode_api4 import UnexpectedResponseError
from linode_api4.common import SSH_KEY_TYPES
Expand Down Expand Up @@ -322,9 +322,9 @@ def ssh_key_upload(self, key, label):
"""
if not key.startswith(SSH_KEY_TYPES):
# this might be a file path - look for it
path = os.path.expanduser(key)
if os.path.isfile(path):
with open(path) as f:
key_path = Path(key).expanduser()
if key_path.is_file():
with open(key_path) as f:
key = f.read().strip()
if not key.startswith(SSH_KEY_TYPES):
raise ValueError("Invalid SSH Public Key")
Expand Down
115 changes: 0 additions & 115 deletions linode_api4/objects/database.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
from dataclasses import dataclass, field
from typing import Optional

from deprecated import deprecated

from linode_api4.objects import (
Base,
DerivedBase,
JSONObject,
MappedObject,
Property,
Expand Down Expand Up @@ -86,69 +83,6 @@ class DatabasePrivateNetwork(JSONObject):
public_access: Optional[bool] = None


@deprecated(
reason="Backups are not supported for non-legacy database clusters."
)
class DatabaseBackup(DerivedBase):
"""
A generic Managed Database backup.

This class is not intended to be used on its own.
Use the appropriate subclasses for the corresponding database engine. (e.g. MySQLDatabaseBackup)
"""

api_endpoint = ""
derived_url_path = "backups"
parent_id_name = "database_id"

properties = {
"created": Property(is_datetime=True),
"id": Property(identifier=True),
"label": Property(),
"type": Property(),
}

def restore(self):
"""
Restore a backup to a Managed Database on your Account.

API Documentation:

- MySQL: https://techdocs.akamai.com/linode-api/reference/post-databases-mysql-instance-backup-restore
- PostgreSQL: https://techdocs.akamai.com/linode-api/reference/post-databases-postgre-sql-instance-backup-restore
"""

return self._client.post(
"{}/restore".format(self.api_endpoint), model=self
)


@deprecated(
reason="Backups are not supported for non-legacy database clusters."
)
class MySQLDatabaseBackup(DatabaseBackup):
"""
A backup for an accessible Managed MySQL Database.

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-databases-mysql-instance-backup
"""

api_endpoint = "/databases/mysql/instances/{database_id}/backups/{id}"


@deprecated(
reason="Backups are not supported for non-legacy database clusters."
)
class PostgreSQLDatabaseBackup(DatabaseBackup):
"""
A backup for an accessible Managed PostgreSQL Database.

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-databases-postgresql-instance-backup
"""

api_endpoint = "/databases/postgresql/instances/{database_id}/backups/{id}"


@dataclass
class MySQLDatabaseConfigMySQLOptions(JSONObject):
"""
Expand Down Expand Up @@ -296,15 +230,13 @@ class MySQLDatabase(Base):
"id": Property(identifier=True),
"label": Property(mutable=True),
"allow_list": Property(mutable=True, unordered=True),
"backups": Property(derived_class=MySQLDatabaseBackup),
"cluster_size": Property(mutable=True),
"created": Property(is_datetime=True),
"encrypted": Property(),
"engine": Property(),
"hosts": Property(),
"port": Property(),
"region": Property(),
"replication_type": Property(),
"ssl_connection": Property(),
"status": Property(volatile=True),
"type": Property(mutable=True),
Expand Down Expand Up @@ -393,28 +325,6 @@ def patch(self):
"{}/patch".format(MySQLDatabase.api_endpoint), model=self
)

@deprecated(
reason="Backups are not supported for non-legacy database clusters."
)
def backup_create(self, label, **kwargs):
"""
Creates a snapshot backup of a Managed MySQL Database.

API Documentation: https://techdocs.akamai.com/linode-api/reference/post-databases-mysql-instance-backup
"""

params = {
"label": label,
}
params.update(kwargs)

self._client.post(
"{}/backups".format(MySQLDatabase.api_endpoint),
model=self,
data=params,
)
self.invalidate()

def invalidate(self):
"""
Clear out cached properties.
Expand Down Expand Up @@ -464,16 +374,13 @@ class PostgreSQLDatabase(Base):
"id": Property(identifier=True),
"label": Property(mutable=True),
"allow_list": Property(mutable=True, unordered=True),
"backups": Property(derived_class=PostgreSQLDatabaseBackup),
"cluster_size": Property(mutable=True),
"created": Property(is_datetime=True),
"encrypted": Property(),
"engine": Property(),
"hosts": Property(),
"port": Property(),
"region": Property(),
"replication_commit_type": Property(),
"replication_type": Property(),
"ssl_connection": Property(),
"status": Property(volatile=True),
"type": Property(mutable=True),
Expand Down Expand Up @@ -563,28 +470,6 @@ def patch(self):
"{}/patch".format(PostgreSQLDatabase.api_endpoint), model=self
)

@deprecated(
reason="Backups are not supported for non-legacy database clusters."
)
def backup_create(self, label, **kwargs):
"""
Creates a snapshot backup of a Managed PostgreSQL Database.

API Documentation: https://techdocs.akamai.com/linode-api/reference/post-databases-postgre-sql-instance-backup
"""

params = {
"label": label,
}
params.update(kwargs)

self._client.post(
"{}/backups".format(PostgreSQLDatabase.api_endpoint),
model=self,
data=params,
)
self.invalidate()

def invalidate(self):
"""
Clear out cached properties.
Expand Down
17 changes: 15 additions & 2 deletions linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@
from linode_api4.objects.serializable import JSONObject, StrEnum
from linode_api4.objects.vpc import VPC, VPCSubnet
from linode_api4.paginated_list import PaginatedList
from linode_api4.util import drop_null_keys
from linode_api4.util import drop_null_keys, generate_device_suffixes

PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation
MIN_DEVICE_LIMIT = 8
MB_PER_GB = 1024
MAX_DEVICE_LIMIT = 64


class InstanceDiskEncryptionType(StrEnum):
Expand Down Expand Up @@ -1272,9 +1275,19 @@ def config_create(
from .volume import Volume # pylint: disable=import-outside-toplevel

hypervisor_prefix = "sd" if self.hypervisor == "kvm" else "xvd"

device_limit = int(
max(
MIN_DEVICE_LIMIT,
min(self.specs.memory // MB_PER_GB, MAX_DEVICE_LIMIT),
)
)

device_names = [
hypervisor_prefix + string.ascii_lowercase[i] for i in range(0, 8)
hypervisor_prefix + suffix
for suffix in generate_device_suffixes(device_limit)
]

device_map = {
device_names[i]: None for i in range(0, len(device_names))
}
Expand Down
12 changes: 7 additions & 5 deletions linode_api4/objects/nodebalancer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import os
from pathlib import Path
from urllib import parse

from linode_api4.common import Price, RegionPrice
Expand Down Expand Up @@ -220,12 +220,14 @@ def load_ssl_data(self, cert_file, key_file):

# we're disabling warnings here because these attributes are defined dynamically
# through linode.objects.Base, and pylint isn't privy
if os.path.isfile(os.path.expanduser(cert_file)):
with open(os.path.expanduser(cert_file)) as f:
cert_path = Path(cert_file).expanduser()
if cert_path.is_file():
with open(cert_path) as f:
self.ssl_cert = f.read()

if os.path.isfile(os.path.expanduser(key_file)):
with open(os.path.expanduser(key_file)) as f:
key_path = Path(key_file).expanduser()
if key_path.is_file():
with open(key_path) as f:
self.ssl_key = f.read()


Expand Down
26 changes: 26 additions & 0 deletions linode_api4/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Contains various utility functions.
"""

import string
from typing import Any, Dict


Expand All @@ -27,3 +28,28 @@ def recursive_helper(value: Any) -> Any:
return value

return recursive_helper(data)


def generate_device_suffixes(n: int) -> list[str]:
"""
Generate n alphabetical suffixes starting with a, b, c, etc.
After z, continue with aa, ab, ac, etc. followed by aaa, aab, etc.
Example:
generate_device_suffixes(30) ->
['a', 'b', 'c', ..., 'z', 'aa', 'ab', 'ac', 'ad']
"""
letters = string.ascii_lowercase
result = []
i = 0

while len(result) < n:
s = ""
x = i
while True:
s = letters[x % 26] + s
x = x // 26 - 1
if x < 0:
break
result.append(s)
i += 1
return result
13 changes: 0 additions & 13 deletions test/fixtures/databases_mysql_instances_123_backups.json

This file was deleted.

This file was deleted.

13 changes: 0 additions & 13 deletions test/fixtures/databases_postgresql_instances_123_backups.json

This file was deleted.

This file was deleted.

Loading
Loading