Skip to content

Commit

Permalink
Updated Linode (Akamai Connected Cloud) support (including cloud-init)
Browse files Browse the repository at this point in the history
1. cloud-init support is a new feature available in 2023.
2. The main entry point (create_node) had an arrangement of non-keyword parameters that were not consistent with other libcloud drivers. This has been fixed.
3. One remaining function (already available in the API) was exposed to also be consistent with other drivers.
  • Loading branch information
mraygalaxy2 committed Sep 13, 2023
1 parent 78103f9 commit bf2f681
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 16 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion docs/compute/_supported_methods_image_management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/compute/_supported_methods_key_pair_management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/upgrade_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------

Expand Down
80 changes: 77 additions & 3 deletions libcloud/compute/drivers/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from libcloud.utils.py3 import httplib
from libcloud.compute.base import (
Node,
KeyPair,
NodeSize,
NodeImage,
NodeDriver,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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`
"""
Expand All @@ -1014,6 +1061,9 @@ def create_node(
"backups_enabled": ex_backups_enabled,
}

if ex_userdata:
attr["metadata"] = {"user_data": ex_userdata}

if image is not None:
if root_pass is None:
raise LinodeExceptionV4("root password required " "when providing an image")
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"],
Expand Down
6 changes: 6 additions & 0 deletions libcloud/test/compute/fixtures/linode_v4/create_key_pair.json
Original file line number Diff line number Diff line change
@@ -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"
}
13 changes: 13 additions & 0 deletions libcloud/test/compute/fixtures/linode_v4/list_key_pairs.json
Original file line number Diff line number Diff line change
@@ -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
}
43 changes: 32 additions & 11 deletions libcloud/test/compute/test_linode_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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))

Expand Down Expand Up @@ -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"],
Expand All @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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])
Expand Down

0 comments on commit bf2f681

Please sign in to comment.