diff --git a/ec2api/api/availability_zone.py b/ec2api/api/availability_zone.py index 290fae35..0b1dadda 100644 --- a/ec2api/api/availability_zone.py +++ b/ec2api/api/availability_zone.py @@ -16,6 +16,7 @@ from oslo.config import cfg from ec2api.api import clients from ec2api.api import common +from ec2api import exception from ec2api.openstack.common import log as logging from ec2api import utils @@ -49,13 +50,23 @@ CONF = cfg.CONF CONF.register_opts(availability_zone_opts) LOG = logging.getLogger(__name__) -"""Availability zones and regions related API implementation +"""Availability zones, regions, account attributes related API implementation """ +# TODO(ft): implement messages, regionName AvailabilityZone properties +# TODO(ft): implement vpc-max-security-groups-per-interface, max-elastic-ips, +# vpc-max-elastic-ips Account Attributes Validator = common.Validator +def get_account_attribute_engine(): + if CONF.full_vpc_support: + return AccountAttributeEngineNeutron() + else: + return AccountAttributeEngineNova() + + class AvailabilityZoneDescriber(common.UniversalDescriber): KIND = 'az' @@ -120,6 +131,29 @@ def describe_regions(context, region_name=None, filter=None): return {'regionInfo': regions} +def describe_account_attributes(context, attribute_name=None): + def get_max_instances(): + nova = clients.nova(context) + quotas = nova.quotas.get(context.project_id, context.user_id) + return quotas.instances + + attribute_getters = { + 'supported-platforms': ( + account_attribute_engine.get_supported_platforms), + 'default-vpc': account_attribute_engine.get_default_vpc, + 'max-instances': get_max_instances, + } + + formatted_attributes = [] + for attribute in (attribute_name or attribute_getters): + if attribute not in attribute_getters: + raise exception.InvalidParameter(name=attribute) + formatted_attributes.append( + _format_account_attribute(attribute, + attribute_getters[attribute]())) + return {'accountAttributeSet': formatted_attributes} + + def _format_availability_zone(zone): return {'zoneName': zone.zoneName, 'zoneState': ('available' @@ -128,6 +162,13 @@ def _format_availability_zone(zone): } +def _format_account_attribute(attribute, value): + if not isinstance(value, list): + value = [value] + return {'attributeName': attribute, + 'attributeValueSet': [{'attributeValue': val} for val in value]} + + # NOTE(Alex): Openstack extension, AWS-incompability # The whole function and its result is incompatible with AWS. @@ -152,3 +193,24 @@ def _describe_verbose(context): values['updated_at']))}) return {'availabilityZoneInfo': formatted_availability_zones} + + +class AccountAttributeEngineNeutron(object): + + def get_supported_platforms(self): + return ['EC2', 'VPC'] + + def get_default_vpc(self): + return 'none' + + +class AccountAttributeEngineNova(object): + + def get_supported_platforms(self): + return ['EC2'] + + def get_default_vpc(self): + return 'none' + + +account_attribute_engine = get_account_attribute_engine() diff --git a/ec2api/api/cloud.py b/ec2api/api/cloud.py index 9a38d02a..3ccd37d0 100644 --- a/ec2api/api/cloud.py +++ b/ec2api/api/cloud.py @@ -320,7 +320,7 @@ class CloudController(object): @module_and_param_types(security_group, 'sg_id', 'security_group_str', 'dummy') def revoke_security_group_ingress(self, context, group_id=None, - group_name=None, ip_permissions=None): + group_name=None, ip_permissions=None): """Removes one or more ingress rules from a security group. Args: @@ -357,7 +357,7 @@ class CloudController(object): @module_and_param_types(security_group, 'sg_id', 'dummy') def revoke_security_group_egress(self, context, group_id, - ip_permissions=None): + ip_permissions=None): """Removes one or more egress rules from a security group for EC2-VPC. Args: @@ -676,6 +676,23 @@ class CloudController(object): Specified regions. """ + @module_and_param_types(availability_zone, 'strs') + def describe_account_attributes(self, context, attribute_name=None): + """Describes attributes of your EC2 account. + + Args: + context (RequestContext): The request context. + attribute_name (list of str): One or more account attribute names. + The following are the supported account attributes: + supported-platforms | default-vpc | max-instances | + vpc-max-security-groups-per-interface (unsupported now) | + max-elastic-ips (unsupported now) | + vpc-max-elastic-ips (unsupported now) + + Returns: + Information about one or more account attributes. + """ + @module_and_param_types(instance, 'i_id') def get_password_data(self, context, instance_id): """Retrieves the encrypted administrator password for Windows instance. @@ -1606,8 +1623,8 @@ class VpcCloudController(CloudController): Returns: A list of network interfaces. """ - return network_interface.describe_network_interfaces(context, - network_interface_id, filter) + return network_interface.describe_network_interfaces( + context, network_interface_id, filter) @module_and_param_types(network_interface, 'eni_id', 'str') @@ -1635,10 +1652,10 @@ class VpcCloudController(CloudController): 'bool', 'sg_ids') def modify_network_interface_attribute(self, context, - network_interface_id, - description=None, - source_dest_check=None, - security_group_id=None): + network_interface_id, + description=None, + source_dest_check=None, + security_group_id=None): """Modifies the specified attribute of the specified network interface. @@ -1661,8 +1678,8 @@ class VpcCloudController(CloudController): @module_and_param_types(network_interface, 'eni_id', 'str') def reset_network_interface_attribute(self, context, - network_interface_id, - attribute): + network_interface_id, + attribute): """Resets the specified attribute of the specified network interface. diff --git a/ec2api/exception.py b/ec2api/exception.py index ea90f374..cd95f11d 100644 --- a/ec2api/exception.py +++ b/ec2api/exception.py @@ -288,6 +288,10 @@ class MissingParameter(Invalid): msg_fmt = _("The required parameter '%(param)s' is missing") +class InvalidParameter(Invalid): + msg_fmt = _("The property '%(name)s' is not valid") + + class InvalidParameterValue(Invalid): msg_fmt = _("Value (%(value)s) for parameter %(parameter)s is invalid. " "%(reason)s") diff --git a/ec2api/tests/unit/base.py b/ec2api/tests/unit/base.py index 80c025fc..656f8eda 100644 --- a/ec2api/tests/unit/base.py +++ b/ec2api/tests/unit/base.py @@ -53,6 +53,7 @@ class ApiTestCase(test_base.BaseTestCase): self.nova_security_group_rules = ( nova_mock.return_value.security_group_rules) self.nova_volumes = nova_mock.return_value.volumes + self.nova_quotas = nova_mock.return_value.quotas self.addCleanup(nova_patcher.stop) glance_patcher = mock.patch('glanceclient.client.Client') diff --git a/ec2api/tests/unit/test_availability_zone.py b/ec2api/tests/unit/test_availability_zone.py index 9502640e..48a10405 100644 --- a/ec2api/tests/unit/test_availability_zone.py +++ b/ec2api/tests/unit/test_availability_zone.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import mock + +from ec2api.api import availability_zone from ec2api.tests.unit import base from ec2api.tests.unit import fakes from ec2api.tests.unit import matchers @@ -50,3 +53,62 @@ class AvailabilityZoneCase(base.ApiTestCase): self.assertEqual(resp['regionInfo'][0]['regionName'], 'nova') self.assertTrue(resp['regionInfo'][0].get('regionEndpoint') is not None) + + def test_describe_account_attributes(self): + self.nova_quotas.get.return_value = mock.Mock(instances=77) + + availability_zone.account_attribute_engine = ( + availability_zone.AccountAttributeEngineNeutron()) + resp = self.execute('DescribeAccountAttributes', {}) + self.assertEqual(200, resp['http_status_code']) + self.assertThat(resp['accountAttributeSet'], + matchers.ListMatches( + [{'attributeName': 'supported-platforms', + 'attributeValueSet': [ + {'attributeValue': 'EC2'}, + {'attributeValue': 'VPC'}]}, + {'attributeName': 'default-vpc', + 'attributeValueSet': [ + {'attributeValue': 'none'}]}, + {'attributeName': 'max-instances', + 'attributeValueSet': [ + {'attributeValue': 77}]}], + orderless_lists=True)) + self.nova_quotas.get.assert_called_once_with( + fakes.ID_OS_PROJECT, fakes.ID_OS_USER) + + availability_zone.account_attribute_engine = ( + availability_zone.AccountAttributeEngineNova()) + resp = self.execute('DescribeAccountAttributes', {}) + self.assertEqual(200, resp['http_status_code']) + self.assertThat(resp['accountAttributeSet'], + matchers.ListMatches( + [{'attributeName': 'supported-platforms', + 'attributeValueSet': [ + {'attributeValue': 'EC2'}]}, + {'attributeName': 'default-vpc', + 'attributeValueSet': [ + {'attributeValue': 'none'}]}, + {'attributeName': 'max-instances', + 'attributeValueSet': [ + {'attributeValue': 77}]}], + orderless_lists=True)) + + resp = self.execute('DescribeAccountAttributes', + {'AttributeName.1': 'default-vpc', + 'AttributeName.2': 'max-instances'}) + self.assertEqual(200, resp['http_status_code']) + self.assertThat(resp['accountAttributeSet'], + matchers.ListMatches( + [{'attributeName': 'default-vpc', + 'attributeValueSet': [ + {'attributeValue': 'none'}]}, + {'attributeName': 'max-instances', + 'attributeValueSet': [ + {'attributeValue': 77}]}], + orderless_lists=True)) + + resp = self.execute('DescribeAccountAttributes', + {'AttributeName.1': 'fake'}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('InvalidParameter', resp['Error']['Code'])