LIBCLOUD-392: Add support for keypairs management in the OpenStack driver. Signed-off-by: Tomaz Muraus <[email protected]>
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/aadbd4c3 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/aadbd4c3 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/aadbd4c3 Branch: refs/heads/trunk Commit: aadbd4c3eeadd391262cda664303e2f3d5564eed Parents: 63a5b8d Author: schaubl <[email protected]> Authored: Tue Sep 10 11:03:26 2013 +0200 Committer: Tomaz Muraus <[email protected]> Committed: Wed Sep 25 21:10:21 2013 +0200 ---------------------------------------------------------------------- libcloud/compute/drivers/openstack.py | 117 +++++++++++++++++++ .../fixtures/openstack_v1.1/_os_keypairs.json | 18 +++ .../openstack_v1.1/_os_keypairs_create.json | 9 ++ .../_os_keypairs_create_import.json | 8 ++ libcloud/test/compute/test_openstack.py | 65 ++++++++++- 5 files changed, 216 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/aadbd4c3/libcloud/compute/drivers/openstack.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/openstack.py b/libcloud/compute/drivers/openstack.py index 9cbaa5e..875094d 100644 --- a/libcloud/compute/drivers/openstack.py +++ b/libcloud/compute/drivers/openstack.py @@ -28,6 +28,7 @@ from libcloud.utils.py3 import b from libcloud.utils.py3 import next from libcloud.utils.py3 import urlparse +import os import base64 from xml.etree import ElementTree as ET @@ -1121,6 +1122,43 @@ class OpenStackSecurityGroupRule(object): self.to_port)) +class OpenStackKeyPair(object): + """ + A KeyPair. + """ + + def __init__(self, name, fingerprint, public_key, driver, private_key=None, + extra=None): + """ + Constructor. + + @keyword name: Name of the KeyPair. + @type name: C{str} + + @keyword fingerprint: Fingerprint of the KeyPair + @type fingerprint: C{str} + + @keyword public_key: Public key in OpenSSH format. + @type public_key: C{str} + + @keyword private_key: Private key in PEM format. + @type private_key: C{str} + + @keyword extra: Extra attributes associated with this KeyPair. + @type extra: C{dict} + """ + self.name = name + self.fingerprint = fingerprint + self.public_key = public_key + self.private_key = private_key + self.driver = driver + self.extra = extra or {} + + def __repr__(self): + return ('<OpenStackKeyPair name=%s fingerprint=%s public_key=%s ...>' + % (self.name, self.fingerprint, self.public_key)) + + class OpenStack_1_1_Connection(OpenStackComputeConnection): responseCls = OpenStack_1_1_Response accept_format = 'application/json' @@ -1636,6 +1674,85 @@ class OpenStack_1_1_NodeDriver(OpenStackNodeDriver): (rule.id), method='DELETE') return resp.status == httplib.NO_CONTENT + def _to_keypairs(self, obj): + keypairs = obj['keypairs'] + return [self._to_keypair(keypair['keypair']) for keypair in keypairs] + + def _to_keypair(self, obj): + return OpenStackKeyPair(name=obj['name'], + fingerprint=obj['fingerprint'], + public_key=obj['public_key'], + private_key=obj.get('private_key', None), + driver=self) + + def ex_list_keypairs(self): + """ + Get a list of KeyPairs that are available. + + @rtype: C{list} of L{OpenStackKeyPair} + """ + return self._to_keypairs( + self.connection.request('/os-keypairs').object) + + def ex_create_keypair(self, name): + """ + Create a new KeyPair + + @param name: Name of the new KeyPair + @type name: C{str} + + @rtype: L{OpenStackKeyPair} + """ + return self._to_keypair(self.connection.request( + '/os-keypairs', method='POST', + data={'keypair': {'name': name}} + ).object['keypair']) + + def ex_import_keypair(self, name, public_key_file): + """ + Import a KeyPair from a file + + @param name: Name of the new KeyPair + @type name: C{str} + + @param public_key_file: Path to the public key file (in OpenSSH format) + @type public_key_file: C{str} + + @rtype: L{OpenStackKeyPair} + """ + public_key = open(os.path.expanduser(public_key_file), 'r').read() + return self.ex_import_keypair_from_string(name, public_key) + + def ex_import_keypair_from_string(self, name, public_key): + """ + Import a KeyPair from a string + + @param name: Name of the new KeyPair + @type name: C{str} + + @param public_key: Public key (in OpenSSH format) + @type public_key: C{str} + + @rtype: L{OpenStackKeyPair} + """ + return self._to_keypair(self.connection.request( + '/os-keypairs', method='POST', + data={'keypair': {'name': name, 'public_key': public_key}} + ).object['keypair']) + + def ex_delete_keypair(self, keypair): + """ + Delete a KeyPair. + + @param keypair: KeyPair to delete + @type keypair: L{OpenStackKeyPair} + + @rtype: C{bool} + """ + resp = self.connection.request('/os-keypairs/%s' % (keypair.name), + method='DELETE') + return resp.status == httplib.ACCEPTED + def ex_get_size(self, size_id): """ Get a NodeSize http://git-wip-us.apache.org/repos/asf/libcloud/blob/aadbd4c3/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs.json b/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs.json new file mode 100644 index 0000000..dff7164 --- /dev/null +++ b/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs.json @@ -0,0 +1,18 @@ +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "22:0e:d6:f7:bd:5e:ee:49:cf:1f:10:d5:9c:a8:35:64", + "name": "key1", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC/ePvJuMEOc90gidxWN+8lYekv+S6j8SJhcQRBjE5DVs/M+3VXyJTQc6fguUS9c7o8GZXpP/0dwbVa9y76HeZs6In+XE1egoUyz4zLHQ5jUepFeekChpSlo6yQWI2SHUxJOshqPLOEU1XlrwvN0h5FcXGVV0x6DJgLZuCRS7oIxQ== Generated by Nova\n" + } + }, + { + "keypair": { + "fingerprint": "5d:66:33:ae:99:0f:fb:cb:86:f2:bc:ae:53:99:b6:ed", + "name": "key2", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCz5sy4u8KwAPAMPr+4bEMlU6BwpSD6eZVokwMclojqIz9nKAvQD9AEw/6ok9Xsn0oixBrCoW2HYsXIiUziufzheoGsZIzuj3D7Rpbtrft53FtICe5UtQrOo3WJb8bvbzpDDd7xYlb9PpQTXoxInzjgBW+Ox6OODx2NazTk7PHZDQ== Generated by Nova\n" + } + } + ] +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/aadbd4c3/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs_create.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs_create.json b/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs_create.json new file mode 100644 index 0000000..fbfdbb9 --- /dev/null +++ b/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs_create.json @@ -0,0 +1,9 @@ +{ + "keypair": { + "fingerprint": "80:f8:03:a7:8e:c1:c3:b1:7e:c5:8c:50:04:5e:1c:5b", + "name": "key0", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgQDPC4MDHBbUjeGZ4pK5svGxkFHJFdDatpMAYcW/fyDxsMbyiHnu\nUOxB0WJupUQd4tc7B8+MNOLzcZVQkUjIhhkb5qCbjcoOqzb59owtNCSi7TleaC6w\n15j1LJb3zdHVxEhGJ19I95DhOtiFRHp2Ik3bYV6p+uv0sQxfaqw3q5M3+QIDAQAB\nAoGAW2LqZfH9Bb7GSEUgnESmt8hKwSYW9KLHidCeFyNG6Ect2RlyMEWZsod4Gfxq\nb4KTm6Ob8XfagLeuv0wRQyklZUbyb4aurfn4hX0cpkxSPAVar8uG/0TJY1wswxfo\nkReZCq7CQFlt7w3Y1RHZyXo/inyAxohi393trVhIGAqdXp0CQQDt7/GeI5QWKjYj\nwe5kFTRowVJ+y61MP237Bz+YF5+pq28ikdLAMzdDOyd3LJTnBGJ/DK1ksfJDCSue\nEgdifYJrAkEA3sM1fRQB/PyyyCR1DcZGlOfc/OBCSG4aTMYOK+g0PnibKPj5wS6q\nuK8w1q+0CztpgKsmEtQ+H7H8Fva81S7wKwJANY7tNEuN6e9WgHYG00Byq6HYj/II\n8EDW4Mqg5ftrVSXhvkZUyi69IcUO/SRr4BR8l1yjKydjAPPvfYVRZDocQQJASHXr\nQkJt2yM/7IafZNmoP+ukIMW6CeF2wJ50IagoxmFo500FwOczNVwXYN5KjJTI3sfN\nXLaZdqnovHeKOTZJfQJAZ2HBnmgsLoFE6ONF492TXIs7JxJr8z4QUp1AXGUXcZmy\njuL3b9XW6K908Ev8uTSNzRo6TyGuYKGllp10K6A3bA==\n-----END RSA PRIVATE KEY-----\n", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDPC4MDHBbUjeGZ4pK5svGxkFHJFdDatpMAYcW/fyDxsMbyiHnuUOxB0WJupUQd4tc7B8+MNOLzcZVQkUjIhhkb5qCbjcoOqzb59owtNCSi7TleaC6w15j1LJb3zdHVxEhGJ19I95DhOtiFRHp2Ik3bYV6p+uv0sQxfaqw3q5M3+Q== Generated by Nova\n", + "user_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/aadbd4c3/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs_create_import.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs_create_import.json b/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs_create_import.json new file mode 100644 index 0000000..56f0958 --- /dev/null +++ b/libcloud/test/compute/fixtures/openstack_v1.1/_os_keypairs_create_import.json @@ -0,0 +1,8 @@ +{ + "keypair": { + "fingerprint": "97:10:a6:e7:92:65:7e:69:fe:e6:81:8f:39:3c:8f:5a", + "name": "key3", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzTJr5BNSlTIDFsVY3zUJtbbcPsWbw7XDE/eXRQ+704790ARKKvE3FsERqdMZvwcx1osR0sGVdpgAiV/z5iEb5z2juQp7yQJHePiEnfHTH99NVJN+Y1BztchRoz224IaP987bN+fd8Pl/O1YDCyw+bX5zI/ekCC9z8fTdI2l1AbTnKVn7UjZBjKZi1uPMaH016fp039pIOtkjvIgDWjeGwOiJjY1vzaX3nxQje4kprEZ4FKk4yyG61qveBZr+/0Xq6ocNOYUSpB29AZ0IcfJa7P3yMxVRzSS9aN0fmrlf3kIFkVAy45A83GfZpiMxo/ulTaO9+tTSwulZP+0bxkCkn dummycomment\n", + "user_id": "dbdf4c6cab0c4ae78bef0bcdb03c2440" + } +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/aadbd4c3/libcloud/test/compute/test_openstack.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/test_openstack.py b/libcloud/test/compute/test_openstack.py index 5512167..5de21fb 100644 --- a/libcloud/test/compute/test_openstack.py +++ b/libcloud/test/compute/test_openstack.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import sys import unittest import datetime @@ -39,7 +40,7 @@ from libcloud.compute.drivers.openstack import ( OpenStack_1_0_NodeDriver, OpenStack_1_0_Response, OpenStack_1_1_NodeDriver, OpenStackSecurityGroup, OpenStackSecurityGroupRule, OpenStack_1_1_FloatingIpPool, - OpenStack_1_1_FloatingIpAddress + OpenStack_1_1_FloatingIpAddress, OpenStackKeyPair ) from libcloud.compute.base import Node, NodeImage, NodeSize, StorageVolume from libcloud.pricing import set_pricing, clear_pricing_data @@ -1142,6 +1143,49 @@ class OpenStack_1_1_Tests(unittest.TestCase, TestCaseMixin): result = self.driver.ex_delete_security_group_rule(security_group_rule) self.assertTrue(result) + def test_ex_list_keypairs(self): + keypairs = self.driver.ex_list_keypairs() + self.assertEqual(len(keypairs), 2, 'Wrong keypairs count') + keypair = keypairs[1] + self.assertEqual(keypair.name, 'key2') + self.assertEqual(keypair.fingerprint, '5d:66:33:ae:99:0f:fb:cb:86:f2:bc:ae:53:99:b6:ed') + self.assertEqual(keypair.public_key, 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCz5sy4u8KwAPAMPr+4bEMlU6BwpSD6eZVokwMclojqIz9nKAvQD9AEw/6ok9Xsn0oixBrCoW2HYsXIiUziufzheoGsZIzuj3D7Rpbtrft53FtICe5UtQrOo3WJb8bvbzpDDd7xYlb9PpQTXoxInzjgBW+Ox6OODx2NazTk7PHZDQ== Generated by Nova\n') + self.assertEqual(keypair.private_key, None) + + def test_ex_create_keypair(self): + name = 'key0' + keypair = self.driver.ex_create_keypair(name) + self.assertEqual(keypair.name, name) + self.assertEqual(keypair.fingerprint, '80:f8:03:a7:8e:c1:c3:b1:7e:c5:8c:50:04:5e:1c:5b') + self.assertEqual(keypair.public_key, 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDPC4MDHBbUjeGZ4pK5svGxkFHJFdDatpMAYcW/fyDxsMbyiHnuUOxB0WJupUQd4tc7B8+MNOLzcZVQkUjIhhkb5qCbjcoOqzb59owtNCSi7TleaC6w15j1LJb3zdHVxEhGJ19I95DhOtiFRHp2Ik3bYV6p+uv0sQxfaqw3q5M3+Q== Generated by Nova\n') + self.assertEqual(keypair.private_key, '-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgQDPC4MDHBbUjeGZ4pK5svGxkFHJFdDatpMAYcW/fyDxsMbyiHnu\nUOxB0WJupUQd4tc7B8+MNOLzcZVQkUjIhhkb5qCbjcoOqzb59owtNCSi7TleaC6w\n15j1LJb3zdHVxEhGJ19I95DhOtiFRHp2Ik3bYV6p+uv0sQxfaqw3q5M3+QIDAQAB\nAoGAW2LqZfH9Bb7GSEUgnESmt8hKwSYW9KLHidCeFyNG6Ect2RlyMEWZsod4Gfxq\nb4KTm6Ob8XfagLeuv0wRQyklZUbyb4aurfn4hX0cpkxSPAVar8uG/0TJY1wswxfo\nkReZCq7CQFlt7w3Y1RHZyXo/inyAxohi393trVhIGAqdXp0CQQDt7/GeI5QWKjYj\nwe5kFTRowVJ+y61MP237Bz+YF5+pq28ikdLAMzdDOyd3LJTnBGJ/DK1ksfJDCSue\nEgdifYJrAkEA3sM1fRQB/PyyyCR1DcZGlOfc/OBCSG4aTMYOK+g0PnibKPj5wS6q\nuK8w1q+0CztpgKsmEtQ+H7H8Fva81S7wKwJANY7tNEuN6e9WgHYG00Byq6HYj/II\n8EDW4Mqg5ftrVSXhvkZUyi69IcUO/SRr4BR8l1yjKydjAPPvfYVRZDocQQJASHXr\nQkJt2yM/7IafZNmoP+ukIMW6CeF2wJ50IagoxmFo500FwOczNVwXYN5KjJTI3sfN\nXLaZdqnovHeKOTZJfQJAZ2HBnmgsLoFE6ONF492TXIs7JxJr8z4QUp1AXGUXcZmy\njuL3b9XW6K908Ev8uTSNzRo6TyGuYKGllp10K6A3bA==\n-----END RSA PRIVATE KEY-----\n') + + def test_ex_import_keypair(self): + name = 'key3' + path = os.path.join(os.path.dirname(__file__), "fixtures", "misc", "dummy_rsa.pub") + pub_key = open(path, 'r').read() + keypair = self.driver.ex_import_keypair(name, path) + self.assertEqual(keypair.name, name) + self.assertEqual(keypair.fingerprint, '97:10:a6:e7:92:65:7e:69:fe:e6:81:8f:39:3c:8f:5a') + self.assertEqual(keypair.public_key, pub_key) + self.assertEqual(keypair.private_key, None) + + + def test_ex_import_keypair_from_string(self): + name = 'key3' + path = os.path.join(os.path.dirname(__file__), "fixtures", "misc", "dummy_rsa.pub") + pub_key = open(path, 'r').read() + keypair = self.driver.ex_import_keypair_from_string(name, pub_key) + self.assertEqual(keypair.name, name) + self.assertEqual(keypair.fingerprint, '97:10:a6:e7:92:65:7e:69:fe:e6:81:8f:39:3c:8f:5a') + self.assertEqual(keypair.public_key, pub_key) + self.assertEqual(keypair.private_key, None) + + def test_ex_delete_keypair(self): + keypair = OpenStackKeyPair(name='key1', fingerprint=None, public_key=None, driver=self.driver) + result = self.driver.ex_delete_keypair(keypair) + self.assertTrue(result) + def test_ex_list_floating_ip_pools(self): ret = self.driver.ex_list_floating_ip_pools() self.assertEqual(ret[0].name, 'public') @@ -1404,6 +1448,25 @@ class OpenStack_1_1_MockHttp(MockHttpTestCase): else: raise NotImplementedError() + def _v1_1_slug_os_keypairs(self, method, url, body, headers): + if method == "GET": + body = self.fixtures.load('_os_keypairs.json') + elif method == "POST": + if 'public_key' in body: + body = self.fixtures.load('_os_keypairs_create_import.json') + else: + body = self.fixtures.load('_os_keypairs_create.json') + else: + raise NotImplementedError() + + return (httplib.OK, body, self.json_content_headers, httplib.responses[httplib.OK]) + + def _v1_1_slug_os_keypairs_key1(self, method, url, body, headers): + if method == "DELETE": + return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED]) + else: + raise NotImplementedError() + def _v1_1_slug_os_volumes(self, method, url, body, headers): if method == "GET": body = self.fixtures.load('_os_volumes.json')
