Skip to content

Commit dfc536e

Browse files
TPT-4113: Project SLADE + CLEO (#696)
* TPT-4527: Implemented changes for SLADE and CLEO projects (#665) * Implemented changes for SLADE and CLEO projects * Address CoPilot suggestions * Reference newer debian version in examples * Fixed boot_size in unit tests * Add authorized_users as one of the required params alongside root_pass and authorized_keys * Fix lint * Fix docstring * Address suggestions * Drop root_pass from returned fields for instance_create, rebuild, and disk_create * Fix lint * TPT-4261: Implement integration tests for Expand Create Linodes Options and Password-less Linodes (#693) * Create tests test_expected_error_if_fields_authorized_users_authorized_keys_root_pass_are_not_set and def test_create_linode_with_kernel_and_boot_size_then_add_disk_and_rebuild * Add copilot suggestions * Add code review suggestions --------- Co-authored-by: Pawel <100145168+psnoch-akamai@users.noreply.github.com>
1 parent 368aed4 commit dfc536e

15 files changed

Lines changed: 449 additions & 105 deletions

File tree

linode_api4/groups/linode.py

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ def instance_create(
162162
interface_generation: Optional[Union[InterfaceGeneration, str]] = None,
163163
network_helper: Optional[bool] = None,
164164
maintenance_policy: Optional[str] = None,
165+
root_pass: Optional[str] = None,
166+
kernel: Optional[str] = None,
167+
boot_size: Optional[int] = None,
168+
authorized_users: Optional[List[str]] = None,
165169
**kwargs,
166170
):
167171
"""
@@ -172,27 +176,26 @@ def instance_create(
172176
To create an Instance from an :any:`Image`, call `instance_create` with
173177
a :any:`Type`, a :any:`Region`, and an :any:`Image`. All three of
174178
these fields may be provided as either the ID or the appropriate object.
175-
In this mode, a root password will be generated and returned with the
176-
new Instance object.
179+
When an Image is provided, at least one of ``root_pass``, ``authorized_users``, or
180+
``authorized_keys`` must also be given.
177181
178182
For example::
179183
180-
new_linode, password = client.linode.instance_create(
184+
new_linode = client.linode.instance_create(
181185
"g6-standard-2",
182186
"us-east",
183-
image="linode/debian9")
187+
image="linode/debian13",
188+
root_pass="aComplex@Password123")
184189
185190
ltype = client.linode.types().first()
186191
region = client.regions().first()
187192
image = client.images().first()
188193
189-
another_linode, password = client.linode.instance_create(
194+
another_linode = client.linode.instance_create(
190195
ltype,
191196
region,
192-
image=image)
193-
194-
To output the password from the above example:
195-
print(password)
197+
image=image,
198+
authorized_keys="ssh-rsa AAAA")
196199
197200
To output the first IPv4 address of the new Linode:
198201
print(new_linode.ipv4[0])
@@ -210,10 +213,11 @@ def instance_create(
210213
211214
stackscript = StackScript(client, 10079)
212215
213-
new_linode, password = client.linode.instance_create(
216+
new_linode = client.linode.instance_create(
214217
"g6-standard-2",
215218
"us-east",
216-
image="linode/debian9",
219+
image="linode/debian13",
220+
root_pass="aComplex@Password123",
217221
stackscript=stackscript,
218222
stackscript_data={"gh_username": "example"})
219223
@@ -244,10 +248,11 @@ def instance_create(
244248
To create a new Instance with explicit interfaces, provide list of
245249
LinodeInterfaceOptions objects or dicts to the "interfaces" field::
246250
247-
linode, password = client.linode.instance_create(
251+
linode = client.linode.instance_create(
248252
"g6-standard-1",
249253
"us-mia",
250254
image="linode/ubuntu24.04",
255+
root_pass="aComplex@Password123",
251256
252257
# This can be configured as an account-wide default
253258
interface_generation=InterfaceGeneration.LINODE,
@@ -280,10 +285,14 @@ def instance_create(
280285
:type ltype: str or Type
281286
:param region: The Region in which we are creating the Instance
282287
:type region: str or Region
283-
:param image: The Image to deploy to this Instance. If this is provided
284-
and no root_pass is given, a password will be generated
285-
and returned along with the new Instance.
288+
:param image: The Image to deploy to this Instance. If this is provided,
289+
at least one of root_pass, authorized_users, or authorized_keys must also be
290+
provided.
286291
:type image: str or Image
292+
:param root_pass: The root password for the new Instance. Required when
293+
an image is provided and neither authorized_users nor
294+
authorized_keys are given.
295+
:type root_pass: str
287296
:param stackscript: The StackScript to deploy to the new Instance. If
288297
provided, "image" is required and must be compatible
289298
with the chosen StackScript.
@@ -300,6 +309,11 @@ def instance_create(
300309
be a single key, or a path to a file containing
301310
the key.
302311
:type authorized_keys: list or str
312+
:param authorized_users: A list of usernames whose keys should be installed
313+
as trusted for the root user. These user's keys
314+
should already be set up, see :any:`ProfileGroup.ssh_keys`
315+
for details.
316+
:type authorized_users: list[str]
303317
:param label: The display label for the new Instance
304318
:type label: str
305319
:param group: The display group for the new Instance
@@ -335,26 +349,39 @@ def instance_create(
335349
:param maintenance_policy: The slug of the maintenance policy to apply during maintenance.
336350
If not provided, the default policy (linode/migrate) will be applied.
337351
:type maintenance_policy: str
338-
339-
:returns: A new Instance object, or a tuple containing the new Instance and
340-
the generated password.
341-
:rtype: Instance or tuple(Instance, str)
352+
:param kernel: The kernel to boot the Instance with. If provided, this will be used as the
353+
kernel for the default configuration profile.
354+
:type kernel: str
355+
:param boot_size: The size of the boot disk in MB. If provided, this will be used to create
356+
the boot disk for the Instance.
357+
:type boot_size: int
358+
359+
:returns: A new Instance object
360+
:rtype: Instance
342361
:raises ApiError: If contacting the API fails
343362
:raises UnexpectedResponseError: If the API response is somehow malformed.
344363
This usually indicates that you are using
345364
an outdated library.
346365
"""
347366

348-
ret_pass = None
349-
if image and not "root_pass" in kwargs:
350-
ret_pass = Instance.generate_root_password()
351-
kwargs["root_pass"] = ret_pass
367+
if (
368+
image
369+
and not root_pass
370+
and not authorized_keys
371+
and not authorized_users
372+
):
373+
raise ValueError(
374+
"When creating an Instance from an Image, at least one of "
375+
"root_pass, authorized_users, or authorized_keys must be provided."
376+
)
352377

353378
params = {
354379
"type": ltype,
355380
"region": region,
356381
"image": image,
382+
"root_pass": root_pass,
357383
"authorized_keys": load_and_validate_keys(authorized_keys),
384+
"authorized_users": authorized_users,
358385
# These will automatically be flattened below
359386
"firewall_id": firewall,
360387
"backup_id": backup,
@@ -372,6 +399,8 @@ def instance_create(
372399
"interfaces": interfaces,
373400
"interface_generation": interface_generation,
374401
"network_helper": network_helper,
402+
"kernel": kernel,
403+
"boot_size": boot_size,
375404
}
376405

377406
params.update(kwargs)
@@ -386,10 +415,7 @@ def instance_create(
386415
"Unexpected response when creating linode!", json=result
387416
)
388417

389-
l = Instance(self.client, result["id"], result)
390-
if not ret_pass:
391-
return l
392-
return l, ret_pass
418+
return Instance(self.client, result["id"], result)
393419

394420
@staticmethod
395421
def build_instance_metadata(user_data=None, encode_user_data=True):
@@ -398,10 +424,11 @@ def build_instance_metadata(user_data=None, encode_user_data=True):
398424
the :any:`instance_create` method. This helper can also be used
399425
when cloning and rebuilding Instances.
400426
**Creating an Instance with User Data**::
401-
new_linode, password = client.linode.instance_create(
427+
new_linode = client.linode.instance_create(
402428
"g6-standard-2",
403429
"us-east",
404430
image="linode/ubuntu22.04",
431+
root_pass="aComplex@Password123",
405432
metadata=client.linode.build_instance_metadata(user_data="myuserdata")
406433
)
407434
:param user_data: User-defined data to provide to the Linode Instance through

linode_api4/objects/linode.py

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,11 +1437,10 @@ def disk_create(
14371437
for the image deployed the disk will be used. Required
14381438
if creating a disk without an image.
14391439
:param read_only: If True, creates a read-only disk
1440-
:param image: The Image to deploy to the disk.
1440+
:param image: The Image to deploy to the disk. If provided, at least one of
1441+
root_pass, authorized_users or authorized_keys must also be given.
14411442
:param root_pass: The password to configure for the root user when deploying an
1442-
image to this disk. Not used if image is not given. If an
1443-
image is given and root_pass is not, a password will be
1444-
generated and returned alongside the new disk.
1443+
image to this disk. Not used if image is not given.
14451444
:param authorized_keys: A list of SSH keys to install as trusted for the root user.
14461445
:param authorized_users: A list of usernames whose keys should be installed
14471446
as trusted for the root user. These user's keys
@@ -1453,12 +1452,21 @@ def disk_create(
14531452
disk. Requires deploying a compatible image.
14541453
:param **stackscript_args: Any arguments to pass to the StackScript, as defined
14551454
by its User Defined Fields.
1455+
1456+
:returns: A new Disk object.
1457+
:rtype: Disk
14561458
"""
14571459

1458-
gen_pass = None
1459-
if image and not root_pass:
1460-
gen_pass = Instance.generate_root_password()
1461-
root_pass = gen_pass
1460+
if (
1461+
image
1462+
and not root_pass
1463+
and not authorized_keys
1464+
and not authorized_users
1465+
):
1466+
raise ValueError(
1467+
"When creating a Disk from an Image, at least one of "
1468+
"root_pass, authorized_users, or authorized_keys must be provided."
1469+
)
14621470

14631471
authorized_keys = load_and_validate_keys(authorized_keys)
14641472

@@ -1505,11 +1513,7 @@ def disk_create(
15051513
"Unexpected response creating disk!", json=result
15061514
)
15071515

1508-
d = Disk(self._client, result["id"], self.id, result)
1509-
1510-
if gen_pass:
1511-
return d, gen_pass
1512-
return d
1516+
return Disk(self._client, result["id"], self.id, result)
15131517

15141518
def enable_backups(self):
15151519
"""
@@ -1621,6 +1625,7 @@ def rebuild(
16211625
disk_encryption: Optional[
16221626
Union[InstanceDiskEncryptionType, str]
16231627
] = None,
1628+
authorized_users: Optional[List[str]] = None,
16241629
**kwargs,
16251630
):
16261631
"""
@@ -1632,25 +1637,30 @@ def rebuild(
16321637
16331638
:param image: The Image to deploy to this Instance
16341639
:type image: str or Image
1635-
:param root_pass: The root password for the newly rebuilt Instance. If
1636-
omitted, a password will be generated and returned.
1640+
:param root_pass: The root password for the newly rebuilt Instance. At least
1641+
one of root_pass, authorized_users, or authorized_keys must be provided.
16371642
:type root_pass: str
16381643
:param authorized_keys: The ssh public keys to install in the linode's
16391644
/root/.ssh/authorized_keys file. Each entry may
16401645
be a single key, or a path to a file containing
16411646
the key.
16421647
:type authorized_keys: list or str
1648+
:param authorized_users: A list of usernames whose keys should be installed
1649+
as trusted for the root user. These user's keys
1650+
should already be set up, see :any:`ProfileGroup.ssh_keys`
1651+
for details.
1652+
:type authorized_users: list[str]
16431653
:param disk_encryption: The disk encryption policy for this Linode.
16441654
:type disk_encryption: InstanceDiskEncryptionType or str
16451655
1646-
:returns: The newly generated password, if one was not provided
1647-
(otherwise True)
1648-
:rtype: str or bool
1656+
:returns: True.
1657+
:rtype: bool
16491658
"""
1650-
ret_pass = None
1651-
if not root_pass:
1652-
ret_pass = Instance.generate_root_password()
1653-
root_pass = ret_pass
1659+
if not root_pass and not authorized_keys and not authorized_users:
1660+
raise ValueError(
1661+
"When rebuilding an Instance, at least one of "
1662+
"root_pass, authorized_users, or authorized_keys must be provided."
1663+
)
16541664

16551665
authorized_keys = load_and_validate_keys(authorized_keys)
16561666

@@ -1661,6 +1671,7 @@ def rebuild(
16611671
"disk_encryption": (
16621672
str(disk_encryption) if disk_encryption else None
16631673
),
1674+
"authorized_users": authorized_users,
16641675
}
16651676

16661677
params.update(kwargs)
@@ -1679,10 +1690,7 @@ def rebuild(
16791690
# update ourself with the newly-returned information
16801691
self._populate(result)
16811692

1682-
if not ret_pass:
1683-
return True
1684-
else:
1685-
return ret_pass
1693+
return True
16861694

16871695
def rescue(self, *disks):
16881696
"""

test/integration/conftest.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,13 @@ def create_linode(test_linode_client, e2e_test_firewall):
222222
region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core")
223223
label = get_test_label(length=8)
224224

225-
linode_instance, password = client.linode.instance_create(
225+
linode_instance = client.linode.instance_create(
226226
"g6-nanode-1",
227227
region,
228228
image="linode/debian12",
229229
label=label,
230230
firewall=e2e_test_firewall,
231+
root_pass="aComplex@Password123",
231232
)
232233

233234
yield linode_instance
@@ -241,13 +242,15 @@ def create_linode_for_pass_reset(test_linode_client, e2e_test_firewall):
241242

242243
region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core")
243244
label = get_test_label(length=8)
245+
password = "aComplex@Password123"
244246

245-
linode_instance, password = client.linode.instance_create(
247+
linode_instance = client.linode.instance_create(
246248
"g6-nanode-1",
247249
region,
248250
image="linode/debian12",
249251
label=label,
250252
firewall=e2e_test_firewall,
253+
root_pass=password,
251254
)
252255

253256
yield linode_instance, password
@@ -487,15 +490,16 @@ def create_vpc_with_subnet_and_linode(
487490

488491
label = get_test_label(length=8)
489492

490-
instance, password = test_linode_client.linode.instance_create(
493+
instance = test_linode_client.linode.instance_create(
491494
"g6-standard-1",
492495
vpc.region,
493496
image="linode/debian11",
494497
label=label,
495498
firewall=e2e_test_firewall,
499+
root_pass="aComplex@Password123",
496500
)
497501

498-
yield vpc, subnet, instance, password
502+
yield vpc, subnet, instance
499503

500504
instance.delete()
501505

@@ -578,12 +582,13 @@ def linode_for_vlan_tests(test_linode_client, e2e_test_firewall):
578582
region = get_region(client, {"Linodes", "Vlans"}, site_type="core")
579583
label = get_test_label(length=8)
580584

581-
linode_instance, password = client.linode.instance_create(
585+
linode_instance = client.linode.instance_create(
582586
"g6-nanode-1",
583587
region,
584588
image="linode/debian12",
585589
label=label,
586590
firewall=e2e_test_firewall,
591+
root_pass="aComplex@Password123",
587592
)
588593

589594
yield linode_instance
@@ -627,13 +632,14 @@ def linode_with_linode_interfaces(
627632
region = vpc.region
628633
label = get_test_label()
629634

630-
instance, _ = client.linode.instance_create(
635+
instance = client.linode.instance_create(
631636
"g6-nanode-1",
632637
region,
633638
image="linode/debian12",
634639
label=label,
635640
booted=False,
636641
interface_generation=InterfaceGeneration.LINODE,
642+
root_pass="aComplex@Password123",
637643
interfaces=[
638644
LinodeInterfaceOptions(
639645
firewall_id=e2e_test_firewall.id,

0 commit comments

Comments
 (0)