PSARC/2016/531 OpenStack Neutron Security Groups
24691270 add support for OpenStack Neutron Security Groups
--- a/components/openstack/horizon/files/overrides.py Tue Nov 15 16:50:44 2016 -0800
+++ b/components/openstack/horizon/files/overrides.py Tue Nov 15 18:00:12 2016 -0800
@@ -28,39 +28,16 @@
LiveMigrateForm
from openstack_dashboard.dashboards.admin.instances.tables import \
AdminInstancesTable, MigrateInstance
-from openstack_dashboard.dashboards.project.access_and_security.tabs import \
- AccessAndSecurityTabs, APIAccessTab, FloatingIPsTab, KeypairsTab
from openstack_dashboard.dashboards.project.images.images.tables import \
ImagesTable, CreateVolumeFromImage
from openstack_dashboard.dashboards.project.instances import tables \
as project_tables
from openstack_dashboard.dashboards.project.instances.tables import \
- InstancesTable, TogglePause, EditInstanceSecurityGroups
+ InstancesTable, TogglePause
from openstack_dashboard.dashboards.project.instances.workflows import \
create_instance, update_instance
-# Remove Security Groups from the LaunchInstance workflow
-
-class SolarisSetAccessControlsAction(create_instance.SetAccessControlsAction):
- def __init__(self, request, *args, **kwargs):
- super(SolarisSetAccessControlsAction, self).__init__(request,
- *args,
- **kwargs)
-
- del self.fields['groups']
-
- class Meta(object):
- name = _("Access & Security")
- help_text = _("Control access to your instance via key pairs, "
- "and other mechanisms.")
-
- def populate_groups_choices(self, request, context):
- return []
-
-create_instance.SetAccessControls.action_class = SolarisSetAccessControlsAction
-
-
# Bootargs feature:
# Add bootargs feature to 'SetAdvanced' workflow action.
# Part of Project/Compute/Instances/Launch Instance
@@ -168,12 +145,6 @@
contributes = ("bootargs", "bootargs_persist",)
-# Remove 'UpdateInstanceSecurityGroups' from
-# Project/Compute/Instances/Actions/Edit Instance
-update_instance.UpdateInstance.default_steps = (
- update_instance.UpdateInstanceInfo,
-)
-
# Bootargs feature:
# if locally configured to do so add UpdateInstanceBootarg
# to Project/Compute/Instances/Actions/Edit Instance
@@ -182,9 +153,6 @@
UpdateInstanceBootarg,
)
-# Remove 'SecurityGroupsTab' tab from Project/Compute/Access & Security
-AccessAndSecurityTabs.tabs = (KeypairsTab, FloatingIPsTab, APIAccessTab)
-
# Remove 'TogglePause', 'MigrateInstance' actions from
# Admin/System/Instances/Actions
temp = list(AdminInstancesTable._meta.row_actions)
@@ -192,10 +160,8 @@
temp.remove(TogglePause)
AdminInstancesTable._meta.row_actions = tuple(temp)
-# Remove 'EditInstanceSecurityGroups', 'TogglePause' actions from
-# Project/Compute/Instances/Actions
+# Remove 'TogglePause' action from Project/Compute/Instances/Actions
temp = list(InstancesTable._meta.row_actions)
-temp.remove(EditInstanceSecurityGroups)
temp.remove(TogglePause)
InstancesTable._meta.row_actions = tuple(temp)
--- a/components/openstack/neutron/Makefile Tue Nov 15 16:50:44 2016 -0800
+++ b/components/openstack/neutron/Makefile Tue Nov 15 18:00:12 2016 -0800
@@ -125,6 +125,7 @@
files/agent/solaris/net_lib.py \
files/agent/solaris/packetfilter.py \
files/agent/solaris/pd.py \
+ files/agent/solaris/pf_firewall.py \
files/agent/solaris/ra.py \
$(PROTO_DIR)/$(PYTHON_LIB)/neutron/agent/solaris; \
$(MKDIR) $(PROTO_DIR)/$(PYTHON_LIB)/$(DEVICE_TEMPLATE); \
--- a/components/openstack/neutron/files/agent/solaris/packetfilter.py Tue Nov 15 16:50:44 2016 -0800
+++ b/components/openstack/neutron/files/agent/solaris/packetfilter.py Tue Nov 15 18:00:12 2016 -0800
@@ -24,7 +24,7 @@
class PacketFilter(object):
'''Wrapper around Solaris pfctl(1M) command'''
- def __init__(self, anchor_name):
+ def __init__(self, anchor_name, layer2=False):
'''All the PF rules/anchors will be placed under anchor_name.
An anchor is a collection of rules, tables, and other anchors. They
@@ -41,6 +41,14 @@
the methods in this class will operate under that root anchor.
'''
self.root_anchor_path = anchor_name
+ self.layer2 = layer2
+
+ def _build_pfctl_cmd(self, options):
+ cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl']
+ if self.layer2:
+ cmd.append('-2')
+ cmd.extend(options)
+ return cmd
def get_anchor_path(self, subanchors):
if subanchors:
@@ -72,8 +80,7 @@
anchor_rule = anchor_rule + " " + anchor_option
existing_anchor_rules.append(anchor_rule)
process_input = '%s\n' % '\n'.join(sorted(existing_anchor_rules))
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-f', '-']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-f', '-'])
utils.execute(cmd, process_input=process_input)
def remove_nested_anchor_rule(self, parent_anchor, child_anchor):
@@ -94,13 +101,12 @@
return
existing_anchor_rules.remove(rule)
process_input = '%s\n' % '\n'.join(existing_anchor_rules)
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-f', '-']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-f', '-'])
utils.execute(cmd, process_input=process_input)
def list_anchor_rules(self, subanchors=None):
anchor_path = self.get_anchor_path(subanchors)
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path, '-sr']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-sr'])
try:
stdout = utils.execute(cmd)
except:
@@ -109,7 +115,7 @@
def list_anchors(self, subanchors=None):
anchor_path = self.get_anchor_path(subanchors)
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path, '-sA']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-sA'])
try:
stdout = utils.execute(cmd)
except:
@@ -118,29 +124,29 @@
def add_table(self, name, subanchors=None):
anchor_path = self.get_anchor_path(subanchors)
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-t', name, '-T', 'add']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-t', name,
+ '-T', 'add'])
utils.execute(cmd)
def add_table_entry(self, name, cidrs, subanchors=None):
anchor_path = self.get_anchor_path(subanchors)
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-t', name, '-T', 'add']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-t', name,
+ '-T', 'add'])
cmd.extend(cidrs)
utils.execute(cmd)
def replace_table_entry(self, name, cidrs, subanchors=None):
anchor_path = self.get_anchor_path(subanchors)
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-t', name, '-T', 'replace']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-t', name,
+ '-T', 'replace'])
cmd.extend(cidrs)
utils.execute(cmd)
def table_exists(self, name, subanchors=None):
anchor_path = self.get_anchor_path(subanchors)
try:
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-t', name, '-T', 'show']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-t', name,
+ '-T', 'show'])
utils.execute(cmd)
except:
return False
@@ -152,8 +158,8 @@
'deleting') % name)
return
anchor_path = self.get_anchor_path(subanchors)
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-t', name, '-T', 'delete']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-t', name,
+ '-T', 'delete'])
utils.execute(cmd)
def remove_table_entry(self, name, cidrs, subanchors=None):
@@ -162,16 +168,15 @@
'deleting') % name)
return
anchor_path = self.get_anchor_path(subanchors)
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-t', name, '-T', 'delete']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-t', name,
+ '-T', 'delete'])
cmd.extend(cidrs)
utils.execute(cmd)
def add_rules(self, rules, subanchors=None):
anchor_path = self.get_anchor_path(subanchors)
process_input = '\n'.join(rules) + '\n'
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-f', '-']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-f', '-'])
utils.execute(cmd, process_input=process_input)
def _get_rule_label(self, rule):
@@ -188,7 +193,7 @@
# retrieve all the labels for rules, we will delete the state
# after removing the rules
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path, '-sr']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-sr'])
try:
stdout = utils.execute(cmd)
except:
@@ -204,14 +209,12 @@
labels.append(label)
# delete the rules and tables
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path,
- '-F', 'all']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-F', 'all'])
utils.execute(cmd)
# clear the state
for label in labels:
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-k', 'label',
- '-k', label]
+ cmd = self._build_pfctl_cmd(['-k', 'label', '-k', label])
utils.execute(cmd)
def _get_relative_nested_anchors(self, anchorname):
@@ -225,7 +228,7 @@
def remove_anchor_recursively(self, subanchors=None, recurse_ctxt=False):
anchor_path = self.get_anchor_path(subanchors)
- cmd = ['/usr/bin/pfexec', '/usr/sbin/pfctl', '-a', anchor_path, '-sA']
+ cmd = self._build_pfctl_cmd(['-a', anchor_path, '-sA'])
try:
stdout = utils.execute(cmd)
except:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/components/openstack/neutron/files/agent/solaris/pf_firewall.py Tue Nov 15 18:00:12 2016 -0800
@@ -0,0 +1,438 @@
+# Copyright 2012, Nachi Ueno, NTT MCL, Inc.
+# All Rights Reserved.
+#
+# Copyright (c) 2016, Oracle and/or its affiliates. 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.
+
+import collections
+
+import netaddr
+from oslo_config import cfg
+from oslo_log import log as logging
+import six
+
+from neutron._i18n import _LI
+from neutron.agent.common import ovs_lib
+from neutron.agent import firewall
+from neutron.agent.solaris import packetfilter
+from neutron.common import constants
+from neutron.common import utils as c_utils
+
+
+LOG = logging.getLogger(__name__)
+ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES = [131, 135, 143]
+
+DIRECTION_PF_PARAM = {firewall.INGRESS_DIRECTION: 'in',
+ firewall.EGRESS_DIRECTION: 'out'}
+
+
+class PFBaseOVS(ovs_lib.BaseOVS):
+ def get_port_by_id(self, port_id):
+ ports = self.ovsdb.db_find(
+ 'Interface', ('external_ids', '=', {'iface-id': port_id}),
+ columns=['name']).execute()
+ if ports:
+ return ports[0]['name']
+ return None
+
+
+class PFFirewallDriver(firewall.FirewallDriver):
+ """Driver which enforces security groups through PF rules.
+
+ Please look at neutron.agent.firewall.FirewallDriver for more information
+ on how the methods below are called from the Neutron Open vSwitch agent. It
+ all starts at prepare_port_filter() and then _setup_pf_rules() has all the
+ PF based logic to add correct rules on guest instance's port.
+ """
+
+ def __init__(self):
+ self.pf = packetfilter.PacketFilter("_auto/neutron:ovs:agent",
+ layer2=True)
+ # List of port which has security group
+ self.filtered_ports = {}
+ self.unfiltered_ports = {}
+ # List of security group rules for ports residing on this host
+ self.sg_rules = {}
+ # List of security group member ips for ports residing on this host
+ self.sg_members = collections.defaultdict(
+ lambda: collections.defaultdict(list))
+ # Every PF rule needs to be labeled so that we can later kill the state
+ # associated with that rule (using pfctl -k label -k 110). It is hard
+ # to come up with a meaningfully named label for each PF rule, so we
+ # are resorting to numbers here.
+ self.label_num = 100
+ self.portid_to_devname = {}
+
+ def prepare_port_filter(self, port):
+ LOG.debug("Preparing device (%s) filter", port['device'])
+ self._setup_pf_rules(port)
+
+ def apply_port_filter(self, port):
+ """We never call this method
+
+ It exists here to override abstract method of parent abstract class.
+ """
+ pass
+
+ def update_port_filter(self, port):
+ LOG.debug("Updating device (%s) filter", port['device'])
+ self._setup_pf_rules(port, update=True)
+
+ def remove_port_filter(self, port):
+ LOG.debug("Removing device (%s) filter", port['device'])
+ self.unfiltered_ports.pop(port['device'], None)
+ self.filtered_ports.pop(port['device'], None)
+ self._remove_rule_port_sec(port)
+
+ @property
+ def ports(self):
+ return dict(self.filtered_ports, **self.unfiltered_ports)
+
+ def update_security_group_rules(self, sg_id, sg_rules):
+ LOG.debug("Update rules of security group %s(%s)" % (sg_id, sg_rules))
+ self.sg_rules[sg_id] = sg_rules
+
+ def update_security_group_members(self, sg_id, sg_members):
+ LOG.debug("Update members of security group %s(%s)" %
+ (sg_id, sg_members))
+ self.sg_members[sg_id] = collections.defaultdict(list, sg_members)
+
+ def security_group_updated(self, action_type, sec_group_ids,
+ device_ids=None):
+ # TODO(gmoodalb): Extend this later to optimize handling of security
+ # groups update
+ pass
+
+ def _get_label_number(self):
+ self.label_num += 1
+ return self.label_num
+
+ def _remove_rule_port_sec(self, port):
+ device_name = self.portid_to_devname.pop(port['id'], None)
+ if not device_name:
+ LOG.info(_LI("Could not find port: %s. Failed to remove PF rules "
+ "for that port"), port['id'])
+ return
+ LOG.debug("Removing PF rules for device_name(%s)" % device_name)
+ # we need to remove both ingress and egress
+ if '/' in device_name:
+ instance_name, datalink = device_name.split('/')
+ instance_name = instance_name.split(':')[1]
+ ingress = '%s_in' % datalink
+ egress = '%s_out' % datalink
+ else:
+ instance_name = device_name
+ ingress = 'in'
+ egress = 'out'
+ existing_anchor_rules = set(self.pf.list_anchor_rules([instance_name]))
+ existing_anchor_rules.discard('anchor "%s" in on %s all' %
+ (ingress, device_name))
+ existing_anchor_rules.discard('anchor "%s" out on %s all' %
+ (egress, device_name))
+ self.pf.add_rules(list(existing_anchor_rules), [instance_name])
+ if existing_anchor_rules:
+ self.pf.remove_anchor_recursively([instance_name, ingress])
+ self.pf.remove_anchor_recursively([instance_name, egress])
+ else:
+ self.pf.remove_anchor_recursively([instance_name])
+
+ def _setup_pf_rules(self, port, update=False):
+ if not firewall.port_sec_enabled(port):
+ self.unfiltered_ports[port['device']] = port
+ self.filtered_ports.pop(port['device'], None)
+ self._remove_rule_port_sec(port)
+ else:
+ self.filtered_ports[port['device']] = port
+ self.unfiltered_ports.pop(port['device'], None)
+ if update:
+ self._remove_rule_port_sec(port)
+ self._add_rules_by_security_group(port, firewall.INGRESS_DIRECTION)
+ self._add_rules_by_security_group(port, firewall.EGRESS_DIRECTION)
+
+ def _get_device_name(self, port):
+ bridge = PFBaseOVS()
+ device_name = bridge.get_port_by_id(port['id'])
+ if '/' in device_name:
+ device_name = 'dl:' + device_name
+ return device_name
+
+ def _split_sgr_by_ethertype(self, security_group_rules):
+ ipv4_sg_rules = []
+ ipv6_sg_rules = []
+ for rule in security_group_rules:
+ if rule.get('ethertype') == constants.IPv4:
+ ipv4_sg_rules.append(rule)
+ elif rule.get('ethertype') == constants.IPv6:
+ if rule.get('protocol') in ['icmp', 'icmp6']:
+ rule['protocol'] = 'ipv6-icmp'
+ ipv6_sg_rules.append(rule)
+ return ipv4_sg_rules, ipv6_sg_rules
+
+ def _select_sgr_by_direction(self, port, direction):
+ return [rule
+ for rule in port.get('security_group_rules', [])
+ if rule['direction'] == direction]
+
+ def _spoofing_rule(self, port, device_name, ipv4_rules, ipv6_rules):
+ # Fixed rules for traffic sourced from unspecified addresses: 0.0.0.0
+ # and ::
+ # Allow dhcp client discovery and request
+ ipv4_rules.append('pass out on %s proto udp from 0.0.0.0/32 port 68 '
+ 'to 255.255.255.255/32 port 67 label "%s"' %
+ (device_name, self._get_label_number()))
+
+ # Allow neighbor solicitation and multicast listener discovery
+ # from the unspecified address for duplicate address detection
+ for icmp6_type in ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES:
+ ipv6_rules.append('pass out on %s inet6 proto ipv6-icmp '
+ 'from ::/128 to ff02::/16 icmp6-type %s '
+ 'label "%s"' % (device_name, icmp6_type,
+ self._get_label_number()))
+
+ # Fixed rules for traffic after source address is verified
+ # Allow dhcp client renewal and rebinding
+ ipv4_rules.append('pass out on %s proto udp from port 68 to port 67 '
+ 'label "%s"' % (device_name,
+ self._get_label_number()))
+
+ # Drop Router Advts from the port.
+ ipv6_rules.append('block out quick on %s inet6 proto ipv6-icmp '
+ 'icmp6-type %s label "%s"' %
+ (device_name, constants.ICMPV6_TYPE_RA,
+ self._get_label_number()))
+ # Allow IPv6 ICMP traffic
+ ipv6_rules.append('pass out on %s inet6 proto ipv6-icmp label "%s"' %
+ (device_name, self._get_label_number()))
+ # Allow IPv6 DHCP Client traffic
+ ipv6_rules.append('pass out on %s inet6 proto udp from port 546 '
+ 'to port 547 label "%s"' %
+ (device_name, self._get_label_number()))
+
+ def _drop_dhcp_rule(self, device_name, ipv4_rules, ipv6_rules):
+ # Drop dhcp packet from VM
+ ipv4_rules.append('block out quick on %s proto udp from port 67 '
+ 'to port 68 label "%s"' %
+ (device_name, self._get_label_number()))
+ ipv6_rules.append('block out quick on %s inet6 proto udp '
+ 'from port 547 to port 546 label "%s"' %
+ (device_name, self._get_label_number()))
+
+ def _accept_inbound_icmpv6(self, device_name, ipv6_pf_rules):
+ # Allow multicast listener, neighbor solicitation and
+ # neighbor advertisement into the instance
+ for icmp6_type in constants.ICMPV6_ALLOWED_TYPES:
+ ipv6_pf_rules.append('pass in on %s inet6 proto ipv6-icmp '
+ 'icmp6-type %s label "%s"' %
+ (device_name, icmp6_type,
+ self._get_label_number()))
+
+ def _select_sg_rules_for_port(self, port, direction):
+ """Select rules from the security groups the port is member of."""
+ port_sg_ids = port.get('security_groups', [])
+ port_rules = []
+
+ for sg_id in port_sg_ids:
+ for rule in self.sg_rules.get(sg_id, []):
+ if rule['direction'] == direction:
+ port_rules.extend(
+ self._expand_sg_rule_with_remote_ips(
+ rule, port, direction))
+ return port_rules
+
+ def _expand_sg_rule_with_remote_ips(self, rule, port, direction):
+ """Expand a remote group rule to rule per remote group IP."""
+ remote_group_id = rule.get('remote_group_id')
+ if remote_group_id:
+ ethertype = rule['ethertype']
+ port_ips = port.get('fixed_ips', [])
+ LOG.debug("Expanding rule: %s with remote IPs: %s" %
+ (rule, self.sg_members[remote_group_id][ethertype]))
+ for ip in self.sg_members[remote_group_id][ethertype]:
+ if ip not in port_ips:
+ ip_rule = rule.copy()
+ direction_ip_prefix = firewall.DIRECTION_IP_PREFIX[
+ direction]
+ ip_prefix = str(netaddr.IPNetwork(ip).cidr)
+ ip_rule[direction_ip_prefix] = ip_prefix
+ yield ip_rule
+ else:
+ yield rule
+
+ def _get_remote_sg_ids(self, port, direction=None):
+ sg_ids = port.get('security_groups', [])
+ remote_sg_ids = {constants.IPv4: set(), constants.IPv6: set()}
+ for sg_id in sg_ids:
+ for rule in self.sg_rules.get(sg_id, []):
+ if not direction or rule['direction'] == direction:
+ remote_sg_id = rule.get('remote_group_id')
+ ether_type = rule.get('ethertype')
+ if remote_sg_id and ether_type:
+ remote_sg_ids[ether_type].add(remote_sg_id)
+ return remote_sg_ids
+
+ def _add_pf_rules(self, port, device_name, direction, ipv4_pf_rules,
+ ipv6_pf_rules):
+ if '/' in device_name:
+ instance_name, datalink = device_name.split('/')
+ instance_name = instance_name.split(':')[1]
+ else:
+ instance_name, datalink = (device_name, "")
+ self.pf.add_nested_anchor_rule(None, instance_name, None)
+ if direction == firewall.INGRESS_DIRECTION:
+ subanchor = '%s%s' % (datalink, '_in' if datalink else 'in')
+ new_anchor_rule = ['anchor "%s" in on %s all' % (subanchor,
+ device_name)]
+ else:
+ subanchor = '%s%s' % (datalink, '_out' if datalink else 'out')
+ new_anchor_rule = ['anchor "%s" out on %s all' % (subanchor,
+ device_name)]
+ existing_anchor_rules = self.pf.list_anchor_rules([instance_name])
+ final_anchor_rules = set(existing_anchor_rules) | set(new_anchor_rule)
+ self.pf.add_rules(list(final_anchor_rules), [instance_name])
+
+ # self.pf.add_nested_anchor_rule(None, anchor_name, anchor_option)
+ self.pf.add_rules(ipv4_pf_rules + ipv6_pf_rules,
+ [instance_name, subanchor])
+ self.portid_to_devname[port['id']] = device_name
+
+ def _add_block_everything(self, device_name, direction, ipv4_pf_rules,
+ ipv6_pf_rules):
+ ''' Add a generic block everything rule. The default security group
+ in OpenStack adds 'pass all egress traffic' and prevents all the
+ incoming traffic'''
+ ipv4_pf_rules.append('block %s on %s label "%s"' %
+ (DIRECTION_PF_PARAM[direction], device_name,
+ self._get_label_number()))
+ ipv6_pf_rules.append('block %s on %s inet6 label "%s"' %
+ (DIRECTION_PF_PARAM[direction], device_name,
+ self._get_label_number()))
+
+ def _add_rules_by_security_group(self, port, direction):
+ LOG.debug("Adding rules for Port: %s", port)
+
+ device_name = self._get_device_name(port)
+ if not device_name:
+ LOG.info(_LI("Could not find port: %s on the OVS bridge. Failed "
+ "to add PF rules for that port"), port['id'])
+ return
+ # select rules for current port and direction
+ security_group_rules = self._select_sgr_by_direction(port, direction)
+ security_group_rules += self._select_sg_rules_for_port(port, direction)
+ # split groups by ip version
+ # for ipv4, 'pass' will be used
+ # for ipv6, 'pass inet6' will be used
+ ipv4_sg_rules, ipv6_sg_rules = self._split_sgr_by_ethertype(
+ security_group_rules)
+ ipv4_pf_rules = []
+ ipv6_pf_rules = []
+ self._add_block_everything(device_name, direction, ipv4_pf_rules,
+ ipv6_pf_rules)
+ # include fixed egress/ingress rules
+ if direction == firewall.EGRESS_DIRECTION:
+ self._add_fixed_egress_rules(port, device_name, ipv4_pf_rules,
+ ipv6_pf_rules)
+ elif direction == firewall.INGRESS_DIRECTION:
+ self._accept_inbound_icmpv6(device_name, ipv6_pf_rules)
+ # include IPv4 and IPv6 iptable rules from security group
+ LOG.debug("Converting %s IPv4 SG rules: %s" %
+ (direction, ipv4_sg_rules))
+ ipv4_pf_rules += self._convert_sgr_to_pfr(device_name, direction,
+ ipv4_sg_rules)
+ LOG.debug("... to %s IPv4 PF rules: %s" % (direction, ipv4_pf_rules))
+ LOG.debug("Converting %s IPv6 SG rules: %s" %
+ (direction, ipv6_sg_rules))
+ ipv6_pf_rules += self._convert_sgr_to_pfr(device_name, direction,
+ ipv6_sg_rules)
+ LOG.debug("... to %s IPv6 PF rules: %s" % (direction, ipv6_pf_rules))
+
+ self._add_pf_rules(port, device_name, direction, ipv4_pf_rules,
+ ipv6_pf_rules)
+
+ def _add_fixed_egress_rules(self, port, device_name, ipv4_pf_rules,
+ ipv6_pf_rules):
+ self._spoofing_rule(port, device_name, ipv4_pf_rules, ipv6_pf_rules)
+ self._drop_dhcp_rule(device_name, ipv4_pf_rules, ipv6_pf_rules)
+
+ def _protocol_param(self, protocol, pf_rule):
+ if protocol:
+ pf_rule.append('proto %s' % protocol)
+
+ def _port_param(self, protocol, port_range_min, port_range_max, pf_rule):
+ if port_range_min is None:
+ return
+ if protocol in ('tcp', 'udp'):
+ if port_range_min == port_range_max:
+ pf_rule.append('port %s' % port_range_min)
+ else:
+ pf_rule.append('port %s:%s' % (port_range_min,
+ port_range_max))
+ elif protocol in ('icmp', 'ipv6-icmp'):
+ icmp_type = 'icmp-type' if protocol == 'icmp' else 'icmp6-type'
+ pf_rule.append('%s %s' % (icmp_type, port_range_min))
+ if port_range_max is not None:
+ pf_rule.append('code %s' % port_range_max)
+
+ def _ip_prefix_param(self, direction, ip_prefix, pf_rule):
+ if ip_prefix != 'any':
+ if '/' not in ip_prefix:
+ # we need to convert it into a cidr
+ ip_prefix = c_utils.ip_to_cidr(ip_prefix)
+ elif ip_prefix.endswith('/0'):
+ ip_prefix = 'any'
+ direction = 'from' if direction == firewall.INGRESS_DIRECTION else 'to'
+ pf_rule.append('%s %s' % (direction, ip_prefix))
+
+ def _ip_prefix_port_param(self, direction, sg_rule, pf_rule):
+ protocol = sg_rule.get('protocol')
+ if direction == firewall.INGRESS_DIRECTION:
+ ip_prefix = sg_rule.get('source_ip_prefix')
+ ip_prefix = ip_prefix if ip_prefix else 'any'
+ self._ip_prefix_param(direction, ip_prefix, pf_rule)
+ self._port_param(protocol,
+ sg_rule.get('source_port_range_min'),
+ sg_rule.get('source_port_range_max'), pf_rule)
+ self._ip_prefix_param(firewall.EGRESS_DIRECTION, 'any', pf_rule)
+ self._port_param(protocol,
+ sg_rule.get('port_range_min'),
+ sg_rule.get('port_range_max'), pf_rule)
+ else:
+ self._ip_prefix_param(firewall.INGRESS_DIRECTION, 'any', pf_rule)
+ self._port_param(protocol,
+ sg_rule.get('source_port_range_min'),
+ sg_rule.get('source_port_range_max'), pf_rule)
+
+ ip_prefix = sg_rule.get('dest_ip_prefix')
+ ip_prefix = ip_prefix if ip_prefix else 'any'
+ self._ip_prefix_param(direction, ip_prefix, pf_rule)
+ self._port_param(protocol,
+ sg_rule.get('port_range_min'),
+ sg_rule.get('port_range_max'), pf_rule)
+
+ def _convert_sgr_to_pfr(self, device_name, direction,
+ security_group_rules):
+ pf_rules = []
+ for sg_rule in security_group_rules:
+ pf_rule = ['pass']
+ pf_rule.append("%s on %s" % (DIRECTION_PF_PARAM[direction],
+ device_name))
+ if sg_rule.get('ethertype') == constants.IPv6:
+ pf_rule.append('inet6')
+ else:
+ pf_rule.append('inet')
+ protocol = sg_rule.get('protocol')
+ self._protocol_param(protocol, pf_rule)
+ self._ip_prefix_port_param(direction, sg_rule, pf_rule)
+ pf_rule.append('label "%s"' % self._get_label_number())
+ pf_rules.append(' '.join(pf_rule))
+ return pf_rules
--- a/components/openstack/neutron/files/neutron-openvswitch-agent Tue Nov 15 16:50:44 2016 -0800
+++ b/components/openstack/neutron/files/neutron-openvswitch-agent Tue Nov 15 18:00:12 2016 -0800
@@ -15,10 +15,13 @@
# under the License.
import os
+from subprocess import CalledProcessError, check_call
import sys
import smf_include
+from neutron.agent.solaris import packetfilter
+
def start():
# verify paths are valid
@@ -27,10 +30,30 @@
print '%s does not exist or is not readable' % f
return smf_include.SMF_EXIT_ERR_CONFIG
+ # Cleanup any security groups related PF rules
+ pf = packetfilter.PacketFilter('_auto/neutron:ovs:agent')
+ pf.remove_anchor_recursively()
+
cmd = "/usr/bin/pfexec /usr/lib/neutron/neutron-openvswitch-agent " \
"--config-file %s --config-file %s" % tuple(sys.argv[2:4])
smf_include.smf_subprocess(cmd)
+
+def stop():
+ try:
+ # first kill the SMF contract
+ check_call(["/usr/bin/pkill", "-c", sys.argv[2]])
+ except CalledProcessError as err:
+ print "failed to kill the SMF contract: %s" % err
+ return smf_include.SMF_EXIT_ERR_FATAL
+
+ # We need to remove the PF rules added under _auto/neutron:ovs:agent
+ # anchor.
+ pf = packetfilter.PacketFilter('_auto/neutron:ovs:agent')
+ pf.remove_anchor_recursively()
+ return smf_include.SMF_EXIT_OK
+
+
if __name__ == "__main__":
os.putenv("LC_ALL", "C")
smf_include.smf_main()
--- a/components/openstack/neutron/files/neutron-openvswitch-agent.xml Tue Nov 15 16:50:44 2016 -0800
+++ b/components/openstack/neutron/files/neutron-openvswitch-agent.xml Tue Nov 15 18:00:12 2016 -0800
@@ -61,7 +61,11 @@
</method_context>
</exec_method>
<exec_method timeout_seconds="60" type="method" name="stop"
- exec=":kill"/>
+ exec="/lib/svc/method/neutron-openvswitch-agent %m %{restarter/contract}">
+ <method_context>
+ <method_credential user='neutron' group='neutron'/>
+ </method_context>
+ </exec_method>
<instance name='default' enabled='false'>
<!-- to start/stop/refresh the service -->
--- a/components/openstack/neutron/neutron.p5m Tue Nov 15 16:50:44 2016 -0800
+++ b/components/openstack/neutron/neutron.p5m Tue Nov 15 18:00:12 2016 -0800
@@ -223,6 +223,7 @@
file path=usr/lib/python$(PYVER)/vendor-packages/neutron/agent/solaris/net_lib.py
file path=usr/lib/python$(PYVER)/vendor-packages/neutron/agent/solaris/packetfilter.py
file path=usr/lib/python$(PYVER)/vendor-packages/neutron/agent/solaris/pd.py
+file path=usr/lib/python$(PYVER)/vendor-packages/neutron/agent/solaris/pf_firewall.py
file path=usr/lib/python$(PYVER)/vendor-packages/neutron/agent/solaris/ra.py
file path=usr/lib/python$(PYVER)/vendor-packages/neutron/agent/windows/__init__.py
file path=usr/lib/python$(PYVER)/vendor-packages/neutron/agent/windows/ip_lib.py
--- a/components/openstack/neutron/patches/10-interface-driver-entry-point.patch Tue Nov 15 16:50:44 2016 -0800
+++ b/components/openstack/neutron/patches/10-interface-driver-entry-point.patch Tue Nov 15 18:00:12 2016 -0800
@@ -1,22 +1,21 @@
-The Solaris OVS Interface driver needs to be added to the list of entry points
-in order to be discovered and loaded by stevedore.
+The Solaris OVS Interface driver and Solaris PF security group driver need to
+be added to the list of entry points in order to be discovered and loaded by
+stevedore.
-*** neutron-8.0.0/setup.cfg 2016-04-07 00:47:18.000000000 -0700
---- new/setup.cfg 2016-05-24 13:31:12.688314864 -0700
-***************
-*** 155,161 ****
- ivs = neutron.agent.linux.interface:IVSInterfaceDriver
- linuxbridge = neutron.agent.linux.interface:BridgeInterfaceDriver
- null = neutron.agent.linux.interface:NullDriver
-! openvswitch = neutron.agent.linux.interface:OVSInterfaceDriver
- neutron.agent.firewall_drivers =
- noop = neutron.agent.firewall:NoopFirewallDriver
- iptables = neutron.agent.linux.iptables_firewall:IptablesFirewallDriver
---- 155,161 ----
- ivs = neutron.agent.linux.interface:IVSInterfaceDriver
- linuxbridge = neutron.agent.linux.interface:BridgeInterfaceDriver
- null = neutron.agent.linux.interface:NullDriver
-! openvswitch = neutron.agent.solaris.interface:OVSInterfaceDriver
- neutron.agent.firewall_drivers =
- noop = neutron.agent.firewall:NoopFirewallDriver
- iptables = neutron.agent.linux.iptables_firewall:IptablesFirewallDriver
+--- neutron-8.1.2/setup.cfg 2016-06-09 18:50:09.000000000 -0700
++++ new/setup.cfg 2016-09-08 22:28:20.271867703 -0700
+@@ -155,12 +155,13 @@
+ ivs = neutron.agent.linux.interface:IVSInterfaceDriver
+ linuxbridge = neutron.agent.linux.interface:BridgeInterfaceDriver
+ null = neutron.agent.linux.interface:NullDriver
+- openvswitch = neutron.agent.linux.interface:OVSInterfaceDriver
++ openvswitch = neutron.agent.solaris.interface:OVSInterfaceDriver
+ neutron.agent.firewall_drivers =
+ noop = neutron.agent.firewall:NoopFirewallDriver
+ iptables = neutron.agent.linux.iptables_firewall:IptablesFirewallDriver
+ iptables_hybrid = neutron.agent.linux.iptables_firewall:OVSHybridIptablesFirewallDriver
+ openvswitch = neutron.agent.linux.openvswitch_firewall:OVSFirewallDriver
++ pf = neutron.agent.solaris.pf_firewall:PFFirewallDriver
+
+ [build_sphinx]
+ all_files = 1
--- a/components/openstack/nova/patches/10-no-security-groups.patch Tue Nov 15 16:50:44 2016 -0800
+++ b/components/openstack/nova/patches/10-no-security-groups.patch Tue Nov 15 18:00:12 2016 -0800
@@ -1,15 +1,78 @@
-In-house patch as Solaris doesn't currently support security groups.
+Nova while spawning the instance expects the security group feature to be
+enabled. When not enabled we get 404 Not Found error and this causes the
+spawning of instances to fail. In the case of 404 Not Found error, we just
+need to return an empty security group list. This is an issue with upstream,
+and the patch must be proposed upstream.
---- nova-13.1.0/nova/network/neutronv2/api.py.~1~ 2016-06-14 08:45:49.000000000 -0700
-+++ nova-13.1.0/nova/network/neutronv2/api.py 2016-07-06 18:08:27.484252690 -0700
-@@ -606,8 +606,8 @@ class API(base_api.NetworkAPI):
- self._check_external_network_attach(context, nets)
-
- security_groups = kwargs.get('security_groups', [])
-- security_group_ids = self._process_security_groups(
-- instance, neutron, security_groups)
-+ # TODO(gmoodalb): Solaris doesn't currently support security groups.
-+ security_group_ids = []
-
- preexisting_port_ids = []
- created_port_ids = []
+*** nova-13.1.0/nova/network/neutronv2/api.py 2016-06-14 08:45:49.000000000 -0700
+--- new/nova/network/neutronv2/api.py 2016-10-31 20:37:36.416614641 -0700
+***************
+*** 483,490 ****
+ # group if len(security_groups) == 1
+ if len(security_groups):
+ search_opts = {'tenant_id': instance.project_id}
+! user_security_groups = neutron.list_security_groups(
+! **search_opts).get('security_groups')
+
+ for security_group in security_groups:
+ name_match = None
+--- 483,496 ----
+ # group if len(security_groups) == 1
+ if len(security_groups):
+ search_opts = {'tenant_id': instance.project_id}
+! try:
+! user_security_groups = neutron.list_security_groups(
+! **search_opts).get('security_groups')
+! except neutron_client_exc.NotFound:
+! # An admin could have disabled security group feature for the
+! # cloud, and in that case the API above will end up in 404 not
+! # found, so we need to return an empty list.
+! return []
+
+ for security_group in security_groups:
+ name_match = None
+*** nova-13.1.0/nova/api/openstack/compute/security_groups.py 2016-06-14 08:45:49.000000000 -0700
+--- new/nova/api/openstack/compute/security_groups.py 2016-11-01 11:21:01.453929563 -0700
+***************
+*** 172,178 ****
+ list(sorted(result,
+ key=lambda k: (k['tenant_id'], k['name'])))}
+
+! @extensions.expected_errors((400, 403))
+ def create(self, req, body):
+ """Creates a new security group."""
+ context = _authorize_context(req)
+--- 172,178 ----
+ list(sorted(result,
+ key=lambda k: (k['tenant_id'], k['name'])))}
+
+! @extensions.expected_errors((400, 403, 501))
+ def create(self, req, body):
+ """Creates a new security group."""
+ context = _authorize_context(req)
+*** nova-13.1.0/nova/network/security_group/neutron_driver.py 2016-06-14 08:45:49.000000000 -0700
+--- new/nova/network/security_group/neutron_driver.py 2016-11-10 13:38:32.968864075 -0800
+***************
+*** 50,55 ****
+--- 50,59 ----
+ try:
+ security_group = neutron.create_security_group(
+ body).get('security_group')
++ except n_exc.NotFound:
++ raise exc.HTTPNotImplemented(
++ explanation='Neutron Security Groups feature is not available '
++ 'on this cloud.')
+ except n_exc.BadRequest as e:
+ raise exception.Invalid(six.text_type(e))
+ except n_exc.NeutronClientException as e:
+***************
+*** 188,193 ****
+--- 192,199 ----
+ try:
+ security_groups = neutron.list_security_groups(**params).get(
+ 'security_groups')
++ except n_exc.NotFound:
++ security_groups = []
+ except n_exc.NeutronClientException:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE("Neutron Error getting security groups"))