diff --git a/ec2api/api/cloud.py b/ec2api/api/cloud.py index 61f85ae3..c5f06594 100644 --- a/ec2api/api/cloud.py +++ b/ec2api/api/cloud.py @@ -1242,7 +1242,7 @@ class CloudController(object): return key_pair.delete_key_pair(context, key_name) def import_key_pair(self, context, key_name, public_key_material): - return key_pair.create_key_pair(context, key_name, + return key_pair.import_key_pair(context, key_name, public_key_material) def describe_availability_zones(self, context, zone_name=None, diff --git a/ec2api/api/key_pair.py b/ec2api/api/key_pair.py index 777089ac..47231965 100644 --- a/ec2api/api/key_pair.py +++ b/ec2api/api/key_pair.py @@ -14,6 +14,7 @@ import base64 +from novaclient import exceptions as nova_exception from oslo.config import cfg from ec2api.api import clients @@ -61,25 +62,38 @@ def describe_key_pairs(context, key_name=None, filter=None): return {'keySet': formatted_key_pairs} +def _validate_name(name): + if len(name) > 255: + raise exception.InvalidParameterValue( + value=name, + parameter='KeyName', + reason='lenght is exceeds maximum of 255') + + def create_key_pair(context, key_name): + _validate_name(key_name) nova = clients.nova(context) try: key_pair = nova.keypairs.create(key_name) - except clients.novaclient.exceptions.Conflict as ex: - raise exception.KeyPairExists(key_name=key_name) + except nova_exception.OverLimit: + raise exception.ResourceLimitExceeded(resource='keypairs') + except nova_exception.Conflict: + raise exception.InvalidKeyPairDuplicate(key_name=key_name) formatted_key_pair = _format_key_pair(key_pair) formatted_key_pair['keyMaterial'] = key_pair.private_key return formatted_key_pair def import_key_pair(context, key_name, public_key_material): + _validate_name(key_name) nova = clients.nova(context) public_key = base64.b64decode(public_key_material) try: key_pair = nova.keypairs.create(key_name, public_key) - except clients.novaclient.exceptions.Conflict as ex: - raise exception.KeyPairExists(key_name=key_name) - + except nova_exception.OverLimit: + raise exception.ResourceLimitExceeded(resource='keypairs') + except nova_exception.Conflict: + raise exception.InvalidKeyPairDuplicate(key_name=key_name) return _format_key_pair(key_pair) @@ -87,7 +101,7 @@ def delete_key_pair(context, key_name): nova = clients.nova(context) try: nova.keypairs.delete(key_name) - except exception.NotFound: + except nova_exception.NotFound: # aws returns true even if the key doesn't exist pass return True diff --git a/ec2api/exception.py b/ec2api/exception.py index 1f31fb23..97982fd8 100644 --- a/ec2api/exception.py +++ b/ec2api/exception.py @@ -353,6 +353,10 @@ class ResourceLimitExceeded(Overlimit): msg_fmt = _('You have reached the limit of %(resource)s') +class SecurityGroupLimitExceeded(Overlimit): + msg_fmt = _('You have reached the limit of security groups') + + class ImageNotActive(Invalid): ec2_code = 'InvalidAMIID.Unavailable' # TODO(ft): Change the message with the real AWS message @@ -375,7 +379,7 @@ class InvalidAvailabilityZoneNotFound(NotFound): msg_fmt = _("Availability zone %(id)s not found") -class KeyPairExists(Invalid): +class InvalidKeyPairDuplicate(Invalid): ec2_code = 'InvalidKeyPair.Duplicate' msg_fmt = _("Key pair '%(key_name)s' already exists.") diff --git a/ec2api/tests/base.py b/ec2api/tests/base.py index d22ab5c6..76ac24e1 100644 --- a/ec2api/tests/base.py +++ b/ec2api/tests/base.py @@ -41,6 +41,7 @@ class ApiTestCase(test_base.BaseTestCase): self.nova_servers = nova_mock.return_value.servers self.nova_flavors = nova_mock.return_value.flavors self.nova_floating_ips = nova_mock.return_value.floating_ips + self.nova_key_pairs = nova_mock.return_value.keypairs self.nova_security_groups = nova_mock.return_value.security_groups self.nova_security_group_rules = ( nova_mock.return_value.security_group_rules) diff --git a/ec2api/tests/fakes.py b/ec2api/tests/fakes.py index 0e5702b2..f8b94318 100644 --- a/ec2api/tests/fakes.py +++ b/ec2api/tests/fakes.py @@ -924,6 +924,63 @@ EC2_ROUTE_TABLE_2 = { } +# keypair objects + +class NovaKeyPair(object): + + def __init__(self, nova_keypair_dict): + self.name = nova_keypair_dict['name'] + self.fingerprint = nova_keypair_dict['fingerprint'] + self.private_key = nova_keypair_dict['private_key'] + self.public_key = nova_keypair_dict['public_key'] + +PRIVATE_KEY = ( + '-----BEGIN RSA PRIVATE KEY-----\n' + 'MIIEowIBAAKCAQEAgXvm1sZ9MDiAXvGraRFja0/WqyJ1gE6j/QPjreNryd34zBFcv2pQXLyvb' + 'gQG\nFxN4rMGNScgKgLSgHjE/TNywkT8N7aYOiRmGkzQciP5t+zf8ZdCyl+hqgoQig1uY8sV/' + 'fSxUWCB9\n8sF7Tpl0iGkWM6Wo0H/PvcwiS2+UPSzArj+b+Erb/JbBF4O8GgSmtLMeq60RuDM' + 'dJi5JYCP66HUw\njtYb/f9y1Q9nEGVcxY2v0RI1n0yOaZDKPInLKHeR/ole2QVwPZB69mBj11' + 'LErqb+jzCaSivnhy6g\nPzaSHdZaRmy1f+6ltFI1iKt+4y/iINOY0skYC1hc7IevE7j7dGQTD' + 'wIDAQABAoIBAEbD2Vfd6MM2\nzemVuHFWoHggjRjAX2k9EWCRBJifJuSPXI7imka+qqbUNCgz' + 'KMTpzlTT/wyouBy5Gp0Fmyu9nP30\ncP9FdsI04hiHLWUtcBwQ7+8RDNn6mmM0JcyWfdOIXnG' + 'hjYMQVuUaGvLM6SQ4EnsteUJh57451zBV\nDbYVRES2Fbq+j8tPQj1KuD0HhZBboNPOxo6E5n' + 'TxvMXnvuI+cb9D99lqATcb8c0zsLMl/5SKEBDc\nj72X4GPfE3Dc5/MO6L/89ms3TqF3lx8lh' + 'wFSMfFfA3Nf5xrX3gnorGe81odXBXFveqMCemvfJYxg\nS9KPkM8CMnwn6yPS3ftW5xH3nMkC' + 'gYEAvN4lQuOTy9RONCtfgZ6lhR00xfDiibOsE2jFXqXlXrZS\nunBx2WRwNuhAcYGbC4T71iC' + 'BR+LJHECpFjEFX9cKjd8xZPdIzJmwMBylPnli8IxK9UMroxF/MDNy\nnJfdPIWagIrk9VRsQH' + 'UOQW8Ab5dYJuP6c03L5xwmnFfeFnlz10MCgYEAr4Iu182bC2ppwr5AYD8T\n/QKVPZTmizbtG' + 'H/7a2+WnfNCz2u0MOo2h1rF7/SOYR8nalTTsN1z4D8cRX7YQ0P4yBtNRNiN7WH3\n+smTWztI' + 'VYvJA2RsOeP0zfGLJiFSMWLOjlqpJ7KbkEuPcxshGd+/w8upxgJeV8Dwz0ZWbY302kUC\ngYE' + 'AhneTB+CHpaNuWm5W/S46ol9850DtySSG6vq5Kv3qJFii5eKQ7Do6Op145FdmT/lKY9WYtdmd' + '\nXeQbfpVAQlAUT5YM0NnOlv0FF/wNGkHKU4FPDPfZ5avbZjH688qb1S86JTK+eHy25d1xXNz' + 'u7oRO\nWsIN2nIVLmI4iy90C4RFGYkCgYBXpKPtwk/VkItF46nUJku+Agcy3GOQS5p0rJyJ1w' + 'yYzbykRf2S\nm7MlPpAvtqlPGLafI8MexEe0SO++SIyIcq4Oh4u7gITHcS/bfcPnQCBsD8UOu' + '5xMAGjkWuWI4gTg\ngp3xepaUK14B3anB6l9KQ3DIvrCGH/Kq0b+vUkmgpc4LHQKBgBtul9bN' + 'KLF+LJf4JHYNFSurE8Y/\nn8FZ3dZo3T0Q3Sap9bP3ZHemoQ6QXbmpu3H4Mf+2kcNg6YKFW3p' + 'hxW3cuAcZOMHPCrpr3mCdyhF0\nKM74ANEwg8MekBJTcWZUNFv9HZDvTuhp6HSrbMnNEQogkd' + '5PoubiusvAKpeb6NBGnLMq\n' + '-----END RSA PRIVATE KEY-----' +) + +PUBLIC_KEY = ('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDIkYwwXm8UeQXx1c2eFrDIB6b' + '6ApI0KTKs1wezDfFdSIs93vAt4Jx1MyaR/PwqwLk2CDyFoGJBWBI9YcodLAjoRg' + 'Ovr6JigEv5V3yp+eEkeAJO0cPA21vN/KQ8Vxml68ZvvqbdqKZXc/rpFZ1OgCmHt' + 'udo96uQiRB0FM3mdE8YOTswcfkJxTvCe3axX50pYXXfIb0dn9CzC1hyQWYPXvlv' + 'qFNvr/Li7sSBycTBAh4Ar/uEigs/uOjhvzd7GpzY7qDqBVJFAmP7HiiOxoXPkKu' + 'W62Ftd') + +KEY_FINGERPRINT = '2a:72:dd:aa:0d:a6:45:4d:27:4f:75:28:73:0d:a6:10:35:88:e1:ce' + +OS_KEY_PAIR = {'name': 'keyname', + 'private_key': PRIVATE_KEY, + 'public_key': PUBLIC_KEY, + 'fingerprint': KEY_FINGERPRINT} + +EC2_KEY_PAIR = {'keyName': 'keyname', + 'keyFingerprint': KEY_FINGERPRINT, + 'keyMaterial': PRIVATE_KEY} + + # Object generator functions section # internet gateway generator functions diff --git a/ec2api/tests/test_key_pair.py b/ec2api/tests/test_key_pair.py new file mode 100644 index 00000000..31afa186 --- /dev/null +++ b/ec2api/tests/test_key_pair.py @@ -0,0 +1,101 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed 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 + +from novaclient import exceptions as nova_exception + +from ec2api.tests import base +from ec2api.tests import fakes +from ec2api.tests import matchers +from ec2api.tests import tools + + +class KeyPairCase(base.ApiTestCase): + + def test_create_key_pair(self): + self.nova_key_pairs.create.return_value = ( + fakes.NovaKeyPair(fakes.OS_KEY_PAIR)) + resp = self.execute('CreateKeyPair', {'KeyName': 'keyname'}) + self.assertEqual(200, resp['status']) + self.assertThat(fakes.EC2_KEY_PAIR, matchers.DictMatches( + tools.purge_dict(resp, {'status'}))) + self.nova_key_pairs.create.assert_called_once_with('keyname') + + def test_create_key_pair_invalid(self): + self.nova_key_pairs.create.side_effect = ( + nova_exception.Conflict(409)) + resp = self.execute('CreateKeyPair', {'KeyName': 'keyname'}) + self.assertEqual(400, resp['status']) + self.assertEqual('InvalidKeyPair.Duplicate', resp['Error']['Code']) + resp = self.execute('CreateKeyPair', {'KeyName': 'k' * 256}) + self.assertEqual(400, resp['status']) + self.assertEqual('InvalidParameterValue', resp['Error']['Code']) + self.nova_key_pairs.create.side_effect = ( + nova_exception.OverLimit(413)) + resp = self.execute('CreateKeyPair', {'KeyName': 'keyname'}) + self.assertEqual(400, resp['status']) + self.assertEqual('ResourceLimitExceeded', resp['Error']['Code']) + + def test_import_key_pair(self): + self.nova_key_pairs.create.return_value = ( + fakes.NovaKeyPair(fakes.OS_KEY_PAIR)) + resp = self.execute('ImportKeyPair', + {'KeyName': 'keyname', + 'PublicKeyMaterial': base64.b64encode( + fakes.PUBLIC_KEY)}) + self.assertEqual(200, resp['status']) + self.assertThat(tools.purge_dict(fakes.EC2_KEY_PAIR, {'keyMaterial'}), + matchers.DictMatches(tools.purge_dict(resp, {'status'}))) + self.nova_key_pairs.create.assert_called_once_with('keyname', + fakes.PUBLIC_KEY) + + def test_import_key_pair_invalid(self): + self.nova_key_pairs.create.side_effect = ( + nova_exception.OverLimit(413)) + resp = self.execute('ImportKeyPair', + {'KeyName': 'keyname', + 'PublicKeyMaterial': base64.b64encode( + fakes.PUBLIC_KEY)}) + self.assertEqual(400, resp['status']) + self.assertEqual('ResourceLimitExceeded', resp['Error']['Code']) + + def test_delete_key_pair(self): + self.nova_key_pairs.delete.return_value = True + resp = self.execute('DeleteKeyPair', {'KeyName': 'keyname'}) + self.assertEqual(200, resp['status']) + self.nova_key_pairs.delete.assert_called_once_with('keyname') + self.nova_key_pairs.delete.side_effect = nova_exception.NotFound(404) + resp = self.execute('DeleteKeyPair', {'KeyName': 'keyname1'}) + self.assertEqual(200, resp['status']) + self.nova_key_pairs.delete.assert_any_call('keyname1') + + def test_describe_key_pair(self): + self.nova_key_pairs.list.return_value = [fakes.NovaKeyPair( + fakes.OS_KEY_PAIR)] + resp = self.execute('DescribeKeyPairs', {}) + self.assertEqual(200, resp['status']) + self.assertThat(resp['keySet'], + matchers.ListMatches([ + tools.purge_dict(fakes.EC2_KEY_PAIR, + {'keyMaterial'})])) + self.nova_key_pairs.list.assert_called_once() + + def test_describe_key_pair_invalid(self): + self.nova_key_pairs.list.return_value = [fakes.NovaKeyPair( + fakes.OS_KEY_PAIR)] + resp = self.execute('DescribeKeyPairs', {'KeyName': 'badname'}) + self.assertEqual(404, resp['status']) + self.assertEqual('InvalidKeyPair.NotFound', resp['Error']['Code']) + self.nova_key_pairs.list.assert_called_once()