networking-arista/networking_arista/ml2/arista_resources.py

567 lines
23 KiB
Python

# Copyright (c) 2017 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.
import json
import os
from neutron_lib import constants as n_const
from neutron_lib.services.trunk import constants as t_const
from oslo_log import log as logging
from networking_arista.common import constants as a_const
from networking_arista.common import db_lib
from networking_arista.common import exceptions as arista_exc
from networking_arista.common import utils
LOG = logging.getLogger(__name__)
class AttributeFormatter(object):
"""Formats a single attribute of the CVX model based on the neutron model
There are 4 elements to an AttributeFormatter:
1. neutron_key - name of the key in the neutron model
2. cvx_key - name of the key in the cvx model
3. format(optional) - function to alter the value to be CVX compatible
4. submodel(optional) - If the get_db_resources function queries multiple
models, the name of the model that contains the neutron_key must be
specified
"""
def __init__(self, neutron_key, cvx_key, format=None, submodel=None):
self.neutron_key = neutron_key
self.cvx_key = cvx_key
self.format = format or (
lambda neutron_val, base_resource: neutron_val)
self.submodel = submodel
def transform(self, resource):
base_resource = resource
if self.submodel:
resource = getattr(resource, self.submodel)
return (self.cvx_key, self.format(resource[self.neutron_key],
base_resource))
class AristaResourcesBase(object):
"""Tracks state of resources of one resource type on neutron and CVX
An AristaResources class is responsible for:
- tracking resources that have been provisioned in neutron
- tracking resources that have been provisioned on CVX
- creating and deleting resources on CVX to bring it in line with neutron
- formatting neutron resources to be compatible with CVX's API
- tracking the correct endpoint for CVX API calls
In order to facilitate this each resource type should define:
1. formatter - a list of AttributeFormatters to convert neutron attributes
to models compatible with CVX's API
2. id_key - the key in the CVX model that uniquely identifies the resource
3. endpoint - format string for region resource endpoint
4. get_db_resources - function that queries the neutron db for all
resources of the resource type in question
"""
formatter = [AttributeFormatter('id', 'id')]
id_key = 'id'
endpoint = 'region/%(region)s'
def __init__(self, rpc):
self.region = rpc.region
self.rpc = rpc
self.cvx_data_stale = True
self.neutron_data_stale = True
self.cvx_ids = set()
self.neutron_resources = dict()
def clear_cvx_data(self):
self.cvx_data_stale = True
self.cvx_ids = set()
def clear_neutron_data(self):
self.neutron_data_stale = True
self.neutron_resources = dict()
def clear_all_data(self):
self.clear_cvx_data()
self.clear_neutron_data()
def update_neutron_resource(self, id, action):
LOG.debug("%(pid)s Requesting %(action)s %(class)s resource %(id)s",
{'action': action, 'class': self.__class__.__name__,
'id': id, 'pid': os.getpid()})
resource = self.get_db_resources(id)
if action == a_const.FULL_SYNC:
self.clear_neutron_data()
LOG.info("%(pid)s Full sync resource %(class)s",
{'class': self.__class__.__name__,
'pid': os.getpid()})
return
assert(len(resource) <= 1)
if resource:
# Until we start using etcd, we need to unconditionally send the
# create request because it might have been delete by another
# worker. We force this by removing the resource to our 'view' of
# cvx resources
self.force_resource_update(id)
LOG.debug("%(pid)s Resource %(class)s %(id)s found, creating",
{'class': self.__class__.__name__, 'id': id,
'pid': os.getpid()})
self._add_neutron_resource(resource[0])
else:
LOG.debug("%(pid)s Resource %(class)s %(id)s not found, deleting",
{'class': self.__class__.__name__, 'id': id,
'pid': os.getpid()})
self._delete_neutron_resource(id)
def _add_neutron_resource(self, resource):
formatted_resource = self.format_for_create(resource)
resource_id = list(formatted_resource.keys())[0]
LOG.debug("%(pid)s %(class)s resource %(id)s added locally",
{'class': self.__class__.__name__,
'id': resource_id,
'pid': os.getpid()})
# If the resource has changed, force a POST to CVX
old_resource = self.neutron_resources.get(resource_id)
if old_resource and old_resource != formatted_resource[resource_id]:
LOG.debug("%(pid)s %(class)s resource %(id)s requires update",
{'class': self.__class__.__name__,
'id': resource_id,
'pid': os.getpid()})
self.force_resource_update(resource_id)
self.neutron_resources.update(formatted_resource)
def force_resource_update(self, id):
self.cvx_ids.discard(id)
def _delete_neutron_resource(self, id):
# Until we start using etcd, we need to unconditionally send the
# delete request because it might have been created by another worker.
# We force this by adding the resource to our 'view' of cvx resources
self.cvx_ids.add(id)
try:
del self.neutron_resources[id]
LOG.debug("%(pid)s %(class)s resource %(id)s removed locally",
{'class': self.__class__.__name__, 'id': id,
'pid': os.getpid()})
except KeyError:
LOG.debug("Resource ID %(id)s already deleted locally", {'id': id})
def get_endpoint(self):
return self.endpoint % {'region': self.region}
@classmethod
def get_resource_ids(cls, resource):
return set([resource[cls.id_key]])
def get_cvx_ids(self):
if self.cvx_data_stale:
LOG.info("%(pid)s Getting %(class)s from CVX",
{'class': self.__class__.__name__,
'pid': os.getpid()})
cvx_data = self.rpc.send_api_request(self.get_endpoint(), 'GET')
for resource in cvx_data:
self.cvx_ids |= self.get_resource_ids(resource)
self.cvx_data_stale = False
return self.cvx_ids
@staticmethod
def get_db_resources(key=None):
raise NotImplementedError
def get_neutron_ids(self):
if self.neutron_data_stale:
self.get_neutron_resources()
return set(self.neutron_resources.keys())
def get_neutron_resources(self):
if self.neutron_data_stale:
LOG.info("%(pid)s Getting %(class)s from neutron",
{'class': self.__class__.__name__,
'pid': os.getpid()})
for resource in self.get_db_resources():
self._add_neutron_resource(resource)
self.neutron_data_stale = False
return self.neutron_resources
def resource_ids_to_delete(self):
cvx_resource_ids = self.get_cvx_ids()
neutron_resource_ids = self.get_neutron_ids()
return (cvx_resource_ids - neutron_resource_ids)
def resource_ids_to_create(self):
cvx_resource_ids = self.get_cvx_ids()
neutron_resource_ids = self.get_neutron_ids()
return (neutron_resource_ids - cvx_resource_ids)
@classmethod
def format_for_create(cls, neutron_resource):
cvx_resource = dict(
attr.transform(neutron_resource) for attr in cls.formatter
)
return {cvx_resource[cls.id_key]: cvx_resource}
@classmethod
def format_for_delete(cls, id):
return {cls.id_key: id}
def create_cvx_resources(self):
resource_ids_to_create = self.resource_ids_to_create()
neutron_resources = self.get_neutron_resources()
resources_to_create = list(neutron_resources[resource_id] for
resource_id in resource_ids_to_create)
if resources_to_create:
LOG.info("%(pid)s Creating %(class)s resources with ids %(ids)s "
"on CVX",
{'class': self.__class__.__name__,
'ids': ', '.join(str(r) for r in resource_ids_to_create),
'pid': os.getpid()})
self.rpc.send_api_request(self.get_endpoint(), 'POST',
resources_to_create)
self.cvx_ids.update(resource_ids_to_create)
LOG.info("%(pid)s %(class)s resources with ids %(ids)s created "
"on CVX",
{'class': self.__class__.__name__,
'ids': ', '.join(str(r) for r in resource_ids_to_create),
'pid': os.getpid()})
else:
LOG.debug("%(pid)s No %(class)s resources to create",
{'class': self.__class__.__name__,
'pid': os.getpid()})
return resources_to_create
def delete_cvx_resources(self):
resource_ids_to_delete = self.resource_ids_to_delete()
resources_to_delete = list(self.format_for_delete(id) for id in
resource_ids_to_delete)
if resources_to_delete:
LOG.info("%(pid)s Deleting %(class)s resources with ids %(ids)s "
"from CVX",
{'class': self.__class__.__name__,
'ids': ', '.join(str(r) for r in resource_ids_to_delete),
'pid': os.getpid()})
try:
self.rpc.send_api_request(self.get_endpoint(), 'DELETE',
resources_to_delete)
except arista_exc.AristaRpcError as err:
if not err.msg.startswith('Unknown port id'):
raise
self.cvx_ids -= resource_ids_to_delete
LOG.info("%(pid)s %(class)s resources with ids %(ids)s deleted "
"from CVX",
{'class': self.__class__.__name__,
'ids': ', '.join(str(r) for r in resource_ids_to_delete),
'pid': os.getpid()})
else:
LOG.debug("%(pid)s No %(class)s resources to delete",
{'class': self.__class__.__name__,
'pid': os.getpid()})
return resources_to_delete
class Tenants(AristaResourcesBase):
endpoint = 'region/%(region)s/tenant'
formatter = [AttributeFormatter('project_id', 'id')]
get_db_resources = staticmethod(db_lib.get_tenants)
class Networks(AristaResourcesBase):
def _is_shared(rbac_entries, resource):
for entry in rbac_entries:
if (entry.action == 'access_as_shared' and
entry.target_tenant == '*'):
return True
return False
def modify_blank_project_id(project_id, resource):
if len(project_id) == 0:
l3ha_network = getattr(resource, 'L3HARouterNetwork')
if l3ha_network:
project_id = l3ha_network.project_id
return project_id
formatter = [AttributeFormatter('id', 'id',
submodel='Network'),
AttributeFormatter('project_id', 'tenantId',
modify_blank_project_id,
submodel='Network'),
AttributeFormatter('name', 'name',
submodel='Network'),
AttributeFormatter('rbac_entries', 'shared', _is_shared,
submodel='Network')]
endpoint = 'region/%(region)s/network'
get_db_resources = staticmethod(db_lib.get_networks)
class Segments(AristaResourcesBase):
formatter = [AttributeFormatter('id', 'id'),
AttributeFormatter('network_type', 'type'),
AttributeFormatter('segmentation_id', 'segmentationId'),
AttributeFormatter('network_id', 'networkId'),
AttributeFormatter('is_dynamic', 'segmentType',
lambda n, r: 'dynamic' if n else 'static')]
endpoint = 'region/%(region)s/segment'
get_db_resources = staticmethod(db_lib.get_segments)
class Dhcps(AristaResourcesBase):
formatter = [AttributeFormatter('device_id', 'id',
submodel='Port'),
AttributeFormatter('host', 'hostId',
utils.hostname,
submodel='PortBinding'),
AttributeFormatter('project_id', 'tenantId',
submodel='Port')]
endpoint = 'region/%(region)s/dhcp'
get_db_resources = staticmethod(db_lib.get_dhcp_instances)
class Routers(AristaResourcesBase):
def modify_blank_project_id(project_id, resource):
if len(project_id) == 0:
l3ha_router = getattr(resource, 'Router')
if l3ha_router:
project_id = l3ha_router.project_id
return project_id
formatter = [AttributeFormatter('device_id', 'id',
submodel='Port'),
AttributeFormatter('device_owner', 'hostId',
lambda *args: '(see router ports)',
submodel='Port'),
AttributeFormatter('project_id', 'tenantId',
modify_blank_project_id,
submodel='Port')]
endpoint = 'region/%(region)s/router'
get_db_resources = staticmethod(db_lib.get_router_instances)
class Vms(AristaResourcesBase):
formatter = [AttributeFormatter('device_id', 'id',
submodel='Port'),
AttributeFormatter('host', 'hostId',
utils.hostname,
submodel='PortBinding'),
AttributeFormatter('project_id', 'tenantId',
submodel='Port')]
endpoint = 'region/%(region)s/vm'
get_db_resources = staticmethod(db_lib.get_vm_instances)
class Baremetals(AristaResourcesBase):
formatter = [AttributeFormatter('device_id', 'id',
submodel='Port'),
AttributeFormatter('host', 'hostId',
submodel='PortBinding'),
AttributeFormatter('project_id', 'tenantId',
submodel='Port')]
endpoint = 'region/%(region)s/baremetal'
get_db_resources = staticmethod(db_lib.get_baremetal_instances)
class DhcpPorts(AristaResourcesBase):
endpoint = 'region/%(region)s/port?type=dhcp'
formatter = [AttributeFormatter('id', 'id', submodel='Port'),
AttributeFormatter('name', 'portName', submodel='Port'),
AttributeFormatter('device_owner', 'vlanType',
lambda *args: 'allowed',
submodel='Port'),
AttributeFormatter('network_id', 'networkId',
submodel='Port'),
AttributeFormatter('device_id', 'instanceId',
submodel='Port'),
AttributeFormatter('device_owner', 'instanceType',
lambda *args: 'dhcp',
submodel='Port'),
AttributeFormatter('project_id', 'tenantId',
submodel='Port')]
get_db_resources = staticmethod(db_lib.get_dhcp_ports)
class RouterPorts(AristaResourcesBase):
def modify_blank_project_id(project_id, resource):
if len(project_id) == 0:
l3ha_router = getattr(resource, 'Router')
if l3ha_router:
project_id = l3ha_router.project_id
return project_id
endpoint = 'region/%(region)s/port?type=router'
formatter = [AttributeFormatter('id', 'id', submodel='Port'),
AttributeFormatter('name', 'portName', submodel='Port'),
AttributeFormatter('device_owner', 'vlanType',
lambda *args: 'allowed',
submodel='Port'),
AttributeFormatter('network_id', 'networkId',
submodel='Port'),
AttributeFormatter('device_id', 'instanceId',
submodel='Port'),
AttributeFormatter('device_owner', 'instanceType',
lambda *args: 'router',
submodel='Port'),
AttributeFormatter('project_id', 'tenantId',
modify_blank_project_id, submodel='Port')]
get_db_resources = staticmethod(db_lib.get_router_ports)
class VmPorts(AristaResourcesBase):
endpoint = 'region/%(region)s/port?type=vm'
formatter = [AttributeFormatter('id', 'id', submodel='Port'),
AttributeFormatter('name', 'portName', submodel='Port'),
AttributeFormatter('device_owner', 'vlanType',
lambda *args: 'allowed', submodel='Port'),
AttributeFormatter('network_id', 'networkId',
submodel='Port'),
AttributeFormatter('device_id', 'instanceId',
submodel='Port'),
AttributeFormatter('device_owner', 'instanceType',
lambda *args: 'vm', submodel='Port'),
AttributeFormatter('project_id', 'tenantId',
submodel='Port')]
get_db_resources = staticmethod(db_lib.get_vm_ports)
@classmethod
def format_for_create(cls, resource):
# This is needed until we can update the upstream trunk port
# handling to add device_id to subports
port = getattr(resource, 'Port')
if port['device_owner'] == t_const.TRUNK_SUBPORT_OWNER:
parent_port = db_lib.get_parent(port['id'])
port['device_id'] = parent_port.get('device_id')
return super(VmPorts, cls).format_for_create(resource)
class BaremetalPorts(AristaResourcesBase):
def _get_vlan_type(device_owner, resource):
if (device_owner.startswith(n_const.DEVICE_OWNER_BAREMETAL_PREFIX) or
device_owner.startswith(n_const.DEVICE_OWNER_COMPUTE_PREFIX)):
return 'native'
else:
return 'allowed'
endpoint = 'region/%(region)s/port?type=baremetal'
formatter = [AttributeFormatter('id', 'id', submodel='Port'),
AttributeFormatter('name', 'portName', submodel='Port'),
AttributeFormatter('device_owner', 'vlanType',
_get_vlan_type, submodel='Port'),
AttributeFormatter('network_id', 'networkId',
submodel='Port'),
AttributeFormatter('device_id', 'instanceId',
submodel='Port'),
AttributeFormatter('device_owner', 'instanceType',
lambda *args: 'baremetal',
submodel='Port'),
AttributeFormatter('project_id', 'tenantId', submodel='Port')]
get_db_resources = staticmethod(db_lib.get_baremetal_ports)
class PortBindings(AristaResourcesBase):
endpoint = 'region/%(region)s/portbinding'
get_db_resources = staticmethod(db_lib.get_port_bindings)
@staticmethod
def maybe_strip_fqdn(id):
port_id, binding = id
if type(binding) != tuple:
binding = utils.hostname(binding)
return (port_id, binding)
def force_resource_update(self, id):
id = self.maybe_strip_fqdn(id)
super(PortBindings, self).force_resource_update(id)
def _delete_neutron_resource(self, id):
id = self.maybe_strip_fqdn(id)
super(PortBindings, self)._delete_neutron_resource(id)
@classmethod
def get_resource_ids(cls, resource):
resource_ids = set()
port_id = resource['portId']
for host_binding in resource.get('hostBinding', []):
resource_ids.add((port_id, host_binding['host']))
for switch_binding in resource.get('switchBinding', []):
resource_ids.add((port_id, (switch_binding['switch'],
switch_binding['interface'])))
return resource_ids
@classmethod
def format_for_delete(cls, id):
model = dict()
port_id, binding = id
model['portId'] = port_id
if type(binding) == tuple:
switch, interface = binding
model['switchBinding'] = [{'switch': switch,
'interface': interface}]
else:
host = binding
model['hostBinding'] = [{'host': host}]
return model
@classmethod
def format_for_create(cls, binding):
cvx_resources = {}
# First build the list of segments to which the port is bound
# binding levels are in order from 0 -> highest
# which is typically vxlan -> vlan
# The Arista JSON API depends on this ordering being present in
# the segments list
segments = []
for binding_level in (sorted(binding['levels'],
key=lambda bl: bl.level)):
segments.append({'id': binding_level.segment_id})
# Determine if this is a switch or host bindings and populate
# the appropriate model attribute accordingly
host = utils.hostname(binding['host'])
port_id = binding['port_id']
# If the binding profile isn't valid json, this is a host binding
try:
profile = json.loads(binding.profile)
except ValueError:
profile = {}
if profile.get('local_link_information'):
for link in profile['local_link_information']:
switch_binding = {'host': host,
'switch': link['switch_id'],
'interface': link['port_id'],
'segment': segments}
binding_key = (link['switch_id'], link['port_id'])
cvx_resources[(port_id, binding_key)] = {
'portId': port_id,
'hostBinding': [],
'switchBinding': [switch_binding]}
else:
cvx_resources[(port_id, host)] = {
'portId': port_id,
'hostBinding': [{'host': host, 'segment': segments}],
'switchBinding': []}
return cvx_resources