diff --git a/README.rst b/README.rst index 23a3243a..ab830374 100644 --- a/README.rst +++ b/README.rst @@ -188,6 +188,35 @@ Additions to the legacy nova's EC2 API include: 2. Filtering 3. Tags +Legacy OpenStack release notice +=============================== + +EC2 API supports Havana, Icehouse, Juno with additional limitations: + + +Instance related: +- rootDeviceName Instance property +- kernelId Instance property +- ramdiskId Instance property +- userData Instance property +- hostName Instance property +- reservationId Reservation property (ec2api own ids are generated for +instances launched not by ec2api) +- launchIndex Instance property (0 for instances launched not by ec2api) + +Volume related: +- deleteOnTermination property + +Network interface related: +- deleteOnTermination (False value can be assigned but doesn't supported) + +All theese properties can be specified in RunInstance command though, they are +not reported in describe operations. + +EC2 API supports Nova client (>=2.16.0) with no microversion support. +Additional limitations are the same, except network interfaces's +deleteOnTermination. + References ========== diff --git a/ec2api/api/clients.py b/ec2api/api/clients.py index 697709d3..a91f98ce 100644 --- a/ec2api/api/clients.py +++ b/ec2api/api/clients.py @@ -15,12 +15,13 @@ from keystoneclient.v2_0 import client as kc from novaclient import client as novaclient +from novaclient import exceptions as nova_exception from oslo_config import cfg from oslo_log import log as logging import oslo_messaging as messaging from ec2api import context as ec2_context -from ec2api.i18n import _ +from ec2api.i18n import _, _LW logger = logging.getLogger(__name__) @@ -44,18 +45,46 @@ except ImportError: logger.info(_('glanceclient not available')) +# Nova API's 2.3 microversion provides additional EC2 compliant instance +# properties +_novaclient_vertion = '2.3' +_nova_service_type = 'computev21' + + def nova(context): args = { - 'project_id': context.project_id, 'auth_url': CONF.keystone_url, + 'auth_token': context.auth_token, + # NOTE(ft): These parameters are not used for authentification, + # but are required by novaclient < v2.18 which may be installed in + # Icehouse deployment 'username': None, 'api_key': None, - 'auth_token': context.auth_token, - 'bypass_url': _url_for(context, service_type='computev21'), + 'project_id': None, } - # Nova API's 2.3 microversion provides additional EC2 complient instance - # attributes - return novaclient.Client(2.3, **args) + global _novaclient_vertion, _nova_service_type + bypass_url = _url_for(context, service_type=_nova_service_type) + if not bypass_url and _nova_service_type == 'computev21': + # NOTE(ft): partial compatibility with pre Kilo OS releases: + # if computev21 isn't provided by Nova, use compute instead + logger.warning(_LW("Nova server doesn't support v2.1, use v2 instead. " + "A lot of useful EC2 compliant instance properties " + "will be unavailable.")) + _nova_service_type = 'compute' + return nova(context) + try: + return novaclient.Client(_novaclient_vertion, bypass_url=bypass_url, + **args) + except nova_exception.UnsupportedVersion: + if _novaclient_vertion == '2': + raise + # NOTE(ft): partial compatibility with Nova client w/o microversion + # support + logger.warning(_LW("Nova client doesn't support v2.3, use v2 instead. " + "A lot of useful EC2 compliant instance properties " + "will be unavailable.")) + _novaclient_vertion = '2' + return nova(context) def neutron(context): @@ -123,16 +152,16 @@ def _url_for(context, **kwargs): service_catalog = context.service_catalog if not service_catalog: catalog = keystone(context).service_catalog.catalog - service_catalog = catalog["serviceCatalog"] + service_catalog = catalog['serviceCatalog'] context.service_catalog = service_catalog - service_type = kwargs["service_type"] + service_type = kwargs['service_type'] for service in service_catalog: - if service["type"] != service_type: + if service['type'] != service_type: continue - for endpoint in service["endpoints"]: - if "publicURL" in endpoint: - return endpoint["publicURL"] + for endpoint in service['endpoints']: + if 'publicURL' in endpoint: + return endpoint['publicURL'] else: return None diff --git a/ec2api/api/instance.py b/ec2api/api/instance.py index 77875974..8ba40e76 100644 --- a/ec2api/api/instance.py +++ b/ec2api/api/instance.py @@ -1251,7 +1251,7 @@ def _cloud_format_instance_bdm(context, os_instance, result, 'status': _cloud_get_volume_attach_status(os_volume)} volume_attached = next((va for va in volumes_attached if va['id'] == os_volume.id), None) - if volume_attached: + if volume_attached and 'delete_on_termination' in volume_attached: ebs['deleteOnTermination'] = ( volume_attached['delete_on_termination']) mapping.append({'deviceName': device_name, diff --git a/ec2api/tests/unit/test_clients.py b/ec2api/tests/unit/test_clients.py new file mode 100644 index 00000000..9619aa98 --- /dev/null +++ b/ec2api/tests/unit/test_clients.py @@ -0,0 +1,135 @@ +# 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 fixtures +import mock +from novaclient import exceptions as nova_exception +from oslo_config import fixture as config_fixture +from oslotest import base as test_base + +from ec2api.api import clients + + +class ClientsTestCase(test_base.BaseTestCase): + + def setUp(self): + super(ClientsTestCase, self).setUp() + + conf = self.useFixture(config_fixture.Config()) + conf.config(keystone_url='keystone_url') + + @mock.patch('novaclient.client.Client') + def test_nova(self, nova): + reload(clients) + + # test normal flow + context = mock.Mock( + auth_token='fake_token', + service_catalog=[{'type': 'computev21', + 'endpoints': [{'publicURL': 'novav21_url'}]}]) + with fixtures.LoggerFixture() as logs: + res = clients.nova(context) + self.assertEqual(nova.return_value, res) + nova.assert_called_with( + '2.3', bypass_url='novav21_url', + auth_url='keystone_url', auth_token='fake_token', + username=None, api_key=None, project_id=None) + self.assertEqual(0, len(logs.output)) + + # test switching to v2 client + nova.side_effect = [nova_exception.UnsupportedVersion(), 'v2_client'] + with fixtures.LoggerFixture() as logs: + res = clients.nova(context) + self.assertEqual('v2_client', res) + nova.assert_called_with( + '2', bypass_url='novav21_url', + auth_url='keystone_url', auth_token='fake_token', + username=None, api_key=None, project_id=None) + self.assertNotEqual(0, len(logs.output)) + + # test raising of an exception if v2 client is not supported as well + nova.side_effect = nova_exception.UnsupportedVersion() + self.assertRaises(nova_exception.UnsupportedVersion, + clients.nova, context) + + nova.side_effect = None + reload(clients) + + # test switching to 'compute' service type + context.service_catalog = [{'type': 'compute', + 'endpoints': [{'publicURL': 'nova_url'}]}] + with fixtures.LoggerFixture() as logs: + res = clients.nova(context) + nova.assert_called_with( + '2.3', bypass_url='nova_url', + auth_url='keystone_url', auth_token='fake_token', + username=None, api_key=None, project_id=None) + self.assertNotEqual(0, len(logs.output)) + + # test behavior if 'compute' service type is not found as well + context.service_catalog = [{'type': 'fake'}] + clients.nova(context) + nova.assert_called_with( + '2.3', bypass_url=None, + auth_url='keystone_url', auth_token='fake_token', + username=None, api_key=None, project_id=None) + + @mock.patch('neutronclient.v2_0.client.Client') + def test_neutron(self, neutron): + context = mock.Mock( + auth_token='fake_token', + service_catalog=[{'type': 'network', + 'endpoints': [{'publicURL': 'neutron_url'}]}]) + res = clients.neutron(context) + self.assertEqual(neutron.return_value, res) + neutron.assert_called_with( + auth_url='keystone_url', service_type='network', + token='fake_token', endpoint_url='neutron_url') + + @mock.patch('glanceclient.client.Client') + def test_glance(self, glance): + context = mock.Mock( + auth_token='fake_token', + service_catalog=[{'type': 'image', + 'endpoints': [{'publicURL': 'glance_url'}]}]) + res = clients.glance(context) + self.assertEqual(glance.return_value, res) + glance.assert_called_with( + '1', auth_url='keystone_url', service_type='image', + token='fake_token', endpoint='glance_url') + + @mock.patch('cinderclient.client.Client') + def test_cinder(self, cinder): + context = mock.Mock( + auth_token='fake_token', + service_catalog=[{'type': 'volume', + 'endpoints': [{'publicURL': 'cinder_url'}]}]) + res = clients.cinder(context) + self.assertEqual(cinder.return_value, res) + cinder.assert_called_with( + '1', auth_url='keystone_url', service_type='volume', + username=None, api_key=None) + self.assertEqual('fake_token', res.client.auth_token) + self.assertEqual('cinder_url', res.client.management_url) + + @mock.patch('keystoneclient.v2_0.client.Client') + def test_keystone(self, keystone): + context = mock.Mock( + auth_token='fake_token', + project_id='fake_project') + res = clients.keystone(context) + self.assertEqual(keystone.return_value, res) + keystone.assert_called_with( + auth_url='keystone_url', token='fake_token', + tenant_id='fake_project')