1+ from __future__ import annotations
2+
13import copy
24import string
35import sys
4042from linode_api4 .objects .serializable import JSONObject , StrEnum
4143from linode_api4 .objects .vpc import VPC , VPCSubnet
4244from linode_api4 .paginated_list import PaginatedList
43- from linode_api4 .util import drop_null_keys , generate_device_suffixes
45+ from linode_api4 .util import (
46+ drop_null_keys ,
47+ generate_device_suffixes ,
48+ normalize_as_list ,
49+ )
4450
4551PASSWORD_CHARS = string .ascii_letters + string .digits + string .punctuation
4652MIN_DEVICE_LIMIT = 8
@@ -1246,14 +1252,14 @@ def _func(value):
12461252 # create derived objects
12471253 def config_create (
12481254 self ,
1249- kernel = None ,
1250- label = None ,
1251- devices = [] ,
1252- disks = [] ,
1253- volumes = [] ,
1254- interfaces = [] ,
1255+ kernel : Kernel | str | None = None ,
1256+ label : str | None = None ,
1257+ devices : "Disk | Volume | dict[str, Any] | list[Disk | Volume | dict[str, Any]] | None" = None ,
1258+ disks : Disk | int | list [ Disk | int ] | None = None ,
1259+ volumes : "Volume | int | list[Volume | int] | None" = None ,
1260+ interfaces : list [ ConfigInterface | dict [ str , Any ]] | None = None ,
12551261 ** kwargs ,
1256- ):
1262+ ) -> Config :
12571263 """
12581264 Creates a Linode Config with the given attributes.
12591265
@@ -1263,17 +1269,22 @@ def config_create(
12631269 :param label: The config label
12641270 :param disks: The list of disks, starting at sda, to map to this config.
12651271 :param volumes: The volumes, starting after the last disk, to map to this
1266- config
1272+ config.
12671273 :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.
1274+ index order, a raw device mapping dict to pass directly to the API
1275+ (e.g. ``{"sda": {"disk_id": 123}, "sdb": Volume(...)}``), or
1276+ a single Disk or Volume.
1277+ If this is given, you may not include disks or volumes.
1278+ :param interfaces: A list of ConfigInterface objects or dicts to assign to this config.
12701279 :param **kwargs: Any other arguments accepted by the api.
12711280
12721281 :returns: A new Linode Config
12731282 """
12741283 # needed here to avoid circular imports
12751284 from .volume import Volume # pylint: disable=import-outside-toplevel
12761285
1286+ interfaces = [] if interfaces is None else interfaces
1287+
12771288 hypervisor_prefix = "sd" if self .hypervisor == "kvm" else "xvd"
12781289
12791290 device_limit = int (
@@ -1288,52 +1299,83 @@ def config_create(
12881299 for suffix in generate_device_suffixes (device_limit )
12891300 ]
12901301
1291- device_map = {
1292- device_names [i ]: None for i in range (0 , len (device_names ))
1293- }
1302+ def _flatten_device (device : Disk | Volume | dict | None ):
1303+ if device is None :
1304+ return None
1305+ elif isinstance (device , Disk ):
1306+ return {"disk_id" : device .id }
1307+ elif isinstance (device , Volume ):
1308+ return {"volume_id" : device .id }
1309+ elif isinstance (device , dict ):
1310+ return device
1311+
1312+ raise TypeError ("Disk, Volume, or dict expected!" )
1313+
1314+ def _device_entry (device : Disk | Volume | int , key : str ):
1315+ if isinstance (device , (Disk , Volume )):
1316+ return _flatten_device (device )
1317+
1318+ try :
1319+ device_id = int (device )
1320+ except (TypeError , ValueError ):
1321+ raise TypeError (
1322+ "Disk, Volume, or integer ID expected!"
1323+ ) from None
1324+
1325+ return {key : device_id }
1326+
1327+ def _build_devices ():
1328+ # Devices is a dict, flatten and pass through
1329+ if isinstance (devices , dict ):
1330+ return {
1331+ k : (
1332+ _flatten_device (v )
1333+ if isinstance (v , (Disk , Volume ))
1334+ else v
1335+ )
1336+ for k , v in devices .items ()
1337+ }
12941338
1339+ device_list = []
1340+
1341+ if devices :
1342+ device_list += [
1343+ _flatten_device (device )
1344+ for device in normalize_as_list (devices )
1345+ ]
1346+
1347+ if disks :
1348+ device_list += [
1349+ _device_entry (disk , "disk_id" ) if disk is not None else None
1350+ for disk in normalize_as_list (disks )
1351+ ]
1352+
1353+ if volumes :
1354+ device_list += [
1355+ (
1356+ _device_entry (volume , "volume_id" )
1357+ if volume is not None
1358+ else None
1359+ )
1360+ for volume in normalize_as_list (volumes )
1361+ ]
1362+
1363+ return {
1364+ device_names [i ]: device for i , device in enumerate (device_list )
1365+ }
1366+
1367+ # This validation is enforced for backwards compatibility but isn't
1368+ # technically needed anymore
12951369 if devices and (disks or volumes ):
12961370 raise ValueError (
12971371 'You may not call config_create with "devices" and '
12981372 'either of "disks" or "volumes" specified!'
12991373 )
13001374
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!" )
1375+ device_map = _build_devices ()
13271376
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!" )
1377+ if len (device_map ) < 1 :
1378+ raise ValueError ("Must include at least one disk or volume!" )
13371379
13381380 param_interfaces = []
13391381 for interface in interfaces :
@@ -1845,8 +1887,8 @@ def clone(
18451887 to_linode = None ,
18461888 region = None ,
18471889 instance_type = None ,
1848- configs = [] ,
1849- disks = [] ,
1890+ configs = None ,
1891+ disks = None ,
18501892 label = None ,
18511893 group = None ,
18521894 with_backups = None ,
@@ -1902,7 +1944,10 @@ def clone(
19021944 'You may only specify one of "to_linode" and "region"'
19031945 )
19041946
1905- if region and not type :
1947+ configs = [] if configs is None else configs
1948+ disks = [] if disks is None else disks
1949+
1950+ if region and not instance_type :
19061951 raise ValueError ('Specifying a region requires a "service" as well' )
19071952
19081953 if not isinstance (configs , list ) and not isinstance (
0 commit comments