diff --git a/CHANGES.rst b/CHANGES.rst index fbf2386d2a..228acb4368 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -67,6 +67,11 @@ Compute (#1906) [Ross Vandegrift - @rvandegrift] +- [LINODE] Add support for cloud-init metadata support to create_node() + Add new functions ``create_key_pair``, ``list_key_pairs``, and ``get_image`` + (#1946) + [Michael Galaxy - @mraygalaxy2] + Storage ~~~~~~~ diff --git a/docs/compute/_supported_methods_image_management.rst b/docs/compute/_supported_methods_image_management.rst index c121c6ba7f..7c0bdf9eda 100644 --- a/docs/compute/_supported_methods_image_management.rst +++ b/docs/compute/_supported_methods_image_management.rst @@ -28,7 +28,7 @@ Provider list images get image create image delete image c `KTUCloud`_ yes no no no no `kubevirt`_ yes no no no no `Libvirt`_ no no no no no -`Linode`_ yes no yes yes no +`Linode`_ yes yes yes yes no `Maxihost`_ yes no no no no `Nimbus`_ yes yes yes yes yes `NTTAmerica`_ yes no no no no diff --git a/docs/compute/_supported_methods_key_pair_management.rst b/docs/compute/_supported_methods_key_pair_management.rst index ef255a28fa..ddfed43900 100644 --- a/docs/compute/_supported_methods_key_pair_management.rst +++ b/docs/compute/_supported_methods_key_pair_management.rst @@ -28,7 +28,7 @@ Provider list key pairs get key pair create key pair impor `KTUCloud`_ yes yes yes yes no yes `kubevirt`_ no no no no no no `Libvirt`_ no no no no no no -`Linode`_ no no no no no no +`Linode`_ yes no yes no no no `Maxihost`_ yes no yes no no no `Nimbus`_ yes yes yes yes no yes `NTTAmerica`_ no no no no no no diff --git a/docs/upgrade_notes.rst b/docs/upgrade_notes.rst index 0f7fc359d4..d97cdaca24 100644 --- a/docs/upgrade_notes.rst +++ b/docs/upgrade_notes.rst @@ -5,6 +5,12 @@ This page describes how to upgrade from a previous version to a new version which contains backward incompatible or semi-incompatible changes and how to preserve the old behavior when this is possible. +Libcloud 3.8.0 +-------------- +- [LINODE API v4] Order of arguments to create_node() was changed. The order of the + arguments for name and size were not consistent with the rest of the codebase. + This is possibly a breaking change for anyone using a previous version. + Libcloud 3.7.0 -------------- diff --git a/libcloud/compute/drivers/linode.py b/libcloud/compute/drivers/linode.py index bd213935d0..0b08bca47e 100644 --- a/libcloud/compute/drivers/linode.py +++ b/libcloud/compute/drivers/linode.py @@ -37,6 +37,7 @@ from libcloud.utils.py3 import httplib from libcloud.compute.base import ( Node, + KeyPair, NodeSize, NodeImage, NodeDriver, @@ -861,6 +862,33 @@ def list_images(self): data = self._paginated_request("/v4/images", "data") return [self._to_image(obj) for obj in data] + def create_key_pair(self, name, public_key=""): + """ + Creates an SSH keypair + + :param name: The name to be given to the keypair (required).\ + :type name: `str` + + :keyword public_key: Contents of the public key the the SSH key pair + :type public_key: `str` + + :rtype: :class: `KeyPair` + """ + attr = {"label": name, "ssh_key": public_key} + response = self.connection.request( + "/v4/profile/sshkeys", data=json.dumps(attr), method="POST" + ).object + return self._to_key_pair(response) + + def list_key_pairs(self): + """ + Provide a list of all the SSH keypairs in your account. + + :rtype: ``list`` of :class: `KeyPair` + """ + data = self._paginated_request("/v4/profile/sshkeys", "data") + return [self._to_key_pair(obj) for obj in data] + def list_locations(self): """ Lists the Regions available for Linode services @@ -945,15 +973,28 @@ def reboot_node(self, node): def create_node( self, location, - size, - image=None, - name=None, + # Previously, the following 3 parameters did not match the rest of the libcloud + # codebase drivers. They should be in the same order as other compute drivers. + # Previously, it looked like this: + # size, + # image=None, + # name=None, + # + # Comments welcome on how backwards compatibility (if any) should work here. + # Since it was not compatible with other drivers, it is not clear to me if this + # would break anyone's codebase if they were not using any other libcloud drivers + # to other cloud providers in the first place. If they were not, that seems to + # kind of defeat the purpose of using libcloud. + name, # Can be None + size, # Can be None + image, # Can be None root_pass=None, ex_authorized_keys=None, ex_authorized_users=None, ex_tags=None, ex_backups_enabled=False, ex_private_ip=False, + ex_userdata=False, ): """Creates a Linode Instance. In order for this request to complete successfully, @@ -997,6 +1038,12 @@ def create_node( :keyword ex_private_ip: whether or not to request a private IP :type ex_private_ip: ``bool`` + :keyword ex_userdata: add cloud-config compatible userdata to be + processed by cloud-init inside the Linode instance. NOTE: the + contents of this string must be base64 encoded before passing + it to this function. + :type ex_userdata: ``str`` + :return: Node representing the newly-created node :rtype: :class:`Node` """ @@ -1014,6 +1061,9 @@ def create_node( "backups_enabled": ex_backups_enabled, } + if ex_userdata: + attr["metadata"] = {"user_data": binascii.b2a_base64(bytes(ex_userdata.encode("utf-8"))).decode("ascii").strip()} + if image is not None: if root_pass is None: raise LinodeExceptionV4("root password required " "when providing an image") @@ -1369,6 +1419,18 @@ def ex_get_volume(self, volume_id): response = self.connection.request("/v4/volumes/%s" % volume_id).object return self._to_volume(response) + def get_image(self, image): + """ + Lookup a Linode image + + :param image: The name to image to be looked up (required).\ + :type name: `str` + + :rtype: :class: `NodeImage` + """ + response = self.connection.request("/v4/images/%s" % image, method="GET") + return self._to_image(response.object) + def create_image(self, disk, name=None, description=None): """Creates a private image from a LinodeDisk. Images are limited to three per account. @@ -1554,6 +1616,18 @@ def ex_rename_node(self, node, name): return self._to_node(response) + def _to_key_pair(self, data): + extra = {"id": data["id"]} + + return KeyPair( + name=data["label"], + fingerprint=None, + public_key=data["ssh_key"], + private_key=None, + driver=self, + extra=extra, + ) + def _to_node(self, data): extra = { "tags": data["tags"], diff --git a/libcloud/test/compute/fixtures/linode_v4/create_key_pair.json b/libcloud/test/compute/fixtures/linode_v4/create_key_pair.json new file mode 100644 index 0000000000..58572e4dd6 --- /dev/null +++ b/libcloud/test/compute/fixtures/linode_v4/create_key_pair.json @@ -0,0 +1,6 @@ +{ + "created": "2018-01-01T00:01:01", + "id": 42, + "label": "My SSH Key", + "ssh_key": "ssh-rsa AAAA_valid_public_ssh_key_123456785== user@their-computer" +} diff --git a/libcloud/test/compute/fixtures/linode_v4/list_key_pairs.json b/libcloud/test/compute/fixtures/linode_v4/list_key_pairs.json new file mode 100644 index 0000000000..c476b05bd7 --- /dev/null +++ b/libcloud/test/compute/fixtures/linode_v4/list_key_pairs.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "created": "2018-01-01T00:01:01", + "id": 42, + "label": "My SSH Key", + "ssh_key": "ssh-rsa AAAA_valid_public_ssh_key_123456785== user@their-computer" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} diff --git a/libcloud/test/compute/test_linode_v4.py b/libcloud/test/compute/test_linode_v4.py index 01b7fb1449..c4c5c8dd5d 100644 --- a/libcloud/test/compute/test_linode_v4.py +++ b/libcloud/test/compute/test_linode_v4.py @@ -20,7 +20,7 @@ from libcloud.test import MockHttp from libcloud.utils.py3 import httplib from libcloud.common.types import LibcloudError, InvalidCredsError -from libcloud.compute.base import Node, NodeImage, NodeState, StorageVolume +from libcloud.compute.base import Node, KeyPair, NodeImage, NodeState, StorageVolume from libcloud.test.compute import TestCaseMixin from libcloud.common.linode import LinodeDisk, LinodeIPAddress, LinodeExceptionV4 from libcloud.test.file_fixtures import ComputeFileFixtures @@ -64,6 +64,19 @@ def test_list_images(self): self.assertIsInstance(image.extra["size"], int) self.assertTrue(image.extra["is_public"]) + def test_list_key_pairs(self): + keypairs = self.driver.list_key_pairs() + self.assertIsInstance(keypairs, list) + self.assertEqual(len(keypairs), 1) + self.assertEqual(keypairs[0].extra["id"], 42) + + def test_create_key_pair(self): + keypair = self.driver.create_key_pair( + "mykey", public_key="ssh-rsa AAAA_valid_public_ssh_key_123456785== user@their-computer" + ) + self.assertIsInstance(keypair, KeyPair) + self.assertEqual(keypair.extra["id"], 42) + def test_list_locations(self): locations = self.driver.list_locations() self.assertEqual(len(locations), 10) @@ -78,11 +91,11 @@ def test_create_node_response(self): image = self.driver.list_images()[0] location = self.driver.list_locations()[0] node = self.driver.create_node( - location=location, - name="node-name", + location, + "node-name", + size=size, image=image, root_pass="test123456", - size=size, ) self.assertTrue(isinstance(node, Node)) @@ -114,9 +127,9 @@ def test_create_node(self): node = self.driver.create_node( location, + "TestNode", size, image=image, - name="TestNode", root_pass="test123456", ex_backups_enabled=True, ex_tags=["testing123"], @@ -134,13 +147,13 @@ def test_create_node_no_root_pass(self): location = self.driver.list_locations()[0] with self.assertRaises(LinodeExceptionV4): - self.driver.create_node(location, size, image=image, name="TestNode") + self.driver.create_node(location, "TestNode", size, image=image) def test_create_node_no_image(self): size = self.driver.list_sizes()[0] location = self.driver.list_locations()[0] LinodeMockHttpV4.type = "NO_IMAGE" - node = self.driver.create_node(location, size, name="TestNode", ex_tags=["testing123"]) + node = self.driver.create_node(location, "TestNode", size, None, ex_tags=["testing123"]) self.assertIsNone(node.image) self.assertEqual(node.name, "TestNode") @@ -153,13 +166,13 @@ def test_create_node_invalid_name(self): location = self.driver.list_locations()[0] with self.assertRaises(LinodeExceptionV4): - self.driver.create_node(location, size, name="Test__Node") + self.driver.create_node(location, "Test__Node", size, None) with self.assertRaises(LinodeExceptionV4): - self.driver.create_node(location, size, name="Test Node") + self.driver.create_node(location, "Test Node", size, None) with self.assertRaises(LinodeExceptionV4): - self.driver.create_node(location, size, name="Test--Node") + self.driver.create_node(location, "Test--Node", size, None) with self.assertRaises(LinodeExceptionV4): - self.driver.create_node(location, size, name="Test..Node") + self.driver.create_node(location, "Test..Node", size, None) def test_reboot_node(self): node = Node("22344420", None, None, None, None, driver=self.driver) @@ -424,6 +437,14 @@ def test__paginated_request_two_pages(self): class LinodeMockHttpV4(MockHttp): fixtures = ComputeFileFixtures("linode_v4") + def _v4_profile_sshkeys(self, method, url, body, headers): + if method == "GET": + body = self.fixtures.load("list_key_pairs.json") + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + if method == "POST": + body = self.fixtures.load("create_key_pair.json") + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _v4_regions(self, method, url, body, headers): body = self.fixtures.load("list_locations.json") return (httplib.OK, body, {}, httplib.responses[httplib.OK])