You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by jo...@apache.org on 2013/08/15 13:09:52 UTC

git commit: Allow ec2 drivers to support the ssh-key feature.

Updated Branches:
  refs/heads/trunk 4f5e2ef14 -> 2d42e94da


Allow ec2 drivers to support the ssh-key feature.

This commit adds new utility functions for extracting fingerprints
from public keys. There are 2 versions of this. It is quite easy
to make an OpenSSH style fingerprint, but EC2 uses an SSH2 style
fingerprint (which has extra DER encoding applied to it before
hashing).

This commit allows you to import a public ssh key that you
have as a string (previously the key had to exist on disk).

You can now use ex_list_keypairs rather than ex_describe_all_keypairs.
The latter gives you a list of key names, you then need to do a
HTTP request for each keyname if you want to check their fingerprint.
The former returns a list of dictionaries with more info.
Therefore, ex_list_keypairs is much more efficient.

ex_describe_all_keypairs was reimplemented on top of
ex_list_keypairs, but I think it should be marked as deprecated.

A new ex_ method is now available to find or import a public key.
Given a public key it will find it on ec2 by its fingerprint. If
it is not available it is automatically imported.

The create_node method uses this method to lookup or import an
SSH key that is provided to it by a NodeAuthSSHKey (via auth arg).
This means ec2 now properly supports the ssh-key feature.


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/2d42e94d
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/2d42e94d
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/2d42e94d

Branch: refs/heads/trunk
Commit: 2d42e94da72ef1e8950697a6da22ed9e1b2f964f
Parents: 4f5e2ef
Author: John Carr <jo...@apache.org>
Authored: Thu Aug 15 12:07:19 2013 +0100
Committer: John Carr <jo...@apache.org>
Committed: Thu Aug 15 12:07:19 2013 +0100

----------------------------------------------------------------------
 CHANGES                                         |  5 ++
 libcloud/compute/drivers/ec2.py                 | 87 ++++++++++++++++----
 .../compute/fixtures/ec2/import_key_pair.xml    |  5 ++
 libcloud/test/compute/fixtures/misc/dummy_rsa   | 27 ++++++
 .../test/compute/fixtures/misc/dummy_rsa.pub    |  1 +
 libcloud/test/compute/test_ec2.py               | 35 ++++++--
 libcloud/utils/publickey.py                     | 68 +++++++++++++++
 7 files changed, 208 insertions(+), 20 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/2d42e94d/CHANGES
----------------------------------------------------------------------
diff --git a/CHANGES b/CHANGES
index c56c4d4..c2fc0e1 100644
--- a/CHANGES
+++ b/CHANGES
@@ -41,6 +41,11 @@ Changes with Apache Libcloud in development
       (LIBCLOUD-367)
       [John Carr]
 
+    - Update EC2 driver to accept the auth kwarg (it will accept NodeAuthSSH
+      objects and automatically import a public key that is not already
+      uploaded to the EC2 keyring). (Follow on from LIBCLOUD-367).
+      [John Carr]
+
     - Fix a bug in the ElasticHosts driver and check for right HTTP status
       code when determining drive imaging success. (LIBCLOUD-363)
       [Bob Thompson]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/2d42e94d/libcloud/compute/drivers/ec2.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py
index e9b5577..cc3fb98 100644
--- a/libcloud/compute/drivers/ec2.py
+++ b/libcloud/compute/drivers/ec2.py
@@ -29,6 +29,8 @@ from xml.etree import ElementTree as ET
 from libcloud.utils.py3 import b
 
 from libcloud.utils.xml import fixxpath, findtext, findattr, findall
+from libcloud.utils.publickey import get_pubkey_ssh2_fingerprint
+from libcloud.utils.publickey import get_pubkey_comment
 from libcloud.common.aws import AWSBaseResponse, SignedAWSConnection
 from libcloud.common.types import (InvalidCredsError, MalformedResponseError,
                                    LibcloudError)
@@ -422,6 +424,7 @@ class BaseEC2NodeDriver(NodeDriver):
     """
 
     connectionCls = EC2Connection
+    features = {'create_node': ['ssh_key']}
     path = '/'
 
     NODE_STATE_MAP = {
@@ -740,9 +743,9 @@ class BaseEC2NodeDriver(NodeDriver):
             'keyFingerprint': key_fingerprint,
         }
 
-    def ex_import_keypair(self, name, keyfile):
+    def ex_import_keypair_from_string(self, name, key_material):
         """
-        imports a new public key
+        imports a new public key where the public key is passed in as a string
 
         @note: This is a non-standard extension API, and only works for EC2.
 
@@ -750,15 +753,12 @@ class BaseEC2NodeDriver(NodeDriver):
          unique, otherwise an InvalidKeyPair.Duplicate exception is raised.
         @type       name: C{str}
 
-        @param     keyfile: The filename with path of the public key to import.
-        @type      keyfile: C{str}
+        @param     key_material: The contents of a public key file.
+        @type      key_material: C{str}
 
         @rtype: C{dict}
         """
-        with open(os.path.expanduser(keyfile)) as fh:
-            content = fh.read()
-
-        base64key = base64.b64encode(content)
+        base64key = base64.b64encode(key_material)
 
         params = {
             'Action': 'ImportKeyPair',
@@ -776,27 +776,76 @@ class BaseEC2NodeDriver(NodeDriver):
             'keyFingerprint': key_fingerprint,
         }
 
-    def ex_describe_all_keypairs(self):
+    def ex_import_keypair(self, name, keyfile):
         """
-        Describes all keypairs.
+        imports a new public key where the public key is passed via a filename
 
         @note: This is a non-standard extension API, and only works for EC2.
 
-        @rtype: C{list} of C{str}
+        @param      name: The name of the public key to import. This must be
+         unique, otherwise an InvalidKeyPair.Duplicate exception is raised.
+        @type       name: C{str}
+
+        @param     keyfile: The filename with path of the public key to import.
+        @type      keyfile: C{str}
+
+        @rtype: C{dict}
+        """
+        with open(os.path.expanduser(keyfile)) as fh:
+            content = fh.read()
+        return self.ex_import_keypair_from_string(name, content)
+
+    def ex_find_or_import_keypair_by_key_material(self, pubkey):
+        """
+        Given a public key, look it up in the EC2 KeyPair database. If it
+        exists, return any information we have about it. Otherwise, create it.
+
+        Keys that are created are named based on their comment and fingerprint.
+        """
+        key_fingerprint = get_pubkey_ssh2_fingerprint(pubkey)
+        key_comment = get_pubkey_comment(pubkey, default='unnamed')
+        key_name = "%s-%s" % (key_comment, key_fingerprint)
+
+        for keypair in self.ex_list_keypairs():
+            if keypair['keyFingerprint'] == key_fingerprint:
+                return keypair
+
+        return self.ex_import_keypair_from_string(key_name, pubkey)
+
+    def ex_list_keypairs(self):
         """
+        Lists all the keypair names and fingerprints.
 
+        @rtype: C{list} of C{dict}
+        """
         params = {
             'Action': 'DescribeKeyPairs'
         }
 
         response = self.connection.request(self.path, params=params).object
-        names = []
+        keypairs = []
         for elem in findall(element=response, xpath='keySet/item',
                             namespace=NAMESPACE):
-            name = findtext(element=elem, xpath='keyName', namespace=NAMESPACE)
-            names.append(name)
+            keypair = {
+                'keyName': findtext(element=elem, xpath='keyName',
+                                    namespace=NAMESPACE),
+                'keyFingerprint': findtext(element=elem,
+                                           xpath='keyFingerprint',
+                                           namespace=NAMESPACE).strip(),
+            }
+            keypairs.append(keypair)
 
-        return names
+        return keypairs
+
+    def ex_describe_all_keypairs(self):
+        """
+        Describes all keypairs. This is here for backward compatibilty.
+
+        @note: This is a non-standard extension API, and only works for EC2.
+
+        @rtype: C{list} of C{str}
+        """
+        return [k['keyName'] for k in self.ex_list_keypairs()]
 
     def ex_describe_keypairs(self, name):
         """
@@ -1320,6 +1369,14 @@ class BaseEC2NodeDriver(NodeDriver):
                                          % (availability_zone.name))
                 params['Placement.AvailabilityZone'] = availability_zone.name
 
+        if 'auth' in kwargs and 'ex_keyname' in kwargs:
+            raise AttributeError('Cannot specify auth and ex_keyname together')
+
+        if 'auth' in kwargs:
+            auth = self._get_and_check_auth(kwargs['auth'])
+            params['KeyName'] = \
+                self.ex_find_or_import_keypair_by_key_material(auth.pubkey)
+
         if 'ex_keyname' in kwargs:
             params['KeyName'] = kwargs['ex_keyname']
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/2d42e94d/libcloud/test/compute/fixtures/ec2/import_key_pair.xml
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/ec2/import_key_pair.xml b/libcloud/test/compute/fixtures/ec2/import_key_pair.xml
new file mode 100644
index 0000000..53623ca
--- /dev/null
+++ b/libcloud/test/compute/fixtures/ec2/import_key_pair.xml
@@ -0,0 +1,5 @@
+<ImportKeyPairResponse xmlns="http://ec2.amazonaws.com/doc/2010-08-31/">
+    <requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
+    <keyName>keypair</keyName>
+    <keyFingerprint>00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00</keyFingerprint>
+</ImportKeyPairResponse>

http://git-wip-us.apache.org/repos/asf/libcloud/blob/2d42e94d/libcloud/test/compute/fixtures/misc/dummy_rsa
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/misc/dummy_rsa b/libcloud/test/compute/fixtures/misc/dummy_rsa
new file mode 100644
index 0000000..7f0cd9e
--- /dev/null
+++ b/libcloud/test/compute/fixtures/misc/dummy_rsa
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAs0ya+QTUpUyAxbFWN81CbW23D7Fm8O1wxP3l0UPu9OO/dAES
+irxNxbBEanTGb8HMdaLEdLBlXaYAIlf8+YhG+c9o7kKe8kCR3j4hJ3x0x/fTVSTf
+mNQc7XIUaM9tuCGj/fO2zfn3fD5fztWAwssPm1+cyP3pAgvc/H03SNpdQG05ylZ+
+1I2QYymYtbjzGh9Nen6dN/aSDrZI7yIA1o3hsDoiY2Nb82l958UI3uJKaxGeBSpO
+Mshutar3gWa/v9F6uqHDTmFEqQdvQGdCHHyWuz98jMVUc0kvWjdH5q5X95CBZFQM
+uOQPNxn2aYjMaP7pU2jvfrU0sLpWT/tG8ZApJwIDAQABAoIBAECotZJr7YuW5TFl
+3GPPP89aq5ObNDZaSjqgHUq0Ju5lW1uoL1mcwqcyA9ooNo+C4griIdq66jM1dALu
+nCoYvQ/Ffl+94rgWFQSCf05QEYUzmCCyZXgltjDi3P1XIIgwiYVBaIErTdaeX8ql
+MAQPWpd7iXzqJCc6w/zB4zgAl3Rt1Fb8GBFHlYf3VTpiU9LA5/IG04GoPk80OgiW
+98lercisWT+nPrTMDu2GoEqqls8OkM9CcT5AgeXIpSF9nPmQgUQWXoqWkrZhD+eQ
+mOxCqpqzwkW/JdsUaBqhPAJtK/eBHTPAfsOabQ5G6/Un1HejN0GTIR0GJzTSEOvi
+blM3YuECgYEA53XL8c8S5ckc1BGfM22obY1daAVgFqsNpqVSV4OKKnRlcmtYaUXq
+61vrQN/OG9Ogrr7FuL7HwavJnr3IbT8uET1/pUje/NQviERwSZWiNX++GUCSXUvq
+hSe9LZb3ezTEkUROdGXOfl+TfI/bhojsk6egaqqKAVv8LR92cwzMD28CgYEAxk8T
+x278NOLUn+F6ije186xpcI0JV+NydhXk40ViDHc7M2ycHNArc6iJzQIlQjkEDejK
+yae3c3QMVtszwONSd6wPkPr9bLbiiT0UlG5gpGAGyEyYZjMQukg2e8ImnwMVMm2l
+bJsrDI5CRq4G20CWPDqxzs8FTuX78tX4uewzJckCgYBmi1a2o8JAkZA3GDOLClsj
+Zgzq5+7BPDlJCldntDxDQNwtDAfYZASHD2szi7P5uhGnOZfJmKRRVnV14ajpVaNo
+OfHSXW2FX9BLM973itaZkyW6dFQkB104bvmuOAMez6sCnNuRUAVjEZ77AZUFjqYZ
+aJt2hmWr4n/f0d+dax8A+wKBgEVV7LJ0KZZMIM9txKyC4gk6pPsHNNcX3TNQYGDe
+J3P4VCicttCUMD0WFmgpceF/kd1TIWP0Uf35+z57EdNFJ9ZTwHWObAEQyI/3XTSw
+ivWt5XEu5rIE9LpM+U+4CEzchRLGp2obrqeLLb0Mp7UNFfolA3otg8ucOcUj7v0C
+ireRAoGAMM5MDDtWyduLH9srxC3PBKdD4Hi8dtzkQ9yAFYTJ0HB4vV7MmIZ2U2j7
+x2KTrPc/go/Jm7+UOmVa4LNkdRvXxVOlAxH85Hqr+n74mm/dWcS4dDWrZvL+Sn+l
+GFa29M3Ix5SnlfFkZhijvTFLICC7XPTRj6uqVHscZVfENhAYGoU=
+-----END RSA PRIVATE KEY-----

http://git-wip-us.apache.org/repos/asf/libcloud/blob/2d42e94d/libcloud/test/compute/fixtures/misc/dummy_rsa.pub
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/misc/dummy_rsa.pub b/libcloud/test/compute/fixtures/misc/dummy_rsa.pub
new file mode 100644
index 0000000..f51e5aa
--- /dev/null
+++ b/libcloud/test/compute/fixtures/misc/dummy_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzTJr5BNSlTIDFsVY3zUJtbbcPsWbw7XDE/eXRQ+704790ARKKvE3FsERqdMZvwcx1osR0sGVdpgAiV/z5iEb5z2juQp7yQJHePiEnfHTH99NVJN+Y1BztchRoz224IaP987bN+fd8Pl/O1YDCyw+bX5zI/ekCC9z8fTdI2l1AbTnKVn7UjZBjKZi1uPMaH016fp039pIOtkjvIgDWjeGwOiJjY1vzaX3nxQje4kprEZ4FKk4yyG61qveBZr+/0Xq6ocNOYUSpB29AZ0IcfJa7P3yMxVRzSS9aN0fmrlf3kIFkVAy45A83GfZpiMxo/ulTaO9+tTSwulZP+0bxkCkn dummycomment

http://git-wip-us.apache.org/repos/asf/libcloud/blob/2d42e94d/libcloud/test/compute/test_ec2.py
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/test_ec2.py b/libcloud/test/compute/test_ec2.py
index e4b5ae4..d08936e 100644
--- a/libcloud/test/compute/test_ec2.py
+++ b/libcloud/test/compute/test_ec2.py
@@ -12,6 +12,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+import os
 import sys
 import unittest
 
@@ -41,6 +42,10 @@ from libcloud.test.file_fixtures import ComputeFileFixtures
 from libcloud.test.secrets import EC2_PARAMS
 
 
+null_fingerprint = '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:' + \
+                      '00:00:00:00:00'
+
+
 class BaseEC2Tests(LibcloudTestCase):
     def test_instantiate_driver_valid_datacenters(self):
         datacenters = REGION_DETAILS.keys()
@@ -299,6 +304,13 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin):
         self.assertEqual(availability_zone.zone_state, 'available')
         self.assertEqual(availability_zone.region_name, 'eu-west-1')
 
+    def test_ex_list_keypairs(self):
+        keypairs = self.driver.ex_list_keypairs()
+
+        self.assertEqual(len(keypairs), 1)
+        self.assertEqual(keypairs[0]['keyName'], 'gsg-keypair')
+        self.assertEqual(keypairs[0]['keyFingerprint'], null_fingerprint)
+
     def test_ex_describe_all_keypairs(self):
         keys = self.driver.ex_describe_all_keypairs()
         self.assertEqual(keys, ['gsg-keypair'])
@@ -309,13 +321,10 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin):
         # Test backward compatibility
         keypair2 = self.driver.ex_describe_keypairs('gsg-keypair')
 
-        fingerprint = '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:' + \
-                      '00:00:00:00:00'
-
         self.assertEqual(keypair1['keyName'], 'gsg-keypair')
-        self.assertEqual(keypair1['keyFingerprint'], fingerprint)
+        self.assertEqual(keypair1['keyFingerprint'], null_fingerprint)
         self.assertEqual(keypair2['keyName'], 'gsg-keypair')
-        self.assertEqual(keypair2['keyFingerprint'], fingerprint)
+        self.assertEqual(keypair2['keyFingerprint'], null_fingerprint)
 
     def test_ex_describe_tags(self):
         node = Node('i-4382922a', None, None, None, None, self.driver)
@@ -326,6 +335,18 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin):
         self.assertTrue('owner' in tags)
         self.assertTrue('stack' in tags)
 
+    def test_ex_import_keypair_from_string(self):
+        path = os.path.join(os.path.dirname(__file__), "fixtures", "misc", "dummy_rsa.pub")
+        key = self.driver.ex_import_keypair_from_string('keypair', open(path).read())
+        self.assertEqual(key['keyName'], 'keypair')
+        self.assertEqual(key['keyFingerprint'], null_fingerprint)
+
+    def test_ex_import_keypair(self):
+        path = os.path.join(os.path.dirname(__file__), "fixtures", "misc", "dummy_rsa.pub")
+        key = self.driver.ex_import_keypair('keypair', path)
+        self.assertEqual(key['keyName'], 'keypair')
+        self.assertEqual(key['keyFingerprint'], null_fingerprint)
+
     def test_ex_create_tags(self):
         node = Node('i-4382922a', None, None, None, None, self.driver)
         self.driver.ex_create_tags(node, {'sample': 'tag'})
@@ -583,6 +604,10 @@ class EC2MockHttp(MockHttpTestCase):
         body = self.fixtures.load('describe_key_pairs.xml')
         return (httplib.OK, body, {}, httplib.responses[httplib.OK])
 
+    def _ImportKeyPair(self, method, url, body, headers):
+        body = self.fixtures.load('import_key_pair.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
     def _DescribeTags(self, method, url, body, headers):
         body = self.fixtures.load('describe_tags.xml')
         return (httplib.OK, body, {}, httplib.responses[httplib.OK])

http://git-wip-us.apache.org/repos/asf/libcloud/blob/2d42e94d/libcloud/utils/publickey.py
----------------------------------------------------------------------
diff --git a/libcloud/utils/publickey.py b/libcloud/utils/publickey.py
new file mode 100644
index 0000000..a0915d5
--- /dev/null
+++ b/libcloud/utils/publickey.py
@@ -0,0 +1,68 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import hashlib
+import struct
+
+__all__ = [
+    'get_pubkey_openssh_fingerprint',
+    'get_pubkey_ssh2_fingerprint',
+    'get_pubkey_comment'
+]
+
+try:
+    from Crypto.Util.asn1 import DerSequence, DerObject, DerNull
+    from Crypto.PublicKey.RSA import algorithmIdentifier, importKey
+    pycrypto_available = True
+except ImportError:
+    pycrypto_available = False
+
+
+def _to_md5_fingerprint(data):
+    hashed = hashlib.md5(data).digest()
+    return ":".join(x.encode("hex") for x in hashed)
+
+
+def get_pubkey_openssh_fingerprint(pubkey):
+    # We import and export the key to make sure it is in OpenSSH format
+    if not pycrypto_available:
+        raise RuntimeError('pycrypto is not available')
+    k = importKey(pubkey)
+    pubkey = k.exportKey('OpenSSH')[7:]
+    decoded = base64.decodestring(pubkey)
+    return _to_md5_fingerprint(decoded)
+
+
+def get_pubkey_ssh2_fingerprint(pubkey):
+    # This is the format that EC2 shows for public key fingerprints in its
+    # KeyPair mgmt API
+    if not pycrypto_available:
+        raise RuntimeError('pycrypto is not available')
+    k = importKey(pubkey)
+    derPK = DerSequence([k.n, k.e])
+    bitmap = DerObject('BIT STRING')
+    bitmap.payload = chr(0x00) + derPK.encode()
+    der = DerSequence([algorithmIdentifier, bitmap.encode()])
+    return _to_md5_fingerprint(der.encode())
+
+
+def get_pubkey_comment(pubkey, default=None):
+    if pubkey.startswith("ssh-"):
+        # This is probably an OpenSSH key
+        return pubkey.strip().split(' ', 3)[2]
+    if default:
+        return default
+    raise ValueError('Public key is not in a supported format')