Skip to content

Commit 61d81e5

Browse files
Allow dict passthrough for config_create 'devices' field
1 parent c9e18a5 commit 61d81e5

2 files changed

Lines changed: 116 additions & 46 deletions

File tree

linode_api4/objects/linode.py

Lines changed: 88 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,15 +1243,20 @@ def _func(value):
12431243

12441244
return password
12451245

1246+
# Type aliases for config_create parameters
1247+
ConfigCreateDevice = Union["Disk", "Volume", Dict[str, Any]]
1248+
ConfigCreateDisk = Union["Disk", int]
1249+
ConfigCreateVolume = Union["Volume", int]
1250+
12461251
# create derived objects
12471252
def config_create(
12481253
self,
12491254
kernel=None,
12501255
label=None,
1251-
devices=[],
1252-
disks=[],
1253-
volumes=[],
1254-
interfaces=[],
1256+
devices=None,
1257+
disks=None,
1258+
volumes=None,
1259+
interfaces=None,
12551260
**kwargs,
12561261
):
12571262
"""
@@ -1265,15 +1270,19 @@ def config_create(
12651270
:param volumes: The volumes, starting after the last disk, to map to this
12661271
config
12671272
:param devices: A list of devices to assign to this config, in device
1268-
index order. Values must be of type Disk or Volume. If this is
1269-
given, you may not include disks or volumes.
1273+
index order, a raw device mapping dict to pass directly to the API
1274+
(e.g. ``{"sda": {"disk_id": 123}, "sdb": Volume(...)}``), or
1275+
a single Disk or Volume.
1276+
If this is given, you may not include disks or volumes.
12701277
:param **kwargs: Any other arguments accepted by the api.
12711278
12721279
:returns: A new Linode Config
12731280
"""
12741281
# needed here to avoid circular imports
12751282
from .volume import Volume # pylint: disable=import-outside-toplevel
12761283

1284+
interfaces = [] if interfaces is None else interfaces
1285+
12771286
hypervisor_prefix = "sd" if self.hypervisor == "kvm" else "xvd"
12781287

12791288
device_limit = int(
@@ -1288,52 +1297,82 @@ def config_create(
12881297
for suffix in generate_device_suffixes(device_limit)
12891298
]
12901299

1291-
device_map = {
1292-
device_names[i]: None for i in range(0, len(device_names))
1293-
}
1300+
def _flatten_device(device: Optional[Union[Disk, Volume]]):
1301+
if device is None:
1302+
return None
1303+
elif isinstance(device, Disk):
1304+
return {"disk_id": device.id}
1305+
elif isinstance(device, Volume):
1306+
return {"volume_id": device.id}
1307+
1308+
raise TypeError("Disk or Volume expected!")
1309+
1310+
def _device_entry(device: Union[Disk, Volume, int], key: str):
1311+
return (
1312+
{key: device if isinstance(device, int) else device.id}
1313+
if isinstance(device, int)
1314+
else _flatten_device(device)
1315+
)
1316+
1317+
def _build_devices():
1318+
# Devices is a dict, pass through
1319+
if isinstance(devices, dict):
1320+
return {
1321+
k: (
1322+
_flatten_device(v)
1323+
if isinstance(v, (Disk, Volume))
1324+
else v
1325+
)
1326+
for k, v in devices.items()
1327+
}
1328+
1329+
device_list = []
1330+
1331+
# "Devices" was provided
1332+
if devices:
1333+
devices_norm = (
1334+
devices if isinstance(devices, (list, tuple)) else [devices]
1335+
)
1336+
1337+
device_list += [
1338+
_flatten_device(device) for device in devices_norm
1339+
]
12941340

1341+
if disks:
1342+
disks_norm = (
1343+
disks if isinstance(disks, (list, tuple)) else [disks]
1344+
)
1345+
1346+
device_list += [
1347+
_device_entry(disk, "disk_id") for disk in disks_norm
1348+
]
1349+
1350+
if volumes:
1351+
volumes_norm = (
1352+
volumes if isinstance(volumes, (list, tuple)) else [volumes]
1353+
)
1354+
1355+
device_list += [
1356+
_device_entry(volume, "volume_id")
1357+
for volume in volumes_norm
1358+
]
1359+
1360+
return {
1361+
device_names[i]: device for i, device in enumerate(device_list)
1362+
}
1363+
1364+
# This validation is enforced for backwards compatibility but isn't
1365+
# technically needed anymore
12951366
if devices and (disks or volumes):
12961367
raise ValueError(
12971368
'You may not call config_create with "devices" and '
12981369
'either of "disks" or "volumes" specified!'
12991370
)
13001371

1301-
if not devices:
1302-
if not isinstance(disks, list):
1303-
disks = [disks]
1304-
if not isinstance(volumes, list):
1305-
volumes = [volumes]
1306-
1307-
devices = []
1308-
1309-
for d in disks:
1310-
if d is None:
1311-
devices.append(None)
1312-
elif isinstance(d, Disk):
1313-
devices.append(d)
1314-
else:
1315-
devices.append(Disk(self._client, int(d), self.id))
1316-
1317-
for v in volumes:
1318-
if v is None:
1319-
devices.append(None)
1320-
elif isinstance(v, Volume):
1321-
devices.append(v)
1322-
else:
1323-
devices.append(Volume(self._client, int(v)))
1324-
1325-
if not devices:
1326-
raise ValueError("Must include at least one disk or volume!")
1372+
device_map = _build_devices()
13271373

1328-
for i, d in enumerate(devices):
1329-
if d is None:
1330-
pass
1331-
elif isinstance(d, Disk):
1332-
device_map[device_names[i]] = {"disk_id": d.id}
1333-
elif isinstance(d, Volume):
1334-
device_map[device_names[i]] = {"volume_id": d.id}
1335-
else:
1336-
raise TypeError("Disk or Volume expected!")
1374+
if len(device_map) < 1:
1375+
raise ValueError("Must include at least one disk or volume!")
13371376

13381377
param_interfaces = []
13391378
for interface in interfaces:
@@ -1845,8 +1884,8 @@ def clone(
18451884
to_linode=None,
18461885
region=None,
18471886
instance_type=None,
1848-
configs=[],
1849-
disks=[],
1887+
configs=None,
1888+
disks=None,
18501889
label=None,
18511890
group=None,
18521891
with_backups=None,
@@ -1902,6 +1941,9 @@ def clone(
19021941
'You may only specify one of "to_linode" and "region"'
19031942
)
19041943

1944+
configs = [] if configs is None else configs
1945+
disks = [] if disks is None else disks
1946+
19051947
if region and not type:
19061948
raise ValueError('Specifying a region requires a "service" as well')
19071949

test/unit/objects/linode_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,34 @@ def test_create_disk(self):
459459
assert disk.id == 12345
460460
assert disk.disk_encryption == InstanceDiskEncryptionType.disabled
461461

462+
def test_create_config_with_device_map(self):
463+
"""
464+
Tests that config_create passes through a raw device map unchanged.
465+
"""
466+
linode = Instance(self.client, 123)
467+
devices = {
468+
"sda": {"disk_id": 111},
469+
"sdb": {"volume_id": 222},
470+
"sdc": None,
471+
}
472+
473+
with self.mock_post(
474+
{"id": 456, "devices": devices, "interfaces": []}
475+
) as m:
476+
config = linode.config_create(label="test-config", devices=devices)
477+
478+
self.assertEqual(m.call_url, "/linode/instances/123/configs")
479+
self.assertEqual(
480+
m.call_data,
481+
{
482+
"label": "test-config",
483+
"devices": devices,
484+
"interfaces": [],
485+
},
486+
)
487+
488+
self.assertEqual(config.id, 456)
489+
462490
def test_get_placement_group(self):
463491
"""
464492
Tests that you can get the placement group for a Linode

0 commit comments

Comments
 (0)