Skip to content
59 changes: 42 additions & 17 deletions linode_api4/groups/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ def instance_create(
interface_generation: Optional[Union[InterfaceGeneration, str]] = None,
network_helper: Optional[bool] = None,
maintenance_policy: Optional[str] = None,
root_pass: Optional[str] = None,
kernel: Optional[str] = None,
boot_size: Optional[int] = None,
**kwargs,
):
"""
Expand All @@ -172,26 +175,29 @@ def instance_create(
To create an Instance from an :any:`Image`, call `instance_create` with
a :any:`Type`, a :any:`Region`, and an :any:`Image`. All three of
these fields may be provided as either the ID or the appropriate object.
In this mode, a root password will be generated and returned with the
new Instance object.
When an Image is provided, at least one of ``root_pass`` or
``authorized_keys`` must also be given. If ``root_pass`` is provided,
the Instance and the password are returned as a tuple.

For example::

new_linode, password = client.linode.instance_create(
"g6-standard-2",
"us-east",
image="linode/debian9")
image="linode/debian13",
root_pass="aComplex@Password123")

ltype = client.linode.types().first()
region = client.regions().first()
image = client.images().first()

another_linode, password = client.linode.instance_create(
another_linode = client.linode.instance_create(
ltype,
region,
image=image)
image=image,
authorized_keys="ssh-rsa AAAA")

To output the password from the above example:
To output the password from the first example above:
print(password)

To output the first IPv4 address of the new Linode:
Expand All @@ -213,7 +219,8 @@ def instance_create(
new_linode, password = client.linode.instance_create(
"g6-standard-2",
"us-east",
image="linode/debian9",
image="linode/debian13",
root_pass="aComplex@Password123",
stackscript=stackscript,
stackscript_data={"gh_username": "example"})

Expand Down Expand Up @@ -248,6 +255,7 @@ def instance_create(
"g6-standard-1",
"us-mia",
image="linode/ubuntu24.04",
root_pass="aComplex@Password123",

# This can be configured as an account-wide default
interface_generation=InterfaceGeneration.LINODE,
Expand Down Expand Up @@ -280,10 +288,16 @@ def instance_create(
:type ltype: str or Type
:param region: The Region in which we are creating the Instance
:type region: str or Region
:param image: The Image to deploy to this Instance. If this is provided
and no root_pass is given, a password will be generated
and returned along with the new Instance.
:param image: The Image to deploy to this Instance. If this is provided,
at least one of root_pass or authorized_keys must also be
provided.
:type image: str or Image
:param root_pass: The root password for the new Instance. If an image is
provided and root_pass is given, the Instance and password
will be returned as a tuple. If neither root_pass nor
authorized_keys is provided when an image is specified,
a ValueError will be raised.
:type root_pass: str
:param stackscript: The StackScript to deploy to the new Instance. If
provided, "image" is required and must be compatible
with the chosen StackScript.
Expand Down Expand Up @@ -336,25 +350,33 @@ def instance_create(
:param maintenance_policy: The slug of the maintenance policy to apply during maintenance.
If not provided, the default policy (linode/migrate) will be applied.
:type maintenance_policy: str
:param kernel: The kernel to boot the Instance with. If provided, this will be used as the
kernel for the default configuration profile.
:type kernel: str
:param boot_size: The size of the boot disk in MB. If provided, this will be used to create
the boot disk for the Instance.
:type boot_size: int

:returns: A new Instance object, or a tuple containing the new Instance and
the generated password.
the password if both image and root_pass were provided.
:rtype: Instance or tuple(Instance, str)
:raises ApiError: If contacting the API fails
:raises UnexpectedResponseError: If the API response is somehow malformed.
This usually indicates that you are using
an outdated library.
"""

ret_pass = None
if image and not "root_pass" in kwargs:
ret_pass = Instance.generate_root_password()
kwargs["root_pass"] = ret_pass
if image and not root_pass and not authorized_keys:
raise ValueError(
"When creating an Instance from an Image, at least one of "
"root_pass or authorized_keys must be provided."
)

params = {
"type": ltype,
"region": region,
"image": image,
"root_pass": root_pass,
"authorized_keys": load_and_validate_keys(authorized_keys),
# These will automatically be flattened below
"firewall_id": firewall,
Expand All @@ -373,6 +395,8 @@ def instance_create(
"interfaces": interfaces,
"interface_generation": interface_generation,
"network_helper": network_helper,
"kernel": kernel,
"boot_size": boot_size,
}

params.update(kwargs)
Expand All @@ -388,9 +412,9 @@ def instance_create(
)

l = Instance(self.client, result["id"], result)
if not ret_pass:
if not (image and root_pass):
return l
return l, ret_pass
return l, root_pass

@staticmethod
def build_instance_metadata(user_data=None, encode_user_data=True):
Expand All @@ -403,6 +427,7 @@ def build_instance_metadata(user_data=None, encode_user_data=True):
"g6-standard-2",
"us-east",
image="linode/ubuntu22.04",
root_pass="aComplex@Password123",
metadata=client.linode.build_instance_metadata(user_data="myuserdata")
)
:param user_data: User-defined data to provide to the Linode Instance through
Expand Down
47 changes: 26 additions & 21 deletions linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -1395,11 +1395,12 @@ def disk_create(
for the image deployed the disk will be used. Required
if creating a disk without an image.
:param read_only: If True, creates a read-only disk
:param image: The Image to deploy to the disk.
:param image: The Image to deploy to the disk. If provided, at least one of
root_pass or authorized_keys must also be given.
:param root_pass: The password to configure for the root user when deploying an
image to this disk. Not used if image is not given. If an
image is given and root_pass is not, a password will be
generated and returned alongside the new disk.
image is given and root_pass is provided, it will be returned
alongside the new disk as a tuple.
:param authorized_keys: A list of SSH keys to install as trusted for the root user.
:param authorized_users: A list of usernames whose keys should be installed
as trusted for the root user. These user's keys
Expand All @@ -1412,12 +1413,17 @@ def disk_create(
disk. Requires deploying a compatible image.
:param **stackscript_args: Any arguments to pass to the StackScript, as defined
by its User Defined Fields.

:returns: A new Disk object, or a tuple containing the new Disk and the
password if both image and root_pass were provided.
:rtype: Disk or tuple(Disk, str)
"""

gen_pass = None
if image and not root_pass:
gen_pass = Instance.generate_root_password()
root_pass = gen_pass
if image and not root_pass and not authorized_keys:
raise ValueError(
"When creating a Disk from an Image, at least one of "
"root_pass or authorized_keys must be provided."
)

authorized_keys = load_and_validate_keys(authorized_keys)

Expand Down Expand Up @@ -1466,8 +1472,8 @@ def disk_create(

d = Disk(self._client, result["id"], self.id, result)

if gen_pass:
return d, gen_pass
if image and root_pass:
return d, root_pass
return d

def enable_backups(self):
Expand Down Expand Up @@ -1591,8 +1597,8 @@ def rebuild(

:param image: The Image to deploy to this Instance
:type image: str or Image
:param root_pass: The root password for the newly rebuilt Instance. If
omitted, a password will be generated and returned.
:param root_pass: The root password for the newly rebuilt Instance. At least
one of root_pass or authorized_keys must be provided.
:type root_pass: str
:param authorized_keys: The ssh public keys to install in the linode's
/root/.ssh/authorized_keys file. Each entry may
Expand All @@ -1603,14 +1609,14 @@ def rebuild(
NOTE: Disk encryption may not currently be available to all users.
:type disk_encryption: InstanceDiskEncryptionType or str

:returns: The newly generated password, if one was not provided
(otherwise True)
:returns: The root_pass if provided, otherwise True.
:rtype: str or bool
"""
ret_pass = None
if not root_pass:
ret_pass = Instance.generate_root_password()
root_pass = ret_pass
if not root_pass and not authorized_keys:
raise ValueError(
"When rebuilding an Instance, at least one of "
"root_pass or authorized_keys must be provided."
)

authorized_keys = load_and_validate_keys(authorized_keys)

Expand Down Expand Up @@ -1639,10 +1645,9 @@ def rebuild(
# update ourself with the newly-returned information
self._populate(result)

if not ret_pass:
return True
else:
return ret_pass
if root_pass:
return root_pass
return True

def rescue(self, *disks):
"""
Expand Down
133 changes: 131 additions & 2 deletions test/unit/linode_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,13 +686,140 @@ def test_instance_create(self):

def test_instance_create_with_image(self):
"""
Tests that a Linode Instance can be created with an image, and a password generated
Tests that a Linode Instance can be created with an image and root_pass
"""
with self.mock_post("linode/instances/123") as m:
l, pw = self.client.linode.instance_create(
"g6-standard-1",
"us-east-1a",
image="linode/debian9",
root_pass="aComplex@Password123",
)

self.assertIsNotNone(l)
self.assertEqual(l.id, 123)

self.assertEqual(m.call_url, "/linode/instances")

self.assertEqual(
m.call_data,
{
"region": "us-east-1a",
"type": "g6-standard-1",
"image": "linode/debian9",
"root_pass": "aComplex@Password123",
},
)

def test_instance_create_with_image_authorized_keys(self):
"""
Tests that a Linode Instance can be created with an image and authorized_keys only
"""
with self.mock_post("linode/instances/123") as m:
l = self.client.linode.instance_create(
"g6-standard-1",
"us-east-1a",
image="linode/debian9",
authorized_keys="ssh-rsa AAAA",
)

self.assertIsNotNone(l)
self.assertEqual(l.id, 123)

self.assertEqual(m.call_url, "/linode/instances")

self.assertEqual(
m.call_data,
{
"region": "us-east-1a",
"type": "g6-standard-1",
"image": "linode/debian9",
"authorized_keys": ["ssh-rsa AAAA"],
},
)

def test_instance_create_with_image_requires_auth(self):
"""
Tests that creating an Instance from an Image without root_pass or
authorized_keys raises a ValueError
"""
with self.assertRaises(ValueError):
self.client.linode.instance_create(
"g6-standard-1", "us-east-1a", image="linode/debian9"
)

def test_instance_create_with_kernel(self):
"""
Tests that a Linode Instance can be created with a kernel
"""
with self.mock_post("linode/instances/123") as m:
l, pw = self.client.linode.instance_create(
"g6-standard-1",
"us-east-1a",
image="linode/debian9",
root_pass="aComplex@Password123",
kernel="linode/latest-64bit",
)

self.assertIsNotNone(l)
self.assertEqual(l.id, 123)

self.assertEqual(m.call_url, "/linode/instances")

self.assertEqual(
m.call_data,
{
"region": "us-east-1a",
"type": "g6-standard-1",
"image": "linode/debian9",
"root_pass": "aComplex@Password123",
"kernel": "linode/latest-64bit",
},
)

def test_instance_create_with_boot_size(self):
"""
Tests that a Linode Instance can be created with a boot_size
"""
with self.mock_post("linode/instances/123") as m:
l, pw = self.client.linode.instance_create(
"g6-standard-1",
"us-east-1a",
image="linode/debian9",
root_pass="aComplex@Password123",
boot_size=8192,
)

self.assertIsNotNone(l)
self.assertEqual(l.id, 123)

self.assertEqual(m.call_url, "/linode/instances")

self.assertEqual(
m.call_data,
{
"region": "us-east-1a",
"type": "g6-standard-1",
"image": "linode/debian9",
"root_pass": "aComplex@Password123",
"boot_size": 8192,
},
)

def test_instance_create_with_kernel_and_boot_size(self):
"""
Tests that a Linode Instance can be created with both kernel and boot_size
"""
with self.mock_post("linode/instances/123") as m:
l, pw = self.client.linode.instance_create(
"g6-standard-1",
"us-east-1a",
image="linode/debian9",
root_pass="aComplex@Password123",
kernel="linode/latest-64bit",
boot_size=8192,
)

self.assertIsNotNone(l)
self.assertEqual(l.id, 123)

Expand All @@ -704,7 +831,9 @@ def test_instance_create_with_image(self):
"region": "us-east-1a",
"type": "g6-standard-1",
"image": "linode/debian9",
"root_pass": pw,
"root_pass": "aComplex@Password123",
"kernel": "linode/latest-64bit",
"boot_size": 8192,
},
)

Expand Down
Loading
Loading