Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions docs/linode_api4/linode_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ with buckets and objects, use the s3 API directly with a library like `boto3`_.

.. _boto3: https://github.com/boto/boto3

PlacementAPIGroup
^^^^^^^^^^^^

Includes methods related to VM placement.

.. autoclass:: linode_api4.linode_client.PlacementAPIGroup
:members:
:special-members:

PollingGroup
^^^^^^^^^^^^

Expand Down
9 changes: 9 additions & 0 deletions docs/linode_api4/objects/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ Object Storage Models
:undoc-members:
:inherited-members:

Placement Models
--------------

.. automodule:: linode_api4.objects.placement
:members:
:exclude-members: api_endpoint, properties, derived_url_path, id_attribute, parent_id_name
:undoc-members:
:inherited-members:

Profile Models
--------------

Expand Down
1 change: 1 addition & 0 deletions linode_api4/groups/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .networking import *
from .nodebalancer import *
from .object_storage import *
from .placement import *
from .polling import *
from .profile import *
from .region import *
Expand Down
8 changes: 8 additions & 0 deletions linode_api4/groups/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Type,
)
from linode_api4.objects.filtering import Filter
from linode_api4.objects.linode import _expand_placement_group_assignment
from linode_api4.paginated_list import PaginatedList


Expand Down Expand Up @@ -265,6 +266,8 @@ def instance_create(
:param interfaces: An array of Network Interfaces to add to this Linode鈥檚 Configuration Profile.
At least one and up to three Interface objects can exist in this array.
:type interfaces: list[ConfigInterface] or list[dict[str, Any]]
:param placement_group: A Placement Group to create this Linode under.
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]

:returns: A new Instance object, or a tuple containing the new Instance and
the generated password.
Expand Down Expand Up @@ -311,6 +314,11 @@ def instance_create(
for i in interfaces
]

if "placement_group" in kwargs:
kwargs["placement_group"] = _expand_placement_group_assignment(
kwargs.get("placement_group")
)

params = {
"type": ltype.id if issubclass(type(ltype), Base) else ltype,
"region": region.id if issubclass(type(region), Base) else region,
Expand Down
72 changes: 72 additions & 0 deletions linode_api4/groups/placement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from typing import Union

from linode_api4.errors import UnexpectedResponseError
from linode_api4.groups import Group
from linode_api4.objects.placement import PlacementGroup
from linode_api4.objects.region import Region


class PlacementAPIGroup(Group):
def groups(self, *filters):
"""
NOTE: Placement Groups may not currently be available to all users.

Returns a list of Placement Groups on your account. You may filter
this query to return only Placement Groups that match specific criteria::

groups = client.placement.groups(PlacementGroup.label == "test")

API Documentation: TODO

:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.

:returns: A list of Placement Groups that matched the query.
:rtype: PaginatedList of PlacementGroup
"""
return self.client._get_and_filter(PlacementGroup, *filters)

def group_create(
self,
label: str,
region: Union[Region, str],
affinity_type: str,
is_strict: bool = False,
**kwargs,
) -> PlacementGroup:
"""
NOTE: Placement Groups may not currently be available to all users.

Create a placement group with the specified parameters.

:param label: The label for the placement group.
:type label: str
:param region: The region where the placement group will be created. Can be either a Region object or a string representing the region ID.
:type region: Union[Region, str]
:param affinity_type: The affinity type of the placement group.
:type affinity_type: PlacementGroupAffinityType
:param is_strict: Whether the placement group is strict (defaults to False).
:type is_strict: bool

:returns: The new Placement Group.
:rtype: PlacementGroup
"""
params = {
"label": label,
"region": region.id if isinstance(region, Region) else region,
"affinity_type": affinity_type,
"is_strict": is_strict,
}

params.update(kwargs)

result = self.client.post("/placement/groups", data=params)

if not "id" in result:
raise UnexpectedResponseError(
"Unexpected response when creating Placement Group", json=result
)

d = PlacementGroup(self.client, result["id"], result)
return d
4 changes: 4 additions & 0 deletions linode_api4/linode_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from linode_api4.objects import Image, and_

from .groups.placement import PlacementAPIGroup
from .paginated_list import PaginatedList

package_version = version("linode_api4")
Expand Down Expand Up @@ -197,6 +198,9 @@ def __init__(
#: Access methods related to Beta Program - See :any:`BetaProgramGroup` for more information.
self.beta = BetaProgramGroup(self)

#: Access methods related to VM placement - See :any:`PlacementAPIGroup` for more information.
self.placement = PlacementAPIGroup(self)

@property
def _user_agent(self):
return "{}python-linode_api4/{} {}".format(
Expand Down
1 change: 1 addition & 0 deletions linode_api4/objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
from .database import *
from .vpc import *
from .beta import *
from .placement import *
111 changes: 110 additions & 1 deletion linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,18 @@ def subnet(self) -> VPCSubnet:
return VPCSubnet(self._client, self.subnet_id, self.vpc_id)


@dataclass
class InstancePlacementGroupAssignment(JSONObject):
"""
Represents an assignment between an instance and a Placement Group.
This is intended to be used when creating, cloning, and migrating
instances.
"""

id: int
compliant_only: bool = False


@dataclass
class ConfigInterface(JSONObject):
"""
Expand Down Expand Up @@ -870,6 +882,37 @@ def transfer(self):

return self._transfer

@property
def placement_group(self) -> Optional["PlacementGroup"]:
"""
Returns the PlacementGroup object for the Instance.

:returns: The Placement Group this instance is under.
:rtype: Optional[PlacementGroup]
"""
# Workaround to avoid circular import
from linode_api4.objects.placement import ( # pylint: disable=import-outside-toplevel
PlacementGroup,
)

if not hasattr(self, "_placement_group"):
# Refresh the instance if necessary
if not self._populated:
self._api_get()

pg_data = self._raw_json.get("placement_group", None)

if pg_data is None:
return None

setattr(
self,
"_placement_group",
PlacementGroup(self._client, pg_data.get("id"), json=pg_data),
)

return self._placement_group

def _populate(self, json):
if json is not None:
# fixes ipv4 and ipv6 attribute of json to make base._populate work
Expand All @@ -885,11 +928,16 @@ def invalidate(self):
"""Clear out cached properties"""
if hasattr(self, "_avail_backups"):
del self._avail_backups

if hasattr(self, "_ips"):
del self._ips

if hasattr(self, "_transfer"):
del self._transfer

if hasattr(self, "_placement_group"):
del self._placement_group

Base.invalidate(self)

def boot(self, config=None):
Expand Down Expand Up @@ -1471,6 +1519,9 @@ def initiate_migration(
region=None,
upgrade=None,
migration_type: MigrationType = MigrationType.COLD,
placement_group: Union[
InstancePlacementGroupAssignment, Dict[str, Any], int
] = None,
):
"""
Initiates a pending migration that is already scheduled for this Linode
Expand All @@ -1496,12 +1547,19 @@ def initiate_migration(
:param migration_type: The type of migration that will be used for this Linode migration.
Customers can only use this param when activating a support-created migration.
Customers can choose between a cold and warm migration, cold is the default type.
:type: mirgation_type: str
:type: migration_type: str

:param placement_group: Information about the placement group to create this instance under.
:type placement_group: Union[InstancePlacementGroupAssignment, Dict[str, Any], int]
"""

params = {
"region": region.id if issubclass(type(region), Base) else region,
"upgrade": upgrade,
"type": migration_type,
"placement_group": _expand_placement_group_assignment(
placement_group
),
}

util.drop_null_keys(params)
Expand Down Expand Up @@ -1583,6 +1641,12 @@ def clone(
label=None,
group=None,
with_backups=None,
placement_group: Union[
InstancePlacementGroupAssignment,
"PlacementGroup",
Dict[str, Any],
int,
] = None,
):
"""
Clones this linode into a new linode or into a new linode in the given region
Expand Down Expand Up @@ -1618,6 +1682,9 @@ def clone(
enrolled in the Linode Backup service. This will incur an additional charge.
:type: with_backups: bool

:param placement_group: Information about the placement group to create this instance under.
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]

:returns: The cloned Instance.
:rtype: Instance
"""
Expand Down Expand Up @@ -1654,8 +1721,13 @@ def clone(
"label": label,
"group": group,
"with_backups": with_backups,
"placement_group": _expand_placement_group_assignment(
placement_group
),
}

util.drop_null_keys(params)

result = self._client.post(
"{}/clone".format(Instance.api_endpoint), model=self, data=params
)
Expand Down Expand Up @@ -1790,3 +1862,40 @@ def _serialize(self):
dct = Base._serialize(self)
dct["images"] = [d.id for d in self.images]
return dct


def _expand_placement_group_assignment(
pg: Union[
InstancePlacementGroupAssignment, "PlacementGroup", Dict[str, Any], int
]
) -> Optional[Dict[str, Any]]:
"""
Expands the placement group argument into a dict for use in an API request body.

:param pg: The placement group argument to be expanded.
:type pg: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]

:returns: The expanded placement group.
:rtype: Optional[Dict[str, Any]]
"""
# Workaround to avoid circular import
from linode_api4.objects.placement import ( # pylint: disable=import-outside-toplevel
PlacementGroup,
)

if pg is None:
return None

if isinstance(pg, dict):
return pg

if isinstance(pg, InstancePlacementGroupAssignment):
return pg.dict

if isinstance(pg, PlacementGroup):
return {"id": pg.id}

if isinstance(pg, int):
return {"id": pg}

raise TypeError(f"Invalid type for Placement Group: {type(pg)}")
Loading