# 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 copy try: from neutronclient.common import exceptions as neutron_exception except ImportError: pass # clients will log absense of neutronclient in this case from novaclient import exceptions as nova_exception from oslo.config import cfg from ec2api.api import clients from ec2api.api import common from ec2api.api import ec2utils from ec2api.api import validator from ec2api.db import api as db_api from ec2api import exception from ec2api.openstack.common.gettextutils import _ from ec2api.openstack.common import log as logging CONF = cfg.CONF LOG = logging.getLogger(__name__) """Security Groups related API implementation """ Validator = common.Validator SECURITY_GROUP_MAP = {'domain-name-servers': 'dns-servers', 'domain-name': 'domain-name', 'ntp-servers': 'ntp-server', 'netbios-name-servers': 'netbios-ns', 'netbios-node-type': 'netbios-nodetype'} def get_security_group_engine(): if CONF.full_vpc_support: return SecurityGroupEngineNeutron() else: return SecurityGroupEngineNova() def create_security_group(context, group_name, group_description, vpc_id=None): nova = clients.nova(context) with common.OnCrashCleaner() as cleaner: try: os_security_group = nova.security_groups.create(group_name, group_description) except nova_exception.OverLimit: raise exception.ResourceLimitExceeded(resource='security groups') cleaner.addCleanup(nova.security_groups.delete, os_security_group.id) if vpc_id: # NOTE(Alex) Check if such vpc exists ec2utils.get_db_item(context, 'vpc', vpc_id) security_group = db_api.add_item(context, 'sg', {'vpc_id': vpc_id, 'os_id': os_security_group.id}) return {'return': 'true', 'groupId': security_group['id']} return {'return': 'true'} def _create_default_security_group(context, vpc): # NOTE(Alex): OpenStack doesn't allow creation of another group # named 'default' hence 'Default' is used. return create_security_group(context, 'Default', 'Default VPC security group', vpc['id']) def delete_security_group(context, group_name=None, group_id=None): if group_name is None and group_id is None: raise exception.MissingParameter(param='group id or name') security_group_engine.delete_group(context, group_name, group_id) return True class SecurityGroupDescriber(common.TaggableItemsDescriber): KIND = 'sg' FILTER_MAP = {'vpc-id': 'vpcId', 'group-name': 'groupName', 'group-id': 'groupId'} def __init__(self): super(SecurityGroupDescriber, self).__init__() self.all_db_items = None def format(self, item=None, os_item=None): if self.all_db_items is None: self.all_db_items = ec2utils.get_db_items(self.context, 'sg', None) return _format_security_group(item, os_item, self.all_db_items, self.os_items) def get_os_items(self): return security_group_engine.get_os_groups(self.context) def describe_security_groups(context, group_name=None, group_id=None, filter=None): formatted_security_groups = SecurityGroupDescriber().describe( context, group_id, group_name, filter) return {'securityGroupInfo': formatted_security_groups} # TODO(Alex) cidr/ports/protocol/source_group should be possible # to pass in root set of parameters, not in ip_permissions as now only # supported, for authorize and revoke functions. # The new parameters appeared only in the very recent version of AWS doc. # API version 2014-06-15 didn't claim support of it. def authorize_security_group_ingress(context, group_id=None, group_name=None, ip_permissions=None): return _authorize_security_group(context, group_id, group_name, ip_permissions, 'ingress') def authorize_security_group_egress(context, group_id, ip_permissions=None): return _authorize_security_group(context, group_id, None, ip_permissions, 'egress') def _authorize_security_group(context, group_id, group_name, ip_permissions, direction): rules_bodies = _build_rules(context, group_id, group_name, ip_permissions, direction) for rule_body in rules_bodies: security_group_engine.authorize_security_group(context, rule_body) return True def _validate_parameters(protocol, from_port, to_port): if (not isinstance(protocol, int) and protocol not in ['tcp', 'udp', 'icmp']): raise exception.InvalidParameterValue( _('Invalid value for IP protocol. Unknown protocol.')) if (not isinstance(from_port, int) or not isinstance(to_port, int)): raise exception.InvalidParameterValue( _('Integer values should be specified for ports')) if protocol in ['tcp', 'udp', 6, 17]: if from_port == -1 or to_port == -1: raise exception.InvalidParameterValue( _('Must specify both from and to ports with TCP/UDP.')) if from_port > to_port: raise exception.InvalidParameterValue( _('Invalid TCP/UDP port range.')) if from_port < 0 or from_port > 65535: raise exception.InvalidParameterValue( _('TCP/UDP from port is out of range.')) if to_port < 0 or to_port > 65535: raise exception.InvalidParameterValue( _('TCP/UDP to port is out of range.')) elif protocol in ['icmp', 1]: if from_port < -1 or from_port > 255: raise exception.InvalidParameterValue( _('ICMP type is out of range.')) if to_port < -1 or to_port > 255: raise exception.InvalidParameterValue( _('ICMP code is out of range.')) def _build_rules(context, group_id, group_name, ip_permissions, direction): if group_name is None and group_id is None: raise exception.MissingParameter(param='group id or name') if ip_permissions is None: raise exception.MissingParameter(param='source group or cidr') os_security_group_id = security_group_engine.get_group_os_id(context, group_id, group_name) os_security_group_rule_bodies = [] if ip_permissions is None: ip_permissions = [] for rule in ip_permissions: os_security_group_rule_body = ( {'security_group_id': os_security_group_id, 'direction': direction, 'ethertype': 'IPv4'}) protocol = rule.get('ip_protocol', -1) from_port = rule.get('from_port', -1) to_port = rule.get('to_port', -1) _validate_parameters(protocol, from_port, to_port) if protocol != -1: os_security_group_rule_body['protocol'] = rule['ip_protocol'] if from_port != -1: os_security_group_rule_body['port_range_min'] = rule['from_port'] if to_port != -1: os_security_group_rule_body['port_range_max'] = rule['to_port'] # TODO(Alex) AWS protocol claims support of multiple groups and cidrs, # however, neutron doesn't support it at the moment. # It's possible in the future to convert list values incoming from # REST API into several neutron rules and squeeze them back into one # for describing. # For now only 1 value is supported for either. if rule.get('groups'): os_security_group_rule_body['remote_group_id'] = ( security_group_engine.get_group_os_id( context, rule['groups'][0].get('group_id'), rule['groups'][0].get('group_name'))) elif rule.get('ip_ranges'): os_security_group_rule_body['remote_ip_prefix'] = ( rule['ip_ranges'][0]['cidr_ip']) validator.validate_cidr_with_ipv6( os_security_group_rule_body['remote_ip_prefix'], 'cidr_ip') else: raise exception.MissingParameter(param='source group or cidr') os_security_group_rule_bodies.append(os_security_group_rule_body) return os_security_group_rule_bodies def revoke_security_group_ingress(context, group_id=None, group_name=None, ip_permissions=None): return _revoke_security_group(context, group_id, group_name, ip_permissions, 'ingress') def revoke_security_group_egress(context, group_id, ip_permissions=None): return _revoke_security_group(context, group_id, None, ip_permissions, 'egress') def _are_identical_rules(rule1, rule2): def significant_values(rule): dict = {} for key, value in rule.items(): if (value is not None and value != -1 and value != '0.0.0.0/0' and key not in ['id', 'tenant_id', 'security_group_id']): dict[key] = str(value) return dict r1 = significant_values(rule1) r2 = significant_values(rule2) return r1 == r2 def _revoke_security_group(context, group_id, group_name, ip_permissions, direction): rules_bodies = _build_rules(context, group_id, group_name, ip_permissions, direction) if not rules_bodies: return True os_rules = security_group_engine.get_os_group_rules( context, rules_bodies[0]['security_group_id']) os_rules_to_delete = [] for rule_body in rules_bodies: for os_rule in os_rules: if _are_identical_rules(rule_body, os_rule): os_rules_to_delete.append(os_rule['id']) if len(os_rules_to_delete) != len(rules_bodies): raise exception.InvalidPermissionNotFound() for os_rule_id in os_rules_to_delete: security_group_engine.delete_os_group_rule(context, os_rule_id) return True def _format_security_groups_ids_names(context): neutron = clients.neutron(context) os_security_groups = neutron.list_security_groups()['security_groups'] security_groups = db_api.get_items(context, 'sg') ec2_security_groups = {} for os_security_group in os_security_groups: security_group = next((g for g in security_groups if g['os_id'] == os_security_group['id']), None) if security_group is None: continue ec2_security_groups[os_security_group['id']] = ( {'groupId': security_group['id'], 'groupName': os_security_group['name']}) return ec2_security_groups def _format_security_group(security_group, os_security_group, security_groups, os_security_groups): ec2_security_group = {} if security_group is not None: ec2_security_group['groupId'] = security_group['id'] ec2_security_group['vpcId'] = security_group['vpc_id'] ec2_security_group['ownerId'] = os_security_group['tenant_id'] ec2_security_group['groupName'] = os_security_group['name'] ec2_security_group['groupDescription'] = os_security_group['description'] ingress_permissions = [] egress_permissions = [] for os_rule in os_security_group.get('security_group_rules', []): # NOTE(Alex) We're skipping IPv6 rules because AWS doesn't support # them. if os_rule.get('ethertype', 'IPv4') == 'IPv6': continue ec2_rule = {'ipProtocol': -1 if os_rule['protocol'] is None else os_rule['protocol'], 'fromPort': -1 if os_rule['port_range_min'] is None else os_rule['port_range_min'], 'toPort': -1 if os_rule['port_range_max'] is None else os_rule['port_range_max']} remote_group_id = os_rule['remote_group_id'] if remote_group_id is not None: ec2_remote_group = {} db_remote_group = next((g for g in security_groups if g['os_id'] == remote_group_id), None) if db_remote_group is not None: ec2_remote_group['groupId'] = db_remote_group['id'] else: # TODO(Alex) Log absence of remote_group pass os_remote_group = next((g for g in os_security_groups if g['id'] == remote_group_id), None) if os_remote_group is not None: ec2_remote_group['groupName'] = os_remote_group['name'] ec2_remote_group['userId'] = os_remote_group['tenant_id'] else: # TODO(Alex) Log absence of remote_group pass ec2_rule['groups'] = [ec2_remote_group] elif os_rule['remote_ip_prefix'] is not None: ec2_rule['ipRanges'] = [{'cidrIp': os_rule['remote_ip_prefix']}] if os_rule.get('direction') == 'egress': egress_permissions.append(ec2_rule) else: if security_group is None and os_rule['protocol'] is None: for protocol, min_port, max_port in (('icmp', -1, -1), ('tcp', 1, 65535), ('udp', 1, 65535)): ec2_rule['ipProtocol'] = protocol ec2_rule['fromPort'] = min_port ec2_rule['toPort'] = max_port ingress_permissions.append(copy.deepcopy(ec2_rule)) else: ingress_permissions.append(ec2_rule) ec2_security_group['ipPermissions'] = ingress_permissions if security_group is not None: ec2_security_group['ipPermissionsEgress'] = egress_permissions return ec2_security_group class SecurityGroupEngineNeutron(object): def delete_group(self, context, group_name=None, group_id=None): neutron = clients.neutron(context) if group_id is None or not group_id.startswith('sg-'): return SecurityGroupEngineNova().delete_group(context, group_name, group_id) security_group = ec2utils.get_db_item(context, 'sg', group_id) try: neutron.delete_security_group(security_group['os_id']) except neutron_exception.Conflict as ex: # TODO(Alex): Instance ID is unknown here, report exception message # in its place - looks readable. raise exception.DependencyViolation( obj1_id=group_id, obj2_id=ex.message) except neutron_exception.NeutronClientException as ex: # TODO(Alex): do log error # TODO(Alex): adjust caught exception classes to catch: # the port doesn't exist pass db_api.delete_item(context, group_id) def get_os_groups(self, context): neutron = clients.neutron(context) return neutron.list_security_groups()['security_groups'] def authorize_security_group(self, context, rule_body): neutron = clients.neutron(context) try: os_security_group_rule = neutron.create_security_group_rule( {'security_group_rule': rule_body})['security_group_rule'] except neutron_exception.OverQuotaClient: raise exception.RulesPerSecurityGroupLimitExceeded() except neutron_exception.Conflict as ex: raise exception.InvalidPermissionDuplicate() def get_os_group_rules(self, context, os_id): neutron = clients.neutron(context) os_security_group = ( neutron.show_security_group(os_id)['security_group']) return os_security_group.get('security_group_rules') def delete_os_group_rule(self, context, os_id): neutron = clients.neutron(context) neutron.delete_security_group_rule(os_id) def get_group_os_id(self, context, group_id, group_name): if group_name: return SecurityGroupEngineNova().get_group_os_id(context, group_id, group_name) return ec2utils.get_db_item(context, 'sg', group_id)['os_id'] class SecurityGroupEngineNova(object): def delete_group(self, context, group_name=None, group_id=None): nova = clients.nova(context) os_id = self.get_group_os_id(context, group_id, group_name) try: nova.security_groups.delete(os_id) except Exception as ex: # TODO(Alex): do log error # nova doesn't differentiate Conflict exception like neutron does pass def get_os_groups(self, context): nova = clients.nova(context) return self.convert_groups_to_neutron_format( context, nova.security_groups.list()) def authorize_security_group(self, context, rule_body): nova = clients.nova(context) try: os_security_group_rule = nova.security_group_rules.create( rule_body['security_group_id'], rule_body.get('protocol'), rule_body.get('port_range_min', -1), rule_body.get('port_range_max', -1), rule_body.get('remote_ip_prefix'), rule_body.get('remote_group_id')) except nova_exception.Conflict: raise exception.InvalidPermissionDuplicate() except nova_exception.OverLimit: raise exception.RulesPerSecurityGroupLimitExceeded() def get_os_group_rules(self, context, os_id): nova = clients.nova(context) os_security_group = nova.security_groups.get(os_id) os_rules = os_security_group.rules neutron_rules = [] for os_rule in os_rules: neutron_rules.append( self.convert_rule_to_neutron(context, os_rule, nova.security_groups.list())) return neutron_rules def delete_os_group_rule(self, context, os_id): nova = clients.nova(context) nova.security_group_rules.delete(os_id) def convert_groups_to_neutron_format(self, context, nova_security_groups): neutron_security_groups = [] for nova_group in nova_security_groups: neutron_group = {'id': nova_group.id, 'name': nova_group.name, 'description': nova_group.description, 'tenant_id': nova_group.tenant_id} neutron_rules = [] for rule in nova_group.rules: neutron_rules.append( self.convert_rule_to_neutron(context, rule, nova_security_groups)) if neutron_rules: neutron_group['security_group_rules'] = neutron_rules neutron_security_groups.append(neutron_group) return neutron_security_groups def convert_rule_to_neutron(self, context, nova_rule, nova_security_groups=None): neutron_rule = {'id': nova_rule['id'], 'protocol': nova_rule['ip_protocol'], 'port_range_min': nova_rule['from_port'], 'port_range_max': nova_rule['to_port'], 'remote_ip_prefix': ( nova_rule.get('ip_range') or {}).get('cidr'), 'remote_group_id': None, 'direction': 'ingress', 'ethertype': 'IPv4', 'security_group_id': nova_rule['parent_group_id']} if (nova_rule.get('group') or {}).get('name'): neutron_rule['remote_group_id'] = ( self.get_group_os_id(context, None, nova_rule['group']['name'], nova_security_groups)) return neutron_rule def get_group_os_id(self, context, group_id, group_name, nova_security_groups=None): if group_id: return group_id nova_group = self.get_nova_group_by_name(context, group_name, nova_security_groups) return nova_group.id def get_nova_group_by_name(self, context, group_name, nova_security_groups=None): if nova_security_groups is None: nova = clients.nova(context) nova_security_groups = nova.security_groups.list() nova_group = next((g for g in nova_security_groups if g.name == group_name), None) if nova_group is None: raise exception.InvalidGroupNotFound(sg_id=group_name) return nova_group security_group_engine = get_security_group_engine()