omni/neutron/neutron/plugins/ml2/drivers/aws/mechanism_aws.py

488 lines
22 KiB
Python

"""
Copyright 2016 Platform9 Systems Inc.(http://www.platform9.com)
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 json
import random
import requests
import six
from neutron.callbacks import events
from neutron.callbacks import resources
from neutron.common.aws_utils import AwsException
from neutron.common.aws_utils import AwsUtils
from neutron.db import omni_resources
from neutron.plugins.ml2 import driver_api as api
from neutron.plugins.ml2.drivers.aws import callbacks
from neutron_lib import exceptions
from neutron_lib.plugins import directory
from oslo_config import cfg
from oslo_log import log
LOG = log.getLogger(__name__)
AZ = 'availability_zone'
AZ_HINT = 'availability_zone_hints'
class NetworkWithMultipleAZs(exceptions.NeutronException):
message = "Network shouldn't have more than one availability zone"
class AzNotProvided(exceptions.NeutronException):
"""Raise exception if AZ is not provided in subnet or network."""
message = "No AZ provided either in Subnet or in Network context"
class InvalidAzValue(exceptions.NeutronException):
"""Raise exception if AZ value is incorrect."""
message = ("Invalid AZ value. It should be a single string value and from"
" provided AWS region")
class AwsMechanismDriver(api.MechanismDriver):
"""Ml2 Mechanism driver for AWS"""
def __init__(self):
self.aws_utils = None
self._default_sgr_to_remove = []
super(AwsMechanismDriver, self).__init__()
def initialize(self):
self.aws_utils = AwsUtils()
callbacks.subscribe(self)
# NETWORK
def create_network_precommit(self, context):
pass
def create_network_postcommit(self, context):
pass
def update_network_precommit(self, context):
try:
network_name = context.current['name']
original_network_name = context.original['name']
LOG.debug("Update network original: %s current: %s",
original_network_name, network_name)
if network_name == original_network_name:
return
neutron_network_id = context.current['id']
project_id = context.current['project_id']
tags_list = [{'Key': 'Name', 'Value': network_name}]
self.aws_utils.create_tags_for_vpc(neutron_network_id, tags_list,
context=context._plugin_context,
project_id=project_id)
except Exception as e:
LOG.error("Error in update subnet precommit: %s" % e)
raise e
def update_network_postcommit(self, context):
pass
def delete_network_precommit(self, context):
neutron_network_id = context.current['id']
project_id = context.current['project_id']
# If user is deleting an empty neutron network then nothing to be done
# on AWS side
if len(context.current['subnets']) > 0:
vpc_id = self.aws_utils.get_vpc_from_neutron_network_id(
neutron_network_id, context=context._plugin_context,
project_id=project_id)
if vpc_id is not None:
LOG.info("Deleting network %s (VPC_ID: %s)" %
(neutron_network_id, vpc_id))
try:
self.aws_utils.delete_vpc(vpc_id=vpc_id,
context=context._plugin_context,
project_id=project_id)
except AwsException as e:
if 'InvalidVpcID.NotFound' in e.msg:
LOG.warn(e.msg)
else:
raise e
omni_resources.delete_mapping(context.current['id'])
def delete_network_postcommit(self, context):
pass
# SUBNET
def create_subnet_precommit(self, context):
network_id = context.network.current['id']
LOG.info("Create subnet for network %s" % network_id)
# External Network doesn't exist on AWS, so no operations permitted
physical_network = context.network.current.get(
'provider:physical_network')
if physical_network == "external":
# Do not create subnets for external & provider networks. Only
# allow tenant network subnet creation at the moment.
LOG.info('Creating external network {0}'.format(
network_id))
return
elif physical_network and physical_network.startswith('vpc'):
LOG.info('Registering AWS network with vpc %s',
physical_network)
subnet_cidr = context.current['cidr']
subnet_id = self.aws_utils.get_subnet_from_vpc_and_cidr(
context._plugin_context, physical_network, subnet_cidr,
context.current['project_id'])
omni_resources.add_mapping(network_id, physical_network)
omni_resources.add_mapping(context.current['id'], subnet_id)
return
if context.current['ip_version'] == 6:
raise AwsException(error_code="IPv6Error",
message="Cannot create subnets with IPv6")
mask = int(context.current['cidr'][-2:])
if mask < 16 or mask > 28:
raise AwsException(error_code="InvalidMask",
message="Subnet mask has to be >16 and <28")
try:
# Check if this is the first subnet to be added to a network
neutron_network = context.network.current
associated_vpc_id = self.aws_utils.get_vpc_from_neutron_network_id(
neutron_network['id'], context=context._plugin_context)
if associated_vpc_id is None:
# Need to create EC2 VPC
vpc_cidr = context.current['cidr'][:-2] + '16'
tags = [
{'Key': 'Name', 'Value': neutron_network['name']},
{'Key': 'openstack_network_id',
'Value': neutron_network['id']},
{'Key': 'openstack_tenant_id',
'Value': context.current['tenant_id']}
]
associated_vpc_id = self.aws_utils.create_vpc_and_tags(
cidr=vpc_cidr, tags_list=tags,
context=context._plugin_context)
omni_resources.add_mapping(neutron_network['id'],
associated_vpc_id)
# Create Subnet in AWS
tags = [
{'Key': 'Name', 'Value': context.current['name']},
{'Key': 'openstack_subnet_id', 'Value': context.current['id']},
{'Key': 'openstack_tenant_id',
'Value': context.current['tenant_id']}
]
if AZ in context.current and context.current[AZ]:
aws_az = context.current[AZ]
elif context.network.current[AZ_HINT]:
network_az_hints = context.network.current[AZ_HINT]
if len(network_az_hints) > 1:
# We use only one AZ hint even if multiple AZ values
# are passed while creating network.
raise NetworkWithMultipleAZs()
aws_az = network_az_hints[0]
else:
aws_az = cfg.CONF.AWS.az
LOG.info("AZ details missing in context, so fetching from the"
" conf file. aws_az: %s", aws_az)
if aws_az is None:
raise AzNotProvided()
self._validate_az(aws_az)
ec2_subnet_id = self.aws_utils.create_subnet_and_tags(
vpc_id=associated_vpc_id, cidr=context.current['cidr'],
tags_list=tags, aws_az=aws_az, context=context._plugin_context)
omni_resources.add_mapping(context.current['id'], ec2_subnet_id)
except Exception as e:
LOG.error("Error in create subnet precommit: %s" % e)
raise e
def _send_request(self, session, url):
headers = {'Content-Type': 'application/json',
'X-Auth-Token': session.get_token()}
response = requests.get(url + "/v1/zones", headers=headers)
response.raise_for_status()
return response.json()
def _validate_az(self, aws_az):
if not isinstance(aws_az, six.string_types):
raise InvalidAzValue()
if ',' in aws_az:
raise NetworkWithMultipleAZs()
session = self.aws_utils.get_keystone_session()
azmgr_url = ''
try:
azmgr_url = session.get_endpoint(service_type='azmanager',
region_name=cfg.CONF.nova_region_name)
if not (azmgr_url and azmgr_url.strip()):
LOG.warning("AZ manager endpoint not found. Proceeding without"
" validating the AZ information. Error: "
"None or empty endpoint found.")
return
except Exception as e:
LOG.error("Error in finding AZ manager endpoint. "
"Proceeding without validating the AZ information. "
"Error: %s" % e)
return
zones = self._send_request(session, azmgr_url)
if aws_az not in zones:
LOG.error("Provided az %s not found in zones %s", aws_az, zones)
raise InvalidAzValue()
def create_subnet_postcommit(self, context):
pass
def update_subnet_precommit(self, context):
try:
subnet_name = context.current['name']
neutron_subnet_id = context.current['id']
tags_list = [{'Key': 'Name', 'Value': subnet_name}]
self.aws_utils.create_subnet_tags(neutron_subnet_id, tags_list,
context=context._plugin_context)
except Exception as e:
LOG.error("Error in update subnet precommit: %s" % e)
raise e
def update_subnet_postcommit(self, context):
pass
def delete_subnet_precommit(self, context):
try:
LOG.info("Deleting subnet %s" % context.current['id'])
project_id = context.current['project_id']
subnet_id = self.aws_utils.get_subnet_from_neutron_subnet_id(
context.current['id'], context=context._plugin_context,
project_id=project_id)
if not subnet_id:
raise Exception("Subnet mapping %s not found" % (
context.current['id']))
try:
self.aws_utils.delete_subnet(
subnet_id=subnet_id, context=context._plugin_context,
project_id=project_id)
omni_resources.delete_mapping(context.current['id'])
except AwsException as e:
if 'InvalidSubnetID.NotFound' in e.msg:
LOG.warn(e.msg)
omni_resources.delete_mapping(context.current['id'])
else:
raise e
except Exception as e:
LOG.error("Error in delete subnet precommit: %s" % e)
raise e
def delete_subnet_postcommit(self, context):
neutron_network = context.network.current
try:
subnets = neutron_network['subnets']
if (len(subnets) == 1 and subnets[0] == context.current['id'] or
len(subnets) == 0):
# Last subnet for this network was deleted, so delete VPC
# because VPC gets created during first subnet creation under
# an OpenStack network
project_id = context.current['project_id']
vpc_id = self.aws_utils.get_vpc_from_neutron_network_id(
neutron_network['id'], context=context._plugin_context,
project_id=project_id)
if not vpc_id:
raise Exception("Network mapping %s not found",
neutron_network['id'])
LOG.info("Deleting VPC %s since this was the last subnet in "
"the vpc" % vpc_id)
self.aws_utils.delete_vpc(
vpc_id=vpc_id, context=context._plugin_context,
project_id=project_id)
omni_resources.delete_mapping(context.network.current['id'])
except Exception as e:
LOG.error("Error in delete subnet postcommit: %s" % e)
raise e
def create_port_precommit(self, context):
pass
def create_port_postcommit(self, context):
pass
def update_port_precommit(self, context):
original_port = context._original_port
updated_port = context._port
sorted_original_sgs = sorted(original_port['security_groups'])
sorted_updated_sgs = sorted(updated_port['security_groups'])
aws_sgs = []
project_id = context.current['project_id']
if sorted_updated_sgs != sorted_original_sgs:
for sg in updated_port['security_groups']:
aws_secgrps = self.aws_utils.get_sec_group_by_id(
sg, context._plugin_context, project_id=project_id)
aws_sgs.append(aws_secgrps[0]['GroupId'])
if aws_sgs:
self.aws_utils.modify_ports(aws_sgs, updated_port['name'],
context._plugin_context, project_id)
def update_port_postcommit(self, context):
pass
def delete_port_precommit(self, context):
pass
def delete_port_postcommit(self, context):
pass
def bind_port(self, context):
fixed_ip_dict = dict()
if 'fixed_ips' in context.current:
if len(context.current['fixed_ips']) > 0:
fixed_ip_dict = context.current['fixed_ips'][0]
openstack_subnet_id = fixed_ip_dict['subnet_id']
aws_subnet_id = \
self.aws_utils.get_subnet_from_neutron_subnet_id(
openstack_subnet_id, context._plugin_context,
project_id=context.current['project_id'])
fixed_ip_dict['subnet_id'] = aws_subnet_id
secgroup_ids = context.current['security_groups']
ec2_secgroup_ids = self.create_security_groups_if_needed(
context, secgroup_ids)
fixed_ip_dict['ec2_security_groups'] = ec2_secgroup_ids
segment_id = random.choice(context.network.network_segments)[api.ID]
context.set_binding(segment_id, "vip_type_a",
json.dumps(fixed_ip_dict), status='ACTIVE')
return True
def create_security_groups_if_needed(self, context, secgrp_ids):
project_id = context.current.get('project_id')
core_plugin = directory.get_plugin()
vpc_id = self.aws_utils.get_vpc_from_neutron_network_id(
context.current['network_id'], context=context._plugin_context,
project_id=project_id)
ec2_secgroup_ids = []
for secgrp_id in secgrp_ids:
tags = [
{'Key': 'openstack_id', 'Value': secgrp_id},
{'Key': 'openstack_network_id',
'Value': context.current['network_id']}
]
secgrp = core_plugin.get_security_group(context._plugin_context,
secgrp_id)
aws_secgrps = self.aws_utils.get_sec_group_by_id(
secgrp_id, group_name=secgrp['name'], vpc_id=vpc_id,
context=context._plugin_context, project_id=project_id)
if not aws_secgrps and secgrp['name'] != 'default':
grp_name = secgrp['name']
tags.append({"Key": "Name", "Value": grp_name})
desc = secgrp['description']
rules = secgrp['security_group_rules']
ec2_secgrp = self.aws_utils.create_security_group(
grp_name, desc, vpc_id, secgrp_id, tags,
context=context._plugin_context,
project_id=project_id
)
self.aws_utils.create_security_group_rules(ec2_secgrp, rules)
# Make sure that omni_resources table is populated with newly
# created security group
aws_secgrps = self.aws_utils.get_sec_group_by_id(
secgrp_id, group_name=secgrp['name'], vpc_id=vpc_id,
context=context._plugin_context, project_id=project_id)
for aws_secgrp in aws_secgrps:
ec2_secgroup_ids.append(aws_secgrp['GroupId'])
return ec2_secgroup_ids
def delete_security_group(self, security_group_id, context, project_id):
core_plugin = directory.get_plugin()
secgrp = core_plugin.get_security_group(context, security_group_id)
self.aws_utils.delete_security_group(security_group_id, context,
project_id,
group_name=secgrp['name'])
def remove_security_group_rule(self, context, rule_id):
core_plugin = directory.get_plugin()
rule = core_plugin.get_security_group_rule(context, rule_id)
secgrp_id = rule['security_group_id']
secgrp = core_plugin.get_security_group(context, secgrp_id)
if "project_id" in rule:
project_id = rule['project_id']
else:
project_id = context.tenant
self.aws_utils.delete_security_group_rule_if_needed(
context, secgrp_id, secgrp['name'], project_id, rule)
def add_security_group_rule(self, context, rule):
core_plugin = directory.get_plugin()
secgrp_id = rule['security_group_id']
secgrp = core_plugin.get_security_group(context, secgrp_id)
if "project_id" in rule:
project_id = rule['project_id']
else:
project_id = context.tenant
self.aws_utils.create_security_group_rule_if_needed(
context, secgrp_id, secgrp['name'], project_id, rule)
def update_security_group_rules(self, context, rule_id):
core_plugin = directory.get_plugin()
rule = core_plugin.get_security_group_rule(context, rule_id)
secgrp_id = rule['security_group_id']
secgrp = core_plugin.get_security_group(context, secgrp_id)
old_rules = secgrp['security_group_rules']
for idx in range(len(old_rules) - 1, -1, -1):
if old_rules[idx]['id'] == rule_id:
old_rules.pop(idx)
break
old_rules.append(rule)
if "project_id" in rule:
project_id = rule['project_id']
else:
project_id = context.tenant
self.aws_utils.update_sec_group(secgrp_id, old_rules, context=context,
project_id=project_id,
group_name=secgrp['name'])
def secgroup_callback(self, resource, event, trigger, **kwargs):
context = kwargs['context']
if resource == resources.SECURITY_GROUP:
if event == events.AFTER_CREATE:
project_id = kwargs.get('security_group')['project_id']
secgrp = kwargs.get('security_group')
security_group_id = secgrp.get('id')
core_plugin = directory.get_plugin()
aws_secgrps = self.aws_utils.get_sec_group_by_id(
security_group_id, group_name=secgrp.get('name'),
context=context, project_id=project_id)
if len(aws_secgrps) == 0:
return
for sgr in secgrp.get('security_group_rules', []):
# This is invoked for discovered security groups only. For
# discovered security groups we do not need default egress
# rules. Those should be reported by discovery service.
# When removing these default security group rules we do
# not need to check against AWS. Store the security group
# rule IDs so that we can ignore them when delete security
# group rule is called here.
self._default_sgr_to_remove.append(sgr.get('id'))
core_plugin.delete_security_group_rule(context,
sgr.get('id'))
if event == events.BEFORE_DELETE:
project_id = kwargs.get('security_group')['project_id']
security_group_id = kwargs.get('security_group_id')
if security_group_id:
self.delete_security_group(security_group_id, context,
project_id)
else:
LOG.warn('Security group ID not found in delete request')
elif resource == resources.SECURITY_GROUP_RULE:
if event == events.BEFORE_CREATE:
rule = kwargs['security_group_rule']
self.add_security_group_rule(context, rule)
elif event == events.BEFORE_DELETE:
rule_id = kwargs['security_group_rule_id']
if rule_id in self._default_sgr_to_remove:
# Check the comment above in security group rule
# AFTER_CREATE event handling
self._default_sgr_to_remove.remove(rule_id)
else:
self.remove_security_group_rule(context, rule_id)
elif event == events.BEFORE_UPDATE:
rule_id = kwargs['security_group_rule_id']
self.update_security_group_rules(context, rule_id)