Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
Cloud:OpenStack:Juno:Staging
openstack-neutron
infoblox-ipam-juno.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File infoblox-ipam-juno.patch of Package openstack-neutron
diff --git a/etc/dhcp_agent.ini b/etc/dhcp_agent.ini old mode 100644 new mode 100755 index 9836d35..6665b89 --- a/etc/dhcp_agent.ini +++ b/etc/dhcp_agent.ini @@ -86,3 +86,16 @@ # Timeout for ovs-vsctl commands. # If the timeout expires, ovs commands will fail with ALARMCLOCK error. # ovs_vsctl_timeout = 10 + +# Name of a bridge through which dhcp relay agent will connect to external +# network in which DHCP server resides +# dhcp_relay_bridge = br-mgmt + +# Path to dhcrelay agent executable +# dhcrelay_path = /usr/local/dhcp-4.3.0/sbin/dhcrelay + +# Path to dhclient executable +# dhcrelay_path = /usr/local/dhcp-4.3.0/sbin/dhclient + +# Relay interface name length +# interface_dev_name_len = 10 diff --git a/etc/neutron.conf b/etc/neutron.conf index 7fa8795..45e0129 100644 --- a/etc/neutron.conf +++ b/etc/neutron.conf @@ -75,6 +75,9 @@ lock_path = $state_path/lock # Paste configuration file # api_paste_config = api-paste.ini +# IPAM driver +# ipam_driver = neutron.ipam.drivers.neutron_ipam.NeutronIPAM + # The strategy to be used for auth. # Supported values are 'keystone'(default), 'noauth'. # auth_strategy = keystone diff --git a/etc/neutron/rootwrap.d/dhcp.filters b/etc/neutron/rootwrap.d/dhcp.filters index 7c11d70..8a8e07d 100644 --- a/etc/neutron/rootwrap.d/dhcp.filters +++ b/etc/neutron/rootwrap.d/dhcp.filters @@ -9,7 +9,8 @@ [Filters] # dhcp-agent -dnsmasq: EnvFilter, dnsmasq, root, NEUTRON_NETWORK_ID= +dnsmasq: EnvFilter, env, root, NEUTRON_NETWORK_ID=, dnsmasq + # dhcp-agent uses kill as well, that's handled by the generic KillFilter # it looks like these are the only signals needed, per # neutron/agent/linux/dhcp.py @@ -20,6 +21,9 @@ ovs-vsctl: CommandFilter, ovs-vsctl, root ivs-ctl: CommandFilter, ivs-ctl, root mm-ctl: CommandFilter, mm-ctl, root dhcp_release: CommandFilter, dhcp_release, root +dhcrelay: CommandFilter, /usr/local/dhcp-4.3.0/sbin/dhcrelay, root +dhclient: CommandFilter, /usr/local/dhcp-4.3.0/sbin/dhclient, root +kill: CommandFilter, kill, root # metadata proxy metadata_proxy: CommandFilter, neutron-ns-metadata-proxy, root diff --git a/neutron/agent/dhcp_agent.py b/neutron/agent/dhcp_agent.py index 19b8e9a..2c24634 100644 --- a/neutron/agent/dhcp_agent.py +++ b/neutron/agent/dhcp_agent.py @@ -23,6 +23,7 @@ from oslo.config import cfg from neutron.agent.common import config from neutron.agent.linux import dhcp +from neutron.agent.linux import dhcp_relay from neutron.agent.linux import external_process from neutron.agent.linux import interface from neutron.agent.linux import ovs_lib # noqa @@ -616,6 +617,7 @@ def register_options(): config.register_agent_state_opts_helper(cfg.CONF) config.register_root_helper(cfg.CONF) cfg.CONF.register_opts(dhcp.OPTS) + cfg.CONF.register_opts(dhcp_relay.OPTS) cfg.CONF.register_opts(interface.OPTS) diff --git a/neutron/agent/linux/dhcp_relay.py b/neutron/agent/linux/dhcp_relay.py new file mode 100755 index 0000000..17b39eb --- /dev/null +++ b/neutron/agent/linux/dhcp_relay.py @@ -0,0 +1,474 @@ +# Copyright 2014 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. +import os +import random + +import netaddr +from oslo.config import cfg + +from neutron.agent.linux import dhcp +from neutron.agent.linux import ip_lib +from neutron.agent.linux import utils +from neutron.common import exceptions as exc +from neutron.common import ipv6_utils +from neutron.openstack.common import log as logging +from neutron.openstack.common import uuidutils + +LOG = logging.getLogger(__name__) + +OPTS = [ + cfg.ListOpt('external_dhcp_servers', + default=None, + help=_('IP addresses of DHCP servers to relay to.')), + cfg.ListOpt('external_dns_servers', + default=None, + help=_('IP addresses of DNS servers to relay to.')), + cfg.StrOpt('dhcp_relay_bridge', + default=None, + help=_('Name of a bridge through which ipam proxy agent will' + ' connect to external network in which DHCP and DNS' + ' server resides.')), + cfg.StrOpt('dhclient_path', + default='dhclient', + help=_('Path to dhclient executable.')), + cfg.StrOpt('dhcrelay_path', + default='dhcrelay', + help=_('Path to dhcrelay executable.')), + cfg.BoolOpt('use_link_selection_option', + default=True, + help=_('Run dhcrelay with -o flag.')), + cfg.BoolOpt('use_ipv6_unicast_requests', + default=True, + help=_('Run dhcrelay -u server1%iface2 -u server2%iface2')), + cfg.StrOpt('dhcp_relay_management_network', + default=None, + help=_("CIDR for the management network served by " + "Infoblox DHCP member")), + cfg.BoolOpt('enable_ipv6_relay', + default=True, + help=_('Enable/Disable DHCP/DNS relay for IPv6')) +] + + +MGMT_INTERFACE_IP_ATTR = 'mgmt_iface_ip' + + +def _generate_mac_address(): + mac = [0x00, 0x16, 0x3e, + random.randint(0x00, 0x7f), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff)] + return ':'.join(map(lambda x: "%02x" % x, mac)) + + +class DhcpDnsProxy(dhcp.DhcpLocalProcess): + """DHCP & DNS relay agent class.""" + + MINIMUM_VERSION = 0 + DEV_NAME_LEN = 14 + RELAY_DEV_NAME_PREFIX = 'trel' + NEUTRON_NETWORK_ID_KEY = 'NEUTRON_NETWORK_ID' + DHCPv4 = 4 + DHCPv6 = 6 + + def _calc_dev_name_len(self): + if self.conf.interface_dev_name_len: + return self.conf.interface_dev_name_len + else: + return self.DEV_NAME_LEN + + def _enable_dns_dhcp(self): + """check if there is a subnet within the network with dhcp enabled.""" + for subnet in self.network.subnets: + if subnet.enable_dhcp: + return True + return False + + def __init__(self, conf, network, root_helper='sudo', + version=None, plugin=None): + super(DhcpDnsProxy, self).__init__(conf, network, root_helper, + version, plugin) + + external_dhcp_servers = self._get_relay_ips('external_dhcp_servers') + external_dns_servers = self._get_relay_ips('external_dns_servers') + required_options = {'dhcp_relay_bridge': self.conf.dhcp_relay_bridge, + 'external_dhcp_servers': external_dhcp_servers, + 'external_dns_servers': external_dns_servers} + + for option_name, option in required_options.iteritems(): + if not option: + LOG.error(_('You must specify an %(opt)s option in config'), + {'opt': option_name}) + raise exc.InvalidConfigurationOption( + opt_name=option_name, + opt_value=option + ) + + self.dev_name_len = self._calc_dev_name_len() + self.device_manager = DnsDhcpProxyDeviceManager( + conf, root_helper, plugin) + + @classmethod + def check_version(cls): + return 0 + + @classmethod + def get_isolated_subnets(cls, network): + """Returns a dict indicating whether or not a subnet is isolated""" + if hasattr(dhcp.Dnsmasq, 'get_isolated_subnets') \ + and callable(getattr(dhcp.Dnsmasq, 'get_isolated_subnets')): + dhcp.Dnsmasq.get_isolated_subnets(network) + + @classmethod + def should_enable_metadata(cls, conf, network): + """True if the metadata-proxy should be enabled for the network.""" + if hasattr(dhcp.Dnsmasq, 'should_enable_metadata') \ + and callable(getattr(dhcp.Dnsmasq, 'should_enable_metadata')): + dhcp.Dnsmasq.should_enable_metadata(conf, network) + else: + conf.enable_isolated_metadata + + @classmethod + def existing_dhcp_networks(cls, conf, root_helper): + """Return a list of existing networks ids that we have configs for.""" + confs_dir = os.path.abspath(os.path.normpath(conf.dhcp_confs)) + return [ + c for c in os.listdir(confs_dir) + if uuidutils.is_uuid_like(c) + ] + + def release_lease(self, mac_address, removed_ips): + """Release a DHCP lease.""" + pass + + def reload_allocations(self): + """Force the DHCP server to reload the assignment database.""" + pass + + @property + def ipv6_enabled(self): + return self.conf.enable_ipv6_relay and ipv6_utils.is_enabled() + + def get_dhcp_pid(self, version): + """Last known pid for the dhcrelay process spawned for this network.""" + return self._get_value_from_conf_file('dhcp%s_pid' % version, int) + + def get_dns_pid(self): + """Last known pid for the dnsmasq process spawned for this network.""" + return self._get_value_from_conf_file('dns_pid', int) + + def is_dhcrelay_pid(self, pid): + pid_path = '/proc/%s/cmdline' % pid + if (pid and os.path.isdir('/proc/%s/' % pid) and + self.conf.dhcrelay_path in open(pid_path).read()): + return True + return False + + def is_dhcp_active(self): + """Is any dhcprelay still active""" + pids = [self.get_dhcp_pid(version=DhcpDnsProxy.DHCPv4)] + if self.ipv6_enabled: + pids.append(self.get_dhcp_pid(version=DhcpDnsProxy.DHCPv6)) + + if not any(pids): + return False + + for pid in pids: + if self.is_dhcrelay_pid(pid): + return True + return False + + def is_dns_active(self): + pid = self.get_dns_pid() + if not pid: + return False + return os.path.isdir('/proc/%s/' % pid) + + def enable(self): + relay_iface_name = self._get_relay_device_name() + relay_iface_mac_address = _generate_mac_address() + self.device_manager.setup_relay( + self.network, + relay_iface_name, + relay_iface_mac_address, + self.conf.dhcp_relay_bridge) + + interface_name = self.device_manager.setup(self.network) + if self.is_dhcp_active() or self.is_dns_active(): + self.restart() + elif self._enable_dns_dhcp(): + self.interface_name = interface_name + self.spawn_process() + + def disable(self, retain_port=False): + def kill_proc(pid): + if not pid: + return + cmd = ['kill', '-9', pid] + utils.execute(cmd, self.root_helper) + + def check_dhcp_pid(): + if self.ipv6_enabled: + return self.get_dhcp_pid(DhcpDnsProxy.DHCPv4) and \ + self.get_dhcp_pid(DhcpDnsProxy.DHCPv6) + else: + return self.get_dhcp_pid(DhcpDnsProxy.DHCPv4) + + def log_dhcp_pid_info(): + if self.ipv6_enabled: + LOG.debug( + _('dhcrelay for %(net_id)s, dhcp_pid %(dhcp_pid)d, ' + 'dhcp6_pid %(dhcp6_pid)d, is stale, ignoring command'), + { + 'net_id': self.network.id, + 'dhcp_pid': self.get_dhcp_pid(DhcpDnsProxy.DHCPv4), + 'dhcp6_pid': self.get_dhcp_pid(DhcpDnsProxy.DHCPv6) + }) + else: + LOG.debug( + _('dhcrelay for %(net_id)s, dhcp_pid %(dhcp_pid)d ' + 'is stale, ignoring command'), + { + 'net_id': self.network.id, + 'dhcp_pid': self.get_dhcp_pid(DhcpDnsProxy.DHCPv4) + }) + + if self.is_dhcp_active(): + kill_proc(self.get_dhcp_pid(DhcpDnsProxy.DHCPv4)) + if self.ipv6_enabled: + kill_proc(self.get_dhcp_pid(DhcpDnsProxy.DHCPv6)) + elif check_dhcp_pid(): + log_dhcp_pid_info() + else: + LOG.debug(_('No dhcrelay started for %s'), self.network.id) + + if self.is_dns_active(): + kill_proc(self.get_dns_pid()) + elif self.get_dns_pid(): + LOG.debug(_('dnsmasq for %(net_id)s, dhcp_pid %(dns_pid)d, is' + ' stale, ignoring command'), + {'net_id': self.network.id, + 'dns_pid': self.get_dns_pid()} + ) + else: + LOG.debug(_('No dnsmasq started for %s'), self.network.id) + + if not retain_port: + self.device_manager.destroy(self.network, self.interface_name) + self.device_manager.destroy_relay( + self.network, + self._get_relay_device_name(), + self.conf.dhcp_relay_bridge) + + if self.conf.dhcp_delete_namespaces and self.network.namespace: + ns_ip = ip_lib.IPWrapper(self.root_helper, + self.network.namespace) + try: + ns_ip.netns.delete(self.network.namespace) + except RuntimeError: + msg = _('Failed trying to delete namespace: %s') + LOG.exception(msg, self.network.namespace) + self._remove_config_files() + + def spawn_process(self): + """Spawns a IPAM proxy processes for the network.""" + self._spawn_dhcp_proxy() + self._spawn_dns_proxy() + + def _construct_dhcrelay_commands(self, relay_ips, relay_ipv6s): + dhcrelay_v4_command = [ + self.conf.dhcrelay_path, '-4', '-a', + '-pf', self.get_conf_file_name('dhcp4_pid', ensure_conf_dir=True), + '-i', self.interface_name] + + ipv6_ok = self.ipv6_enabled + if ipv6_ok: + dhcrelay_v6_command = [ + self.conf.dhcrelay_path, '-6', '-I', + '-pf', self.get_conf_file_name('dhcp6_pid', ensure_conf_dir=True), + '-l', self.interface_name] + + if self.conf.use_link_selection_option: + # dhcrelay -4 -a -i iface1 -l iface2 server1 server2 + dhcrelay_v4_command.append('-o') + dhcrelay_v4_command.append(self._get_relay_device_name()) + + if ipv6_ok: + # dhcrelay -6 -l iface1 -u server1%iface2 -u server2%iface2 + if relay_ipv6s: + for ipv6_addr in relay_ipv6s: + dhcrelay_v6_command.append('-u') + + if self.conf.use_ipv6_unicast_requests: + dhcrelay_v6_command.append("%".join(( + ipv6_addr, self._get_relay_device_name()))) + else: + dhcrelay_v6_command.append( + self._get_relay_device_name()) + + dhcrelay_v4_command.append(" ".join(relay_ips)) + + if ipv6_ok: + return [ + dhcrelay_v4_command, + dhcrelay_v6_command + ] + else: + return [ + dhcrelay_v4_command + ] + + def _spawn_dhcp_proxy(self): + """Spawns a dhcrelay process for the network.""" + relay_ips = self._get_relay_ips('external_dhcp_servers') + relay_ipv6s = self._get_relay_ips('external_dhcp_ipv6_servers') + + if not relay_ips: + LOG.error(_('DHCP relay server isn\'t defined for network %s'), + self.network.id) + return + + commands = self._construct_dhcrelay_commands(relay_ips, relay_ipv6s) + + for cmd in commands: + if self.network.namespace: + ip_wrapper = ip_lib.IPWrapper(self.root_helper, + self.network.namespace) + try: + ip_wrapper.netns.execute(cmd) + except RuntimeError: + LOG.info(_("Can't start dhcrelay for %(command)s"), + {'command': cmd}) + else: + utils.execute(cmd, self.root_helper) + + def _spawn_dns_proxy(self): + """Spawns a Dnsmasq process in DNS relay only mode for the network.""" + relay_ips = self._get_relay_ips('external_dns_servers') + + if not relay_ips: + LOG.error(_('DNS relay server isn\'t defined for network %s'), + self.network.id) + return + + server_list = [] + for relay_ip in relay_ips: + server_list.append("--server=%s" % relay_ip) + + env = { + self.NEUTRON_NETWORK_ID_KEY: self.network.id, + } + + cmd = [ + 'dnsmasq', + '--no-hosts', + '--no-resolv', + '--strict-order', + '--bind-interfaces', + '--interface=%s' % self.interface_name, + '--except-interface=lo', + '--all-servers'] + cmd += server_list + cmd += ['--pid-file=%s' % self.get_conf_file_name( + 'dns_pid', ensure_conf_dir=True)] + + if self.network.namespace: + ip_wrapper = ip_lib.IPWrapper(self.root_helper, + self.network.namespace) + ip_wrapper.netns.execute(cmd, addl_env=env) + else: + utils.execute(cmd, self.root_helper, addl_env=env) + + def _get_relay_device_name(self): + return (self.RELAY_DEV_NAME_PREFIX + + self.network.id)[:self.dev_name_len] + + def _get_relay_ips(self, ip_opt_name): + # Try to get relay IP from the config. + relay_ips = getattr(self.conf, ip_opt_name, None) + # If not specified in config try to get from network object. + if not relay_ips: + relay_ips = getattr(self.network, ip_opt_name, None) + + if not relay_ips: + return None + + try: + for relay_ip in relay_ips: + netaddr.IPAddress(relay_ip) + except netaddr.core.AddrFormatError: + LOG.error(_('An invalid option value has been provided:' + ' %(opt_name)s=%(opt_value)s') % + dict(opt_name=ip_opt_name, opt_value=relay_ip)) + return None + + return list(set(relay_ips)) + + +class DnsDhcpProxyDeviceManager(dhcp.DeviceManager): + def setup_relay(self, network, iface_name, mac_address, relay_bridge): + if ip_lib.device_exists(iface_name, + self.root_helper, + network.namespace): + LOG.debug(_('Reusing existing device: %s.'), iface_name) + else: + self.driver.plug(network.id, + network.id, + iface_name, + mac_address, + namespace=network.namespace, + bridge=relay_bridge) + + use_static_ip_allocation = ( + self.conf.dhcp_relay_management_network + and hasattr(network, MGMT_INTERFACE_IP_ATTR)) + + if use_static_ip_allocation: + self._allocate_static_ip(network, iface_name) + else: + self._allocate_ip_via_dhcp(network, iface_name) + + def destroy_relay(self, network, device_name, relay_bridge): + self.driver.unplug(device_name, namespace=network.namespace, + bridge=relay_bridge) + + def _allocate_static_ip(self, network, iface_name): + mgmt_net = self.conf.dhcp_relay_management_network + relay_ip = netaddr.IPAddress(getattr(network, MGMT_INTERFACE_IP_ATTR)) + relay_net = netaddr.IPNetwork(mgmt_net) + relay_ip_cidr = '/'.join([str(relay_ip), str(relay_net.prefixlen)]) + relay_iface = ip_lib.IPDevice(iface_name, self.root_helper) + + LOG.info(_('Allocating static IP %(relay_ip)s for %(iface_name)s'), + {'relay_ip': relay_ip, 'iface_name': iface_name}) + + if network.namespace: + relay_iface.namespace = network.namespace + + relay_iface.addr.add( + relay_ip.version, relay_ip_cidr, relay_net.broadcast, + scope='link') + + def _allocate_ip_via_dhcp(self, network, iface_name): + dhcp_client_cmd = [self.conf.dhclient_path, iface_name] + + LOG.info(_('Running DHCP client for %s interface'), iface_name) + + if network.namespace: + ip_wrapper = ip_lib.IPWrapper(self.root_helper, + network.namespace) + ip_wrapper.netns.execute(dhcp_client_cmd) + else: + utils.execute(dhcp_client_cmd) diff --git a/neutron/agent/linux/interface.py b/neutron/agent/linux/interface.py index a693cf1..bfb4951 100644 --- a/neutron/agent/linux/interface.py +++ b/neutron/agent/linux/interface.py @@ -65,6 +65,9 @@ OPTS = [ default='publicURL', help=_("Network service endpoint type to pull from " "the keystone catalog")), + cfg.IntOpt('interface_dev_name_len', + default=14, + help=_("Maximum interace name length")), ] @@ -174,7 +177,8 @@ class LinuxInterfaceDriver(object): raise exceptions.BridgeDoesNotExist(bridge=bridge) def get_device_name(self, port): - return (self.DEV_NAME_PREFIX + port.id)[:self.DEV_NAME_LEN] + return ((self.DEV_NAME_PREFIX + port.id) + [:self.conf.interface_dev_name_len]) @abc.abstractmethod def plug(self, network_id, port_id, device_name, mac_address, diff --git a/neutron/common/config.py b/neutron/common/config.py index cda8d05..9bd5eec 100644 --- a/neutron/common/config.py +++ b/neutron/common/config.py @@ -115,6 +115,9 @@ core_opts = [ cfg.IntOpt('send_events_interval', default=2, help=_('Number of seconds between sending events to nova if ' 'there are any events to send.')), + cfg.StrOpt('ipam_driver', + default='neutron.ipam.drivers.neutron_ipam.NeutronIPAM', + help=_('IPAM driver')) ] core_cli_opts = [ diff --git a/neutron/db/agents_db.py b/neutron/db/agents_db.py index 0a013d5..aaa3983 100644 --- a/neutron/db/agents_db.py +++ b/neutron/db/agents_db.py @@ -115,7 +115,7 @@ class AgentDbMixin(ext_agent.AgentPluginBase): conf = {} return conf - def _make_agent_dict(self, agent, fields=None): + def _make_agent_dict(self, agent, fields=None, context=None): attr = ext_agent.RESOURCE_ATTRIBUTE_MAP.get( ext_agent.RESOURCE_NAME + 's') res = dict((k, agent[k]) for k in attr diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py old mode 100644 new mode 100755 index 3df2fe5..f013d40 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -38,7 +38,6 @@ from neutron.openstack.common import log as logging from neutron.openstack.common import uuidutils from neutron.plugins.common import constants as service_constants - LOG = logging.getLogger(__name__) # Ports with the following 'device_owner' values will not prevent @@ -51,8 +50,8 @@ LOG = logging.getLogger(__name__) AUTO_DELETE_PORT_OWNERS = [constants.DEVICE_OWNER_DHCP] -class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, - common_db_mixin.CommonDbMixin): +class NeutronCorePluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, + common_db_mixin.CommonDbMixin): """V2 Neutron plugin interface implementation using SQLAlchemy models. Whenever a non-read call happens the plugin will call an event handler @@ -136,8 +135,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, if base_mac[3] != '00': mac[3] = int(base_mac[3], 16) mac_address = ':'.join(map(lambda x: "%02x" % x, mac)) - if NeutronDbPluginV2._check_unique_mac(context, network_id, - mac_address): + if NeutronCorePluginV2._check_unique_mac(context, network_id, + mac_address): LOG.debug(_("Generated mac for network %(network_id)s " "is %(mac_address)s"), {'network_id': network_id, @@ -196,11 +195,11 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, @staticmethod def _generate_ip(context, subnets): try: - return NeutronDbPluginV2._try_generate_ip(context, subnets) + return NeutronCorePluginV2._try_generate_ip(context, subnets) except n_exc.IpAddressGenerationFailure: - NeutronDbPluginV2._rebuild_availability_ranges(context, subnets) + NeutronCorePluginV2._rebuild_availability_ranges(context, subnets) - return NeutronDbPluginV2._try_generate_ip(context, subnets) + return NeutronCorePluginV2._try_generate_ip(context, subnets) @staticmethod def _try_generate_ip(context, subnets): @@ -431,9 +430,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, if 'ip_address' in fixed: # Ensure that the IP's are unique - if not NeutronDbPluginV2._check_unique_ip(context, network_id, - subnet_id, - fixed['ip_address']): + if not NeutronCorePluginV2._check_unique_ip( + context, network_id, subnet_id, fixed['ip_address']): raise n_exc.IpAddressInUse(net_id=network_id, ip_address=fixed['ip_address']) @@ -474,7 +472,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, for fixed in fixed_ips: if 'ip_address' in fixed: # Remove the IP address from the allocation pool - NeutronDbPluginV2._allocate_specific_ip( + NeutronCorePluginV2._allocate_specific_ip( context, fixed['subnet_id'], fixed['ip_address']) ips.append({'ip_address': fixed['ip_address'], 'subnet_id': fixed['subnet_id']}) @@ -523,17 +521,17 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, device_owner) for ip in original_ips: LOG.debug(_("Port update. Hold %s"), ip) - NeutronDbPluginV2._delete_ip_allocation(context, - network_id, - ip['subnet_id'], - ip['ip_address']) + NeutronCorePluginV2._delete_ip_allocation(context, + network_id, + ip['subnet_id'], + ip['ip_address']) if to_add: LOG.debug(_("Port update. Adding %s"), to_add) ips = self._allocate_fixed_ips(context, to_add, mac_address) return ips, prev_ips - def _allocate_ips_for_port(self, context, port): + def _allocate_ips_for_port(self, context, port, port_id): """Allocate IP addresses for the port. If port['fixed_ips'] is set to 'ATTR_NOT_SPECIFIED', allocate IP @@ -541,6 +539,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, a subnet_id then allocate an IP address accordingly. """ p = port['port'] + p['id'] = port_id ips = [] fixed_configured = p['fixed_ips'] is not attributes.ATTR_NOT_SPECIFIED @@ -583,7 +582,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, version_subnets = [v4, v6_stateful] for subnets in version_subnets: if subnets: - result = NeutronDbPluginV2._generate_ip(context, subnets) + result = NeutronCorePluginV2._generate_ip(context, subnets) ips.append({'ip_address': result['ip_address'], 'subnet_id': result['subnet_id']}) return ips @@ -830,6 +829,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, 'shared': network['shared'], 'subnets': [subnet['id'] for subnet in network['subnets']]} + # Call auxiliary extend functions, if any if process_extensions: self._apply_dict_extend_functions( @@ -861,8 +861,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, self._apply_dict_extend_functions(attributes.SUBNETS, res, subnet) return self._fields(res, fields) - def _make_port_dict(self, port, fields=None, - process_extensions=True): + def _make_port_dict(self, port, fields=None, process_extensions=True): res = {"id": port["id"], 'name': port['name'], "network_id": port["network_id"], @@ -1098,55 +1097,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, self._validate_subnet(context, s) - tenant_id = self._get_tenant_id_for_create(context, s) - with context.session.begin(subtransactions=True): - network = self._get_network(context, s["network_id"]) - self._validate_subnet_cidr(context, network, s['cidr']) - # The 'shared' attribute for subnets is for internal plugin - # use only. It is not exposed through the API - args = {'tenant_id': tenant_id, - 'id': s.get('id') or uuidutils.generate_uuid(), - 'name': s['name'], - 'network_id': s['network_id'], - 'ip_version': s['ip_version'], - 'cidr': s['cidr'], - 'enable_dhcp': s['enable_dhcp'], - 'gateway_ip': s['gateway_ip'], - 'shared': network.shared} - if s['ip_version'] == 6 and s['enable_dhcp']: - if attributes.is_attr_set(s['ipv6_ra_mode']): - args['ipv6_ra_mode'] = s['ipv6_ra_mode'] - if attributes.is_attr_set(s['ipv6_address_mode']): - args['ipv6_address_mode'] = s['ipv6_address_mode'] - subnet = models_v2.Subnet(**args) - - context.session.add(subnet) - if s['dns_nameservers'] is not attributes.ATTR_NOT_SPECIFIED: - for addr in s['dns_nameservers']: - ns = models_v2.DNSNameServer(address=addr, - subnet_id=subnet.id) - context.session.add(ns) - - if s['host_routes'] is not attributes.ATTR_NOT_SPECIFIED: - for rt in s['host_routes']: - route = models_v2.SubnetRoute( - subnet_id=subnet.id, - destination=rt['destination'], - nexthop=rt['nexthop']) - context.session.add(route) - - for pool in s['allocation_pools']: - ip_pool = models_v2.IPAllocationPool(subnet=subnet, - first_ip=pool['start'], - last_ip=pool['end']) - context.session.add(ip_pool) - ip_range = models_v2.IPAvailabilityRange( - ipallocationpool=ip_pool, - first_ip=pool['start'], - last_ip=pool['end']) - context.session.add(ip_range) - - return self._make_subnet_dict(subnet) + return s def _update_subnet_dns_nameservers(self, context, id, s): old_dns_list = self._get_dns_by_subnet(context, id) @@ -1207,7 +1158,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, first_ip=p['start'], last_ip=p['end'], subnet_id=id) for p in s['allocation_pools']] context.session.add_all(new_pools) - NeutronDbPluginV2._rebuild_availability_ranges(context, [s]) + NeutronCorePluginV2._rebuild_availability_ranges(context, [s]) #Gather new pools for result: result_pools = [{'start': pool['start'], 'end': pool['end']} @@ -1232,6 +1183,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, s['ip_version'] = db_subnet.ip_version s['cidr'] = db_subnet.cidr s['id'] = db_subnet.id + s['network_id'] = db_subnet.network_id + s['tenant_id'] = db_subnet.tenant_id self._validate_subnet(context, s, cur_subnet=db_subnet) if 'gateway_ip' in s and s['gateway_ip'] is not None: @@ -1283,7 +1236,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, for a in allocated: if (is_auto_addr_subnet or a.ports.device_owner in AUTO_DELETE_PORT_OWNERS): - NeutronDbPluginV2._delete_ip_allocation( + NeutronCorePluginV2._delete_ip_allocation( context, subnet.network_id, id, a.ip_address) else: raise n_exc.SubnetInUse(subnet_id=id) @@ -1334,13 +1287,12 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, #Note(scollins) Add the generated mac_address to the port, #since _allocate_ips_for_port will need the mac when #calculating an EUI-64 address for a v6 subnet - p['mac_address'] = NeutronDbPluginV2._generate_mac(context, - network_id) + p['mac_address'] = NeutronCorePluginV2._generate_mac( + context, network_id) else: # Ensure that the mac on the network is unique - if not NeutronDbPluginV2._check_unique_mac(context, - network_id, - p['mac_address']): + if not NeutronCorePluginV2._check_unique_mac( + context, network_id, p['mac_address']): raise n_exc.MacAddressInUse(net_id=network_id, mac=p['mac_address']) @@ -1361,12 +1313,12 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, context.session.add(db_port) # Update the IP's for the port - ips = self._allocate_ips_for_port(context, port) + ips = self._allocate_ips_for_port(context, port, port_id) if ips: for ip in ips: ip_address = ip['ip_address'] subnet_id = ip['subnet_id'] - NeutronDbPluginV2._store_ip_allocation( + NeutronCorePluginV2._store_ip_allocation( context, ip_address, network_id, subnet_id, port_id) return self._make_port_dict(db_port, process_extensions=False) @@ -1406,7 +1358,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, # Update ips if necessary for ip in added_ips: - NeutronDbPluginV2._store_ip_allocation( + NeutronCorePluginV2._store_ip_allocation( context, ip['ip_address'], port['network_id'], ip['subnet_id'], port.id) # Remove all attributes in p which are not in the port DB model @@ -1530,3 +1482,210 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, device_id=device_id) if tenant_id != router['tenant_id']: raise n_exc.DeviceIDNotOwnedByTenant(device_id=device_id) + + +class NeutronIPAMPlugin(NeutronCorePluginV2): + @property + def ipam(self): + return manager.NeutronManager.get_ipam() + + def _update_ips_for_port(self, context, network_id, port_id, original_ips, + new_ips, mac_address, device_owner): + """Add or remove IPs from the port.""" + ips = [] + # These ips are still on the port and haven't been removed + prev_ips = [] + + # the new_ips contain all of the fixed_ips that are to be updated + if len(new_ips) > cfg.CONF.max_fixed_ips_per_port: + msg = _('Exceeded maximim amount of fixed ips per port') + raise n_exc.InvalidInput(error_message=msg) + + # Remove all of the intersecting elements + for original_ip in original_ips[:]: + for new_ip in new_ips[:]: + if ('ip_address' in new_ip and + original_ip['ip_address'] == new_ip['ip_address']): + original_ips.remove(original_ip) + new_ips.remove(new_ip) + prev_ips.append(original_ip) + + # Check if the IP's to add are OK + to_add = self._test_fixed_ips_for_port(context, network_id, + new_ips, device_owner) + + for ip in original_ips: + LOG.debug(_("Port update. Hold %s"), ip) + NeutronIPAMPlugin._delete_ip_allocation(context, + network_id, + ip['subnet_id'], + ip['ip_address']) + port = self._get_port(context, port_id) + self.ipam.deallocate_ip(context, port, ip) + + if to_add: + LOG.debug(_("Port update. Adding %s"), to_add) + ips = self._allocate_fixed_ips(context, to_add, mac_address) + + return ips, prev_ips + + def _allocate_ips_for_port(self, context, port, port_id): + """Allocate IP addresses for the port. + + If port['fixed_ips'] is set to 'ATTR_NOT_SPECIFIED', allocate IP + addresses for the port. If port['fixed_ips'] contains an IP address or + a subnet_id then allocate an IP address accordingly. + """ + p = port['port'] + p['id'] = port_id + ips = [] + + fixed_configured = p['fixed_ips'] is not attributes.ATTR_NOT_SPECIFIED + if fixed_configured: + configured_ips = self._test_fixed_ips_for_port(context, + p["network_id"], + p['fixed_ips'], + p['device_owner']) + + for ip in configured_ips: + ips.append(self.ipam.allocate_ip(context, p, ip=ip)) + else: + filter = {'network_id': [p['network_id']]} + subnets = self.get_subnets(context, filters=filter) + # Split into v4 and v6 subnets + v4 = [] + v6_stateful = [] + v6_stateless = [] + for subnet in subnets: + if subnet['ip_version'] == 4: + v4.append(subnet) + else: + if ipv6_utils.is_auto_address_subnet(subnet): + v6_stateless.append(subnet) + else: + v6_stateful.append(subnet) + + for subnet in v6_stateless: + prefix = subnet['cidr'] + ip_address = ipv6_utils.get_ipv6_addr_by_EUI64( + prefix, p['mac_address']) + if not self._check_unique_ip( + context, p['network_id'], + subnet['id'], ip_address.format()): + raise n_exc.IpAddressInUse( + net_id=p['network_id'], + ip_address=ip_address.format()) + ips.append({'ip_address': ip_address.format(), + 'subnet_id': subnet['id']}) + + version_subnets = [v4, v6_stateful] + for subnets in version_subnets: + if subnets: + ip = None + for subnet in subnets: + ip = self.ipam.allocate_ip( + context, p, ip={'subnet_id': subnet['id']}) + if ip: + ips.append(ip) + break + if not ip: + raise n_exc.IpAddressGenerationFailure( + net_id=subnet['network_id']) + + return ips + + def _update_subnet_dns_nameservers(self, context, id, s): + subnet, dhcp_changes = self.ipam.update_subnet(context, id, s) + + result = self._make_subnet_dict(subnet) + # Keep up with fields that changed + if 'new_dns' in dhcp_changes: + result['dns_nameservers'] = dhcp_changes['new_dns'] + if 'new_routes' in dhcp_changes: + result['host_routes'] = dhcp_changes['new_routes'] + if "dns_nameservers" in s: + del s["dns_nameservers"] + return dhcp_changes['new_dns'] + + def create_subnet(self, context, subnet): + s = super(NeutronIPAMPlugin, self).create_subnet(context, subnet) + with context.session.begin(subtransactions=True): + network = self._get_network(context, s["network_id"]) + self._validate_subnet_cidr(context, network, s['cidr']) + subnet = self.ipam.create_subnet(context, s) + return subnet + + def delete_subnet(self, context, id): + with context.session.begin(subtransactions=True): + subnet = self._get_subnet(context, id) + self.ipam.delete_subnet(context, subnet) + super(NeutronIPAMPlugin, self).delete_subnet(context, id) + + def create_network(self, context, network): + net = super(NeutronIPAMPlugin, self).create_network(context, + network) + n = network['network'] + n['id'] = net['id'] + self.ipam.create_network(context, n) + + return net + + def get_networks(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + nets = super(NeutronIPAMPlugin, self).get_networks( + context, filters, fields, sorts, limit, marker, page_reverse) + + for net in nets: + if 'id' in net: + net.update(self.ipam.get_additional_network_dict_params( + context, net['id'])) + + return nets + + def delete_network(self, context, id): + with context.session.begin(subtransactions=True): + network = self._get_network(context, id) + self.ipam.delete_network(context, network) + super(NeutronIPAMPlugin, self).delete_network(context, id) + + def update_network(self, context, id, network): + n = super(NeutronIPAMPlugin, self).update_network(context, + id, network) + + subnets = self._get_subnets_by_network(context, id) + for subnet in subnets: + self.ipam.update_subnet(context, subnet['id'], subnet) + + return n + + def create_port(self, context, port): + port_dict = super(NeutronIPAMPlugin, self).create_port(context, + port) + self.ipam.create_port(context, port_dict) + return port_dict + + def _delete_port(self, context, id): + query = (context.session.query(models_v2.Port). + enable_eagerloads(False).filter_by(id=id)) + if not context.is_admin: + query = query.filter_by(tenant_id=context.tenant_id) + + port = query.with_lockmode('update').one() + self.ipam.delete_port(context, port) + + allocated_qry = context.session.query( + models_v2.IPAllocation).with_lockmode('update') + # recycle all of the IP's + allocated = allocated_qry.filter_by(port_id=id) + host = dict(name=(port.get('id') or uuidutils.generate_uuid()), + mac_address=port['mac_address']) + for a in allocated: + ip = dict(subnet_id=a['subnet_id'], + ip_address=a['ip_address']) + self.ipam.deallocate_ip(context, host, ip) + + query.delete() + + +NeutronDbPluginV2 = NeutronIPAMPlugin diff --git a/neutron/db/infoblox/__init__.py b/neutron/db/infoblox/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron/db/infoblox/infoblox_db.py b/neutron/db/infoblox/infoblox_db.py new file mode 100755 index 0000000..b3b4d81 --- /dev/null +++ b/neutron/db/infoblox/infoblox_db.py @@ -0,0 +1,294 @@ +# Copyright 2014 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.config import cfg +from sqlalchemy.orm import exc + +import functools +import time + +from oslo.config import cfg +from neutron.db import external_net_db +from neutron.db.infoblox import models +from neutron.db import l3_db +from neutron.db import models_v2 +from neutron.openstack.common import log as logging +#from neutron.openstack.common.db import exception as db_exc +from oslo.db import exception as db_exc + + +OPTS = [ + cfg.IntOpt('infoblox_db_retry_interval', + default=5, + help=_('Seconds between db connection retries')), + cfg.IntOpt('infoblox_db_max_try', + default=20, + help=_('Maximum retries before error is raised. ' + '(setting -1 implies an infinite retry count)')), + cfg.BoolOpt('infoblox_db_inc_retry_interval', + default=True, + help=_('Whether to increase interval between retries, ' + 'up to infoblox_db_max_retry_interval')), + cfg.IntOpt('infoblox_db_max_retry_interval', + default=600, + help='Max seconds for total retries, if ' + 'infoblox_db_inc_retry_interval is enabled') +] + +cfg.CONF.register_opts(OPTS) + +LOG = logging.getLogger(__name__) + + +def _retry_db_error(func): + @functools.wraps(func) + def callee(*args, **kwargs): + next_interval = cfg.CONF.infoblox_db_retry_interval + remaining = cfg.CONF.infoblox_db_max_try + while True: + try: + return func(*args, **kwargs) + except Exception as e: + LOG.warn(_("DB error on %s: %s" + % (func, str(e)))) + if remaining == 0: + LOG.exception(_('DB exceeded retry limit.')) + raise db_exc.DBError(e) + if remaining != -1: + remaining -= 1 + time.sleep(next_interval) + if cfg.CONF.infoblox_db_inc_retry_interval: + next_interval = min( + next_interval * 2, + cfg.CONF.infoblox_db_max_retry_interval + ) + return callee + + +def get_used_members(context, member_type): + """Returns used members where map_id is not null.""" + q = context.session.query(models.InfobloxMemberMap) + members = q.filter(models.InfobloxMemberMap.member_type == + member_type).\ + filter(models.InfobloxMemberMap.map_id.isnot(None)).\ + distinct() + return members + + +def get_registered_members(context, member_type): + """Returns registered members.""" + q = context.session.query(models.InfobloxMemberMap) + members = q.filter_by(member_type=member_type).distinct() + return members + + +@_retry_db_error +def get_available_member(context, member_type): + """Returns available members.""" + q = context.session.query(models.InfobloxMemberMap) + q = q.filter(models.InfobloxMemberMap.member_type == member_type) + member = q.filter(models.InfobloxMemberMap.map_id.is_(None)).\ + with_lockmode("update").\ + first() + return member + + +def get_members(context, map_id, member_type): + """Returns members used by currently used mapping (tenant id, + network id or Infoblox netview name). + """ + q = context.session.query(models.InfobloxMemberMap) + members = q.filter_by(map_id=map_id, member_type=member_type).all() + return members + + +def register_member(context, map_id, member_name, member_type): + if map_id: + context.session.add( + models.InfobloxMemberMap(map_id=map_id, + member_name=member_name, + member_type=member_type)) + else: + context.session.add( + models.InfobloxMemberMap(member_name=member_name, + member_type=member_type)) + + +def attach_member(context, map_id, member_name, member_type): + context.session.query(models.InfobloxMemberMap.member_name).\ + filter_by(member_name=member_name, member_type=member_type).\ + update({'map_id': map_id}) + + +def delete_members(context, map_id): + with context.session.begin(subtransactions=True): + context.session.query( + models.InfobloxMemberMap).filter_by(map_id=map_id).delete() + + +@_retry_db_error +def release_member(context, map_id): + context.session.query(models.InfobloxMemberMap.member_name).\ + filter_by(map_id=map_id).update({'map_id': None}) + + +def is_last_subnet(context, subnet_id): + q = context.session.query(models_v2.Subnet) + return q.filter(models_v2.Subnet.id != subnet_id).count() == 0 + + +def is_last_subnet_in_network(context, subnet_id, network_id): + q = context.session.query(models_v2.Subnet) + return q.filter(models_v2.Subnet.id != subnet_id, + models_v2.Subnet.network_id == network_id).count() == 0 + + +def is_last_subnet_in_tenant(context, subnet_id, tenant_id): + q = context.session.query(models_v2.Subnet) + return q.filter(models_v2.Subnet.id != subnet_id, + models_v2.Subnet.tenant_id == tenant_id).count() == 0 + + +def is_last_subnet_in_private_networks(context, subnet_id): + sub_qry = context.session.query( + external_net_db.ExternalNetwork.network_id) + q = context.session.query(models_v2.Subnet.id) + q = q.filter(models_v2.Subnet.id != subnet_id) + q = q.filter(~models_v2.Subnet.network_id.in_(sub_qry)) + return q.count() == 0 + + +def is_network_external(context, network_id): + q = context.session.query(external_net_db.ExternalNetwork) + return q.filter_by(network_id=network_id).count() > 0 + + +def delete_ip_allocation(context, network_id, subnet, ip_address): + # Delete the IP address from the IPAllocate table + subnet_id = subnet['id'] + LOG.debug(_("Delete allocated IP %(ip_address)s " + "(%(network_id)s/%(subnet_id)s)"), locals()) + alloc_qry = context.session.query( + models_v2.IPAllocation).with_lockmode('update') + alloc_qry.filter_by(network_id=network_id, + ip_address=ip_address, + subnet_id=subnet_id).delete() + + +def get_subnets_by_network(context, network_id): + subnet_qry = context.session.query(models_v2.Subnet) + return subnet_qry.filter_by(network_id=network_id).all() + + +def get_subnets_by_port(context, port_id): + allocs = (context.session.query(models_v2.IPAllocation). + join(models_v2.Port). + filter_by(id=port_id) + .all()) + subnets = [] + subnet_qry = context.session.query(models_v2.Subnet) + for allocation in allocs: + subnets.append(subnet_qry. + filter_by(id=allocation.subnet_id). + first()) + return subnets + + +def get_port_by_id(context, port_id): + query = context.session.query(models_v2.Port) + return query.filter_by(id=port_id).one() + + +def get_network_name(context, subnet): + q = context.session.query(models_v2.Network) + net_name = q.join(models_v2.Subnet).filter( + models_v2.Subnet.id == subnet['id']).first() + if net_name: + return net_name.name + return None + + +def get_instance_id_by_floating_ip(context, floating_ip_id): + query = context.session.query(l3_db.FloatingIP, models_v2.Port) + query = query.filter(l3_db.FloatingIP.id == floating_ip_id) + query = query.filter(models_v2.Port.id == l3_db.FloatingIP.fixed_port_id) + result = query.first() + if result: + return result.Port.device_id + return None + + +def get_subnet_dhcp_port_address(context, subnet_id): + dhcp_port = (context.session.query(models_v2.IPAllocation). + filter_by(subnet_id=subnet_id). + join(models_v2.Port). + filter_by(device_owner='network:dhcp') + .first()) + if dhcp_port: + return dhcp_port.ip_address + return None + + +def get_network_view(context, network_id): + query = context.session.query(models.InfobloxNetViews) + net_view = query.filter_by(network_id=network_id).first() + if net_view: + return net_view.network_view + return None + + +def set_network_view(context, network_view, network_id): + ib_net_view = models.InfobloxNetViews(network_id=network_id, + network_view=network_view) + + # there should be only one NIOS network view per Openstack network + query = context.session.query(models.InfobloxNetViews) + obj = query.filter_by(network_id=network_id).first() + if not obj: + context.session.add(ib_net_view) + + +def add_management_ip(context, network_id, fixed_address): + context.session.add(models.InfobloxManagementNetIps( + network_id=network_id, + ip_address=fixed_address.ip, + fixed_address_ref=fixed_address.ref)) + + +def delete_management_ip(context, network_id): + query = context.session.query(models.InfobloxManagementNetIps) + query.filter_by(network_id=network_id).delete() + + +def get_management_ip_ref(context, network_id): + query = context.session.query(models.InfobloxManagementNetIps) + mgmt_ip = query.filter_by(network_id=network_id).first() + return mgmt_ip.fixed_address_ref if mgmt_ip else None + + +def get_management_net_ip(context, network_id): + query = context.session.query(models.InfobloxManagementNetIps) + mgmt_ip = query.filter_by(network_id=network_id).first() + return mgmt_ip.ip_address if mgmt_ip else None + + +def get_network(context, network_id): + network_qry = context.session.query(models_v2.Network) + return network_qry.filter_by(id=network_id).one() + + +def get_subnet(context, subnet_id): + subnet_qry = context.session.query(models_v2.Subnet) + return subnet_qry.filter_by(id=subnet_id).one() diff --git a/neutron/db/infoblox/models.py b/neutron/db/infoblox/models.py new file mode 100755 index 0000000..2f2bd5e --- /dev/null +++ b/neutron/db/infoblox/models.py @@ -0,0 +1,82 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 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. + +import sqlalchemy as sa + +from neutron.db import model_base +from neutron.db import models_v2 + + +DHCP_MEMBER_TYPE = 'dhcp' +DNS_MEMBER_TYPE = 'dns' + + +class InfobloxDNSMember(model_base.BASEV2, models_v2.HasId): + __tablename__ = 'infoblox_dns_members' + + network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id', + ondelete="CASCADE")) + server_ip = sa.Column(sa.String(40)) + server_ipv6 = sa.Column(sa.String(40)) + + +class InfobloxDHCPMember(model_base.BASEV2, models_v2.HasId): + __tablename__ = 'infoblox_dhcp_members' + + network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id', + ondelete="CASCADE")) + server_ip = sa.Column(sa.String(40)) + server_ipv6 = sa.Column(sa.String(40)) + + +class InfobloxMemberMap(model_base.BASEV2): + """Maps Neutron object to Infoblox member. + + map_id may point to Network ID, Tenant ID or Infoblox network view name + depending on configuration. Infoblox member names are unique. + """ + __tablename__ = 'infoblox_member_maps' + + member_name = sa.Column(sa.String(255), nullable=False, primary_key=True) + map_id = sa.Column(sa.String(255), nullable=True) + member_type = sa.Column(sa.String(10)) + + +class InfobloxNetViews(model_base.BASEV2): + """Connects Infoblox network views with Openstack networks. + This is needed to properly delete network views in NIOS on network + delete + """ + + __tablename__ = 'infoblox_net_views' + + network_id = sa.Column(sa.String(36), sa.ForeignKey("networks.id", + ondelete="CASCADE"), + nullable=False, primary_key=True) + network_view = sa.Column(sa.String(56)) + + +class InfobloxManagementNetIps(model_base.BASEV2): + """Holds IP addresses allocated on management network for DHCP relay + interface + """ + + __tablename__ = 'infoblox_mgmt_net_ips' + + network_id = sa.Column(sa.String(length=255), primary_key=True) + ip_address = sa.Column(sa.String(length=64), nullable=False) + fixed_address_ref = sa.Column(sa.String(length=255), nullable=False) diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index 2c915cd..2e037af 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -458,6 +458,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase): subnet['id'], subnet['cidr']) port.update({'device_id': router.id, 'device_owner': owner}) + ipam = manager.NeutronManager.get_ipam() + ipam.update_port(context, port) return port def _add_interface_by_subnet(self, context, router, subnet_id, owner): @@ -810,9 +812,12 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase): fip['tenant_id'] = floatingip_db['tenant_id'] fip['id'] = id fip_port_id = floatingip_db['floating_port_id'] - self._update_fip_assoc(context, fip, floatingip_db, - self._core_plugin.get_port( - context.elevated(), fip_port_id)) + port = self._core_plugin.get_port(context.elevated(), + fip_port_id) + self._update_fip_assoc(context, fip, floatingip_db, port) + ipam = manager.NeutronManager.get_ipam() + ipam.associate_floatingip(context, floatingip, port) + return old_floatingip, self._make_floatingip_dict(floatingip_db) def _floatingips_to_router_ids(self, floatingips): @@ -920,6 +925,10 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase): floating_ips = fip_qry.filter_by(fixed_port_id=port_id) for floating_ip in floating_ips: router_ids.add(floating_ip['router_id']) + + ipam = manager.NeutronManager.get_ipam() + ipam.disassociate_floatingip(context, floating_ip, port_id) + floating_ip.update({'fixed_port_id': None, 'fixed_ip_address': None, 'router_id': None}) diff --git a/neutron/db/migration/alembic_migrations/versions/172ace2194db_infoblox_net_view_to_network_id.py b/neutron/db/migration/alembic_migrations/versions/172ace2194db_infoblox_net_view_to_network_id.py new file mode 100644 index 0000000..76c37c1 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/172ace2194db_infoblox_net_view_to_network_id.py @@ -0,0 +1,48 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""Infoblox net view to network ID + +Revision ID: 172ace2194db +Revises: d9841b33bd +Create Date: 2014-09-09 11:03:23.737412 + +""" + +# revision identifiers, used by Alembic. +revision = '172ace2194db' +down_revision = 'd9841b33bd' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = ['*'] + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + op.create_table('infoblox_net_views', + sa.Column('network_id', + sa.String(36), + sa.ForeignKey("networks.id", + ondelete="CASCADE"), + nullable=False, + primary_key=True), + sa.Column('network_view', sa.String(56))) + + +def downgrade(active_plugins=None, options=None): + op.drop_table('infoblox_net_views') diff --git a/neutron/db/migration/alembic_migrations/versions/256b90dd9824_multiple_dhcp_members.py b/neutron/db/migration/alembic_migrations/versions/256b90dd9824_multiple_dhcp_members.py new file mode 100644 index 0000000..3445cc8 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/256b90dd9824_multiple_dhcp_members.py @@ -0,0 +1,62 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""Multiple DHCP members + +Revision ID: 256b90dd9824 +Revises: 172ace2194db +Create Date: 2014-09-15 05:54:38.612277 + +""" + +# revision identifiers, used by Alembic. +revision = '256b90dd9824' +down_revision = '172ace2194db' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + '*' +] + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + op.create_table( + 'infoblox_dhcp_members', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('network_id', sa.String(length=36), nullable=False), + sa.Column('server_ip', sa.String(length=40), nullable=False), + sa.Column('server_ipv6', sa.String(length=40), nullable=True), + sa.ForeignKeyConstraint(['network_id'], ['networks.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id')) + + op.create_table( + 'infoblox_dns_members', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('network_id', sa.String(length=36), nullable=False), + sa.Column('server_ip', sa.String(length=40), nullable=False), + sa.Column('server_ipv6', sa.String(length=40), nullable=True), + sa.ForeignKeyConstraint(['network_id'], ['networks.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id')) + + +def downgrade(active_plugins=None, options=None): + op.drop_table('infoblox_dhcp_members') + op.drop_table('infoblox_dns_members') diff --git a/neutron/db/migration/alembic_migrations/versions/33150f5993b6_infoblox_management_net_handling.py b/neutron/db/migration/alembic_migrations/versions/33150f5993b6_infoblox_management_net_handling.py new file mode 100644 index 0000000..e3ef4ae --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/33150f5993b6_infoblox_management_net_handling.py @@ -0,0 +1,58 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""Infoblox management net handling + +Currently DHCP relay is using dynamic DHCP to get the management IP address. +This is not desirable from customer point of view as they do want a static IP +assignment for such cases which will reduce any potential issue. + +Revision ID: 33150f5993b6 +Revises: 256b90dd9824 +Create Date: 2014-08-28 14:40:20.585390 + +""" + +# revision identifiers, used by Alembic. +revision = '33150f5993b6' +down_revision = '256b90dd9824' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + '*' +] + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + op.create_table('infoblox_mgmt_net_ips', + sa.Column('network_id', + sa.String(length=255), + primary_key=True), + sa.Column('ip_address', + sa.String(length=64), + nullable=False), + sa.Column('fixed_address_ref', + sa.String(length=255), + nullable=False)) + + +def downgrade(active_plugins=None, options=None): + op.drop_table('infoblox_mgmt_net_ips') diff --git a/neutron/db/migration/alembic_migrations/versions/78d27e9172_infoblox_db_v2.py b/neutron/db/migration/alembic_migrations/versions/78d27e9172_infoblox_db_v2.py new file mode 100755 index 0000000..4fcf87b --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/78d27e9172_infoblox_db_v2.py @@ -0,0 +1,46 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""Infoblox DB v2 + +Revision ID: 78d27e9172 +Revises: 45284803ad19 +Create Date: 2014-05-28 11:11:34.815843 + +""" + +# revision identifiers, used by Alembic. +revision = '78d27e9172' +down_revision = 'juno' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = ['*'] + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + op.create_table( + 'infoblox_member_maps', + sa.Column('member_name', sa.String(255), nullable=False), + sa.Column('map_id', sa.String(255), nullable=True)) + + +def downgrade(active_plugins=None, options=None): + op.drop_table('infoblox_member_maps') diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index 7a30775..a27236f 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -juno +172ace2194db diff --git a/neutron/db/migration/alembic_migrations/versions/d9841b33bd_add_infoblox_member_type.py b/neutron/db/migration/alembic_migrations/versions/d9841b33bd_add_infoblox_member_type.py new file mode 100644 index 0000000..de2b39b --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/d9841b33bd_add_infoblox_member_type.py @@ -0,0 +1,45 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""Add Infoblox member type + +Revision ID: d9841b33bd +Revises: 78d27e9172 +Create Date: 2014-06-23 18:25:15.557835 + +""" + +# revision identifiers, used by Alembic. +revision = 'd9841b33bd' +down_revision = '78d27e9172' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = ['*'] + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + op.add_column('infoblox_member_maps', + sa.Column('member_type', sa.String(10))) + op.execute("UPDATE infoblox_member_maps SET member_type='dhcp'") + + +def downgrade(active_plugins=None, options=None): + op.drop_column('infoblox_member_maps', 'member_type') diff --git a/neutron/ipam/__init__.py b/neutron/ipam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron/ipam/base.py b/neutron/ipam/base.py new file mode 100644 index 0000000..2109c46 --- /dev/null +++ b/neutron/ipam/base.py @@ -0,0 +1,357 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2014 OpenStack LLC. +# 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 abc + +import six + +from neutron.ipam.drivers import neutron_db +from neutron.openstack.common import log as logging + +# Ports with the following 'device_owner' values will not prevent +# network deletion. If delete_network() finds that all ports on a +# network have these owners, it will explicitly delete each port +# and allow network deletion to continue. Similarly, if delete_subnet() +# finds out that all existing IP Allocations are associated with ports +# with these owners, it will allow subnet deletion to proceed with the +# IP allocations being cleaned up by cascade. +AUTO_DELETE_PORT_OWNERS = ['network:dhcp'] + +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class DHCPController(neutron_db.NeutronPluginController): + """Base class for IPAM DHCP controller. Incapsulates logic for handling + DHCP service related actions. + """ + + @abc.abstractmethod + def configure_dhcp(self, context, backend_subnet, dhcp_params): + """Implement this if you need extra actions to be taken on DHCP server + during subnet creation. + :param backend_subnet: models_v2.Subnet object, represents a subnet + being created + :param dhcp_params: dict with DHCP arguments, such as dns_nameservers, + and host_routes + """ + pass + + @abc.abstractmethod + def reconfigure_dhcp(self, context, backend_subnet, dhcp_params): + """This is called on subnet update. Implement if DHCP needs to be + reconfigured on subnet change + :param backend_subnet: models_v2.Subnet object being updated + :param dhcp_params: dict with DHCP parameters, such as DNS nameservers, + and host routes + """ + pass + + @abc.abstractmethod + def disable_dhcp(self, context, backend_subnet): + """This is called on subnet delete. Implement if DHCP service needs to + be disabled for a given subnet. + :param backend_subnet: models_v2.Subnet object being deleted + """ + pass + + @abc.abstractmethod + def dhcp_is_enabled(self, context, backend_subnet): + """Returns True if DHDC service is enabled for a subnet, False + otherwise + :param backend_subnet: models_v2.Subnet object + """ + pass + + @abc.abstractmethod + def get_dhcp_ranges(self, context, backend_subnet): + """Returns DHCP range for a subnet + :param backend_subnet: models_v2.Subnet object + """ + pass + + @abc.abstractmethod + def bind_mac(self, context, backend_subnet, ip_address, mac_address): + """Binds IP address with MAC. + :param backend_subnet: models_v2.Subnet object + :param ip_address: IP address to be bound + :param mac_address: MAC address to be bound + """ + pass + + @abc.abstractmethod + def unbind_mac(self, context, backend_subnet, ip_address): + """Inverse action for bind_mac. + :param backend_subnet: models_v2.Subnet object; + :param ip_address: IP address to be unbound + """ + pass + + +@six.add_metaclass(abc.ABCMeta) +class DNSController(neutron_db.NeutronPluginController): + """Incapsulates DNS related logic""" + + @abc.abstractmethod + def bind_names(self, context, backend_port): + """Associate domain name with IP address for a given port + :param backend_port: models_v2.Port object + """ + pass + + @abc.abstractmethod + def unbind_names(self, context, backend_port): + """Disassociate domain name from a given port + :param backend_port: models_v2.Port object + """ + pass + + @abc.abstractmethod + def create_dns_zones(self, context, backend_subnet): + """Creates domain name space for a given subnet. This is called on + subnet creation. + :param backend_subnet: models_v2.Subnet object + """ + pass + + @abc.abstractmethod + def delete_dns_zones(self, context, backend_subnet): + """Deletes domain name space associated with a subnet. Called on + delete subnet. + :param backend_subnet: models_v2.Subnet object + """ + pass + + @abc.abstractmethod + def disassociate_floatingip(self, context, floatingip, port_id): + """Called when floating IP gets disassociated from port + :param floatingip: l3_db.FloatingIP object to be disassociated + :param port_id: UUID of a port being disassociated + """ + pass + + +@six.add_metaclass(abc.ABCMeta) +class IPAMController(neutron_db.NeutronPluginController): + """IP address management controller. Operates with higher-level entities + like networks, subnets and ports + """ + + @abc.abstractmethod + def create_subnet(self, context, subnet): + """Creates allocation pools and IP ranges for a subnet. + + :param subnet: user-supplied subnet + :return models_v2.Subnet object. + """ + pass + + @abc.abstractmethod + def update_subnet(self, context, subnet_id, subnet): + """Called on subnet update. + :param subnet_id: ID of a subnet being updated + :param subnet: user-supplied subnet object (dict) + """ + pass + + @abc.abstractmethod + def delete_subnet(self, context, subnet): + """Called on subnet delete. Remove all the higher-level objects + associated with a subnet + :param subnet: user-supplied subnet object (dict) + """ + pass + + @abc.abstractmethod + def get_subnets(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + pass + + @abc.abstractmethod + def force_off_ports(self, context, ports): + """Disable ports on subnet delete event + :param ports: list of models_v2.Port objects to be disabled + """ + pass + + @abc.abstractmethod + def get_subnet_by_id(self, context, subnet_id): + """Returns subnet by UUID + :param subnet_id: UUID of a subnet + """ + pass + + @abc.abstractmethod + def allocate_ip(self, context, backend_subnet, host, ip=None): + """Allocates IP address based either on a subnet's IP range or an IP + address provided as an argument + :param backend_subnet: models_v2.Subnet object + :param host: port which needs IP generated + :param ip: IP address to be allocated for a port/host. If not set, IP + address will be generated from subnet range + :returns: IP address allocated + """ + pass + + @abc.abstractmethod + def deallocate_ip(self, context, backend_subnet, host, ip): + """Frees IP allocation for a given address + :param backend_subnet: models_v2.Subnet object + :param host: host/port which has IP allocated + :param ip: IP address to be revoked + """ + pass + + @abc.abstractmethod + def create_network(self, context, network): + """Creates network in the database + :param network: user-supplied network object (dict) + :returns: models_v2.Network object + """ + pass + + @abc.abstractmethod + def delete_network(self, context, network_id): + """Deletes network from the database + :param network_id: UUID of a network to be deleted + """ + pass + + +@six.add_metaclass(abc.ABCMeta) +class IPAMManager(object): + """IPAM subsystem manager class which controls IPAM by calling DCHP, DNS + and IPAM controller methods + """ + + @abc.abstractmethod + def create_subnet(self, context, subnet): + """Called on subnet create event + :param subnet: user-supplied subnet object (dict) + :returns: models_v2.Subnet object being created + """ + pass + + @abc.abstractmethod + def update_subnet(self, context, id, subnet): + """Called on subnet update event + :param id: UUID of a subnet being updated + :param subnet: user-supplied subnet object (dict) + :returns: updated subnet + """ + pass + + @abc.abstractmethod + def delete_subnet(self, context, subnet_id): + """Called on delete subnet event + :param subnet_id: UUID of a subnet to be deleted + """ + pass + + @abc.abstractmethod + def delete_subnets_by_network(self, context, network_id): + pass + + @abc.abstractmethod + def get_subnet_by_id(self, context, subnet_id): + pass + + @abc.abstractmethod + def allocate_ip(self, context, host, ip): + """Called on port create event. Incapsulates logic associated with IP + allocation process. + :param host: host/port which needs IP to be allocated + :param ip: IP address for a port + """ + pass + + @abc.abstractmethod + def deallocate_ip(self, context, host, ip): + """Revoke IP allocated previously + :param host: host/port to have IP address deallocated + :param ip: IP address to revoke + """ + pass + + @abc.abstractmethod + def get_subnets(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + pass + + @abc.abstractmethod + def create_network(self, context, network): + """Called on network create event + :param network: user-supplied network object (dict) + """ + pass + + @abc.abstractmethod + def delete_network(self, context, network_id): + """Called on delete network event + :param network_id: UUID of network to be deleted + """ + pass + + @abc.abstractmethod + def create_port(self, context, port): + """Called on port create event + :param port: user-supplied port dict + """ + pass + + @abc.abstractmethod + def update_port(self, context, port): + """Called on port update event + :param port: user-supplied port dict + """ + pass + + @abc.abstractmethod + def delete_port(self, context, port): + """Called on port delete event + :param port: user-supplied port dict + """ + pass + + @abc.abstractmethod + def associate_floatingip(self, context, floatingip, port): + """Called on floating IP being associated with a port + :param floatingip: l3_db.FloatingIP object + :param port: models_v2.Port to be associated with floating IP + """ + pass + + @abc.abstractmethod + def disassociate_floatingip(self, context, floatingip, port_id): + """Inverse of associate floating IP. Removes relationship between + floating IP and a port + :param floatingip: l3_db.FloatingIP object to be disassociated from + port + :param port_id: port UUID to be disassociated from floating IP + """ + pass + + @abc.abstractmethod + def get_additional_network_dict_params(self, ctx, network_id): + """Returns a dict of extra arguments for a network. Place your + implementation if neutron agent(s) require extra information to + provision DHCP/DNS properly + :param network_id: UUID of a network to have extra arguments + """ + pass diff --git a/neutron/ipam/drivers/__init__.py b/neutron/ipam/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron/ipam/drivers/infoblox/__init__.py b/neutron/ipam/drivers/infoblox/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron/ipam/drivers/infoblox/config.py b/neutron/ipam/drivers/infoblox/config.py new file mode 100755 index 0000000..bba27cb --- /dev/null +++ b/neutron/ipam/drivers/infoblox/config.py @@ -0,0 +1,617 @@ +# Copyright 2014 OpenStack LLC. +# 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 re +import io +import logging +import operator + +from oslo.config import cfg as neutron_conf + +from neutron.db.infoblox import infoblox_db as ib_db +from neutron.db.infoblox import models as ib_models +from neutron.ipam.drivers.infoblox import exceptions +from neutron.ipam.drivers.infoblox import nova_manager +from neutron.ipam.drivers.infoblox import objects +from neutron.openstack.common import jsonutils + +LOG = logging.getLogger(__name__) +OPTS = [ + neutron_conf.StrOpt('conditional_config', default=None, + help=_("Infoblox conditional config path")), + neutron_conf.StrOpt('infoblox_members_config', default=None, + help=_("Path to infoblox members config file.")) +] + +neutron_conf.CONF.register_opts(OPTS) + + +class ConfigFinder(object): + """ + _variable_conditions: contains tenant_id or subnet_range "condition" + _static_conditions: contains global or tenant "condition" + _assigned_members: contains dhcp members to be registered in db + with its mapping id as network view + """ + VALID_STATIC_CONDITIONS = ['global', 'tenant'] + VALID_VARIABLE_CONDITIONS = ['tenant_id:', 'subnet_range:'] + VALID_CONDITIONS = VALID_STATIC_CONDITIONS + VALID_VARIABLE_CONDITIONS + + def __init__(self, stream=None, member_manager=None): + """Reads config from `io.IOBase`:stream:. Config is JSON format.""" + self._member_manager = member_manager + self._variable_conditions = [] + self._static_conditions = [] + self._assigned_members = dict() + self._is_member_registered = False + + if not member_manager: + self._member_manager = MemberManager() + if not stream: + config_file = neutron_conf.CONF.conditional_config + if not config_file: + raise exceptions.InfobloxConfigException( + msg="Config not found") + stream = io.FileIO(config_file) + + with stream: + try: + self._conf = jsonutils.loads(stream.read()) + self._read_conditions() + except ValueError as e: + raise exceptions.InfobloxConfigException(msg=e) + + def configure_members(self, context): + # Do this only once after neutron server is restarted + if self._is_member_registered: + return + + reg_members = self._member_manager.get_registered_members( + context) + + # 1. register unregistered members + # ------------------------------------------------------- + reg_member_names = [] + # if never been registered + if len(reg_members) > 0: + reg_member_names = map(operator.attrgetter('name'), + reg_members) + reg_member_name_set = set(reg_member_names) + conf_member_name_set = set( + map(operator.attrgetter('name'), + self._member_manager.configured_members) + ) + unreg_member_name_set = conf_member_name_set.difference( + reg_member_name_set) + self._member_manager.register_members(context, + list(unreg_member_name_set)) + + # 2. reserve the assigned members + # ------------------------------------------------------- + reserv_member_names = [] + reserv_mapped_ids = [] + if len(reg_members) > 0: + zip_list = zip(*[(m.name, m.map_id) + for m in reg_members if m.map_id]) + if len(zip_list) == 2: + reserv_member_names = list(zip_list[0]) + reserv_mapped_ids = list(zip_list[1]) + reserv_member_name_set = set(reserv_member_names) + + for netview, memberset in self._assigned_members.items(): + if netview in reserv_mapped_ids: + continue + + unreserv_member_name_set = memberset.difference( + reserv_member_name_set) + if len(unreserv_member_name_set) == 0: + continue + + member_name = unreserv_member_name_set.pop() + self._member_manager.reserve_member(context, + netview, + member_name, + ib_models.DHCP_MEMBER_TYPE) + self._member_manager.reserve_member(context, + netview, + member_name, + ib_models.DNS_MEMBER_TYPE) + + self._is_member_registered = True + + def find_config_for_subnet(self, context, subnet): + """ + Returns first configuration which matches the object being created. + :param context: + :param subnet: + :return: :raise exceptions.InfobloxConfigException: + """ + if not self._is_member_registered: + self.configure_members(context) + + # First search for matching variable condition + for conditions in [self._variable_conditions, self._static_conditions]: + for cfg in conditions: + cfg = Config(cfg, context, subnet, self._member_manager) + if self._condition_matches(context, cfg, subnet): + return cfg + + raise exceptions.InfobloxConfigException( + msg="No config found for subnet %s" % subnet) + + def get_all_configs(self, context, subnet): + cfgs = [] + for conditions in [self._variable_conditions, self._static_conditions]: + for cfg in conditions: + cfg = Config(cfg, context, subnet, self._member_manager) + cfgs.append(cfg) + return cfgs + + @staticmethod + def _variable_condition_match(condition, var, expected): + return (condition.startswith(var) and + condition.split(':')[1] == expected) + + def _condition_matches(self, context, config, subnet): + net_id = subnet.get('network_id', subnet.get('id')) + cidr = subnet.get('cidr') + tenant_id = subnet['tenant_id'] + + is_external = ib_db.is_network_external(context, net_id) + cond = config.condition + condition_matches = ( + cond == 'global' or cond == 'tenant' or + self._variable_condition_match(cond, 'tenant_id', tenant_id) or + self._variable_condition_match(cond, 'subnet_range', cidr)) + + return config.is_external == is_external and condition_matches + + def _read_conditions(self): + # Define lambdas to check + is_static_cond = lambda cond, static_conds: cond in static_conds + is_var_cond = lambda cond, var_conds: any([cond.startswith(valid) + for valid in var_conds]) + + for conf in self._conf: + # If condition contain colon: validate it as variable + if ':' in conf['condition'] and\ + is_var_cond(conf['condition'], + self.VALID_VARIABLE_CONDITIONS): + self._variable_conditions.append(conf) + # If not: validate it as static + elif is_static_cond(conf['condition'], + self.VALID_STATIC_CONDITIONS): + self._static_conditions.append(conf) + # If any of previous checker cannot validate value - rise error + else: + msg = 'Invalid condition specified: {0}'.format( + conf['condition']) + raise exceptions.InfobloxConfigException(msg=msg) + + # Look for assigned member; if dhcp_members list specific + # members, then network_view should be static as well + netview = conf.get('network_view', 'default') + members = conf.get('dhcp_members', Config.NEXT_AVAILABLE_MEMBER) + if not isinstance(members, list) and \ + members != Config.NEXT_AVAILABLE_MEMBER: + members = [members] + if isinstance(members, list) and \ + not netview.startswith('{'): + if self._assigned_members.get(netview): + self._assigned_members[netview].update(set(members)) + else: + self._assigned_members[netview] = set(members) + + +class PatternBuilder(object): + def __init__(self, *pattern): + self.pattern = '.'.join([el.strip('.') + for el in pattern if el is not None]) + + def build(self, context, subnet, port=None, ip_addr=None, instance_name=None): + """ + Builds string by passing supplied values into pattern + :raise exceptions.InfobloxConfigException: + """ + self._validate_pattern() + + subnet_name = subnet['name'] if subnet['name'] else subnet['id'] + + pattern_dict = { + 'network_id': subnet['network_id'], + 'network_name': ib_db.get_network_name(context, subnet), + 'tenant_id': subnet['tenant_id'], + 'subnet_name': subnet_name, + 'subnet_id': subnet['id'], + 'user_id': context.user_id + } + + if ip_addr: + octets = ip_addr.split('.') + ip_addr = ip_addr.replace('.', '-').replace(':', '-') + pattern_dict['ip_address'] = ip_addr + for i in xrange(len(octets)): + octet_key = 'ip_address_octet{i}'.format(i=(i + 1)) + pattern_dict[octet_key] = octets[i] + + if port: + pattern_dict['port_id'] = port['id'] + pattern_dict['instance_id'] = port['device_id'] + if instance_name: + pattern_dict['instance_name'] = instance_name + else: + if '{instance_name}' in self.pattern: + nm = nova_manager.NovaManager() + pattern_dict['instance_name'] = nm.get_instance_name_by_id( + port['device_id']) + + try: + fqdn = self.pattern.format(**pattern_dict) + except (KeyError, IndexError) as e: + raise exceptions.InfobloxConfigException( + msg="Invalid pattern %s" % e) + + return fqdn + + def _validate_pattern(self): + invalid_values = ['..'] + for val in invalid_values: + if val in self.pattern: + error_message = "Invalid pattern value {0}".format(val) + raise exceptions.InfobloxConfigException(msg=error_message) + + +class Config(object): + NEXT_AVAILABLE_MEMBER = '<next-available-member>' + NETWORK_VIEW_TEMPLATES = ['{tenant_id}', + '{network_name}', + '{network_id}'] + + DEFINING_ATTRS = ['condition', '_dhcp_members', '_dns_members', + '_net_view', '_dns_view'] + + def __init__(self, config_dict, context, subnet, + member_manager=None): + if not member_manager: + _member_manager = MemberManager() + + if 'condition' not in config_dict: + raise exceptions.InfobloxConfigException( + msg="Missing mandatory 'condition' config option") + + self.condition = config_dict['condition'] + self.is_external = config_dict.get('is_external', False) + + self._net_view = config_dict.get('network_view', 'default') + self._set_network_view_scope() + + self._dns_view = config_dict.get('dns_view', 'default') + + self.require_dhcp_relay = config_dict.get('require_dhcp_relay', False) + + self._dhcp_members = self._members_identifier( + config_dict.get('dhcp_members', self.NEXT_AVAILABLE_MEMBER)) + self._dns_members = self._members_identifier( + config_dict.get('dns_members', self._dhcp_members)) + + self.domain_suffix_pattern = config_dict.get( + 'domain_suffix_pattern', 'global.com') + self.hostname_pattern = config_dict.get( + 'hostname_pattern', 'host-{ip_address}.{subnet_name}') + + pattern = re.compile(r'\{\S+\}') + if pattern.findall(self.domain_suffix_pattern): + self.is_static_domain_suffix = False + else: + self.is_static_domain_suffix = True + + self.network_template = config_dict.get('network_template') + self.ns_group = config_dict.get('ns_group') + + self.context = context + self.subnet = subnet + self._member_manager = member_manager + + def __eq__(self, other): + return (isinstance(other, self.__class__) and + all(map(lambda attr: + getattr(self, attr) == getattr(other, attr), + self.DEFINING_ATTRS))) + + def __hash__(self): + return hash(tuple(self.DEFINING_ATTRS)) + + def __repr__(self): + values = { + 'condition': self.condition, + 'dns_members': self._dns_members, + 'dhcp_members': self._dhcp_members, + 'net_view': self._net_view, + 'dns_view': self._dns_view + } + + return "ConditionalConfig{0}".format(values) + + @property + def network_view_scope(self): + return self._net_view_scope + + @property + def network_view(self): + if self._net_view_scope == 'tenant_id': + return self.subnet['tenant_id'] + if self._net_view_scope == 'network_name': + return ib_db.get_network_name(self.context, self.subnet) + if self._net_view_scope == 'network_id': + return self.subnet['network_id'] + return self._net_view + + @property + def dns_view(self): + if self.network_view == 'default': + return self._dns_view + + return '.'.join([self._dns_view, self.network_view]) + + @property + def dhcp_members(self): + return self._get_members(ib_models.DHCP_MEMBER_TYPE) + + @property + def dns_members(self): + return self._get_members(ib_models.DNS_MEMBER_TYPE) + + @property + def is_global_config(self): + return self.condition == 'global' + + def reserve_dns_members(self): + reserved_members = self._reserve_members(self._dns_members, + self.ns_group, + ib_models.DNS_MEMBER_TYPE) + + if isinstance(reserved_members, list): + return reserved_members + elif reserved_members: + return [reserved_members] + else: + return [] + + def reserve_dhcp_members(self): + reserved_members = self._reserve_members(self._dhcp_members, + self.network_template, + ib_models.DHCP_MEMBER_TYPE) + + if isinstance(reserved_members, list): + return reserved_members + else: + return [reserved_members] + + def release_member(self, map_id): + self._member_manager.release_member(self.context, map_id) + + def requires_net_view(self): + return True + + def verify_subnet_update_is_allowed(self, subnet_new): + """ + Subnet update procedure is not allowed if Infoblox zone name exists on + NIOS. This can only happen if domain suffix pattern has subnet name + included. + """ + subnet_new_name = subnet_new.get('name') + subnet_name = self.subnet.get('name') + pattern = self.domain_suffix_pattern + update_allowed = not (subnet_name is not None and + subnet_new_name is not None and + subnet_name != subnet_new_name and + '{subnet_name}' in pattern) + + if not update_allowed: + raise exceptions.OperationNotAllowed( + reason="subnet_name is in domain name pattern") + + if subnet_new.get('network') and subnet_new.get('network_before'): + network_new_name = subnet_new.get('network').get('name') + network_name = subnet_new.get('network_before').get('name') + update_allowed = not (network_name is not None and + network_new_name is not None and + network_name != network_new_name and + '{network_name}' in pattern) + + if not update_allowed: + raise exceptions.OperationNotAllowed( + reason="network_name is in domain name pattern") + + def _set_network_view_scope(self): + if (self._net_view.startswith('{') and + self._net_view not in self.NETWORK_VIEW_TEMPLATES): + raise exceptions.InfobloxConfigException( + msg="Invalid config value for 'network_view'") + + if self._net_view == '{tenant_id}': + self._net_view_scope = 'tenant_id' + elif self._net_view == '{network_name}': + self._net_view_scope = 'network_name' + elif self._net_view == '{network_id}': + self._net_view_scope = 'network_id' + else: + self._net_view_scope = 'static' + + def _get_members(self, member_type): + members = self._member_manager.find_members(self.context, + self.network_view, + member_type) + if members: + return members + + msg = ("Looks like you're trying to call config.{member_type}_member " + "without reserving one. You should call " + "reserve_{member_type}_member() " + "first!".format(member_type=member_type)) + raise RuntimeError(msg) + + def _reserve_members_list(self, assigned_members, member_type): + members_to_reserve = [self._member_manager.get_member(member) + for member in assigned_members] + for member in members_to_reserve: + self._member_manager.reserve_member(self.context, + self.network_view, + member.name, + member_type) + return members_to_reserve + + def _reserve_by_template(self, assigned_members, template, member_type): + member = self._get_member_from_template(assigned_members, template) + self._member_manager.reserve_member(self.context, + self.network_view, + member.name, + member_type) + return member + + def _reserve_next_avaliable(self, member_type): + member = self._member_manager.next_available(self.context, + member_type) + self._member_manager.reserve_member(self.context, + self.network_view, + member.name, + member_type) + return member + + def _reserve_members(self, assigned_members, template, member_type): + members = self._member_manager.find_members(self.context, + self.network_view, + member_type) + if members: + return members + + if assigned_members == self.NEXT_AVAILABLE_MEMBER: + return self._reserve_next_avaliable(member_type) + elif isinstance(assigned_members, list): + return self._reserve_members_list(assigned_members, + member_type) + elif template: + return self._reserve_by_template(assigned_members, + template, + member_type) + + def _get_member_from_template(self, assigned_members, template): + member_defined = (assigned_members != self.NEXT_AVAILABLE_MEMBER + and isinstance(assigned_members, basestring)) + if template and not member_defined: + msg = 'Member MUST be configured for {template}'.format( + template=template) + raise exceptions.InfobloxConfigException(msg=msg) + return self._member_manager.get_member(assigned_members) + + def _members_identifier(self, members): + if not isinstance(members, list) and \ + members != self.NEXT_AVAILABLE_MEMBER: + members = [members] + return members + + +class MemberManager(object): + def __init__(self, member_config_stream=None): + if not member_config_stream: + config_file = neutron_conf.CONF.infoblox_members_config + if not config_file: + raise exceptions.InfobloxConfigException( + msg="Config not found") + member_config_stream = io.FileIO(config_file) + + with member_config_stream: + all_members = jsonutils.loads(member_config_stream.read()) + + try: + self.configured_members = map( + lambda m: objects.Member(name=m.get('name'), + ip=m.get('ipv4addr'), + ipv6=m.get('ipv6addr'), + delegate=m.get('delegate'), + map_id=None), + filter(lambda m: m.get('is_available', True), + all_members)) + except KeyError as key: + raise exceptions.InfobloxConfigException( + msg="Invalid member config key: %s" % key) + + if self.configured_members is None or \ + len(self.configured_members) == 0: + raise exceptions.InfobloxConfigException( + msg="Configured member not found") + + def __repr__(self): + values = { + 'configured_members': self.configured_members + } + return "MemberManager{0}".format(values) + + def register_members(self, context, member_names): + for member_name in member_names: + ib_db.register_member(context, None, member_name, + ib_models.DHCP_MEMBER_TYPE) + ib_db.register_member(context, None, member_name, + ib_models.DNS_MEMBER_TYPE) + + def get_registered_members(self, context, + member_type=ib_models.DHCP_MEMBER_TYPE): + registered_members = ib_db.get_registered_members(context, + member_type) + members = [] + for reg_member in registered_members: + for member in self.configured_members: + if member.name == reg_member.member_name: + member.map_id = reg_member.map_id + members.append(member) + return members + + def next_available(self, context, member_type): + avail_member = ib_db.get_available_member(context, member_type) + if not avail_member: + raise exceptions.InfobloxConfigException( + msg="No infoblox member available") + + return self.get_member(avail_member.member_name) + + def reserve_member(self, context, mapping, member_name, member_type): + ib_db.attach_member(context, mapping, member_name, member_type) + + def release_member(self, context, mapping): + ib_db.release_member(context, mapping) + + def get_member(self, member_name): + for member in self.configured_members: + if member.name == member_name: + return member + raise exceptions.InfobloxConfigException( + msg="No infoblox member available") + + def find_members(self, context, map_id, member_type): + existing_members = ib_db.get_members(context, map_id, member_type) + if not existing_members: + return [] + + members = [] + for existing_member in existing_members: + for member in self.configured_members: + if member.name == existing_member.member_name: + members.append(member) + + if not members: + msg = "Reserved member not available in config" + raise exceptions.InfobloxConfigException(msg=msg) + + return members diff --git a/neutron/ipam/drivers/infoblox/connector.py b/neutron/ipam/drivers/infoblox/connector.py new file mode 100755 index 0000000..aaa168f --- /dev/null +++ b/neutron/ipam/drivers/infoblox/connector.py @@ -0,0 +1,357 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2014 OpenStack LLC. +# 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 functools +import re + +from oslo.config import cfg +import requests +from requests import exceptions as req_exc +import urllib +import urlparse + +from neutron.ipam.drivers.infoblox import exceptions as exc +from neutron.openstack.common import jsonutils +from neutron.openstack.common import log as logging + + +OPTS = [ + cfg.StrOpt('infoblox_wapi', help=_("REST API url")), + cfg.StrOpt('infoblox_username', help=_("User name")), + cfg.StrOpt('infoblox_password', help=_("User password")), + cfg.BoolOpt('infoblox_sslverify', default=False), + cfg.IntOpt('infoblox_http_pool_connections', default=100), + cfg.IntOpt('infoblox_http_pool_maxsize', default=100), + cfg.IntOpt('infoblox_http_request_timeout', default=120) +] + +cfg.CONF.register_opts(OPTS) + + +LOG = logging.getLogger(__name__) + + +def reraise_neutron_exception(func): + @functools.wraps(func) + def callee(*args, **kwargs): + try: + return func(*args, **kwargs) + except req_exc.Timeout as e: + LOG.error(e.message) + raise exc.InfobloxTimeoutError(e) + except req_exc.RequestException as e: + LOG.error(_("HTTP request failed: %s"), e) + raise exc.InfobloxConnectionError(reason=e) + return callee + + +class Infoblox(object): + """ + Infoblox class + + Defines methods for getting, creating, updating and + removing objects from an Infoblox server instance. + """ + + CLOUD_WAPI_MAJOR_VERSION = 2 + + def __init__(self): + """ + Initialize a new Infoblox object instance + Args: + config (str): Path to the Infoblox configuration file + """ + self.wapi = cfg.CONF.infoblox_wapi + self.username = cfg.CONF.infoblox_username + self.password = cfg.CONF.infoblox_password + self.sslverify = cfg.CONF.infoblox_sslverify + self.request_timeout = cfg.CONF.infoblox_http_request_timeout + + if not self.wapi or not self.username or not self.password: + raise exc.InfobloxIsMisconfigured() + + self.is_cloud = self.is_cloud_wapi(self.wapi) + self.session = requests.Session() + adapter = requests.adapters.HTTPAdapter( + pool_connections=cfg.CONF.infoblox_http_pool_connections, + pool_maxsize=cfg.CONF.infoblox_http_pool_maxsize) + self.session.mount('http://', adapter) + self.session.mount('https://', adapter) + self.session.auth = (self.username, self.password) + self.session.verify = self.sslverify + + @staticmethod + def is_cloud_wapi(wapi_url): + version_match = re.search('\/wapi\/v(\d+)\.(\d+)', wapi_url) + if version_match: + if int(version_match.group(1)) >= Infoblox.CLOUD_WAPI_MAJOR_VERSION: + return True + return False + + def _construct_url(self, relative_path, query_params=None, extattrs=None): + if query_params is None: + query_params = {} + if extattrs is None: + extattrs = {} + + if not relative_path or relative_path[0] == '/': + raise ValueError('Path in request must be relative.') + query = '' + if query_params or extattrs: + query = '?' + + if extattrs: + attrs_queries = [] + for key, value in extattrs.items(): + attrs_queries.append('*' + key + '=' + value['value']) + query += '&'.join(attrs_queries) + if query_params: + if len(query) > 1: + query += '&' + query += urllib.urlencode(query_params) + + baseurl = urlparse.urljoin(self.wapi, urllib.quote(relative_path)) + return baseurl + query + + @staticmethod + def _validate_objtype_or_die(objtype, objtype_expected=True): + if not objtype: + raise ValueError('WAPI object type can\'t be empty.') + if objtype_expected and '/' in objtype: + raise ValueError('WAPI object type can\'t contain slash.') + + @reraise_neutron_exception + def get_object(self, objtype, payload=None, return_fields=None, + extattrs=None, proxy=False): + """ + Retrieve a list of Infoblox objects of type 'objtype' + Args: + objtype (str): Infoblox object type, e.g. 'network', + 'range', etc. + payload (dict): Payload with data to send + return_fields (list): List of fields to be returned + extattrs (list): List of Extensible Attributes + Returns: + A list of the Infoblox objects requested + Raises: + InfobloxObjectNotFound + """ + if return_fields is None: + return_fields = [] + if extattrs is None: + extattrs = {} + + self._validate_objtype_or_die(objtype, objtype_expected=False) + + query_params = dict() + if payload: + query_params = payload + + if return_fields: + query_params['_return_fields'] = ','.join(return_fields) + + # Some get requests like 'ipv4address' should be always + # proxied to GM on Hellfire + # If request is cloud and proxy is not forced yet, + # then plan to do 2 request: + # - the first one is not proxified to GM + # - the second is proxified to GM + urls = dict() + urls['direct'] = self._construct_url(objtype, query_params, extattrs) + if self.is_cloud: + query_params['_proxy_search'] = 'GM' + urls['proxy'] = self._construct_url(objtype, query_params, extattrs) + + url = urls['direct'] + if self.is_cloud and proxy: + url = urls['proxy'] + + headers = {'Content-type': 'application/json'} + + ib_object = self._get_object(objtype, url, headers) + if ib_object: + return ib_object + + # if cloud api and proxy is not used, use proxy + if self.is_cloud and not proxy: + return self._get_object(objtype, urls['proxy'], headers) + + return None + + def _get_object(self, objtype, url, headers): + r = self.session.get(url, + verify=self.sslverify, + timeout=self.request_timeout, + headers=headers) + + if r.status_code == requests.codes.UNAUTHORIZED: + raise exc.InfobloxBadWAPICredential(response='') + + if r.status_code != requests.codes.ok: + raise exc.InfobloxSearchError( + response=jsonutils.loads(r.content), + objtype=objtype, + content=r.content, + code=r.status_code) + + return jsonutils.loads(r.content) + + @reraise_neutron_exception + def create_object(self, objtype, payload, + return_fields=None, delegate_member=None): + """ + Create an Infoblox object of type 'objtype' + Args: + objtype (str): Infoblox object type, + e.g. 'network', 'range', etc. + payload (dict): Payload with data to send + return_fields (list): List of fields to be returned + Returns: + The object reference of the newly create object + Raises: + InfobloxException + """ + if self.is_cloud and delegate_member and delegate_member.delegate: + payload.update({"cloud_info": { + "delegated_member": delegate_member.specifier}}) + + if not return_fields: + return_fields = [] + + self._validate_objtype_or_die(objtype) + + query_params = dict() + + if return_fields: + query_params['_return_fields'] = ','.join(return_fields) + + url = self._construct_url(objtype, query_params) + data = jsonutils.dumps(payload) + headers = {'Content-type': 'application/json'} + r = self.session.post(url, + data=data, + verify=self.sslverify, + timeout=self.request_timeout, + headers=headers) + + if r.status_code == requests.codes.UNAUTHORIZED: + raise exc.InfobloxBadWAPICredential(response='') + + if r.status_code != requests.codes.CREATED: + raise exc.InfobloxCannotCreateObject( + response=jsonutils.loads(r.content), + objtype=objtype, + content=r.content, + args=payload, + code=r.status_code) + + return jsonutils.loads(r.content) + + @reraise_neutron_exception + def call_func(self, func_name, ref, payload, return_fields=None): + if not return_fields: + return_fields = [] + + query_params = dict() + query_params['_function'] = func_name + + if return_fields: + query_params['_return_fields'] = ','.join(return_fields) + + url = self._construct_url(ref, query_params) + + headers = {'Content-type': 'application/json'} + r = self.session.post(url, + data=jsonutils.dumps(payload), + verify=self.sslverify, + headers=headers) + + if r.status_code == requests.codes.UNAUTHORIZED: + raise exc.InfobloxBadWAPICredential(response='') + + if r.status_code not in (requests.codes.CREATED, + requests.codes.ok): + raise exc.InfobloxFuncException( + response=jsonutils.loads(r.content), + ref=ref, + func_name=func_name, + content=r.content, + code=r.status_code) + + return jsonutils.loads(r.content) + + @reraise_neutron_exception + def update_object(self, ref, payload, return_fields=None): + """ + Update an Infoblox object + Args: + ref (str): Infoblox object reference + payload (dict): Payload with data to send + Returns: + The object reference of the updated object + Raises: + InfobloxException + """ + query_params = {} + if return_fields: + query_params['_return_fields'] = ','.join(return_fields) + + headers = {'Content-type': 'application/json'} + r = self.session.put(self._construct_url(ref, query_params), + data=jsonutils.dumps(payload), + verify=self.sslverify, + timeout=self.request_timeout, + headers=headers) + + if r.status_code == requests.codes.UNAUTHORIZED: + raise exc.InfobloxBadWAPICredential(response='') + + if r.status_code != requests.codes.ok: + raise exc.InfobloxCannotUpdateObject( + response=jsonutils.loads(r.content), + ref=ref, + content=r.content, + code=r.status_code) + + return jsonutils.loads(r.content) + + @reraise_neutron_exception + def delete_object(self, ref): + """ + Remove an Infoblox object + Args: + ref (str): Object reference + Returns: + The object reference of the removed object + Raises: + InfobloxException + """ + r = self.session.delete(self._construct_url(ref), + verify=self.sslverify, + timeout=self.request_timeout) + + if r.status_code == requests.codes.UNAUTHORIZED: + raise exc.InfobloxBadWAPICredential(response='') + + if r.status_code != requests.codes.ok: + raise exc.InfobloxCannotDeleteObject( + response=jsonutils.loads(r.content), + ref=ref, + content=r.content, + code=r.status_code) + + return jsonutils.loads(r.content) diff --git a/neutron/ipam/drivers/infoblox/constants.py b/neutron/ipam/drivers/infoblox/constants.py new file mode 100644 index 0000000..cef4cea --- /dev/null +++ b/neutron/ipam/drivers/infoblox/constants.py @@ -0,0 +1,31 @@ +# Copyright 2014 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from neutron.common import constants as neutron_constants +from neutron.plugins.common import constants as plugins_constants + +NEUTRON_DEVICE_OWNER_TO_PATTERN_MAP = { + neutron_constants.DEVICE_OWNER_DHCP: 'dhcp-port-{ip_address}', + neutron_constants.DEVICE_OWNER_ROUTER_INTF: 'router-iface-{ip_address}', + neutron_constants.DEVICE_OWNER_ROUTER_GW: 'router-gw-{ip_address}', + neutron_constants.DEVICE_OWNER_FLOATINGIP: 'floating-ip-{ip_address}', + 'neutron:' + plugins_constants.LOADBALANCER: 'lb-vip-{ip_address}', +} + +NEUTRON_INTERNAL_SERVICE_DEVICE_OWNERS = [ + neutron_constants.DEVICE_OWNER_DHCP, + neutron_constants.DEVICE_OWNER_ROUTER_INTF, + neutron_constants.DEVICE_OWNER_ROUTER_GW, + 'neutron:' + plugins_constants.LOADBALANCER +] diff --git a/neutron/ipam/drivers/infoblox/dns_controller.py b/neutron/ipam/drivers/infoblox/dns_controller.py new file mode 100755 index 0000000..e8130ea --- /dev/null +++ b/neutron/ipam/drivers/infoblox/dns_controller.py @@ -0,0 +1,308 @@ +# Copyright 2014 OpenStack LLC. +# 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 re +from oslo.config import cfg as neutron_conf +from taskflow.patterns import linear_flow + +from neutron.common import constants as neutron_constants +from neutron.db.infoblox import infoblox_db as infoblox_db +from neutron.common import ipv6_utils +from neutron.ipam.drivers.infoblox import config +from neutron.ipam.drivers.infoblox import connector +from neutron.ipam.drivers.infoblox import constants as ib_constants +from neutron.ipam.drivers.infoblox import ea_manager +from neutron.ipam.drivers.infoblox import exceptions +from neutron.ipam.drivers.infoblox import object_manipulator +from neutron.ipam.drivers.infoblox import tasks +from neutron.ipam.drivers import neutron_ipam +from neutron.openstack.common import log as logging + +OPTS = [ + neutron_conf.StrOpt('private_dns_view_name', + default=None, + help=_("If single_network_view_name is specified, " + "this option will define DNS View name used " + "to serve networks from the single network " + "view. Otherwise it is ignored and " + "'default.<netview_name>' is used.")), + neutron_conf.StrOpt('external_dns_view_name', + default=None, + help=_("All the subnets created in external networks " + "will be associated with DNS View with such " + "name. If not specified, name " + "'default.<netview_name>' will be used.")), + neutron_conf.StrOpt('subnet_fqdn_suffix', + default='com', + help=_("Suffix for subnet domain name. Used to " + "generate subnet FQDN which is built using " + "the following pattern " + "<subnet_domain><subnet_fqdn_suffix>. " + "Subnet domain uniquely represents subnet and " + "equal to subnet name if specified, otherwise " + "equal to the first part of subnet uuid.")), + neutron_conf.BoolOpt('use_global_dns_zone', + default=True, + help=_("Use global DNS zone. Global private DNS zone " + "only make sense when we use single network " + "view")), + neutron_conf.BoolOpt('allow_admin_network_deletion', + default=False, + help=_("Allow admin network which is global, " + "external, or shared to be deleted")) +] + +LOG = logging.getLogger(__name__) +neutron_conf.CONF.register_opts(OPTS) + + +class InfobloxDNSController(neutron_ipam.NeutronDNSController): + + SUBDOMAIN_NAME_LEN = 8 + + def __init__(self, ip_allocator, manipulator=None, config_finder=None): + super(InfobloxDNSController, self).__init__() + + if not manipulator: + manipulator = object_manipulator.InfobloxObjectManipulator( + connector.Infoblox()) + + self.infoblox = manipulator + self.ip_allocator = ip_allocator + self.config_finder = config_finder + self.ea_manager = ea_manager.InfobloxEaManager(infoblox_db) + self.pattern_builder = config.PatternBuilder + + def disassociate_floatingip(self, context, ip_address, port_id): + floating_port_id = ip_address.get('floating_port_id') + port = infoblox_db.get_port_by_id(context, floating_port_id) + extattrs = self.ea_manager.get_extattrs_for_ip(context, port, True) + self.bind_names(context, port, disassociate=True) + + @staticmethod + def get_hostname_pattern(port, cfg, instance_name): + port_owner = port['device_owner'] + if port_owner == neutron_constants.DEVICE_OWNER_FLOATINGIP: + if instance_name and "{instance_name}" in cfg.hostname_pattern: + return cfg.hostname_pattern + if (port_owner + in ib_constants.NEUTRON_DEVICE_OWNER_TO_PATTERN_MAP.keys()): + return ib_constants.NEUTRON_DEVICE_OWNER_TO_PATTERN_MAP[port_owner] + else: + return cfg.hostname_pattern + + @staticmethod + def get_instancename(extattrs): + instance_name = None + if extattrs: + vm_attr = extattrs.get('VM Name') + if vm_attr: + instance_name = vm_attr.get('value') + return instance_name + + def _bind_names(self, context, backend_port, binding_func, extattrs=None): + all_dns_members = [] + + for ip in backend_port['fixed_ips']: + subnet = infoblox_db.get_subnet(context, ip['subnet_id']) + if subnet['ip_version'] == 4 or \ + not ipv6_utils.is_auto_address_subnet(subnet): + cfg = self.config_finder.find_config_for_subnet(context, + subnet) + dns_members = cfg.reserve_dns_members() + all_dns_members.extend(dns_members) + ip_addr = ip['ip_address'] + instance_name = self.get_instancename(extattrs) + + hostname_pattern = self.get_hostname_pattern( + backend_port, cfg, instance_name) + pattern_builder = self.pattern_builder( + hostname_pattern, cfg.domain_suffix_pattern) + fqdn = pattern_builder.build( + context, subnet, backend_port, ip_addr, instance_name) + + binding_func(cfg.network_view, cfg.dns_view, ip_addr, fqdn, + extattrs) + + for member in set(all_dns_members): + self.infoblox.restart_all_services(member) + + def bind_names(self, context, backend_port, disassociate=False): + if not backend_port['device_owner']: + return + # In the case of disassociating floatingip, we need to explicitly + # indicate to ignore instance id associated with the floating ip. + # This is because, at this point, the floating ip is still associated + # with instance in the neutron database. + extattrs = self.ea_manager.get_extattrs_for_ip( + context, backend_port, ignore_instance_id=disassociate) + try: + self._bind_names(context, backend_port, + self.ip_allocator.bind_names, extattrs) + except exceptions.InfobloxCannotCreateObject as e: + self.unbind_names(context, backend_port) + raise e + + def unbind_names(self, context, backend_port): + self._bind_names(context, backend_port, self.ip_allocator.unbind_names) + + def create_dns_zones(self, context, backend_subnet): + cfg = self.config_finder.find_config_for_subnet(context, + backend_subnet) + dns_members = cfg.reserve_dns_members() + + dns_zone = self.pattern_builder(cfg.domain_suffix_pattern).\ + build(context, backend_subnet) + zone_extattrs = self.ea_manager.get_extattrs_for_zone( + context, subnet=backend_subnet) + + # Add prefix only for classless networks (ipv4) + # mask greater than 24 needs prefix. + # use meaningful prefix if used + prefix = None + if backend_subnet['ip_version'] == 4: + m = re.search(r'/\d+', backend_subnet['cidr']) + mask = m.group().replace("/", "") + if int(mask) > 24: + if len(backend_subnet['name']) > 0: + prefix = backend_subnet['name'] + else: + prefix = '-'.join( + filter(None, + re.split(r'[.:/]', backend_subnet['cidr'])) + ) + + args = { + 'backend_subnet': backend_subnet, + 'dnsview_name': cfg.dns_view, + 'fqdn': dns_zone, + 'cidr': backend_subnet['cidr'], + 'prefix': prefix, + 'zone_format': 'IPV%s' % backend_subnet['ip_version'], + 'zone_extattrs': zone_extattrs, + 'obj_manip': self.infoblox + } + create_dns_zones_flow = linear_flow.Flow('create-dns-zones') + + if cfg.ns_group: + args['ns_group'] = cfg.ns_group + create_dns_zones_flow.add( + tasks.CreateDNSZonesFromNSGroupTask(), + tasks.CreateDNSZonesCidrFromNSGroupTask(), + ) + else: + args['dns_member'] = dns_members[0] + args['secondary_dns_members'] = dns_members[1:] + create_dns_zones_flow.add( + tasks.CreateDNSZonesTask(), + tasks.CreateDNSZonesTaskCidr()) + + context.store.update(args) + context.parent_flow.add(create_dns_zones_flow) + + def delete_dns_zones(self, context, backend_subnet): + cfg = self.config_finder.find_config_for_subnet(context, + backend_subnet) + dns_zone_fqdn = self.pattern_builder(cfg.domain_suffix_pattern).\ + build(context, backend_subnet) + dnsview_name = cfg.dns_view + + network = self._get_network(context, backend_subnet['network_id']) + is_external = infoblox_db.is_network_external(context, + network.get('id')) + is_shared = network.get('shared') + + # If config is global, do not delete dns zone for that subnet + # If subnet is for external or shared network, do not delete a zone + # for the subnet. + # If subnet is for private network (not external, shared, or global), + # check if domain suffix is unique to the subnet. + # if subnet name is part of the domain suffix pattern, then delete + # forward zone. + # if network name is part of the domain suffix pattern, then delete + # forward zone only if the subnet is only remaining subnet + # in the network. + # Reverse zone is deleted when not global, not external, and not shared + if neutron_conf.CONF.allow_admin_network_deletion or \ + not (cfg.is_global_config or is_external or is_shared): + if (('{subnet_name}' in cfg.domain_suffix_pattern or + '{subnet_id}' in cfg.domain_suffix_pattern) or + (('{network_name}' in cfg.domain_suffix_pattern or + '{network_id}' in cfg.domain_suffix_pattern) and + infoblox_db.is_last_subnet_in_network( + context, backend_subnet['id'], + backend_subnet['network_id'])) or + ('{tenant_id}' in cfg.domain_suffix_pattern and + infoblox_db.is_last_subnet_in_tenant( + context, backend_subnet['id'], + backend_subnet['tenant_id'])) or + (self._determine_static_zone_deletion( + context, backend_subnet, + cfg.is_static_domain_suffix))): + # delete forward zone + self.infoblox.delete_dns_zone(dnsview_name, dns_zone_fqdn) + + # delete reverse zone + self.infoblox.delete_dns_zone(dnsview_name, + backend_subnet['cidr']) + + def _determine_static_zone_deletion(self, context, + backend_subnet, is_static): + """ + Checking config if deletion is possible: + global tenant + x x n/a + o x cannot delete, global cannot be deleted + x o allow delete, only tenant should use + o o cannot delete, global cannot be deleted + If possible, then subnet must be the last one among all private + networks. + """ + if not is_static: + return False + + cfgs = self.config_finder.get_all_configs(context, backend_subnet) + for cfg in cfgs: + if cfg.is_global_config and cfg.is_static_domain_suffix: + return False + + return infoblox_db.is_last_subnet_in_private_networks( + context, backend_subnet['id']) + + +def has_nameservers(subnet): + try: + has_dns = iter(subnet['dns_nameservers']) is not None + except (TypeError, KeyError): + has_dns = False + + return has_dns + + +def get_nameservers(subnet): + if has_nameservers(subnet): + return subnet['dns_nameservers'] + return [] + + +def build_fqdn(prefix, zone, ip_address): + ip_address = ip_address.replace('.', '-') + if zone: + zone.lstrip('.') + return "%(prefix)s%(ip_address)s.%(zone)s" % { + 'prefix': prefix, + 'ip_address': ip_address, + 'zone': zone + } diff --git a/neutron/ipam/drivers/infoblox/ea_manager.py b/neutron/ipam/drivers/infoblox/ea_manager.py new file mode 100755 index 0000000..42fd79f --- /dev/null +++ b/neutron/ipam/drivers/infoblox/ea_manager.py @@ -0,0 +1,329 @@ +# Copyright 2014 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.config import cfg + +from neutron.db import db_base_plugin_v2 +from neutron.db import l3_db +from neutron.db import models_v2 +from neutron.ipam.drivers.infoblox import connector +from neutron.ipam.drivers.infoblox import constants as ib_constants +from neutron.ipam.drivers.infoblox import exceptions +from neutron.ipam.drivers.infoblox import l2_driver +from neutron.ipam.drivers.infoblox import nova_manager +from neutron.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + + +class InfobloxEaManager(object): + # CMP == cloud management platform + OPENSTACK_OBJECT_FLAG = 'CMP Type' + + def __init__(self, infoblox_db): + # Passing this thru constructor to avoid cyclic imports + self.db = infoblox_db + self._network_l2_info_provider = l2_driver.L2Info() + + def get_extattrs_for_nview(self, context): + """ + Generates EAs for Network View + :param context: current neutron context + :return: dict with extensible attributes ready to be sent as part of + NIOS WAPI + """ + os_tenant_id = context.tenant_id + + attributes = { + 'Tenant ID': os_tenant_id, + # OpenStack should not own entire network view, + # since shared or external networks may be created in it + 'Cloud API Owned': False, + } + return self._build_extattrs(attributes) + + def get_extattrs_for_network(self, context, subnet=None, network=None): + """ + Sets non-null values from subnet and network to corresponding EAs in + NIOS + :param context: current neutron context + :param subnet: neutron subnet object + :param network: neutron network object + :return: dict with extensible attributes ready to be sent as part of + NIOS WAPI + """ + if subnet is None: + subnet = {} + if network is None: + network = {} + + os_subnet_id = subnet.get('id') + os_subnet_name = subnet.get('name') + + os_network_id = network.get('id') + os_network_name = network.get('name') + os_network_l2_info = self._network_l2_info_provider.\ + get_network_l2_info(context.session, os_network_id) + os_network_type = os_network_l2_info.get('network_type').upper() + + os_segmentation_id = os_network_l2_info.get('segmentation_id') + os_physical_network = os_network_l2_info.get('physical_network') + os_tenant_id = (network.get('tenant_id') or + subnet.get('tenant_id') or + context.get('tenant_id')) + os_user_id = context.user_id + + attributes = { + 'Subnet ID': os_subnet_id, + 'Subnet Name': os_subnet_name, + 'Network ID': os_network_id, + 'Network Name': os_network_name, + 'Network Encap': os_network_type, + 'Segmentation ID': os_segmentation_id, + 'Physical Network Name': os_physical_network, + 'Tenant ID': os_tenant_id, + 'Account': os_user_id, + } + + # set clowd_api_owned, is_external, is_shared from common routine + common_ea = self._get_common_ea(context, subnet, network) + attributes.update(common_ea) + + return self._build_extattrs(attributes) + + def get_extattrs_for_range(self, context, network): + os_user_id = context.user_id + os_tenant_id = context.tenant_id + common_ea = self._get_common_ea(context, network=network) + + attributes = { + 'Tenant ID': os_tenant_id, + 'Account': os_user_id, + 'Cloud API Owned': common_ea['Cloud API Owned'], + } + return self._build_extattrs(attributes) + + def get_default_extattrs_for_ip(self, context): + attributes = { + 'Tenant ID': context.tenant_id, + 'Account': context.user_id, + 'Port ID': None, + 'Port Attached Device - Device Owner': None, + 'Port Attached Device - Device ID': None, + 'Cloud API Owned': True, + 'IP Type': 'Fixed', + 'VM ID': None, + 'VM Name': None + } + return self._build_extattrs(attributes) + + def get_extattrs_for_ip(self, context, port, ignore_instance_id=False): + # Fallback to 'None' as string since NIOS requires this value to be set + os_tenant_id = port.get('tenant_id') or context.tenant_id or 'None' + os_user_id = context.user_id + + neutron_internal_services_dev_owners = \ + ib_constants.NEUTRON_INTERNAL_SERVICE_DEVICE_OWNERS + + # for gateway ip, no instance id exists + os_instance_id = None + os_instance_name = None + + set_os_instance_id = ((not ignore_instance_id) and + (port['device_owner'] not in + neutron_internal_services_dev_owners)) + + if set_os_instance_id: + # for floating ip, no instance id exists + os_instance_id = self._get_instance_id(context, port) + if os_instance_id: + nm = nova_manager.NovaManager() + os_instance_name = nm.get_instance_name_by_id(os_instance_id) + + network = self.db.get_network(context, port['network_id']) + common_ea = self._get_common_ea(context, network=network) + + attributes = { + 'Tenant ID': os_tenant_id, + 'Account': os_user_id, + 'Port ID': port['id'], + 'Port Attached Device - Device Owner': port['device_owner'], + 'Port Attached Device - Device ID': port['device_id'], + 'Cloud API Owned': common_ea['Cloud API Owned'], + 'VM ID': os_instance_id, + 'VM Name': os_instance_name, + } + + if self.db.is_network_external(context, port['network_id']): + attributes['IP Type'] = 'Floating' + else: + attributes['IP Type'] = 'Fixed' + + return self._build_extattrs(attributes) + + def get_extattrs_for_zone(self, context, subnet=None, network=None): + os_user_id = context.user_id + os_tenant_id = context.tenant_id + common_ea = self._get_common_ea(context, subnet=subnet, network=None) + + attributes = { + 'Tenant ID': os_tenant_id, + 'Account': os_user_id, + 'Cloud API Owned': common_ea['Cloud API Owned'], + } + return self._build_extattrs(attributes) + + def _get_common_ea(self, context, subnet=None, network=None): + if hasattr(subnet, 'external'): + os_network_is_external = subnet.get('external') + elif network: + os_network_is_external = self.db.is_network_external( + context, network.get('id')) + else: + os_network_is_external = False + + if network: + os_network_is_shared = network.get('shared') + else: + os_network_is_shared = False + + os_cloud_owned = not (os_network_is_external or os_network_is_shared) + attributes = { + 'Is External': os_network_is_external, + 'Is Shared': os_network_is_shared, + 'Cloud API Owned': os_cloud_owned, + } + return attributes + + def _get_instance_id(self, context, port): + is_floatingip = port['device_owner'] == l3_db.DEVICE_OWNER_FLOATINGIP + + if is_floatingip: + os_instance_id = self.db.get_instance_id_by_floating_ip( + context, floating_ip_id=port['device_id']) + else: + os_instance_id = port['device_id'] + + return os_instance_id + + @staticmethod + def _to_str_or_none(value): + retval = None + if not isinstance(value, basestring): + if value is not None: + retval = str(value) + else: + retval = value + return retval + + def _build_extattrs(self, attributes): + extattrs = {} + for name, value in attributes.iteritems(): + str_val = self._to_str_or_none(value) + if str_val: + extattrs[name] = {'value': str_val} + + self.add_openstack_extattrs_marker(extattrs) + return extattrs + + @classmethod + def add_openstack_extattrs_marker(cls, extattrs): + extattrs[cls.OPENSTACK_OBJECT_FLAG] = {'value': 'OpenStack'} + + +def _construct_extattrs(filters): + extattrs = {} + for name, filter_value_list in filters.items(): + # Filters in Neutron look like a dict + # { + # 'filter1_name': ['filter1_value'], + # 'filter2_name': ['filter2_value'] + # } + # So we take only the first item from user's input which is + # filter_value_list here. + # Also not Infoblox filters must be removed from filters. + # Infoblox filters must be as following: + # neutron <command> --infoblox_ea:<EA_name> <EA_value> + infoblox_prefix = 'infoblox_ea:' + if name.startswith(infoblox_prefix) and filter_value_list: + # "infoblox-ea:" removed from filter name + prefix_len = len(infoblox_prefix) + attr_name = name[prefix_len:] + extattrs[attr_name] = {'value': filter_value_list[0]} + return extattrs + + +def _extattrs_result_filter_hook(query, filters, db_model, + os_object, ib_objtype, mapping_id): + """Result filter hook which filters Infoblox objects by + Extensible Attributes (EAs) and returns Query object containing + OpenStack objects which are equal to Infoblox ones. + """ + infoblox = connector.Infoblox() + infoblox_objects_ids = [] + extattrs = _construct_extattrs(filters) + + if extattrs: + InfobloxEaManager.add_openstack_extattrs_marker(extattrs) + infoblox_objects = infoblox.get_object( + ib_objtype, return_fields=['extattrs'], + extattrs=extattrs) + if infoblox_objects: + for infoblox_object in infoblox_objects: + try: + obj_id = infoblox_object['extattrs'][mapping_id]['value'] + except KeyError: + raise exceptions.NoAttributeInInfobloxObject( + os_object=os_object, ib_object=ib_objtype, + attribute=mapping_id) + infoblox_objects_ids.append(obj_id) + query = query.filter(db_model.id.in_(infoblox_objects_ids)) + return query + + +def subnet_extattrs_result_filter_hook(query, filters): + return _extattrs_result_filter_hook( + query, filters, models_v2.Subnet, 'subnet', 'network', 'Subnet ID') + + +def network_extattrs_result_filter_hook(query, filters): + return _extattrs_result_filter_hook( + query, filters, models_v2.Network, 'subnet', 'network', + 'Network ID') + + +def port_extattrs_result_filter_hook(query, filters): + if cfg.CONF.use_host_records_for_ip_allocation: + ib_objtype = 'record:host' + else: + ib_objtype = 'record:a' + return _extattrs_result_filter_hook( + query, filters, models_v2.Port, 'port', ib_objtype, 'Port ID') + + +if (cfg.CONF.ipam_driver == + 'neutron.ipam.drivers.infoblox.infoblox_ipam.InfobloxIPAM'): + db_base_plugin_v2.NeutronDbPluginV2.register_model_query_hook( + models_v2.Network, 'network_extattrs', None, None, + network_extattrs_result_filter_hook) + + db_base_plugin_v2.NeutronDbPluginV2.register_model_query_hook( + models_v2.Subnet, 'subnet_extattrs', None, None, + subnet_extattrs_result_filter_hook) + + db_base_plugin_v2.NeutronDbPluginV2.register_model_query_hook( + models_v2.Port, 'port_extattrs', None, None, + port_extattrs_result_filter_hook) diff --git a/neutron/ipam/drivers/infoblox/exceptions.py b/neutron/ipam/drivers/infoblox/exceptions.py new file mode 100755 index 0000000..abe1762 --- /dev/null +++ b/neutron/ipam/drivers/infoblox/exceptions.py @@ -0,0 +1,125 @@ +# Copyright 2014 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.common import exceptions + + +class InfobloxException(exceptions.NeutronException): + """Generic Infoblox Exception.""" + def __init__(self, response, **kwargs): + self.response = response + super(InfobloxException, self).__init__(**kwargs) + + +class InfobloxIsMisconfigured(exceptions.NeutronException): + message = _("Infoblox IPAM is misconfigured: infoblox_wapi, " + "infoblox_username and infoblox_password must be defined.") + + +class InfobloxSearchError(InfobloxException): + message = _("Cannot search '%(objtype)s' object(s): " + "%(content)s [code %(code)s]") + + +class InfobloxCannotCreateObject(InfobloxException): + message = _("Cannot create '%(objtype)s' object(s): " + "%(content)s [code %(code)s]") + + +class InfobloxCannotDeleteObject(InfobloxException): + message = _("Cannot delete object with ref %(ref)s: " + "%(content)s [code %(code)s]") + + +class InfobloxCannotUpdateObject(InfobloxException): + message = _("Cannot update object with ref %(ref)s: " + "%(content)s [code %(code)s]") + + +class InfobloxFuncException(InfobloxException): + message = _("Error occured during function's '%(func_name)s' call: " + "ref %(ref)s: %(content)s [code %(code)s]") + + +class InfobloxHostRecordIpAddrNotCreated(exceptions.NeutronException): + message = _("Infoblox host record ipv4addr/ipv6addr has not been " + "created for IP %(ip)s, mac %(mac)s") + + + +class InfobloxCannotAllocateIpForSubnet(exceptions.NeutronException): + message = _("Infoblox Network view %(netview)s, Network %(cidr)s " + "does not have IPs available for allocation.") + + +class InfobloxCannotAllocateIp(exceptions.NeutronException): + message = _("Cannot allocate IP %(ip_data)s") + + +class InfobloxDidNotReturnCreatedIPBack(exceptions.NeutronException): + message = _("Infoblox did not return created IP back") + + +class InfobloxNetworkNotAvailable(exceptions.NeutronException): + message = _("No network view %(net_view_name)s for %(cidr)s") + + +class InfobloxObjectParsingError(exceptions.NeutronException): + message = _("Infoblox object cannot be parsed from dict: %(data)s") + + +class HostRecordNotPresent(InfobloxObjectParsingError): + message = _("Cannot parse Host Record object from dict because " + "'ipv4addrs'/'ipv6addrs' is absent.") + + +class InfobloxInvalidIp(InfobloxObjectParsingError): + message = _("Bad IP address: %(ip)s") + + +class NoAttributeInInfobloxObject(exceptions.NeutronException): + message = _("To find OpenStack %(os_object)s for Infoblox %(ib_object)s, " + "%(attribute)s must be in extensible attributes.") + + +class OperationNotAllowed(exceptions.NeutronException): + message = _("Requested operation is not allowed: %(reason)s") + + +class InfobloxConnectionError(exceptions.NeutronException): + message = _("Infoblox HTTP request failed with: %(reason)s") + + +class InfobloxConfigException(exceptions.NeutronException): + """Generic Infoblox Config Exception.""" + message = _("Config error: %(msg)s") + + +class InfobloxInternalPrivateSubnetAlreadyExist(exceptions.Conflict): + message = _("Network with the same CIDR already exists on NIOS.") + + +class InfobloxNetworkTypeNotAllowed(InfobloxException): + message = _("Network with network_type '%(network_type)s' " + "not allowed by NIOS.") + + +class InfobloxBadWAPICredential(InfobloxException): + message = _("Infoblox IPAM is misconfigured: " + "infoblox_username and infoblox_password are incorrect.") + + +class InfobloxTimeoutError(InfobloxException): + message = _("Connection to NIOS timed out") diff --git a/neutron/ipam/drivers/infoblox/infoblox_ipam.py b/neutron/ipam/drivers/infoblox/infoblox_ipam.py new file mode 100755 index 0000000..e0f6456 --- /dev/null +++ b/neutron/ipam/drivers/infoblox/infoblox_ipam.py @@ -0,0 +1,92 @@ +# Copyright 2014 OpenStack LLC. +# 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 taskflow.engines +from taskflow.patterns import linear_flow + +from neutron.db.infoblox import models +from neutron.ipam.drivers.infoblox import config +from neutron.ipam.drivers.infoblox import connector +from neutron.ipam.drivers.infoblox import dns_controller +from neutron.ipam.drivers.infoblox import ip_allocator +from neutron.ipam.drivers.infoblox import ipam_controller +from neutron.ipam.drivers.infoblox import object_manipulator +from neutron.ipam.drivers import neutron_ipam + + +class FlowContext(object): + def __init__(self, neutron_context, flow_name): + self.parent_flow = linear_flow.Flow(flow_name) + self.context = neutron_context + self.store = {} + + def __getattr__(self, item): + return getattr(self.context, item) + + +class InfobloxIPAM(neutron_ipam.NeutronIPAM): + def __init__(self): + super(InfobloxIPAM, self).__init__() + + config_finder = config.ConfigFinder() + obj_manipulator = object_manipulator.InfobloxObjectManipulator( + connector=connector.Infoblox()) + ip_alloc = ip_allocator.get_ip_allocator(obj_manipulator) + + self.ipam_controller = ipam_controller.InfobloxIPAMController( + config_finder=config_finder, + obj_manip=obj_manipulator, + ip_allocator=ip_alloc) + + self.dns_controller = dns_controller.InfobloxDNSController( + config_finder=config_finder, + manipulator=obj_manipulator, + ip_allocator=ip_alloc + ) + + def create_subnet(self, context, subnet): + context = FlowContext(context, 'create-subnet') + context.store['subnet'] = subnet + retval = super(InfobloxIPAM, self).create_subnet(context, subnet) + taskflow.engines.run(context.parent_flow, store=context.store) + return retval + + def _collect_members_ips(self, context, network_id, model): + members = context.session.query(model) + result = members.filter_by(network_id=network_id) + ip_list = [] + ipv6_list = [] + for member in result: + ip_list.append(member.server_ip) + ipv6_list.append(member.server_ipv6) + return (ip_list, ipv6_list) + + def get_additional_network_dict_params(self, ctx, network_id): + dns_list, dns_ipv6_list = self._collect_members_ips( + ctx, network_id, models.InfobloxDNSMember) + + dhcp_list, dhcp_ipv6_list = self._collect_members_ips( + ctx, network_id, models.InfobloxDHCPMember) + + ib_mgmt_ip = self.ipam_controller.ib_db.get_management_net_ip( + ctx, network_id) + + return { + 'external_dhcp_servers': dhcp_list, + 'external_dns_servers': dns_list, + 'external_dhcp_ipv6_servers': dhcp_ipv6_list, + 'external_dns_ipv6_servers': dns_ipv6_list, + 'mgmt_iface_ip': ib_mgmt_ip + } diff --git a/neutron/ipam/drivers/infoblox/ip_allocator.py b/neutron/ipam/drivers/infoblox/ip_allocator.py new file mode 100755 index 0000000..13178ff --- /dev/null +++ b/neutron/ipam/drivers/infoblox/ip_allocator.py @@ -0,0 +1,180 @@ +# Copyright 2014 OpenStack LLC. +# 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 abc + +from oslo.config import cfg +from neutron.openstack.common import log as logging +from neutron.common import constants as neutron_constants +import six + + +OPTS = [ + cfg.ListOpt('bind_dns_records_to_fixed_address', + default=[], + help=_("List of DNS records to bind to " + "Fixed Address during create_port")), + cfg.ListOpt('unbind_dns_records_from_fixed_address', + default=[], + help=_("List of DNS records to unbind from " + "Fixed Address during delete_port. " + "This is typically the same list as " + "that for " + "bind_resource_records_to_fixedaddress")), + cfg.ListOpt('delete_dns_records_associated_with_fixed_address', + default=[], + help=_("List of associated DNS records to delete " + "when a Fixed Address is deleted. This is " + "typically a list of DNS records created " + "independent of the Infoblox Openstack " + "Adaptor (IOA)")) +] + +cfg.CONF.register_opts(OPTS) +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class IPAllocator(object): + + def __init__(self, infoblox): + self.infoblox = infoblox + + @abc.abstractmethod + def allocate_ip_from_range(self, dnsview_name, networkview_name, zone_auth, + hostname, mac, first_ip, last_ip, + extattrs=None): + pass + + @abc.abstractmethod + def allocate_given_ip(self, netview_name, dnsview_name, zone_auth, + hostname, mac, ip, extattrs=None): + pass + + @abc.abstractmethod + def deallocate_ip(self, network_view, dns_view_name, ip): + pass + + @abc.abstractmethod + def bind_names(self, netview_name, dnsview_name, ip, name, extattrs): + pass + + @abc.abstractmethod + def unbind_names(self, netview_name, dnsview_name, ip, name, extattrs): + pass + + +class HostRecordIPAllocator(IPAllocator): + def bind_names(self, netview_name, dnsview_name, ip, name, extattrs): + # See OPENSTACK-181. In case hostname already exists on NIOS, update + # host record which contains that hostname with the new IP address + # rather than creating a separate host record object + reserved_hostname_hr = self.infoblox.find_hostname(dnsview_name, + name, ip) + reserved_ip_hr = self.infoblox.get_host_record(dnsview_name, ip) + + if reserved_hostname_hr == reserved_ip_hr: + return + if reserved_hostname_hr: + for hr_ip in reserved_ip_hr.ips: + if hr_ip == ip: + self.infoblox.delete_host_record(dnsview_name, ip) + self.infoblox.add_ip_to_record(reserved_hostname_hr, + ip, + hr_ip.mac) + break + else: + self.infoblox.bind_name_with_host_record(dnsview_name, ip, + name, extattrs) + + def unbind_names(self, netview_name, dnsview_name, ip, name, extattrs): + # Nothing to delete, all will be deleted together with host record. + pass + + def allocate_ip_from_range(self, dnsview_name, networkview_name, + zone_auth, hostname, mac, first_ip, last_ip, + extattrs=None): + fqdn = hostname + '.' + zone_auth + host_record = self.infoblox.find_hostname(dnsview_name, fqdn, + first_ip) + if host_record: + hr = self.infoblox.add_ip_to_host_record_from_range( + host_record, networkview_name, mac, first_ip, last_ip) + else: + hr = self.infoblox.create_host_record_from_range( + dnsview_name, networkview_name, zone_auth, hostname, mac, + first_ip, last_ip, extattrs) + return hr.ips[-1].ip + + def allocate_given_ip(self, netview_name, dnsview_name, zone_auth, + hostname, mac, ip, extattrs=None): + hr = self.infoblox.create_host_record_for_given_ip( + dnsview_name, zone_auth, hostname, mac, ip, extattrs) + return hr.ips[-1].ip + + def deallocate_ip(self, network_view, dns_view_name, ip): + host_record = self.infoblox.get_host_record(dns_view_name, ip) + + if host_record and len(host_record.ips) > 1: + self.infoblox.delete_ip_from_host_record(host_record, ip) + else: + self.infoblox.delete_host_record(dns_view_name, ip) + + +class FixedAddressIPAllocator(IPAllocator): + def bind_names(self, netview_name, dnsview_name, ip, name, extattrs): + bind_cfg = cfg.CONF.bind_dns_records_to_fixed_address + if extattrs.get('Port Attached Device - Device Owner').\ + get('value') == neutron_constants.DEVICE_OWNER_FLOATINGIP: + self.infoblox.update_fixed_address_eas(netview_name, ip, + extattrs) + self.infoblox.update_dns_record_eas(dnsview_name, ip, + extattrs) + if bind_cfg: + self.infoblox.bind_name_with_record_a( + dnsview_name, ip, name, bind_cfg, extattrs) + + def unbind_names(self, netview_name, dnsview_name, ip, name, extattrs): + unbind_cfg = cfg.CONF.unbind_dns_records_from_fixed_address + if unbind_cfg: + self.infoblox.unbind_name_from_record_a( + dnsview_name, ip, name, unbind_cfg) + + def allocate_ip_from_range(self, dnsview_name, networkview_name, + zone_auth, hostname, mac, first_ip, last_ip, + extattrs=None): + fa = self.infoblox.create_fixed_address_from_range( + networkview_name, mac, first_ip, last_ip, extattrs) + return fa.ip + + def allocate_given_ip(self, netview_name, dnsview_name, zone_auth, + hostname, mac, ip, extattrs=None): + fa = self.infoblox.create_fixed_address_for_given_ip( + netview_name, mac, ip, extattrs) + return fa.ip + + def deallocate_ip(self, network_view, dns_view_name, ip): + delete_cfg = cfg.CONF.delete_dns_records_associated_with_fixed_address + if delete_cfg: + self.infoblox.delete_all_associated_objects( + network_view, ip, delete_cfg) + self.infoblox.delete_fixed_address(network_view, ip) + + +def get_ip_allocator(obj_manipulator): + if cfg.CONF.use_host_records_for_ip_allocation: + return HostRecordIPAllocator(obj_manipulator) + else: + return FixedAddressIPAllocator(obj_manipulator) diff --git a/neutron/ipam/drivers/infoblox/ipam_controller.py b/neutron/ipam/drivers/infoblox/ipam_controller.py new file mode 100755 index 0000000..4ac87a2 --- /dev/null +++ b/neutron/ipam/drivers/infoblox/ipam_controller.py @@ -0,0 +1,420 @@ +# Copyright 2014 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.config import cfg as neutron_conf +from taskflow.patterns import linear_flow + +from neutron.api.v2 import attributes +from neutron.db.infoblox import infoblox_db +from neutron.db.infoblox import models +from neutron.ipam.drivers.infoblox import config +from neutron.ipam.drivers.infoblox import dns_controller +from neutron.ipam.drivers.infoblox import ea_manager +from neutron.ipam.drivers.infoblox import exceptions +from neutron.ipam.drivers.infoblox import tasks +from neutron.ipam.drivers import neutron_db +from neutron.ipam.drivers import neutron_ipam +from neutron.openstack.common import log as logging +from neutron.openstack.common import uuidutils + + +OPTS = [ + neutron_conf.BoolOpt('use_host_records_for_ip_allocation', + default=True, + help=_("Use host records for IP allocation. " + "If False then Fixed IP + A + PTR record " + "are used.")), + neutron_conf.StrOpt('dhcp_relay_management_network_view', + default="default", + help=_("NIOS network view to be used for DHCP inside " + "management network")), + neutron_conf.StrOpt('dhcp_relay_management_network', + default=None, + help=_("CIDR for the management network served by " + "Infoblox DHCP member")), + neutron_conf.BoolOpt('allow_admin_network_deletion', + default=False, + help=_("Allow admin network which is global, " + "external, or shared to be deleted")) +] + +neutron_conf.CONF.register_opts(OPTS) +LOG = logging.getLogger(__name__) + + +class InfobloxIPAMController(neutron_ipam.NeutronIPAMController): + def __init__(self, obj_manip=None, config_finder=None, ip_allocator=None, + extattr_manager=None, ib_db=None, db_mgr=None): + """IPAM backend implementation for Infoblox.""" + self.infoblox = obj_manip + self.config_finder = config_finder + self.ip_allocator = ip_allocator + self.pattern_builder = config.PatternBuilder + + if extattr_manager is None: + extattr_manager = ea_manager.InfobloxEaManager(infoblox_db) + + if db_mgr is None: + db_mgr = neutron_db + + self.db_manager = db_mgr + self.ea_manager = extattr_manager + + if ib_db is None: + ib_db = infoblox_db + + self.ib_db = ib_db + + def create_subnet(self, context, s): + subnet = super(InfobloxIPAMController, self).create_subnet(context, s) + + cfg = self.config_finder.find_config_for_subnet(context, subnet) + dhcp_members = cfg.reserve_dhcp_members() + dns_members = cfg.reserve_dns_members() + + network = self._get_network(context, subnet['network_id']) + create_infoblox_member = True + + create_subnet_flow = linear_flow.Flow('ib_create_subnet') + + if self.infoblox.network_exists(cfg.network_view, subnet['cidr']): + create_subnet_flow.add(tasks.ChainInfobloxNetworkTask()) + create_infoblox_member = False + + if not infoblox_db.get_network_view(context, subnet['network_id']): + infoblox_db.set_network_view(context, cfg.network_view, + subnet['network_id']) + + # Neutron will sort this later so make sure infoblox copy is + # sorted too. + user_nameservers = sorted(dns_controller.get_nameservers(s)) + + # For flat network we save member IP as a primary DNS server: to + # the beginning of the list. + # If this net is not flat, Member IP will later be replaced by + # DNS relay IP. + nameservers = [item.ipv6 if subnet['ip_version'] == 6 + else item.ip for item in dns_members] + + nameservers += [n for n in user_nameservers if n not in nameservers] + + nview_extattrs = self.ea_manager.get_extattrs_for_nview(context) + network_extattrs = self.ea_manager.get_extattrs_for_network( + context, subnet, network) + range_extattrs = self.ea_manager.get_extattrs_for_range( + context, network) + method_arguments = { + 'obj_manip': self.infoblox, + 'net_view_name': cfg.network_view, + 'dns_view_name': cfg.dns_view, + 'cidr': subnet['cidr'], + 'dhcp_member': dhcp_members, + 'gateway_ip': subnet['gateway_ip'], + 'disable': True, + 'nameservers': nameservers, + 'range_extattrs': range_extattrs, + 'network_extattrs': network_extattrs, + 'nview_extattrs': nview_extattrs, + 'related_members': set(cfg.dhcp_members + cfg.dns_members), + 'dhcp_trel_ip': infoblox_db.get_management_net_ip( + context, subnet['network_id']), + 'ip_version': subnet['ip_version'] + } + + if subnet['ip_version'] == 6 and subnet['enable_dhcp']: + if attributes.is_attr_set(subnet.get('ipv6_ra_mode')): + method_arguments['ipv6_ra_mode'] = subnet['ipv6_ra_mode'] + if attributes.is_attr_set(subnet.get('ipv6_address_mode')): + method_arguments[ + 'ipv6_address_mode'] = subnet['ipv6_address_mode'] + + if cfg.require_dhcp_relay: + for member in dhcp_members: + dhcp_member = models.InfobloxDHCPMember( + server_ip=member.ip, + server_ipv6=member.ipv6, + network_id=network.id + ) + context.session.add(dhcp_member) + + for member in dns_members: + dns_member = models.InfobloxDNSMember( + server_ip=member.ip, + server_ipv6=member.ipv6, + network_id=network.id + ) + context.session.add(dns_member) + + if cfg.requires_net_view(): + create_subnet_flow.add(tasks.CreateNetViewTask()) + + if cfg.network_template: + method_arguments['template'] = cfg.network_template + create_subnet_flow.add(tasks.CreateNetworkFromTemplateTask()) + elif create_infoblox_member: + create_subnet_flow.add(tasks.CreateNetworkTask()) + + create_subnet_flow.add(tasks.CreateDNSViewTask()) + + for ip_range in subnet['allocation_pools']: + # context.store is a global dict of method arguments for tasks + # in current flow, hence method arguments need to be rebound + first = ip_range['start'] + last = ip_range['end'] + first_ip_arg = 'ip_range %s' % first + last_ip_arg = 'ip_range %s' % last + method_arguments[first_ip_arg] = first + method_arguments[last_ip_arg] = last + task_name = '-'.join([first, last]) + + create_subnet_flow.add( + tasks.CreateIPRange(name=task_name, + rebind={'start_ip': first_ip_arg, + 'end_ip': last_ip_arg})) + + context.store.update(method_arguments) + context.parent_flow.add(create_subnet_flow) + + return subnet + + def update_subnet(self, context, subnet_id, subnet): + backend_subnet = self.get_subnet_by_id(context, subnet_id) + cfg = self.config_finder.find_config_for_subnet(context, + backend_subnet) + cfg.verify_subnet_update_is_allowed(subnet) + + ib_network = self.infoblox.get_network(cfg.network_view, + subnet['cidr']) + + user_nameservers = sorted(subnet.get('dns_nameservers', [])) + updated_nameservers = user_nameservers + if (ib_network.member_ip_addrs and + ib_network.member_ip_addrs[0] in ib_network.dns_nameservers): + # Flat network, primary dns is member_ip + primary_dns = ib_network.member_ip_addrs[0] + updated_nameservers = [primary_dns] + user_nameservers + else: + # Network with relays, primary dns is relay_ip + primary_dns = self.ib_db.get_subnet_dhcp_port_address( + context, subnet['id']) + if primary_dns: + updated_nameservers = [primary_dns] + user_nameservers + + ib_network.dns_nameservers = updated_nameservers + + network = self._get_network(context, subnet['network_id']) + eas = self.ea_manager.get_extattrs_for_network(context, subnet, + network) + self.infoblox.update_network_options(ib_network, eas) + + self.restart_services(context, subnet=subnet) + return backend_subnet + + def delete_subnet(self, context, subnet): + deleted_subnet = super(InfobloxIPAMController, self).delete_subnet( + context, subnet) + + cfg = self.config_finder.find_config_for_subnet(context, subnet) + network = self._get_network(context, subnet['network_id']) + members_to_restart = list(set(cfg.dhcp_members + cfg.dns_members)) + is_shared = network.get('shared') + is_external = infoblox_db.is_network_external(context, + subnet['network_id']) + + if neutron_conf.CONF.allow_admin_network_deletion or \ + not (cfg.is_global_config or is_shared or is_external): + self.infoblox.delete_network( + cfg.network_view, cidr=subnet['cidr']) + + if self._determine_member_deletion(context, + cfg.network_view_scope, + subnet['id'], + subnet['network_id'], + subnet['tenant_id']): + cfg.release_member(cfg.network_view) + + if cfg.require_dhcp_relay and \ + self.ib_db.is_last_subnet_in_network(context, subnet['id'], + subnet['network_id']): + member = context.session.query(models.InfobloxDNSMember) + member.filter_by(network_id=network.id).delete() + + member = context.session.query(models.InfobloxDHCPMember) + member.filter_by(network_id=network.id).delete() + + preconf_dns_view = cfg._dns_view + if (preconf_dns_view and not preconf_dns_view.startswith('default') + and not self.infoblox.has_dns_zones(preconf_dns_view)): + self.infoblox.delete_dns_view(preconf_dns_view) + + self.restart_services(context, members=members_to_restart) + return deleted_subnet + + def _determine_member_deletion(self, context, network_view_scope, + subnet_id, network_id, tenant_id): + if network_view_scope == 'static': + return self.ib_db.is_last_subnet(context, subnet_id) + if network_view_scope == 'tenant_id': + return self.ib_db.is_last_subnet_in_tenant(context, + subnet_id, + tenant_id) + if network_view_scope == 'network_id': + return self.ib_db.is_last_subnet_in_network(context, + subnet_id, + network_id) + # In order to use network_name scope, a network name must be unique. + # Openstack does not enforce this so user has to make sure that + # each network name is unique when {network_name} pattern is used + # for network view name. Then this is the same as network_id scope. + if network_view_scope == 'network_name': + return self.ib_db.is_last_subnet_in_network(context, + subnet_id, + network_id) + + def allocate_ip(self, context, subnet, port, ip=None): + hostname = uuidutils.generate_uuid() + mac = port['mac_address'] + extattrs = self.ea_manager.get_extattrs_for_ip(context, port) + + LOG.debug("Trying to allocate IP for %s on Infoblox NIOS" % hostname) + + cfg = self.config_finder.find_config_for_subnet(context, subnet) + + networkview_name = cfg.network_view + dnsview_name = cfg.dns_view + zone_auth = self.pattern_builder(cfg.domain_suffix_pattern).build( + context, subnet) + + if ip and ip.get('ip_address', None): + subnet_id = ip.get('subnet_id', None) + ip_to_be_allocated = ip.get('ip_address', None) + allocated_ip = self.ip_allocator.allocate_given_ip( + networkview_name, dnsview_name, zone_auth, hostname, mac, + ip_to_be_allocated, extattrs) + allocated_ip = {'subnet_id': subnet_id, + 'ip_address': allocated_ip} + else: + # Allocate next available considering IP ranges. + ip_ranges = subnet['allocation_pools'] + # Let Infoblox try to allocate an IP from each ip_range + # consistently, and break on the first successful allocation. + for ip_range in ip_ranges: + first_ip = ip_range['first_ip'] + last_ip = ip_range['last_ip'] + try: + allocated_ip = self.ip_allocator.allocate_ip_from_range( + dnsview_name, networkview_name, zone_auth, hostname, + mac, first_ip, last_ip, extattrs) + allocated_ip = {'subnet_id': subnet['id'], + 'ip_address': allocated_ip} + + break + except exceptions.InfobloxCannotAllocateIp: + LOG.debug("Failed to allocate IP from range (%s-%s)." % + (first_ip, last_ip)) + continue + else: + # We went through all the ranges and Infoblox did not + # allocated any IP. + LOG.debug("Network %s does not have IPs " + "available for allocation." % subnet['cidr']) + return None + + LOG.debug('IP address allocated on Infoblox NIOS: %s', allocated_ip) + + for member in set(cfg.dhcp_members): + self.infoblox.restart_all_services(member) + + return allocated_ip + + def deallocate_ip(self, context, subnet, port, ip): + cfg = self.config_finder.find_config_for_subnet(context, subnet) + net_id = subnet['network_id'] + self.ip_allocator.deallocate_ip(cfg.network_view, cfg.dns_view, ip) + self.ib_db.delete_ip_allocation(context, net_id, subnet, ip) + + for member in set(cfg.dhcp_members): + self.infoblox.restart_all_services(member) + + def set_dns_nameservers(self, context, port): + # Replace member IP in DNS nameservers by DNS relay IP. + for ip in port['fixed_ips']: + subnet = self._get_subnet(context, ip['subnet_id']) + cfg = self.config_finder.find_config_for_subnet(context, subnet) + net = self.infoblox.get_network(cfg.network_view, subnet['cidr']) + if not net.members: + continue + if not net.has_dns_members(): + LOG.debug("No domain-name-servers option found, it will" + "not be updated to the private IPAM relay IP.") + continue + net.update_member_ip_in_dns_nameservers(ip['ip_address']) + self.infoblox.update_network_options(net) + + def create_network(self, context, network): + if not neutron_conf.CONF.dhcp_relay_management_network: + LOG.info(_('dhcp_relay_management_network option is not set in ' + 'config. DHCP will be used for management network ' + 'interface.')) + return network + + net_view_name = neutron_conf.CONF.dhcp_relay_management_network_view + cidr = neutron_conf.CONF.dhcp_relay_management_network + mac = ':'.join(['00'] * 6) + + # Note(pbondar): If IP is allocated for dhcp relay (trel interface) + # when dhcp relay management network is set, + # OpenStack is unware of this so no port to associate with. + # In this case, we still need to populate EAs with default values. + ip_extattrs = self.ea_manager.get_default_extattrs_for_ip(context) + created_fixed_address = self.infoblox.create_fixed_address_from_cidr( + net_view_name, mac, cidr, ip_extattrs) + + self.ib_db.add_management_ip(context, + network['id'], + created_fixed_address) + return network + + def delete_network(self, context, network_id): + subnets = self.db_manager.get_subnets_by_network(context, network_id) + net_view = infoblox_db.get_network_view(context, network_id) + + for subnet in subnets: + LOG.info('Removing subnet %s from network %s.' % ( + subnet.id, network_id + )) + self.delete_subnet(context, subnet) + + if net_view and not self.infoblox.has_networks(net_view): + self.infoblox.delete_network_view(net_view) + + fixed_address_ref = self.ib_db.get_management_ip_ref(context, + network_id) + + if fixed_address_ref is not None: + self.infoblox.delete_object_by_ref(fixed_address_ref) + self.ib_db.delete_management_ip(context, network_id) + + def restart_services(self, context, members=None, subnet=None): + if not members: + members = [] + + if subnet: + cfg = self.config_finder.find_config_for_subnet(context, subnet) + for member in set(cfg.dhcp_members + cfg.dns_members): + members.append(member) + + for member in set(members): + self.infoblox.restart_all_services(member) diff --git a/neutron/ipam/drivers/infoblox/l2_driver.py b/neutron/ipam/drivers/infoblox/l2_driver.py new file mode 100644 index 0000000..83ab633 --- /dev/null +++ b/neutron/ipam/drivers/infoblox/l2_driver.py @@ -0,0 +1,140 @@ +# Copyright 2014 OpenStack LLC. +# 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 abc +import re +import six + +from oslo.config import cfg + +from neutron.openstack.common import importutils +from neutron.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class L2DriverBase(object): + """Defines interface for retreiving info from L2 pluings. + L2 Driver should: + - be located under 'l2_drivers' directory; + - have name {core_plugin_name}.py; + - have class name 'Driver'; + - be inherited from this class; + - implement methods which are marked as abstractmethods; + """ + @abc.abstractmethod + def init_driver(self): + """Should be implemented in driver for L2 plugin. + This method should import needed L2 plugin and + store reference to it somewhere in self. + So any import exception should be raised at this point. + """ + pass + + @abc.abstractmethod + def get_network_binding(self, session, network_id): + """Should be implemented in driver for L2 plugin. + :param session: database session object + :param network_id: network id + """ + pass + + def __init__(self): + """No need to override this in child. + Just inits L2 modules for now. + """ + self.init_driver() + + +class L2Info(object): + """This class provides network info from L2 plugins + using factory of facades. + """ + def __init__(self, core_plugin=None): + """ + :param: core_plugin: OpenStack core plugin + will be loaded from config if not provided + """ + if not core_plugin: + core_plugin = cfg.CONF.core_plugin + self.core_plugin = core_plugin + self.driver = None + + def _get_driver(self): + """Return Driver for L2 plugin. + Loads appropriate module if not loaded yet. + """ + if not self.driver: + self.driver = L2DriverFactory.load(self.core_plugin) + return self.driver + + def get_network_l2_info(self, session, network_id): + """Decorator/wrapper method for get_network_binding() + Converts network info from L2Driver(list format) into + dict with fixed keys. + :param session: database session object + :param network_id: network id + """ + segments = None + l2_info = {'network_type': None, + 'segmentation_id': None, + 'physical_network': None} + + driver = self._get_driver() + segments = driver.get_network_binding(session, network_id) + + if segments: + for name, value in segments.iteritems(): + l2_info[name] = value + + return l2_info + + +class L2DriverFactory(object): + """This class loads Driver for L2 plugin + depending on L2 core_plugin class name. + """ + + @classmethod + def load(cls, core_plugin): + """Loads driver for core_plugin + """ + driver_prefix = 'neutron.ipam.drivers.infoblox.l2_drivers.' + driver_postfix = '.Driver' + # Look for infoblox driver for core plugin + plugin_name = cls.get_plugin_name(core_plugin) + driver = driver_prefix + plugin_name + driver_postfix + LOG.info(_("Loading driver %s for core plugin"), driver) + # Try to load driver, generates exception if fails + driver_class = importutils.import_class(driver) + return driver_class() + + @classmethod + def get_plugin_name(cls, core_plugin): + """Returns plugin name based on plugin path. + Plugin name can be found on position number three in path + neutron.plugins.{plugin_name}.(path_to_module) + """ + plugin = str(core_plugin) + match = re.match(r'^neutron\.plugins\.([a-zA-Z0-9_]+)\.', + plugin) + if match: + return match.group(1) + + # if plugin doesn't match, assume core_plugin is short name + # instead of full path to module. + # See examples of full path / short name in setup.cfg + return plugin diff --git a/neutron/ipam/drivers/infoblox/l2_drivers/__init__.py b/neutron/ipam/drivers/infoblox/l2_drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron/ipam/drivers/infoblox/l2_drivers/ml2.py b/neutron/ipam/drivers/infoblox/l2_drivers/ml2.py new file mode 100644 index 0000000..51e5f36 --- /dev/null +++ b/neutron/ipam/drivers/infoblox/l2_drivers/ml2.py @@ -0,0 +1,27 @@ +# Copyright 2014 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.ipam.drivers.infoblox import l2_driver + + +class Driver(l2_driver.L2DriverBase): + def init_driver(self): + from neutron.plugins.ml2 import db + self.db = db + + def get_network_binding(self, session, network_id): + # get_network_segments returns array of arrays, + # and we need only the first array + return self.db.get_network_segments(session, network_id)[0] diff --git a/neutron/ipam/drivers/infoblox/l2_drivers/nuage.py b/neutron/ipam/drivers/infoblox/l2_drivers/nuage.py new file mode 100644 index 0000000..14095d5 --- /dev/null +++ b/neutron/ipam/drivers/infoblox/l2_drivers/nuage.py @@ -0,0 +1,29 @@ +# Copyright 2014 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.ipam.drivers.infoblox import l2_driver + + +class Driver(l2_driver.L2DriverBase): + def init_driver(self): + pass + + def get_network_binding(self, session, network_id): + # get_network_segments returns array of arrays, + # and we need only the first array + return {'network_type': 'none', + 'segmentation_id': 'none', + 'physical_network': 'none'} + diff --git a/neutron/ipam/drivers/infoblox/nova_manager.py b/neutron/ipam/drivers/infoblox/nova_manager.py new file mode 100644 index 0000000..5a98e18 --- /dev/null +++ b/neutron/ipam/drivers/infoblox/nova_manager.py @@ -0,0 +1,47 @@ +# Copyright 2014 Infoblox. +# 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 novaclient.v1_1.client as nclient +from novaclient import exceptions as novaexc +from oslo.config import cfg + +import logging + +LOG = logging.getLogger(__name__) + + +class NovaManager(object): + _nova_client = None + + def __init__(self): + if not NovaManager._nova_client: + NovaManager._nova_client = nclient.Client( + cfg.CONF.nova_admin_username, + cfg.CONF.nova_admin_password, + None, # project_id - not actually used + auth_url=cfg.CONF.nova_admin_auth_url, + tenant_id=cfg.CONF.nova_admin_tenant_id, + service_type='compute') + self.nova = NovaManager._nova_client + + def get_instance_name_by_id(self, instance_id): + try: + instance = self.nova.servers.get(instance_id) + if instance.human_id: + return instance.human_id + except (novaexc.NotFound, novaexc.BadRequest): + LOG.debug(_("Instance not found: %{instance_id}s"), + {'instance_id': instance_id}) + return instance_id diff --git a/neutron/ipam/drivers/infoblox/object_manipulator.py b/neutron/ipam/drivers/infoblox/object_manipulator.py new file mode 100755 index 0000000..77122fc --- /dev/null +++ b/neutron/ipam/drivers/infoblox/object_manipulator.py @@ -0,0 +1,883 @@ +# Copyright 2014 OpenStack LLC. +# 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 netaddr + +from neutron.ipam.drivers.infoblox import exceptions as exc +from neutron.ipam.drivers.infoblox import objects +from neutron.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + +class IPBackend(): + def __init__(self, object_manipulator): + self.obj_man = object_manipulator + + def get_network(self, net_view_name, cidr): + net_data = {'network_view': net_view_name, + 'network': cidr} + net = self.obj_man._get_infoblox_object_or_none( + self.ib_network_name, net_data, + return_fields=['options', 'members']) + if not net: + raise exc.InfobloxNetworkNotAvailable( + net_view_name=net_view_name, cidr=cidr) + return objects.Network.from_dict(net) + + def get_all_associated_objects(self, net_view_name, ip): + assoc_with_ip = { + 'network_view': net_view_name, + 'ip_address': ip + } + assoc_objects = self.obj_man._get_infoblox_object_or_none( + self.ib_ipaddr_object_name, assoc_with_ip, + return_fields=['objects'], proxy=True) + if assoc_objects: + return assoc_objects['objects'] + return [] + + def network_exists(self, net_view_name, cidr): + net_data = {'network_view': net_view_name, 'network': cidr} + try: + net = self.obj_man._get_infoblox_object_or_none( + self.ib_network_name, net_data, + return_fields=['options', 'members']) + except exc.InfobloxSearchError: + net = None + return net is not None + + def delete_network(self, net_view_name, cidr): + payload = {'network_view': net_view_name, + 'network': cidr} + self.obj_man._delete_infoblox_object( + self.ib_network_name, payload) + + def delete_ip_range(self, net_view, start_ip, end_ip): + range_data = {'start_addr': start_ip, + 'end_addr': end_ip, + 'network_view': net_view} + self.obj_man._delete_infoblox_object(self.ib_range_name, range_data) + + def delete_ip_from_host_record(self, host_record, ip): + host_record.ips.remove(ip) + self.obj_man._update_host_record_ips(self.ib_ipaddrs_name, host_record) + return host_record + + def delete_host_record(self, dns_view_name, ip_address): + host_record_data = {'view': dns_view_name, + self.ib_ipaddr_name: ip_address} + self.obj_man._delete_infoblox_object( + 'record:host', host_record_data) + + def delete_fixed_address(self, network_view, ip): + fa_data = {'network_view': network_view, + self.ib_ipaddr_name: ip} + self.obj_man._delete_infoblox_object( + self.ib_fixedaddress_name, fa_data) + + def bind_name_with_host_record(self, dnsview_name, ip, name, extattrs): + record_host = { + self.ib_ipaddr_name: ip, + 'view': dnsview_name + } + update_kwargs = {'name': name, 'extattrs': extattrs} + self.obj_man._update_infoblox_object( + 'record:host', record_host, update_kwargs) + + def update_host_record_eas(self, dns_view, ip, extattrs): + fa_data = {'view': dns_view, + self.ib_ipaddr_name: ip} + fa = self.obj_man._get_infoblox_object_or_none( + 'record:host', fa_data) + if fa: + self.obj_man._update_infoblox_object_by_ref( + fa, {'extattrs': extattrs}) + + def update_fixed_address_eas(self, network_view, ip, extattrs): + fa_data = {'network_view': network_view, + self.ib_ipaddr_name: ip} + fa = self.obj_man._get_infoblox_object_or_none( + self.ib_fixedaddress_name, fa_data) + if fa: + self.obj_man._update_infoblox_object_by_ref( + fa, {'extattrs': extattrs}) + + def update_dns_record_eas(self, dns_view, ip, extattrs): + fa_data = {'view': dns_view, + self.ib_ipaddr_name: ip} + fa = self.obj_man._get_infoblox_object_or_none( + 'record:a', fa_data) + if fa: + self.obj_man._update_infoblox_object_by_ref( + fa, {'extattrs': extattrs}) + + fa = self.obj_man._get_infoblox_object_or_none( + 'record:ptr', fa_data) + if fa: + self.obj_man._update_infoblox_object_by_ref( + fa, {'extattrs': extattrs}) + + +class IPv4Backend(IPBackend): + ib_ipaddr_name = 'ipv4addr' + ib_ipaddrs_name = 'ipv4addrs' + ib_ipaddr_object_name = 'ipv4address' + ib_network_name = 'network' + ib_fixedaddress_name = 'fixedaddress' + ib_range_name = 'range' + + def create_network(self, net_view_name, cidr, nameservers=None, + members=None, gateway_ip=None, dhcp_trel_ip=None, + network_extattrs=None): + network_data = {'network_view': net_view_name, + 'network': cidr, + 'extattrs': network_extattrs} + members_struct = [] + for member in members: + members_struct.append({'ipv4addr': member.ip, + '_struct': 'dhcpmember'}) + network_data['members'] = members_struct + + dhcp_options = [] + + if nameservers: + dhcp_options.append({'name': 'domain-name-servers', + 'value': ",".join(nameservers)}) + + if gateway_ip: + dhcp_options.append({'name': 'routers', + 'value': gateway_ip}) + + if dhcp_trel_ip: + dhcp_options.append({'name': 'dhcp-server-identifier', + 'num': 54, + 'value': dhcp_trel_ip}) + + if dhcp_options: + network_data['options'] = dhcp_options + + return self.obj_man._create_infoblox_object( + self.ib_network_name, network_data, check_if_exists=False) + + def create_ip_range(self, network_view, start_ip, end_ip, network, + disable, range_extattrs): + range_data = {'start_addr': start_ip, + 'end_addr': end_ip, + 'extattrs': range_extattrs, + 'network_view': network_view} + ib_object = self.obj_man._get_infoblox_object_or_none('range', + range_data) + if not ib_object: + range_data['disable'] = disable + self.obj_man._create_infoblox_object( + 'range', range_data, check_if_exists=False) + + def add_ip_to_record(self, host_record, ip, mac): + host_record.ips.append(objects.IPv4(ip, mac)) + ips = self.obj_man._update_host_record_ips('ipv4addrs', host_record) + hr = objects.HostRecordIPv4.from_dict(ips) + return hr + + def create_host_record(self): + return objects.HostRecordIPv4() + + def get_host_record(self, dns_view, ip): + data = { + 'view': dns_view, + 'ipv4addr': ip + } + + raw_host_record = self.obj_man._get_infoblox_object_or_none( + 'record:host', data, return_fields=['ipv4addrs']) + + if raw_host_record: + hr = objects.HostRecordIPv4.from_dict(raw_host_record) + return hr + + def get_fixed_address(self): + return objects.FixedAddressIPv4() + + def bind_name_with_record_a(self, dnsview_name, ip, name, bind_list, + extattrs): + # Forward mapping + if 'record:a' in bind_list: + payload = { + self.ib_ipaddr_name: ip, + 'view': dnsview_name + } + additional_create_kwargs = { + 'name': name, + 'extattrs':extattrs + } + self.obj_man._create_infoblox_object( + 'record:a', payload, + additional_create_kwargs, + update_if_exists=True) + + # Reverse mapping + if 'record:ptr' in bind_list: + record_ptr_data = { + self.ib_ipaddr_name: ip, + 'view': dnsview_name + } + additional_create_kwargs = { + 'ptrdname': name, + 'extattrs': extattrs + } + self.obj_man._create_infoblox_object( + 'record:ptr', record_ptr_data, + additional_create_kwargs, + update_if_exists=True) + + def unbind_name_from_record_a(self, dnsview_name, ip, name, unbind_list): + if 'record:a' in unbind_list: + dns_record_a = { + 'name': name, + self.ib_ipaddr_name: ip, + 'view': dnsview_name + } + self.obj_man._delete_infoblox_object( + 'record:a', dns_record_a) + + if 'record:ptr' in unbind_list: + dns_record_ptr = { + 'ptrdname': name, + 'view': dnsview_name + } + self.obj_man._delete_infoblox_object( + 'record:ptr', dns_record_ptr) + + def find_hostname(self, dns_view, hostname): + data = { + 'name': hostname, + 'view': dns_view + } + + raw_host_record = self.obj_man._get_infoblox_object_or_none( + 'record:host', data, return_fields=['ipv4addrs']) + + if raw_host_record: + hr = objects.HostRecordIPv4.from_dict(raw_host_record) + return hr + + +class IPv6Backend(IPBackend): + ib_ipaddr_name = 'ipv6addr' + ib_ipaddrs_name = 'ipv6addrs' + ib_ipaddr_object_name = 'ipv6address' + ib_network_name = 'ipv6network' + ib_fixedaddress_name = 'ipv6fixedaddress' + ib_range_name = 'ipv6range' + + def create_network(self, net_view_name, cidr, nameservers=None, + members=None, gateway_ip=None, dhcp_trel_ip=None, + network_extattrs=None): + network_data = {'network_view': net_view_name, + 'network': cidr, + 'extattrs': network_extattrs} + + # member here takes ipv4addr since pre-hellfire NIOS version does not + # support ipv6addr (Hellfire supports ipv6addr. + # We are using ipv4addr to suppport both versions + # This is just to the GM know which member is used for + # their internal communication between GM and member so + # it has nothing to do wiht DHCP protocol. + members_struct = [] + for member in members: + members_struct.append(member.specifier) + network_data['members'] = members_struct + + dhcp_options = [] + + if nameservers: + dhcp_options.append({'name': 'domain-name-servers', + 'value': ",".join(nameservers)}) + + if dhcp_options: + network_data['options'] = dhcp_options + + return self.obj_man._create_infoblox_object( + self.ib_network_name, network_data, check_if_exists=False) + + def create_ip_range(self, network_view, start_ip, end_ip, network, + disable, range_extattrs): + range_data = {'start_addr': start_ip, + 'end_addr': end_ip, + 'extattrs': range_extattrs, + 'network': network, + 'network_view': network_view} + ib_object = self.obj_man._get_infoblox_object_or_none('ipv6range', + range_data) + if not ib_object: + range_data['disable'] = disable + self.obj_man._create_infoblox_object( + 'ipv6range', range_data, check_if_exists=False) + + def add_ip_to_record(self, host_record, ip, mac): + host_record.ips.append(objects.IPv6(ip, mac)) + ips = self.obj_man._update_host_record_ips('ipv6addrs', host_record) + hr = objects.HostRecordIPv6.from_dict(ips) + return hr + + def create_host_record(self): + return objects.HostRecordIPv6() + + def get_host_record(self, dns_view, ip): + data = { + 'view': dns_view, + 'ipv6addr': ip + } + + raw_host_record = self.obj_man._get_infoblox_object_or_none( + 'record:host', data, return_fields=['ipv6addrs']) + + if raw_host_record: + hr = objects.HostRecordIPv6.from_dict(raw_host_record) + return hr + + def get_fixed_address(self): + return objects.FixedAddressIPv6() + + def bind_name_with_record_a(self, dnsview_name, ip, name, bind_list, + extattrs): + # Forward mapping + if 'record:aaaa' in bind_list: + payload = { + self.ib_ipaddr_name: ip, + 'view': dnsview_name + } + additional_create_kwargs = { + 'name': name, + 'extattrs':extattrs + } + self.obj_man._create_infoblox_object( + 'record:aaaa', payload, + additional_create_kwargs, + update_if_exists=True) + + # Reverse mapping + if 'record:ptr' in bind_list: + record_ptr_data = { + self.ib_ipaddr_name: ip, + 'view': dnsview_name + } + additional_create_kwargs = { + 'ptrdname': name, + 'extattrs': extattrs + } + self.obj_man._create_infoblox_object( + 'record:ptr', record_ptr_data, + additional_create_kwargs, + update_if_exists=True) + + def unbind_name_from_record_a(self, dnsview_name, ip, name, unbind_list): + if 'record:aaaa' in unbind_list: + dns_record_a = { + 'name': name, + self.ib_ipaddr_name: ip, + 'view': dnsview_name + } + self.obj_man._delete_infoblox_object( + 'record:aaaa', dns_record_a) + + if 'record:ptr' in unbind_list: + dns_record_ptr = { + 'ptrdname': name, + 'view': dnsview_name + } + self.obj_man._delete_infoblox_object( + 'record:ptr', dns_record_ptr) + + def find_hostname(self, dns_view, hostname): + data = { + 'name': hostname, + 'view': dns_view + } + + raw_host_record = self.obj_man._get_infoblox_object_or_none( + 'record:host', data, return_fields=['ipv6addrs']) + + if raw_host_record: + hr = objects.HostRecordIPv6.from_dict(raw_host_record) + return hr + + +class IPBackendFactory(): + @staticmethod + def get_ip_version(ipaddr): + if type(ipaddr) is dict: + ip = ipaddr['ip_address'] + else: + ip = ipaddr + + try: + ip = netaddr.IPAddress(ip) + except ValueError: + ip = netaddr.IPNetwork(ip) + return ip.version + + @staticmethod + def get(obj_man, ip): + ip = IPBackendFactory.get_ip_version(ip) + if ip == 4: + return IPv4Backend(obj_man) + elif ip == 6: + return IPv6Backend(obj_man) + + +class InfobloxObjectManipulator(object): + def __init__(self, connector): + self.connector = connector + + def create_network_view(self, netview_name, nview_extattrs, member): + net_view_data = {'name': netview_name, + 'extattrs': nview_extattrs} + return self._create_infoblox_object('networkview', net_view_data, + delegate_member=member) + + def delete_network_view(self, net_view_name): + # never delete default network view + if net_view_name == 'default': + return + + net_view_data = {'name': net_view_name} + self._delete_infoblox_object('networkview', net_view_data) + + def create_dns_view(self, net_view_name, dns_view_name): + dns_view_data = {'name': dns_view_name, + 'network_view': net_view_name} + return self._create_infoblox_object('view', dns_view_data) + + def delete_dns_view(self, net_view_name): + net_view_data = {'name': net_view_name} + self._delete_infoblox_object('view', net_view_data) + + def get_network(self, net_view_name, cidr): + ip_backend = IPBackendFactory.get(self, cidr) + return ip_backend.get_network(net_view_name, cidr) + + def has_networks(self, network_view_name): + net_data = {'network_view': network_view_name} + try: + ib_net = self._get_infoblox_object_or_none('network', net_data) + return bool(ib_net) + except exc.InfobloxSearchError: + return False + + def network_exists(self, net_view_name, cidr): + ip_backend = IPBackendFactory.get(self, cidr) + return ip_backend.network_exists(net_view_name, cidr) + + def create_network(self, net_view_name, cidr, nameservers=None, + members=None, gateway_ip=None, dhcp_trel_ip=None, + network_extattrs=None): + ip_backend = IPBackendFactory.get(self, cidr) + ip_backend.create_network(net_view_name, cidr, nameservers, + members, gateway_ip, dhcp_trel_ip, + network_extattrs) + + def delete_network(self, net_view_name, cidr): + ip_backend = IPBackendFactory.get(self, cidr) + ip_backend.delete_network(net_view_name, cidr) + + def create_network_from_template(self, net_view_name, cidr, template, + network_extattrs): + network_data = { + 'network_view': net_view_name, + 'network': cidr, + 'template': template, + 'extattrs': network_extattrs + } + return self._create_infoblox_object('network', network_data, + check_if_exists=False) + + def update_network_options(self, ib_network, extattrs=None): + payload = {} + if ib_network.options: + payload['options'] = ib_network.options + if extattrs: + payload['extattrs'] = extattrs + self._update_infoblox_object_by_ref(ib_network.ref, payload) + + def create_ip_range(self, network_view, start_ip, end_ip, network, + disable, range_extattrs): + ip_backend = IPBackendFactory.get(self, start_ip) + ip_backend.create_ip_range(network_view, start_ip, end_ip, + network, disable, range_extattrs) + + def delete_ip_range(self, net_view, start_ip, end_ip): + ip_backend = IPBackendFactory.get(self, start_ip) + ip_backend.delete_ip_range(net_view, start_ip, end_ip) + + def get_host_record(self, dns_view, ip): + ip_backend = IPBackendFactory.get(self, ip) + return ip_backend.get_host_record(dns_view, ip) + + def find_hostname(self, dns_view, hostname, ip): + ip_backend = IPBackendFactory.get(self, ip) + return ip_backend.find_hostname(dns_view, hostname) + + def create_host_record_for_given_ip(self, dns_view_name, zone_auth, + hostname, mac, ip, extattrs): + ip_backend = IPBackendFactory.get(self, ip) + + hr = ip_backend.create_host_record() + hr.ip_version = IPBackendFactory.get_ip_version(ip) + hr.hostname = hostname + hr.zone_auth = zone_auth + hr.mac = mac + hr.dns_view = dns_view_name + hr.ip = ip + hr.extattrs = extattrs + + created_hr = self._create_infoblox_ip_address(hr) + return created_hr + + def create_host_record_from_range(self, dns_view_name, network_view_name, + zone_auth, hostname, mac, first_ip, + last_ip, extattrs): + ip_backend = IPBackendFactory.get(self, first_ip) + + hr = ip_backend.create_host_record() + hr.ip_version = IPBackendFactory.get_ip_version(first_ip) + hr.hostname = hostname + hr.zone_auth = zone_auth + hr.mac = mac + hr.dns_view = dns_view_name + hr.ip = objects.IPAllocationObject.next_available_ip_from_range( + network_view_name, first_ip, last_ip) + hr.extattrs = extattrs + + created_hr = self._create_infoblox_ip_address(hr) + return created_hr + + def delete_host_record(self, dns_view_name, ip_address): + ip_backend = IPBackendFactory.get(self, ip_address) + ip_backend.delete_host_record(dns_view_name, ip_address) + + def create_fixed_address_for_given_ip(self, network_view, mac, ip, + extattrs): + ip_backend = IPBackendFactory.get(self, ip) + + fa = ip_backend.get_fixed_address() + fa.ip = ip + fa.net_view = network_view + fa.mac = mac + fa.extattrs = extattrs + + created_fa = self._create_infoblox_ip_address(fa) + return created_fa + + def create_fixed_address_from_range(self, network_view, mac, first_ip, + last_ip, extattrs): + ip_backend = IPBackendFactory.get(self, first_ip) + + fa = ip_backend.get_fixed_address() + fa.ip = objects.IPAllocationObject.next_available_ip_from_range( + network_view, first_ip, last_ip) + fa.net_view = network_view + fa.mac = mac + fa.extattrs = extattrs + + created_fa = self._create_infoblox_ip_address(fa) + return created_fa + + def create_fixed_address_from_cidr(self, network_view, mac, cidr, + extattrs): + ip_backend = IPBackendFactory.get(self, cidr) + + fa = ip_backend.get_fixed_address() + fa.ip = objects.IPAllocationObject.next_available_ip_from_cidr( + network_view, cidr) + fa.mac = mac + fa.net_view = network_view + fa.extattrs = extattrs + + created_fa = self._create_infoblox_ip_address(fa) + return created_fa + + def delete_fixed_address(self, network_view, ip_address): + ip_backend = IPBackendFactory.get(self, ip_address) + ip_backend.delete_fixed_address(network_view, ip_address) + + def add_ip_to_record(self, host_record, ip, mac): + ip_backend = IPBackendFactory.get(self, ip) + ip_backend.add_ip_to_record(host_record, ip, mac) + + def add_ip_to_host_record_from_range(self, host_record, network_view, + mac, first_ip, last_ip): + ip = objects.IPAllocationObject.next_available_ip_from_range( + network_view, first_ip, last_ip) + hr = self.add_ip_to_record(host_record, ip, mac) + return hr + + def delete_ip_from_host_record(self, host_record, ip): + ip_backend = IPBackendFactory.get(self, ip) + ip_backend.delete_ip_from_host_record(host_record, ip) + + def has_dns_zones(self, dns_view): + zone_data = {'view': dns_view} + try: + zone = self._get_infoblox_object_or_none('zone_auth', zone_data) + return bool(zone) + except exc.InfobloxSearchError: + return False + + def create_dns_zone(self, dns_view, dns_zone_fqdn, primary_dns_member=None, + secondary_dns_members=None, zone_format=None, + ns_group=None, prefix=None, zone_extattrs={}): + # TODO(mirantis) support IPv6 + dns_zone_data = {'fqdn': dns_zone_fqdn, + 'view': dns_view, + 'extattrs': zone_extattrs} + additional_create_kwargs = {} + + if primary_dns_member: + grid_primary = [{'name': primary_dns_member.name, + '_struct': 'memberserver'}] + additional_create_kwargs['grid_primary'] = grid_primary + + if secondary_dns_members: + grid_secondaries = [{'name': member.name, + '_struct': 'memberserver'} + for member in secondary_dns_members] + additional_create_kwargs['grid_secondaries'] = grid_secondaries + + if zone_format: + additional_create_kwargs['zone_format'] = zone_format + + if ns_group: + additional_create_kwargs['ns_group'] = ns_group + + if prefix: + additional_create_kwargs['prefix'] = prefix + + try: + self._create_infoblox_object( + 'zone_auth', dns_zone_data, additional_create_kwargs, + check_if_exists=True) + except exc.InfobloxCannotCreateObject: + LOG.warning( + _('Unable to create DNS zone %(dns_zone_fqdn)s ' + 'for %(dns_view)s'), + {'dns_zone_fqdn': dns_zone_fqdn, 'dns_view': dns_view}) + + def delete_dns_zone(self, dns_view, dns_zone_fqdn): + dns_zone_data = {'fqdn': dns_zone_fqdn, + 'view': dns_view} + self._delete_infoblox_object('zone_auth', dns_zone_data) + + def update_host_record_eas(self, dns_view, ip, extattrs): + ip_backend = IPBackendFactory.get(self, ip) + ip_backend.update_host_record_eas(dns_view, ip, extattrs) + + def update_fixed_address_eas(self, network_view, ip, extattrs): + ip_backend = IPBackendFactory.get(self, ip) + ip_backend.update_fixed_address_eas(network_view, ip, extattrs) + + def update_dns_record_eas(self, dns_view, ip, extattrs): + ip_backend = IPBackendFactory.get(self, ip) + ip_backend.update_dns_record_eas(dns_view, ip, extattrs) + + def bind_name_with_host_record(self, dnsview_name, ip, name, extattrs): + ip_backend = IPBackendFactory.get(self, ip) + ip_backend.bind_name_with_host_record(dnsview_name, ip, name, extattrs) + + def bind_name_with_record_a(self, dnsview_name, ip, name, bind_list, + extattrs): + ip_backend = IPBackendFactory.get(self, ip) + ip_backend.bind_name_with_record_a(dnsview_name, ip, name, bind_list, + extattrs) + + def unbind_name_from_record_a(self, dnsview_name, ip, name, unbind_list): + ip_backend = IPBackendFactory.get(self, ip) + ip_backend.unbind_name_from_record_a( + dnsview_name, ip, name, unbind_list) + + def get_member(self, member): + return self.connector.get_object('member', {'host_name': member.name}) + + def restart_all_services(self, member): + ib_member = self.get_member(member)[0] + self.connector.call_func('restartservices', ib_member['_ref'], + {'restart_option': 'RESTART_IF_NEEDED', + 'service_option': 'ALL'}) + + def get_object_refs_associated_with_a_record(self, a_record_ref): + associated_with_a_record = [ # {object_type, search_field} + {'type': 'record:cname', 'search': 'canonical'}, + {'type': 'record:txt', 'search': 'name'} + ] + + obj_refs = [] + a_record = self.connector.get_object(a_record_ref) + + for rec_inf in associated_with_a_record: + objs = self.connector.get_object( + rec_inf['type'], {'view': a_record['view'], + rec_inf['search']: a_record['name']}) + if objs: + for obj in objs: + obj_refs.append(obj['_ref']) + + return obj_refs + + def get_all_associated_objects(self, net_view_name, ip): + ip_backend = IPBackendFactory.get(self, ip) + return ip_backend.get_all_associated_objects(net_view_name, ip) + + def delete_all_associated_objects(self, net_view_name, ip, delete_list): + del_objs = [] + obj_refs = self.get_all_associated_objects(net_view_name, ip) + + for obj_ref in obj_refs: + del_objs.append(obj_ref) + if self._get_object_type_from_ref(obj_ref) \ + in ['record:a', 'record:aaaa']: + del_objs.extend( + self.get_object_refs_associated_with_a_record(obj_ref)) + + for obj_ref in del_objs: + if self._get_object_type_from_ref(obj_ref) in delete_list: + self.connector.delete_object(obj_ref) + + def delete_object_by_ref(self, ref): + try: + self.connector.delete_object(ref) + except exc.InfobloxCannotDeleteObject as e: + LOG.info(_("Failed to delete an object: %s"), e) + + def _create_infoblox_ip_address(self, ip_object): + try: + created_ip_json = self._create_infoblox_object( + ip_object.infoblox_type, + ip_object.to_dict(), + check_if_exists=False, + return_fields=ip_object.return_fields) + + return ip_object.from_dict(created_ip_json) + except exc.InfobloxCannotCreateObject as e: + if "Cannot find 1 available IP" in e.response['text']: + raise exc.InfobloxCannotAllocateIp(ip_data=ip_object.to_dict()) + else: + raise e + except exc.HostRecordNotPresent: + raise exc.InfobloxHostRecordIpAddrNotCreated(ip=ip_object.ip, + mac=ip_object.mac) + except exc.InfobloxInvalidIp: + raise exc.InfobloxDidNotReturnCreatedIPBack() + + def _create_infoblox_object(self, obj_type, payload, + additional_create_kwargs=None, + check_if_exists=True, + return_fields=None, + delegate_member=None, + update_if_exists=False): + if additional_create_kwargs is None: + additional_create_kwargs = {} + + ib_object = None + if check_if_exists or update_if_exists: + ib_object = self._get_infoblox_object_or_none(obj_type, payload) + if ib_object: + LOG.info( + _("Infoblox %(obj_type)s " + "already exists: %(ib_object)s"), + {'obj_type': obj_type, 'ib_object': ib_object}) + + if not ib_object: + payload.update(additional_create_kwargs) + ib_object = self.connector.create_object(obj_type, payload, + return_fields, delegate_member) + LOG.info( + _("Infoblox %(obj_type)s " + "was created: %(ib_object)s"), + {'obj_type': obj_type, 'ib_object': ib_object}) + elif update_if_exists: + self._update_infoblox_object_by_ref(ib_object, + additional_create_kwargs) + + return ib_object + + def _get_infoblox_object_or_none(self, obj_type, payload, + return_fields=None, proxy=False): + # Ignore 'extattrs' for get_object, since this field is not searchible + search_payload = {} + for key in payload: + if key is not 'extattrs': + search_payload[key] = payload[key] + ib_object = self.connector.get_object(obj_type, search_payload, + return_fields, proxy=proxy) + if ib_object: + if return_fields: + return ib_object[0] + else: + return ib_object[0]['_ref'] + + return None + + def _update_infoblox_object(self, obj_type, payload, update_kwargs): + ib_object_ref = None + warn_msg = _('Infoblox %(obj_type)s will not be updated because' + ' it cannot be found: %(payload)s') + try: + ib_object_ref = self._get_infoblox_object_or_none(obj_type, + payload) + if not ib_object_ref: + LOG.warning(warn_msg, {'obj_type': obj_type, + 'payload': payload}) + except exc.InfobloxSearchError as e: + LOG.warning(warn_msg, obj_type, payload) + LOG.info(e) + + if ib_object_ref: + self._update_infoblox_object_by_ref(ib_object_ref, update_kwargs) + + def _update_infoblox_object_by_ref(self, ref, update_kwargs, + return_fields=None): + updated_object = self.connector.update_object(ref, update_kwargs, + return_fields) + LOG.info(_('Infoblox object was updated: %s'), ref) + return updated_object + + def _delete_infoblox_object(self, obj_type, payload): + ib_object_ref = None + warn_msg = _('Infoblox %(obj_type)s will not be deleted because' + ' it cannot be found: %(payload)s') + try: + ib_object_ref = self._get_infoblox_object_or_none(obj_type, + payload) + if not ib_object_ref: + LOG.warning(warn_msg, {'obj_type': obj_type, + 'payload': payload}) + except exc.InfobloxSearchError as e: + LOG.warning(warn_msg, {'obj_type': obj_type, + 'payload': payload}) + LOG.info(e) + + if ib_object_ref: + self.connector.delete_object(ib_object_ref) + LOG.info(_('Infoblox object was deleted: %s'), ib_object_ref) + + def _update_host_record_ips(self, ipaddrs_name, host_record): + ipaddrs = {ipaddrs_name: [ip.to_dict(add_host=False) + for ip in host_record.ips]} + return self._update_infoblox_object_by_ref( + host_record.ref, ipaddrs, return_fields=[ipaddrs_name]) + + @staticmethod + def _get_object_type_from_ref(ref): + return ref.split('/', 1)[0] diff --git a/neutron/ipam/drivers/infoblox/objects.py b/neutron/ipam/drivers/infoblox/objects.py new file mode 100755 index 0000000..24cdd48 --- /dev/null +++ b/neutron/ipam/drivers/infoblox/objects.py @@ -0,0 +1,663 @@ +# Copyright 2014 OpenStack LLC. +# 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 logging +import netaddr +import six +import random +from oslo.config import cfg +import neutron.ipam.drivers.infoblox.exceptions as ib_exc + + +OPTS = [ + cfg.BoolOpt('use_dhcp_for_ip_allocation_record', + default=True, + help=_("Used to set 'configure_for_dhcp' option to enable " + " or disable dhcp for host or fixed record")) +] + +cfg.CONF.register_opts(OPTS) +LOG = logging.getLogger(__name__) + + +def is_valid_ip(ip): + try: + netaddr.IPAddress(ip) + except netaddr.core.AddrFormatError: + return False + return True + + +def generate_duid(mac): + duid = [0x00, + random.randint(0x00, 0x7f), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff)] + return ':'.join(map(lambda x: "%02x" % x, duid)) + ':' + mac + + +class Network(object): + """Sample Infoblox 'network' object in JSON format: + [ + { + "_ref": "network/ZG5zLm5ldHdvcmskMTAuMzkuMTEuMC8yNC8w: + 10.39.11.0/24/default", + "members": [ + { + "_struct": "dhcpmember", + "ipv4addr": "10.39.11.123", + "name": "infoblox.localdomain" + } + ], + "options": [ + { + "name": "dhcp-lease-time", + "num": 51, + "use_option": false, + "value": "43200", + "vendor_class": "DHCP" + }, + { + "name": "domain-name-servers", + "num": 6, + "use_option": true, + "value": "10.39.11.123", + "vendor_class": "DHCP" + }, + { + "name": "routers", + "num": 3, + "use_option": false, + "value": "10.39.11.1", + "vendor_class": "DHCP" + } + ] + } + ] + """ + DNS_NAMESERVERS_OPTION = 'domain-name-servers' + + def __init__(self): + self.infoblox_type = 'network' + self.members = [] + self.options = [] + self.member_ip_addrs = [] + self.infoblox_reference = None + self.ref = None + + def __repr__(self): + return "{0}".format(self.to_dict()) + + @staticmethod + def from_dict(network_ib_object): + net = Network() + net.members = network_ib_object['members'] + net.options = network_ib_object['options'] + + for member in net.members: + net.member_ip_addrs.append(member['ipv4addr']) + net.ref = network_ib_object['_ref'] + return net + + @property + def dns_nameservers(self): + # NOTE(max_lobur): The behaviour of the WAPI is as follows: + # * If the subnet created without domain-name-servers option it will + # be absent in the options list. + # * If the subnet created with domain-name-servers option and then + # it's cleared by update operation, the option will be present in + # the list, will carry the last data, but will have use_option = False + # Both cases mean that there are NO specified nameservers on NIOS. + dns_nameservers = [] + for opt in self.options: + if self._is_dns_option(opt): + if opt.get('use_option', True): + dns_nameservers = opt['value'].split(',') + break + return dns_nameservers + + @dns_nameservers.setter + def dns_nameservers(self, value): + for opt in self.options: + if self._is_dns_option(opt): + if value: + opt['value'] = ",".join(value) + opt['use_option'] = True + else: + opt['use_option'] = False + break + else: + if value: + self.options.append(dict( + name=self.DNS_NAMESERVERS_OPTION, + value=",".join(value), + use_option=True + )) + + def has_dns_members(self): + for opt in self.options: + if self._is_dns_option(opt): + return True + return False + + def _is_member_ip(self, ip): + return ip in self.member_ip_addrs + + def update_member_ip_in_dns_nameservers(self, relay_ip): + for opt in self.options: + if self._is_dns_option(opt): + original_value = opt['value'].split(',') + original_value.insert(0, relay_ip) + opt['value'] = ",".join( + [val for val in original_value + if val and not self._is_member_ip(val)]) + + return + + def to_dict(self): + return { + 'members': self.members, + 'options': self.options + } + + @staticmethod + def _is_dns_option(option): + return option['name'] == Network.DNS_NAMESERVERS_OPTION + + +class IPAddress(object): + def __init__(self, ip=None, mac=None): + self.ip = ip + self.mac = mac + self.configure_for_dhcp = cfg.CONF.use_dhcp_for_ip_allocation_record + self.hostname = None + self.dns_zone = None + self.fqdn = None + + def __eq__(self, other): + if isinstance(other, six.string_types): + return self.ip == other + elif isinstance(other, self.__class__): + return self.ip == other.ip and self.dns_zone == other.dns_zone + return False + + +class IPv4(IPAddress): + def to_dict(self, add_host=False): + d = { + "ipv4addr": self.ip, + "configure_for_dhcp": self.configure_for_dhcp + } + + if self.fqdn and add_host: + d['host'] = self.fqdn + + if self.mac: + d['mac'] = self.mac + + return d + + def __repr__(self): + return "IPv4Addr{0}".format(self.to_dict()) + + @staticmethod + def from_dict(d): + ip = d.get('ipv4addr') + if not is_valid_ip(ip): + raise ib_exc.InfobloxInvalidIp(ip=ip) + + ipv4obj = IPv4() + host = d.get('host', 'unknown.unknown') + hostname, _, dns_zone = host.partition('.') + ipv4obj.ip = ip + ipv4obj.mac = d.get('mac') + ipv4obj.configure_for_dhcp = d.get('configure_for_dhcp') + ipv4obj.hostname = hostname + ipv4obj.zone_auth = dns_zone + ipv4obj.fqdn = host + return ipv4obj + + +class IPv6(IPAddress): + def to_dict(self, add_host=False): + d = { + "ipv6addr": self.ip, + "configure_for_dhcp": self.configure_for_dhcp + } + + if self.fqdn and add_host: + d['host'] = self.fqdn + + if self.mac: + d['duid'] = generate_duid(self.mac) + + return d + + def __repr__(self): + return "IPv6Addr{0}".format(self.to_dict()) + + @staticmethod + def from_dict(d): + ip = d.get('ipv6addr') + if not is_valid_ip(ip): + raise ib_exc.InfobloxInvalidIp(ip=ip) + + ipv6obj = IPv6() + host = d.get('host', 'unknown.unknown') + hostname, _, dns_zone = host.partition('.') + ipv6obj.ip = ip + ipv6obj.duid = d.get('duid') + ipv6obj.configure_for_dhcp = d.get('configure_for_dhcp') + ipv6obj.hostname = hostname + ipv6obj.zone_auth = dns_zone + ipv6obj.fqdn = host + return ipv6obj + + +class IPAllocationObject(object): + @staticmethod + def next_available_ip_from_cidr(net_view_name, cidr): + return ('func:nextavailableip:' + '{cidr:s},{net_view_name:s}').format(**locals()) + + @staticmethod + def next_available_ip_from_range(net_view_name, first_ip, last_ip): + return ('func:nextavailableip:' + '{first_ip}-{last_ip},{net_view_name}').format(**locals()) + + +class HostRecord(IPAllocationObject): + def __init__(self): + self.infoblox_type = 'record:host' + self.ips = [] + self.ref = None + self.name = None + self.dns_view = None + self.extattrs = None + self.configure_for_dhcp = cfg.CONF.use_dhcp_for_ip_allocation_record + + def __repr__(self): + return "HostRecord{0}".format(self.to_dict()) + + def __eq__(self, other): + return (isinstance(other, self.__class__) and + self.ips == other.ips and + self.name == other.name and + self.dns_view == other.dns_view) + + @property + def zone_auth(self): + return self._zone_auth + + @zone_auth.setter + def zone_auth(self, value): + if value: + self._zone_auth = value.lstrip('.') + + +class HostRecordIPv4(HostRecord): + """Sample Infoblox host record object in JSON format: + { + u'_ref': u'record:host/ZG5zLmhvc3QkLl9kZWZhdWx0LmNvbS5nbG9iYWwuY22NA + :test_host_name.testsubnet.cloud.global.com/default', + u'ipv4addrs': [ + { + u'configure_for_dhcp': False, + u'_ref': u'record:host_ipv4addr/lMmQ3ZjkuMmM4ZjhlOTctMGQ5Mi00Y2:22.0.0.2/ + test_host_name.testsubnet.cloud.global.com/default', u'ipv4addr': u'22.0.0.2', + u'mac': u'fa:16:3e:29:87:70', + u'host': u'2c8f8e97-0d92-4cac-a350-09a0c53fe664.33c00d42-9715-43fe-862c-6ff2b7e2d7f9.cloud.global.com' + } + ], + u'extattrs': { + u'Account': {u'value': u'8a21c40495f04f30a1b2dc6fd1d9ed1a'}, + u'Cloud API Owned': {u'value': u'True'}, + u'VM ID': {u'value': u'None'}, + u'IP Type': {u'value': u'Fixed'}, + u'CMP Type': {u'value': u'OpenStack'}, + u'Port ID': {u'value': u'136ef9ad-9c88-41ea-9fa6-bd48d8ec789a'}, + u'Tenant ID': {u'value': u'00fd80791dee4112bb538c872b206d4c'} + } + } + """ + return_fields = [ + 'ipv4addrs', + 'extattrs' + ] + + def __repr__(self): + return "HostRecord{0}".format(self.to_dict()) + + def __eq__(self, other): + return (isinstance(other, self.__class__) and + self.ips == other.ips and + self.name == other.name and + self.dns_view == other.dns_view) + + @property + def ip(self): + if self.ips: + return self.ips[0].ip + + @ip.setter + def ip(self, ip_address): + if self.ips: + self.ips[0].ip = ip_address + else: + ip_obj = IPv4() + ip_obj.ip = ip_address + + self.ips.append(ip_obj) + + @property + def mac(self): + if self.ips: + return self.ips[0].mac + + @mac.setter + def mac(self, mac_address): + if self.ips: + self.ips[0].mac = mac_address + else: + ip_obj = IPv4() + ip_obj.mac = mac_address + self.ips.append(ip_obj) + + @property + def hostname(self): + if self.ips: + return self.ips[0].hostname + + @hostname.setter + def hostname(self, name): + if self.ips: + self.ips[0].hostname = name + else: + ip_obj = IPv4() + ip_obj.hostname = name + self.ips.append(ip_obj) + + def to_dict(self): + result = { + 'view': self.dns_view, + 'name': '.'.join([self.hostname, self.zone_auth]), + 'extattrs': self.extattrs, + 'ipv4addrs': [ip.to_dict() for ip in self.ips] + } + return result + + @staticmethod + def from_dict(hr_dict): + ipv4addrs = hr_dict.get('ipv4addrs', None) + if not ipv4addrs: + raise ib_exc.HostRecordNotPresent() + + ipv4addr = ipv4addrs[0] + ip = ipv4addr['ipv4addr'] + if not is_valid_ip(ip): + raise ib_exc.InfobloxInvalidIp(ip=ip) + host = ipv4addr.get('host', 'unknown.unknown') + mac = ipv4addr.get('mac') + hostname, _, dns_zone = host.partition('.') + + host_record = HostRecordIPv4() + host_record.hostname = hostname + host_record.zone_auth = dns_zone + host_record.ref = hr_dict.get('_ref') + host_record.ips = [IPv4.from_dict(ip) for ip in ipv4addrs] + host_record.extattrs = hr_dict.get('extattrs') + + return host_record + + @property + def zone_auth(self): + if self.ips: + return self.ips[0].zone_auth + + @zone_auth.setter + def zone_auth(self, value): + if value: + self.ips[0].zone_auth = value.lstrip('.') + + +class HostRecordIPv6(HostRecord): + """Sample Infoblox host record object in JSON format: + { + u'_ref': u'record:host/ZG5zLmhvc3QkLl9kZWZhdWx0LmNvbS5nbG9iYWwuYMQ + :test_host_name.testsubnet.cloud.global.com/default', + u'ipv6addrs': [ + { + u'configure_for_dhcp': False, + u'_ref': u'record:host_ipv6addr/ZG5zLmhvc3RfYWRkcmV:2607%3Af0d0%3A1002%3A51%3A%3A2/ + test_host_name.testsubnet/default', + u'host': u'ea30c45d-6385-44a2-b187-94b0c6f8bad1.9706dd0c-b772-4522-93e3-2e4fea2859de.cloud.global.com', + u'duid': u'00:6f:6d:ba:fa:16:3e:86:40:e3', + u'ipv6addr': u'2607:f0d0:1002:51::2' + } + ], + u'extattrs': { + u'Account': {u'value': u'8a21c40495f04f30a1b2dc6fd1d9ed1a'}, + u'Port ID': {u'value': u'77c2ee08-32bf-4cd6-a24f-586ca91bd533'}, + u'VM ID': {u'value': u'None'}, + u'IP Type': {u'value': u'Fixed'}, + u'CMP Type': {u'value': u'OpenStack'}, + u'Cloud API Owned': {u'value': u'True'}, + u'Tenant ID': {u'value': u'00fd80791dee4112bb538c872b206d4c'} + } + } + """ + return_fields = [ + 'ipv6addrs', + 'extattrs' + ] + + def to_dict(self): + result = { + 'view': self.dns_view, + 'name': '.'.join([self.hostname, self.zone_auth]), + 'extattrs': self.extattrs + } + + result['ipv6addrs'] = [{ + 'configure_for_dhcp': self.configure_for_dhcp, + 'ipv6addr': self.ip, + 'duid': generate_duid(self.mac) + }] + + return result + + @staticmethod + def from_dict(hr_dict): + ipv6addrs = hr_dict.get('ipv6addrs', None) + if not ipv6addrs: + raise ib_exc.HostRecordNotPresent() + + ipv6addr = ipv6addrs[0] + ip = ipv6addr['ipv6addr'] + if not is_valid_ip(ip): + raise ib_exc.InfobloxInvalidIp(ip=ip) + host = ipv6addr.get('host', 'unknown.unknown') + mac = ipv6addr.get('mac') + + hostname, _, dns_zone = host.partition('.') + + host_record = HostRecordIPv6() + host_record.hostname = hostname + host_record.zone_auth = dns_zone + host_record.mac = mac + host_record.ip = ip + host_record.ref = hr_dict.get('_ref') + + return host_record + + @property + def ip(self): + if self.ips: + return self.ips[0].ip + + @ip.setter + def ip(self, ip_address): + if self.ips: + self.ips[0].ip = ip_address + else: + ip_obj = IPv6() + ip_obj.ip = ip_address + + self.ips.append(ip_obj) + + @property + def mac(self): + if self.ips: + return self.ips[0].mac + + @mac.setter + def mac(self, mac_address): + if self.ips: + self.ips[0].mac = mac_address + else: + ip_obj = IPv6() + ip_obj.mac = mac_address + self.ips.append(ip_obj) + + @property + def hostname(self): + if self.ips: + return self.ips[0].hostname + + @hostname.setter + def hostname(self, name): + if self.ips: + self.ips[0].hostname = name + else: + ip_obj = IPv6() + ip_obj.hostname = name + self.ips.append(ip_obj) + + +class FixedAddress(IPAllocationObject): + def __init__(self): + self.infoblox_type = 'fixedaddress' + self.ip = None + self.net_view = None + self.mac = None + self.duid = None + self.extattrs = None + self.ref = None + + def __repr__(self): + return "FixedAddress({0})".format(self.to_dict()) + + +class FixedAddressIPv4(FixedAddress): + def __init__(self): + self.infoblox_type = 'fixedaddress' + + self.return_fields = [ + 'ipv4addr', + 'mac', + 'network_view', + 'extattrs' + ] + + def to_dict(self): + return { + 'mac': self.mac, + 'network_view': self.net_view, + 'ipv4addr': self.ip, + 'extattrs': self.extattrs + } + + @staticmethod + def from_dict(fixed_address_dict): + ip = fixed_address_dict.get('ipv4addr') + if not is_valid_ip(ip): + raise ib_exc.InfobloxInvalidIp(ip=ip) + + fa = FixedAddress() + fa.ip = ip + fa.mac = fixed_address_dict.get('mac') + fa.net_view = fixed_address_dict.get('network_view') + fa.extattrs = fixed_address_dict.get('extattrs') + fa.ref = fixed_address_dict.get('_ref') + return fa + + +class FixedAddressIPv6(FixedAddress): + def __init__(self): + self.infoblox_type = 'ipv6fixedaddress' + + self.return_fields = [ + 'ipv6addr', + 'duid', + 'network_view', + 'extattrs' + ] + + def to_dict(self): + return { + 'duid': generate_duid(self.mac), + 'network_view': self.net_view, + 'ipv6addr': self.ip, + 'extattrs': self.extattrs + } + + @staticmethod + def from_dict(fixed_address_dict): + ip = fixed_address_dict.get('ipv6addr') + if not is_valid_ip(ip): + raise ib_exc.InfobloxInvalidIp(ip=ip) + + fa = FixedAddress() + fa.ip = ip + fa.mac = fixed_address_dict.get('mac') + fa.net_view = fixed_address_dict.get('network_view') + fa.extattrs = fixed_address_dict.get('extattrs') + fa.ref = fixed_address_dict.get('_ref') + return fa + + +class Member(object): + def __init__(self, ip, name, ipv6=None, map_id=None, delegate=False): + self.ip = ip + self.ipv6 = ipv6 + self.name = name + self.map_id = map_id + self.delegate = delegate + + def __eq__(self, other): + return self.ip == other.ip and \ + self.name == other.name and \ + self.map_id == other.map_id + + def __repr__(self): + return \ + ('Member(IP={ip}, IPv6={ipv6}, name={name}, map_id={map_id}, ' + + 'delegate={delegate})').\ + format(ip=self.ip, + ipv6=self.ipv6, + name=self.name, + map_id=self.map_id, + delegate=self.delegate) + + @property + def specifier(self): + """Return _struct dhcpmember that can be used to specify a member""" + specifier = {'_struct': 'dhcpmember'} + if self.name: + specifier['name'] = self.name + elif self.ip: + specifier['ipv4addr'] = self.ip + elif self.ipv6: + specifier['ipv6addr'] = self.ipv6 + return specifier diff --git a/neutron/ipam/drivers/infoblox/tasks.py b/neutron/ipam/drivers/infoblox/tasks.py new file mode 100644 index 0000000..56d8d24 --- /dev/null +++ b/neutron/ipam/drivers/infoblox/tasks.py @@ -0,0 +1,143 @@ +# Copyright 2014 OpenStack LLC. +# 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 operator + +from taskflow import task + +from neutron.ipam.drivers.infoblox import exceptions + + +class CreateNetViewTask(task.Task): + def execute(self, obj_manip, net_view_name, nview_extattrs, dhcp_member): + obj_manip.create_network_view( + net_view_name, nview_extattrs, dhcp_member[0]) + + def revert(self, obj_manip, net_view_name, **kwargs): + if not obj_manip.has_networks(net_view_name): + obj_manip.delete_network_view(net_view_name) + + +class CreateNetworkTask(task.Task): + def execute(self, obj_manip, net_view_name, cidr, nameservers, dhcp_member, + gateway_ip, dhcp_trel_ip, network_extattrs, related_members, + ip_version, ipv6_ra_mode=None, ipv6_address_mode=None): + obj_manip.create_network(net_view_name, cidr, nameservers, dhcp_member, + gateway_ip, dhcp_trel_ip, network_extattrs) + for member in related_members: + obj_manip.restart_all_services(member) + + def revert(self, obj_manip, net_view_name, related_members, cidr, + **kwargs): + obj_manip.delete_network(net_view_name, cidr) + for member in related_members: + obj_manip.restart_all_services(member) + + +class ChainInfobloxNetworkTask(task.Task): + def execute(self, obj_manip, net_view_name, cidr, network_extattrs): + ea_names = ['Is External', 'Is Shared'] + + eas = operator.itemgetter(*ea_names)(network_extattrs) + shared_or_external = any(eval(ea['value']) for ea in eas) + + if shared_or_external: + ib_network = obj_manip.get_network(net_view_name, cidr) + obj_manip.update_network_options(ib_network, network_extattrs) + else: + raise exceptions.InfobloxInternalPrivateSubnetAlreadyExist() + + def revert(self, obj_manip, net_view_name, cidr, network_extattrs, + **kwargs): + # keep NIOS network untouched on rollback + pass + + +class CreateNetworkFromTemplateTask(task.Task): + def execute(self, obj_manip, net_view_name, cidr, template, + network_extattrs): + obj_manip.create_network_from_template( + net_view_name, cidr, template, network_extattrs) + + def revert(self, obj_manip, net_view_name, cidr, **kwargs): + obj_manip.delete_network(net_view_name, cidr) + + +class CreateIPRange(task.Task): + def execute(self, obj_manip, net_view_name, start_ip, end_ip, disable, + cidr, range_extattrs, ip_version, ipv6_ra_mode=None, + ipv6_address_mode=None): + obj_manip.create_ip_range(net_view_name, start_ip, end_ip, + cidr, disable, range_extattrs) + + def revert(self, obj_manip, net_view_name, start_ip, end_ip, + ip_version, ipv6_ra_mode=None, ipv6_address_mode=None, + **kwargs): + obj_manip.delete_ip_range(net_view_name, start_ip, end_ip) + + +class CreateDNSViewTask(task.Task): + def execute(self, obj_manip, net_view_name, dns_view_name): + obj_manip.create_dns_view(net_view_name, dns_view_name) + + def revert(self, **kwargs): + # never delete DNS view + pass + + +class CreateDNSZonesTask(task.Task): + def execute(self, obj_manip, dnsview_name, fqdn, dns_member, + secondary_dns_members, zone_extattrs, **kwargs): + obj_manip.create_dns_zone(dnsview_name, fqdn, dns_member, + secondary_dns_members, + zone_extattrs=zone_extattrs) + + def revert(self, obj_manip, dnsview_name, fqdn, **kwargs): + obj_manip.delete_dns_zone(dnsview_name, fqdn) + + +class CreateDNSZonesTaskCidr(task.Task): + def execute(self, obj_manip, dnsview_name, cidr, dns_member, zone_format, + secondary_dns_members, prefix, zone_extattrs, **kwargs): + obj_manip.create_dns_zone(dnsview_name, cidr, dns_member, + secondary_dns_members, + prefix=prefix, + zone_format=zone_format, + zone_extattrs=zone_extattrs) + + def revert(self, obj_manip, dnsview_name, cidr, **kwargs): + obj_manip.delete_dns_zone(dnsview_name, cidr) + + +class CreateDNSZonesFromNSGroupTask(task.Task): + def execute(self, obj_manip, dnsview_name, fqdn, ns_group, + zone_extattrs, **kwargs): + obj_manip.create_dns_zone(dnsview_name, fqdn, ns_group=ns_group, + zone_extattrs=zone_extattrs) + + def revert(self, obj_manip, dnsview_name, fqdn, **kwargs): + obj_manip.delete_dns_zone(dnsview_name, fqdn) + + +class CreateDNSZonesCidrFromNSGroupTask(task.Task): + def execute(self, obj_manip, dnsview_name, cidr, ns_group, zone_format, + prefix, zone_extattrs, **kwargs): + obj_manip.create_dns_zone(dnsview_name, cidr, + ns_group=ns_group, + prefix=prefix, + zone_format=zone_format, + zone_extattrs=zone_extattrs) + + def revert(self, obj_manip, dnsview_name, cidr, **kwargs): + obj_manip.delete_dns_zone(dnsview_name, cidr) diff --git a/neutron/ipam/drivers/neutron_db.py b/neutron/ipam/drivers/neutron_db.py new file mode 100644 index 0000000..096722b --- /dev/null +++ b/neutron/ipam/drivers/neutron_db.py @@ -0,0 +1,217 @@ +# Copyright (c) 2014 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. +import netaddr + +from sqlalchemy import orm +from sqlalchemy.orm import exc + +from neutron.common import exceptions as n_exc +from neutron.db import common_db_mixin +from neutron.db import models_v2 +from neutron.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + + +class NeutronPluginController(common_db_mixin.CommonDbMixin): + def _get_network(self, context, net_id): + try: + network = self._get_by_id(context, models_v2.Network, net_id) + except exc.NoResultFound: + raise n_exc.NetworkNotFound(net_id=net_id) + return network + + def _get_subnet(self, context, subnet_id): + try: + subnet = self._get_by_id(context, models_v2.Subnet, subnet_id) + except exc.NoResultFound: + raise n_exc.SubnetNotFound(subnet_id=subnet_id) + return subnet + + +def get_subnets_by_port_id(context, port_id): + subnets_with_port = [] + # Get Requested port + port = context.session.query(models_v2.Port).filter_by(id=port_id).one() + # Collect all subnets from port network + subnets = get_subnets_by_network(context, port.network_id) + for sub in subnets: + # Collect all ports from subnet + subnet_ports = get_subnet_ports(context, sub.id) + # Compare them with original port, and if they are the same - save + subnets_with_port += [sub for sp in subnet_ports if sp.id == port.id] + + return subnets_with_port + + +def get_subnet_ports(context, subnet_id): + # Check if any tenant owned ports are using this subnet + ports_qry = context.session.query(models_v2.Port).join( + models_v2.IPAllocation).with_lockmode( + 'update').enable_eagerloads(False) + ports = ports_qry.filter_by(subnet_id=subnet_id) + return ports + + +def get_all_subnets(context): + return context.session.query(models_v2.Subnet).all() + + +def generate_ip(context, subnet): + try: + return _try_generate_ip(context, subnet) + except n_exc.IpAddressGenerationFailure: + _rebuild_availability_ranges(context, subnet) + + return _try_generate_ip(context, subnet) + + +def allocate_specific_ip(context, subnet_id, ip_address): + """Allocate a specific IP address on the subnet.""" + ip = int(netaddr.IPAddress(ip_address)) + range_qry = context.session.query( + models_v2.IPAvailabilityRange).join( + models_v2.IPAllocationPool).with_lockmode('update') + results = range_qry.filter_by(subnet_id=subnet_id) + for range in results: + first = int(netaddr.IPAddress(range['first_ip'])) + last = int(netaddr.IPAddress(range['last_ip'])) + if first <= ip <= last: + if first == last: + context.session.delete(range) + return + elif first == ip: + range['first_ip'] = str(netaddr.IPAddress(ip_address) + 1) + return + elif last == ip: + range['last_ip'] = str(netaddr.IPAddress(ip_address) - 1) + return + else: + # Split into two ranges + new_first = str(netaddr.IPAddress(ip_address) + 1) + new_last = range['last_ip'] + range['last_ip'] = str(netaddr.IPAddress(ip_address) - 1) + ip_range = models_v2.IPAvailabilityRange( + allocation_pool_id=range['allocation_pool_id'], + first_ip=new_first, + last_ip=new_last) + context.session.add(ip_range) + return ip_address + return None + + +def get_dns_by_subnet(context, subnet_id): + dns_qry = context.session.query(models_v2.DNSNameServer) + return dns_qry.filter_by(subnet_id=subnet_id).all() + + +def get_route_by_subnet(context, subnet_id): + route_qry = context.session.query(models_v2.SubnetRoute) + return route_qry.filter_by(subnet_id=subnet_id).all() + + +def get_subnets_by_network(context, network_id): + subnet_qry = context.session.query(models_v2.Subnet) + return subnet_qry.filter_by(network_id=network_id).all() + + +def _try_generate_ip(context, subnet): + """Generate an IP address. + + The IP address will be generated from one of the subnets defined on + the network. + """ + if type(subnet) is list: + subnet = subnet[0] + range_qry = context.session.query( + models_v2.IPAvailabilityRange).join( + models_v2.IPAllocationPool).with_lockmode('update') + range = range_qry.filter_by(subnet_id=subnet.id).first() + if not range: + LOG.debug(_("All IPs from subnet %(subnet_id)s (%(cidr)s) " + "allocated"), + {'subnet_id': subnet.id, + 'cidr': subnet.cidr}) + raise n_exc.IpAddressGenerationFailure( + net_id=subnet['network_id']) + ip_address = range['first_ip'] + LOG.debug(_("Allocated IP - %(ip_address)s from %(first_ip)s " + "to %(last_ip)s"), + {'ip_address': ip_address, + 'first_ip': range['first_ip'], + 'last_ip': range['last_ip']}) + if range['first_ip'] == range['last_ip']: + # No more free indices on subnet => delete + LOG.debug(_("No more free IP's in slice. Deleting allocation " + "pool.")) + context.session.delete(range) + else: + # increment the first free + range['first_ip'] = str(netaddr.IPAddress(ip_address) + 1) + return {'ip_address': ip_address, 'subnet_id': subnet.id} + + +def _rebuild_availability_ranges(context, subnets): + """Rebuild availability ranges. + + This method is called only when there's no more IP available or by + _update_subnet_allocation_pools. Calling + _update_subnet_allocation_pools before calling this function deletes + the IPAllocationPools associated with the subnet that is updating, + which will result in deleting the IPAvailabilityRange too. + """ + ip_qry = context.session.query( + models_v2.IPAllocation).with_lockmode('update') + # PostgreSQL does not support select...for update with an outer join. + # No join is needed here. + pool_qry = context.session.query( + models_v2.IPAllocationPool).options( + orm.noload('available_ranges')).with_lockmode('update') + for subnet in sorted(subnets): + LOG.debug(_("Rebuilding availability ranges for subnet %s") + % subnet) + + # Create a set of all currently allocated addresses + ip_qry_results = ip_qry.filter_by(subnet_id=subnet['id']) + allocations = netaddr.IPSet([netaddr.IPAddress(i['ip_address']) + for i in ip_qry_results]) + + for pool in pool_qry.filter_by(subnet_id=subnet['id']): + # Create a set of all addresses in the pool + poolset = netaddr.IPSet(netaddr.iter_iprange(pool['first_ip'], + pool['last_ip'])) + + # Use set difference to find free addresses in the pool + available = poolset - allocations + + # Generator compacts an ip set into contiguous ranges + def ipset_to_ranges(ipset): + first, last = None, None + for cidr in ipset.iter_cidrs(): + if last and last + 1 != cidr.first: + yield netaddr.IPRange(first, last) + first = None + first, last = first if first else cidr.first, cidr.last + if first: + yield netaddr.IPRange(first, last) + + # Write the ranges to the db + for ip_range in ipset_to_ranges(available): + available_range = models_v2.IPAvailabilityRange( + allocation_pool_id=pool['id'], + first_ip=str(netaddr.IPAddress(ip_range.first)), + last_ip=str(netaddr.IPAddress(ip_range.last))) + context.session.add(available_range) diff --git a/neutron/ipam/drivers/neutron_ipam.py b/neutron/ipam/drivers/neutron_ipam.py new file mode 100755 index 0000000..5df01ce --- /dev/null +++ b/neutron/ipam/drivers/neutron_ipam.py @@ -0,0 +1,449 @@ +# Copyright 2014 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.api.v2 import attributes +from neutron.common import constants +from neutron.common import exceptions as q_exc +from neutron.db import models_v2 +from neutron.ipam import base +from neutron.ipam.drivers import neutron_db +from neutron.openstack.common import log as logging +from neutron.openstack.common import uuidutils + +LOG = logging.getLogger(__name__) + + +class NeutronIPAMController(base.IPAMController): + def _make_allocation_pools(self, context, backend_subnet, subnet): + # Store information about allocation pools and ranges + for pool in subnet['allocation_pools']: + ip_pool = models_v2.IPAllocationPool(subnet=backend_subnet, + first_ip=pool['start'], + last_ip=pool['end']) + context.session.add(ip_pool) + ip_range = models_v2.IPAvailabilityRange( + ipallocationpool=ip_pool, + first_ip=pool['start'], + last_ip=pool['end']) + context.session.add(ip_range) + + def create_subnet(self, context, subnet): + tenant_id = self._get_tenant_id_for_create(context, subnet) + network = self._get_network(context, subnet['network_id']) + + # The 'shared' attribute for subnets is for internal plugin + # use only. It is not exposed through the API + args = {'tenant_id': tenant_id, + 'id': subnet.get('id') or uuidutils.generate_uuid(), + 'name': subnet['name'], + 'network_id': subnet['network_id'], + 'ip_version': subnet['ip_version'], + 'cidr': subnet['cidr'], + 'enable_dhcp': subnet['enable_dhcp'], + 'gateway_ip': subnet['gateway_ip'], + 'shared': network.shared} + if subnet['ip_version'] == 6 and subnet['enable_dhcp']: + if attributes.is_attr_set(subnet.get('ipv6_ra_mode')): + args['ipv6_ra_mode'] = subnet['ipv6_ra_mode'] + if attributes.is_attr_set(subnet.get('ipv6_address_mode')): + args['ipv6_address_mode'] = subnet['ipv6_address_mode'] + backend_subnet = models_v2.Subnet(**args) + + self._make_allocation_pools(context, backend_subnet, subnet) + context.session.add(backend_subnet) + + return self._make_subnet_dict(backend_subnet) + + def create_network(self, context, network): + return network + + def get_subnet_by_id(self, context, subnet_id): + return self._get_subnet(context, subnet_id) + + def update_subnet(self, context, subnet_id, subnet): + backend_subnet = self.get_subnet_by_id(context, subnet_id) + return backend_subnet + + def _make_subnet_dict(self, subnet, fields=None): + res = {'id': subnet['id'], + 'name': subnet['name'], + 'tenant_id': subnet['tenant_id'], + 'network_id': subnet['network_id'], + 'ip_version': subnet['ip_version'], + 'cidr': subnet['cidr'], + 'allocation_pools': [{'start': pool['first_ip'], + 'end': pool['last_ip']} + for pool in subnet['allocation_pools']], + 'gateway_ip': subnet['gateway_ip'], + 'enable_dhcp': subnet['enable_dhcp'], + 'ipv6_ra_mode': subnet['ipv6_ra_mode'], + 'ipv6_address_mode': subnet['ipv6_address_mode'], + 'dns_nameservers': [dns['address'] + for dns in subnet['dns_nameservers']], + 'host_routes': [{'destination': route['destination'], + 'nexthop': route['nexthop']} + for route in subnet['routes']], + 'shared': subnet['shared'] + } + # Call auxiliary extend functions, if any + self._apply_dict_extend_functions(attributes.SUBNETS, res, subnet) + + return self._fields(res, fields) + + def get_subnets(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + marker_obj = self._get_marker_obj(context, 'subnet', limit, marker) + return self._get_collection(context, models_v2.Subnet, + self._make_subnet_dict, + filters=filters, fields=fields, + sorts=sorts, + limit=limit, + marker_obj=marker_obj, + page_reverse=page_reverse) + + def get_subnets_count(self, context, filters=None): + return self._get_collection_count(context, models_v2.Subnet, + filters=filters) + + def delete_subnet(self, context, backend_subnet): + pass + + def force_off_ports(self, context, ports): + """Force Off ports on subnet delete event.""" + for port in ports: + query = (context.session.query(models_v2.Port). + enable_eagerloads(False).filter_by(id=port.id)) + if not context.is_admin: + query = query.filter_by(tenant_id=context.tenant_id) + + query.delete() + + def allocate_ip(self, context, subnet, host, ip=None): + if ip is not None and 'ip_address' in ip: + subnet_id = subnet['id'] + ip_address = {'subnet_id': subnet_id, + 'ip_address': ip['ip_address']} + neutron_db.allocate_specific_ip( + context, subnet_id, ip['ip_address']) + return ip_address + else: + subnets = [subnet] + return neutron_db.generate_ip(context, subnets) + + def deallocate_ip(self, context, backend_subnet, host, ip_address): + # IPAllocations are automatically handled by cascade deletion + pass + + def delete_network(self, context, network_id): + pass + + def set_dns_nameservers(self, context, port): + pass + + +class NeutronDHCPController(base.DHCPController): + def __init__(self, db_mgr=None): + if db_mgr is None: + db_mgr = neutron_db + + self.db_manager = db_mgr + + def configure_dhcp(self, context, backend_subnet, dhcp_params): + # Store information about DNS Servers + if dhcp_params['dns_nameservers'] is not attributes.ATTR_NOT_SPECIFIED: + for addr in dhcp_params['dns_nameservers']: + ns = models_v2.DNSNameServer(address=addr, + subnet_id=backend_subnet['id']) + context.session.add(ns) + backend_subnet['dns_nameservers'].append(addr) + + # Store host routes + if dhcp_params['host_routes'] is not attributes.ATTR_NOT_SPECIFIED: + for rt in dhcp_params['host_routes']: + route = models_v2.SubnetRoute( + subnet_id=backend_subnet['id'], + destination=rt['destination'], + nexthop=rt['nexthop']) + context.session.add(route) + backend_subnet['host_routes'].append({ + 'destination': rt['destination'], + 'nexthop': rt['nexthop']}) + + def reconfigure_dhcp(self, context, backend_subnet, dhcp_params): + changed_dns = False + new_dns = [] + if "dns_nameservers" in dhcp_params: + changed_dns = True + old_dns_list = self.db_manager.get_dns_by_subnet( + context, backend_subnet['id']) + new_dns_addr_set = set(dhcp_params["dns_nameservers"]) + old_dns_addr_set = set([dns['address'] + for dns in old_dns_list]) + + new_dns = list(new_dns_addr_set) + for dns_addr in old_dns_addr_set - new_dns_addr_set: + for dns in old_dns_list: + if dns['address'] == dns_addr: + context.session.delete(dns) + for dns_addr in new_dns_addr_set - old_dns_addr_set: + dns = models_v2.DNSNameServer( + address=dns_addr, + subnet_id=backend_subnet['id']) + context.session.add(dns) + + if len(dhcp_params['dns_nameservers']): + del dhcp_params['dns_nameservers'] + + def _combine(ht): + return ht['destination'] + "_" + ht['nexthop'] + + changed_host_routes = False + new_routes = [] + if "host_routes" in dhcp_params: + changed_host_routes = True + old_route_list = self.db_manager.get_route_by_subnet( + context, backend_subnet['id']) + + new_route_set = set([_combine(route) + for route in dhcp_params['host_routes']]) + + old_route_set = set([_combine(route) + for route in old_route_list]) + + for route_str in old_route_set - new_route_set: + for route in old_route_list: + if _combine(route) == route_str: + context.session.delete(route) + for route_str in new_route_set - old_route_set: + route = models_v2.SubnetRoute( + destination=route_str.partition("_")[0], + nexthop=route_str.partition("_")[2], + subnet_id=backend_subnet['id']) + context.session.add(route) + + # Gather host routes for result + for route_str in new_route_set: + new_routes.append( + {'destination': route_str.partition("_")[0], + 'nexthop': route_str.partition("_")[2]}) + del dhcp_params['host_routes'] + + backend_subnet.update(dhcp_params) + + result = {} + if changed_dns: + result['new_dns'] = new_dns + if changed_host_routes: + result['new_routes'] = new_routes + return result + + def bind_mac(self, context, backend_subnet, ip_address, mac_address): + pass + + def unbind_mac(self, context, backend_subnet, ip_address): + pass + + def dhcp_is_enabled(self, context, backend_subnet): + pass + + def disable_dhcp(self, context, backend_subnet): + pass + + def get_dhcp_ranges(self, context, backend_subnet): + pass + + +class NeutronDNSController(base.DNSController): + """DNS controller for standard neutron behavior is not implemented because + neutron does not provide that functionality + """ + def bind_names(self, context, backend_port): + pass + + def unbind_names(self, context, backend_port): + pass + + def create_dns_zones(self, context, backend_subnet): + pass + + def delete_dns_zones(self, context, backend_subnet): + pass + + def disassociate_floatingip(self, context, floatingip, port_id): + pass + + +class NeutronIPAM(base.IPAMManager): + def __init__(self, dhcp_controller=None, dns_controller=None, + ipam_controller=None, db_mgr=None): + # These should be initialized in derived DDI class + if dhcp_controller is None: + dhcp_controller = NeutronDHCPController() + + if dns_controller is None: + dns_controller = NeutronDNSController() + + if ipam_controller is None: + ipam_controller = NeutronIPAMController() + + if db_mgr is None: + db_mgr = neutron_db + + self.dns_controller = dns_controller + self.ipam_controller = ipam_controller + self.dhcp_controller = dhcp_controller + self.db_manager = db_mgr + + def create_subnet(self, context, subnet): + with context.session.begin(subtransactions=True): + # Allocate IP addresses. Create allocation pools only + backend_subnet = self.ipam_controller.create_subnet(context, + subnet) + self.dns_controller.create_dns_zones(context, backend_subnet) + # Configure DHCP + dhcp_params = subnet + self.dhcp_controller.configure_dhcp(context, backend_subnet, + dhcp_params) + + self.ipam_controller.get_subnet_by_id(context, + backend_subnet['id']) + return backend_subnet + + def update_subnet(self, context, subnet_id, subnet): + with context.session.begin(subtransactions=True): + backend_subnet = self.ipam_controller.update_subnet( + context, subnet_id, subnet) + + # Reconfigure DHCP for subnet + dhcp_params = subnet + dhcp_changes = self.dhcp_controller.reconfigure_dhcp( + context, backend_subnet, dhcp_params) + + return backend_subnet, dhcp_changes + + def delete_subnet(self, context, subnet): + if isinstance(subnet, models_v2.Subnet): + subnet_id = subnet.id + else: + subnet_id = subnet + + with context.session.begin(subtransactions=True): + backend_subnet = self.ipam_controller.get_subnet_by_id(context, + subnet_id) + subnet_ports = self.db_manager.get_subnet_ports(context, subnet_id) + + ports_to_remove = [port for port in subnet_ports if + neutron_db.get_subnets_by_port_id( + context, port.id) <= 1] + + has_ports_allocated = not all( + p.device_owner == constants.DEVICE_OWNER_DHCP + for p in subnet_ports) + + if has_ports_allocated: + raise q_exc.SubnetInUse(subnet_id=backend_subnet['id']) + + self.ipam_controller.force_off_ports(context, ports_to_remove) + self.dns_controller.delete_dns_zones(context, backend_subnet) + self.dhcp_controller.disable_dhcp(context, backend_subnet) + self.ipam_controller.delete_subnet(context, backend_subnet) + + return subnet_id + + def delete_subnets_by_network(self, context, network_id): + with context.session.begin(subtransactions=True): + subnets = neutron_db.get_subnets_by_network( + context, network_id) + for subnet in subnets: + self.delete_subnet(context, subnet['id']) + + def get_subnet_by_id(self, context, subnet_id): + with context.session.begin(subtransactions=True): + return self.ipam_controller.get_subnet_by_id(context, subnet_id) + + def allocate_ip(self, context, host, ip): + with context.session.begin(subtransactions=True): + subnet_id = ip.get('subnet_id', None) + if not subnet_id: + LOG.debug(_("ip object must have %(subnet_id)s") % subnet_id) + raise + backend_subnet = self.ipam_controller.get_subnet_by_id(context, + subnet_id) + ip_address = self.ipam_controller.allocate_ip( + context, + backend_subnet, + host, + ip) + + LOG.debug('IPAMManager allocate IP: %s' % ip_address) + mac_address = host['mac_address'] + self.dhcp_controller.bind_mac( + context, + backend_subnet, + ip_address, + mac_address) + return ip_address + + def deallocate_ip(self, context, host, ip): + with context.session.begin(subtransactions=True): + subnet_id = ip['subnet_id'] + ip_address = ip['ip_address'] + backend_subnet = self.ipam_controller.get_subnet_by_id( + context, subnet_id) + self.dhcp_controller.unbind_mac( + context, + backend_subnet, + ip_address) + self.ipam_controller.deallocate_ip( + context, + backend_subnet, + host, + ip_address) + + def get_subnets(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + return self.ipam_controller.get_subnets(context, filters, fields, + sorts, limit, marker, + page_reverse) + + def create_network(self, context, network): + return self.ipam_controller.create_network(context, network) + + def delete_network(self, context, network_id): + self.ipam_controller.delete_network( + context, network_id) + + def create_port(self, context, port): + self.dns_controller.bind_names(context, port) + if constants.DEVICE_OWNER_DHCP == port['device_owner']: + self.ipam_controller.set_dns_nameservers(context, port) + + def update_port(self, context, port): + self.dns_controller.bind_names(context, port) + + def delete_port(self, context, port): + self.dns_controller.unbind_names(context, port) + + def associate_floatingip(self, context, floatingip, port): + self.create_port(context, port) + + def disassociate_floatingip(self, context, floatingip, port_id): + self.dns_controller.disassociate_floatingip(context, floatingip, + port_id) + + def get_additional_network_dict_params(self, ctx, network_id): + return {} diff --git a/neutron/manager.py b/neutron/manager.py index 3a21f61..6986a34 100644 --- a/neutron/manager.py +++ b/neutron/manager.py @@ -123,6 +123,11 @@ class NeutronManager(object): # the rest of service plugins self.service_plugins = {constants.CORE: self.plugin} self._load_service_plugins() + # Load IPAMManager driver + ipam_driver_name = cfg.CONF.ipam_driver + LOG.info(_("Loading ipam driver: %s"), ipam_driver_name) + self.ipam = self._get_plugin_instance('neutron.ipam_drivers', + ipam_driver_name) def _get_plugin_instance(self, namespace, plugin_provider): try: @@ -223,3 +228,7 @@ class NeutronManager(object): # Return weakrefs to minimize gc-preventing references. return dict((x, weakref.proxy(y)) for x, y in cls.get_instance().service_plugins.iteritems()) + + @classmethod + def get_ipam(cls): + return cls.get_instance().ipam diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 9dcf6f8..1fb3160 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -650,6 +650,10 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, mech_context) self.type_manager.release_network_segments(session, id) + + manager.NeutronManager.get_ipam().delete_network( + context, id) + record = self._get_network(context, id) LOG.debug(_("Deleting network record %s"), record) session.delete(record) @@ -766,6 +770,9 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, self.mechanism_manager.delete_subnet_precommit( mech_context) + manager.NeutronManager.get_ipam().delete_subnet( + context, id) + LOG.debug(_("Deleting subnet record")) session.delete(record) diff --git a/neutron/tests/unit/agent/linux/test_dhcp_relay.py b/neutron/tests/unit/agent/linux/test_dhcp_relay.py new file mode 100644 index 0000000..576ad82 --- /dev/null +++ b/neutron/tests/unit/agent/linux/test_dhcp_relay.py @@ -0,0 +1,63 @@ +# Copyright 2014 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. +import mock + +from neutron.agent.linux import dhcp_relay + +from neutron.agent.common import config +from neutron.common import exceptions as exc +from neutron.tests import base + + +class DhcpDnsProxyTestCase(base.BaseTestCase): + def setUp(self): + super(DhcpDnsProxyTestCase, self).setUp() + + def _get_config(self): + conf = mock.MagicMock() + conf.interface_driver = "neutron.agent.linux.interface.NullDriver" + config.register_interface_driver_opts_helper(conf) + return conf + + def test_raises_exception_on_unset_config_options(self): + conf = self._get_config() + network = mock.Mock() + self.assertRaises(exc.InvalidConfigurationOption, + dhcp_relay.DhcpDnsProxy, conf, network) + + def test_implements_get_isolated_subnets(self): + network = mock.Mock() + conf = self._get_config() + conf.external_dhcp_servers = ['1.1.1.1'] + conf.external_dns_servers = ['1.1.1.2'] + + proxy = dhcp_relay.DhcpDnsProxy(conf, network) + try: + self.assertTrue(callable(proxy.get_isolated_subnets)) + except AttributeError as ex: + self.fail(ex) + + def test_implements_should_enable_metadata(self): + network = mock.Mock() + conf = self._get_config() + conf.external_dhcp_servers = ['1.1.1.1'] + conf.external_dns_servers = ['1.1.1.2'] + + proxy = dhcp_relay.DhcpDnsProxy(conf, network) + + try: + self.assertTrue(callable(proxy.should_enable_metadata)) + except AttributeError as ex: + self.fail(ex) diff --git a/neutron/tests/unit/ipam/__init__.py b/neutron/tests/unit/ipam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron/tests/unit/ipam/drivers/__init__.py b/neutron/tests/unit/ipam/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron/tests/unit/ipam/drivers/infoblox/__init__.py b/neutron/tests/unit/ipam/drivers/infoblox/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/neutron/tests/unit/ipam/drivers/infoblox/test_config.py b/neutron/tests/unit/ipam/drivers/infoblox/test_config.py new file mode 100755 index 0000000..1959efe --- /dev/null +++ b/neutron/tests/unit/ipam/drivers/infoblox/test_config.py @@ -0,0 +1,714 @@ +# Copyright 2014 OpenStack LLC. +# 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 io +import operator + +import mock +from testtools import matchers + +from neutron.db.infoblox import infoblox_db +from neutron.db.infoblox import models +from neutron.ipam.drivers.infoblox import config +from neutron.ipam.drivers.infoblox import exceptions +from neutron.ipam.drivers.infoblox import objects +from neutron.openstack.common import jsonutils +from neutron.tests import base + + +class ConfigFinderTestCase(base.BaseTestCase): + def test_config_reads_data_from_json(self): + valid_config = """ + [ + { + "condition": "tenant", + "is_external": false, + "network_view": "{tenant_id}", + "dhcp_members": "next-available-member", + "require_dhcp_relay": true, + "domain_suffix_pattern": "local.test.com", + "hostname_pattern": "%{instance_id}" + }, + { + "condition": "global", + "is_external": true, + "dhcp_members": "member1.infoblox.com", + "domain_suffix_pattern": "test.com", + "hostname_pattern": "%{instance_id}" + } + ] + """ + + subnet = { + 'network_id': 'some-net-id', + 'cidr': '192.168.1.0/24', + 'tenant_id': 'some-tenant-id' + } + context = mock.MagicMock() + + cfg = config.ConfigFinder(stream=io.BytesIO(valid_config), + member_manager=mock.Mock()) + cfg.configure_members = mock.Mock() + subnet_config = cfg.find_config_for_subnet(context, subnet) + + self.assertIsNotNone(subnet_config) + self.assertIsInstance(subnet_config, config.Config) + + def test_throws_error_on_invalid_configuration(self): + invalid_config = """ + [ + { + "condition": "tenant", + "is_external": false, + "network_view": "{tenant_id}", + "dhcp_members": "next-available-member", + "require_dhcp_relay": True, + "domain_suffix_pattern": "local.test.com", + "hostname_pattern": "{instance_id}" + }, + { + "condition": "global" + "is_external": true, + "dhcp_members": "member1.infoblox.com", + "domain_suffix_pattern": "test.com", + "hostname_pattern": "{instance_id}" + } + ] + """ + + # configuration is considered invalid if JSON parser has failed + self.assertRaises(exceptions.InfobloxConfigException, + config.ConfigFinder, + stream=io.BytesIO(invalid_config), + member_manager=mock.Mock()) + + @mock.patch('neutron.db.infoblox.infoblox_db.is_network_external') + def test_external_network_matches_first_external_config(self, is_external): + expected_condition = 'global' + external_config = """ + [ + {{ + "condition": "tenant", + "is_external": false, + "network_view": "{{tenant_id}}", + "dhcp_members": "next-available-member", + "require_dhcp_relay": true, + "domain_suffix_pattern": "local.test.com", + "hostname_pattern": "{{instance_id}}" + }}, + {{ + "condition": "{expected_condition:s}", + "is_external": true, + "dhcp_members": "member1.infoblox.com", + "domain_suffix_pattern": "test.com", + "hostname_pattern": "{{instance_id}}" + }}, + {{ + "condition": "tenant", + "is_external": true, + "dhcp_members": "member1.infoblox.com", + "domain_suffix_pattern": "test.com", + "hostname_pattern": "{{instance_id}}" + }} + ] + """.format(**locals()) + context = mock.Mock() + subnet = { + 'network_id': 'some-net-id', + 'cidr': '192.168.1.0/24', + 'tenant_id': 'some-tenant-id' + } + is_external.return_value = True + cfg = config.ConfigFinder(stream=io.BytesIO(external_config), + member_manager=mock.Mock()) + cfg._is_member_registered = mock.Mock(return_value=True) + config_for_subnet = cfg.find_config_for_subnet(context, subnet) + + self.assertThat(config_for_subnet.condition, + matchers.Equals(expected_condition)) + + @mock.patch('neutron.db.infoblox.infoblox_db.is_network_external') + def test_subnet_range_condition_matches(self, is_external): + expected_cidr = '10.0.0.0/24' + expected_condition = 'subnet_range:{0}'.format(expected_cidr) + external_config = """ + [ + {{ + "condition": "{expected_condition:s}", + "is_external": false, + "dhcp_members": "member1.infoblox.com", + "domain_suffix_pattern": "test.com", + "hostname_pattern": "{{instance_id}}" + }}, + {{ + "condition": "tenant", + "is_external": false, + "network_view": "{{tenant_id}}", + "dhcp_members": "next-available-member", + "require_dhcp_relay": true, + "domain_suffix_pattern": "local.test.com", + "hostname_pattern": "{{instance_id}}" + }}, + {{ + "condition": "tenant", + "is_external": true, + "dhcp_members": "member1.infoblox.com", + "domain_suffix_pattern": "test.com", + "hostname_pattern": "{{instance_id}}" + }} + ] + """.format(**locals()) + context = mock.Mock() + subnet = { + 'network_id': 'some-net-id', + 'cidr': expected_cidr, + 'tenant_id': 'some-tenant-id' + } + is_external.return_value = False + cfg = config.ConfigFinder(stream=io.BytesIO(external_config), + member_manager=mock.Mock()) + cfg._is_member_registered = mock.Mock(return_value=True) + config_for_subnet = cfg.find_config_for_subnet(context, subnet) + + self.assertThat(config_for_subnet.condition, + matchers.Equals(expected_condition)) + + @mock.patch('neutron.db.infoblox.infoblox_db.is_network_external') + def test_tenant_id_condition_matches(self, is_external_mock): + expected_tenant_id = 'some-tenant-id' + expected_condition = 'tenant_id:{0}'.format(expected_tenant_id) + tenant_id_conf = """ + [ + {{ + "condition": "{expected_condition:s}", + "is_external": false, + "dhcp_members": "member1.infoblox.com", + "domain_suffix_pattern": "test.com", + "hostname_pattern": "{{instance_id}}" + }}, + {{ + "condition": "global", + "is_external": false, + "network_view": "{{tenant_id}}", + "dhcp_members": "next-available-member", + "require_dhcp_relay": true, + "domain_suffix_pattern": "local.test.com", + "hostname_pattern": "{{instance_id}}" + }}, + {{ + "condition": "tenant", + "is_external": true, + "dhcp_members": "member1.infoblox.com", + "domain_suffix_pattern": "test.com", + "hostname_pattern": "{{instance_id}}" + }} + ] + """.format(**locals()) + context = mock.Mock() + subnet = { + 'network_id': 'some-net-id', + 'cidr': '10.0.0.0/24', + 'tenant_id': expected_tenant_id + } + is_external_mock.return_value = False + cfg = config.ConfigFinder(stream=io.BytesIO(tenant_id_conf), + member_manager=mock.Mock()) + cfg._is_member_registered = mock.Mock(return_value=True) + config_for_subnet = cfg.find_config_for_subnet(context, subnet) + + self.assertThat(config_for_subnet.condition, + matchers.Equals(expected_condition)) + + def test_raises_on_invalid_condition(self): + config_template = """ + [ + {{ + "condition": "{condition}", + "is_external": false, + "dhcp_members": "member1.infoblox.com", + "domain_suffix_pattern": "test.com", + "hostname_pattern": "{{instance_id}}" + }} + ] + """ + + member_manager = mock.Mock() + for valid in config.ConfigFinder.VALID_CONDITIONS: + valid_conf = config_template.format(condition=valid) + try: + config.ConfigFinder(io.BytesIO(valid_conf), member_manager) + except exceptions.InfobloxConfigException as e: + msg = 'Unexpected {error_type} for {config}'.format( + error_type=type(e), config=valid_conf) + self.fail(msg) + + invalid_cof = config_template.format(condition='invalid-condition') + self.assertRaises(exceptions.InfobloxConfigException, + config.ConfigFinder, io.BytesIO(invalid_cof), + member_manager) + + def test_raises_if_no_suitable_config_found(self): + cfg = """ + [ + { + "condition": "tenant_id:wrong-id", + "is_external": false + } + ] + """ + context = mock.MagicMock() + subnet = mock.MagicMock() + + cf = config.ConfigFinder(io.BytesIO(cfg), member_manager=mock.Mock()) + cf._is_member_registered = mock.Mock(return_value=True) + self.assertRaises(exceptions.InfobloxConfigException, + cf.find_config_for_subnet, context, subnet) + + +class ConfigTestCase(base.BaseTestCase): + def test_exception_raised_on_missing_condition(self): + context = mock.Mock() + subnet = mock.Mock() + + self.assertRaises(exceptions.InfobloxConfigException, + config.Config, {}, context, subnet, + member_manager=mock.Mock()) + + def test_default_values_are_set(self): + context = mock.Mock() + subnet = mock.Mock() + + cfg = config.Config({'condition': 'global'}, context, subnet, + member_manager=mock.Mock()) + + self.assertFalse(cfg.is_external) + self.assertFalse(cfg.require_dhcp_relay) + self.assertEqual(cfg.network_view, 'default') + self.assertEqual(cfg.dns_view, 'default') + self.assertIsNone(cfg.network_template) + self.assertIsNone(cfg.ns_group) + self.assertEqual(cfg.hostname_pattern, + 'host-{ip_address}.{subnet_name}') + self.assertEqual(cfg.domain_suffix_pattern, 'global.com') + + def test_dhcp_member_is_returned_as_is_if_explicitly_set(self): + context = mock.Mock() + subnet = mock.Mock() + + expected_member_name = 'some-dhcp-member.com' + expected_dhcp_member = [objects.Member(name=expected_member_name, + ip='some-ip')] + + conf_dict = { + 'condition': 'global', + 'dhcp_members': expected_member_name + } + + member_manager = mock.Mock() + member_manager.find_members.return_value = expected_dhcp_member + cfg = config.Config(conf_dict, context, subnet, member_manager) + + self.assertEqual(cfg.dhcp_members, expected_dhcp_member) + assert member_manager.find_members.called + assert not member_manager.next_available.called + assert not member_manager.reserve_member.called + + def test_dhcp_member_is_taken_from_member_config_if_next_available(self): + context = mock.Mock() + subnet = mock.Mock() + + expected_member = objects.Member('some-member-ip', 'some-member-name') + + member_manager = mock.Mock() + member_manager.find_members.return_value = None + member_manager.next_available.return_value = expected_member + + conf_dict = { + 'condition': 'global', + 'dhcp_members': config.Config.NEXT_AVAILABLE_MEMBER + } + + cfg = config.Config(conf_dict, context, subnet, member_manager) + members = cfg.reserve_dhcp_members() + self.assertEqual(members[0], expected_member) + assert member_manager.find_members.called_once + assert member_manager.next_available.called_once + assert member_manager.reserve_meber.called_once + + def test_dns_view_joins_net_view_with_default_if_not_default(self): + context = mock.Mock() + subnet = mock.Mock() + member_manager = mock.Mock() + + expected_net_view = 'non-default-net-view' + conf_dict = { + 'condition': 'global', + 'network_view': expected_net_view, + } + + cfg = config.Config(conf_dict, context, subnet, member_manager) + + self.assertTrue(cfg.dns_view.startswith('default')) + self.assertTrue(cfg.dns_view.endswith(expected_net_view)) + + def test_dns_view_is_default_if_netview_is_default(self): + context = mock.Mock() + subnet = mock.Mock() + member_manager = mock.Mock() + + conf_dict = { + 'condition': 'global', + 'network_view': 'default', + } + + cfg = config.Config(conf_dict, context, subnet, member_manager) + + self.assertThat(cfg.dns_view, matchers.Equals(cfg.network_view)) + + def test_configured_value_is_returned_for_dns_view(self): + context = mock.Mock() + subnet = mock.Mock() + member_manager = mock.Mock() + + expected_dns_view = 'some-dns-view' + conf_dict = { + 'condition': 'global', + 'dns_view': expected_dns_view + } + + cfg = config.Config(conf_dict, context, subnet, member_manager) + self.assertEqual(cfg.dns_view, expected_dns_view) + + def test_reserve_dns_members_always_returns_list(self): + configs = [ + { + 'dns_members': 'member1', + 'condition': 'global' + }, + { + 'dns_members': ['member1', 'member2'], + 'condition': 'global' + }, + { + 'condition': 'global' + } + ] + context = mock.Mock() + subnet = mock.Mock() + member_manager = mock.Mock() + + for conf in configs: + cfg = config.Config(conf, context, subnet, member_manager) + + reserved_members = cfg.reserve_dns_members() + + self.assertTrue(isinstance(reserved_members, list)) + + def test_reserve_members_list(self): + def mock_get_member(member_name): + return objects.Member(ip='10.20.30.40', name=member_name) + + conf = {'condition': 'global'} + members = ['member40.com', 'member41.com'] + + context = mock.Mock() + subnet = mock.Mock() + + member_manager = mock.Mock() + member_manager.get_member = mock_get_member + member_manager.find_members = mock.Mock(return_value=None) + + cfg = config.Config(conf, context, subnet, member_manager) + cfg._dhcp_members = members + reserved_members = cfg.reserve_dhcp_members() + + self.assertEqual( + reserved_members, + [ + objects.Member(ip='10.20.30.40', name='member40.com'), + objects.Member(ip='10.20.30.40', name='member41.com') + ] + ) + + def test_subnet_update_not_allowed_if_subnet_name_is_in_pattern(self): + context = mock.Mock() + subnet = mock.Mock() + subnet_new = mock.Mock() + member_manager = mock.Mock() + + cfg = { + 'condition': 'global', + 'hostname_pattern': 'host-{ip_address}', + 'domain_suffix_pattern': '{subnet_name}.global.com' + } + + conf = config.Config(cfg, context, subnet, member_manager) + self.assertRaises(exceptions.OperationNotAllowed, + conf.verify_subnet_update_is_allowed, subnet_new) + + def test_subnet_update_is_allowed_if_subnet_name_is_not_in_pattern(self): + context = mock.Mock() + subnet = mock.Mock() + subnet_new = mock.Mock() + member_manager = mock.Mock() + + allowed_suffixes = [ + 'network_id', + 'subnet_id', + 'user_id', + 'tenant_id', + 'ip_address', + 'network_name', + 'instance_id' + ] + + for allowed_suffix in allowed_suffixes: + domain_pattern = '{{0}}.global.com'.format(allowed_suffix) + + cfg = { + 'condition': 'global', + 'hostname_pattern': 'host-{ip_address}', + 'domain_suffix_pattern': domain_pattern + } + + conf = config.Config(cfg, context, subnet, member_manager) + try: + conf.verify_subnet_update_is_allowed(subnet_new) + except exceptions.OperationNotAllowed as e: + self.fail('Unexpected exception {}'.format(e)) + + def test_same_configs_are_equal(self): + member_manager = mock.Mock() + context = mock.Mock() + subnet = mock.Mock() + + cfg = { + 'condition': 'global', + 'network_view': 'netview', + 'dns_view': 'dnsview', + 'dhcp_members': ['m1', 'm2', 'm3'], + 'dns_members': ['m1', 'm2', 'm3'] + } + + c1 = config.Config(cfg, context, subnet, member_manager) + c2 = config.Config(cfg, context, subnet, member_manager) + + self.assertTrue(c1 == c1) + self.assertTrue(c1 == c2) + self.assertTrue(c2 == c1) + self.assertTrue(c2 == c2) + self.assertTrue(c1 != object()) + self.assertTrue(c2 != object()) + + def test_same_configs_are_not_added_to_set(self): + member_manager = mock.Mock() + context = mock.Mock() + subnet = mock.Mock() + + cfg = { + 'condition': 'global', + 'network_view': 'netview', + 'dns_view': 'dnsview', + 'dhcp_members': ['m1', 'm2', 'm3'], + 'dns_members': ['m1', 'm2', 'm3'] + } + + s = set() + + for _ in xrange(10): + s.add(config.Config(cfg, context, subnet, member_manager)) + + self.assertEqual(len(s), 1) + + cfg['condition'] = 'tenant' + s.add(config.Config(cfg, context, subnet, member_manager)) + self.assertEqual(len(s), 2) + + s.add(config.Config(cfg, context, subnet, member_manager)) + self.assertEqual(len(s), 2) + + +class MemberManagerTestCase(base.BaseTestCase): + def test_raises_error_if_no_config_file(self): + self.assertRaises(exceptions.InfobloxConfigException, + config.MemberManager) + + def test_returns_next_available_member(self): + context = mock.MagicMock() + member_config = [{"name": "member%d" % i, + "member_name": "member%d" % i, + "ipv4addr": "192.168.1.%d" % i, + "ipv6addr": "2001:DB8::%s" % i} + for i in xrange(1, 5)] + + avail_members = [] + for i in xrange(1, 5): + member = type('Member', (object,), {}) + member.member_name = "member%d" % i + member.member_type = models.DHCP_MEMBER_TYPE + avail_members.append(member) + + avail_member = avail_members[0] + + mm = config.MemberManager(io.BytesIO(jsonutils.dumps(member_config))) + + with mock.patch.object(infoblox_db, 'get_available_member', + mock.Mock(return_value=avail_member)): + available_member = mm.next_available(context, models.DHCP_MEMBER_TYPE) + self.assertIn(available_member.ip, + map(operator.itemgetter('ipv4addr'), member_config)) + + def test_raises_no_member_available_if_all_members_used(self): + context = mock.MagicMock() + member_config = [{"name": "member%d" % i, + "ipv4addr": "192.168.1.%d" % i, + "ipv6addr": "2001:DB8::%s" % i} + for i in xrange(1, 5)] + + used_members = [member_config[i]['name'] + for i in xrange(len(member_config))] + + mm = config.MemberManager(io.BytesIO(jsonutils.dumps(member_config))) + + with mock.patch.object(infoblox_db, 'get_used_members', + mock.Mock(return_value=used_members)): + self.assertRaises(exceptions.InfobloxConfigException, + mm.next_available, + context, + models.DHCP_MEMBER_TYPE) + + def test_reserve_member_stores_member_in_db(self): + context = mock.Mock() + mapping = 'some-mapping-value' + member_name = 'member1' + member_config = [{"name": member_name, + "ipv4addr": "192.168.1.1", + "ipv6addr": "2001:DB8::1"}] + + mm = config.MemberManager(io.BytesIO(jsonutils.dumps(member_config))) + + with mock.patch.object(infoblox_db, 'attach_member') as attach_mock: + mm.reserve_member(context, mapping, member_name, + models.DHCP_MEMBER_TYPE) + + attach_mock.assert_called_once_with( + context, mapping, member_name, models.DHCP_MEMBER_TYPE) + + def test_finds_member_for_mapping(self): + context = mock.Mock() + mapping = 'some-mapping-value' + expected_member_name = 'member1' + expected_ip = '10.0.0.1' + expected_ipv6 = '2001:DB8::3' + expected_map_id = None + + expected_member = type('Member', (object,), {}) + expected_member.member_name = expected_member_name + expected_member.map_id = expected_map_id + expected_member.member_type = models.DHCP_MEMBER_TYPE + + expected_members = [expected_member] + + mm = config.MemberManager( + io.BytesIO(jsonutils.dumps([{'name': expected_member_name, + 'ipv4addr': expected_ip, + 'ipv6addr': expected_ipv6, + 'map_id': expected_map_id}]))) + + with mock.patch.object(infoblox_db, 'get_members') as get_mock: + get_mock.return_value = expected_members + members = mm.find_members(context, mapping, + models.DHCP_MEMBER_TYPE) + + self.assertEqual(expected_ip, members[0].ip) + self.assertEqual(expected_members[0].member_name, + members[0].name) + + def test_builds_member_from_config(self): + ip = 'some-ip' + name = 'some-name' + + mm = config.MemberManager( + io.BytesIO(jsonutils.dumps([{'name': name, + 'ipv4addr': ip, + 'ipv6addr': ip}]))) + + m = mm.get_member(name) + + self.assertEqual(m.name, name) + self.assertEqual(m.ip, ip) + + def test_raises_member_not_available_if_member_is_not_in_config(self): + ip = 'some-ip' + actual_name = 'some-name' + search_for_name = 'some-other-name' + + mm = config.MemberManager( + io.BytesIO(jsonutils.dumps([{'name': actual_name, + 'ipv4addr': ip, + 'ipv6addr': ip}]))) + + self.assertRaises(exceptions.InfobloxConfigException, mm.get_member, + search_for_name) + + def test_member_marked_as_unavailable(self): + expected_ip = "192.168.1.2" + expected_ipv6 = "2001:DB8::3" + expected_name = "available_member" + member_config = [{"name": expected_name, + "ipv4addr": expected_ip, + "ipv6addr": expected_ipv6}, + {"name": "unavailable_member", + "ipv4addr": "192.168.1.3", + "ipv6addr": "2001:DB8::3", + "is_available": False}] + + expected_member = objects.Member(ip=expected_ip, name=expected_name) + mm = config.MemberManager(io.BytesIO(jsonutils.dumps(member_config))) + + self.assertEqual(1, len(mm.configured_members)) + self.assertEqual(expected_member, mm.configured_members[0]) + + def test_config_mismatch_tenant_id_value(self): + valid_config = """ + [ + { + "condition": "tenant_id" + } + ] + """ + + self.assertRaises( + exceptions.InfobloxConfigException, + config.ConfigFinder, + stream=io.BytesIO(valid_config), + member_manager=mock.Mock() + ) + + def test_config_mismatch_subnet_range_value(self): + valid_config = """ + [ + { + "condition": "subnet_range" + } + ] + """ + + self.assertRaises( + exceptions.InfobloxConfigException, + config.ConfigFinder, + stream=io.BytesIO(valid_config), + member_manager=mock.Mock() + ) diff --git a/neutron/tests/unit/ipam/drivers/infoblox/test_connector.py b/neutron/tests/unit/ipam/drivers/infoblox/test_connector.py new file mode 100755 index 0000000..e6e4dcf --- /dev/null +++ b/neutron/tests/unit/ipam/drivers/infoblox/test_connector.py @@ -0,0 +1,266 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2014 OpenStack LLC. +# 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 mock +import requests +from requests import exceptions as req_exc + +from neutron.ipam.drivers.infoblox import connector +from neutron.ipam.drivers.infoblox import exceptions +from neutron.tests import base + + +valid_config = mock.Mock() +valid_config.infoblox_wapi = 'http://localhost' +valid_config.infoblox_username = 'user' +valid_config.infoblox_password = 'pass' +valid_config.infoblox_sslverify = False +valid_config.infoblox_http_max_retries = 3 + + +class UrlMatcher(object): + def __init__(self, url, obj): + self.url = url + self.obj = obj + + def __eq__(self, actual_url): + return self.url in actual_url and self.obj in actual_url + + +class TestInfobloxConnector(base.BaseTestCase): + def setUp(self): + super(TestInfobloxConnector, self).setUp() + self.config(infoblox_wapi='https://infoblox.example.org/wapi/v1.1/') + self.config(infoblox_username='admin') + self.config(infoblox_password='password') + self.connector = connector.Infoblox() + + def test_throws_error_on_username_not_set(self): + fake_conf = mock.Mock() + fake_conf.infoblox_wapi = 'http://localhost' + fake_conf.infoblox_username = None + fake_conf.infoblox_password = 'password' + + with mock.patch.object(connector.cfg, 'CONF', fake_conf): + self.assertRaises(exceptions.InfobloxIsMisconfigured, + connector.Infoblox) + + def test_throws_error_on_password_not_set(self): + fake_conf = mock.Mock() + fake_conf.infoblox_wapi = 'http://localhost' + fake_conf.infoblox_username = 'user' + fake_conf.infoblox_password = None + + with mock.patch.object(connector.cfg, 'CONF', fake_conf): + self.assertRaises(exceptions.InfobloxIsMisconfigured, + connector.Infoblox) + + def test_throws_error_on_wapi_url_not_set(self): + fake_conf = mock.Mock() + fake_conf.infoblox_wapi = None + fake_conf.infoblox_username = 'user' + fake_conf.infoblox_password = 'pass' + + with mock.patch.object(connector.cfg, 'CONF', fake_conf): + self.assertRaises(exceptions.InfobloxIsMisconfigured, + connector.Infoblox) + + @mock.patch.object(connector.cfg, 'CONF', valid_config) + def test_create_object(self): + objtype = 'network' + payload = {'ip': '0.0.0.0'} + + with mock.patch.object(requests.Session, 'post', + return_value=mock.Mock()) as patched_create: + patched_create.return_value.status_code = 201 + patched_create.return_value.content = '{}' + self.connector.create_object(objtype, payload) + patched_create.assert_called_once_with( + 'https://infoblox.example.org/wapi/v1.1/network', + data='{"ip": "0.0.0.0"}', + headers={'Content-type': 'application/json'}, + timeout=60, + verify=False + ) + + def test_create_object_with_extattrs(self): + objtype = 'network' + payload = {'ip': '0.0.0.0', + 'extattrs': {'Subnet ID': {'value': 'fake_subnet_id'}}} + with mock.patch.object(requests.Session, 'post', + return_value=mock.Mock()) as patched_create: + patched_create.return_value.status_code = 201 + patched_create.return_value.content = '{}' + self.connector.create_object(objtype, payload) + patched_create.assert_called_once_with( + 'https://infoblox.example.org/wapi/v1.1/network', + data='{"ip": "0.0.0.0", "extattrs": {"Subnet ID":' + ' {"value": "fake_subnet_id"}}}', + headers={'Content-type': 'application/json'}, + timeout=60, + verify=False + ) + + @mock.patch.object(connector.cfg, 'CONF', valid_config) + def test_get_object(self): + objtype = 'network' + payload = {'ip': '0.0.0.0'} + + with mock.patch.object(requests.Session, 'get', + return_value=mock.Mock()) as patched_get: + patched_get.return_value.status_code = 200 + patched_get.return_value.content = '{}' + self.connector.get_object(objtype, payload) + patched_get.assert_called_once_with( + 'https://infoblox.example.org/wapi/v1.1/network?ip=0.0.0.0', + headers={'Content-type': 'application/json'}, + timeout=60, + verify=False + ) + + def test_get_object_in_cloud(self): + self.config(infoblox_wapi='https://infoblox.example.org/wapi/v2.0/') + self.connector = connector.Infoblox() + + objtype = 'network' + payload = {'ip': '0.0.0.0'} + + with mock.patch.object(requests.Session, 'get', + return_value=mock.Mock()) as patched_get: + patched_get.return_value.status_code = 200 + patched_get.return_value.content = '{"network": "my-network"}' + self.connector.get_object(objtype, payload) + patched_get.assert_called_once_with( + 'https://infoblox.example.org/wapi/v2.0/network?' + 'ip=0.0.0.0&_proxy_search=GM', + headers={'Content-type': 'application/json'}, + timeout=60, + verify=False + ) + + def test_get_objects_with_extattrs_in_cloud(self): + self.config(infoblox_wapi='https://infoblox.example.org/wapi/v2.0/') + self.connector = connector.Infoblox() + + objtype = 'network' + payload = {'ip': '0.0.0.0'} + extattrs = { + 'Subnet ID': {'value': 'fake_subnet_id'} + } + with mock.patch.object(requests.Session, 'get', + return_value=mock.Mock()) as patched_get: + patched_get.return_value.status_code = 200 + patched_get.return_value.content = '{"network": "my-network"}' + self.connector.get_object(objtype, payload, extattrs=extattrs) + patched_get.assert_called_once_with( + 'https://infoblox.example.org/wapi/v2.0/network?' + '*Subnet ID=fake_subnet_id&ip=0.0.0.0&_proxy_search=GM', + headers={'Content-type': 'application/json'}, + timeout=60, + verify=False + ) + + def test_get_objects_with_extattrs(self): + objtype = 'network' + payload = {'ip': '0.0.0.0'} + extattrs = { + 'Subnet ID': {'value': 'fake_subnet_id'} + } + with mock.patch.object(requests.Session, 'get', + return_value=mock.Mock()) as patched_get: + patched_get.return_value.status_code = 200 + patched_get.return_value.content = '{}' + self.connector.get_object(objtype, payload, extattrs=extattrs) + patched_get.assert_called_once_with( + 'https://infoblox.example.org/wapi/' + 'v1.1/network?*Subnet ID=fake_subnet_id&ip=0.0.0.0', + headers={'Content-type': 'application/json'}, + timeout=60, + verify=False + ) + + @mock.patch.object(connector.cfg, 'CONF', valid_config) + def test_update_object(self): + ref = 'network' + payload = {'ip': '0.0.0.0'} + + with mock.patch.object(requests.Session, 'put', + return_value=mock.Mock()) as patched_update: + patched_update.return_value.status_code = 200 + patched_update.return_value.content = '{}' + self.connector.update_object(ref, payload) + patched_update.assert_called_once_with( + 'https://infoblox.example.org/wapi/v1.1/network', + data='{"ip": "0.0.0.0"}', + headers={'Content-type': 'application/json'}, + timeout=60, + verify=False + ) + + @mock.patch.object(connector.cfg, 'CONF', valid_config) + def test_delete_object(self): + ref = 'network' + with mock.patch.object(requests.Session, 'delete', + return_value=mock.Mock()) as patched_delete: + patched_delete.return_value.status_code = 200 + patched_delete.return_value.content = '{}' + self.connector.delete_object(ref) + patched_delete.assert_called_once_with( + 'https://infoblox.example.org/wapi/v1.1/network', + timeout=60, + verify=False + ) + + def test_neutron_exception_is_raised_on_any_request_error(self): + # timeout exception raises InfobloxTimeoutError + f = mock.Mock() + f.__name__ = 'mock' + f.side_effect = req_exc.Timeout + self.assertRaises(exceptions.InfobloxTimeoutError, + connector.reraise_neutron_exception(f)) + + # all other request exception raises InfobloxConnectionError + supported_exceptions = [req_exc.HTTPError, + req_exc.ConnectionError, + req_exc.ProxyError, + req_exc.SSLError, + req_exc.TooManyRedirects, + req_exc.InvalidURL] + + for exc in supported_exceptions: + f.side_effect = exc + self.assertRaises(exceptions.InfobloxConnectionError, + connector.reraise_neutron_exception(f)) + + def test_non_cloud_api_detection(self): + wapi_not_cloud = ('https://infoblox.example.org/wapi/v1.4.1/', + 'https://infoblox.example.org/wapi/v1.9/', + 'https://wapi.wapi.wap/wapi/v1.99/') + + for url in wapi_not_cloud: + print url + self.assertFalse(self.connector.is_cloud_wapi(url)) + + def test_cloud_api_detection(self): + wapi_cloud = ('https://infoblox.example.org/wapi/v2.1/', + 'https://infoblox.example.org/wapi/v2.0/', + 'https://wapi.wapi.wap/wapi/v2.0.1/', + 'https://wapi.wapi.wap/wapi/v3.0/', + 'https://wapi.wapi.wap/wapi/v11.0.1/') + + for url in wapi_cloud: + self.assertTrue(self.connector.is_cloud_wapi(url)) diff --git a/neutron/tests/unit/ipam/drivers/infoblox/test_dns_controller.py b/neutron/tests/unit/ipam/drivers/infoblox/test_dns_controller.py new file mode 100755 index 0000000..2d3bcb6 --- /dev/null +++ b/neutron/tests/unit/ipam/drivers/infoblox/test_dns_controller.py @@ -0,0 +1,355 @@ +# Copyright 2014 OpenStack LLC. +# 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 mock + +import taskflow.engines + +from neutron.common import constants as neutron_constants +from neutron.db.infoblox import infoblox_db +from neutron.ipam.drivers.infoblox import dns_controller +from neutron.ipam.drivers.infoblox import infoblox_ipam +from neutron.db.infoblox import infoblox_db +from neutron.plugins.common import constants as plugins_constants +from neutron.tests import base + + +class SubstringMatcher(object): + def __init__(self, names): + if not isinstance(names, list): + names = [names] + self.names = names + + def __eq__(self, expected): + return all([name in expected for name in self.names]) + + +class DnsControllerTestCase(base.BaseTestCase): + fip_netview_name = 'my-test-fip-netview-name' + + def setUp(self): + super(DnsControllerTestCase, self).setUp() + self.manip = mock.Mock() + self.context = mock.Mock() + self.port = mock.MagicMock() + config_finder = mock.MagicMock() + expected_value = 'some-expected-value' + + def port_dict(item): + if item == 'fixed_ips': + return [{'ip_address': 'some-ip', + 'subnet_id': 'some-id'}] + + return expected_value + self.port.__getitem__.side_effect = port_dict + + subnet = {'network_id': 'some-net-id', + 'name': 'some-dns'} + + self.ip_allocator = mock.Mock() + + self.dns_ctrlr = dns_controller.InfobloxDNSController( + self.ip_allocator, self.manip, config_finder=config_finder) + self.dns_ctrlr.ea_manager = mock.Mock() + self.dns_ctrlr.pattern_builder = mock.Mock() + self.dns_ctrlr._get_subnet = mock.Mock() + self.dns_ctrlr._get_subnet.return_value = subnet + + def test_bind_host_names_binds_fqdn_with_ip_in_dns_view(self): + self.dns_ctrlr.bind_names(self.context, self.port) + assert self.ip_allocator.bind_names.called_once + + def test_unbind_host_names_binds_fqdn_with_ip_in_dns_view(self): + self.dns_ctrlr.unbind_names(self.context, self.port) + assert self.ip_allocator.unbind_names.called_once + + def test_restarts_services_on_bind(self): + self.dns_ctrlr.bind_names(self.context, self.port) + assert self.manip.restart_all_services.called_once + + def test_restarts_services_on_unbind(self): + self.dns_ctrlr.unbind_names(self.context, self.port) + assert self.manip.restart_all_services.called_once + + def test_get_hostname_pattern_dhcp_port(self): + port = {'device_owner': neutron_constants.DEVICE_OWNER_DHCP} + result = self.dns_ctrlr.get_hostname_pattern(port, mock.Mock()) + self.assertEqual('dhcp-port-{ip_address}', result) + + def test_get_hostname_pattern_router_iface(self): + port = {'device_owner': neutron_constants.DEVICE_OWNER_ROUTER_INTF} + result = self.dns_ctrlr.get_hostname_pattern(port, mock.Mock()) + self.assertEqual('router-iface-{ip_address}', result) + + def test_get_hostname_pattern_router_gw(self): + port = {'device_owner': neutron_constants.DEVICE_OWNER_ROUTER_GW} + result = self.dns_ctrlr.get_hostname_pattern(port, mock.Mock()) + self.assertEqual('router-gw-{ip_address}', result) + + def test_get_hostname_pattern_lb_vip(self): + port = {'device_owner': 'neutron:' + plugins_constants.LOADBALANCER} + result = self.dns_ctrlr.get_hostname_pattern(port, mock.Mock()) + self.assertEqual('lb-vip-{ip_address}', result) + + def test_get_hostname_pattern_instance_port(self): + port = {'device_owner': 'nova:compute'} + cfg_mock = mock.MagicMock() + cfg_mock.hostname_pattern = 'host-{ip_address}' + + result = self.dns_ctrlr.get_hostname_pattern(port, cfg_mock) + self.assertEqual('host-{ip_address}', result) + + +class GenericDNSControllerTestCase(base.BaseTestCase): + def test_fqdn_is_built_with_prefix_ip_address_and_dns_zone(self): + ip_address = '192.168.1.1' + prefix = 'host-' + zone = 'some.dns.zone' + + fqdn = dns_controller.build_fqdn(prefix, zone, ip_address) + + self.assertTrue(fqdn.startswith(prefix)) + self.assertTrue(fqdn.endswith(zone)) + self.assertIn(ip_address.replace('.', '-'), fqdn) + + def test_no_exception_if_subnet_has_no_nameservers_defined(self): + subnet = {} + nss = dns_controller.get_nameservers(subnet) + self.assertTrue(nss == []) + + subnet = {'dns_nameservers': object()} + nss = dns_controller.get_nameservers(subnet) + self.assertTrue(nss == []) + + subnet = {'dns_nameservers': []} + nss = dns_controller.get_nameservers(subnet) + self.assertTrue(nss == []) + + +class DomainZoneTestCase(base.BaseTestCase): + def test_two_dns_zones_created_on_create_dns_zone(self): + manip = mock.Mock() + context = infoblox_ipam.FlowContext(mock.Mock(), 'create-dns') + subnet = {'network_id': 'some-id', + 'name': 'some-name', + 'ip_version': 4, + 'cidr': '10.100.0.0/24'} + expected_member = 'member-name' + + ip_allocator = mock.Mock() + config_finder = mock.Mock() + + cfg = mock.Mock() + cfg.ns_group = None + cfg.reserve_dns_members.return_value = [expected_member] + + config_finder.find_config_for_subnet.return_value = cfg + + dns_ctrlr = dns_controller.InfobloxDNSController( + ip_allocator, manip, config_finder) + dns_ctrlr.pattern_builder = mock.Mock() + dns_ctrlr.create_dns_zones(context, subnet) + + taskflow.engines.run(context.parent_flow, store=context.store) + + assert (manip.method_calls == + [mock.call.create_dns_zone(mock.ANY, + mock.ANY, + expected_member, + mock.ANY, + zone_extattrs=mock.ANY), + mock.call.create_dns_zone(mock.ANY, + mock.ANY, + expected_member, + mock.ANY, + prefix=mock.ANY, + zone_extattrs=mock.ANY, + zone_format=mock.ANY)]) + + def test_secondary_dns_members(self): + manip = mock.Mock() + context = infoblox_ipam.FlowContext(mock.Mock(), 'create-dns') + subnet = {'network_id': 'some-id', + 'name': 'some-name', + 'ip_version': 4, + 'cidr': '10.100.0.0/24'} + primary_dns_member = 'member-primary' + secondary_dns_members = ['member-secondary'] + + ip_allocator = mock.Mock() + config_finder = mock.Mock() + + cfg = mock.Mock() + cfg.ns_group = None + cfg.reserve_dns_members.return_value = ([primary_dns_member] + + secondary_dns_members) + + config_finder.find_config_for_subnet.return_value = cfg + + dns_ctrlr = dns_controller.InfobloxDNSController( + ip_allocator, manip, config_finder) + dns_ctrlr.pattern_builder = mock.Mock() + dns_ctrlr.create_dns_zones(context, subnet) + + taskflow.engines.run(context.parent_flow, store=context.store) + + assert (manip.method_calls == + [mock.call.create_dns_zone(mock.ANY, + mock.ANY, + primary_dns_member, + secondary_dns_members, + zone_extattrs=mock.ANY), + mock.call.create_dns_zone(mock.ANY, + mock.ANY, + primary_dns_member, + secondary_dns_members, + prefix=mock.ANY, + zone_extattrs=mock.ANY, + zone_format=mock.ANY)]) + + def test_prefix_for_classless_networks(self): + manip = mock.Mock() + context = infoblox_ipam.FlowContext(mock.Mock(), 'create-dns') + subnet = {'network_id': 'some-id', + 'name': 'some-name', + 'ip_version': 4, + 'cidr': '192.168.0.128/27'} + + ip_allocator = mock.Mock() + config_finder = mock.Mock() + + cfg = mock.Mock() + cfg.ns_group = None + cfg.reserve_dns_members.return_value = (['some-member']) + + config_finder.find_config_for_subnet.return_value = cfg + + dns_ctrlr = dns_controller.InfobloxDNSController( + ip_allocator, manip, config_finder) + dns_ctrlr.pattern_builder = mock.Mock() + dns_ctrlr.create_dns_zones(context, subnet) + + taskflow.engines.run(context.parent_flow, store=context.store) + + assert (manip.method_calls == + [mock.call.create_dns_zone(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + zone_extattrs=mock.ANY), + mock.call.create_dns_zone(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + prefix=subnet['name'], + zone_extattrs=mock.ANY, + zone_format=mock.ANY)]) + + def test_prefix_for_classfull_networks(self): + manip = mock.Mock() + context = infoblox_ipam.FlowContext(mock.Mock(), 'create-dns') + subnet = {'network_id': 'some-id', + 'name': 'some-name', + 'ip_version': 4, + 'cidr': '192.168.0.0/24'} + + ip_allocator = mock.Mock() + config_finder = mock.Mock() + + cfg = mock.Mock() + cfg.ns_group = None + cfg.reserve_dns_members.return_value = (['some-member']) + + config_finder.find_config_for_subnet.return_value = cfg + + dns_ctrlr = dns_controller.InfobloxDNSController( + ip_allocator, manip, config_finder) + dns_ctrlr.pattern_builder = mock.Mock() + dns_ctrlr.create_dns_zones(context, subnet) + + taskflow.engines.run(context.parent_flow, store=context.store) + + assert (manip.method_calls == + [mock.call.create_dns_zone(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + zone_extattrs=mock.ANY), + mock.call.create_dns_zone(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + prefix=None, + zone_extattrs=mock.ANY, + zone_format=mock.ANY) + ]) + + @mock.patch.object(infoblox_db, 'is_network_external', + mock.Mock()) + def test_two_dns_zones_deleted_when_not_using_global_dns_zone(self): + manip = mock.Mock() + context = mock.Mock() + subnet = {'network_id': 'some-id', + 'cidr': 'some-cidr', + 'name': 'some-name'} + expected_dns_view = 'some-expected-dns-view' + + cfg = mock.Mock() + cfg.is_global_config = False + cfg.domain_suffix_pattern = '{subnet_name}.cloud.com' + cfg.dns_view = expected_dns_view + + ip_allocator = mock.Mock() + config_finder = mock.Mock() + config_finder.find_config_for_subnet.return_value = cfg + dns_ctrlr = dns_controller.InfobloxDNSController( + ip_allocator, manip, config_finder) + dns_ctrlr.pattern_builder = mock.Mock() + + network = {'id': 'some-net-id', + 'shared': False} + dns_ctrlr._get_network = mock.Mock() + dns_ctrlr._get_network.return_value = network + infoblox_db.is_network_external.return_value = False + + dns_ctrlr.delete_dns_zones(context, subnet) + + assert manip.method_calls == [ + mock.call.delete_dns_zone(expected_dns_view, mock.ANY), + mock.call.delete_dns_zone(expected_dns_view, subnet['cidr']) + ] + + def test_no_dns_zone_is_deleted_when_global_dns_zone_used(self): + manip = mock.Mock() + context = mock.Mock() + subnet = {'network_id': 'some-id', + 'cidr': 'some-cidr', + 'name': 'some-name'} + expected_dns_view = 'some-expected-dns-view' + + cfg = mock.Mock() + cfg.is_global_config = True + cfg.domain_suffix_pattern = '{subnet_name}.cloud.com' + cfg.dns_view = expected_dns_view + + ip_allocator = mock.Mock() + config_finder = mock.Mock() + config_finder.find_config_for_subnet.return_value = cfg + dns_ctrlr = dns_controller.InfobloxDNSController( + ip_allocator, manip, config_finder) + dns_ctrlr.pattern_builder = mock.Mock() + dns_ctrlr.delete_dns_zones(context, subnet) + + assert not manip.delete_dns_zone.called diff --git a/neutron/tests/unit/ipam/drivers/infoblox/test_ip_allocator.py b/neutron/tests/unit/ipam/drivers/infoblox/test_ip_allocator.py new file mode 100755 index 0000000..2748c0b --- /dev/null +++ b/neutron/tests/unit/ipam/drivers/infoblox/test_ip_allocator.py @@ -0,0 +1,115 @@ +# Copyright 2014 OpenStack LLC. +# 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 mock + +from neutron.ipam.drivers.infoblox import ip_allocator +from neutron.tests import base + + +class FixedAddressAllocatorTestCase(base.BaseTestCase): + def setUp(self): + super(FixedAddressAllocatorTestCase, self).setUp() + self.ib_mock = mock.Mock() + + self.extattrs = 'test-extattrs' + self.netview = 'some-test-net-view' + self.mac = 'de:ad:be:ef:00:00' + self.ip = '192.168.1.1' + self.dnsview = 'some-dns-view' + self.zone_auth = 'zone-auth' + self.hostname = 'host1' + self.dhcp_enabled = True + + self.allocator = ip_allocator.FixedAddressIPAllocator(self.ib_mock) + + def test_creates_fixed_address_on_allocate_ip(self): + self.allocator.allocate_given_ip( + self.netview, self.dnsview, self.zone_auth, + self.hostname, self.mac, self.ip, self.extattrs) + + self.ib_mock.create_fixed_address_for_given_ip.assert_called_once_with( + self.netview, self.mac, self.ip, self.extattrs) + + def test_creates_fixed_address_range_on_range_allocation(self): + first_ip = '192.168.1.1' + last_ip = '192.168.1.123' + + self.allocator.allocate_ip_from_range( + self.dnsview, self.netview, self.zone_auth, self.hostname, + self.mac, first_ip, last_ip, self.extattrs) + + self.ib_mock.create_fixed_address_from_range.assert_called_once_with( + self.netview, self.mac, first_ip, last_ip, self.extattrs) + + def test_deletes_fixed_address(self): + self.allocator.deallocate_ip(self.netview, self.dnsview, self.ip) + + self.ib_mock.delete_fixed_address.assert_called_once_with(self.netview, + self.ip) + + +class HostRecordAllocatorTestCase(base.BaseTestCase): + def test_creates_host_record_on_allocate_ip(self): + ib_mock = mock.MagicMock() + + netview = 'some-test-net-view' + dnsview = 'some-dns-view' + zone_auth = 'zone-auth' + hostname = 'host1' + mac = 'de:ad:be:ef:00:00' + ip = '192.168.1.1' + + ib_mock.find_hostname.return_value = None + + allocator = ip_allocator.HostRecordIPAllocator(ib_mock) + allocator.allocate_given_ip(netview, dnsview, zone_auth, hostname, + mac, ip) + + ib_mock.create_host_record_for_given_ip.assert_called_once_with( + dnsview, zone_auth, hostname, mac, ip, mock.ANY) + + def test_creates_host_record_range_on_range_allocation(self): + ib_mock = mock.MagicMock() + + netview = 'some-test-net-view' + dnsview = 'some-dns-view' + zone_auth = 'zone-auth' + hostname = 'host1' + mac = 'de:ad:be:ef:00:00' + first_ip = '192.168.1.2' + last_ip = '192.168.1.254' + + ib_mock.find_hostname.return_value = None + + allocator = ip_allocator.HostRecordIPAllocator(ib_mock) + allocator.allocate_ip_from_range( + dnsview, netview, zone_auth, hostname, mac, first_ip, last_ip) + + ib_mock.create_host_record_from_range.assert_called_once_with( + dnsview, netview, zone_auth, hostname, + mac, first_ip, last_ip, mock.ANY) + + def test_deletes_host_record(self): + ib_mock = mock.MagicMock() + + netview = 'some-test-net-view' + dnsview = 'some-dns-view' + ip = '192.168.1.2' + + allocator = ip_allocator.HostRecordIPAllocator(ib_mock) + allocator.deallocate_ip(netview, dnsview, ip) + + ib_mock.delete_host_record.assert_called_once_with(dnsview, ip) diff --git a/neutron/tests/unit/ipam/drivers/infoblox/test_ipam_controller.py b/neutron/tests/unit/ipam/drivers/infoblox/test_ipam_controller.py new file mode 100755 index 0000000..2695722 --- /dev/null +++ b/neutron/tests/unit/ipam/drivers/infoblox/test_ipam_controller.py @@ -0,0 +1,837 @@ +# Copyright 2014 OpenStack LLC. +# 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 mock + +import taskflow.engines + +from neutron.common import exceptions as neutron_exc +from neutron.db.infoblox import infoblox_db as infoblox_db +from neutron.ipam.drivers.infoblox import ea_manager +from neutron.ipam.drivers.infoblox import exceptions as ib_exceptions +from neutron.ipam.drivers.infoblox import infoblox_ipam +from neutron.ipam.drivers.infoblox import ipam_controller +from neutron.ipam.drivers.infoblox import objects +from neutron.tests import base + + +class SubstringMatcher(object): + def __init__(self, expected): + self.expected = expected + + def __eq__(self, actual): + return self.expected in actual + + def __repr__(self): + return "Expected substring: '{}'".format(self.expected) + + +class CreateSubnetTestCases(base.BaseTestCase): + def setUp(self): + super(CreateSubnetTestCases, self).setUp() + + self.expected_net_view_name = 'some-tenant-id' + self.cidr = 'some-cidr' + self.first_ip = '192.168.0.1' + self.last_ip = '192.168.0.254' + self.subnet = mock.MagicMock() + self.subnet.__getitem__.side_effect = mock.MagicMock() + self.object_manipulator = mock.Mock() + ip_allocator = mock.Mock() + self.object_manipulator.network_exists.return_value = False + self.object_manipulator.set_network_view = mock.MagicMock() + + cfg = mock.Mock() + cfg.reserve_dhcp_members = mock.Mock(return_value=[]) + cfg.reserve_dns_members = mock.Mock(return_value=[]) + cfg.dhcp_members = ['member1.com'] + cfg.dns_members = ['member1.com'] + + config_finder = mock.Mock() + config_finder.find_config_for_subnet = mock.Mock(return_value=cfg) + + context = infoblox_ipam.FlowContext(mock.MagicMock(), 'create-subnet') + + b = ipam_controller.InfobloxIPAMController(self.object_manipulator, + config_finder, + ip_allocator) + b.ea_manager = mock.Mock() + b.create_subnet(context, self.subnet) + taskflow.engines.run(context.parent_flow, store=context.store) + + def test_network_view_is_created_on_subnet_creation(self): + assert self.object_manipulator.create_network_view.called_once + + def test_dns_view_is_created_on_subnet_creation(self): + assert self.object_manipulator.create_dns_view.called_once + + def test_infoblox_network_is_created_on_subnet_create(self): + assert self.object_manipulator.create_network.called_once + + def test_ip_range_is_created_on_subnet_create(self): + assert self.object_manipulator.create_ip_range.called_once + + +class UpdateSubnetTestCase(base.BaseTestCase): + def setUp(self): + super(UpdateSubnetTestCase, self).setUp() + self.object_manipulator = mock.Mock() + self.context = mock.Mock() + ip_allocator = mock.Mock() + config_finder = mock.Mock() + + cfg = mock.Mock() + cfg.dhcp_members = ['member1.com'] + cfg.dns_members = ['member1.com'] + config_finder.find_config_for_subnet.return_value = cfg + + self.ipam = ipam_controller.InfobloxIPAMController( + self.object_manipulator, config_finder, ip_allocator) + self.ipam.ea_manager = mock.Mock() + + self.sub_id = 'fake-id' + self.new_nameservers = ['new_serv1', 'new_serv2'] + self.sub = dict( + id=self.sub_id, + cidr='test-cidr', + dns_nameservers=self.new_nameservers, + network_id='some-net-id' + ) + self.ib_net = objects.Network() + self.object_manipulator.get_network.return_value = self.ib_net + + @mock.patch.object(infoblox_db, 'get_subnet_dhcp_port_address', + mock.Mock(return_value=None)) + def test_update_subnet_dns_no_primary_ip(self): + self.ipam.update_subnet(self.context, self.sub_id, self.sub) + + self.assertEqual(self.new_nameservers, self.ib_net.dns_nameservers) + self.object_manipulator.update_network_options.assert_called_once_with( + self.ib_net, mock.ANY + ) + + @mock.patch.object(infoblox_db, 'get_subnet_dhcp_port_address', + mock.Mock(return_value=None)) + def test_update_subnet_dns_primary_is_member_ip(self): + self.ib_net.member_ip_addrs = ['member-ip'] + self.ib_net.dns_nameservers = ['member-ip', 'old_serv1', 'old_serv'] + + self.ipam.update_subnet(self.context, self.sub_id, self.sub) + + self.assertEqual(['member-ip'] + self.new_nameservers, + self.ib_net.dns_nameservers) + self.object_manipulator.update_network_options.assert_called_once_with( + self.ib_net, mock.ANY + ) + + @mock.patch.object(infoblox_db, 'get_subnet_dhcp_port_address', + mock.Mock()) + def test_update_subnet_dns_primary_is_relay_ip(self): + self.ib_net.member_ip_addr = 'fake_ip' + self.ib_net.dns_nameservers = ['relay_ip', '1.1.1.1', '2.2.2.2'] + + infoblox_db.get_subnet_dhcp_port_address.return_value = 'relay-ip' + + self.ipam.update_subnet(self.context, self.sub_id, self.sub) + + self.assertEqual(['relay-ip'] + self.new_nameservers, + self.ib_net.dns_nameservers) + self.object_manipulator.update_network_options.assert_called_once_with( + self.ib_net, mock.ANY + ) + + def test_extensible_attributes_get_updated(self): + ea_manager = mock.Mock() + manip = mock.MagicMock() + config_finder = mock.Mock() + config = mock.Mock() + config.dhcp_members = ['member1.com'] + config.dns_members = ['member1.com'] + config_finder.find_config_for_subnet = mock.Mock(return_value=config) + context = mock.Mock() + subnet_id = 'some-id' + subnet = mock.MagicMock() + subnet.name = None + + ctrlr = ipam_controller.InfobloxIPAMController( + manip, config_finder, extattr_manager=ea_manager) + + ctrlr.update_subnet(context, subnet_id, subnet) + + assert manip.update_network_options.called_once + + +class AllocateIPTestCase(base.BaseTestCase): + def test_host_record_created_on_allocate_ip(self): + infoblox = mock.Mock() + member_config = mock.MagicMock() + ip_allocator = mock.Mock() + context = mock.Mock() + + subnet = {'tenant_id': 'some-id', 'id': 'some-id'} + mac = 'aa:bb:cc:dd:ee:ff' + port = {'id': 'port_id', + 'mac_address': mac} + ip_dict = {'ip_address': '192.168.1.1', + 'subnet_id': 'fake-id'} + ip = {'ip_address': '192.168.1.1'} + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_config, + ip_allocator) + b.pattern_builder = mock.Mock() + b.ea_manager = mock.Mock() + + b.allocate_ip(context, subnet, port, ip_dict) + + ip_allocator.allocate_given_ip.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, mock.ANY, mac, ip['ip_address'], + mock.ANY) + + def test_host_record_from_range_created_on_allocate_ip(self): + infoblox = mock.Mock() + member_config = mock.MagicMock() + ip_allocator = mock.Mock() + context = mock.Mock() + + first_ip = '192.168.1.1' + last_ip = '192.168.1.132' + + subnet = {'allocation_pools': [{'first_ip': first_ip, + 'last_ip': last_ip}], + 'tenant_id': 'some-id', + 'id': 'some-id'} + + mac = 'aa:bb:cc:dd:ee:ff' + port = {'mac_address': mac, + 'device_owner': 'owner'} + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_config, + ip_allocator) + b.pattern_builder = mock.Mock() + b.ea_manager = mock.Mock() + b.allocate_ip(context, subnet, port) + + assert not ip_allocator.allocate_given_ip.called + ip_allocator.allocate_ip_from_range.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, mock.ANY, mac, first_ip, last_ip, + mock.ANY) + + def test_cannot_allocate_ip_raised_if_empty_range(self): + infoblox = mock.Mock() + member_config = mock.Mock() + context = mock.Mock() + ip_allocator = mock.Mock() + + hostname = 'hostname' + subnet = {'allocation_pools': [], + 'tenant_id': 'some-id', + 'cidr': '192.168.0.0/24'} + mac = 'aa:bb:cc:dd:ee:ff' + host = {'name': hostname, + 'mac_address': mac, + 'device_owner': 'owner'} + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_config, + ip_allocator) + b.pattern_builder = mock.Mock() + b.ea_manager = mock.Mock() + + assert not infoblox.create_host_record_range.called + assert not infoblox.create_host_record_ip.called + + ip = b.allocate_ip(context, subnet, host) + self.assertIsNone(ip) + + +class DeallocateIPTestCase(base.BaseTestCase): + def setUp(self): + super(DeallocateIPTestCase, self).setUp() + + self.infoblox = mock.Mock() + + cfg = mock.Mock() + cfg.dhcp_members = ['172.25.1.1'] + + config_finder = mock.MagicMock() + config_finder.find_config_for_subnet = mock.Mock(return_value=cfg) + + context = mock.MagicMock() + self.ip_allocator = mock.Mock() + + hostname = 'hostname' + self.ip = '192.168.0.1' + subnet = {'tenant_id': 'some-id', + 'network_id': 'some-id', + 'id': 'some-id'} + mac = 'aa:bb:cc:dd:ee:ff' + host = {'name': hostname, + 'mac_address': mac} + + b = ipam_controller.InfobloxIPAMController(self.infoblox, + config_finder, + self.ip_allocator) + b.deallocate_ip(context, subnet, host, self.ip) + + def test_ip_is_deallocated(self): + self.ip_allocator.deallocate_ip.assert_called_once_with( + mock.ANY, mock.ANY, self.ip) + + def test_dns_and_dhcp_services_restarted(self): + assert self.infoblox.restart_all_services.called_once + + +class NetOptionsMatcher(object): + def __init__(self, expected_ip): + self.expected_ip = expected_ip + + def __eq__(self, actual_net): + return self.expected_ip in actual_net.dns_nameservers + + def __repr__(self): + return "{}".format(self.expected_ip) + + +class DnsNameserversTestCase(base.BaseTestCase): + def test_network_is_updated_with_new_ip(self): + infoblox = mock.Mock() + ip_allocator = mock.Mock() + member_config = mock.Mock() + context = mock.MagicMock() + + expected_ip = '192.168.1.1' + cidr = '192.168.1.0/24' + port = {'fixed_ips': [{'subnet_id': 'some-id', + 'ip_address': expected_ip}]} + subnet = {'cidr': cidr, + 'tenant_id': 'some-id'} + + network = objects.Network() + network.members = ['member1'] + network.member_ip_addrs = ['192.168.1.2'] + network.dns_nameservers = [expected_ip] + + infoblox.get_network.return_value = network + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_config, + ip_allocator) + b._get_subnet = mock.Mock() + b._get_subnet.return_value = subnet + + b.set_dns_nameservers(context, port) + + matcher = NetOptionsMatcher(expected_ip) + infoblox.update_network_options.assert_called_once_with(matcher) + + def test_network_is_not_updated_if_network_has_no_members(self): + infoblox = mock.Mock() + member_config = mock.Mock() + ip_allocator = mock.Mock() + context = mock.MagicMock() + + expected_ip = '192.168.1.1' + cidr = '192.168.1.0/24' + port = {'fixed_ips': [{'subnet_id': 'some-id', + 'ip_address': expected_ip}]} + subnet = {'cidr': cidr, + 'tenant_id': 'some-id'} + + infoblox.get_network.return_value = objects.Network() + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_config, + ip_allocator) + b._get_subnet = mock.Mock() + b._get_subnet.return_value = subnet + + b.set_dns_nameservers(context, port) + + assert not infoblox.update_network_options.called + + def test_network_is_not_updated_if_network_has_no_dns_members(self): + infoblox = mock.Mock() + member_config = mock.Mock() + ip_allocator = mock.Mock() + context = mock.MagicMock() + + expected_ip = '192.168.1.1' + cidr = '192.168.1.0/24' + port = {'fixed_ips': [{'subnet_id': 'some-id', + 'ip_address': expected_ip}]} + subnet = {'cidr': cidr, + 'tenant_id': 'some-id'} + network = objects.Network() + network.members = ['member1'] + + infoblox.get_network.return_value = network + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_config, + ip_allocator) + b._get_subnet = mock.Mock() + b._get_subnet.return_value = subnet + + b.set_dns_nameservers(context, port) + + assert not infoblox.update_network_options.called + + +class DeleteSubnetTestCase(base.BaseTestCase): + @mock.patch.object(infoblox_db, 'is_network_external', + mock.Mock()) + def test_ib_network_deleted(self): + infoblox = mock.Mock() + member_conf = mock.Mock() + config = mock.Mock() + config.dhcp_members = ['member1.com'] + config.dns_members = ['member1.com'] + config.is_global_config = False + member_conf.find_config_for_subnet = mock.Mock(return_value=config) + ip_allocator = mock.Mock() + context = mock.MagicMock() + + cidr = '192.168.0.0/24' + subnet = mock.MagicMock() + subnet.__getitem__ = mock.Mock(return_value=cidr) + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_conf, + ip_allocator) + b._get_network = mock.Mock() + b._get_network.return_value = { + 'network_id': 'some-net-id', + 'shared': False + } + infoblox_db.is_network_external.return_value = False + + b.delete_subnet(context, subnet) + + infoblox.delete_network.assert_called_once_with(mock.ANY, cidr=cidr) + + def test_member_released(self): + infoblox = mock.Mock() + member_finder = mock.Mock() + config = mock.Mock() + config.dhcp_members = ['member1.com'] + config.dns_members = ['member1.com'] + member_finder.find_config_for_subnet = mock.Mock(return_value=config) + ip_allocator = mock.Mock() + context = mock.MagicMock() + + subnet = mock.MagicMock() + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_finder, + ip_allocator) + b.delete_subnet(context, subnet) + + assert member_finder.member_manager.release_member.called_once + + def test_preconfigured_dns_view_gets_deleted(self): + dns_view = "fake dns view" + infoblox = mock.Mock() + infoblox.has_dns_zones = mock.Mock(return_value=False) + ip_allocator = mock.Mock() + config = mock.Mock() + config.dhcp_members = ['member1.com'] + config.dns_members = ['member1.com'] + config._dns_view = dns_view + config_finder = mock.Mock() + config_finder.find_config_for_subnet = mock.Mock(return_value=config) + context = mock.Mock() + subnet = mock.MagicMock() + + b = ipam_controller.InfobloxIPAMController(infoblox, + config_finder, + ip_allocator) + + b.get_subnets_by_network = mock.MagicMock() + b.delete_subnet(context, subnet) + + infoblox.delete_dns_view.assert_called_once_with(dns_view) + + def test_network_view_deleted(self): + infoblox = mock.Mock() + ip_allocator = mock.Mock() + member_conf = mock.Mock() + config = mock.Mock() + config.dhcp_members = ['member1.com'] + config.dns_members = ['member1.com'] + member_conf.find_config_for_subnet = mock.Mock(return_value=config) + context = mock.Mock() + network = mock.MagicMock() + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_conf, + ip_allocator) + + b.get_subnets_by_network = mock.MagicMock() + b.delete_subnet(context, network) + + assert infoblox.delete_network_view.called_once + + +class CreateSubnetFlowTestCase(base.BaseTestCase): + def setUp(self): + super(CreateSubnetFlowTestCase, self).setUp() + + self.infoblox = mock.Mock() + member_conf = mock.MagicMock() + ip_allocator = mock.Mock() + self.expected_exception = Exception + self.context = infoblox_ipam.FlowContext(mock.MagicMock(), + 'create-subnet') + self.subnet = {'cidr': '192.168.0.0/24', + 'tenant_id': 'some-id', + 'network_id': 'some-id', + 'gateway_ip': '192.168.1.1', + 'allocation_pools': [{'start': 'start', + 'end': 'end'}], + 'ip_version': 'ipv4', + 'name': 'some-name', + 'enable_dhcp': True} + + self.infoblox.create_ip_range.side_effect = Exception() + self.infoblox.network_exists.return_value = False + + self.b = ipam_controller.InfobloxIPAMController(self.infoblox, + member_conf, + ip_allocator) + self.b.pattern_builder = mock.Mock() + self.b.ea_manager = mock.Mock() + + def test_flow_is_reverted_in_case_of_error(self): + self.infoblox.has_networks.return_value = False + self.b.create_subnet(self.context, self.subnet) + self.assertRaises(self.expected_exception, taskflow.engines.run, + self.context.parent_flow, store=self.context.store) + + assert self.infoblox.delete_network.called + assert not self.infoblox.delete_dns_view.called + assert self.infoblox.delete_network_view.called + + def test_network_view_is_not_deleted_if_has_networks(self): + self.infoblox.has_networks.return_value = True + self.b.create_subnet(self.context, self.subnet) + + self.assertRaises(self.expected_exception, taskflow.engines.run, + self.context.parent_flow, store=self.context.store) + + assert self.infoblox.delete_network.called + assert not self.infoblox.delete_dns_view.called + assert not self.infoblox.delete_network_view.called + + +class CreateSubnetFlowNiosNetExistsTestCase(base.BaseTestCase): + def setUp(self): + super(CreateSubnetFlowNiosNetExistsTestCase, self).setUp() + + self.infoblox = mock.Mock() + member_conf = mock.MagicMock() + ip_allocator = mock.Mock() + self.context = infoblox_ipam.FlowContext(mock.MagicMock(), + 'create-subnet') + self.subnet = mock.MagicMock() + self.subnet.__getitem__.side_effect = mock.MagicMock() + + self.infoblox.network_exists.return_value = True + + self.b = ipam_controller.InfobloxIPAMController(self.infoblox, + member_conf, + ip_allocator) + + def test_nios_network_is_updated_for_shared_os_network(self): + self.b.ea_manager.get_extattrs_for_network = mock.Mock( + return_value={ + 'Is External': { + 'value': 'False'}, + 'Is Shared': { + 'value': 'True'} + }) + + self.b.create_subnet(self.context, self.subnet) + + taskflow.engines.run(self.context.parent_flow, + store=self.context.store) + assert self.infoblox.update_network_options.called_once + + def test_nios_network_is_updated_for_shared_external_os_network(self): + self.b.ea_manager.get_extattrs_for_network = mock.Mock( + return_value={ + 'Is External': { + 'value': 'True'}, + 'Is Shared': { + 'value': 'True'} + }) + + self.b.create_subnet(self.context, self.subnet) + + taskflow.engines.run(self.context.parent_flow, + store=self.context.store) + assert self.infoblox.update_network_options.called_once + + def test_nios_network_is_updated_for_external_os_network(self): + self.b.ea_manager.get_extattrs_for_network = mock.Mock( + return_value={ + 'Is External': { + 'value': 'True'}, + 'Is Shared': { + 'value': 'False'} + }) + + self.b.create_subnet(self.context, self.subnet) + + taskflow.engines.run(self.context.parent_flow, + store=self.context.store) + assert self.infoblox.update_network_options.called_once + + def test_exception_is_raised_if_network_is_private(self): + self.b.ea_manager.get_extattrs_for_network = mock.Mock( + return_value={ + 'Is External': { + 'value': 'False'}, + 'Is Shared': { + 'value': 'False'} + }) + + self.b.create_subnet(self.context, self.subnet) + + self.assertRaises( + ib_exceptions.InfobloxInternalPrivateSubnetAlreadyExist, + taskflow.engines.run, + self.context.parent_flow, + store=self.context.store + ) + + +class DeleteNetworkTestCase(base.BaseTestCase): + def test_deletes_all_subnets(self): + infoblox = mock.Mock() + ip_allocator = mock.Mock() + member_conf = mock.Mock() + context = mock.Mock() + db_manager = mock.Mock() + network = {'id': 'some-id'} + num_subnets = 5 + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_conf, + ip_allocator, + db_mgr=db_manager) + + b.delete_subnet = mock.Mock() + db_manager.get_subnets_by_network = mock.Mock() + db_manager.get_subnets_by_network.return_value = [ + mock.Mock() for _ in xrange(num_subnets)] + + b.delete_network(context, network) + + assert b.delete_subnet.called + assert b.delete_subnet.call_count == num_subnets + + def test_deletes_network_view(self): + infoblox = mock.Mock() + ip_allocator = mock.Mock() + member_conf = mock.Mock() + context = mock.MagicMock() + network_id = 'some-id' + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_conf, + ip_allocator) + + b.delete_network(context, network_id) + + assert infoblox.delete_network_view.called_once + + def test_deletes_management_ip(self): + infoblox = mock.Mock() + ip_allocator = mock.Mock() + member_conf = mock.Mock() + context = mock.Mock() + network = mock.MagicMock() + ib_db = mock.Mock() + ea_manager = mock.Mock() + db_manager = mock.Mock() + + ib_db.is_network_external.return_value = False + + net_view_name = 'expected_network_view' + cidr = 'expected_cidr' + + self.config(dhcp_relay_management_network_view=net_view_name, + dhcp_relay_management_network=cidr) + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_conf, + ip_allocator, + ea_manager, + ib_db, + db_mgr=db_manager) + + b.delete_subnet = mock.Mock() + db_manager.get_subnets_by_network = mock.MagicMock() + + b.delete_network(context, network) + infoblox.delete_object_by_ref.assert_called_once_with(mock.ANY) + + def test_deletes_management_ip_from_db(self): + infoblox = mock.Mock() + ip_allocator = mock.Mock() + member_conf = mock.Mock() + context = mock.Mock() + expected_net_id = 'some-net-id' + ib_db = mock.Mock() + db_manager = mock.Mock() + + ib_db.is_network_external.return_value = False + net_view_name = 'expected_network_view' + cidr = 'expected_cidr' + + self.config(dhcp_relay_management_network_view=net_view_name, + dhcp_relay_management_network=cidr) + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_conf, + ip_allocator, + ib_db=ib_db, + db_mgr=db_manager) + + b.delete_subnet = mock.Mock() + db_manager.get_subnets_by_network = mock.MagicMock() + + b.delete_network(context, expected_net_id) + ib_db.delete_management_ip.assert_called_once_with( + context, expected_net_id) + + def test_does_not_delete_management_network_ip_for_external_net(self): + infoblox = mock.Mock() + ip_allocator = mock.Mock() + member_conf = mock.Mock() + context = mock.MagicMock() + network_id = mock.MagicMock() + ib_db = mock.Mock() + ea_manager = mock.Mock() + + ib_db.is_network_external.return_value = True + + net_view_name = 'expected_network_view' + cidr = 'expected_cidr' + + self.config(dhcp_relay_management_network_view=net_view_name, + dhcp_relay_management_network=cidr) + + b = ipam_controller.InfobloxIPAMController(infoblox, + member_conf, + ip_allocator, + ea_manager, + ib_db) + b.delete_network(context, network_id) + + assert infoblox.delete_object_by_ref.called + assert ib_db.delete_management_ip.called + + +class CreateNetworkTestCase(base.BaseTestCase): + def test_creates_fixed_address_object_in_management_network(self): + infoblox = mock.Mock() + config_finder = mock.Mock() + ip_allocator = mock.Mock() + ea_manager = mock.Mock() + context = mock.Mock() + network = mock.MagicMock() + network.get.return_value = False + network.__getitem__.side_effect = mock.Mock() + + expected_net_view = 'expected_net_view_name' + cidr = '1.0.0.0/24' + expected_mac = '00:00:00:00:00:00' + self.config(dhcp_relay_management_network_view=expected_net_view, + dhcp_relay_management_network=cidr) + + c = ipam_controller.InfobloxIPAMController(infoblox, config_finder, + ip_allocator, ea_manager) + + c.create_network(context, network) + infoblox.create_fixed_address_from_cidr.assert_called_once_with( + expected_net_view, expected_mac, cidr, mock.ANY) + + def test_stores_fixed_address_object_in_db(self): + infoblox = mock.Mock() + config_finder = mock.Mock() + ip_allocator = mock.Mock() + ea_manager = mock.Mock() + context = mock.Mock() + ib_db = mock.Mock() + expected_net_id = 'some-net-id' + + network = mock.MagicMock() + network.get.return_value = False + network.__getitem__.side_effect = \ + lambda key: expected_net_id if key == 'id' else None + + expected_net_view = 'expected_net_view_name' + cidr = '1.0.0.0/24' + self.config(dhcp_relay_management_network_view=expected_net_view, + dhcp_relay_management_network=cidr) + + c = ipam_controller.InfobloxIPAMController( + infoblox, config_finder, ip_allocator, ea_manager, ib_db) + + c.create_network(context, network) + + ib_db.add_management_ip.assert_called_once_with( + context, expected_net_id, mock.ANY) + + def test_does_nothing_if_mgmt_net_is_not_set_in_config(self): + infoblox = mock.Mock() + config_finder = mock.Mock() + ip_allocator = mock.Mock() + ea_manager = mock.Mock() + context = mock.Mock() + ib_db = mock.Mock() + network = mock.Mock() + network.get.return_value = False + + c = ipam_controller.InfobloxIPAMController( + infoblox, config_finder, ip_allocator, ea_manager, ib_db) + + c.create_network(context, network) + + assert not infoblox.create_fixed_address_from_cidr.called + assert not ib_db.add_management_ip.called + + def test_does_nothing_for_external_net(self): + infoblox = mock.Mock() + config_finder = mock.Mock() + ip_allocator = mock.Mock() + ea_manager = mock.Mock() + context = mock.Mock() + ib_db = mock.Mock() + network = mock.Mock() + + c = ipam_controller.InfobloxIPAMController( + infoblox, config_finder, ip_allocator, ea_manager, ib_db) + + try: + c.create_network(context, network) + except neutron_exc.InvalidConfigurationOption as e: + self.fail('Unexpected exception: {}'.format(e)) + + assert not infoblox.create_fixed_address_from_cidr.called + assert not ib_db.add_management_ip.called diff --git a/neutron/tests/unit/ipam/drivers/infoblox/test_object_manipulator.py b/neutron/tests/unit/ipam/drivers/infoblox/test_object_manipulator.py new file mode 100755 index 0000000..844e8fc --- /dev/null +++ b/neutron/tests/unit/ipam/drivers/infoblox/test_object_manipulator.py @@ -0,0 +1,514 @@ +# Copyright 2014 OpenStack LLC. +# 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 mock + +from neutron.ipam.drivers.infoblox import exceptions +from neutron.ipam.drivers.infoblox import object_manipulator as om +from neutron.ipam.drivers.infoblox import objects +from neutron.tests import base + + +class PayloadMatcher(object): + ANYKEY = 'MATCH_ANY_KEY' + + def __init__(self, expected_values): + self.args = expected_values + + def __eq__(self, actual): + expected = [] + + for key, expected_value in self.args.iteritems(): + expected.append(self._verify_value_is_expected(actual, key, + expected_value)) + + return all(expected) + + def __repr__(self): + return "Expected args: %s" % self.args + + def _verify_value_is_expected(self, d, key, expected_value): + found = False + if not isinstance(d, dict): + return False + + for k in d: + if isinstance(d[k], dict): + found = self._verify_value_is_expected(d[k], key, + expected_value) + if isinstance(d[k], list): + if k == key and d[k] == expected_value: + return True + for el in d[k]: + found = self._verify_value_is_expected(el, key, + expected_value) + + if found: + return True + if (key == k or key == self.ANYKEY) and d[k] == expected_value: + return True + return found + + +class ObjectManipulatorTestCase(base.BaseTestCase): + def test_create_net_view_creates_network_view_object(self): + connector = mock.Mock() + connector.get_object.return_value = None + connector.create_object.return_value = None + + ibom = om.InfobloxObjectManipulator(connector) + + net_view_name = 'test_net_view_name' + ibom.create_network_view(net_view_name, mock.ANY) + + matcher = PayloadMatcher({'name': net_view_name}) + connector.get_object.assert_called_once_with( + 'networkview', matcher, None, proxy=False) + connector.create_object.assert_called_once_with( + 'networkview', matcher, mock.ANY) + + def test_create_host_record_creates_host_record_object(self): + dns_view_name = 'test_dns_view_name' + zone_auth = 'test.dns.zone.com' + hostname = 'test_hostname' + ip = '192.168.0.1' + mac = 'aa:bb:cc:dd:ee:ff' + + sample_host_record = objects.HostRecordIPv4() + sample_host_record.hostname = hostname + sample_host_record.zone_auth = zone_auth + sample_host_record.ip = ip + + connector = mock.Mock() + connector.create_object.return_value = sample_host_record.to_dict() + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.create_host_record_for_given_ip(dns_view_name, zone_auth, + hostname, mac, ip, mock.ANY) + + exp_payload = { + 'name': 'test_hostname.test.dns.zone.com', + 'view': dns_view_name, + 'ipv4addrs': [ + {'mac': mac, 'configure_for_dhcp': True, 'ipv4addr': ip} + ], + 'extattrs': {} + } + + connector.create_object.assert_called_once_with( + 'record:host', exp_payload, ['ipv4addrs', 'extattrs']) + + def test_create_host_record_range_create_host_record_object(self): + dns_view_name = 'test_dns_view_name' + zone_auth = 'test.dns.zone.com' + hostname = 'test_hostname' + mac = 'aa:bb:cc:dd:ee:ff' + net_view_name = 'test_net_view_name' + first_ip = '192.168.0.1' + last_ip = '192.168.0.254' + + sample_host_record = objects.HostRecordIPv4() + sample_host_record.hostname = hostname + sample_host_record.zone_auth = zone_auth + sample_host_record.ip = first_ip + sample_host_record.view = dns_view_name + + connector = mock.Mock() + connector.create_object.return_value = sample_host_record.to_dict() + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.create_host_record_from_range( + dns_view_name, net_view_name, zone_auth, hostname, mac, first_ip, + last_ip, mock.ANY) + + next_ip = \ + 'func:nextavailableip:192.168.0.1-192.168.0.254,test_net_view_name' + exp_payload = { + 'name': 'test_hostname.test.dns.zone.com', + 'view': dns_view_name, + 'ipv4addrs': [ + {'mac': mac, 'configure_for_dhcp': True, 'ipv4addr': next_ip} + ], + 'extattrs': mock.ANY + } + + connector.create_object.assert_called_once_with( + 'record:host', exp_payload, ['ipv4addrs', 'extattrs']) + + def test_delete_host_record_deletes_host_record_object(self): + connector = mock.Mock() + connector.get_object.return_value = mock.MagicMock() + + ibom = om.InfobloxObjectManipulator(connector) + + dns_view_name = 'test_dns_view_name' + ip_address = '192.168.0.254' + + ibom.delete_host_record(dns_view_name, ip_address) + + matcher = PayloadMatcher({'view': dns_view_name, + PayloadMatcher.ANYKEY: ip_address}) + connector.get_object.assert_called_once_with( + 'record:host', matcher, None, proxy=False) + connector.delete_object.assert_called_once_with(mock.ANY) + + def test_get_network_gets_network_object(self): + connector = mock.Mock() + connector.get_object.return_value = mock.MagicMock() + + ibom = om.InfobloxObjectManipulator(connector) + + net_view_name = 'test_dns_view_name' + cidr = '192.168.0.0/24' + + ibom.get_network(net_view_name, cidr) + + matcher = PayloadMatcher({'network_view': net_view_name, + 'network': cidr}) + connector.get_object.assert_called_once_with('network', + matcher, + mock.ANY, + proxy=False) + + def test_throws_network_not_available_on_get_network(self): + connector = mock.Mock() + connector.get_object.return_value = None + + ibom = om.InfobloxObjectManipulator(connector) + + net_view_name = 'test_dns_view_name' + cidr = '192.168.0.0/24' + + self.assertRaises(exceptions.InfobloxNetworkNotAvailable, + ibom.get_network, net_view_name, cidr) + + matcher = PayloadMatcher({'network_view': net_view_name, + 'network': cidr}) + connector.get_object.assert_called_once_with('network', + matcher, + mock.ANY, + proxy=False) + + def test_object_is_not_created_if_already_exists(self): + connector = mock.Mock() + connector.get_object.return_value = mock.MagicMock() + + ibom = om.InfobloxObjectManipulator(connector) + + net_view_name = 'test_dns_view_name' + + ibom.create_network_view(net_view_name, mock.ANY) + + matcher = PayloadMatcher({'name': net_view_name}) + connector.get_object.assert_called_once_with( + 'networkview', matcher, None, proxy=False) + assert not connector.create_object.called + + def test_get_member_gets_member_object(self): + connector = mock.Mock() + connector.get_object.return_value = None + + ibom = om.InfobloxObjectManipulator(connector) + + member = objects.Member(name='member1', ip='some-ip') + + ibom.get_member(member) + + matcher = PayloadMatcher({'host_name': member.name}) + connector.get_object.assert_called_once_with('member', matcher) + + def test_restart_services_calls_infoblox_function(self): + connector = mock.Mock() + connector.get_object.return_value = mock.MagicMock() + + ibom = om.InfobloxObjectManipulator(connector) + + member = objects.Member(name='member1', ip='some-ip') + + ibom.restart_all_services(member) + + connector.call_func.assert_called_once_with( + 'restartservices', mock.ANY, mock.ANY) + + def test_update_network_updates_object(self): + ref = 'infoblox_object_id' + opts = 'infoblox_options' + + connector = mock.Mock() + ib_network = mock.Mock() + ib_network.ref = ref + ib_network.options = opts + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.update_network_options(ib_network) + + connector.update_object.assert_called_once_with(ref, {'options': opts}, + mock.ANY) + + def test_update_network_updates_eas_if_not_null(self): + ref = 'infoblox_object_id' + opts = 'infoblox_options' + eas = 'some-eas' + + connector = mock.Mock() + ib_network = mock.Mock() + ib_network.ref = ref + ib_network.options = opts + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.update_network_options(ib_network, eas) + + connector.update_object.assert_called_once_with( + ref, {'options': opts, 'extattrs': eas}, mock.ANY) + + def test_member_is_assigned_as_list_on_network_create(self): + net_view = 'net-view-name' + cidr = '192.168.1.0/24' + nameservers = [] + members = [ + objects.Member(name='just-a-single-member-ip', ip='some-ip') + ] + gateway_ip = '192.168.1.1' + expected_members = members[0].ip + extattrs = mock.Mock() + + connector = mock.Mock() + ibom = om.InfobloxObjectManipulator(connector) + + ibom.create_network(net_view, cidr, nameservers, members, gateway_ip, + extattrs) + + assert not connector.get_object.called + matcher = PayloadMatcher({'ipv4addr': expected_members}) + connector.create_object.assert_called_once_with('network', matcher, + None) + + def test_create_ip_range_creates_range_object(self): + net_view = 'net-view-name' + start_ip = '192.168.1.1' + end_ip = '192.168.1.123' + disable = False + + connector = mock.Mock() + connector.get_object.return_value = None + + ibom = om.InfobloxObjectManipulator(connector) + ibom.create_ip_range(net_view, start_ip, end_ip, None, + disable, mock.ANY) + + assert connector.get_object.called + matcher = PayloadMatcher({'start_addr': start_ip, + 'end_addr': end_ip, + 'network_view': net_view, + 'disable': disable}) + connector.create_object.assert_called_once_with('range', matcher, + mock.ANY) + + def test_delete_ip_range_deletes_infoblox_object(self): + net_view = 'net-view-name' + start_ip = '192.168.1.1' + end_ip = '192.168.1.123' + + connector = mock.Mock() + connector.get_object.return_value = mock.MagicMock() + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.delete_ip_range(net_view, start_ip, end_ip) + + matcher = PayloadMatcher({'start_addr': start_ip, + 'end_addr': end_ip, + 'network_view': net_view}) + connector.get_object.assert_called_once_with('range', matcher, + None, proxy=False) + connector.delete_object.assert_called_once_with(mock.ANY) + + def test_delete_network_deletes_infoblox_network(self): + net_view = 'net-view-name' + cidr = '192.168.1.0/24' + + connector = mock.Mock() + connector.get_object.return_value = mock.MagicMock() + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.delete_network(net_view, cidr) + + matcher = PayloadMatcher({'network_view': net_view, + 'network': cidr}) + connector.get_object.assert_called_once_with('network', matcher, + None, proxy=False) + connector.delete_object.assert_called_once_with(mock.ANY) + + def test_delete_network_view_deletes_infoblox_object(self): + net_view = 'net-view-name' + + connector = mock.Mock() + connector.get_object.return_value = mock.MagicMock() + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.delete_network_view(net_view) + + matcher = PayloadMatcher({'name': net_view}) + connector.get_object.assert_called_once_with( + 'networkview', matcher, None, proxy=False) + connector.delete_object.assert_called_once_with(mock.ANY) + + def test_bind_names_updates_host_record(self): + dns_view_name = 'dns-view-name' + fqdn = 'host.global.com' + ip = '192.168.1.1' + + connector = mock.Mock() + connector.get_object.return_value = mock.MagicMock() + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.bind_name_with_host_record(dns_view_name, ip, + fqdn, mock.ANY) + + matcher = PayloadMatcher({'view': dns_view_name, + PayloadMatcher.ANYKEY: ip}) + connector.get_object.assert_called_once_with('record:host', + matcher, + None, + proxy=False) + + matcher = PayloadMatcher({'name': fqdn}) + connector.update_object.assert_called_once_with(mock.ANY, + matcher, + mock.ANY) + + def test_create_dns_zone_creates_zone_auth_object(self): + dns_view_name = 'dns-view-name' + fqdn = 'host.global.com' + member = objects.Member(name='member_name', ip='some-ip') + zone_format = 'IPV4' + + connector = mock.Mock() + connector.get_object.return_value = None + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.create_dns_zone(dns_view_name, fqdn, member, + zone_format=zone_format) + + matcher = PayloadMatcher({'view': dns_view_name, + 'fqdn': fqdn}) + connector.get_object.assert_called_once_with('zone_auth', + matcher, + None, + proxy=False) + + matcher = PayloadMatcher({'view': dns_view_name, + 'fqdn': fqdn, + 'zone_format': zone_format, + 'name': member.name}) + connector.create_object.assert_called_once_with('zone_auth', + matcher, + None) + + def test_create_dns_zone_with_grid_secondaries(self): + dns_view_name = 'dns-view-name' + fqdn = 'host.global.com' + primary_dns_member = objects.Member(name='member_primary', + ip='some-ip') + secondary_dns_members = [objects.Member(name='member_secondary', + ip='some-ip')] + zone_format = 'IPV4' + + connector = mock.Mock() + connector.get_object.return_value = None + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.create_dns_zone(dns_view_name, fqdn, primary_dns_member, + secondary_dns_members, + zone_format=zone_format) + + matcher = PayloadMatcher({'view': dns_view_name, + 'fqdn': fqdn}) + connector.get_object.assert_called_once_with('zone_auth', + matcher, + None, + proxy=False) + + payload = {'view': dns_view_name, + 'fqdn': fqdn, + 'zone_format': zone_format, + 'grid_primary': [{'name': primary_dns_member.name, + '_struct': 'memberserver'}], + 'grid_secondaries': [{'name': member.name, + '_struct': 'memberserver'} + for member in secondary_dns_members], + 'extattrs': {} + } + connector.create_object.assert_called_once_with('zone_auth', + payload, + None) + + def test_create_host_record_throws_exception_on_error(self): + dns_view_name = 'dns-view-name' + hostname = 'host.global.com' + mac = 'aa:bb:cc:dd:ee:ff' + ip = '192.168.1.1' + zone_auth = 'my.auth.zone.com' + + connector = mock.Mock() + response = {'text': "Cannot find 1 available IP"} + + connector.create_object.side_effect = \ + exceptions.InfobloxCannotCreateObject(response=response, + objtype='host:record', + content='adsfasd', + code='1234') + ibom = om.InfobloxObjectManipulator(connector) + + self.assertRaises(exceptions.InfobloxCannotAllocateIp, + ibom.create_host_record_for_given_ip, + dns_view_name, zone_auth, hostname, mac, ip, + mock.ANY) + + def test_create_dns_view_creates_view_object(self): + net_view_name = 'net-view-name' + dns_view_name = 'dns-view-name' + + connector = mock.Mock() + connector.get_object.return_value = None + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.create_dns_view(net_view_name, dns_view_name) + + matcher = PayloadMatcher({'name': dns_view_name, + 'network_view': net_view_name}) + connector.get_object.assert_called_once_with('view', matcher, + None, proxy=False) + connector.create_object.assert_called_once_with('view', matcher, + None) + + def test_default_net_view_is_never_deleted(self): + connector = mock.Mock() + + ibom = om.InfobloxObjectManipulator(connector) + + ibom.delete_network_view('default') + + assert not connector.delete_object.called diff --git a/neutron/tests/unit/ipam/drivers/infoblox/test_objects.py b/neutron/tests/unit/ipam/drivers/infoblox/test_objects.py new file mode 100755 index 0000000..c8531ad --- /dev/null +++ b/neutron/tests/unit/ipam/drivers/infoblox/test_objects.py @@ -0,0 +1,353 @@ +# Copyright 2014 OpenStack LLC. +# 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 mock +from testtools import matchers + +from neutron.ipam.drivers.infoblox import objects +from neutron.openstack.common import jsonutils +from neutron.tests import base + + +class InfobloxNetworkObjectTestCase(base.BaseTestCase): + def setUp(self): + super(InfobloxNetworkObjectTestCase, self).setUp() + self.network_object_dict = jsonutils.loads("""[ + { + "_ref": "network/ZG5zLldHdvcmskMTAuMzkuMTE:10.39.11.0/24/default", + "members": [ + { + "_struct": "dhcpmember", + "ipv4addr": "10.39.11.123", + "name": "infoblox.localdomain" + } + ], + "options": [ + { + "name": "dhcp-lease-time", + "num": 51, + "use_option": false, + "value": "43200", + "vendor_class": "DHCP" + }, + { + "name": "domain-name-servers", + "num": 6, + "use_option": true, + "value": "10.39.11.123,10.39.11.124,10.39.11.125", + "vendor_class": "DHCP" + }, + { + "name": "routers", + "num": 3, + "use_option": false, + "value": "10.39.11.1", + "vendor_class": "DHCP" + } + ] + } + ]""")[0] + + def _get_nameservers_opt(self, network): + nameservers_opts = filter(lambda opt: + opt['name'] == + objects.Network.DNS_NAMESERVERS_OPTION, + network['options']) + if nameservers_opts: + return nameservers_opts[0] + return None + + def test_has_members(self): + net = objects.Network.from_dict(self.network_object_dict) + self.assertTrue(net.members) + + def test_update_member_ip_modifies_member_ip(self): + net = objects.Network.from_dict(self.network_object_dict) + new_ip = '!!!NEW_IP!!!' + net.update_member_ip_in_dns_nameservers(new_ip) + self.assertIn(new_ip, net.dns_nameservers) + + def test_get_dns_nameservers(self): + net = objects.Network.from_dict(self.network_object_dict) + servers = "10.39.11.123,10.39.11.124,10.39.11.125".split(',') + self.assertEqual(servers, net.dns_nameservers) + + def test_get_dns_nameservers_no_option(self): + nameservers_opt = self._get_nameservers_opt(self.network_object_dict) + self.network_object_dict['options'].remove(nameservers_opt) + net = objects.Network.from_dict(self.network_object_dict) + self.assertEqual([], net.dns_nameservers) + + def test_get_dns_nameservers_use_option_false(self): + nameservers_opt = self._get_nameservers_opt(self.network_object_dict) + nameservers_opt['use_option'] = False + net = objects.Network.from_dict(self.network_object_dict) + self.assertEqual([], net.dns_nameservers) + + def test_set_dns_nameservers(self): + net = objects.Network.from_dict(self.network_object_dict) + net.dns_nameservers = ['1.1.1.1', '3.3.3.3'] + net_dict = net.to_dict() + nameservers_opt = self._get_nameservers_opt(net_dict) + self.assertEqual('1.1.1.1,3.3.3.3', nameservers_opt['value']) + self.assertTrue(nameservers_opt['use_option']) + + def test_set_dns_nameservers_empty_val(self): + net = objects.Network.from_dict(self.network_object_dict) + net.dns_nameservers = [] + net_dict = net.to_dict() + nameservers_opt = self._get_nameservers_opt(net_dict) + self.assertFalse(nameservers_opt['use_option']) + + def test_set_dns_nameservers_no_previous_option(self): + nameservers_opt = self._get_nameservers_opt(self.network_object_dict) + self.network_object_dict['options'].remove(nameservers_opt) + net = objects.Network.from_dict(self.network_object_dict) + net.dns_nameservers = ['7.7.7.7', '8.8.8.8'] + net_dict = net.to_dict() + nameservers_opt = self._get_nameservers_opt(net_dict) + self.assertEqual('7.7.7.7,8.8.8.8', nameservers_opt['value']) + self.assertTrue(nameservers_opt['use_option']) + + def test_set_dns_nameservers_no_previous_option_and_empty_val(self): + nameservers_opt = self._get_nameservers_opt(self.network_object_dict) + self.network_object_dict['options'].remove(nameservers_opt) + net = objects.Network.from_dict(self.network_object_dict) + net.dns_nameservers = [] + net_dict = net.to_dict() + nameservers_opt = self._get_nameservers_opt(net_dict) + self.assertIsNone(nameservers_opt) + + +class InfobloxIPv4ObjectTestCase(base.BaseTestCase): + def test_removes_correct_object_from_list_of_ips(self): + removed_ip = '192.168.1.2' + ips = [ + objects.IPv4(ip='192.168.1.1'), + objects.IPv4(ip=removed_ip), + objects.IPv4(ip='192.168.1.3') + ] + + ips.remove(removed_ip) + + self.assertEqual(len(ips), 2) + for ip in ips: + self.assertTrue(ip.ip != removed_ip) + + +class InfobloxIPv4HostRecordObjectTestCase(base.BaseTestCase): + def setUp(self): + super(InfobloxIPv4HostRecordObjectTestCase, self).setUp() + host_record_ref = ("record:host/ZG5zLmhvc3QkLjY3OC5jb20uZ2xvYmFsLmNs" + "b3VkLnRlc3RzdWJuZXQudGVzdF9ob3N0X25hbWU:" + "test_host_name.testsubnet.cloud.global.com/" + "default.687401e9f7a7471abbf301febf99854e") + ipv4addrs_ref = ("record:host_ipv4addr/ZG5zLmhvc3RfYWRkcmVzcyQuNjc" + "4LmNvbS5nbG9iYWwuY2xvdWQudGVzdHN1Ym5ldC50ZXN0X2h" + "vc3RfbmFtZS4xOTIuMTY4LjAuNS4:192.168.0.5/" + "test_host_name.testsubnet.cloud.global.com/" + "default.687401e9f7a7471abbf301febf99854e") + self.host_record = jsonutils.loads("""{ + "_ref": "%s", + "ipv4addrs": [ + { + "_ref": "%s", + "configure_for_dhcp": false, + "host": "test_host_name.testsubnet.cloud.global.com", + "ipv4addr": "192.168.0.5", + "mac": "aa:bb:cc:dd:ee:ff" + } + ] + } + """ % (host_record_ref, ipv4addrs_ref)) + + def test_constructs_object_from_dict(self): + host_record = objects.HostRecordIPv4.from_dict(self.host_record) + self.assertIsNotNone(host_record) + + def test_hostname_is_set_from_dict(self): + expected_hostname = 'expected_hostname' + expected_dns_zone = 'expected.dns.zone.com' + self.host_record['ipv4addrs'][0]['host'] = '.'.join( + [expected_hostname, expected_dns_zone]) + host_record = objects.HostRecordIPv4.from_dict(self.host_record) + + self.assertEqual(expected_hostname, host_record.hostname) + self.assertEqual(expected_dns_zone, host_record.zone_auth) + + def test_all_attributes_are_set_from_dict(self): + expected_attributes = ['hostname', 'dns_view', 'mac', 'ip'] + hr = objects.HostRecordIPv4.from_dict(self.host_record) + self.assertTrue(all([expected in dir(hr) + for expected in expected_attributes])) + + +class InfobloxIPv6HostRecordObjectTestCase(base.BaseTestCase): + def setUp(self): + super(InfobloxIPv6HostRecordObjectTestCase, self).setUp() + host_record_ref = ("record:host/ZG5zLmhvc3QkLjY3OC5jb20uZ2xvYmFsLmNs" + "b3VkLnRlc3RzdWJuZXQudGVzdF9ob3N0X25hbWU:" + "test_host_name.testsubnet.cloud.global.com/" + "default.687401e9f7a7471abbf301febf99854e") + ipv6addrs_ref = ("record:host_ipv4addr/ZG5zLmhvc3RfYWRkcmVzcyQuNjc" + "4LmNvbS5nbG9iYWwuY2xvdWQudGVzdHN1Ym5ldC50ZXN0X2h" + "vc3RfbmFtZS4xOTIuMTY4LjAuNS4:2001:DB8::3/" + "test_host_name.testsubnet.cloud.global.com/" + "default.687401e9f7a7471abbf301febf99854e") + self.host_record = jsonutils.loads("""{ + "_ref": "%s", + "ipv6addrs": [ + { + "_ref": "%s", + "configure_for_dhcp": false, + "host": "test_host_name.testsubnet.cloud.global.com", + "ipv6addr": "2001:DB8::3", + "mac": "aa:bb:cc:dd:ee:ff" + } + ] + } + """ % (host_record_ref, ipv6addrs_ref)) + + def test_constructs_object_from_dict(self): + host_record = objects.HostRecordIPv6.from_dict(self.host_record) + self.assertIsNotNone(host_record) + + def test_hostname_is_set_from_dict(self): + expected_hostname = 'expected_hostname' + expected_dns_zone = 'expected.dns.zone.com' + self.host_record['ipv6addrs'][0]['host'] = '.'.join( + [expected_hostname, expected_dns_zone]) + host_record = objects.HostRecordIPv6.from_dict(self.host_record) + + self.assertEqual(expected_hostname, host_record.hostname) + self.assertEqual(expected_dns_zone, host_record.zone_auth) + + def test_all_attributes_are_set_from_dict(self): + expected_attributes = ['hostname', 'dns_view', 'mac', 'ip'] + hr = objects.HostRecordIPv6.from_dict(self.host_record) + self.assertTrue(all([expected in dir(hr) + for expected in expected_attributes])) + + +class FixedAddressIPv4TestCase(base.BaseTestCase): + def test_builds_valid_fa_from_infoblox_returned_json(self): + fixed_address_ref = ("fixedaddress/ZG5zLmZpeGVkX2FkZHJlc3MkMTAuMC4wLj" + "EwMC42ODAuLg:10.0.0.100/rv-test-netview") + ip = "10.0.0.100" + fixed_address = jsonutils.loads("""{ + "_ref": "%s", + "ipv4addr": "%s" + }""" % (fixed_address_ref, ip)) + + fa = objects.FixedAddressIPv4.from_dict(fixed_address) + self.assertEqual(fa.ip, ip) + + def test_dict_contains_mac_ip_and_net_view(self): + expected_ip = "1.2.3.4" + expected_mac = "aa:bb:cc:dd:ee:ff" + expected_net_view = "test-net-view-name" + expected_extattrs = "test-extattrs" + + expected_dict = { + 'mac': expected_mac, + 'ipv4addr': expected_ip, + 'network_view': expected_net_view, + 'extattrs': expected_extattrs + } + + fa = objects.FixedAddressIPv4() + fa.ip = expected_ip + fa.net_view = expected_net_view + fa.mac = expected_mac + fa.extattrs = expected_extattrs + + self.assertThat(fa.to_dict(), matchers.KeysEqual(expected_dict)) + self.assertThat(fa.to_dict(), matchers.Equals(expected_dict)) + + +class FixedAddressIPv6TestCase(base.BaseTestCase): + def test_builds_valid_fa_from_infoblox_returned_json(self): + fixed_address_ref = ("fixedaddress/ZG5zLmZpeGVkX2FkZHJlc3MkMTAuMC4wLj" + "EwMC42ODAuLg:10.0.0.100/rv-test-netview") + ip = "2001:DB8::3" + fixed_address = jsonutils.loads("""{ + "_ref": "%s", + "ipv6addr": "%s" + }""" % (fixed_address_ref, ip)) + + fa = objects.FixedAddressIPv6.from_dict(fixed_address) + self.assertEqual(fa.ip, ip) + + @mock.patch.object(objects, 'generate_duid', + mock.Mock(return_value=None)) + def test_dict_contains_mac_ip_and_net_view(self): + expected_ip = "2001:DB8::3" + duid = "aa:bb:cc:dd:ee:ff" + expected_duid = "00:03:00:01:aa:bb:cc:dd:ee:ff" + expected_net_view = "test-net-view-name" + expected_extattrs = "test-extattrs" + + objects.generate_duid.return_value = expected_duid + + expected_dict = { + 'duid': expected_duid, + 'ipv6addr': expected_ip, + 'network_view': expected_net_view, + 'extattrs': expected_extattrs + } + + fa = objects.FixedAddressIPv6() + fa.ip = expected_ip + fa.net_view = expected_net_view + fa.mac = duid + fa.extattrs = expected_extattrs + + self.assertThat(fa.to_dict(), matchers.KeysEqual(expected_dict)) + self.assertThat(fa.to_dict(), matchers.Equals(expected_dict)) + + +class MemberTestCase(base.BaseTestCase): + def test_two_identical_members_are_equal(self): + ip = 'some-ip' + name = 'some-name' + + m1 = objects.Member(ip, name) + m2 = objects.Member(ip, name) + + self.assertEqual(m1, m2) + + +class IPAllocationObjectTestCase(base.BaseTestCase): + def test_next_available_ip_returns_properly_formatted_string(self): + net_view = 'expected_net_view_name' + first_ip = '1.2.3.4' + last_ip = '1.2.3.14' + cidr = '1.2.3.0/24' + + naip = objects.IPAllocationObject.next_available_ip_from_range( + net_view, first_ip, last_ip) + + self.assertTrue(isinstance(naip, basestring)) + self.assertTrue(naip.startswith('func:nextavailableip:')) + self.assertTrue(naip.endswith( + '{first_ip}-{last_ip},{net_view}'.format(**locals()))) + + naip = objects.IPAllocationObject.next_available_ip_from_cidr( + net_view, cidr) + + self.assertTrue(isinstance(naip, basestring)) + self.assertTrue(naip.startswith('func:nextavailableip:')) + self.assertTrue(naip.endswith( + '{cidr},{net_view}'.format(**locals()))) diff --git a/neutron/tests/unit/ipam/drivers/infoblox/test_pattern_builder.py b/neutron/tests/unit/ipam/drivers/infoblox/test_pattern_builder.py new file mode 100644 index 0000000..acae9ac --- /dev/null +++ b/neutron/tests/unit/ipam/drivers/infoblox/test_pattern_builder.py @@ -0,0 +1,114 @@ +# Copyright 2014 OpenStack LLC. +# 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 mock + +from neutron.ipam.drivers.infoblox import config +from neutron.ipam.drivers.infoblox import exceptions +from neutron.tests import base + + +class PatternBuilderTestCase(base.BaseTestCase): + def test_dots_in_ip_address_replaced_with_dashes(self): + context = mock.MagicMock() + subnet = mock.MagicMock() + ip = '192.168.1.1' + + res = config.PatternBuilder("{ip_address}").build( + context, subnet, ip_addr=ip) + + self.assertEqual(res, ip.replace('.', '-')) + + def test_raises_error_if_pattern_is_invalid(self): + context = mock.MagicMock() + subnet = mock.MagicMock() + + pb = config.PatternBuilder("{}") + self.assertRaises(exceptions.InfobloxConfigException, + pb.build, context, subnet) + + pb = config.PatternBuilder("start..end") + self.assertRaises(exceptions.InfobloxConfigException, + pb.build, context, subnet) + + pb = config.PatternBuilder("{non-existing-variable}") + self.assertRaises(exceptions.InfobloxConfigException, + pb.build, context, subnet) + + def test_subnet_id_used_if_subnet_has_no_name(self): + context = mock.MagicMock() + subnet = mock.MagicMock() + subnet_id = 'some-id' + + def get_id(item): + if item == 'id': + return subnet_id + return None + subnet.__getitem__.side_effect = get_id + + pb = config.PatternBuilder("{subnet_name}") + built = pb.build(context, subnet) + + self.assertEqual(built, subnet_id) + + def test_value_is_built_using_pattern(self): + context = mock.MagicMock() + subnet = mock.MagicMock() + subnet_name = 'subnet-name' + ip_address = 'ip_address' + + def get_id(item): + if item == 'name': + return subnet_name + else: + return None + subnet.__getitem__.side_effect = get_id + + pattern = "host-{ip_address}.{subnet_name}.custom_stuff" + pb = config.PatternBuilder(pattern) + built = pb.build(context, subnet, ip_addr=ip_address) + + self.assertEqual(built, pattern.format(subnet_name=subnet_name, + ip_address=ip_address)) + + def test_all_required_pattern_variables_are_supported(self): + required_variables = [ + 'tenant_id', 'instance_id', 'ip_address', + 'ip_address_octet1', 'ip_address_octet2', 'ip_address_octet3', + 'ip_address_octet4', 'subnet_id', 'subnet_name', 'user_id', + 'network_id', 'network_name' + ] + + pattern = '.'.join(['{%s}' % v for v in required_variables]) + context = mock.Mock() + context.user_id = 'user-id' + subnet = { + 'network_id': 'some-net-id', + 'tenant_id': 'some-tenant-id', + 'id': 'some-subnet-id', + 'name': 'some-subnet-name' + } + port = { + 'id': 'some-port-id', + 'device_id': 'some-device-id' + } + ip_addr = '10.0.0.3' + + pb = config.PatternBuilder(pattern) + + try: + pb.build(context, subnet, port, ip_addr) + except exceptions.InvalidPattern as e: + self.fail('Unexpected exception: {}'.format(e)) diff --git a/neutron/tests/unit/ipam/test_base.py b/neutron/tests/unit/ipam/test_base.py new file mode 100644 index 0000000..24a7a8a --- /dev/null +++ b/neutron/tests/unit/ipam/test_base.py @@ -0,0 +1,181 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2014 OpenStack LLC. +# 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 mock + +from neutron.common import exceptions as q_exc +from neutron.ipam.drivers import neutron_ipam +from neutron.tests import base + + +class SubnetCreateTestCase(base.BaseTestCase): + def test_subnet_is_created(self): + dns_controller = mock.Mock() + dhcp_controller = mock.Mock() + ipam_controller = mock.MagicMock() + db_manager = mock.Mock() + context = mock.MagicMock() + subnet = mock.Mock() + + ipam_driver = neutron_ipam.NeutronIPAM( + dhcp_controller=dhcp_controller, + dns_controller=dns_controller, + ipam_controller=ipam_controller, + db_mgr=db_manager) + + ipam_driver.create_subnet(context, subnet) + + assert ipam_controller.create_subnet.called_once + + def test_dhcp_is_configured(self): + dns_controller = mock.Mock() + dhcp_controller = mock.Mock() + ipam_controller = mock.MagicMock() + db_manager = mock.Mock() + context = mock.MagicMock() + subnet = mock.Mock() + + ipam_driver = neutron_ipam.NeutronIPAM( + dhcp_controller=dhcp_controller, + dns_controller=dns_controller, + ipam_controller=ipam_controller, + db_mgr=db_manager) + + ipam_driver.create_subnet(context, subnet) + + assert dhcp_controller.configure_dhcp.called_once + + +class SubnetDeleteTestCase(base.BaseTestCase): + def test_dns_zones_are_deleted(self): + dns_controller = mock.Mock() + dhcp_controller = mock.Mock() + ipam_controller = mock.Mock() + db_manager = mock.Mock() + context = mock.MagicMock() + subnet_id = mock.Mock() + + ipam_driver = neutron_ipam.NeutronIPAM( + dhcp_controller=dhcp_controller, + dns_controller=dns_controller, + ipam_controller=ipam_controller, + db_mgr=db_manager) + + db_manager.subnet_has_ports_allocated.return_value = False + db_manager.get_subnet_ports.return_value = [] + ipam_driver.delete_subnet(context, subnet_id) + + assert dns_controller.delete_dns_zones.called_once + + def test_dhcp_gets_disabled(self): + dns_controller = mock.Mock() + dhcp_controller = mock.Mock() + ipam_controller = mock.Mock() + db_manager = mock.Mock() + context = mock.MagicMock() + subnet_id = mock.Mock() + + ipam_driver = neutron_ipam.NeutronIPAM( + dhcp_controller=dhcp_controller, + dns_controller=dns_controller, + ipam_controller=ipam_controller, + db_mgr=db_manager) + + db_manager.subnet_has_ports_allocated.return_value = False + db_manager.get_subnet_ports.return_value = [] + ipam_driver.delete_subnet(context, subnet_id) + + assert dhcp_controller.disable_dhcp.called_once + + def test_subnet_is_deleted_from_ipam(self): + dns_controller = mock.Mock() + dhcp_controller = mock.Mock() + ipam_controller = mock.Mock() + db_manager = mock.Mock() + context = mock.MagicMock() + subnet_id = mock.Mock() + + ipam_driver = neutron_ipam.NeutronIPAM( + dhcp_controller=dhcp_controller, + dns_controller=dns_controller, + ipam_controller=ipam_controller, + db_mgr=db_manager) + + db_manager.subnet_has_ports_allocated.return_value = False + db_manager.get_subnet_ports.return_value = [] + ipam_driver.delete_subnet(context, subnet_id) + + assert ipam_controller.delete_subnet.called_once + + def test_raises_error_if_subnet_has_active_ports(self): + dns_controller = mock.Mock() + dhcp_controller = mock.Mock() + ipam_controller = mock.MagicMock() + db_manager = mock.Mock() + context = mock.MagicMock() + subnet_id = mock.Mock() + + ipam_driver = neutron_ipam.NeutronIPAM( + dhcp_controller=dhcp_controller, + dns_controller=dns_controller, + ipam_controller=ipam_controller, + db_mgr=db_manager) + + db_manager.subnet_has_ports_allocated.return_value = True + db_manager.get_subnet_ports.return_value = [mock.MagicMock()] + self.assertRaises(q_exc.SubnetInUse, ipam_driver.delete_subnet, + context, subnet_id) + + +class AllocateIPTestCase(base.BaseTestCase): + def test_allocates_ip(self): + dns_controller = mock.Mock() + dhcp_controller = mock.Mock() + ipam_controller = mock.Mock() + db_manager = mock.Mock() + context = mock.MagicMock() + host = mock.MagicMock() + ip = mock.Mock() + + ipam_driver = neutron_ipam.NeutronIPAM( + dhcp_controller=dhcp_controller, + dns_controller=dns_controller, + ipam_controller=ipam_controller, + db_mgr=db_manager) + + ipam_driver.allocate_ip(context, host, ip) + + assert ipam_controller.allocate_ip.called_once + + def test_binds_mac(self): + dns_controller = mock.Mock() + dhcp_controller = mock.Mock() + ipam_controller = mock.Mock() + db_manager = mock.Mock() + context = mock.MagicMock() + host = mock.MagicMock() + ip = mock.Mock() + + ipam_driver = neutron_ipam.NeutronIPAM( + dhcp_controller=dhcp_controller, + dns_controller=dns_controller, + ipam_controller=ipam_controller, + db_mgr=db_manager) + + ipam_driver.allocate_ip(context, host, ip) + + assert dhcp_controller.bind_mac.called_once diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index 1f91e74..812fb2a 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -20,6 +20,7 @@ import itertools import mock from oslo.config import cfg from testtools import matchers +from testtools import testcase import webob.exc import neutron @@ -33,8 +34,10 @@ from neutron.common import ipv6_utils from neutron.common import test_lib from neutron.common import utils from neutron import context +from neutron.db import api as db from neutron.db import db_base_plugin_v2 from neutron.db import models_v2 +from neutron.ipam.drivers import neutron_ipam from neutron import manager from neutron.openstack.common import importutils from neutron.tests import base @@ -90,6 +93,12 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, if not plugin: plugin = DB_PLUGIN_KLASS + # Create the default configurations + args = ['--config-file', etcdir('neutron.conf.test')] + # If test_config specifies some config-file, use it, as well + for config_file in test_config.get('config_files', []): + args.extend(['--config-file', config_file]) + config.parse(args=args) # Update the plugin self.setup_coreplugin(plugin) cfg.CONF.set_override( @@ -145,6 +154,9 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, self._skip_native_pagination = None self._skip_native_sortin = None self.ext_api = None + # NOTE(jkoelker) for a 'pluggable' framework, Neutron sure + # doesn't like when the plugin changes ;) + db.clear_db() # Restore the original attribute map attributes.RESOURCE_ATTRIBUTE_MAP = self._attribute_map_bk super(NeutronDbPluginV2TestCase, self).tearDown() @@ -521,10 +533,17 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, def network(self, name='net1', admin_state_up=True, fmt=None, + do_delete=True, **kwargs): network = self._make_network(fmt or self.fmt, name, admin_state_up, **kwargs) yield network + if do_delete: + # The do_delete parameter allows you to control whether the + # created network is immediately deleted again. Therefore, this + # function is also usable in tests, which require the creation + # of many networks. + self._delete('networks', network['network']['id']) @contextlib.contextmanager def subnet(self, network=None, @@ -537,6 +556,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, dns_nameservers=None, host_routes=None, shared=None, + do_delete=True, ipv6_ra_mode=None, ipv6_address_mode=None): with optional_ctx(network, self.network) as network_to_use: @@ -553,13 +573,18 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, ipv6_ra_mode=ipv6_ra_mode, ipv6_address_mode=ipv6_address_mode) yield subnet + if do_delete: + self._delete('subnets', subnet['subnet']['id']) @contextlib.contextmanager - def port(self, subnet=None, fmt=None, **kwargs): + def port(self, subnet=None, fmt=None, no_delete=False, + **kwargs): with optional_ctx(subnet, self.subnet) as subnet_to_use: net_id = subnet_to_use['subnet']['network_id'] port = self._make_port(fmt or self.fmt, net_id, **kwargs) yield port + if not no_delete: + self._delete('ports', port['port']['id']) def _test_list_with_sort(self, resource, items, sorts, resources=None, query_params=''): @@ -576,7 +601,8 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, resource = resource.replace('-', '_') resources = resources.replace('-', '_') expected_res = [item[resource]['id'] for item in items] - self.assertEqual(expected_res, [n['id'] for n in res[resources]]) + self.assertEqual(sorted([n['id'] for n in res[resources]]), + sorted(expected_res)) def _test_list_with_pagination(self, resource, items, sort, limit, expected_page_num, @@ -610,8 +636,9 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, self.assertEqual(len(res[resources]), limit) self.assertEqual(expected_page_num, page_num) - self.assertEqual([item[resource][verify_key] for item in items], - [n[verify_key] for n in items_res]) + self.assertEqual(sorted([item[resource][verify_key] + for item in items]), + sorted([n[verify_key] for n in items_res])) def _test_list_with_pagination_reverse(self, resource, items, sort, limit, expected_page_num, @@ -650,7 +677,8 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, self.assertEqual(expected_page_num, page_num) expected_res = [item[resource]['id'] for item in items] expected_res.reverse() - self.assertEqual(expected_res, [n['id'] for n in item_res]) + self.assertEqual(sorted(expected_res), + sorted([n['id'] for n in item_res])) class TestBasicGet(NeutronDbPluginV2TestCase): @@ -777,7 +805,7 @@ class TestPortsV2(NeutronDbPluginV2TestCase): self.assertEqual('myname', port['port']['name']) def test_create_port_as_admin(self): - with self.network() as network: + with self.network(do_delete=False) as network: self._create_port(self.fmt, network['network']['id'], webob.exc.HTTPCreated.code, @@ -989,17 +1017,20 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s 'neutron.api.v2.base.Controller._get_sorting_helper', new=_fake_get_sorting_helper) helper_patcher.start() - cfg.CONF.set_default('allow_overlapping_ips', True) - with contextlib.nested(self.port(admin_state_up='True', - mac_address='00:00:00:00:00:01'), - self.port(admin_state_up='False', - mac_address='00:00:00:00:00:02'), - self.port(admin_state_up='False', - mac_address='00:00:00:00:00:03') - ) as (port1, port2, port3): - self._test_list_with_sort('port', (port3, port2, port1), - [('admin_state_up', 'asc'), - ('mac_address', 'desc')]) + try: + cfg.CONF.set_default('allow_overlapping_ips', True) + with contextlib.nested(self.port(admin_state_up='True', + mac_address='00:00:00:00:00:01'), + self.port(admin_state_up='False', + mac_address='00:00:00:00:00:02'), + self.port(admin_state_up='False', + mac_address='00:00:00:00:00:03') + ) as (port1, port2, port3): + self._test_list_with_sort('port', (port3, port2, port1), + [('admin_state_up', 'asc'), + ('mac_address', 'desc')]) + finally: + helper_patcher.stop() def test_list_ports_with_pagination_native(self): if self._skip_native_pagination: @@ -1018,14 +1049,17 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s 'neutron.api.v2.base.Controller._get_pagination_helper', new=_fake_get_pagination_helper) helper_patcher.start() - cfg.CONF.set_default('allow_overlapping_ips', True) - with contextlib.nested(self.port(mac_address='00:00:00:00:00:01'), - self.port(mac_address='00:00:00:00:00:02'), - self.port(mac_address='00:00:00:00:00:03') - ) as (port1, port2, port3): - self._test_list_with_pagination('port', - (port1, port2, port3), - ('mac_address', 'asc'), 2, 2) + try: + cfg.CONF.set_default('allow_overlapping_ips', True) + with contextlib.nested(self.port(mac_address='00:00:00:00:00:01'), + self.port(mac_address='00:00:00:00:00:02'), + self.port(mac_address='00:00:00:00:00:03') + ) as (port1, port2, port3): + self._test_list_with_pagination('port', + (port1, port2, port3), + ('mac_address', 'asc'), 2, 2) + finally: + helper_patcher.stop() def test_list_ports_with_pagination_reverse_native(self): if self._skip_native_pagination: @@ -1045,15 +1079,18 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s 'neutron.api.v2.base.Controller._get_pagination_helper', new=_fake_get_pagination_helper) helper_patcher.start() - cfg.CONF.set_default('allow_overlapping_ips', True) - with contextlib.nested(self.port(mac_address='00:00:00:00:00:01'), - self.port(mac_address='00:00:00:00:00:02'), - self.port(mac_address='00:00:00:00:00:03') - ) as (port1, port2, port3): - self._test_list_with_pagination_reverse('port', - (port1, port2, port3), - ('mac_address', 'asc'), - 2, 2) + try: + cfg.CONF.set_default('allow_overlapping_ips', True) + with contextlib.nested(self.port(mac_address='00:00:00:00:00:01'), + self.port(mac_address='00:00:00:00:00:02'), + self.port(mac_address='00:00:00:00:00:03') + ) as (port1, port2, port3): + self._test_list_with_pagination_reverse('port', + (port1, port2, port3), + ('mac_address', 'asc'), + 2, 2) + finally: + helper_patcher.stop() def test_show_port(self): with self.port() as port: @@ -1062,7 +1099,7 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s self.assertEqual(port['port']['id'], sport['port']['id']) def test_delete_port(self): - with self.port() as port: + with self.port(no_delete=True) as port: self._delete('ports', port['port']['id']) self._show('ports', port['port']['id'], expected_code=webob.exc.HTTPNotFound.code) @@ -1309,8 +1346,9 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s def fake_gen_mac(context, net_id): raise n_exc.MacAddressGenerationFailure(net_id=net_id) - with mock.patch.object(neutron.db.db_base_plugin_v2.NeutronDbPluginV2, - '_generate_mac', new=fake_gen_mac): + with mock.patch.object( + neutron.db.db_base_plugin_v2.NeutronCorePluginV2, + '_generate_mac', new=fake_gen_mac): res = self._create_network(fmt=self.fmt, name='net1', admin_state_up=True) network = self.deserialize(self.fmt, res) @@ -1333,6 +1371,32 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s res = self._create_port(self.fmt, net_id=net_id, **kwargs) self.assertEqual(res.status_int, webob.exc.HTTPConflict.code) + def test_requested_subnet_delete(self): + with self.subnet() as subnet: + with self.port(subnet=subnet) as port: + ips = port['port']['fixed_ips'] + self.assertEqual(len(ips), 1) + self.assertEqual(ips[0]['ip_address'], '10.0.0.2') + self.assertEqual(ips[0]['subnet_id'], subnet['subnet']['id']) + req = self.new_delete_request('subnet', + subnet['subnet']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, webob.exc.HTTPNotFound.code) + + def test_generated_duplicate_ip_ipv6(self): + with self.subnet(ip_version=6, + cidr="2014::/64", + ipv6_address_mode=constants.IPV6_SLAAC) as subnet: + with self.port(subnet=subnet, + fixed_ips=[{'subnet_id': subnet['subnet']['id'], + 'ip_address': + "2014::1322:33ff:fe44:5566"}]) as port: + # Check configuring of duplicate IP + kwargs = {"mac_address": "11:22:33:44:55:66"} + net_id = port['port']['network_id'] + res = self._create_port(self.fmt, net_id=net_id, **kwargs) + self.assertEqual(res.status_int, webob.exc.HTTPConflict.code) + def test_requested_subnet_id(self): with self.subnet() as subnet: with self.port(subnet=subnet) as port: @@ -1785,8 +1849,8 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s ctx = context.get_admin_context() with self.subnet() as subnet: with contextlib.nested( - self.port(subnet=subnet, device_id='owner1'), - self.port(subnet=subnet, device_id='owner1'), + self.port(subnet=subnet, device_id='owner1', no_delete=True), + self.port(subnet=subnet, device_id='owner1', no_delete=True), self.port(subnet=subnet, device_id='owner2'), ) as (p1, p2, p3): network_id = subnet['subnet']['network_id'] @@ -1803,7 +1867,7 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s ctx = context.get_admin_context() with self.subnet() as subnet: with contextlib.nested( - self.port(subnet=subnet, device_id='owner1'), + self.port(subnet=subnet, device_id='owner1', no_delete=True), self.port(subnet=subnet, device_id='owner1'), self.port(subnet=subnet, device_id='owner2'), ) as (p1, p2, p3): @@ -2199,16 +2263,19 @@ class TestNetworksV2(NeutronDbPluginV2TestCase): 'neutron.api.v2.base.Controller._get_sorting_helper', new=_fake_get_sorting_helper) helper_patcher.start() - with contextlib.nested(self.network(admin_status_up=True, - name='net1'), - self.network(admin_status_up=False, - name='net2'), - self.network(admin_status_up=False, - name='net3') - ) as (net1, net2, net3): - self._test_list_with_sort('network', (net3, net2, net1), - [('admin_state_up', 'asc'), - ('name', 'desc')]) + try: + with contextlib.nested(self.network(admin_status_up=True, + name='net1'), + self.network(admin_status_up=False, + name='net2'), + self.network(admin_status_up=False, + name='net3') + ) as (net1, net2, net3): + self._test_list_with_sort('network', (net3, net2, net1), + [('admin_state_up', 'asc'), + ('name', 'desc')]) + finally: + helper_patcher.stop() def test_list_networks_with_pagination_native(self): if self._skip_native_pagination: @@ -2226,31 +2293,37 @@ class TestNetworksV2(NeutronDbPluginV2TestCase): 'neutron.api.v2.base.Controller._get_pagination_helper', new=_fake_get_pagination_helper) helper_patcher.start() - with contextlib.nested(self.network(name='net1'), - self.network(name='net2'), - self.network(name='net3') - ) as (net1, net2, net3): - self._test_list_with_pagination('network', - (net1, net2, net3), - ('name', 'asc'), 2, 2) + try: + with contextlib.nested(self.network(name='net1'), + self.network(name='net2'), + self.network(name='net3') + ) as (net1, net2, net3): + self._test_list_with_pagination('network', + (net1, net2, net3), + ('name', 'asc'), 2, 2) + finally: + helper_patcher.stop() def test_list_networks_without_pk_in_fields_pagination_emulated(self): helper_patcher = mock.patch( 'neutron.api.v2.base.Controller._get_pagination_helper', new=_fake_get_pagination_helper) helper_patcher.start() - with contextlib.nested(self.network(name='net1', - shared=True), - self.network(name='net2', - shared=False), - self.network(name='net3', - shared=True) - ) as (net1, net2, net3): - self._test_list_with_pagination('network', - (net1, net2, net3), - ('name', 'asc'), 2, 2, - query_params="fields=name", - verify_key='name') + try: + with contextlib.nested(self.network(name='net1', + shared=True), + self.network(name='net2', + shared=False), + self.network(name='net3', + shared=True) + ) as (net1, net2, net3): + self._test_list_with_pagination('network', + (net1, net2, net3), + ('name', 'asc'), 2, 2, + query_params="fields=name", + verify_key='name') + finally: + helper_patcher.stop() def test_list_networks_without_pk_in_fields_pagination_native(self): if self._skip_native_pagination: @@ -2281,13 +2354,16 @@ class TestNetworksV2(NeutronDbPluginV2TestCase): 'neutron.api.v2.base.Controller._get_pagination_helper', new=_fake_get_pagination_helper) helper_patcher.start() - with contextlib.nested(self.network(name='net1'), - self.network(name='net2'), - self.network(name='net3') - ) as (net1, net2, net3): - self._test_list_with_pagination_reverse('network', - (net1, net2, net3), - ('name', 'asc'), 2, 2) + try: + with contextlib.nested(self.network(name='net1'), + self.network(name='net2'), + self.network(name='net3') + ) as (net1, net2, net3): + self._test_list_with_pagination_reverse('network', + (net1, net2, net3), + ('name', 'asc'), 2, 2) + finally: + helper_patcher.stop() def test_list_networks_with_parameters(self): with contextlib.nested(self.network(name='net1', @@ -2667,8 +2743,8 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): with self.network() as network: with contextlib.nested( self.subnet(network=network), - self.subnet(network=network, cidr='10.0.1.0/24'), - ) as (subnet1, subnet2): + self.subnet(network=network, cidr='10.0.1.0/24', + do_delete=False)) as (subnet1, subnet2): subnet1_id = subnet1['subnet']['id'] subnet2_id = subnet2['subnet']['id'] with self.port( @@ -2734,7 +2810,7 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): set_context=True) def test_create_subnet_as_admin(self): - with self.network() as network: + with self.network(do_delete=False) as network: self._create_subnet(self.fmt, network['network']['id'], '10.0.2.0/24', @@ -3220,6 +3296,16 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): res = subnet_req.get_response(self.api) self.assertEqual(res.status_int, webob.exc.HTTPClientError.code) + def test_create_subnet_ipv6_attributes(self): + gateway_ip = 'fe80::1' + cidr = 'fe80::/80' + + for mode in constants.IPV6_MODES: + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode=mode, + ipv6_address_mode=mode) + def _test_validate_subnet_ipv6_modes(self, cur_subnet=None, expect_success=True, **modes): plugin = manager.NeutronManager.get_plugin() @@ -3325,6 +3411,31 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): self.assertEqual(ctx_manager.exception.code, webob.exc.HTTPClientError.code) + def test_create_subnet_invalid_ipv6_combination(self): + gateway_ip = 'fe80::1' + cidr = 'fe80::/80' + with testlib_api.ExpectedException( + webob.exc.HTTPClientError) as ctx_manager: + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode='stateful', + ipv6_address_mode='stateless') + self.assertEqual(ctx_manager.exception.code, + webob.exc.HTTPClientError.code) + + def test_create_subnet_ipv6_single_attribute_set(self): + gateway_ip = 'fe80::1' + cidr = 'fe80::/80' + for mode in constants.IPV6_MODES: + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode=None, + ipv6_address_mode=mode) + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode=mode, + ipv6_address_mode=None) + def test_create_subnet_ipv6_ra_mode_ip_version_4(self): cidr = '10.0.2.0/24' with testlib_api.ExpectedException( @@ -3700,18 +3811,21 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): 'neutron.api.v2.base.Controller._get_sorting_helper', new=_fake_get_sorting_helper) helper_patcher.start() - with contextlib.nested(self.subnet(enable_dhcp=True, - cidr='10.0.0.0/24'), - self.subnet(enable_dhcp=False, - cidr='11.0.0.0/24'), - self.subnet(enable_dhcp=False, - cidr='12.0.0.0/24') - ) as (subnet1, subnet2, subnet3): - self._test_list_with_sort('subnet', (subnet3, - subnet2, - subnet1), - [('enable_dhcp', 'asc'), - ('cidr', 'desc')]) + try: + with contextlib.nested(self.subnet(enable_dhcp=True, + cidr='10.0.0.0/24'), + self.subnet(enable_dhcp=False, + cidr='11.0.0.0/24'), + self.subnet(enable_dhcp=False, + cidr='12.0.0.0/24') + ) as (subnet1, subnet2, subnet3): + self._test_list_with_sort('subnet', (subnet3, + subnet2, + subnet1), + [('enable_dhcp', 'asc'), + ('cidr', 'desc')]) + finally: + helper_patcher.stop() def test_list_subnets_with_pagination_native(self): if self._skip_native_pagination: @@ -3729,13 +3843,16 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): 'neutron.api.v2.base.Controller._get_pagination_helper', new=_fake_get_pagination_helper) helper_patcher.start() - with contextlib.nested(self.subnet(cidr='10.0.0.0/24'), - self.subnet(cidr='11.0.0.0/24'), - self.subnet(cidr='12.0.0.0/24') - ) as (subnet1, subnet2, subnet3): - self._test_list_with_pagination('subnet', - (subnet1, subnet2, subnet3), - ('cidr', 'asc'), 2, 2) + try: + with contextlib.nested(self.subnet(cidr='10.0.0.0/24'), + self.subnet(cidr='11.0.0.0/24'), + self.subnet(cidr='12.0.0.0/24') + ) as (subnet1, subnet2, subnet3): + self._test_list_with_pagination('subnet', + (subnet1, subnet2, subnet3), + ('cidr', 'asc'), 2, 2) + finally: + helper_patcher.stop() def test_list_subnets_with_pagination_reverse_native(self): if self._skip_native_sorting: @@ -3754,14 +3871,17 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): 'neutron.api.v2.base.Controller._get_pagination_helper', new=_fake_get_pagination_helper) helper_patcher.start() - with contextlib.nested(self.subnet(cidr='10.0.0.0/24'), - self.subnet(cidr='11.0.0.0/24'), - self.subnet(cidr='12.0.0.0/24') - ) as (subnet1, subnet2, subnet3): - self._test_list_with_pagination_reverse('subnet', - (subnet1, subnet2, - subnet3), - ('cidr', 'asc'), 2, 2) + try: + with contextlib.nested(self.subnet(cidr='10.0.0.0/24'), + self.subnet(cidr='11.0.0.0/24'), + self.subnet(cidr='12.0.0.0/24') + ) as (subnet1, subnet2, subnet3): + self._test_list_with_pagination_reverse('subnet', + (subnet1, subnet2, + subnet3), + ('cidr', 'asc'), 2, 2) + finally: + helper_patcher.stop() def test_invalid_ip_version(self): with self.network() as network: @@ -4074,9 +4194,9 @@ class TestNeutronDbPluginV2(base.BaseTestCase): """Unit Tests for NeutronDbPluginV2 IPAM Logic.""" def test_generate_ip(self): - with mock.patch.object(db_base_plugin_v2.NeutronDbPluginV2, + with mock.patch.object(db_base_plugin_v2.NeutronCorePluginV2, '_try_generate_ip') as generate: - with mock.patch.object(db_base_plugin_v2.NeutronDbPluginV2, + with mock.patch.object(db_base_plugin_v2.NeutronCorePluginV2, '_rebuild_availability_ranges') as rebuild: db_base_plugin_v2.NeutronDbPluginV2._generate_ip('c', 's') @@ -4085,15 +4205,22 @@ class TestNeutronDbPluginV2(base.BaseTestCase): self.assertEqual(0, rebuild.call_count) def test_generate_ip_exhausted_pool(self): - with mock.patch.object(db_base_plugin_v2.NeutronDbPluginV2, + with mock.patch.object(db_base_plugin_v2.NeutronCorePluginV2, '_try_generate_ip') as generate: - with mock.patch.object(db_base_plugin_v2.NeutronDbPluginV2, + with mock.patch.object(db_base_plugin_v2.NeutronCorePluginV2, '_rebuild_availability_ranges') as rebuild: exception = n_exc.IpAddressGenerationFailure(net_id='n') - # fail first call but not second - generate.side_effect = [exception, None] - db_base_plugin_v2.NeutronDbPluginV2._generate_ip('c', 's') + generate.side_effect = exception + + # I want the side_effect to throw an exception once but I + # didn't see a way to do this. So, let it throw twice and + # catch the second one. Check below to ensure that + # _try_generate_ip was called twice. + try: + db_base_plugin_v2.NeutronDbPluginV2._generate_ip('c', 's') + except n_exc.IpAddressGenerationFailure: + pass self.assertEqual(2, generate.call_count) rebuild.assert_called_once_with('c', 's') @@ -4236,6 +4363,12 @@ class NeutronDbPluginV2AsMixinTestCase(testlib_api.SqlTestCase): def setUp(self): super(NeutronDbPluginV2AsMixinTestCase, self).setUp() + + mock_nm_get_ipam = mock.patch( + 'neutron.manager.NeutronManager.get_ipam') + self.mock_nm_get_ipam = mock_nm_get_ipam.start() + self.mock_nm_get_ipam.return_value = neutron_ipam.NeutronIPAM() + self.plugin = importutils.import_object(DB_PLUGIN_KLASS) self.context = context.get_admin_context() self.net_data = {'network': {'id': 'fake-id', @@ -4244,6 +4377,9 @@ class NeutronDbPluginV2AsMixinTestCase(testlib_api.SqlTestCase): 'tenant_id': 'test-tenant', 'shared': False}} + self.addCleanup(mock_nm_get_ipam.stop) + self.addCleanup(db.clear_db) + def test_create_network_with_default_status(self): net = self.plugin.create_network(self.context, self.net_data) default_net_create_status = 'ACTIVE' diff --git a/neutron/tests/unit/test_linux_dhcp_relay.py b/neutron/tests/unit/test_linux_dhcp_relay.py new file mode 100644 index 0000000..8e24ae5 --- /dev/null +++ b/neutron/tests/unit/test_linux_dhcp_relay.py @@ -0,0 +1,537 @@ +# Copyright 2014 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. + +import __builtin__ +import os + +import mock + +from neutron.agent.common import config +from neutron.agent.linux import dhcp +from neutron.agent.linux import dhcp_relay +from neutron.agent.linux import interface +from neutron.agent.linux import utils +from neutron.common import config as base_config +from neutron.common import exceptions as exc +from neutron.tests import base + + +DHCP_RELAY_IP = '192.168.122.32' +DNS_RELAY_IP = '192.168.122.32' + + +class FakeDeviceManager(): + def __init__(self, conf, root_helper, plugin): + pass + + def get_interface_name(self, network, port): + pass + + def get_device_id(self, network): + pass + + def setup_dhcp_port(self, network): + pass + + def setup(self, network, reuse_existing=False): + pass + + def update(self, network): + pass + + def destroy(self, network, device_name): + pass + + def setup_relay(self, network, iface_name, mac_address, relay_bridge): + pass + + def destroy_relay(self, network, device_name, relay_bridge): + pass + + +class FakeIPAllocation: + def __init__(self, address): + self.ip_address = address + + +class FakePort1: + id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + admin_state_up = True + fixed_ips = [FakeIPAllocation('192.168.0.2')] + mac_address = '00:00:80:aa:bb:cc' + + def __init__(self): + self.extra_dhcp_opts = [] + + +class FakeV4HostRoute: + destination = '20.0.0.1/24' + nexthop = '20.0.0.1' + + +class FakeV4Subnet: + id = 'dddddddd-dddd-dddd-dddd-dddddddddddd' + ip_version = 4 + cidr = '192.168.0.0/24' + gateway_ip = '192.168.0.1' + enable_dhcp = True + host_routes = [FakeV4HostRoute] + dns_nameservers = ['8.8.8.8'] + + +class FakeV4Network: + id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + tenant_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + subnets = [FakeV4Subnet()] + ports = [FakePort1()] + namespace = 'qdhcp-ns' + dhcp_relay_ip = DHCP_RELAY_IP + dns_relay_ip = DNS_RELAY_IP + + +class FakeOpen(): + def __init__(self, *args): + pass + + def read(self): + return 'dhcrelay -a -i tap77777777-77 %s' % DHCP_RELAY_IP + + +class TestBase(base.BaseTestCase): + def setUp(self): + super(TestBase, self).setUp() + self.network = FakeV4Network() + root = os.path.dirname(os.path.dirname(__file__)) + args = ['--config-file', + os.path.join(root, 'etc', 'neutron.conf.test')] + self.conf = config.setup_conf() + self.conf.register_opts(base_config.core_opts) + self.conf.register_opts(dhcp.OPTS) + self.conf.register_opts(dhcp_relay.OPTS) + self.conf.register_opts(interface.OPTS) + + def dhcp_dns_proxy_init_mock(self, conf, network, root_helper='sudo', + version=None, plugin=None): + super(dhcp_relay.DhcpDnsProxy, self).__init__( + conf, network, + root_helper, + version, plugin) + + external_dhcp_servers = self._get_relay_ips( + 'external_dhcp_servers') + external_dns_servers = self._get_relay_ips('external_dns_servers') + required_options = { + 'dhcp_relay_bridge': self.conf.dhcp_relay_bridge, + 'external_dhcp_servers': external_dhcp_servers, + 'external_dns_servers': external_dns_servers + } + + for option_name, option in required_options.iteritems(): + if not option: + raise exc.InvalidConfigurationOption( + opt_name=option_name, + opt_value=option + ) + + self.dev_name_len = self._calc_dev_name_len() + self.device_manager = mock.Mock() + + dhcp_dns_proxy_mock = mock.patch( + "neutron.agent.linux.dhcp_relay.DhcpDnsProxy.__init__", + dhcp_dns_proxy_init_mock + ) + dhcp_dns_proxy_mock.start() + + device_manager_init = mock.patch( + "neutron.agent.linux.dhcp.DeviceManager.__init__", + lambda *args, **kwargs: None) + device_manager_init.start() + + self.conf(args=args) + self.conf.set_override('state_path', '') + self.conf.dhcp_relay_bridge = 'br-dhcp' + + self.replace_p = mock.patch('neutron.agent.linux.utils.replace_file') + self.execute_p = mock.patch('neutron.agent.linux.utils.execute') + self.addCleanup(self.replace_p.stop) + self.addCleanup(self.execute_p.stop) + self.safe = self.replace_p.start() + self.execute = self.execute_p.start() + + def get_fixed_ddi_proxy(self): + def _get_relay_ips(self, data): + return ['192.168.122.32'] + + attrs_to_mock = { + '_get_relay_ips': _get_relay_ips + } + + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock): + return dhcp_relay.DhcpDnsProxy + + +class TestDnsDhcpProxy(TestBase): + + def test_create_instance_no_dhcp_relay_server(self): + self.conf.dhcp_relay_ip = None + self.network.dhcp_relay_ip = None + self.assertRaises(exc.InvalidConfigurationOption, + dhcp_relay.DhcpDnsProxy, + self.conf, + self.network) + + def test_create_instance_bad_dhcp_relay_server(self): + self.conf.dhcp_relay_ip = '192.168.122.322' + self.assertRaises(exc.InvalidConfigurationOption, + dhcp_relay.DhcpDnsProxy, + self.conf, + self.network) + + def test_create_instance_no_dns_relay_server(self): + self.conf.dns_relay_ip = None + self.network.dhcp_relay_ip = None + self.assertRaises(exc.InvalidConfigurationOption, + dhcp_relay.DhcpDnsProxy, + self.conf, + self.network) + + def test_create_instance_bad_dns_relay_server(self): + self.conf.dns_relay_ip = '192.168.122.322' + self.assertRaises(exc.InvalidConfigurationOption, + dhcp_relay.DhcpDnsProxy, + self.conf, + self.network) + + def test_create_instance_no_relay_bridge(self): + self.conf.dhcp_relay_bridge = None + self.assertRaises(exc.InvalidConfigurationOption, + dhcp_relay.DhcpDnsProxy, + self.conf, + self.network) + + @mock.patch.object(dhcp_relay.DhcpDnsProxy, "_spawn_dhcp_proxy") + @mock.patch.object(dhcp_relay.DhcpDnsProxy, "_spawn_dns_proxy") + def test_spawn(self, mock_spawn_dns, mock_spawn_dhcp): + attrs_to_mock = { + '_get_relay_ips': mock.Mock(return_value=['192.168.122.32']) + } + + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock): + dm = dhcp_relay.DhcpDnsProxy(self.conf, self.network) + dm.spawn_process() + + mock_spawn_dns.assert_called_once_with() + mock_spawn_dhcp.assert_called_once_with() + + @mock.patch.object(dhcp.DhcpLocalProcess, 'get_conf_file_name') + def test__spawn_dhcp_proxy(self, get_conf_file_name_mock): + test_dns_conf_path = 'test_dir/dhcp/test_dns_pid' + get_conf_file_name_mock.return_value = test_dns_conf_path + + expected = [ + 'ip', + 'netns', + 'exec', + 'qdhcp-ns', + 'dhcrelay', + '-4', + '-a', + '-pf', + test_dns_conf_path, + '-i', + 'tap0', + '-o', + 'trelaaaaaaaa-a', + self.network.dhcp_relay_ip + ] + + self.execute.return_value = ('', '') + + attrs_to_mock = dict( + [(a, mock.DEFAULT) for a in + ['interface_name', '_get_relay_ips']] + ) + + attrs_to_mock.update({ + '_get_relay_ips': mock.Mock(return_value=['192.168.122.32']) + }) + + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock) as mocks: + mocks['interface_name'].__get__ = mock.Mock(return_value='tap0') + + dm = dhcp_relay.DhcpDnsProxy(self.conf, self.network) + dm._spawn_dhcp_proxy() + self.execute.assert_called_once_with(expected, + root_helper='sudo', + check_exit_code=True) + + @mock.patch.object(dhcp.DhcpLocalProcess, 'get_conf_file_name') + def test__spawn_dhcp_proxy_no_namespace(self, get_conf_file_name_mock): + test_dns_conf_path = 'test_dir/dhcp/test_dns_pid' + get_conf_file_name_mock.return_value = test_dns_conf_path + self.network.namespace = None + + expected = [ + 'dhcrelay', + '-4', + '-a', + '-pf', + test_dns_conf_path, + '-i', + 'tap0', + '-o', + 'trelaaaaaaaa-a', + self.network.dhcp_relay_ip + ] + + self.execute.return_value = ('', '') + + attrs_to_mock = dict( + [(a, mock.DEFAULT) for a in + ['interface_name', '_get_relay_ips']] + ) + + attrs_to_mock.update({ + '_get_relay_ips': mock.Mock(return_value=['192.168.122.32']) + }) + + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock) as mocks: + mocks['interface_name'].__get__ = mock.Mock(return_value='tap0') + + dm = dhcp_relay.DhcpDnsProxy(self.conf, self.network) + dm._spawn_dhcp_proxy() + self.execute.assert_called_once_with(expected, 'sudo') + + @mock.patch.object(dhcp_relay.DhcpDnsProxy, 'interface_name') + @mock.patch.object(dhcp.DhcpLocalProcess, 'get_conf_file_name') + def test__spawn_dns_proxy(self, get_conf_file_name_mock, iface_name_mock): + test_dns_conf_path = 'test_dir/dhcp/test_dns_pid' + get_conf_file_name_mock.return_value = test_dns_conf_path + iface_name_mock.__get__ = mock.Mock(return_value='test_tap0') + expected = [ + 'ip', + 'netns', + 'exec', + 'qdhcp-ns', + 'dnsmasq', + '--no-hosts', + '--no-resolv', + '--strict-order', + '--bind-interfaces', + '--interface=test_tap0', + '--except-interface=lo', + '--all-servers', + '--server=%s' % self.network.dns_relay_ip, + '--pid-file=%s' % test_dns_conf_path + ] + self.execute.return_value = ('', '') + + attrs_to_mock = { + '_get_relay_ips': mock.Mock(return_value=['192.168.122.32']) + } + + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock): + + dm = dhcp_relay.DhcpDnsProxy(self.conf, self.network) + dm._spawn_dns_proxy() + + self.execute.assert_called_once_with(expected, + root_helper='sudo', + check_exit_code=True) + + @mock.patch.object(dhcp_relay.DhcpDnsProxy, 'interface_name') + @mock.patch.object(dhcp.DhcpLocalProcess, 'get_conf_file_name') + def test__spawn_dns_proxy_no_namespace(self, get_conf_file_name_mock, + iface_name_mock): + self.network.namespace = None + test_dns_conf_path = 'test_dir/dhcp/test_dns_pid' + get_conf_file_name_mock.return_value = test_dns_conf_path + iface_name_mock.__get__ = mock.Mock(return_value='test_tap0') + expected = [ + 'dnsmasq', + '--no-hosts', + '--no-resolv', + '--strict-order', + '--bind-interfaces', + '--interface=test_tap0', + '--except-interface=lo', + '--all-servers', + '--server=%s' % self.network.dns_relay_ip, + '--pid-file=%s' % test_dns_conf_path + ] + self.execute.return_value = ('', '') + + attrs_to_mock = { + '_get_relay_ips': mock.Mock(return_value=['192.168.122.32'])} + + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock): + dm = dhcp_relay.DhcpDnsProxy(self.conf, self.network) + dm._spawn_dns_proxy() + + self.execute.assert_called_once_with(expected, 'sudo') + + @mock.patch.object(dhcp_relay, '_generate_mac_address', + mock.Mock(return_value='77:77:77:77:77:77')) + @mock.patch.object(dhcp_relay.DhcpDnsProxy, '_get_relay_device_name', + mock.Mock(return_value='tap-relay77777')) + @mock.patch.object(FakeDeviceManager, 'setup', + mock.Mock(return_value='tap-77777777')) + @mock.patch.object(FakeDeviceManager, 'setup_relay', mock.Mock()) + def test_enable_dhcp_dns_inactive(self): + attrs_to_mock = dict( + [(a, mock.DEFAULT) for a in + ['interface_name', '_get_relay_ips', 'restart', + 'is_dhcp_active', 'is_dns_active', 'spawn_process']] + ) + + attrs_to_mock.update( + {'_get_relay_ips': mock.Mock(return_value=['192.168.122.32']), + 'is_dhcp_active': mock.Mock(return_value=True), + 'is_dns_active': mock.Mock(return_value=True), + 'spawn_process': mock.Mock(return_value=None)} + ) + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock) as mocks: + mocks['interface_name'].__set__ = mock.Mock() + + dr = dhcp_relay.DhcpDnsProxy(self.conf, self.network) + dr.enable() + + dr.device_manager.setup_relay.assert_called_once_with( + self.network, + 'tap-relay77777', + '77:77:77:77:77:77', + self.conf.dhcp_relay_bridge) + dr.device_manager.setup.assert_called_once_with( + self.network, + reuse_existing=True) + + @mock.patch.object(dhcp_relay, '_generate_mac_address', + mock.Mock(return_value='77:77:77:77:77:77')) + @mock.patch.object(dhcp_relay.DhcpDnsProxy, '_get_relay_device_name', + mock.Mock(return_value='tap-relay77777')) + @mock.patch.object(FakeDeviceManager, 'setup', + mock.Mock(return_value='tap-77777777')) + @mock.patch.object(FakeDeviceManager, 'setup_relay', mock.Mock()) + def test_enable_dhcp_dns_active(self): + attrs_to_mock = dict( + [(a, mock.DEFAULT) for a in + ['interface_name', 'is_dhcp_active', 'is_dns_active', 'restart', + '_get_relay_ips']] + ) + + attrs_to_mock.update( + {'_get_relay_ips': mock.Mock(return_value=['192.168.122.32']), + 'is_dhcp_active': mock.Mock(return_value=True), + 'is_dns_active': mock.Mock(return_value=True)} + ) + + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock) as mocks: + mocks['interface_name'].__set__ = mock.Mock() + + dr = dhcp_relay.DhcpDnsProxy(self.conf, self.network) + dr.enable() + + dr.device_manager.setup_relay.assert_called_once_with( + self.network, + 'tap-relay77777', + '77:77:77:77:77:77', + self.conf.dhcp_relay_bridge) + dr.device_manager.setup.assert_called_once_with( + self.network, + reuse_existing=True) + mocks['restart'].assert_any_call() + + def test_disable_retain_port(self): + attrs_to_mock = dict( + [(a, mock.DEFAULT) for a in + ['is_dhcp_active', 'is_dns_active', 'get_dhcp_pid', 'get_dns_pid', + 'restart', '_remove_config_files']] + ) + + attrs_to_mock.update( + {'_get_relay_ips': mock.Mock(return_value=['192.168.122.32']), + 'is_dhcp_active': mock.Mock(return_value=True), + 'is_dns_active': mock.Mock(return_value=True), + 'get_dhcp_pid': mock.Mock(return_value='1111'), + 'get_dns_pid': mock.Mock(return_value='2222')} + ) + + kill_proc_calls = [ + mock.call(['kill', '-9', '1111'], 'sudo'), + mock.call(['kill', '-9', '2222'], 'sudo')] + + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock) as mocks: + dr = dhcp_relay.DhcpDnsProxy(self.conf, self.network) + dr.disable(retain_port=True) + + self.execute.assert_has_calls(kill_proc_calls) + mocks['_remove_config_files'].assert_any_call() + + @mock.patch.object(dhcp_relay.DhcpDnsProxy, '_get_relay_device_name', + mock.Mock(return_value='tap-relay77777')) + @mock.patch.object(FakeDeviceManager, 'destroy', mock.Mock()) + @mock.patch.object(FakeDeviceManager, 'destroy_relay', mock.Mock()) + def test_disable_no_retain_port(self): + attrs_to_mock = dict( + [(a, mock.DEFAULT) for a in + ['is_dhcp_active', 'is_dns_active', 'get_dhcp_pid', + 'get_dhcp_pid', 'interface_name', '_remove_config_files']] + ) + + attrs_to_mock.update( + {'_get_relay_ips': mock.Mock(return_value=['192.168.122.32']), + 'is_dhcp_active': mock.Mock(return_value=True), + 'is_dns_active': mock.Mock(return_value=True), + 'get_dhcp_pid': mock.Mock(return_value='1111'), + 'get_dns_pid': mock.Mock(return_value='2222')} + ) + + kill_proc_calls = [ + mock.call(['kill', '-9', '1111'], 'sudo'), + mock.call(['kill', '-9', '2222'], 'sudo')] + + with mock.patch.multiple(dhcp_relay.DhcpDnsProxy, + **attrs_to_mock) as mocks: + mocks['interface_name'].__get__ = mock.Mock( + return_value='tap-77777777') + + dr = dhcp_relay.DhcpDnsProxy(self.conf, self.network) + dr.disable(retain_port=False) + + self.execute.assert_has_calls(kill_proc_calls) + dr.device_manager.destroy.assert_called_once_with(self.network, + 'tap-77777777') + dr.device_manager.destroy_relay.assert_called_once_with( + self.network, + 'tap-relay77777', + self.conf.dhcp_relay_bridge) + + mocks['_remove_config_files'].assert_any_call() + + def test_generate_mac(self): + mac = dhcp_relay._generate_mac_address() + mac_array = mac.split(':') + self.assertEqual(6, len(mac_array)) + for item in mac_array: + self.assertEqual(2, len(item)) diff --git a/neutron/tests/unit/vmware/test_nsx_sync.py b/neutron/tests/unit/vmware/test_nsx_sync.py index cacfc24..ff8e6bf 100644 --- a/neutron/tests/unit/vmware/test_nsx_sync.py +++ b/neutron/tests/unit/vmware/test_nsx_sync.py @@ -304,6 +304,22 @@ class SyncTestCase(testlib_api.SqlTestCase): super(SyncTestCase, self).setUp() self.addCleanup(self.fc.reset_all) + mock_nm_get_ipam = mock.patch('neutron.manager.NeutronManager.' + 'get_ipam') + self.mock_nm_get_ipam = mock_nm_get_ipam.start() + ipam = mock.Mock() + ipam.create_network = lambda ctx, net: net + + self.allocate_counter = 1 + ipam.allocate_ip = self._allocate_ip_mock + + self.mock_nm_get_ipam.return_value = ipam + self.addCleanup(mock_nm_get_ipam.stop) + + def _allocate_ip_mock(self, *args): + self.allocate_counter += 1 + return '10.20.30.%s' % self.allocate_counter + @contextlib.contextmanager def _populate_data(self, ctx, net_size=2, port_size=2, router_size=2): diff --git a/requirements.txt b/requirements.txt index 9ab21f6..49ab3b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,3 +31,5 @@ oslo.messaging<1.5.0,>=1.4.0 oslo.rootwrap<=1.5.0,>=1.3.0 python-novaclient<=2.20.0,>=2.18.0 + +taskflow>=0.1.3,<=0.3.21 diff --git a/test-requirements.txt b/test-requirements.txt index 04d0e59..9536425 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,3 +16,4 @@ oslosphinx<2.5.0,>=2.2.0 # Apache-2.0 testrepository<=0.0.20,>=0.0.18 testtools!=1.4.0,<=1.5.0,>=0.9.34 WebTest<=2.0.18,>=2.0 +taskflow
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor