Skip to content

Commit d4e8f9f

Browse files
Merge pull request #424 from linode/proj/vm-placement
project: VM Placement
2 parents 3168a17 + 92614c9 commit d4e8f9f

File tree

19 files changed

+672
-15
lines changed

19 files changed

+672
-15
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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
Type,
1717
)
1818
from linode_api4.objects.filtering import Filter
19+
from linode_api4.objects.linode import _expand_placement_group_assignment
1920
from linode_api4.paginated_list import PaginatedList
2021

2122

@@ -265,6 +266,8 @@ def instance_create(
265266
:param interfaces: An array of Network Interfaces to add to this Linode’s Configuration Profile.
266267
At least one and up to three Interface objects can exist in this array.
267268
:type interfaces: list[ConfigInterface] or list[dict[str, Any]]
269+
:param placement_group: A Placement Group to create this Linode under.
270+
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]
268271
269272
:returns: A new Instance object, or a tuple containing the new Instance and
270273
the generated password.
@@ -311,6 +314,11 @@ def instance_create(
311314
for i in interfaces
312315
]
313316

317+
if "placement_group" in kwargs:
318+
kwargs["placement_group"] = _expand_placement_group_assignment(
319+
kwargs.get("placement_group")
320+
)
321+
314322
params = {
315323
"type": ltype.id if issubclass(type(ltype), Base) else ltype,
316324
"region": region.id if issubclass(type(region), Base) else region,

linode_api4/groups/placement.py

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

linode_api4/linode_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
)
3333
from linode_api4.objects import Image, and_
3434

35+
from .groups.placement import PlacementAPIGroup
3536
from .paginated_list import PaginatedList
3637

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

201+
#: Access methods related to VM placement - See :any:`PlacementAPIGroup` for more information.
202+
self.placement = PlacementAPIGroup(self)
203+
200204
@property
201205
def _user_agent(self):
202206
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)