Browse Source
Tailor sync logic to the json_api. Since GET/DELETE/POST logic is so similar for all resource types, a great deal of common code can be used. Each resource must simply define a neutron db get helper, a cvx endpoint and a conversion helper from neutron format to CVX format. Change-Id: I91273d3da3ddbcee970ab10dbb51d726a417fae9changes/07/535507/13
13 changed files with 2374 additions and 1010 deletions
@ -0,0 +1,395 @@
|
||||
# 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 |
||||
from neutron_lib import constants as n_const |
||||
|
||||
from networking_arista.common import db_lib |
||||
from networking_arista.common import utils |
||||
|
||||
|
||||
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 arg: arg) |
||||
self.submodel = submodel |
||||
|
||||
def transform(self, resource): |
||||
if self.submodel: |
||||
resource = getattr(resource, self.submodel) |
||||
return (self.cvx_key, self.format(resource[self.neutron_key])) |
||||
|
||||
|
||||
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_ids = set() |
||||
self.neutron_resources = dict() |
||||
|
||||
def clear_cvx_data(self): |
||||
self.cvx_ids = set() |
||||
|
||||
def clear_neutron_data(self): |
||||
self.neutron_resources = dict() |
||||
|
||||
def clear_all_data(self): |
||||
self.clear_cvx_data() |
||||
self.clear_neutron_data() |
||||
|
||||
def add_neutron_resource(self, resource): |
||||
formatted_resource = self.format_for_create(resource) |
||||
self.neutron_resources.update(formatted_resource) |
||||
|
||||
def delete_neutron_resource(self, id): |
||||
del self.neutron_resources[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 not self.cvx_ids: |
||||
cvx_data = self.rpc.send_api_request(self.get_endpoint(), 'GET') |
||||
for resource in cvx_data: |
||||
self.cvx_ids |= self.get_resource_ids(resource) |
||||
return self.cvx_ids |
||||
|
||||
@staticmethod |
||||
def get_db_resources(): |
||||
raise NotImplementedError |
||||
|
||||
def get_neutron_ids(self): |
||||
if not self.neutron_resources: |
||||
self.get_neutron_resources() |
||||
return set(self.neutron_resources.keys()) |
||||
|
||||
def get_neutron_resources(self): |
||||
if not self.neutron_resources: |
||||
for resource in self.get_db_resources(): |
||||
self.add_neutron_resource(resource) |
||||
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() |
||||
cvx_resource.update( |
||||
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: |
||||
self.rpc.send_api_request(self.get_endpoint(), 'POST', |
||||
resources_to_create) |
||||
self.cvx_ids.update(resource_ids_to_create) |
||||
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: |
||||
self.rpc.send_api_request(self.get_endpoint(), 'DELETE', |
||||
resources_to_delete) |
||||
self.cvx_ids -= resource_ids_to_delete |
||||
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): |
||||
for entry in rbac_entries: |
||||
if (entry.action == 'access_as_shared' and |
||||
entry.target_tenant == '*'): |
||||
return True |
||||
return False |
||||
|
||||
formatter = [AttributeFormatter('id', 'id'), |
||||
AttributeFormatter('project_id', 'tenantId'), |
||||
AttributeFormatter('name', 'networkName'), |
||||
AttributeFormatter('rbac_entries', 'shared', _is_shared)] |
||||
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 x: 'dynamic' if x else 'static')] |
||||
endpoint = 'region/%(region)s/segment' |
||||
get_db_resources = staticmethod(db_lib.get_segments) |
||||
|
||||
|
||||
class Dhcps(AristaResourcesBase): |
||||
|
||||
formatter = [AttributeFormatter('device_id', 'dhcpInstanceId', |
||||
submodel='Port'), |
||||
AttributeFormatter('host', 'dhcpHostId', |
||||
utils.hostname, |
||||
submodel='PortBinding'), |
||||
AttributeFormatter('project_id', 'tenantId', |
||||
submodel='Port')] |
||||
id_key = 'dhcpInstanceId' |
||||
endpoint = 'region/%(region)s/dhcp' |
||||
get_db_resources = staticmethod(db_lib.get_dhcp_instances) |
||||
|
||||
|
||||
class Routers(AristaResourcesBase): |
||||
|
||||
formatter = [AttributeFormatter('device_id', 'routerInstanceId', |
||||
submodel='Port'), |
||||
AttributeFormatter('device_owner', 'routerHostId', |
||||
lambda *args: 'distributed', |
||||
submodel='Port'), |
||||
AttributeFormatter('project_id', 'tenantId', |
||||
submodel='Port')] |
||||
id_key = 'routerInstanceId' |
||||
endpoint = 'region/%(region)s/router' |
||||
get_db_resources = staticmethod(db_lib.get_router_instances) |
||||
|
||||
|
||||
class Vms(AristaResourcesBase): |
||||
|
||||
formatter = [AttributeFormatter('device_id', 'vmInstanceId', |
||||
submodel='Port'), |
||||
AttributeFormatter('host', 'vmHostId', |
||||
utils.hostname, |
||||
submodel='PortBinding'), |
||||
AttributeFormatter('project_id', 'tenantId', |
||||
submodel='Port')] |
||||
id_key = 'vmInstanceId' |
||||
endpoint = 'region/%(region)s/vm' |
||||
get_db_resources = staticmethod(db_lib.get_vm_instances) |
||||
|
||||
|
||||
class Baremetals(AristaResourcesBase): |
||||
|
||||
formatter = [AttributeFormatter('device_id', 'baremetalInstanceId', |
||||
submodel='Port'), |
||||
AttributeFormatter('host', 'baremetalHostId', |
||||
submodel='PortBinding'), |
||||
AttributeFormatter('project_id', 'tenantId', |
||||
submodel='Port')] |
||||
id_key = 'baremetalInstanceId' |
||||
endpoint = 'region/%(region)s/baremetal' |
||||
get_db_resources = staticmethod(db_lib.get_baremetal_instances) |
||||
|
||||
|
||||
class PortResourcesBase(AristaResourcesBase): |
||||
id_key = 'id' |
||||
|
||||
|
||||
class DhcpPorts(PortResourcesBase): |
||||
|
||||
endpoint = 'region/%(region)s/port?type=dhcp' |
||||
formatter = [AttributeFormatter('id', 'id'), |
||||
AttributeFormatter('name', 'portName'), |
||||
AttributeFormatter('device_owner', 'vlanType', |
||||
lambda *args: 'allowed'), |
||||
AttributeFormatter('network_id', 'networkId'), |
||||
AttributeFormatter('device_id', 'instanceId'), |
||||
AttributeFormatter('device_owner', 'instanceType', |
||||
lambda *args: 'dhcp'), |
||||
AttributeFormatter('project_id', 'tenantId')] |
||||
get_db_resources = staticmethod(db_lib.get_dhcp_ports) |
||||
|
||||
|
||||
class RouterPorts(PortResourcesBase): |
||||
|
||||
endpoint = 'region/%(region)s/port?type=router' |
||||
formatter = [AttributeFormatter('id', 'id'), |
||||
AttributeFormatter('name', 'portName'), |
||||
AttributeFormatter('device_owner', 'vlanType', |
||||
lambda *args: 'allowed'), |
||||
AttributeFormatter('network_id', 'networkId'), |
||||
AttributeFormatter('device_id', 'instanceId'), |
||||
AttributeFormatter('device_owner', 'instanceType', |
||||
lambda *args: 'router'), |
||||
AttributeFormatter('project_id', 'tenantId')] |
||||
get_db_resources = staticmethod(db_lib.get_router_ports) |
||||
|
||||
|
||||
class VmPorts(PortResourcesBase): |
||||
|
||||
endpoint = 'region/%(region)s/port?type=vm' |
||||
formatter = [AttributeFormatter('id', 'id'), |
||||
AttributeFormatter('name', 'portName'), |
||||
AttributeFormatter('device_owner', 'vlanType', |
||||
lambda *args: 'allowed'), |
||||
AttributeFormatter('network_id', 'networkId'), |
||||
AttributeFormatter('device_id', 'instanceId'), |
||||
AttributeFormatter('device_owner', 'instanceType', |
||||
lambda *args: 'vm'), |
||||
AttributeFormatter('project_id', 'tenantId')] |
||||
get_db_resources = staticmethod(db_lib.get_vm_ports) |
||||
|
||||
|
||||
class BaremetalPorts(PortResourcesBase): |
||||
|
||||
def _get_vlan_type(device_owner): |
||||
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'), |
||||
AttributeFormatter('name', 'portName'), |
||||
AttributeFormatter('device_owner', 'vlanType', |
||||
_get_vlan_type), |
||||
AttributeFormatter('network_id', 'networkId'), |
||||
AttributeFormatter('device_id', 'instanceId'), |
||||
AttributeFormatter('device_owner', 'instanceType', |
||||
lambda *args: 'baremetal'), |
||||
AttributeFormatter('project_id', 'tenantId')] |
||||
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) |
||||
|
||||
@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 |