578 lines
22 KiB
Python
578 lines
22 KiB
Python
# 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 oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
|
|
from vmware_nsx.db import db as nsx_db
|
|
from vmware_nsx.plugins.nsx_v3 import utils as nsx_utils
|
|
|
|
from vmware_nsxlib import v3
|
|
from vmware_nsxlib.v3 import config
|
|
from vmware_nsxlib.v3 import exceptions as nsxlib_exc
|
|
from vmware_nsxlib.v3 import resources as nsx_resources
|
|
|
|
from gbpservice.neutron.services.grouppolicy.common import constants as g_const
|
|
from gbpservice.neutron.services.grouppolicy.common import exceptions as gpexc
|
|
from gbpservice.neutron.services.grouppolicy.drivers import (
|
|
resource_mapping as api)
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
SINGLE_ENTRY_ID = 'GBP'
|
|
DRIVER_NAME = 'NSX Policy driver'
|
|
DRIVER_OPT_GROUP = 'NSX_POLICY'
|
|
NSX_V3_GROUP = 'nsx_v3'
|
|
|
|
policy_opts = [
|
|
cfg.StrOpt('nsx_policy_manager',
|
|
help=_("Nsx Policy manager IP address or host.")),
|
|
cfg.StrOpt('nsx_policy_username',
|
|
help=_("Nsx Policy username.")),
|
|
cfg.StrOpt('nsx_policy_password',
|
|
help=_("Nsx Policy password.")),
|
|
cfg.StrOpt('nsx_manager_thumbprint',
|
|
help=_("Thumbprint of nsx manager"))
|
|
]
|
|
|
|
cfg.CONF.register_opts(policy_opts, DRIVER_OPT_GROUP)
|
|
|
|
|
|
class HierarchicalContractsNotSupported(gpexc.GroupPolicyBadRequest):
|
|
message = ("Hierarchy in rule sets is not supported with %s." %
|
|
DRIVER_NAME)
|
|
|
|
|
|
class UpdateOperationNotSupported(gpexc.GroupPolicyBadRequest):
|
|
message = ("Update operation on this object is not supported with %s." %
|
|
DRIVER_NAME)
|
|
|
|
|
|
class ProxyGroupsNotSupported(gpexc.GroupPolicyBadRequest):
|
|
message = ("Proxy groups are not supported with %s." % DRIVER_NAME)
|
|
|
|
|
|
#TODO(annak): remove when ipv6 is supported + add support for ICMPv6 service
|
|
class Ipv6NotSupported(gpexc.GroupPolicyBadRequest):
|
|
message = ("Ipv6 not supported with %s" % DRIVER_NAME)
|
|
|
|
|
|
class UpdateClassifierProtocolNotSupported(gpexc.GroupPolicyBadRequest):
|
|
message = ("Update operation on classifier protocol is not supported "
|
|
"with %s" % DRIVER_NAME)
|
|
|
|
|
|
class ProtocolNotSupported(gpexc.GroupPolicyBadRequest):
|
|
message = ("Unsupported classifier protocol. Only icmp, tcp and udp are "
|
|
"supported with %s" % DRIVER_NAME)
|
|
|
|
|
|
def append_in_dir(name):
|
|
return name + '_I'
|
|
|
|
|
|
def append_out_dir(name):
|
|
return name + '_O'
|
|
|
|
|
|
def generate_nsx_name(uuid, name, tag=None, maxlen=80):
|
|
short_uuid = '_' + uuid[:5] + '...' + uuid[-5:]
|
|
maxlen = maxlen - len(short_uuid)
|
|
if not name:
|
|
name = ''
|
|
if tag:
|
|
maxlen = maxlen - len(tag) - 1
|
|
return name[:maxlen] + '_' + tag + short_uuid
|
|
else:
|
|
return name[:maxlen] + short_uuid
|
|
|
|
|
|
class NsxPolicyMappingDriver(api.ResourceMappingDriver):
|
|
"""Nsx Policy Mapping driver for Group Policy plugin.
|
|
|
|
This mapping driver is only supported with nsxv3 core plugin.
|
|
NSX Manager is the network virtualization appliance configured by the core
|
|
plugin.
|
|
NSX Policy is a separate appliance that provides grouping API. Behind the
|
|
scenes, NSX Policy configures same NSX manager.
|
|
|
|
At current phase of development, security is achieved via NSX Policy,
|
|
while connectivity functionality is inherited from resource mapping driver.
|
|
|
|
This driver configures services, connectivity rules and grouping objects
|
|
on NSX Policy. In addition, it configures logical port tag directly on
|
|
NSX manager, in order to provide port membership in the desired group.
|
|
|
|
The driver does not maintain state of its own (no db extension). This is
|
|
for sake of reducing failure recovery problems, at cost of making few more
|
|
backend roundtrips.
|
|
"""
|
|
def get_nsxpolicy_lib(self):
|
|
""" Prepare agent for NSX Policy API calls"""
|
|
nsxlib_config = config.NsxLibConfig(
|
|
nsx_api_managers=[cfg.CONF.NSX_POLICY.nsx_policy_manager],
|
|
username=cfg.CONF.NSX_POLICY.nsx_policy_username,
|
|
password=cfg.CONF.NSX_POLICY.nsx_policy_password)
|
|
|
|
return v3.NsxPolicyLib(nsxlib_config)
|
|
|
|
def get_nsxmanager_client(self):
|
|
"""Prepare agent for NSX Manager API calls"""
|
|
nsxlib = nsx_utils.get_nsxlib_wrapper()
|
|
|
|
return nsxlib.client
|
|
|
|
def initialize(self):
|
|
super(NsxPolicyMappingDriver, self).initialize()
|
|
self._gbp_plugin = None
|
|
self.nsx_policy = self.get_nsxpolicy_lib()
|
|
self.policy_api = self.nsx_policy.policy_api
|
|
|
|
nsx_manager_client = self.get_nsxmanager_client()
|
|
self.nsx_port = nsx_resources.LogicalPort(nsx_manager_client)
|
|
self._verify_enforcement_point()
|
|
|
|
# TODO(annak): add validation for core plugin (can only be nsxv3)
|
|
|
|
def _verify_enforcement_point(self):
|
|
"""Configure NSX Policy to enforce grouping rules on NSX Manager"""
|
|
|
|
# We only support a single NSX manager at this point
|
|
nsx_manager_ip = cfg.CONF.nsx_v3.nsx_api_managers[0]
|
|
nsx_manager_username = cfg.CONF.nsx_v3.nsx_api_user[0]
|
|
nsx_manager_password = cfg.CONF.nsx_v3.nsx_api_password[0]
|
|
nsx_manager_thumbprint = cfg.CONF.NSX_POLICY.nsx_manager_thumbprint
|
|
epoints = self.nsx_policy.enforcement_point.list()
|
|
for ep in epoints:
|
|
conn = ep['connection_info']
|
|
if conn and conn['enforcement_point_address'] == nsx_manager_ip:
|
|
LOG.debug('Enforcement point for %s already exists (%s)',
|
|
nsx_manager_ip, ep['id'])
|
|
return
|
|
|
|
LOG.info('Creating enforcement point for %s', nsx_manager_ip)
|
|
self.nsx_policy.enforcement_point.create_or_overwrite(
|
|
name=nsx_manager_ip,
|
|
ep_id=SINGLE_ENTRY_ID,
|
|
ip_address=nsx_manager_ip,
|
|
username=nsx_manager_username,
|
|
password=nsx_manager_password,
|
|
thumbprint=nsx_manager_thumbprint)
|
|
|
|
def _create_domain(self, context):
|
|
project_id = context.current['project_id']
|
|
tenant_name = context._plugin_context.tenant_name
|
|
domain_name = generate_nsx_name(project_id, tenant_name)
|
|
|
|
LOG.info('Creating domain %(domain)s for project %(project)s',
|
|
{'domain': domain_name,
|
|
'project': project_id})
|
|
|
|
self.nsx_policy.domain.create_or_overwrite(
|
|
name=domain_name,
|
|
domain_id=project_id,
|
|
description=_('Domain for tenant %s') % tenant_name)
|
|
|
|
self.nsx_policy.deployment_map.create_or_overwrite(
|
|
name=domain_name,
|
|
map_id=project_id,
|
|
domain_id=project_id,
|
|
ep_id=SINGLE_ENTRY_ID)
|
|
|
|
def _delete_domain(self, project_id):
|
|
try:
|
|
self.nsx_policy.deployment_map.delete(project_id,
|
|
domain_id=project_id)
|
|
except nsxlib_exc.ManagerError:
|
|
LOG.warning('Domain %s is not deployed on backend',
|
|
project_id)
|
|
|
|
try:
|
|
self.nsx_policy.domain.delete(project_id)
|
|
except nsxlib_exc.ManagerError:
|
|
LOG.warning('Domain %s was not found on backend',
|
|
project_id)
|
|
|
|
def _create_or_update_communication_profile(self, profile_id, name,
|
|
description, rules,
|
|
update_flow=False):
|
|
|
|
services = [rule['policy_classifier_id']
|
|
for rule in rules]
|
|
|
|
self.nsx_policy.comm_profile.create_or_overwrite(
|
|
name=generate_nsx_name(profile_id, name),
|
|
profile_id=profile_id,
|
|
description=description,
|
|
services=services)
|
|
|
|
def _split_rules_by_direction(self, context, rules):
|
|
in_dir = [g_const.GP_DIRECTION_BI, g_const.GP_DIRECTION_IN]
|
|
out_dir = [g_const.GP_DIRECTION_BI, g_const.GP_DIRECTION_OUT]
|
|
|
|
in_rules = []
|
|
out_rules = []
|
|
|
|
for rule in rules:
|
|
classifier = context._plugin.get_policy_classifier(
|
|
context._plugin_context,
|
|
rule['policy_classifier_id'])
|
|
direction = classifier['direction']
|
|
if direction in in_dir:
|
|
in_rules.append(rule)
|
|
|
|
if direction in out_dir:
|
|
out_rules.append(rule)
|
|
|
|
return in_rules, out_rules
|
|
|
|
def _delete_comm_profile(self, comm_profile_id):
|
|
try:
|
|
self.nsx_policy.comm_profile.delete(comm_profile_id)
|
|
except nsxlib_exc.ManagerError:
|
|
LOG.error('Communication profile %s not found on backend',
|
|
comm_profile_id)
|
|
|
|
def _create_or_update_policy_rule_set(self, context, update_flow=False):
|
|
|
|
rule_set_id = context.current['id']
|
|
|
|
rules = self.gbp_plugin.get_policy_rules(
|
|
context._plugin_context,
|
|
{'id': context.current['policy_rules']})
|
|
|
|
in_rules, out_rules = self._split_rules_by_direction(context, rules)
|
|
|
|
if in_rules:
|
|
self._create_or_update_communication_profile(
|
|
append_in_dir(rule_set_id),
|
|
generate_nsx_name(rule_set_id,
|
|
context.current['name'],
|
|
'_IN'),
|
|
context.current['description'] + '(ingress)',
|
|
in_rules)
|
|
elif update_flow:
|
|
self._delete_comm_profile(append_in_dir(rule_set_id))
|
|
|
|
if out_rules:
|
|
self._create_or_update_communication_profile(
|
|
append_out_dir(rule_set_id),
|
|
generate_nsx_name(rule_set_id,
|
|
context.current['name'],
|
|
'_OUT'),
|
|
context.current['description'] + '(egress)',
|
|
out_rules)
|
|
elif update_flow:
|
|
self._delete_comm_profile(append_out_dir(rule_set_id))
|
|
|
|
def _filter_ptgs_by_ruleset(self, ptgs, ruleset_id):
|
|
providing_ptgs = [ptg['id'] for ptg in ptgs
|
|
if ruleset_id in ptg['provided_policy_rule_sets']]
|
|
consuming_ptgs = [ptg['id'] for ptg in ptgs
|
|
if ruleset_id in ptg['consumed_policy_rule_sets']]
|
|
return providing_ptgs, consuming_ptgs
|
|
|
|
def _map_rule_set(self, ptgs, profiles, project_id,
|
|
group_id, ruleset_id, delete_flow):
|
|
|
|
providing_ptgs, consuming_ptgs = self._filter_ptgs_by_ruleset(
|
|
ptgs, ruleset_id)
|
|
|
|
ruleset_in = append_in_dir(ruleset_id)
|
|
ruleset_out = append_out_dir(ruleset_id)
|
|
if not consuming_ptgs or not providing_ptgs:
|
|
if not delete_flow:
|
|
return
|
|
if not consuming_ptgs and not providing_ptgs:
|
|
return
|
|
|
|
# we need to delete map entry if exists
|
|
for ruleset in (ruleset_in, ruleset_out):
|
|
if ruleset in profiles:
|
|
try:
|
|
self.nsx_policy.comm_map.delete(project_id, ruleset)
|
|
except nsxlib_exc.ManagerError:
|
|
pass
|
|
return
|
|
|
|
if ruleset_in in profiles:
|
|
self.nsx_policy.comm_map.create_or_overwrite(
|
|
name=ruleset_in,
|
|
domain_id=project_id,
|
|
map_id=ruleset_in,
|
|
description="GBP ruleset ingress",
|
|
profile_id=ruleset_in,
|
|
source_groups=consuming_ptgs,
|
|
dest_groups=providing_ptgs)
|
|
|
|
if ruleset_out in profiles:
|
|
self.nsx_policy.comm_map.create_or_overwrite(
|
|
name=ruleset_out,
|
|
domain_id=project_id,
|
|
map_id=ruleset_out,
|
|
description="GBP ruleset egress",
|
|
profile_id=ruleset_out,
|
|
source_groups=providing_ptgs,
|
|
dest_groups=consuming_ptgs)
|
|
|
|
def _map_group_rule_sets(self, context, group_id,
|
|
provided_policy_rule_sets,
|
|
consumed_policy_rule_sets,
|
|
delete_flow=False):
|
|
|
|
project_id = context.current['project_id']
|
|
|
|
profiles = self.nsx_policy.comm_profile.list()
|
|
profiles = [p['id'] for p in profiles]
|
|
|
|
# create communication maps
|
|
ptgs = context._plugin.get_policy_target_groups(
|
|
context._plugin_context)
|
|
for ruleset in provided_policy_rule_sets:
|
|
self._map_rule_set(ptgs, profiles, project_id,
|
|
group_id, ruleset, delete_flow)
|
|
|
|
for ruleset in consumed_policy_rule_sets:
|
|
self._map_rule_set(ptgs, profiles, project_id,
|
|
group_id, ruleset, delete_flow)
|
|
|
|
# overrides base class, called from base group_create_postcommit
|
|
# REVISIT(annak): Suggest a better design for driver-specific callbacks,
|
|
# based on connectivity vs. security
|
|
def _set_sg_rules_for_subnets(self, context, subnets,
|
|
provided_policy_rule_sets,
|
|
consumed_policy_rule_sets):
|
|
pass
|
|
|
|
# overrides base class, called from base group_delete_postcommit
|
|
def _unset_sg_rules_for_subnets(self, context, subnets,
|
|
provided_policy_rule_sets,
|
|
consumed_policy_rule_sets):
|
|
pass
|
|
|
|
# Overrides base class
|
|
def _update_sgs_on_ptg(self, context, ptg_id,
|
|
provided_policy_rule_sets,
|
|
consumed_policy_rule_sets, op):
|
|
|
|
group_id = context.current['id']
|
|
|
|
self._map_group_rule_sets(
|
|
context, group_id,
|
|
provided_policy_rule_sets,
|
|
consumed_policy_rule_sets,
|
|
delete_flow=(op == "DISASSOCIATE"))
|
|
|
|
def create_policy_action_precommit(self, context):
|
|
pass
|
|
|
|
def create_policy_action_postcommit(self, context):
|
|
super(NsxPolicyMappingDriver,
|
|
self).create_policy_action_postcommit(context)
|
|
|
|
def create_policy_classifier_precommit(self, context):
|
|
if context.current['protocol'] not in ('icmp', 'tcp', 'udp'):
|
|
raise ProtocolNotSupported()
|
|
|
|
def create_policy_classifier_postcommit(self, context):
|
|
classifier = context.current
|
|
|
|
if classifier['protocol'] == 'icmp':
|
|
self.nsx_policy.icmp_service.create_or_overwrite(
|
|
name=classifier['name'],
|
|
service_id=classifier['id'],
|
|
description=classifier['description'])
|
|
return
|
|
|
|
ports = []
|
|
if classifier['port_range']:
|
|
port_range = classifier['port_range'].split(':', 1)
|
|
lower = int(port_range[0])
|
|
upper = int(port_range[-1]) + 1
|
|
ports = [str(p) for p in range(lower, upper)]
|
|
|
|
# service entry in nsx policy has single direction
|
|
# directions will be enforced on communication profile level
|
|
self.nsx_policy.service.create_or_overwrite(
|
|
name=generate_nsx_name(classifier['id'], classifier['name']),
|
|
service_id=classifier['id'],
|
|
description=classifier['description'],
|
|
protocol=classifier['protocol'],
|
|
dest_ports=ports)
|
|
|
|
def create_policy_rule_precommit(self, context):
|
|
pass
|
|
|
|
def create_policy_rule_postcommit(self, context, transaction=None):
|
|
pass
|
|
|
|
def create_policy_rule_set_precommit(self, context):
|
|
if context.current['child_policy_rule_sets']:
|
|
raise HierarchicalContractsNotSupported()
|
|
|
|
def create_policy_rule_set_postcommit(self, context):
|
|
self._create_or_update_policy_rule_set(context)
|
|
|
|
def create_policy_target_precommit(self, context):
|
|
super(NsxPolicyMappingDriver,
|
|
self).create_policy_target_precommit(context)
|
|
|
|
def _tag_port(self, context, port_id, tag):
|
|
# Translate neutron port id to nsx port id
|
|
_net_id, nsx_port_id = nsx_db.get_nsx_switch_and_port_id(
|
|
context._plugin_context.session, port_id)
|
|
self.nsx_port.update(nsx_port_id, None,
|
|
tags_update=[{'scope': 'gbp',
|
|
'tag': tag}])
|
|
|
|
def _get_project_ptgs(self, context, project_id):
|
|
ptgs = context._plugin.get_policy_target_groups(
|
|
context._plugin_context)
|
|
|
|
return [ptg for ptg in ptgs if ptg['project_id'] == project_id]
|
|
|
|
def create_policy_target_postcommit(self, context):
|
|
if not context.current['port_id']:
|
|
self._use_implicit_port(context)
|
|
self._tag_port(context,
|
|
context.current['port_id'],
|
|
context.current['policy_target_group_id'])
|
|
|
|
# Below is inherited behaviour
|
|
self._update_cluster_membership(
|
|
context, new_cluster_id=context.current['cluster_id'])
|
|
self._associate_fip_to_pt(context)
|
|
|
|
def create_policy_target_group_precommit(self, context):
|
|
if context.current.get('proxied_group_id'):
|
|
raise ProxyGroupsNotSupported()
|
|
|
|
super(NsxPolicyMappingDriver,
|
|
self).create_policy_target_group_precommit(context)
|
|
|
|
def create_policy_target_group_postcommit(self, context):
|
|
# create the group on backend
|
|
group_id = context.current['id']
|
|
project_id = context.current['project_id']
|
|
|
|
# create the domain for this project if needed
|
|
project_ptgs = self._get_project_ptgs(context, project_id)
|
|
if len(project_ptgs) == 1:
|
|
# we've just created the first group for this project
|
|
# need to create a domain for the project on backend
|
|
self._create_domain(context)
|
|
|
|
self.nsx_policy.group.create_or_overwrite(
|
|
name=generate_nsx_name(group_id, context.current['name']),
|
|
domain_id=project_id,
|
|
group_id=group_id,
|
|
description=context.current['description'],
|
|
cond_val=group_id)
|
|
|
|
# This will take care of connectivity and invoke overriden
|
|
# callbacks defined above for security
|
|
super(NsxPolicyMappingDriver,
|
|
self).create_policy_target_group_postcommit(context)
|
|
|
|
def delete_policy_target_group_postcommit(self, context):
|
|
group_id = context.current['id']
|
|
project_id = context.current['project_id']
|
|
self.nsx_policy.group.delete(project_id, group_id)
|
|
|
|
# delete the domain for this project if needed
|
|
project_ptgs = self._get_project_ptgs(context, project_id)
|
|
if len(project_ptgs) == 0:
|
|
# we've just deleted the last group for this project
|
|
# need to clean up the project domain on backend
|
|
self._delete_domain(project_id)
|
|
|
|
# This will take care of connectivity and invoke overriden
|
|
# callbacks defined above for security
|
|
super(NsxPolicyMappingDriver,
|
|
self).delete_policy_target_group_postcommit(context)
|
|
|
|
def delete_policy_classifier_precommit(self, context):
|
|
pass
|
|
|
|
def delete_policy_classifier_postcommit(self, context):
|
|
classifier_id = context.current['id']
|
|
if context.current['protocol'] == 'icmp':
|
|
self.nsx_policy.icmp_service.delete(classifier_id)
|
|
else:
|
|
self.nsx_policy.service.delete(classifier_id)
|
|
|
|
def delete_policy_rule_set_precommit(self, context):
|
|
pass
|
|
|
|
def delete_policy_rule_set_postcommit(self, context):
|
|
ruleset_id = context.current['id']
|
|
rules = self.gbp_plugin.get_policy_rules(
|
|
context._plugin_context,
|
|
{'id': context.current['policy_rules']})
|
|
|
|
in_rules, out_rules = self._split_rules_by_direction(context, rules)
|
|
if in_rules:
|
|
self._delete_comm_profile(append_in_dir(ruleset_id))
|
|
|
|
if out_rules:
|
|
self._delete_comm_profile(append_out_dir(ruleset_id))
|
|
|
|
def delete_policy_target_postcommit(self, context):
|
|
# This is inherited behavior without:
|
|
# 1. sg disassociation
|
|
# 2. proxy handling
|
|
port_id = context.current['port_id']
|
|
for fip in context.fips:
|
|
self._delete_fip(context._plugin_context,
|
|
fip.floatingip_id)
|
|
self._cleanup_port(context._plugin_context, port_id)
|
|
|
|
def update_policy_rule_set_precommit(self, context):
|
|
self._reject_shared(context.current, 'policy_rule_set')
|
|
|
|
def update_policy_rule_set_postcommit(self, context):
|
|
self._create_or_update_policy_rule_set(context, update_flow=True)
|
|
|
|
def update_policy_target_precommit(self, context):
|
|
# Parent call verifies change of PTG is not supported
|
|
super(NsxPolicyMappingDriver,
|
|
self).update_policy_target_precommit(context)
|
|
|
|
def update_policy_target_postcommit(self, context):
|
|
# Since change of PTG is not supported, nothing to add here
|
|
super(NsxPolicyMappingDriver,
|
|
self).update_policy_target_postcommit(context)
|
|
|
|
def update_policy_rule_precommit(self, context):
|
|
raise UpdateOperationNotSupported()
|
|
|
|
def update_policy_rule_postcommit(self, context):
|
|
pass
|
|
|
|
def update_policy_action_precommit(self, context):
|
|
raise UpdateOperationNotSupported()
|
|
|
|
def update_policy_classifier_precommit(self, context):
|
|
if context.current['protocol'] != context.original['protocol']:
|
|
raise UpdateClassifierProtocolNotSupported()
|
|
|
|
def update_policy_classifier_postcommit(self, context):
|
|
self.create_policy_classifier_postcommit(context)
|
|
|
|
def create_l3_policy_precommit(self, context):
|
|
if context.current['ip_version'] != 4:
|
|
raise Ipv6NotSupported()
|
|
|
|
super(NsxPolicyMappingDriver,
|
|
self).create_l3_policy_precommit(context)
|