Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 74 additions & 15 deletions libcloud/compute/drivers/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
from libcloud.utils.py3 import b

from libcloud.utils.xml import fixxpath, findtext, findattr, findall
from libcloud.utils.publickey import pycrypto_available
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)
Expand Down Expand Up @@ -422,6 +425,8 @@ class BaseEC2NodeDriver(NodeDriver):
"""

connectionCls = EC2Connection
if pycrypto_available:
features = {'create_node': ['ssh_key']}
path = '/'

NODE_STATE_MAP = {
Expand Down Expand Up @@ -740,25 +745,22 @@ def ex_create_keypair(self, name):
'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.

@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}
@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',
Expand All @@ -776,27 +778,76 @@ def ex_import_keypair(self, name, keyfile):
'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):
"""
Expand Down Expand Up @@ -1320,6 +1371,14 @@ def create_node(self, **kwargs):
% (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']

Expand Down
5 changes: 5 additions & 0 deletions libcloud/test/compute/fixtures/ec2/import_key_pair.xml
Original file line number Diff line number Diff line change
@@ -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>
27 changes: 27 additions & 0 deletions libcloud/test/compute/fixtures/misc/dummy_rsa
Original file line number Diff line number Diff line change
@@ -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-----
1 change: 1 addition & 0 deletions libcloud/test/compute/fixtures/misc/dummy_rsa.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzTJr5BNSlTIDFsVY3zUJtbbcPsWbw7XDE/eXRQ+704790ARKKvE3FsERqdMZvwcx1osR0sGVdpgAiV/z5iEb5z2juQp7yQJHePiEnfHTH99NVJN+Y1BztchRoz224IaP987bN+fd8Pl/O1YDCyw+bX5zI/ekCC9z8fTdI2l1AbTnKVn7UjZBjKZi1uPMaH016fp039pIOtkjvIgDWjeGwOiJjY1vzaX3nxQje4kprEZ4FKk4yyG61qveBZr+/0Xq6ocNOYUSpB29AZ0IcfJa7P3yMxVRzSS9aN0fmrlf3kIFkVAy45A83GfZpiMxo/ulTaO9+tTSwulZP+0bxkCkn dummycomment
45 changes: 40 additions & 5 deletions libcloud/test/compute/test_ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -41,6 +42,10 @@
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()
Expand Down Expand Up @@ -299,6 +304,13 @@ def test_ex_list_availability_zones(self):
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'])
Expand All @@ -309,13 +321,10 @@ def test_ex_describe_keypairs(self):
# 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)
Expand All @@ -326,6 +335,28 @@ def test_ex_describe_tags(self):
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_find_or_import_keypair_by_key_material(self):
if not 'ssh_key' in self.driver.features['create_node']:
print "Need 'pycrypto' to test ex_find_or_import_keypair_by_key_material"
return
path = os.path.join(os.path.dirname(__file__), "fixtures", "misc", "dummy_rsa.pub")
key_material = open(path).read()
key = self.driver.ex_find_or_import_keypair_by_key_material(key_material)
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'})
Expand Down Expand Up @@ -583,6 +614,10 @@ def _DescribeKeyPairs(self, method, url, body, headers):
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])
Expand Down
68 changes: 68 additions & 0 deletions libcloud/utils/publickey.py
Original file line number Diff line number Diff line change
@@ -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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor style thing - please add __all__ for exported functions.

__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')