Skip to content

Commit e458a46

Browse files
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
1 parent d18b54e commit e458a46

15 files changed

Lines changed: 374 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
@@ -336,26 +350,39 @@ def instance_create(
336350
:param maintenance_policy: The slug of the maintenance policy to apply during maintenance.
337351
If not provided, the default policy (linode/migrate) will be applied.
338352
:type maintenance_policy: str
339-
340-
:returns: A new Instance object, or a tuple containing the new Instance and
341-
the generated password.
342-
:rtype: Instance or tuple(Instance, str)
353+
:param kernel: The kernel to boot the Instance with. If provided, this will be used as the
354+
kernel for the default configuration profile.
355+
:type kernel: str
356+
:param boot_size: The size of the boot disk in MB. If provided, this will be used to create
357+
the boot disk for the Instance.
358+
:type boot_size: int
359+
360+
:returns: A new Instance object
361+
:rtype: Instance
343362
:raises ApiError: If contacting the API fails
344363
:raises UnexpectedResponseError: If the API response is somehow malformed.
345364
This usually indicates that you are using
346365
an outdated library.
347366
"""
348367

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

354379
params = {
355380
"type": ltype,
356381
"region": region,
357382
"image": image,
383+
"root_pass": root_pass,
358384
"authorized_keys": load_and_validate_keys(authorized_keys),
385+
"authorized_users": authorized_users,
359386
# These will automatically be flattened below
360387
"firewall_id": firewall,
361388
"backup_id": backup,
@@ -373,6 +400,8 @@ def instance_create(
373400
"interfaces": interfaces,
374401
"interface_generation": interface_generation,
375402
"network_helper": network_helper,
403+
"kernel": kernel,
404+
"boot_size": boot_size,
376405
}
377406

378407
params.update(kwargs)
@@ -387,10 +416,7 @@ def instance_create(
387416
"Unexpected response when creating linode!", json=result
388417
)
389418

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

395421
@staticmethod
396422
def build_instance_metadata(user_data=None, encode_user_data=True):
@@ -399,10 +425,11 @@ def build_instance_metadata(user_data=None, encode_user_data=True):
399425
the :any:`instance_create` method. This helper can also be used
400426
when cloning and rebuilding Instances.
401427
**Creating an Instance with User Data**::
402-
new_linode, password = client.linode.instance_create(
428+
new_linode = client.linode.instance_create(
403429
"g6-standard-2",
404430
"us-east",
405431
image="linode/ubuntu22.04",
432+
root_pass="aComplex@Password123",
406433
metadata=client.linode.build_instance_metadata(user_data="myuserdata")
407434
)
408435
: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
@@ -1395,11 +1395,10 @@ def disk_create(
13951395
for the image deployed the disk will be used. Required
13961396
if creating a disk without an image.
13971397
:param read_only: If True, creates a read-only disk
1398-
:param image: The Image to deploy to the disk.
1398+
:param image: The Image to deploy to the disk. If provided, at least one of
1399+
root_pass, authorized_users or authorized_keys must also be given.
13991400
:param root_pass: The password to configure for the root user when deploying an
1400-
image to this disk. Not used if image is not given. If an
1401-
image is given and root_pass is not, a password will be
1402-
generated and returned alongside the new disk.
1401+
image to this disk. Not used if image is not given.
14031402
:param authorized_keys: A list of SSH keys to install as trusted for the root user.
14041403
:param authorized_users: A list of usernames whose keys should be installed
14051404
as trusted for the root user. These user's keys
@@ -1412,12 +1411,21 @@ def disk_create(
14121411
disk. Requires deploying a compatible image.
14131412
:param **stackscript_args: Any arguments to pass to the StackScript, as defined
14141413
by its User Defined Fields.
1414+
1415+
:returns: A new Disk object.
1416+
:rtype: Disk
14151417
"""
14161418

1417-
gen_pass = None
1418-
if image and not root_pass:
1419-
gen_pass = Instance.generate_root_password()
1420-
root_pass = gen_pass
1419+
if (
1420+
image
1421+
and not root_pass
1422+
and not authorized_keys
1423+
and not authorized_users
1424+
):
1425+
raise ValueError(
1426+
"When creating a Disk from an Image, at least one of "
1427+
"root_pass, authorized_users, or authorized_keys must be provided."
1428+
)
14211429

14221430
authorized_keys = load_and_validate_keys(authorized_keys)
14231431

@@ -1464,11 +1472,7 @@ def disk_create(
14641472
"Unexpected response creating disk!", json=result
14651473
)
14661474

1467-
d = Disk(self._client, result["id"], self.id, result)
1468-
1469-
if gen_pass:
1470-
return d, gen_pass
1471-
return d
1475+
return Disk(self._client, result["id"], self.id, result)
14721476

14731477
def enable_backups(self):
14741478
"""
@@ -1580,6 +1584,7 @@ def rebuild(
15801584
disk_encryption: Optional[
15811585
Union[InstanceDiskEncryptionType, str]
15821586
] = None,
1587+
authorized_users: Optional[List[str]] = None,
15831588
**kwargs,
15841589
):
15851590
"""
@@ -1591,26 +1596,31 @@ def rebuild(
15911596
15921597
:param image: The Image to deploy to this Instance
15931598
:type image: str or Image
1594-
:param root_pass: The root password for the newly rebuilt Instance. If
1595-
omitted, a password will be generated and returned.
1599+
:param root_pass: The root password for the newly rebuilt Instance. At least
1600+
one of root_pass, authorized_users, or authorized_keys must be provided.
15961601
:type root_pass: str
15971602
:param authorized_keys: The ssh public keys to install in the linode's
15981603
/root/.ssh/authorized_keys file. Each entry may
15991604
be a single key, or a path to a file containing
16001605
the key.
16011606
:type authorized_keys: list or str
1607+
:param authorized_users: A list of usernames whose keys should be installed
1608+
as trusted for the root user. These user's keys
1609+
should already be set up, see :any:`ProfileGroup.ssh_keys`
1610+
for details.
1611+
:type authorized_users: list[str]
16021612
:param disk_encryption: The disk encryption policy for this Linode.
16031613
NOTE: Disk encryption may not currently be available to all users.
16041614
:type disk_encryption: InstanceDiskEncryptionType or str
16051615
1606-
:returns: The newly generated password, if one was not provided
1607-
(otherwise True)
1608-
:rtype: str or bool
1616+
:returns: True.
1617+
:rtype: bool
16091618
"""
1610-
ret_pass = None
1611-
if not root_pass:
1612-
ret_pass = Instance.generate_root_password()
1613-
root_pass = ret_pass
1619+
if not root_pass and not authorized_keys and not authorized_users:
1620+
raise ValueError(
1621+
"When rebuilding an Instance, at least one of "
1622+
"root_pass, authorized_users, or authorized_keys must be provided."
1623+
)
16141624

16151625
authorized_keys = load_and_validate_keys(authorized_keys)
16161626

@@ -1621,6 +1631,7 @@ def rebuild(
16211631
"disk_encryption": (
16221632
str(disk_encryption) if disk_encryption else None
16231633
),
1634+
"authorized_users": authorized_users,
16241635
}
16251636

16261637
params.update(kwargs)
@@ -1639,10 +1650,7 @@ def rebuild(
16391650
# update ourself with the newly-returned information
16401651
self._populate(result)
16411652

1642-
if not ret_pass:
1643-
return True
1644-
else:
1645-
return ret_pass
1653+
return True
16461654

16471655
def rescue(self, *disks):
16481656
"""

test/integration/conftest.py

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

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

234235
yield linode_instance
@@ -242,13 +243,15 @@ def create_linode_for_pass_reset(test_linode_client, e2e_test_firewall):
242243

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

246-
linode_instance, password = client.linode.instance_create(
248+
linode_instance = client.linode.instance_create(
247249
"g6-nanode-1",
248250
region,
249251
image="linode/debian12",
250252
label=label,
251253
firewall=e2e_test_firewall,
254+
root_pass=password,
252255
)
253256

254257
yield linode_instance, password
@@ -488,15 +491,16 @@ def create_vpc_with_subnet_and_linode(
488491

489492
label = get_test_label(length=8)
490493

491-
instance, password = test_linode_client.linode.instance_create(
494+
instance = test_linode_client.linode.instance_create(
492495
"g6-standard-1",
493496
vpc.region,
494497
image="linode/debian11",
495498
label=label,
496499
firewall=e2e_test_firewall,
500+
root_pass="aComplex@Password123",
497501
)
498502

499-
yield vpc, subnet, instance, password
503+
yield vpc, subnet, instance
500504

501505
instance.delete()
502506

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

582-
linode_instance, password = client.linode.instance_create(
586+
linode_instance = client.linode.instance_create(
583587
"g6-nanode-1",
584588
region,
585589
image="linode/debian12",
586590
label=label,
587591
firewall=e2e_test_firewall,
592+
root_pass="aComplex@Password123",
588593
)
589594

590595
yield linode_instance
@@ -628,13 +633,14 @@ def linode_with_linode_interfaces(
628633
region = vpc.region
629634
label = get_test_label()
630635

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

0 commit comments

Comments
 (0)