Aaron Rosen 86118f6692 nxv3: mass refactor of nsxlib
This patch refactors the current codebase to be more modular
and testable. The main changes are:

  - pull out all of the profile setup logic from the __init__
    method to it's own method _init_nsx_profiles method to make
    testing the code easier.

  - refactors the nsxlib.v3 code to break out all neutron related
    exceptions and cleans up the interface so we can make nsxlib.v3
    it's own standalone library eventually.

To improve:

  - Currently we have nsxlib.v3.dfw_api and nsxlib.v3.firewall,
    we should refactor this code and merge them into one file.

  - refactor nsxlib to section of each api component to it's own
    subclass. For example, nsxlib().port.create() rather than
    nsxlib().create_port(). I think this would be most useful
    for the security group/firewall integration as there are many
    methods there that are needed to interface with nsx as the security
    group feature is requires the most orchestration with nsx.
    Breaking them into a sub class will make things more easy to understand.

Change-Id: If2fe1e014b78703ff0a9cdff1e4e8d45f3a4a16d
2016-08-19 12:28:20 +00:00

310 lines
13 KiB
Python

# Copyright 2015 OpenStack Foundation
# 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.
"""
NSX-V3 Plugin security integration module
"""
from neutron_lib import constants
from oslo_config import cfg
from oslo_log import log
from oslo_utils import excutils
from vmware_nsx._i18n import _LE
from vmware_nsx.common import utils
from vmware_nsx.db import nsx_models
from vmware_nsx.extensions import secgroup_rule_local_ip_prefix
from vmware_nsx.extensions import securitygrouplogging as sg_logging
from vmware_nsx.nsxlib.v3 import dfw_api as firewall
from vmware_nsx.nsxlib.v3 import exceptions
LOG = log.getLogger(__name__)
DEFAULT_SECTION = 'OS Default Section for Neutron Security-Groups'
DEFAULT_SECTION_TAG_NAME = 'neutron_default_dfw_section'
PORT_SG_SCOPE = 'os-security-group'
MAX_NSGROUPS_CRITERIA_TAGS = 10
# XXX this method should be refactored to pull the common stuff out to
# a security_group utils file.
class Security(object):
def _get_l4_protocol_name(self, protocol_number):
if protocol_number is None:
return
protocol_number = constants.IP_PROTOCOL_MAP.get(protocol_number,
protocol_number)
protocol_number = int(protocol_number)
if protocol_number == 6:
return firewall.TCP
elif protocol_number == 17:
return firewall.UDP
elif protocol_number == 1:
return firewall.ICMPV4
else:
return protocol_number
def _get_direction(self, sg_rule):
return (
firewall.IN if sg_rule['direction'] == 'ingress' else firewall.OUT
)
def _decide_service(self, sg_rule):
l4_protocol = self._get_l4_protocol_name(sg_rule['protocol'])
direction = self._get_direction(sg_rule)
if l4_protocol in [firewall.TCP, firewall.UDP]:
# If port_range_min is not specified then we assume all ports are
# matched, relying on neutron to perform validation.
source_ports = []
if sg_rule['port_range_min'] is None:
destination_ports = []
elif sg_rule['port_range_min'] != sg_rule['port_range_max']:
# NSX API requires a non-empty range (e.g - '22-23')
destination_ports = ['%(port_range_min)s-%(port_range_max)s'
% sg_rule]
else:
destination_ports = ['%(port_range_min)s' % sg_rule]
if direction == firewall.OUT:
source_ports, destination_ports = destination_ports, []
return self.get_nsservice(firewall.L4_PORT_SET_NSSERVICE,
l4_protocol=l4_protocol,
source_ports=source_ports,
destination_ports=destination_ports)
elif l4_protocol == firewall.ICMPV4:
return self.get_nsservice(firewall.ICMP_TYPE_NSSERVICE,
protocol=l4_protocol,
icmp_type=sg_rule['port_range_min'],
icmp_code=sg_rule['port_range_max'])
elif l4_protocol is not None:
return self.get_nsservice(firewall.IP_PROTOCOL_NSSERVICE,
protocol_number=l4_protocol)
def _get_fw_rule_from_sg_rule(self, sg_rule, nsgroup_id, rmt_nsgroup_id,
logged, action):
# IPV4 or IPV6
ip_protocol = sg_rule['ethertype'].upper()
direction = self._get_direction(sg_rule)
if sg_rule.get(secgroup_rule_local_ip_prefix.LOCAL_IP_PREFIX):
local_ip_prefix = self.get_ip_cidr_reference(
sg_rule[secgroup_rule_local_ip_prefix.LOCAL_IP_PREFIX],
ip_protocol)
else:
local_ip_prefix = None
source = None
local_group = self.get_nsgroup_reference(nsgroup_id)
if sg_rule['remote_ip_prefix'] is not None:
source = self.get_ip_cidr_reference(
sg_rule['remote_ip_prefix'], ip_protocol)
destination = local_ip_prefix or local_group
else:
if rmt_nsgroup_id:
source = self.get_nsgroup_reference(rmt_nsgroup_id)
destination = local_ip_prefix or local_group
if direction == firewall.OUT:
source, destination = destination, source
service = self._decide_service(sg_rule)
name = sg_rule['id']
return self.get_firewall_rule_dict(name, source,
destination, direction,
ip_protocol, service,
action, logged)
def create_firewall_rules(self, context, section_id, nsgroup_id,
logging_enabled, action, security_group_rules):
# 1. translate rules
# 2. insert in section
# 3. save mappings
firewall_rules = []
for sg_rule in security_group_rules:
remote_nsgroup_id = self._get_remote_nsg_mapping(
context, sg_rule, nsgroup_id)
fw_rule = self._get_fw_rule_from_sg_rule(
sg_rule, nsgroup_id, remote_nsgroup_id,
logging_enabled, action)
firewall_rules.append(fw_rule)
return self.add_rules_in_section(firewall_rules, section_id)
def _process_firewall_section_rules_logging_for_update(self, section_id,
logging_enabled):
rules = self.get_section_rules(section_id).get('results', [])
update_rules = False
for rule in rules:
if rule['logged'] != logging_enabled:
rule['logged'] = logging_enabled
update_rules = True
return rules if update_rules else None
def set_firewall_rule_logging_for_section(self, section_id, logging):
rules = self._process_firewall_section_rules_logging_for_update(
section_id, logging)
self.update_section(section_id, rules=rules)
def update_security_group_on_backend(self, context, security_group):
nsgroup_id, section_id = self.get_sg_mappings(context.session,
security_group['id'])
name = self.get_nsgroup_name(security_group)
description = security_group['description']
logging = (cfg.CONF.nsx_v3.log_security_groups_allowed_traffic or
security_group[sg_logging.LOGGING])
rules = self._process_firewall_section_rules_logging_for_update(
section_id, logging)
self.update_nsgroup(nsgroup_id, name, description)
self.update_section(section_id, name, description, rules=rules)
def get_nsgroup_name(self, security_group):
# NOTE(roeyc): We add the security-group id to the NSGroup name,
# for usability purposes.
return '%(name)s - %(id)s' % security_group
def save_sg_rule_mappings(self, session, firewall_rules):
# REVISIT(roeyc): This method should take care db access only.
rules = [(rule['display_name'], rule['id']) for rule in firewall_rules]
with session.begin(subtransactions=True):
for neutron_id, nsx_id in rules:
mapping = nsx_models.NeutronNsxRuleMapping(
neutron_id=neutron_id, nsx_id=nsx_id)
session.add(mapping)
return mapping
# XXX db calls should not be here...
def save_sg_mappings(self, session, sg_id, nsgroup_id, section_id):
with session.begin(subtransactions=True):
session.add(
nsx_models.NeutronNsxFirewallSectionMapping(neutron_id=sg_id,
nsx_id=section_id))
session.add(
nsx_models.NeutronNsxSecurityGroupMapping(neutron_id=sg_id,
nsx_id=nsgroup_id))
# XXX db calls should not be here...
def get_sg_rule_mapping(self, session, rule_id):
rule_mapping = session.query(
nsx_models.NeutronNsxRuleMapping).filter_by(
neutron_id=rule_id).one()
return rule_mapping.nsx_id
# XXX db calls should not be here...
def get_sg_mappings(self, session, sg_id):
nsgroup_mapping = session.query(
nsx_models.NeutronNsxSecurityGroupMapping
).filter_by(neutron_id=sg_id).one()
section_mapping = session.query(
nsx_models.NeutronNsxFirewallSectionMapping
).filter_by(neutron_id=sg_id).one()
return nsgroup_mapping.nsx_id, section_mapping.nsx_id
def _get_remote_nsg_mapping(self, context, sg_rule, nsgroup_id):
remote_nsgroup_id = None
remote_group_id = sg_rule.get('remote_group_id')
# skip unnecessary db access when possible
if remote_group_id == sg_rule['security_group_id']:
remote_nsgroup_id = nsgroup_id
elif remote_group_id:
remote_nsgroup_id, s = self.get_sg_mappings(context.session,
remote_group_id)
return remote_nsgroup_id
def get_lport_tags_for_security_groups(self, secgroups):
if len(secgroups) > MAX_NSGROUPS_CRITERIA_TAGS:
raise exceptions.NumberOfNsgroupCriteriaTagsReached(
max_num=MAX_NSGROUPS_CRITERIA_TAGS)
tags = []
for sg in secgroups:
tags = utils.add_v3_tag(tags, PORT_SG_SCOPE, sg)
if not tags:
# This port shouldn't be associated with any security-group
tags = [{'scope': PORT_SG_SCOPE, 'tag': None}]
return tags
def update_lport_with_security_groups(self, context, lport_id,
original, updated):
added = set(updated) - set(original)
removed = set(original) - set(updated)
for sg_id in added:
nsgroup_id, s = self.get_sg_mappings(context.session, sg_id)
try:
self.add_nsgroup_members(
nsgroup_id, firewall.LOGICAL_PORT, [lport_id])
except exceptions.NSGroupIsFull:
for sg_id in added:
nsgroup_id, s = self.get_sg_mappings(
context.session, sg_id)
# NOTE(roeyc): If the port was not added to the nsgroup
# yet, then this request will silently fail.
self.remove_nsgroup_member(
nsgroup_id, firewall.LOGICAL_PORT, lport_id)
raise exceptions.SecurityGroupMaximumCapacityReached(
sg_id=sg_id)
except exceptions.ResourceNotFound:
with excutils.save_and_reraise_exception():
LOG.error(_LE("NSGroup %s doesn't exists"), nsgroup_id)
for sg_id in removed:
nsgroup_id, s = self.get_sg_mappings(context.session, sg_id)
self.remove_nsgroup_member(
nsgroup_id, firewall.LOGICAL_PORT, lport_id)
def _init_default_section(self, name, description, nested_groups):
fw_sections = self.list_sections()
for section in fw_sections:
if section['display_name'] == name:
break
else:
tags = utils.build_v3_api_version_tag()
section = self.create_empty_section(
name, description, nested_groups, tags)
block_rule = self.get_firewall_rule_dict(
'Block All', action=firewall.DROP,
logged=cfg.CONF.nsx_v3.log_security_groups_blocked_traffic)
# TODO(roeyc): Add additional rules to allow IPV6 NDP.
dhcp_client = self.get_nsservice(firewall.L4_PORT_SET_NSSERVICE,
l4_protocol=firewall.UDP,
source_ports=[67],
destination_ports=[68])
dhcp_client_rule_in = self.get_firewall_rule_dict(
'DHCP Reply', direction=firewall.IN, service=dhcp_client)
dhcp_server = (
self.get_nsservice(firewall.L4_PORT_SET_NSSERVICE,
l4_protocol=firewall.UDP,
source_ports=[68],
destination_ports=[67]))
dhcp_client_rule_out = self.get_firewall_rule_dict(
'DHCP Request', direction=firewall.OUT, service=dhcp_server)
self.update_section(section['id'],
name, section['description'],
applied_tos=nested_groups,
rules=[dhcp_client_rule_out,
dhcp_client_rule_in,
block_rule])
return section['id']