From 2dd71331d4d204466e7b066f62952990e55c2e24 Mon Sep 17 00:00:00 2001 From: Alexander Nusov Date: Tue, 29 Nov 2016 14:21:41 +0300 Subject: [PATCH] add freebsd_net driver --- nova/network/freebsd_net.py | 1226 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1226 insertions(+) create mode 100644 nova/network/freebsd_net.py diff --git a/nova/network/freebsd_net.py b/nova/network/freebsd_net.py new file mode 100644 index 0000000..b71fcf6 --- /dev/null +++ b/nova/network/freebsd_net.py @@ -0,0 +1,1226 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +"""Implements vlans, bridges, and iptables rules using linux utilities.""" + +import calendar +import inspect +import os +import re +import time +import json + +import netaddr +import netifaces +import socket +import struct + +from oslo_concurrency import processutils +from oslo_log import log as logging +from oslo_serialization import jsonutils +from oslo_utils import excutils +from oslo_utils import fileutils +from oslo_utils import importutils +from oslo_utils import timeutils +import six + +import nova.conf +from nova import exception +from nova.i18n import _, _LE, _LW +from nova.network import model as network_model +from nova import objects +from nova.pci import utils as pci_utils +from nova import utils + +LOG = logging.getLogger(__name__) + + +CONF = nova.conf.CONF + + +# NOTE(vish): Iptables supports chain names of up to 28 characters, and we +# add up to 12 characters to binary_name which is used as a prefix, +# so we limit it to 16 characters. +# (max_chain_name_length - len('-POSTROUTING') == 16) +def get_binary_name(): + """Grab the name of the binary we're running in.""" + return os.path.basename(inspect.stack()[-1][1])[:16] + +binary_name = get_binary_name() + + +# NOTE(jkoelker) This is just a nice little stub point since mocking +# builtins with mox is a nightmare +def write_to_file(file, data, mode='w'): + with open(file, mode) as f: + f.write(data) + + +def is_pid_cmdline_correct(pid, match): + """Ensure that the cmdline for a pid seems sane + + Because pids are recycled, blindly killing by pid is something to + avoid. This provides the ability to include a substring that is + expected in the cmdline as a safety check. + """ + try: + with open('/proc/%d/cmdline' % pid) as f: + cmdline = f.read() + return match in cmdline + except EnvironmentError: + return False + + +def metadata_forward(): + """Create forwarding rule for metadata.""" + firewall_manager.add_rule("rdr proto tcp from any to 169.254.169.254 " + "port 80 -> %s port %s" % + (CONF.metadata_host, CONF.metadata_port)) + firewall_manager.add_rule("pass out route-to (lo0 127.0.0.1) proto tcp " + "from any to 169.254.169.254 port 80") + firewall_manager.apply() + + +def metadata_accept(): + """Create the filter accept rule for metadata.""" + firewall_manager.add_rule("pass in inet proto tcp from any to " + "169.254.169.254 port = http " + "flags S/SA keep state") + firewall_manager.apply() + + +def init_host(ip_range, is_external=False): + """Basic networking setup goes here.""" + # NOTE(devcamcar): Cloud public SNAT entries and the default + # SNAT rule for outbound traffic. + + firewall_manager.add_snat_rule(ip_range, is_external) + if is_external: + for snat_range in CONF.force_snat_range: + firewall_manager.add_rule("pass quick inet from %s to %s" % + (ip_range, snat_range)) + firewall_manager.add_rule("pass quick inet from %s to %s/32" % + (ip_range, CONF.metadata_host)) + for dmz in CONF.dmz_cidr: + firewall_manager.add_rule("pass quick inet from %s to %s" % + (ip_range, dmz)) + + """ + iptables_manager.ipv4['nat'].add_rule('POSTROUTING', + '-s %(range)s -d %(range)s ' + '-m conntrack ! --ctstate DNAT ' + '-j ACCEPT' % + {'range': ip_range}) + """ + firewall_manager.apply() + + +def send_arp_for_ip(ip, device, count): + out, err = _execute('arping', '-U', '-i', device, '-c', str(count), ip, + run_as_root=True, check_exit_code=False) + + if err: + LOG.debug('arping error for IP %s', ip) + + +def bind_floating_ip(floating_ip, device): + """Bind IP to public interface.""" + _execute('ifconfig', device, str(floating_ip) + '/32', 'add', + run_as_root=True, check_exit_code=0) + + if CONF.send_arp_for_ha and CONF.send_arp_for_ha_count > 0: + send_arp_for_ip(floating_ip, device, CONF.send_arp_for_ha_count) + + +def unbind_floating_ip(floating_ip, device): + """Unbind a public IP from public interface.""" + _execute('ifconfig', device, str(floating_ip) + '/32', 'delete', + run_as_root=True, check_exit_code=0) + + +def ensure_metadata_ip(): + """Sets up local metadata IP.""" + _execute('ifconfig', 'lo0', 'alias', '169.254.169.254/32', + run_as_root=True, check_exit_code=0) + + +def ensure_vpn_forward(public_ip, port, private_ip): + """Sets up forwarding rules for vlan.""" + firewall_manager.add_rule("pass in proto udp " + "from any to %s port 1194 " % + (private_ip)) + firewall_manager.add_rule("rdr proto udp from any to %s port %s -> " + "%s port 1194" % + (public_ip, port, private_ip)) + firewall_manager.apply() + + +def ensure_floating_forward(floating_ip, fixed_ip, device, network): + """Ensure floating IP forwarding rule.""" + firewall_manager.ensure_floating_rules(floating_ip, fixed_ip, device) + if device != network['bridge']: + firewall_manager.ensure_in_network_traffic_rules(fixed_ip, network) + firewall_manager.apply() + + +def remove_floating_forward(floating_ip, fixed_ip, device, network): + """Remove forwarding for floating IP.""" + firewall_manager.remove_floating_rules(floating_ip, fixed_ip, device) + if device != network['bridge']: + firewall_manager.remove_in_network_traffic_rules(fixed_ip, network) + firewall_manager.apply() + + +def clean_conntrack(fixed_ip): + pass + + +def _enable_ipv4_forwarding(): + sysctl_key = 'net.inet.ip.forwarding' + stdout, stderr = _execute('sysctl', '-n', sysctl_key) + if stdout.strip() is not '1': + _execute('sysctl', '%s=1' % sysctl_key, run_as_root=True) + + +@utils.synchronized('lock_gateway', external=True) +def initialize_gateway_device(dev, network_ref): + if not network_ref: + return + + _enable_ipv4_forwarding() + + # NOTE(vish): The ip for dnsmasq has to be the first address on the + # bridge for it to respond to requests properly + try: + prefix = network_ref.cidr.prefixlen + except AttributeError: + prefix = network_ref['cidr'].rpartition('/')[2] + + full_ip = '%s/%s' % (network_ref['dhcp_server'], prefix) + new_ip_params = [['inet', full_ip, 'broadcast', network_ref['broadcast']]] + old_ip_params = [] + out, err = _execute('ifconfig', dev) + for line in out.split('\n'): + fields = line.split() + if fields and fields[0] == 'inet': + old_ip_params.append(fields) + if _address_to_cidr(fields[1], fields[3]) != full_ip: + new_ip_params.append(fields) + if not old_ip_params or _address_to_cidr(old_ip_params[0][1], old_ip_params[0][3]) != full_ip: + old_routes = [] + result = _execute('netstat', '-nrW', '-f', 'inet') + if result: + out, err = result + for line in out.split('\n'): + fields = line.split() + if len(fields) > 6 and (fields[6] == dev) and ('G' in fields[2]): + old_routes.append(fields) + _execute('route', '-q', 'delete', fields[0], fields[1], + run_as_root=True) + for ip_params in old_ip_params: + _execute(*_ifconfig_tail_cmd(dev, ip_params, 'delete'), + run_as_root=True) + for ip_params in new_ip_params: + _execute(*_ifconfig_tail_cmd(dev, ip_params, 'add'), + run_as_root=True) + + for fields in old_routes: + _execute('route', '-q', 'add', fields[0], fields[1], + run_as_root=True) + if CONF.send_arp_for_ha and CONF.send_arp_for_ha_count > 0: + send_arp_for_ip(network_ref['dhcp_server'], dev, + CONF.send_arp_for_ha_count) + if CONF.use_ipv6: + _execute('ifconfig', dev, 'inet6', network_ref['cidr_v6'], + run_as_root=True) + + +def get_dhcp_leases(context, network_ref): + """Return a network's hosts config in dnsmasq leasefile format.""" + hosts = [] + host = None + if network_ref['multi_host']: + host = CONF.host + for fixedip in objects.FixedIPList.get_by_network(context, + network_ref, + host=host): + # NOTE(cfb): Don't return a lease entry if the IP isn't + # already leased + if fixedip.leased: + hosts.append(_host_lease(fixedip)) + + return '\n'.join(hosts) + + +def get_dhcp_hosts(context, network_ref, fixedips): + """Get network's hosts config in dhcp-host format.""" + hosts = [] + macs = set() + for fixedip in fixedips: + if fixedip.allocated: + if fixedip.virtual_interface.address not in macs: + hosts.append(_host_dhcp(fixedip)) + macs.add(fixedip.virtual_interface.address) + return '\n'.join(hosts) + + +def get_dns_hosts(context, network_ref): + """Get network's DNS hosts in hosts format.""" + hosts = [] + for fixedip in objects.FixedIPList.get_by_network(context, network_ref): + if fixedip.allocated: + hosts.append(_host_dns(fixedip)) + return '\n'.join(hosts) + + +def _add_dnsmasq_accept_rules(dev): + """Allow DHCP and DNS traffic through to dnsmasq.""" + for port in [67, 53]: + for proto in ['udp', 'tcp']: + firewall_manager.add_rule("pass in on %s inet proto %s " + "from any to any port %s" % + (dev, proto, port)) + firewall_manager.apply() + + +def _remove_dnsmasq_accept_rules(dev): + """Remove DHCP and DNS traffic allowed through to dnsmasq.""" + for port in [67, 53]: + for proto in ['udp', 'tcp']: + firewall_manager.remove_rule("pass in on %s inet proto %s " + "from any to any port %s" % + (dev, proto, port)) + firewall_manager.apply() + + +def get_dhcp_opts(context, network_ref, fixedips): + """Get network's hosts config in dhcp-opts format.""" + gateway = network_ref['gateway'] + # NOTE(vish): if we are in multi-host mode and we are not sharing + # addresses, then we actually need to hand out the + # dhcp server address as the gateway. + if network_ref['multi_host'] and not (network_ref['share_address'] or + CONF.share_dhcp_address): + gateway = network_ref['dhcp_server'] + hosts = [] + if CONF.use_single_default_gateway: + for fixedip in fixedips: + if fixedip.allocated: + vif_id = fixedip.virtual_interface_id + if fixedip.default_route: + hosts.append(_host_dhcp_opts(vif_id, gateway)) + else: + hosts.append(_host_dhcp_opts(vif_id)) + else: + hosts.append(_host_dhcp_opts(None, gateway)) + return '\n'.join(hosts) + + +def release_dhcp(dev, address, mac_address): + if device_exists(dev): + try: + utils.execute('dhcp_release', dev, address, mac_address, + run_as_root=True) + except processutils.ProcessExecutionError: + raise exception.NetworkDhcpReleaseFailed(address=address, + mac_address=mac_address) + + +def update_dhcp(context, dev, network_ref): + conffile = _dhcp_file(dev, 'conf') + host = None + if network_ref['multi_host']: + host = CONF.host + fixedips = objects.FixedIPList.get_by_network(context, + network_ref, + host=host) + write_to_file(conffile, get_dhcp_hosts(context, network_ref, fixedips)) + restart_dhcp(context, dev, network_ref, fixedips) + + +def update_dns(context, dev, network_ref): + hostsfile = _dhcp_file(dev, 'hosts') + host = None + if network_ref['multi_host']: + host = CONF.host + fixedips = objects.FixedIPList.get_by_network(context, + network_ref, + host=host) + write_to_file(hostsfile, get_dns_hosts(context, network_ref)) + restart_dhcp(context, dev, network_ref, fixedips) + + +def kill_dhcp(dev): + pid = _dnsmasq_pid_for(dev) + if pid: + # Check that the process exists and looks like a dnsmasq process + conffile = _dhcp_file(dev, 'conf') + if is_pid_cmdline_correct(pid, conffile.split('/')[-1]): + _execute('kill', '-9', pid, run_as_root=True) + else: + LOG.debug('Pid %d is stale, skip killing dnsmasq', pid) + _remove_dnsmasq_accept_rules(dev) + + +# NOTE(ja): Sending a HUP only reloads the hostfile, so any +# configuration options (like dchp-range, vlan, ...) +# aren't reloaded. +@utils.synchronized('dnsmasq_start') +def restart_dhcp(context, dev, network_ref, fixedips): + """(Re)starts a dnsmasq server for a given network. + + If a dnsmasq instance is already running then send a HUP + signal causing it to reload, otherwise spawn a new instance. + + """ + conffile = _dhcp_file(dev, 'conf') + + optsfile = _dhcp_file(dev, 'opts') + write_to_file(optsfile, get_dhcp_opts(context, network_ref, fixedips)) + os.chmod(optsfile, 0o644) + + # Make sure dnsmasq can actually read it (it setuid()s to "nobody") + os.chmod(conffile, 0o644) + + pid = _dnsmasq_pid_for(dev) + + # if dnsmasq is already running, then tell it to reload + if pid: + if is_pid_cmdline_correct(pid, conffile.split('/')[-1]): + try: + _execute('kill', '-HUP', pid, run_as_root=True) + _add_dnsmasq_accept_rules(dev) + return + except Exception as exc: + LOG.error(_LE('kill -HUP dnsmasq threw %s'), exc) + else: + LOG.debug('Pid %d is stale, relaunching dnsmasq', pid) + + cmd = ['env', + 'CONFIG_FILE=%s' % jsonutils.dumps(CONF.dhcpbridge_flagfile), + 'NETWORK_ID=%s' % str(network_ref['id']), + 'dnsmasq', + '--strict-order', + '--bind-interfaces', + '--conf-file=%s' % CONF.dnsmasq_config_file, + '--pid-file=%s' % _dhcp_file(dev, 'pid'), + '--dhcp-optsfile=%s' % _dhcp_file(dev, 'opts'), + '--listen-address=%s' % network_ref['dhcp_server'], + '--except-interface=lo', + '--dhcp-range=set:%s,%s,static,%s,%ss' % + (network_ref['label'], + network_ref['dhcp_start'], + network_ref['netmask'], + CONF.dhcp_lease_time), + '--dhcp-lease-max=%s' % len(netaddr.IPNetwork(network_ref['cidr'])), + '--dhcp-hostsfile=%s' % _dhcp_file(dev, 'conf'), + '--dhcp-script=%s' % CONF.dhcpbridge, + '--no-hosts', + '--leasefile-ro'] + + # dnsmasq currently gives an error for an empty domain, + # rather than ignoring. So only specify it if defined. + if CONF.dhcp_domain: + cmd.append('--domain=%s' % CONF.dhcp_domain) + + dns_servers = CONF.dns_server + if CONF.use_network_dns_servers: + if network_ref.get('dns1'): + dns_servers.append(network_ref.get('dns1')) + if network_ref.get('dns2'): + dns_servers.append(network_ref.get('dns2')) + if network_ref['multi_host']: + cmd.append('--addn-hosts=%s' % _dhcp_file(dev, 'hosts')) + if dns_servers: + cmd.append('--no-resolv') + for dns_server in dns_servers: + cmd.append('--server=%s' % dns_server) + + _execute(*cmd, run_as_root=True) + + _add_dnsmasq_accept_rules(dev) + + +@utils.synchronized('radvd_start') +def update_ra(context, dev, network_ref): + conffile = _ra_file(dev, 'conf') + conf_str = """ +interface %s +{ + AdvSendAdvert on; + MinRtrAdvInterval 3; + MaxRtrAdvInterval 10; + prefix %s + { + AdvOnLink on; + AdvAutonomous on; + }; +}; +""" % (dev, network_ref['cidr_v6']) + write_to_file(conffile, conf_str) + + # Make sure radvd can actually read it (it setuid()s to "nobody") + os.chmod(conffile, 0o644) + + pid = _ra_pid_for(dev) + + # if radvd is already running, then tell it to reload + if pid: + if is_pid_cmdline_correct(pid, conffile): + try: + _execute('kill', pid, run_as_root=True) + except Exception as exc: + LOG.error(_LE('killing radvd threw %s'), exc) + else: + LOG.debug('Pid %d is stale, relaunching radvd', pid) + + cmd = ['radvd', + '-C', '%s' % _ra_file(dev, 'conf'), + '-p', '%s' % _ra_file(dev, 'pid')] + + _execute(*cmd, run_as_root=True) + + +def _host_lease(fixedip): + """Return a host string for an address in leasefile format.""" + timestamp = timeutils.utcnow() + seconds_since_epoch = calendar.timegm(timestamp.utctimetuple()) + return '%d %s %s %s *' % (seconds_since_epoch + CONF.dhcp_lease_time, + fixedip.virtual_interface.address, + fixedip.address, + fixedip.instance.hostname or '*') + + +def _host_dhcp_network(vif_id): + return 'NW-%s' % vif_id + + +def _host_dhcp(fixedip): + """Return a host string for an address in dhcp-host format.""" + # NOTE(cfb): dnsmasq on linux only supports 64 characters in the hostname + # field (LP #1238910). Since the . counts as a character we need + # to truncate the hostname to only 63 characters. + hostname = fixedip.instance.hostname + if len(hostname) > 63: + LOG.warning(_LW('hostname %s too long, truncating.'), hostname) + hostname = fixedip.instance.hostname[:2] + '-' +\ + fixedip.instance.hostname[-60:] + if CONF.use_single_default_gateway: + net = _host_dhcp_network(fixedip.virtual_interface_id) + return '%s,%s.%s,%s,net:%s' % (fixedip.virtual_interface.address, + hostname, + CONF.dhcp_domain, + fixedip.address, + net) + else: + return '%s,%s.%s,%s' % (fixedip.virtual_interface.address, + hostname, + CONF.dhcp_domain, + fixedip.address) + + +def _host_dns(fixedip): + return '%s\t%s.%s' % (fixedip.address, + fixedip.instance.hostname, + CONF.dhcp_domain) + + +def _host_dhcp_opts(vif_id=None, gateway=None): + """Return an empty gateway option.""" + values = [] + if vif_id is not None: + values.append(_host_dhcp_network(vif_id)) + # NOTE(vish): 3 is the dhcp option for gateway. + values.append('3') + if gateway: + values.append('%s' % gateway) + return ','.join(values) + + +def _execute(*cmd, **kwargs): + """Wrapper around utils._execute for fake_network.""" + if CONF.fake_network: + LOG.debug('FAKE NET: %s', ' '.join(map(str, cmd))) + return 'fake', 0 + else: + return utils.execute(*cmd, **kwargs) + + +def device_exists(device): + """Check if ethernet device exists.""" + try: + _execute('ifconfig', device, run_as_root=True, check_exit_code=[0]) + except processutils.ProcessExecutionError: + return False + else: + return True + + +def _dhcp_file(dev, kind): + """Return path to a pid, leases, hosts or conf file for a bridge/device.""" + fileutils.ensure_tree(CONF.networks_path) + return os.path.abspath('%s/nova-%s.%s' % (CONF.networks_path, + dev, + kind)) + + +def _ra_file(dev, kind): + """Return path to a pid or conf file for a bridge/device.""" + fileutils.ensure_tree(CONF.networks_path) + return os.path.abspath('%s/nova-ra-%s.%s' % (CONF.networks_path, + dev, + kind)) + + +def _dnsmasq_pid_for(dev): + """Returns the pid for prior dnsmasq instance for a bridge/device. + + Returns None if no pid file exists. + + If machine has rebooted pid might be incorrect (caller should check). + + """ + pid_file = _dhcp_file(dev, 'pid') + + if os.path.exists(pid_file): + try: + with open(pid_file, 'r') as f: + return int(f.read()) + except (ValueError, IOError): + return None + + +def _ra_pid_for(dev): + """Returns the pid for prior radvd instance for a bridge/device. + + Returns None if no pid file exists. + + If machine has rebooted pid might be incorrect (caller should check). + + """ + pid_file = _ra_file(dev, 'pid') + + if os.path.exists(pid_file): + with open(pid_file, 'r') as f: + return int(f.read()) + + +def _address_to_cidr(address, hexmask): + """Produce a CIDR format address/netmask.""" + netmask = socket.inet_ntoa(struct.pack(">I", int(hexmask, 16))) + ip_cidr = netaddr.IPNetwork("%s/%s" % (address, netmask)) + return str(ip_cidr) + + +def _ifconfig_tail_cmd(netif, params, action): + """Construct ifconfig command""" + cmd = ['ifconfig', netif] + cmd.extend(params) + cmd.extend([action]) + return cmd + + +def _set_device_mtu(dev, mtu=None): + """Set the device MTU.""" + if mtu: + utils.execute('ifconfig', dev, 'mtu', mtu, + run_as_root=True, check_exit_code=0) + + +def _ovs_vsctl(args): + full_args = ['ovs-vsctl', '--timeout=%s' % CONF.ovs_vsctl_timeout] + args + try: + return utils.execute(*full_args, run_as_root=True) + except Exception as e: + LOG.error(_LE("Unable to execute %(cmd)s. Exception: %(exception)s"), + {'cmd': full_args, 'exception': e}) + raise exception.OvsConfigurationFailure(inner_exception=e) + + +def _create_ovs_vif_cmd(bridge, dev, iface_id, mac, + instance_id, interface_type=None): + cmd = ['--', '--if-exists', 'del-port', dev, '--', + 'add-port', bridge, dev, + '--', 'set', 'Interface', dev, + 'external-ids:iface-id=%s' % iface_id, + 'external-ids:iface-status=active', + 'external-ids:attached-mac=%s' % mac, + 'external-ids:vm-uuid=%s' % instance_id] + if interface_type: + cmd += ['type=%s' % interface_type] + return cmd + + +def create_ovs_vif_port(bridge, dev, iface_id, mac, instance_id, + mtu=None, interface_type=None): + _ovs_vsctl(_create_ovs_vif_cmd(bridge, dev, iface_id, + mac, instance_id, + interface_type)) + # Note at present there is no support for setting the + # mtu for vhost-user type ports. + if interface_type != network_model.OVS_VHOSTUSER_INTERFACE_TYPE: + _set_device_mtu(dev, mtu) + else: + LOG.debug("MTU not set on %(interface_name)s interface " + "of type %(interface_type)s.", + {'interface_name': dev, + 'interface_type': interface_type}) + + +def delete_ovs_vif_port(bridge, dev, delete_dev=True): + _ovs_vsctl(['--', '--if-exists', 'del-port', bridge, dev]) + if delete_dev: + delete_net_dev(dev) + + +def create_tap_dev(dev, mac_address=None): + if not device_exists(dev): + utils.execute('ifconfig', 'tap', 'create', 'name', dev, + run_as_root=True, check_exit_code=[0]) + if mac_address: + utils.execute('ifconfig', dev, 'ether', mac_address, + run_as_root=True, check_exit_code=[0]) + utils.execute('ifconfig', dev, 'up', + run_as_root=True, check_exit_code=[0]) + + +def delete_net_dev(dev): + """Delete a network device only if it exists.""" + if device_exists(dev): + try: + utils.execute('ifconfig', dev, 'destroy', + run_as_root=True, check_exit_code=0) + LOG.debug("Net device removed: '%s'", dev) + except processutils.ProcessExecutionError: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Failed removing net device: '%s'"), dev) + + +def delete_bridge_dev(dev): + """Delete a network bridge.""" + if device_exists(dev): + try: + utils.execute('ifconfig', dev, 'down', run_as_root=True) + utils.execute('ifconfig', dev, 'destroy', run_as_root=True) + except processutils.ProcessExecutionError: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Failed removing bridge device: '%s'"), dev) + + +# Similar to compute virt layers, the FreeBSD network node +# code uses a flexible driver model to support different ways +# of creating ethernet interfaces and attaching them to the network. +# In the case of a network host, these interfaces +# act as gateway/dhcp/vpn/etc. endpoints not VM interfaces. +interface_driver = None + + +def _get_interface_driver(): + global interface_driver + if not interface_driver: + interface_driver = importutils.import_object( + CONF.freebsdnet_interface_driver) + return interface_driver + + +def plug(network, mac_address, gateway=True): + return _get_interface_driver().plug(network, mac_address, gateway) + + +def unplug(network): + return _get_interface_driver().unplug(network) + + +def get_dev(network): + return _get_interface_driver().get_dev(network) + + +class FreeBSDNetInterfaceDriver(object): + """Abstract class that defines generic network host API + for all FreeBSD interface drivers. + """ + + def plug(self, network, mac_address): + """Create FreeBSD device, return device name.""" + raise NotImplementedError() + + def unplug(self, network): + """Destroy FreeBSD device, return device name.""" + raise NotImplementedError() + + def get_dev(self, network): + """Get device name.""" + raise NotImplementedError() + + +# plugs interfaces using FreeBSD Bridge +class FreeBSDBridgeInterfaceDriver(FreeBSDNetInterfaceDriver): + + def plug(self, network, mac_address, gateway=True): + vlan = network.get('vlan') + if vlan is not None: + iface = CONF.vlan_interface or network['bridge_interface'] + FreeBSDBridgeInterfaceDriver.ensure_vlan_bridge( + vlan, + network['bridge'], + iface, + network, + mac_address, + network.get('mtu')) + iface = 'vlan%s' % vlan + else: + iface = CONF.flat_interface or network['bridge_interface'] + FreeBSDBridgeInterfaceDriver.ensure_bridge( + network['bridge'], + iface, + network, gateway) + + if network['share_address'] or CONF.share_dhcp_address: + isolate_dhcp_address(iface, network['dhcp_server']) + # NOTE(vish): applying here so we don't get a lock conflict + firewall_manager.apply() + return network['bridge'] + + def unplug(self, network, gateway=True): + vlan = network.get('vlan') + if vlan is not None: + iface = 'vlan%s' % vlan + FreeBSDBridgeInterfaceDriver.remove_vlan_bridge(vlan, + network['bridge']) + else: + iface = CONF.flat_interface or network['bridge_interface'] + FreeBSDBridgeInterfaceDriver.remove_bridge(network['bridge'], + gateway) + + if network['share_address'] or CONF.share_dhcp_address: + remove_isolate_dhcp_address(iface, network['dhcp_server']) + + firewall_manager.apply() + return self.get_dev(network) + + def get_dev(self, network): + return network['bridge'] + + @staticmethod + def ensure_vlan_bridge(vlan_num, bridge, bridge_interface, + net_attrs=None, mac_address=None, + mtu=None): + """Create a vlan and bridge unless they already exist.""" + interface = FreeBSDBridgeInterfaceDriver.ensure_vlan(vlan_num, + bridge_interface, mac_address, + mtu) + FreeBSDBridgeInterfaceDriver.ensure_bridge(bridge, interface, net_attrs) + return interface + + @staticmethod + def remove_vlan_bridge(vlan_num, bridge): + """Delete a bridge and vlan.""" + FreeBSDBridgeInterfaceDriver.remove_bridge(bridge) + FreeBSDBridgeInterfaceDriver.remove_vlan(vlan_num) + + @staticmethod + @utils.synchronized('lock_vlan', external=True) + def ensure_vlan(vlan_num, bridge_interface, mac_address=None, mtu=None, + interface=None): + """Create a vlan unless it already exists.""" + if interface is None: + interface = 'vlan%s' % vlan_num + if not device_exists(interface): + LOG.debug('Starting VLAN interface %s', interface) + out, err = _execute('ifconfig', 'vlan', 'create', + 'vlan', vlan_num, + 'vlandev', bridge_interface, + 'name', interface, + run_as_root=True) + if err and 'File exists' not in err: + msg = _('Failed to add vlan: %s') % err + raise exception.NovaException(msg) + # (danwent) the bridge will inherit this address, so we want to + # make sure it is the value set from the NetworkManager + if mac_address: + _execute('ifconfig', interface, 'ether', mac_address, + run_as_root=True) + _execute('ifconfig',interface, 'up', + run_as_root=True) + # NOTE(vish): set mtu every time to ensure that changes to mtu get + # propagated + _set_device_mtu(interface, mtu) + return interface + + @staticmethod + @utils.synchronized('lock_vlan', external=True) + def remove_vlan(vlan_num): + """Delete a vlan.""" + vlan_interface = 'vlan%s' % vlan_num + delete_net_dev(vlan_interface) + + @staticmethod + @utils.synchronized('lock_bridge', external=True) + def ensure_bridge(bridge, interface, net_attrs=None, gateway=True, + filtering=True): + """Create a bridge unless it already exists. + + :param interface: the interface to create the bridge on. + :param net_attrs: dictionary with attributes used to create bridge. + :param gateway: whether or not the bridge is a gateway. + :param filtering: whether or not to create filters on the bridge. + + If net_attrs is set, it will add the net_attrs['gateway'] to the bridge + using net_attrs['broadcast'] and net_attrs['cidr']. It will also add + the ip_v6 address specified in net_attrs['cidr_v6'] if use_ipv6 is set. + + The code will attempt to move any IPs that already exist on the + interface onto the bridge and reset the default gateway if necessary. + + """ + if not device_exists(bridge): + LOG.debug('Starting Bridge %s', bridge) + out, err = _execute('ifconfig', 'bridge', 'create', 'name', bridge, + check_exit_code=False, run_as_root=True) + if err and 'File exists' not in err: + msg = _('Failed to add bridge: %s') % err + raise exception.NovaException(msg) + + _execute('ifconfig', bridge, 'up', run_as_root=True) + + if interface: + LOG.debug('Adding interface %(interface)s to bridge %(bridge)s', + {'interface': interface, 'bridge': bridge}) + out, err = _execute('ifconfig', bridge, 'addm', interface, + check_exit_code=False, run_as_root=True) + if err and 'File exists' not in err: + msg = _('Failed to add interface: %s') % err + raise exception.NovaException(msg) + + # NOTE(apmelton): Linux bridge's default behavior is to use the + # lowest mac of all plugged interfaces. This isn't a problem when + # it is first created and the only interface is the bridged + # interface. But, as instance interfaces are plugged, there is a + # chance for the mac to change. So, set it here so that it won't + # change in the future. + if not CONF.fake_network: + interface_addrs = netifaces.ifaddresses(interface) + interface_mac = interface_addrs[netifaces.AF_LINK][0]['addr'] + _execute('ifconfig', bridge, 'ether', interface_mac, + run_as_root=True) + + out, err = _execute('ifconfig', interface, 'up', + check_exit_code=False, run_as_root=True) + + # NOTE(vish): This will break if there is already an ip on the + # interface, so we move any ips to the bridge + # NOTE(danms): We also need to copy routes to the bridge so as + # not to break existing connectivity on the interface + old_routes = [] + out, err = _execute('netstat', '-nrW', '-f', 'inet') + for line in out.split('\n'): + fields = line.split() + if len(fields) > 6 and (fields[6] == interface) and ('G' in fields[2]): + old_routes.append(fields) + _execute('route', '-q', 'delete', fields[0], fields[1], + run_as_root=True) + out, err = _execute('ifconfig', interface) + for line in out.split('\n'): + fields = line.split() + if fields and fields[0] == 'inet': + _execute(*_ifconfig_tail_cmd(interface, fields, 'delete'), + run_as_root=True) + _execute(*_ifconfig_tail_cmd(bridge, fields, 'add'), + run_as_root=True) + for fields in old_routes: + _execute('route', '-q', 'add', fields[0], fields[1], + run_as_root=True) + + if filtering: + # Don't forward traffic unless we were told to be a gateway + if gateway: + firewall_manager.ensure_gateway_rules(bridge) + else: + firewall_manager.ensure_bridge_rules(bridge) + + @staticmethod + @utils.synchronized('lock_bridge', external=True) + def remove_bridge(bridge, gateway=True, filtering=True): + """Delete a bridge.""" + if not device_exists(bridge): + return + else: + if filtering: + if gateway: + firewall_manager.remove_gateway_rules(bridge) + else: + firewall_manager.remove_bridge_rules(bridge) + delete_bridge_dev(bridge) + + +def isolate_dhcp_address(interface, address): + # block arp traffic to address across the interface + firewall_manager.ensure_dhcp_isolation(interface, address) + + +def remove_isolate_dhcp_address(interface, address): + # block arp traffic to address across the interface + firewall_manager.remove_dhcp_isolation(interface, address) + + +# plugs interfaces using Open vSwitch +class FreeBSDOVSInterfaceDriver(FreeBSDNetInterfaceDriver): + + def plug(self, network, mac_address, gateway=True): + dev = self.get_dev(network) + if not device_exists(dev): + bridge = CONF.freebsdnet_ovs_integration_bridge + _ovs_vsctl(['--', '--may-exist', 'add-port', bridge, dev, + '--', 'set', 'Interface', dev, 'type=internal', + '--', 'set', 'Interface', dev, + 'external-ids:iface-id=%s' % dev, + '--', 'set', 'Interface', dev, + 'external-ids:iface-status=active', + '--', 'set', 'Interface', dev, + 'external-ids:attached-mac=%s' % mac_address]) + _execute('ifconfig', dev, 'ether', mac_address, run_as_root=True) + _set_device_mtu(dev, network.get('mtu')) + _execute('ifconfig', dev, 'up', run_as_root=True) + if not gateway: + # If we weren't instructed to act as a gateway then add the + # appropriate flows to block all non-dhcp traffic. + _execute('ovs-ofctl', + 'add-flow', bridge, 'priority=1,actions=drop', + run_as_root=True) + _execute('ovs-ofctl', 'add-flow', bridge, + 'udp,tp_dst=67,dl_dst=%s,priority=2,actions=normal' % + mac_address, run_as_root=True) + # .. and make sure iptbles won't forward it as well. + firewall_manager.ensure_bridge_rules(bridge) + else: + firewall_manager.ensure_gateway_rules(bridge) + + return dev + + def unplug(self, network): + dev = self.get_dev(network) + bridge = CONF.freebsdnet_ovs_integration_bridge + _ovs_vsctl(['--', '--if-exists', 'del-port', bridge, dev]) + return dev + + def get_dev(self, network): + dev = 'gw-' + str(network['uuid'][0:11]) + return dev + + +# plugs interfaces using FreeBSD Bridge when using NeutronManager +class NeutronFreeBSDBridgeInterfaceDriver(FreeBSDNetInterfaceDriver): + + BRIDGE_NAME_PREFIX = 'brq' + GATEWAY_INTERFACE_PREFIX = 'gw-' + + def plug(self, network, mac_address, gateway=True): + dev = self.get_dev(network) + bridge = self.get_bridge(network) + if not gateway: + # If we weren't instructed to act as a gateway then add the + # appropriate flows to block all non-dhcp traffic. + # .. and make sure iptbles won't forward it as well. + firewall_manager.ensure_bridge_rules(bridge) + return bridge + else: + firewall_manager.ensure_gateway_rules(bridge) + + create_tap_dev(dev, mac_address) + + if not device_exists(bridge): + LOG.debug("Starting bridge %s ", bridge) + utils.execute('ifconfig', 'bridge', 'create', 'name', bridge, run_as_root=True) + utils.execute('ifconfig', bridge, 'ether', mac_address, run_as_root=True) + utils.execute('ifconfig', bridge, 'up', run_as_root=True) + LOG.debug("Done starting bridge %s", bridge) + + full_ip = '%s/%s' % (network['dhcp_server'], + network['cidr'].rpartition('/')[2]) + utils.execute('ifconfig', bridge, full_ip, 'add', run_as_root=True) + + return dev + + def unplug(self, network): + dev = self.get_dev(network) + if not device_exists(dev): + return None + else: + delete_net_dev(dev) + return dev + + def get_dev(self, network): + dev = self.GATEWAY_INTERFACE_PREFIX + str(network['uuid'][0:11]) + return dev + + def get_bridge(self, network): + bridge = self.BRIDGE_NAME_PREFIX + str(network['uuid'][0:11]) + return bridge + + +class FirewallManager(object): + def __init__(self, execute=_execute): + self.execute = execute + self.apply_deferred = False + self.anchor = 'org.openstack/%s' % get_binary_name() + self.rules = { + "translation": [], + "filtering": [] + } + self.is_dirty = False + + def _get_rule_section(self, rule): + LOG.warning("processing rule: %s" % rule) + head, tail = rule.split(' ', 1) + if head in ('nat', 'rdr'): + return 'translation' + elif head in ('pass', 'block'): + return 'filtering' + else: + return None + + def add_rule(self, rule): + cleaned_rule = rule.strip() + section = self._get_rule_section(cleaned_rule) + if section: + if cleaned_rule not in self.rules[section]: + self.rules[section].append(cleaned_rule) + self.is_dirty = True + LOG.warning("Added rule to %s: %s", section, cleaned_rule) + + def remove_rule(self, rule): + cleaned_rule = rule.strip() + section = self._get_rule_section(cleaned_rule) + LOG.warning("Removing rule from %s: %s", section, cleaned_rule) + if section: + try: + self.rules[section].remove(cleaned_rule) + self.is_dirty = True + except: + pass + + def defer_apply_on(self): + self.apply_deferred = True + + def defer_apply_off(self): + self.apply_deferred = False + self.apply() + + def dirty(self): + return self.is_dirty + + def apply(self): + if self.apply_deferred: + return + if self.dirty(): + self._apply() + else: + LOG.debug("Skipping apply due to lack of new rules") + + @utils.synchronized('pfctl', external=True) + def _apply(self): + all_lines = [] + all_lines.extend(self.rules['translation']) + all_lines.extend(self.rules['filtering']) + all_lines.extend(["\n"]) + + self.is_dirty = False + self.execute("pfctl", "-a", self.anchor, "-f", "-", + process_input="\n".join(all_lines), + run_as_root=True) + LOG.warning("FirewallManager.apply completed with success") + + def get_gateway_rules(self, bridge): + LOG.warning("FirewallManager.get_gateway_rules: " + "Please configure rules in pf.conf") + return [] + + def ensure_gateway_rules(self, bridge): + for rule in self.get_gateway_rules(bridge): + self.add_rule(rule) + + def remove_gateway_rules(self, bridge): + for rule in self.get_gateway_rules(bridge): + self.remove_rule(rule) + + def ensure_bridge_rules(self, bridge): + LOG.warning("FirewallManager.ensure_bridge_rules: " + "Please configure rules in pf.conf") + + def remove_bridge_rules(self, bridge): + LOG.warning("FirewallManager.remove_bridge_rules: " + "Please configure rules in pf.conf") + + def ensure_dhcp_isolation(self, interface, address): + LOG.warning("FirewallManager.ensure_dhcp_isolation: " + "DHCP isolation is not yet implemented") + + def remove_dhcp_isolation(self, interface, address): + LOG.warning("FirewallManager.remove_dhcp_isolation: " + "DHCP isolation is not yet implemented") + + def ensure_in_network_traffic_rules(self, fixed_ip, network): + LOG.warning("FirewallManager.ensure_in_network_traffic_rules: " + "Please configure rules in pf.conf") + + def remove_in_network_traffic_rules(self, fixed_ip, network): + LOG.warning("FirewallManager.remove_in_network_traffic_rules: " + "Please configure rules in pf.conf") + + def floating_forward_rules(self, floating_ip, fixed_ip, device): + rules = [] + rules.append("rdr inet from any to %s -> %s" % (floating_ip, fixed_ip)) + + return rules + + def ensure_floating_rules(self, floating_ip, fixed_ip, device): + for rule in self.floating_forward_rules(floating_ip, fixed_ip, device): + self.add_rule(rule) + + def remove_floating_rules(self, floating_ip, fixed_ip, device): + for rule in self.floating_forward_rules(floating_ip, fixed_ip, device): + self.remove_rule(rule) + + def add_snat_rule(self, ip_range, is_external=False): + if CONF.routing_source_ip: + if is_external: + if CONF.force_snat_range: + snat_range = CONF.force_snat_range + else: + snat_range = [] + else: + snat_range = ['0.0.0.0/0'] + for dest_range in snat_range: + if not is_external and CONF.public_interface: + firewall_manager.add_rule("nat on %s inet from %s to %s -> %s" % + (CONF.public_interface, + ip_range, + dest_range, + CONF.routing_source_ip)) + else: + firewall_manager.add_rule("nat inet from %s to %s -> %s" % + (ip_range, + dest_range, + CONF.routing_source_ip)) + firewall_manager.apply() + + +firewall_manager = FirewallManager() + + +def get_firewall_manager(): + return firewall_manager -- 2.8.1