Skip to content

Commit 0c039d8

Browse files
new: Add support for Placement Groups (#396)
1 parent a25850f commit 0c039d8

File tree

20 files changed

+687
-16
lines changed

20 files changed

+687
-16
lines changed

docs/linode_api4/linode_client.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,15 @@ with buckets and objects, use the s3 API directly with a library like `boto3`_.
155155

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

158+
PlacementAPIGroup
159+
^^^^^^^^^^^^
160+
161+
Includes methods related to VM placement.
162+
163+
.. autoclass:: linode_api4.linode_client.PlacementAPIGroup
164+
:members:
165+
:special-members:
166+
158167
PollingGroup
159168
^^^^^^^^^^^^
160169

docs/linode_api4/objects/models.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ Object Storage Models
104104
:undoc-members:
105105
:inherited-members:
106106

107+
Placement Models
108+
--------------
109+
110+
.. automodule:: linode_api4.objects.placement
111+
:members:
112+
:exclude-members: api_endpoint, properties, derived_url_path, id_attribute, parent_id_name
113+
:undoc-members:
114+
:inherited-members:
115+
107116
Profile Models
108117
--------------
109118

linode_api4/groups/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .networking import *
1313
from .nodebalancer import *
1414
from .object_storage import *
15+
from .placement import *
1516
from .polling import *
1617
from .profile import *
1718
from .region import *

linode_api4/groups/linode.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
from collections.abc import Iterable
44

5-
from linode_api4 import Profile
5+
from linode_api4 import InstancePlacementGroupAssignment, Profile
66
from linode_api4.common import SSH_KEY_TYPES, load_and_validate_keys
77
from linode_api4.errors import UnexpectedResponseError
88
from linode_api4.groups import Group
@@ -20,6 +20,7 @@
2020
Type,
2121
)
2222
from linode_api4.objects.filtering import Filter
23+
from linode_api4.objects.linode import _expand_placement_group_assignment
2324
from linode_api4.paginated_list import PaginatedList
2425

2526

@@ -269,6 +270,8 @@ def instance_create(
269270
:param interfaces: An array of Network Interfaces to add to this Linode’s Configuration Profile.
270271
At least one and up to three Interface objects can exist in this array.
271272
:type interfaces: list[ConfigInterface] or list[dict[str, Any]]
273+
:param placement_group: A Placement Group to create this Linode under.
274+
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]
272275
273276
:returns: A new Instance object, or a tuple containing the new Instance and
274277
the generated password.
@@ -315,6 +318,11 @@ def instance_create(
315318
for i in interfaces
316319
]
317320

321+
if "placement_group" in kwargs:
322+
kwargs["placement_group"] = _expand_placement_group_assignment(
323+
kwargs.get("placement_group")
324+
)
325+
318326
params = {
319327
"type": ltype.id if issubclass(type(ltype), Base) else ltype,
320328
"region": region.id if issubclass(type(region), Base) else region,

linode_api4/groups/placement.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from typing import Union
2+
3+
from linode_api4.errors import UnexpectedResponseError
4+
from linode_api4.groups import Group
5+
from linode_api4.objects.placement import PlacementGroup
6+
from linode_api4.objects.region import Region
7+
8+
9+
class PlacementAPIGroup(Group):
10+
def groups(self, *filters):
11+
"""
12+
Returns a list of Placement Groups on your account. You may filter
13+
this query to return only Placement Groups that match specific criteria::
14+
15+
groups = client.placement.groups(PlacementGroup.label == "test")
16+
17+
API Documentation: TODO
18+
19+
:param filters: Any number of filters to apply to this query.
20+
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
21+
for more details on filtering.
22+
23+
:returns: A list of Placement Groups that matched the query.
24+
:rtype: PaginatedList of PlacementGroup
25+
"""
26+
return self.client._get_and_filter(PlacementGroup, *filters)
27+
28+
def group_create(
29+
self,
30+
label: str,
31+
region: Union[Region, str],
32+
affinity_type: str,
33+
is_strict: bool = False,
34+
**kwargs,
35+
) -> PlacementGroup:
36+
"""
37+
Create a placement group with the specified parameters.
38+
39+
:param label: The label for the placement group.
40+
:type label: str
41+
:param region: The region where the placement group will be created. Can be either a Region object or a string representing the region ID.
42+
:type region: Union[Region, str]
43+
:param affinity_type: The affinity type of the placement group.
44+
:type affinity_type: PlacementGroupAffinityType
45+
:param is_strict: Whether the placement group is strict (defaults to False).
46+
:type is_strict: bool
47+
48+
:returns: The new Placement Group.
49+
:rtype: PlacementGroup
50+
"""
51+
params = {
52+
"label": label,
53+
"region": region.id if isinstance(region, Region) else region,
54+
"affinity_type": affinity_type,
55+
"is_strict": is_strict,
56+
}
57+
58+
params.update(kwargs)
59+
60+
result = self.client.post("/placement/groups", data=params)
61+
62+
if not "id" in result:
63+
raise UnexpectedResponseError(
64+
"Unexpected response when creating Placement Group", json=result
65+
)
66+
67+
d = PlacementGroup(self.client, result["id"], result)
68+
return d

linode_api4/linode_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from linode_api4.objects.filtering import Filter
3535

3636
from .common import SSH_KEY_TYPES, load_and_validate_keys
37+
from .groups.placement import PlacementAPIGroup
3738
from .paginated_list import PaginatedList
3839
from .util import drop_null_keys
3940

@@ -200,6 +201,9 @@ def __init__(
200201
#: Access methods related to Beta Program - See :any:`BetaProgramGroup` for more information.
201202
self.beta = BetaProgramGroup(self)
202203

204+
#: Access methods related to VM placement - See :any:`PlacementAPIGroup` for more information.
205+
self.placement = PlacementAPIGroup(self)
206+
203207
@property
204208
def _user_agent(self):
205209
return "{}python-linode_api4/{} {}".format(

linode_api4/objects/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
from .database import *
2121
from .vpc import *
2222
from .beta import *
23+
from .placement import *

linode_api4/objects/linode.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,18 @@ def subnet(self) -> VPCSubnet:
335335
return VPCSubnet(self._client, self.subnet_id, self.vpc_id)
336336

337337

338+
@dataclass
339+
class InstancePlacementGroupAssignment(JSONObject):
340+
"""
341+
Represents an assignment between an instance and a Placement Group.
342+
This is intended to be used when creating, cloning, and migrating
343+
instances.
344+
"""
345+
346+
id: int
347+
compliant_only: bool = False
348+
349+
338350
@dataclass
339351
class ConfigInterface(JSONObject):
340352
"""
@@ -870,6 +882,37 @@ def transfer(self):
870882

871883
return self._transfer
872884

885+
@property
886+
def placement_group(self) -> Optional["PlacementGroup"]:
887+
"""
888+
Returns the PlacementGroup object for the Instance.
889+
890+
:returns: The Placement Group this instance is under.
891+
:rtype: Optional[PlacementGroup]
892+
"""
893+
# Workaround to avoid circular import
894+
from linode_api4.objects.placement import ( # pylint: disable=import-outside-toplevel
895+
PlacementGroup,
896+
)
897+
898+
if not hasattr(self, "_placement_group"):
899+
# Refresh the instance if necessary
900+
if not self._populated:
901+
self._api_get()
902+
903+
pg_data = self._raw_json.get("placement_group", None)
904+
905+
if pg_data is None:
906+
return None
907+
908+
setattr(
909+
self,
910+
"_placement_group",
911+
PlacementGroup(self._client, pg_data.get("id"), json=pg_data),
912+
)
913+
914+
return self._placement_group
915+
873916
def _populate(self, json):
874917
if json is not None:
875918
# fixes ipv4 and ipv6 attribute of json to make base._populate work
@@ -885,11 +928,16 @@ def invalidate(self):
885928
"""Clear out cached properties"""
886929
if hasattr(self, "_avail_backups"):
887930
del self._avail_backups
931+
888932
if hasattr(self, "_ips"):
889933
del self._ips
934+
890935
if hasattr(self, "_transfer"):
891936
del self._transfer
892937

938+
if hasattr(self, "_placement_group"):
939+
del self._placement_group
940+
893941
Base.invalidate(self)
894942

895943
def boot(self, config=None):
@@ -1471,6 +1519,9 @@ def initiate_migration(
14711519
region=None,
14721520
upgrade=None,
14731521
migration_type: MigrationType = MigrationType.COLD,
1522+
placement_group: Union[
1523+
InstancePlacementGroupAssignment, Dict[str, Any], int
1524+
] = None,
14741525
):
14751526
"""
14761527
Initiates a pending migration that is already scheduled for this Linode
@@ -1496,12 +1547,19 @@ def initiate_migration(
14961547
:param migration_type: The type of migration that will be used for this Linode migration.
14971548
Customers can only use this param when activating a support-created migration.
14981549
Customers can choose between a cold and warm migration, cold is the default type.
1499-
:type: mirgation_type: str
1550+
:type: migration_type: str
1551+
1552+
:param placement_group: Information about the placement group to create this instance under.
1553+
:type placement_group: Union[InstancePlacementGroupAssignment, Dict[str, Any], int]
15001554
"""
1555+
15011556
params = {
15021557
"region": region.id if issubclass(type(region), Base) else region,
15031558
"upgrade": upgrade,
15041559
"type": migration_type,
1560+
"placement_group": _expand_placement_group_assignment(
1561+
placement_group
1562+
),
15051563
}
15061564

15071565
util.drop_null_keys(params)
@@ -1583,6 +1641,12 @@ def clone(
15831641
label=None,
15841642
group=None,
15851643
with_backups=None,
1644+
placement_group: Union[
1645+
InstancePlacementGroupAssignment,
1646+
"PlacementGroup",
1647+
Dict[str, Any],
1648+
int,
1649+
] = None,
15861650
):
15871651
"""
15881652
Clones this linode into a new linode or into a new linode in the given region
@@ -1618,6 +1682,9 @@ def clone(
16181682
enrolled in the Linode Backup service. This will incur an additional charge.
16191683
:type: with_backups: bool
16201684
1685+
:param placement_group: Information about the placement group to create this instance under.
1686+
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]
1687+
16211688
:returns: The cloned Instance.
16221689
:rtype: Instance
16231690
"""
@@ -1654,8 +1721,13 @@ def clone(
16541721
"label": label,
16551722
"group": group,
16561723
"with_backups": with_backups,
1724+
"placement_group": _expand_placement_group_assignment(
1725+
placement_group
1726+
),
16571727
}
16581728

1729+
util.drop_null_keys(params)
1730+
16591731
result = self._client.post(
16601732
"{}/clone".format(Instance.api_endpoint), model=self, data=params
16611733
)
@@ -1790,3 +1862,40 @@ def _serialize(self):
17901862
dct = Base._serialize(self)
17911863
dct["images"] = [d.id for d in self.images]
17921864
return dct
1865+
1866+
1867+
def _expand_placement_group_assignment(
1868+
pg: Union[
1869+
InstancePlacementGroupAssignment, "PlacementGroup", Dict[str, Any], int
1870+
]
1871+
) -> Optional[Dict[str, Any]]:
1872+
"""
1873+
Expands the placement group argument into a dict for use in an API request body.
1874+
1875+
:param pg: The placement group argument to be expanded.
1876+
:type pg: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]
1877+
1878+
:returns: The expanded placement group.
1879+
:rtype: Optional[Dict[str, Any]]
1880+
"""
1881+
# Workaround to avoid circular import
1882+
from linode_api4.objects.placement import ( # pylint: disable=import-outside-toplevel
1883+
PlacementGroup,
1884+
)
1885+
1886+
if pg is None:
1887+
return None
1888+
1889+
if isinstance(pg, dict):
1890+
return pg
1891+
1892+
if isinstance(pg, InstancePlacementGroupAssignment):
1893+
return pg.dict
1894+
1895+
if isinstance(pg, PlacementGroup):
1896+
return {"id": pg.id}
1897+
1898+
if isinstance(pg, int):
1899+
return {"id": pg}
1900+
1901+
raise TypeError(f"Invalid type for Placement Group: {type(pg)}")

0 commit comments

Comments
 (0)