diff --git a/linode_api4/groups/linode.py b/linode_api4/groups/linode.py index 2bd51fa97..e47154f75 100644 --- a/linode_api4/groups/linode.py +++ b/linode_api4/groups/linode.py @@ -162,6 +162,10 @@ 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, + authorized_users: Optional[List[str]] = None, **kwargs, ): """ @@ -172,27 +176,26 @@ 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``, ``authorized_users``, or + ``authorized_keys`` must also be given. For example:: - new_linode, password = client.linode.instance_create( + new_linode = 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) - - To output the password from the above example: - print(password) + image=image, + authorized_keys="ssh-rsa AAAA") To output the first IPv4 address of the new Linode: print(new_linode.ipv4[0]) @@ -210,10 +213,11 @@ def instance_create( stackscript = StackScript(client, 10079) - new_linode, password = client.linode.instance_create( + new_linode = 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"}) @@ -244,10 +248,11 @@ def instance_create( To create a new Instance with explicit interfaces, provide list of LinodeInterfaceOptions objects or dicts to the "interfaces" field:: - linode, password = client.linode.instance_create( + linode = client.linode.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, @@ -280,10 +285,14 @@ 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, authorized_users, or authorized_keys must also be + provided. :type image: str or Image + :param root_pass: The root password for the new Instance. Required when + an image is provided and neither authorized_users nor + authorized_keys are given. + :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. @@ -300,6 +309,11 @@ def instance_create( be a single key, or a path to a file containing the key. :type authorized_keys: list or str + :param authorized_users: A list of usernames whose keys should be installed + as trusted for the root user. These user's keys + should already be set up, see :any:`ProfileGroup.ssh_keys` + for details. + :type authorized_users: list[str] :param label: The display label for the new Instance :type label: str :param group: The display group for the new Instance @@ -336,26 +350,39 @@ 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 - - :returns: A new Instance object, or a tuple containing the new Instance and - the generated password. - :rtype: Instance or tuple(Instance, 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 + :rtype: Instance :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 + and not authorized_users + ): + raise ValueError( + "When creating an Instance from an Image, at least one of " + "root_pass, authorized_users, 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), + "authorized_users": authorized_users, # These will automatically be flattened below "firewall_id": firewall, "backup_id": backup, @@ -373,6 +400,8 @@ def instance_create( "interfaces": interfaces, "interface_generation": interface_generation, "network_helper": network_helper, + "kernel": kernel, + "boot_size": boot_size, } params.update(kwargs) @@ -387,10 +416,7 @@ def instance_create( "Unexpected response when creating linode!", json=result ) - l = Instance(self.client, result["id"], result) - if not ret_pass: - return l - return l, ret_pass + return Instance(self.client, result["id"], result) @staticmethod 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): the :any:`instance_create` method. This helper can also be used when cloning and rebuilding Instances. **Creating an Instance with User Data**:: - new_linode, password = client.linode.instance_create( + new_linode = client.linode.instance_create( "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 diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py index 3ffe4b232..7c48f09dc 100644 --- a/linode_api4/objects/linode.py +++ b/linode_api4/objects/linode.py @@ -1395,11 +1395,10 @@ 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, authorized_users 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 to this disk. Not used if image is not given. :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 @@ -1412,12 +1411,21 @@ 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. + :rtype: Disk """ - 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 + and not authorized_users + ): + raise ValueError( + "When creating a Disk from an Image, at least one of " + "root_pass, authorized_users, or authorized_keys must be provided." + ) authorized_keys = load_and_validate_keys(authorized_keys) @@ -1464,11 +1472,7 @@ def disk_create( "Unexpected response creating disk!", json=result ) - d = Disk(self._client, result["id"], self.id, result) - - if gen_pass: - return d, gen_pass - return d + return Disk(self._client, result["id"], self.id, result) def enable_backups(self): """ @@ -1580,6 +1584,7 @@ def rebuild( disk_encryption: Optional[ Union[InstanceDiskEncryptionType, str] ] = None, + authorized_users: Optional[List[str]] = None, **kwargs, ): """ @@ -1591,26 +1596,31 @@ 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, authorized_users, 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 be a single key, or a path to a file containing the key. :type authorized_keys: list or str + :param authorized_users: A list of usernames whose keys should be installed + as trusted for the root user. These user's keys + should already be set up, see :any:`ProfileGroup.ssh_keys` + for details. + :type authorized_users: list[str] :param disk_encryption: The disk encryption policy for this Linode. 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) - :rtype: str or bool + :returns: True. + :rtype: 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 and not authorized_users: + raise ValueError( + "When rebuilding an Instance, at least one of " + "root_pass, authorized_users, or authorized_keys must be provided." + ) authorized_keys = load_and_validate_keys(authorized_keys) @@ -1621,6 +1631,7 @@ def rebuild( "disk_encryption": ( str(disk_encryption) if disk_encryption else None ), + "authorized_users": authorized_users, } params.update(kwargs) @@ -1639,10 +1650,7 @@ def rebuild( # update ourself with the newly-returned information self._populate(result) - if not ret_pass: - return True - else: - return ret_pass + return True def rescue(self, *disks): """ diff --git a/test/integration/conftest.py b/test/integration/conftest.py index a5c832f4f..793e9be9e 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -223,12 +223,13 @@ def create_linode(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label(length=8) - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) yield linode_instance @@ -242,13 +243,15 @@ def create_linode_for_pass_reset(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label(length=8) + password = "aComplex@Password123" - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label, firewall=e2e_test_firewall, + root_pass=password, ) yield linode_instance, password @@ -488,15 +491,16 @@ def create_vpc_with_subnet_and_linode( label = get_test_label(length=8) - instance, password = test_linode_client.linode.instance_create( + instance = test_linode_client.linode.instance_create( "g6-standard-1", vpc.region, image="linode/debian11", label=label, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) - yield vpc, subnet, instance, password + yield vpc, subnet, instance instance.delete() @@ -579,12 +583,13 @@ def linode_for_vlan_tests(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Vlans"}, site_type="core") label = get_test_label(length=8) - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) yield linode_instance @@ -628,13 +633,14 @@ def linode_with_linode_interfaces( region = vpc.region label = get_test_label() - instance, _ = client.linode.instance_create( + instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label, booted=False, interface_generation=InterfaceGeneration.LINODE, + root_pass="aComplex@Password123", interfaces=[ LinodeInterfaceOptions( firewall_id=e2e_test_firewall.id, diff --git a/test/integration/linode_client/test_linode_client.py b/test/integration/linode_client/test_linode_client.py index 4060064d3..83a38c29c 100644 --- a/test/integration/linode_client/test_linode_client.py +++ b/test/integration/linode_client/test_linode_client.py @@ -16,12 +16,13 @@ def setup_client_and_linode(test_linode_client, e2e_test_firewall): label = get_test_label() - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) yield client, linode_instance @@ -258,7 +259,7 @@ def test_create_linode_with_interfaces(test_linode_client): region = get_region(client, {"Vlans", "Linodes"}, site_type="core").id label = get_test_label() - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, label=label, @@ -269,6 +270,7 @@ def test_create_linode_with_interfaces(test_linode_client): purpose="vlan", label="cool-vlan", ipam_address="10.0.0.4/32" ), ], + root_pass="aComplex@Password123", ) assert len(linode_instance.configs[0].interfaces) == 2 diff --git a/test/integration/models/account/test_account.py b/test/integration/models/account/test_account.py index 4c4dcc134..2bb3c48f0 100644 --- a/test/integration/models/account/test_account.py +++ b/test/integration/models/account/test_account.py @@ -98,12 +98,13 @@ def test_latest_get_event(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label() - linode, password = client.linode.instance_create( + linode = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) def get_linode_status(): diff --git a/test/integration/models/firewall/test_firewall.py b/test/integration/models/firewall/test_firewall.py index 16805f3b8..31adf1c5e 100644 --- a/test/integration/models/firewall/test_firewall.py +++ b/test/integration/models/firewall/test_firewall.py @@ -13,8 +13,12 @@ def linode_fw(test_linode_client): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label() - linode_instance, password = client.linode.instance_create( - "g6-nanode-1", region, image="linode/debian12", label=label + linode_instance = client.linode.instance_create( + "g6-nanode-1", + region, + image="linode/debian12", + label=label, + root_pass="aComplex@Password123", ) yield linode_instance diff --git a/test/integration/models/linode/test_linode.py b/test/integration/models/linode/test_linode.py index 9f6194fa9..13f8c7c93 100644 --- a/test/integration/models/linode/test_linode.py +++ b/test/integration/models/linode/test_linode.py @@ -37,11 +37,12 @@ def linode_with_volume_firewall(test_linode_client): "inbound_policy": "DROP", } - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label + "_modlinode", + root_pass="aComplex@Password123", ) volume = client.volume_create( @@ -75,13 +76,14 @@ def linode_for_legacy_interface_tests(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label(length=8) - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label, firewall=e2e_test_firewall, interface_generation=InterfaceGeneration.LEGACY_CONFIG, + root_pass="aComplex@Password123", ) yield linode_instance @@ -97,7 +99,7 @@ def linode_and_vpc_for_legacy_interface_tests_offline( label = get_test_label(length=8) - instance, password = test_linode_client.linode.instance_create( + instance = test_linode_client.linode.instance_create( "g6-standard-1", vpc.region, booted=False, @@ -105,9 +107,10 @@ def linode_and_vpc_for_legacy_interface_tests_offline( label=label, firewall=e2e_test_firewall, interface_generation=InterfaceGeneration.LEGACY_CONFIG, + root_pass="aComplex@Password123", ) - yield vpc, subnet, instance, password + yield vpc, subnet, instance instance.delete() @@ -119,12 +122,13 @@ def linode_for_vpu_tests(test_linode_client, e2e_test_firewall): label = get_test_label(length=8) - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g1-accelerated-netint-vpu-t1u1-s", region, image="linode/debian12", label=label, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) yield linode_instance @@ -138,12 +142,13 @@ def linode_for_disk_tests(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label() - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/alpine3.19", label=label + "_long_tests", firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) # Provisioning time @@ -171,12 +176,13 @@ def linode_with_block_storage_encryption(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Block Storage Encryption"}) label = get_test_label() - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/alpine3.19", label=label + "block-storage-encryption", firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) yield linode_instance @@ -190,12 +196,13 @@ def create_linode_for_long_running_tests(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label() - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label + "_long_tests", firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) yield linode_instance @@ -212,13 +219,14 @@ def linode_with_disk_encryption(test_linode_client, request): disk_encryption = request.param - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", target_region, image="linode/ubuntu24.10", label=label, booted=False, disk_encryption=disk_encryption, + root_pass="aComplex@Password123", ) yield linode_instance @@ -266,8 +274,12 @@ def test_linode_rebuild(test_linode_client): label = get_test_label() + "_rebuild" - linode, password = client.linode.instance_create( - "g6-nanode-1", region, image="linode/debian12", label=label + linode = client.linode.instance_create( + "g6-nanode-1", + region, + image="linode/debian12", + label=label, + root_pass="aComplex@Password123", ) wait_for_condition(10, 100, get_status, linode, "running") @@ -276,6 +288,7 @@ def test_linode_rebuild(test_linode_client): 3, linode.rebuild, "linode/debian12", + root_pass="aComplex@Password123", disk_encryption=InstanceDiskEncryptionType.disabled, ) @@ -322,11 +335,12 @@ def test_delete_linode(test_linode_client): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label() - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label + "_linode", + root_pass="aComplex@Password123", ) linode_instance.delete() @@ -595,12 +609,13 @@ def test_linode_initate_migration(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label() + "_migration" - linode, _ = client.linode.instance_create( + linode = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) # Says it could take up to ~6 hrs for migration to fully complete @@ -626,7 +641,7 @@ def test_linode_upgrade_interfaces( linode_for_legacy_interface_tests, linode_and_vpc_for_legacy_interface_tests_offline, ): - vpc, subnet, linode, _ = linode_and_vpc_for_legacy_interface_tests_offline + vpc, subnet, linode = linode_and_vpc_for_legacy_interface_tests_offline config = linode.configs[0] new_interfaces = [ @@ -909,9 +924,7 @@ def test_create_vpc( test_linode_client, linode_and_vpc_for_legacy_interface_tests_offline, ): - vpc, subnet, linode, _ = ( - linode_and_vpc_for_legacy_interface_tests_offline - ) + vpc, subnet, linode = linode_and_vpc_for_legacy_interface_tests_offline config: Config = linode.configs[0] @@ -1019,9 +1032,7 @@ def test_update_vpc( self, linode_and_vpc_for_legacy_interface_tests_offline, ): - vpc, subnet, linode, _ = ( - linode_and_vpc_for_legacy_interface_tests_offline - ) + vpc, subnet, linode = linode_and_vpc_for_legacy_interface_tests_offline config: Config = linode.configs[0] @@ -1082,7 +1093,7 @@ def test_reorder(self, linode_for_legacy_interface_tests): def test_delete_interface_containing_vpc( self, create_vpc_with_subnet_and_linode ): - vpc, subnet, linode, _ = create_vpc_with_subnet_and_linode + vpc, subnet, linode = create_vpc_with_subnet_and_linode config: Config = linode.configs[0] @@ -1116,12 +1127,13 @@ def test_create_linode_with_maintenance_policy(test_linode_client): non_default_policy = next((p for p in policies if not p.is_default), None) assert non_default_policy, "No non-default maintenance policy available" - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label + "_with_policy", maintenance_policy=non_default_policy.slug, + root_pass="aComplex@Password123", ) assert linode_instance.id is not None diff --git a/test/integration/models/lock/test_lock.py b/test/integration/models/lock/test_lock.py index f2139a176..31f89b992 100644 --- a/test/integration/models/lock/test_lock.py +++ b/test/integration/models/lock/test_lock.py @@ -18,12 +18,13 @@ def linode_for_lock(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label(length=8) - linode_instance, _ = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/debian12", label=label, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) yield linode_instance diff --git a/test/integration/models/networking/test_networking.py b/test/integration/models/networking/test_networking.py index 27ffbb444..47eeaf0e6 100644 --- a/test/integration/models/networking/test_networking.py +++ b/test/integration/models/networking/test_networking.py @@ -37,11 +37,12 @@ def create_linode_func(test_linode_client): label = get_test_label() - linode_instance, _ = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", TEST_REGION, image="linode/debian12", label=label, + root_pass="aComplex@Password123", ) return linode_instance @@ -220,7 +221,7 @@ def test_ip_addresses_unshare( def test_ip_info_vpc(test_linode_client, create_vpc_with_subnet_and_linode): - vpc, subnet, linode, _ = create_vpc_with_subnet_and_linode + vpc, subnet, linode = create_vpc_with_subnet_and_linode config: Config = linode.configs[0] diff --git a/test/integration/models/nodebalancer/test_nodebalancer.py b/test/integration/models/nodebalancer/test_nodebalancer.py index 692efb027..039259c68 100644 --- a/test/integration/models/nodebalancer/test_nodebalancer.py +++ b/test/integration/models/nodebalancer/test_nodebalancer.py @@ -33,13 +33,14 @@ def linode_with_private_ip(test_linode_client, e2e_test_firewall): client = test_linode_client label = get_test_label(8) - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", TEST_REGION, image="linode/debian12", label=label, private_ip=True, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) yield linode_instance diff --git a/test/integration/models/sharegroups/test_sharegroups.py b/test/integration/models/sharegroups/test_sharegroups.py index 9c66bad90..a7dc6f628 100644 --- a/test/integration/models/sharegroups/test_sharegroups.py +++ b/test/integration/models/sharegroups/test_sharegroups.py @@ -41,11 +41,12 @@ def sample_linode(test_linode_client, e2e_test_firewall): region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core") label = get_test_label(length=8) - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", region, image="linode/alpine3.19", label=label + "_modlinode", + root_pass="aComplex@Password123", ) yield linode_instance linode_instance.delete() diff --git a/test/integration/models/volume/test_blockstorage.py b/test/integration/models/volume/test_blockstorage.py index 8dac88e18..91a152409 100644 --- a/test/integration/models/volume/test_blockstorage.py +++ b/test/integration/models/volume/test_blockstorage.py @@ -8,11 +8,12 @@ def test_config_create_with_extended_volume_limit(test_linode_client): region = get_region(client, {"Linodes", "Block Storage"}, site_type="core") label = get_test_label() - linode, _ = client.linode.instance_create( + linode = client.linode.instance_create( "g6-standard-6", region, image="linode/debian12", label=label, + root_pass="aComplex@Password123", ) volumes = [ diff --git a/test/integration/models/volume/test_volume.py b/test/integration/models/volume/test_volume.py index 56395d203..7f9045e2e 100644 --- a/test/integration/models/volume/test_volume.py +++ b/test/integration/models/volume/test_volume.py @@ -46,12 +46,13 @@ def linode_for_volume(test_linode_client, e2e_test_firewall): label = get_test_label(length=8) - linode_instance, password = client.linode.instance_create( + linode_instance = client.linode.instance_create( "g6-nanode-1", TEST_REGION, image="linode/debian12", label=label, firewall=e2e_test_firewall, + root_pass="aComplex@Password123", ) yield linode_instance diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py index e82f3562d..a90190839 100644 --- a/test/unit/linode_client_test.py +++ b/test/unit/linode_client_test.py @@ -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( + l = 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 = 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 = 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 = 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) @@ -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, }, ) diff --git a/test/unit/objects/linode_test.py b/test/unit/objects/linode_test.py index 40bbb5069..5960e41d4 100644 --- a/test/unit/objects/linode_test.py +++ b/test/unit/objects/linode_test.py @@ -88,30 +88,62 @@ def test_transfer(self): def test_rebuild(self): """ - Tests that you can rebuild with an image + Tests that you can rebuild with an image and root_pass """ linode = Instance(self.client, 123) with self.mock_post("/linode/instances/123") as m: - pw = linode.rebuild( + linode.rebuild( "linode/debian9", + root_pass="aComplex@Password123", disk_encryption=InstanceDiskEncryptionType.enabled, ) - self.assertIsNotNone(pw) - self.assertTrue(isinstance(pw, str)) - self.assertEqual(m.call_url, "/linode/instances/123/rebuild") self.assertEqual( m.call_data, { "image": "linode/debian9", - "root_pass": pw, + "root_pass": "aComplex@Password123", "disk_encryption": "enabled", }, ) + def test_rebuild_with_authorized_keys(self): + """ + Tests that you can rebuild with an image and authorized_keys only + """ + linode = Instance(self.client, 123) + + with self.mock_post("/linode/instances/123") as m: + result = linode.rebuild( + "linode/debian9", + authorized_keys="ssh-rsa AAAA", + ) + + self.assertTrue(result) + + self.assertEqual(m.call_url, "/linode/instances/123/rebuild") + + self.assertEqual( + m.call_data, + { + "image": "linode/debian9", + "authorized_keys": ["ssh-rsa AAAA"], + }, + ) + + def test_rebuild_requires_auth(self): + """ + Tests that rebuild raises ValueError when neither root_pass nor + authorized_keys is provided + """ + linode = Instance(self.client, 123) + + with self.assertRaises(ValueError): + linode.rebuild("linode/debian9") + def test_available_backups(self): """ Tests that a Linode can retrieve its own backups @@ -437,11 +469,12 @@ def test_create_disk(self): linode = Instance(self.client, 123) with self.mock_post("/linode/instances/123/disks/12345") as m: - disk, gen_pass = linode.disk_create( + disk = linode.disk_create( 1234, label="test", authorized_users=["test"], image="linode/debian12", + root_pass="aComplex@Password123", ) self.assertEqual(m.call_url, "/linode/instances/123/disks") self.assertEqual( @@ -449,7 +482,7 @@ def test_create_disk(self): { "size": 1234, "label": "test", - "root_pass": gen_pass, + "root_pass": "aComplex@Password123", "image": "linode/debian12", "authorized_users": ["test"], "read_only": False, @@ -459,6 +492,47 @@ def test_create_disk(self): assert disk.id == 12345 assert disk.disk_encryption == InstanceDiskEncryptionType.disabled + def test_create_disk_with_authorized_keys(self): + """ + Tests that disk_create works with authorized_keys and no root_pass + """ + linode = Instance(self.client, 123) + + with self.mock_post("/linode/instances/123/disks/12345") as m: + disk = linode.disk_create( + 1234, + label="test", + image="linode/debian12", + authorized_keys="ssh-rsa AAAA", + ) + self.assertEqual(m.call_url, "/linode/instances/123/disks") + self.assertEqual( + m.call_data, + { + "size": 1234, + "label": "test", + "image": "linode/debian12", + "authorized_keys": ["ssh-rsa AAAA"], + "read_only": False, + }, + ) + + assert disk.id == 12345 + + def test_create_disk_with_image_requires_auth(self): + """ + Tests that disk_create raises ValueError when image is provided + without root_pass or authorized_keys + """ + linode = Instance(self.client, 123) + + with self.assertRaises(ValueError): + linode.disk_create( + 1234, + label="test", + image="linode/debian12", + ) + def test_get_placement_group(self): """ Tests that you can get the placement group for a Linode