congress/congress/datasources/neutronv2_driver.py
Eric Kao 734997a6ff Add attach/detach security group action to neutron driver
To modify the security groups attached to the list through congress
policy, actions to attach/detach a security group to/from port is
needed.

Because the neutron v2 API and client do not support attach/detach
of a single security group, but only the replacement of the entire
security group list, we add these custom actions which combine
a read and update action to accomplish the attach/detach.

Caveat: Another concurrent update to the list of attached ports may
be lost. Unfortunately, there is simply no better solution given the
limitations of the neutron v2 API. So we enable these actions anyway
with documented warning in the driver actions description.

Change-Id: I51ec7452a5a7a77cc8cfc57b87e671685280ea73
2018-01-04 21:55:22 +00:00

528 lines
23 KiB
Python

#!/usr/bin/env python
# Copyright (c) 2014 VMware, Inc. All rights reserved.
#
# 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.
#
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import neutronclient.v2_0.client
from oslo_log import log as logging
from congress.datasources import constants
from congress.datasources import datasource_driver
from congress.datasources import datasource_utils as ds_utils
LOG = logging.getLogger(__name__)
class NeutronV2Driver(datasource_driver.PollingDataSourceDriver,
datasource_driver.ExecutionDriver):
NETWORKS = 'networks'
FIXED_IPS = 'fixed_ips'
SECURITY_GROUP_PORT_BINDINGS = 'security_group_port_bindings'
PORTS = 'ports'
ALLOCATION_POOLS = 'allocation_pools'
DNS_NAMESERVERS = 'dns_nameservers'
HOST_ROUTES = 'host_routes'
SUBNETS = 'subnets'
EXTERNAL_FIXED_IPS = 'external_fixed_ips'
EXTERNAL_GATEWAY_INFOS = 'external_gateway_infos'
ROUTERS = 'routers'
SECURITY_GROUP_RULES = 'security_group_rules'
SECURITY_GROUPS = 'security_groups'
FLOATING_IPS = 'floating_ips'
# This is the most common per-value translator, so define it once here.
value_trans = {'translation-type': 'VALUE'}
floating_ips_translator = {
'translation-type': 'HDICT',
'table-name': FLOATING_IPS,
'selector-type': 'DICT_SELECTOR',
'field-translators':
({'fieldname': 'id', 'desc': 'The UUID of the floating IP address',
'translator': value_trans},
{'fieldname': 'router_id', 'desc': 'UUID of router',
'translator': value_trans},
{'fieldname': 'tenant_id', 'desc': 'Tenant ID',
'translator': value_trans},
{'fieldname': 'floating_network_id',
'desc': 'The UUID of the network associated with floating IP',
'translator': value_trans},
{'fieldname': 'fixed_ip_address',
'desc': 'Fixed IP address associated with floating IP address',
'translator': value_trans},
{'fieldname': 'floating_ip_address',
'desc': 'The floating IP address', 'translator': value_trans},
{'fieldname': 'port_id', 'desc': 'UUID of port',
'translator': value_trans},
{'fieldname': 'status', 'desc': 'The floating IP status',
'translator': value_trans})}
networks_translator = {
'translation-type': 'HDICT',
'table-name': NETWORKS,
'selector-type': 'DICT_SELECTOR',
'field-translators':
({'fieldname': 'id', 'desc': 'Network ID',
'translator': value_trans},
{'fieldname': 'tenant_id', 'desc': 'Tenant ID',
'translator': value_trans},
{'fieldname': 'name', 'desc': 'Network name',
'translator': value_trans},
{'fieldname': 'status', 'desc': 'Network status',
'translator': value_trans},
{'fieldname': 'admin_state_up',
'desc': 'Administrative state of the network (true/false)',
'translator': value_trans},
{'fieldname': 'shared',
'desc': 'Indicates if network is shared across all tenants',
'translator': value_trans})}
ports_fixed_ips_translator = {
'translation-type': 'HDICT',
'table-name': FIXED_IPS,
'parent-key': 'id',
'parent-col-name': 'port_id',
'parent-key-desc': 'UUID of Port',
'selector-type': 'DICT_SELECTOR',
'in-list': True,
'field-translators':
({'fieldname': 'ip_address',
'desc': 'The IP addresses for the port',
'translator': value_trans},
{'fieldname': 'subnet_id',
'desc': 'The UUID of the subnet to which the port is attached',
'translator': value_trans})}
ports_security_groups_translator = {
'translation-type': 'LIST',
'table-name': SECURITY_GROUP_PORT_BINDINGS,
'parent-key': 'id',
'parent-col-name': 'port_id',
'parent-key-desc': 'UUID of port',
'val-col': 'security_group_id',
'val-col-desc': 'UUID of security group',
'translator': value_trans}
ports_translator = {
'translation-type': 'HDICT',
'table-name': PORTS,
'selector-type': 'DICT_SELECTOR',
'field-translators':
({'fieldname': 'id', 'desc': 'UUID of port',
'translator': value_trans},
{'fieldname': 'tenant_id', 'desc': 'tenant ID',
'translator': value_trans},
{'fieldname': 'name', 'desc': 'port name',
'translator': value_trans},
{'fieldname': 'network_id', 'desc': 'UUID of attached network',
'translator': value_trans},
{'fieldname': 'mac_address', 'desc': 'MAC address of the port',
'translator': value_trans},
{'fieldname': 'admin_state_up',
'desc': 'Administrative state of the port',
'translator': value_trans},
{'fieldname': 'status', 'desc': 'Port status',
'translator': value_trans},
{'fieldname': 'device_id',
'desc': 'The UUID of the device that uses this port',
'translator': value_trans},
{'fieldname': 'device_owner',
'desc': 'The UUID of the entity that uses this port',
'translator': value_trans},
{'fieldname': 'fixed_ips',
'desc': 'The IP addresses for the port',
'translator': ports_fixed_ips_translator},
{'fieldname': 'security_groups',
'translator': ports_security_groups_translator})}
subnets_allocation_pools_translator = {
'translation-type': 'HDICT',
'table-name': ALLOCATION_POOLS,
'parent-key': 'id',
'parent-col-name': 'subnet_id',
'parent-key-desc': 'UUID of subnet',
'selector-type': 'DICT_SELECTOR',
'in-list': True,
'field-translators':
({'fieldname': 'start',
'desc': 'The start address for the allocation pools',
'translator': value_trans},
{'fieldname': 'end',
'desc': 'The end address for the allocation pools',
'translator': value_trans})}
subnets_dns_nameservers_translator = {
'translation-type': 'LIST',
'table-name': DNS_NAMESERVERS,
'parent-key': 'id',
'parent-col-name': 'subnet_id',
'parent-key-desc': 'UUID of subnet',
'val-col': 'dns_nameserver',
'val-col-desc': 'The DNS server',
'translator': value_trans}
subnets_routes_translator = {
'translation-type': 'HDICT',
'table-name': HOST_ROUTES,
'parent-key': 'id',
'parent-col-name': 'subnet_id',
'parent-key-desc': 'UUID of subnet',
'selector-type': 'DICT_SELECTOR',
'in-list': True,
'field-translators':
({'fieldname': 'destination',
'desc': 'The destination for static route',
'translator': value_trans},
{'fieldname': 'nexthop',
'desc': 'The next hop for the destination',
'translator': value_trans})}
subnets_translator = {
'translation-type': 'HDICT',
'table-name': SUBNETS,
'selector-type': 'DICT_SELECTOR',
'field-translators':
({'fieldname': 'id', 'desc': 'UUID of subnet',
'translator': value_trans},
{'fieldname': 'tenant_id', 'desc': 'tenant ID',
'translator': value_trans},
{'fieldname': 'name', 'desc': 'subnet name',
'translator': value_trans},
{'fieldname': 'network_id', 'desc': 'UUID of attached network',
'translator': value_trans},
{'fieldname': 'ip_version',
'desc': 'The IP version, which is 4 or 6',
'translator': value_trans},
{'fieldname': 'cidr', 'desc': 'The CIDR',
'translator': value_trans},
{'fieldname': 'gateway_ip', 'desc': 'The gateway IP address',
'translator': value_trans},
{'fieldname': 'enable_dhcp', 'desc': 'Is DHCP is enabled or not',
'translator': value_trans},
{'fieldname': 'ipv6_ra_mode', 'desc': 'The IPv6 RA mode',
'translator': value_trans},
{'fieldname': 'ipv6_address_mode',
'desc': 'The IPv6 address mode', 'translator': value_trans},
{'fieldname': 'allocation_pools',
'translator': subnets_allocation_pools_translator},
{'fieldname': 'dns_nameservers',
'translator': subnets_dns_nameservers_translator},
{'fieldname': 'host_routes',
'translator': subnets_routes_translator})}
external_fixed_ips_translator = {
'translation-type': 'HDICT',
'table-name': EXTERNAL_FIXED_IPS,
'parent-key': 'router_id',
'parent-col-name': 'router_id',
'parent-key-desc': 'UUID of router',
'selector-type': 'DICT_SELECTOR',
'in-list': True,
'field-translators':
({'fieldname': 'subnet_id', 'desc': 'UUID of the subnet',
'translator': value_trans},
{'fieldname': 'ip_address', 'desc': 'IP Address',
'translator': value_trans})}
routers_external_gateway_infos_translator = {
'translation-type': 'HDICT',
'table-name': EXTERNAL_GATEWAY_INFOS,
'parent-key': 'id',
'parent-col-name': 'router_id',
'parent-key-desc': 'UUID of router',
'selector-type': 'DICT_SELECTOR',
'field-translators':
({'fieldname': 'network_id', 'desc': 'Network ID',
'translator': value_trans},
{'fieldname': 'enable_snat',
'desc': 'current Source NAT status for router',
'translator': value_trans},
{'fieldname': 'external_fixed_ips',
'translator': external_fixed_ips_translator})}
routers_translator = {
'translation-type': 'HDICT',
'table-name': ROUTERS,
'selector-type': 'DICT_SELECTOR',
'field-translators':
({'fieldname': 'id', 'desc': 'uuid of the router',
'translator': value_trans},
{'fieldname': 'tenant_id', 'desc': 'tenant ID',
'translator': value_trans},
{'fieldname': 'status', 'desc': 'router status',
'translator': value_trans},
{'fieldname': 'admin_state_up',
'desc': 'administrative state of router',
'translator': value_trans},
{'fieldname': 'name', 'desc': 'router name',
'translator': value_trans},
{'fieldname': 'distributed',
'desc': "indicates if it's distributed router ",
'translator': value_trans},
{'fieldname': 'external_gateway_info',
'translator': routers_external_gateway_infos_translator})}
security_group_rules_translator = {
'translation-type': 'HDICT',
'table-name': SECURITY_GROUP_RULES,
'parent-key': 'id',
'parent-col-name': 'security_group_id',
'parent-key-desc': 'uuid of security group',
'selector-type': 'DICT_SELECTOR',
'in-list': True,
'field-translators':
({'fieldname': 'id', 'desc': 'The UUID of the security group rule',
'translator': value_trans},
{'fieldname': 'tenant_id', 'desc': 'tenant ID',
'translator': value_trans},
{'fieldname': 'remote_group_id',
'desc': 'remote group id to associate with security group rule',
'translator': value_trans},
{'fieldname': 'direction',
'desc': 'Direction in which the security group rule is applied',
'translator': value_trans},
{'fieldname': 'ethertype', 'desc': 'IPv4 or IPv6',
'translator': value_trans},
{'fieldname': 'protocol',
'desc': 'protocol that is matched by the security group rule.',
'translator': value_trans},
{'fieldname': 'port_range_min',
'desc': 'Min port number in the range',
'translator': value_trans},
{'fieldname': 'port_range_max',
'desc': 'Max port number in the range',
'translator': value_trans},
{'fieldname': 'remote_ip_prefix',
'desc': 'Remote IP prefix to be associated',
'translator': value_trans})}
security_group_translator = {
'translation-type': 'HDICT',
'table-name': SECURITY_GROUPS,
'selector-type': 'DICT_SELECTOR',
'field-translators':
({'fieldname': 'id', 'desc': 'The UUID for the security group',
'translator': value_trans},
{'fieldname': 'tenant_id', 'desc': 'Tenant ID',
'translator': value_trans},
{'fieldname': 'name', 'desc': 'The security group name',
'translator': value_trans},
{'fieldname': 'description', 'desc': 'security group description',
'translator': value_trans},
{'fieldname': 'security_group_rules',
'translator': security_group_rules_translator})}
TRANSLATORS = [networks_translator, ports_translator, subnets_translator,
routers_translator, security_group_translator,
floating_ips_translator]
def __init__(self, name='', args=None):
super(NeutronV2Driver, self).__init__(name, args=args)
datasource_driver.ExecutionDriver.__init__(self)
self.creds = args
session = ds_utils.get_keystone_session(self.creds)
self.neutron = neutronclient.v2_0.client.Client(session=session)
self.add_executable_method('update_resource_attrs',
[{'name': 'resource_type',
'description': 'resource type (e.g. ' +
'port, network, subnet)'},
{'name': 'id',
'description': 'ID of the resource'},
{'name': 'attr1',
'description': 'attribute name to ' +
'update (e.g. admin_state_up)'},
{'name': 'attr1-value',
'description': 'updated attr1 value'},
{'name': 'attrN',
'description': 'attribute name to ' +
'update'},
{'name': 'attrN-value',
'description': 'updated attrN value'}],
"A wrapper for update_<resource_type>()")
self.add_executable_method('attach_port_security_group',
[{'name': 'port_id',
'description': 'ID of target port'},
{'name': 'security_group_id',
'description': 'ID security group to be '
'attached'}],
"Attach a security group to port (WARNING: "
"may overwrite concurrent changes to "
"port's security groups list.")
self.add_executable_method('detach_port_security_group',
[{'name': 'port_id',
'description': 'ID of target port'},
{'name': 'security_group_id',
'description': 'ID security group to be '
'detached'}],
"Detach a security group to port (WARNING: "
"may overwrite concurrent changes to "
"port's security groups list.")
self.add_executable_client_methods(self.neutron,
'neutronclient.v2_0.client')
self.initialize_update_methods()
self._init_end_start_poll()
@staticmethod
def get_datasource_info():
result = {}
result['id'] = 'neutronv2'
result['description'] = ('Datasource driver that interfaces with '
'OpenStack Networking aka Neutron.')
result['config'] = ds_utils.get_openstack_required_config()
result['config']['lazy_tables'] = constants.OPTIONAL
result['secret'] = ['password']
return result
def initialize_update_methods(self):
networks_method = lambda: self._translate_networks(
self.neutron.list_networks())
self.add_update_method(networks_method, self.networks_translator)
subnets_method = lambda: self._translate_subnets(
self.neutron.list_subnets())
self.add_update_method(subnets_method, self.subnets_translator)
ports_method = lambda: self._translate_ports(self.neutron.list_ports())
self.add_update_method(ports_method, self.ports_translator)
routers_method = lambda: self._translate_routers(
self.neutron.list_routers())
self.add_update_method(routers_method, self.routers_translator)
security_method = lambda: self._translate_security_groups(
self.neutron.list_security_groups())
self.add_update_method(security_method,
self.security_group_translator)
floatingips_method = lambda: self._translate_floating_ips(
self.neutron.list_floatingips())
self.add_update_method(floatingips_method,
self.floating_ips_translator)
@ds_utils.update_state_on_changed(FLOATING_IPS)
def _translate_floating_ips(self, obj):
LOG.debug("floating_ips: %s", dict(obj))
row_data = NeutronV2Driver.convert_objs(obj['floatingips'],
self.floating_ips_translator)
return row_data
@ds_utils.update_state_on_changed(NETWORKS)
def _translate_networks(self, obj):
LOG.debug("networks: %s", dict(obj))
row_data = NeutronV2Driver.convert_objs(obj['networks'],
self.networks_translator)
return row_data
@ds_utils.update_state_on_changed(PORTS)
def _translate_ports(self, obj):
LOG.debug("ports: %s", obj)
row_data = NeutronV2Driver.convert_objs(obj['ports'],
self.ports_translator)
return row_data
@ds_utils.update_state_on_changed(SUBNETS)
def _translate_subnets(self, obj):
LOG.debug("subnets: %s", obj)
row_data = NeutronV2Driver.convert_objs(obj['subnets'],
self.subnets_translator)
return row_data
@ds_utils.update_state_on_changed(ROUTERS)
def _translate_routers(self, obj):
LOG.debug("routers: %s", obj)
row_data = NeutronV2Driver.convert_objs(obj['routers'],
self.routers_translator)
return row_data
@ds_utils.update_state_on_changed(SECURITY_GROUPS)
def _translate_security_groups(self, obj):
LOG.debug("security_groups: %s", obj)
row_data = NeutronV2Driver.convert_objs(obj['security_groups'],
self.security_group_translator)
return row_data
def execute(self, action, action_args):
"""Overwrite ExecutionDriver.execute()."""
# action can be written as a method or an API call.
func = getattr(self, action, None)
if func and self.is_executable(func):
func(action_args)
else:
self._execute_api(self.neutron, action, action_args)
def update_resource_attrs(self, args):
positional_args = args.get('positional', [])
if not positional_args or len(positional_args) < 4:
LOG.error('Args for update_resource_attrs() must contain resource '
'type, resource ID and pairs of key-value attributes to '
'update')
return
resource_type = positional_args.pop(0)
resource_id = positional_args.pop(0)
action = 'update_%s' % resource_type
update_attrs = self._convert_args(positional_args)
body = {resource_type: update_attrs}
action_args = {'named': {resource_type: resource_id,
'body': body}}
self._execute_api(self.neutron, action, action_args)
def attach_port_security_group(self, args):
self._attach_detach_port_security_group(args, attach=True)
def detach_port_security_group(self, args):
self._attach_detach_port_security_group(args, attach=False)
def _attach_detach_port_security_group(self, args, attach):
positional_args = args.get('positional', [])
if not positional_args or len(positional_args) < 2:
LOG.error('Args for attach_port_security_group() must contain '
'port id and security group id')
return
port_id = positional_args[0]
security_group_id = positional_args[1]
# get existing port security groups
port_state = self.neutron.show_port(port_id).get('port')
if not port_state:
return
port_security_groups = port_state.get('security_groups', [])
# add/remove security group
if security_group_id in port_security_groups:
if attach: # no change needed
return
port_security_groups.remove(security_group_id)
else:
if not attach: # no change needed
return
port_security_groups.append(security_group_id)
# call client to make change
# WARNING: intervening changes to security groups binding may be lost
body = {
"port": {
"security_groups": port_security_groups,
}
}
self.neutron.update_port(port_id, body)