Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 41 additions & 16 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/debian9",
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 @@ -214,6 +220,7 @@ def instance_create(
"g6-standard-2",
"us-east",
image="linode/debian9",
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=5000,
)

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": 5000,
},
)

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=5000,
)

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": 5000,
},
)

Expand Down
Loading
Loading