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,77 @@ 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 | 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+
1310+ raise TypeError ("Disk or Volume expected!" )
1311+
1312+ def _device_entry (device : Disk | Volume | int , key : str ):
1313+ if isinstance (device , (Disk , Volume )):
1314+ return _flatten_device (device )
1315+
1316+ try :
1317+ device_id = int (device )
1318+ except (TypeError , ValueError ):
1319+ raise TypeError (
1320+ "Disk, Volume, or integer ID expected!"
1321+ ) from None
1322+
1323+ return {key : device_id }
1324+
1325+ def _build_devices ():
1326+ # Devices is a dict, flatten and pass through
1327+ if isinstance (devices , dict ):
1328+ return {
1329+ k : (
1330+ _flatten_device (v )
1331+ if isinstance (v , (Disk , Volume ))
1332+ else v
1333+ )
1334+ for k , v in devices .items ()
1335+ }
1336+
1337+ device_list = []
12941338
1339+ if devices :
1340+ device_list += [
1341+ _flatten_device (device )
1342+ for device in normalize_as_list (devices )
1343+ ]
1344+
1345+ if disks :
1346+ device_list += [
1347+ _device_entry (disk , "disk_id" )
1348+ for disk in normalize_as_list (disks )
1349+ ]
1350+
1351+ if volumes :
1352+ device_list += [
1353+ _device_entry (volume , "volume_id" )
1354+ for volume in normalize_as_list (volumes )
1355+ ]
1356+
1357+ return {
1358+ device_names [i ]: device for i , device in enumerate (device_list )
1359+ }
1360+
1361+ # This validation is enforced for backwards compatibility but isn't
1362+ # technically needed anymore
12951363 if devices and (disks or volumes ):
12961364 raise ValueError (
12971365 'You may not call config_create with "devices" and '
12981366 'either of "disks" or "volumes" specified!'
12991367 )
13001368
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!" )
1369+ device_map = _build_devices ()
13271370
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!" )
1371+ if len (device_map ) < 1 :
1372+ raise ValueError ("Must include at least one disk or volume!" )
13371373
13381374 param_interfaces = []
13391375 for interface in interfaces :
@@ -1845,8 +1881,8 @@ def clone(
18451881 to_linode = None ,
18461882 region = None ,
18471883 instance_type = None ,
1848- configs = [] ,
1849- disks = [] ,
1884+ configs = None ,
1885+ disks = None ,
18501886 label = None ,
18511887 group = None ,
18521888 with_backups = None ,
@@ -1902,7 +1938,10 @@ def clone(
19021938 'You may only specify one of "to_linode" and "region"'
19031939 )
19041940
1905- if region and not type :
1941+ configs = [] if configs is None else configs
1942+ disks = [] if disks is None else disks
1943+
1944+ if region and not instance_type :
19061945 raise ValueError ('Specifying a region requires a "service" as well' )
19071946
19081947 if not isinstance (configs , list ) and not isinstance (
0 commit comments