text: # Copyright 2014 Cisco Systems, Inc.  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 eventlet
  
  from oslo_config import cfg
  from oslo_log import log as logging
  from sqlalchemy.orm import exc
  from sqlalchemy.sql import expression as expr
  
  from neutron.api.v2 import attributes
  from neutron.common import exceptions as n_exc
  from neutron import context as n_context
  from neutron.db import models_v2
  from neutron.extensions import providernet as pr_net
  from neutron.i18n import _LE, _LI, _LW
  from neutron import manager
  from neutron.plugins.cisco.db.l3 import l3_models
  from neutron.plugins.common import constants
  
  from networking_cisco.plugins.cisco.extensions import n1kv
  import networking_cisco.plugins.cisco.l3.plugging_drivers as plug
  from networking_cisco.plugins.cisco.l3.plugging_drivers import (
      n1kv_plugging_constants as n1kv_const)
  
  LOG = logging.getLogger(__name__)
  
  
  N1KV_TRUNKING_DRIVER_OPTS = [
      cfg.StrOpt('management_port_profile', default='osn_mgmt_pp',
                 help=_("Name of N1kv port profile for management ports.")),
      cfg.StrOpt('t1_port_profile', default='osn_t1_pp',
                 help=_("Name of N1kv port profile for T1 ports (i.e., ports "
                        "carrying traffic from VXLAN segmented networks).")),
      cfg.StrOpt('t2_port_profile', default='osn_t2_pp',
                 help=_("Name of N1kv port profile for T2 ports (i.e., ports "
                        "carrying traffic from VLAN segmented networks).")),
      cfg.StrOpt('t1_network_profile', default='osn_t1_np',
                 help=_("Name of N1kv network profile for T1 networks (i.e., "
                        "trunk networks for VXLAN segmented traffic).")),
      cfg.StrOpt('t2_network_profile', default='osn_t2_np',
                 help=_("Name of N1kv network profile for T2 networks (i.e., "
                        "trunk networks for VLAN segmented traffic).")),
  ]
  
  cfg.CONF.register_opts(N1KV_TRUNKING_DRIVER_OPTS, "n1kv")
  
  MIN_LL_VLAN_TAG = 10
  MAX_LL_VLAN_TAG = 200
  FULL_VLAN_SET = set(range(MIN_LL_VLAN_TAG, MAX_LL_VLAN_TAG + 1))
  DELETION_ATTEMPTS = 5
  SECONDS_BETWEEN_DELETION_ATTEMPTS = 3
  
  # Port lookups can fail so retries are needed
  MAX_HOSTING_PORT_LOOKUP_ATTEMPTS = 10
  SECONDS_BETWEEN_HOSTING_PORT_LOOKSUPS = 2
  
  
  class N1kvTrunkingPlugDriver(plug.PluginSidePluggingDriver):
      """Driver class for service VMs used with the N1kv plugin.
  
      The driver makes use N1kv plugin's VLAN trunk feature.
      """
      _mgmt_port_profile_id = None
      _t1_port_profile_id = None
      _t2_port_profile_id = None
      _t1_network_profile_id = None
      _t2_network_profile_id = None
  
      @property
      def _core_plugin(self):
          return manager.NeutronManager.get_plugin()
  
      @classmethod
      def _get_profile_id(cls, p_type, resource, name):
          try:
              tenant_id = manager.NeutronManager.get_service_plugins()[
                  constants.L3_ROUTER_NAT].l3_tenant_id()
          except AttributeError:
              return
          if tenant_id is None:
              return
          core_plugin = manager.NeutronManager.get_plugin()
          if p_type == 'net_profile':
              profiles = core_plugin.get_network_profiles(
                  n_context.get_admin_context(),
                  {'tenant_id': [tenant_id], 'name': [name]},
                  ['id'])
          else:
              profiles = core_plugin.get_policy_profiles(
                  n_context.get_admin_context(),
                  {'tenant_id': [tenant_id], 'name': [name]},
                  ['id'])
          if len(profiles) == 1:
              return profiles[0]['id']
          elif len(profiles) > 1:
              # Profile must have a unique name.
              LOG.error(_LE('The %(resource)s %(name)s does not have unique '
                            'name. Please refer to admin guide and create one.'),
                        {'resource': resource, 'name': name})
          else:
              # Profile has not been created.
              LOG.error(_LE('There is no %(resource)s %(name)s. Please refer to '
                          'admin guide and create one.'),
                        {'resource': resource, 'name': name})
  
      @classmethod
      def mgmt_port_profile_id(cls):
          if cls._mgmt_port_profile_id is None:
              cls._mgmt_port_profile_id = cls._get_profile_id(
                  'port_profile', 'N1kv port profile',
                  cfg.CONF.n1kv.management_port_profile)
          return cls._mgmt_port_profile_id
  
      @classmethod
      def t1_port_profile_id(cls):
          if cls._t1_port_profile_id is None:
              cls._t1_port_profile_id = cls._get_profile_id(
                  'port_profile', 'N1kv port profile',
                  cfg.CONF.n1kv.t1_port_profile)
          return cls._t1_port_profile_id
  
      @classmethod
      def t2_port_profile_id(cls):
          if cls._t2_port_profile_id is None:
              cls._t2_port_profile_id = cls._get_profile_id(
                  'port_profile', 'N1kv port profile',
                  cfg.CONF.n1kv.t2_port_profile)
          return cls._t2_port_profile_id
  
      @classmethod
      def t1_network_profile_id(cls):
          if cls._t1_network_profile_id is None:
              cls._t1_network_profile_id = cls._get_profile_id(
                  'net_profile', 'N1kv network profile',
                  cfg.CONF.n1kv.t1_network_profile)
          return cls._t1_network_profile_id
  
      @classmethod
      def t2_network_profile_id(cls):
          if cls._t2_network_profile_id is None:
              cls._t2_network_profile_id = cls._get_profile_id(
                  'net_profile', 'N1kv network profile',
                  cfg.CONF.n1kv.t2_network_profile)
          return cls._t2_network_profile_id
  
      def create_hosting_device_resources(self, context, complementary_id,
                                          tenant_id, mgmt_nw_id,
                                          mgmt_sec_grp_id, max_hosted):
          mgmt_port = None
          t1_n, t1_sn, t2_n, t2_sn, t_p = [], [], [], [], []
          if mgmt_nw_id is not None and tenant_id is not None:
              # Create port for mgmt interface
              p_spec = {'port': {
                  'tenant_id': tenant_id,
                  'admin_state_up': True,
                  'name': 'mgmt',
                  'network_id': mgmt_nw_id,
                  'mac_address': attributes.ATTR_NOT_SPECIFIED,
                  'fixed_ips': attributes.ATTR_NOT_SPECIFIED,
                  'n1kv:profile_id': self.mgmt_port_profile_id(),
                  'device_id': "",
                  # Use device_owner attribute to ensure we can query for these
                  # ports even before Nova has set device_id attribute.
                  'device_owner': complementary_id}}
              try:
                  mgmt_port = self._core_plugin.create_port(context,
                                                            p_spec)
                  # The trunk networks
                  n_spec = {'network': {'tenant_id': tenant_id,
                                        'admin_state_up': True,
                                        'name': n1kv_const.T1_NETWORK_NAME,
                                        'shared': False}}
                  # Until Nova allows spinning up VMs with VIFs on
                  # networks without subnet(s) we create "dummy" subnets
                  # for the trunk networks
                  s_spec = {'subnet': {
                      'tenant_id': tenant_id,
                      'admin_state_up': True,
                      'cidr': n1kv_const.SUBNET_PREFIX,
                      'enable_dhcp': False,
                      'gateway_ip': attributes.ATTR_NOT_SPECIFIED,
                      'allocation_pools': attributes.ATTR_NOT_SPECIFIED,
                      'ip_version': 4,
                      'dns_nameservers': attributes.ATTR_NOT_SPECIFIED,
                      'host_routes': attributes.ATTR_NOT_SPECIFIED}}
                  for i in xrange(max_hosted):
                      # Create T1 trunk network for this router
                      self._create_resources(
                          context, "T1", i, n_spec, n1kv_const.T1_NETWORK_NAME,
                          self.t1_network_profile_id(), t1_n, s_spec,
                          n1kv_const.T1_SUBNET_NAME, t1_sn, p_spec,
                          n1kv_const.T1_PORT_NAME, self.t1_port_profile_id(),
                          t_p)
                      # Create T2 trunk network for this router
                      self._create_resources(
                          context, "T2", i, n_spec, n1kv_const.T2_NETWORK_NAME,
                          self.t2_network_profile_id(), t2_n, s_spec,
                          n1kv_const.T2_SUBNET_NAME, t2_sn, p_spec,
                          n1kv_const.T2_PORT_NAME, self.t2_port_profile_id(),
                          t_p)
              except n_exc.NeutronException as e:
                  LOG.error(_LE('Error %s when creating service VM resources. '
                              'Cleaning up.'), e)
                  resources = {'ports': t_p, 'networks': t1_n + t2_n,
                               'subnets': t1_sn + t2_sn}
                  self.delete_hosting_device_resources(
                      context, tenant_id, mgmt_port, **resources)
                  mgmt_port = None
                  t1_n, t1_sn, t2_n, t2_sn, t_p = [], [], [], [], []
          return {'mgmt_port': mgmt_port,
                  'ports': t_p,
                  'networks': t1_n + t2_n,
                  'subnets': t1_sn + t2_sn}
  
      def _create_resources(self, context, type_name, resource_index,
                            n_spec, net_namebase, net_profile, t_n,
                            s_spec, subnet_namebase, t_sn,
                            p_spec, port_namebase, port_profile, t_p):
          index = str(resource_index + 1)
          # Create trunk network
          n_spec['network'].update({'name': net_namebase + index,
                                    'n1kv:profile_id': net_profile})
          t_n.append(self._core_plugin.create_network(context, n_spec))
          LOG.debug('Created %(t_n)s network with name %(name)s and id %(id)s',
                    {'t_n': type_name, 'name': n_spec['network']['name'],
                     'id': t_n[resource_index]['id']})
          # Create dummy subnet for the trunk network
          s_spec['subnet'].update({'name': subnet_namebase + index,
                                  'network_id': t_n[resource_index]['id']})
          t_sn.append(self._core_plugin.create_subnet(context, s_spec))
          # Create port for on trunk network
          p_spec['port'].update({'name': port_namebase + index,
                                 'network_id': t_n[resource_index]['id'],
                                 'n1kv:profile_id': port_profile})
          t_p.append(self._core_plugin.create_port(context, p_spec))
          LOG.debug('Created %(t_n)s port with name %(name)s, id %(id)s on '
                    'subnet %(subnet)s',
                    {'t_n': type_name, 'name': t_n[resource_index]['name'],
                     'id': t_n[resource_index]['id'],
                     'subnet': t_sn[resource_index]['id']})
  
      def get_hosting_device_resources(self, context, id, complementary_id,
                                       tenant_id, mgmt_nw_id):
          ports, nets, subnets = [], [], []
          mgmt_port = None
          # Ports for hosting device may not yet have 'device_id' set to
          # Nova assigned uuid of VM instance. However, those ports will still
          # have 'device_owner' attribute set to complementary_id. Hence, we
          # use both attributes in the query to ensure we find all ports.
          query = context.session.query(models_v2.Port)
          query = query.filter(expr.or_(
              models_v2.Port.device_id == id,
              models_v2.Port.device_owner == complementary_id))
          for port in query:
              if port['network_id'] != mgmt_nw_id:
                  ports.append(port)
                  nets.append({'id': port['network_id']})
                  subnets.append({'id': port['fixed_ips'][0]['subnet_id']})
              else:
                  mgmt_port = port
          return {'mgmt_port': mgmt_port,
                  'ports': ports, 'networks': nets, 'subnets': subnets}
  
      def delete_hosting_device_resources(self, context, tenant_id, mgmt_port,
                                          **kwargs):
          attempts = 1
          port_ids = set(p['id'] for p in kwargs['ports'])
          subnet_ids = set(s['id'] for s in kwargs['subnets'])
          net_ids = set(n['id'] for n in kwargs['networks'])
  
          while mgmt_port is not None or port_ids or subnet_ids or net_ids:
              if attempts == DELETION_ATTEMPTS:
                  LOG.warning(_LW('Aborting resource deletion after %d '
                                'unsuccessful attempts'), DELETION_ATTEMPTS)
                  return
              else:
                  if attempts > 1:
                      eventlet.sleep(SECONDS_BETWEEN_DELETION_ATTEMPTS)
                  LOG.info(_LI('Resource deletion attempt %d starting'),
                           attempts)
              # Remove anything created.
              if mgmt_port is not None:
                  ml = set([mgmt_port['id']])
                  self._delete_resources(context, "management port",
                                         self._core_plugin.delete_port,
                                         n_exc.PortNotFound, ml)
                  if not ml:
                      mgmt_port = None
              self._delete_resources(context, "trunk port",
                                     self._core_plugin.delete_port,
                                     n_exc.PortNotFound, port_ids)
              self._delete_resources(context, "subnet",
                                     self._core_plugin.delete_subnet,
                                     n_exc.SubnetNotFound, subnet_ids)
              self._delete_resources(context, "trunk network",
                                     self._core_plugin.delete_network,
                                     n_exc.NetworkNotFound, net_ids)
              attempts += 1
          LOG.info(_LI('Resource deletion succeeded'))
  
      def _delete_resources(self, context, name, deleter, exception_type,
                            resource_ids):
          for item_id in resource_ids.copy():
              try:
                  deleter(context, item_id)
                  resource_ids.remove(item_id)
              except exception_type:
                  resource_ids.remove(item_id)
              except n_exc.NeutronException as e:
                  LOG.error(_LE('Failed to delete %(resource_name)s %(net_id)s '
                              'for service vm due to %(err)s'),
                            {'resource_name': name, 'net_id': item_id, 'err': e})
  
      def setup_logical_port_connectivity(self, context, port_db):
          # Add the VLAN to the VLANs that the hosting port trunks.
          self._perform_logical_port_connectivity_action(
              context, port_db, 'Adding', n1kv.SEGMENT_ADD)
  
      def teardown_logical_port_connectivity(self, context, port_db):
          # Remove the VLAN from the VLANs that the hosting port trunks.
          self._perform_logical_port_connectivity_action(
              context, port_db, 'Removing', n1kv.SEGMENT_DEL)
  
      def extend_hosting_port_info(self, context, port_db, hosting_info):
          hosting_info['segmentation_id'] = port_db.hosting_info.segmentation_id
  
      def allocate_hosting_port(self, context, router_id, port_db, network_type,
                                hosting_device_id):
          allocations = self._get_router_ports_with_hosting_info_qry(
              context, router_id).all()
          trunk_mappings = {}
          if not allocations:
              # Router has no ports with hosting port allocated to them yet
              # whatsoever, so we select an unused port (that trunks networks
              # of correct type) on the hosting device.
              id_allocated_port = self._get_unused_service_vm_trunk_port(
                  context, hosting_device_id, network_type)
          else:
              # Router has at least one port with hosting port allocated to it.
              # If there is only one allocated hosting port then it may be for
              # the wrong network type. Iterate to determine the hosting port.
              id_allocated_port = None
              for item in allocations:
                  if item.hosting_info['network_type'] == network_type:
                      # For VXLAN we need to determine used link local tags.
                      # For VLAN we don't need to but the following lines will
                      # be performed once anyway since we break out of the
                      # loop later. That does not matter.
                      tag = item.hosting_info['segmentation_id']
                      trunk_mappings[item['network_id']] = tag
                      id_allocated_port = item.hosting_info['hosting_port_id']
                  else:
                      port_twin_id = item.hosting_info['hosting_port_id']
                  if network_type == 'vlan':
                      # For a router port belonging to a VLAN network we can
                      # break here since we now know (or have information to
                      # determine) hosting_port and the VLAN tag is provided by
                      # the core plugin.
                      break
              if id_allocated_port is None:
                  # Router only had hosting port for wrong network
                  # type allocated yet. So get that port's sibling.
                  id_allocated_port = self._get_other_port_id_in_pair(
                      context, port_twin_id, hosting_device_id)
          if id_allocated_port is None:
              # Database must have been messed up if this happens ...
              LOG.debug('n1kv_trunking_driver: Could not allocate hosting port')
              return
          if network_type == 'vxlan':
              # For VLXAN we choose the (link local) VLAN tag
              used_tags = set(trunk_mappings.values())
              allocated_vlan = min(sorted(FULL_VLAN_SET - used_tags))
          else:
              # For VLAN core plugin provides VLAN tag.
              trunk_mappings[port_db['network_id']] = None
              tags = self._core_plugin.get_networks(
                  context, {'id': [port_db['network_id']]},
                  [pr_net.SEGMENTATION_ID])
              allocated_vlan = (None if tags == []
                                else tags[0].get(pr_net.SEGMENTATION_ID))
          if allocated_vlan is None:
              # Database must have been messed up if this happens ...
              LOG.debug('n1kv_trunking_driver: Could not allocate VLAN')
              return
          return {'allocated_port_id': id_allocated_port,
                  'allocated_vlan': allocated_vlan}
  
      def _perform_logical_port_connectivity_action(self, context, port_db,
                                                    action_str, action):
          if (port_db is None or port_db.hosting_info is None or
                  port_db.hosting_info.hosting_port is None):
              return
          np_id_t_nw = self._core_plugin.get_network(
              context, port_db.hosting_info.hosting_port['network_id'],
              [n1kv.PROFILE_ID])
          if np_id_t_nw.get(n1kv.PROFILE_ID) == self.t1_network_profile_id():
              # for vxlan trunked segment, id:s end with ':'link local vlan tag
              trunk_spec = (port_db['network_id'] + ':' +
                            str(port_db.hosting_info.segmentation_id))
          else:
              trunk_spec = port_db['network_id']
          LOG.info(_LI('Updating trunk: %(action)s VLAN %(tag)d for network_id '
                     '%(id)s'), {'action': action,
                                 'tag': port_db.hosting_info.segmentation_id,
                                 'id': port_db['network_id']})
          #TODO(bobmel): enable statement below when N1kv does not trunk all
          if False:
              self._core_plugin.update_network(
                  context, port_db.hosting_info.hosting_port['network_id'],
                  {'network': {action: trunk_spec}})
  
      def _get_trunk_mappings(self, context, hosting_port_id):
          query = context.session.query(l3_models.HostedHostingPortBinding)
          query = query.filter(
              l3_models.HostedHostingPortBinding.hosting_port_id ==
              hosting_port_id)
          return dict((hhpb.logical_port['network_id'], hhpb.segmentation_id)
                      for hhpb in query)
  
      def _get_unused_service_vm_trunk_port(self, context, hd_id, network_type):
          name = (n1kv_const.T2_PORT_NAME if network_type == 'vlan'
                  else n1kv_const.T1_PORT_NAME)
          attempts = 0
          while True:
              # mysql> SELECT * FROM ports WHERE device_id = 'hd_id1' AND
              # id NOT IN (SELECT hosting_port_id FROM hostedhostingportbindings)
              # AND
              # name LIKE '%t1%'
              # ORDER BY name;
              stmt = context.session.query(
                  l3_models.HostedHostingPortBinding.hosting_port_id).subquery()
              query = context.session.query(models_v2.Port.id)
              query = query.filter(
                  expr.and_(models_v2.Port.device_id == hd_id,
                            ~models_v2.Port.id.in_(stmt),
                            models_v2.Port.name.like('%' + name + '%')))
              query = query.order_by(models_v2.Port.name)
              res = query.first()
              if res is None:
                  if attempts >= MAX_HOSTING_PORT_LOOKUP_ATTEMPTS:
                      # This should not happen ...
                      LOG.error(_LE('Hosting port DB inconsistency for '
                                  'hosting device %s'), hd_id)
                      return
                  else:
                      # The service VM may not have plugged its VIF into the
                      # Neutron Port yet so we wait and make another lookup.
                      attempts += 1
                      LOG.info(_LI('Attempt %(attempt)d to find trunk ports for '
                                 'hosting device %(hd_id)s failed. Trying '
                                 'again in %(time)d seconds.'),
                               {'attempt': attempts, 'hd_id': hd_id,
                                'time': SECONDS_BETWEEN_HOSTING_PORT_LOOKSUPS})
                      eventlet.sleep(SECONDS_BETWEEN_HOSTING_PORT_LOOKSUPS)
              else:
                  break
          return res[0]
  
      def _get_router_ports_with_hosting_info_qry(self, context, router_id,
                                                  device_owner=None,
                                                  hosting_port_id=None):
          # Query for a router's ports that have trunking information
          query = context.session.query(models_v2.Port)
          query = query.join(
              l3_models.HostedHostingPortBinding,
              models_v2.Port.id ==
              l3_models.HostedHostingPortBinding.logical_port_id)
          query = query.filter(models_v2.Port.device_id == router_id)
          if device_owner is not None:
              query = query.filter(models_v2.Port.device_owner == device_owner)
          if hosting_port_id is not None:
              query = query.filter(
                  l3_models.HostedHostingPortBinding.hosting_port_id ==
                  hosting_port_id)
          return query
  
      def _get_other_port_id_in_pair(self, context, port_id, hosting_device_id):
          query = context.session.query(models_v2.Port)
          query = query.filter(models_v2.Port.id == port_id)
          try:
              port = query.one()
              name, index = port['name'].split(':')
              name += ':'
              if name == n1kv_const.T1_PORT_NAME:
                  other_port_name = n1kv_const.T2_PORT_NAME + index
              else:
                  other_port_name = n1kv_const.T1_PORT_NAME + index
              query = context.session.query(models_v2.Port)
              query = query.filter(models_v2.Port.device_id == hosting_device_id,
                                   models_v2.Port.name == other_port_name)
              other_port = query.one()
              return other_port['id']
          except (exc.NoResultFound, exc.MultipleResultsFound):
              # This should not happen ...
              LOG.error(_LE('Port trunk pair DB inconsistency for port %s'),
                        port_id)
              return
  