Skip to content

Commit 04d5132

Browse files
Allow dict passthrough for config_create 'devices' field
1 parent c9e18a5 commit 04d5132

File tree

2 files changed

+93
-46
lines changed

2 files changed

+93
-46
lines changed

linode_api4/objects/linode.py

Lines changed: 65 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,20 @@ 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, or a raw device mapping dict to pass directly to the
1274+
API. List values must be of type Disk or Volume. If this is given,
1275+
you may not include disks or volumes.
12701276
:param **kwargs: Any other arguments accepted by the api.
12711277
12721278
:returns: A new Linode Config
12731279
"""
12741280
# needed here to avoid circular imports
12751281
from .volume import Volume # pylint: disable=import-outside-toplevel
12761282

1283+
disks = [] if disks is None else disks
1284+
volumes = [] if volumes is None else volumes
1285+
interfaces = [] if interfaces is None else interfaces
1286+
12771287
hypervisor_prefix = "sd" if self.hypervisor == "kvm" else "xvd"
12781288

12791289
device_limit = int(
@@ -1288,52 +1298,58 @@ def config_create(
12881298
for suffix in generate_device_suffixes(device_limit)
12891299
]
12901300

1291-
device_map = {
1292-
device_names[i]: None for i in range(0, len(device_names))
1293-
}
1301+
def _flatten_device(device: Any):
1302+
if device is None:
1303+
return None
1304+
elif isinstance(device, Disk):
1305+
return {"disk_id": device.id}
1306+
elif isinstance(device, Volume):
1307+
return {"volume_id": device.id}
1308+
else:
1309+
raise TypeError("Disk or Volume expected!")
1310+
1311+
def _build_devices():
1312+
# Devices is a dict, pass through
1313+
if isinstance(devices, dict):
1314+
return devices
1315+
1316+
if devices is not None:
1317+
device_list = (
1318+
devices if isinstance(devices, list) else [devices]
1319+
)
1320+
1321+
return {
1322+
device_names[i]: _flatten_device(device_list[i])
1323+
for i in range(len(devices))
1324+
}
1325+
1326+
# Devices were given through named args
1327+
return [
1328+
(
1329+
{"volume_id": volume.id}
1330+
if isinstance(volume, int)
1331+
else _flatten_device(volume)
1332+
)
1333+
for volume in volumes
1334+
] + [
1335+
(
1336+
{"disk_id": disk.id}
1337+
if isinstance(disk, int)
1338+
else _flatten_device(disk)
1339+
)
1340+
for disk in disks
1341+
]
12941342

12951343
if devices and (disks or volumes):
12961344
raise ValueError(
12971345
'You may not call config_create with "devices" and '
12981346
'either of "disks" or "volumes" specified!'
12991347
)
13001348

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!")
1349+
device_map = _build_devices()
13271350

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!")
1351+
if len(device_map) < 1:
1352+
raise ValueError("Must include at least one disk or volume!")
13371353

13381354
param_interfaces = []
13391355
for interface in interfaces:
@@ -1845,8 +1861,8 @@ def clone(
18451861
to_linode=None,
18461862
region=None,
18471863
instance_type=None,
1848-
configs=[],
1849-
disks=[],
1864+
configs=None,
1865+
disks=None,
18501866
label=None,
18511867
group=None,
18521868
with_backups=None,
@@ -1902,6 +1918,9 @@ def clone(
19021918
'You may only specify one of "to_linode" and "region"'
19031919
)
19041920

1921+
configs = [] if configs is None else configs
1922+
disks = [] if disks is None else disks
1923+
19051924
if region and not type:
19061925
raise ValueError('Specifying a region requires a "service" as well')
19071926

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)