c9f23471e3
- Added the name field to the network table - Removed mtu, link_capacity and vlan_id from the network table - Removed unused networktypes for bm, control and data-vrs - Added four interface classes (platform, data, pci-passthrough, pci-sriov) to interface table - Assign networks to interface during interface creation and modification - Allow infrastructure network and management network to share an interface with or without VLAN. - Updated sysinv puppet for interface and network Story: 2003087 Task: 23171 Depends-On: https://review.openstack.org/#/c/601155 Change-Id: I2e211767639484992f868084eb47baacbe5ea83a Signed-off-by: Teresa Ho <teresa.ho@windriver.com>
383 lines
14 KiB
Python
383 lines
14 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright 2013 UnitedStack 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.
|
|
#
|
|
# Copyright (c) 2015-2016 Wind River Systems, Inc.
|
|
#
|
|
|
|
|
|
import netaddr
|
|
import uuid
|
|
|
|
import pecan
|
|
from pecan import rest
|
|
|
|
from wsme import types as wtypes
|
|
import wsmeext.pecan as wsme_pecan
|
|
|
|
from sysinv.api.controllers.v1 import base
|
|
from sysinv.api.controllers.v1 import collection
|
|
from sysinv.api.controllers.v1 import types
|
|
from sysinv.api.controllers.v1 import utils
|
|
from sysinv.common import exception
|
|
from sysinv.common import constants
|
|
from sysinv.common import utils as cutils
|
|
from sysinv import objects
|
|
from sysinv.openstack.common import log
|
|
from sysinv.openstack.common.gettextutils import _
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
# Maximum number of equal cost paths for a destination subnet
|
|
SYSINV_ROUTE_MAX_PATHS = 4
|
|
|
|
# Defines the list of interface network types that support routes
|
|
ALLOWED_NETWORK_TYPES = [constants.NETWORK_TYPE_DATA,
|
|
constants.NETWORK_TYPE_MGMT]
|
|
|
|
|
|
class Route(base.APIBase):
|
|
"""API representation of an IP route.
|
|
|
|
This class enforces type checking and value constraints, and converts
|
|
between the internal object model and the API representation of an IP
|
|
route.
|
|
"""
|
|
|
|
id = int
|
|
"Unique ID for this route"
|
|
|
|
uuid = types.uuid
|
|
"Unique UUID for this route"
|
|
|
|
interface_uuid = types.uuid
|
|
"Unique UUID of the parent interface"
|
|
|
|
ifname = wtypes.text
|
|
"User defined name of the interface"
|
|
|
|
network = types.ipaddress
|
|
"IP route network address"
|
|
|
|
prefix = int
|
|
"IP route prefix length"
|
|
|
|
gateway = types.ipaddress
|
|
"IP route nexthop gateway address"
|
|
|
|
metric = int
|
|
"IP route metric"
|
|
|
|
forihostid = int
|
|
"The ID of the host this interface belongs to"
|
|
|
|
def __init__(self, **kwargs):
|
|
self.fields = objects.route.fields.keys()
|
|
for k in self.fields:
|
|
if not hasattr(self, k):
|
|
# Skip fields that we choose to hide
|
|
continue
|
|
setattr(self, k, kwargs.get(k, wtypes.Unset))
|
|
|
|
def _get_family(self):
|
|
value = netaddr.IPAddress(self.network)
|
|
return value.version
|
|
|
|
def as_dict(self):
|
|
"""
|
|
Sets additional DB only attributes when converting from an API object
|
|
type to a dictionary that will be used to populate the DB.
|
|
"""
|
|
data = super(Route, self).as_dict()
|
|
data['family'] = self._get_family()
|
|
return data
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, rpc_route, expand=True):
|
|
route = Route(**rpc_route.as_dict())
|
|
if not expand:
|
|
route.unset_fields_except(['uuid', 'network', 'prefix', 'gateway',
|
|
'metric',
|
|
'inteface_uuid', 'ifname',
|
|
'forihostid'])
|
|
return route
|
|
|
|
def _validate_network_prefix(self):
|
|
"""
|
|
Validates that the prefix is valid for the IP address family and that
|
|
there are no host bits set.
|
|
"""
|
|
try:
|
|
cidr = netaddr.IPNetwork(self.network + "/" + str(self.prefix))
|
|
except netaddr.core.AddrFormatError:
|
|
raise ValueError(_("Invalid IP address and prefix"))
|
|
address = netaddr.IPAddress(self.network)
|
|
if address != cidr.network:
|
|
raise ValueError(_("Invalid IP network %(address)s/%(prefix)s "
|
|
"expecting %(network)s/%(prefix)s") %
|
|
{'address': self.network,
|
|
'prefix': self.prefix,
|
|
'network': cidr.network})
|
|
|
|
def _validate_zero_network(self):
|
|
data = netaddr.IPNetwork(self.network + "/" + str(self.prefix))
|
|
network = data.network
|
|
if self.prefix != 0 and network.value == 0:
|
|
raise ValueError(_("Network must not be null when prefix is non zero"))
|
|
|
|
def _validate_metric(self):
|
|
if self.metric < 0:
|
|
raise ValueError(_("Route metric must be greater than zero"))
|
|
|
|
@classmethod
|
|
def address_in_subnet(self, gateway, address, prefix):
|
|
subnet = netaddr.IPNetwork(address + "/" + str(prefix))
|
|
ipaddr = netaddr.IPAddress(gateway)
|
|
if subnet.network == (ipaddr & subnet.netmask):
|
|
return True
|
|
return False
|
|
|
|
def _validate_gateway(self):
|
|
gateway = netaddr.IPAddress(self.gateway)
|
|
if gateway.value == 0:
|
|
raise ValueError(_("Gateway address must not be null"))
|
|
if self.prefix and Route.address_in_subnet(
|
|
self.gateway, self.network, self.prefix):
|
|
|
|
raise ValueError(_("Gateway address must not be within "
|
|
"destination subnet"))
|
|
|
|
def _validate_addresses(self):
|
|
network = netaddr.IPAddress(self.network)
|
|
gateway = netaddr.IPAddress(self.gateway)
|
|
if network == gateway:
|
|
raise ValueError(_("Network and gateway IP addresses "
|
|
"must be different"))
|
|
|
|
def _validate_families(self):
|
|
network = netaddr.IPAddress(self.network)
|
|
gateway = netaddr.IPAddress(self.gateway)
|
|
if network.version != gateway.version:
|
|
raise ValueError(_("Network and gateway IP versions must match"))
|
|
|
|
def _validate_unicast_addresses(self):
|
|
network = netaddr.IPAddress(self.network)
|
|
gateway = netaddr.IPAddress(self.gateway)
|
|
if not network.is_unicast():
|
|
raise ValueError(_("Network address must be a unicast address"))
|
|
if not gateway.is_unicast():
|
|
raise ValueError(_("Gateway address must be a unicast address"))
|
|
|
|
def validate_syntax(self):
|
|
"""
|
|
Validates the syntax of each field.
|
|
"""
|
|
self._validate_network_prefix()
|
|
self._validate_zero_network()
|
|
self._validate_families()
|
|
self._validate_unicast_addresses()
|
|
self._validate_addresses()
|
|
self._validate_gateway()
|
|
self._validate_metric()
|
|
|
|
|
|
class RouteCollection(collection.Collection):
|
|
"""API representation of a collection of IP routes."""
|
|
|
|
routes = [Route]
|
|
"A list containing IP Route objects"
|
|
|
|
def __init__(self, **kwargs):
|
|
self._type = 'routes'
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, rpc_routes, limit, url=None,
|
|
expand=False, **kwargs):
|
|
collection = RouteCollection()
|
|
collection.routes = [Route.convert_with_links(a, expand)
|
|
for a in rpc_routes]
|
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
|
return collection
|
|
|
|
|
|
LOCK_NAME = 'RouteController'
|
|
|
|
|
|
class RouteController(rest.RestController):
|
|
"""REST controller for Routes."""
|
|
|
|
def __init__(self, parent=None, **kwargs):
|
|
self._parent = parent
|
|
|
|
def _get_route_collection(self, parent_uuid=None,
|
|
marker=None, limit=None, sort_key=None,
|
|
sort_dir=None, expand=False,
|
|
resource_url=None):
|
|
limit = utils.validate_limit(limit)
|
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
|
marker_obj = None
|
|
|
|
if marker:
|
|
marker_obj = objects.route.get_by_uuid(
|
|
pecan.request.context, marker)
|
|
|
|
if self._parent == "ihosts":
|
|
routes = pecan.request.dbapi.routes_get_by_host(
|
|
parent_uuid,
|
|
limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
|
elif self._parent == "iinterfaces":
|
|
routes = pecan.request.dbapi.routes_get_by_interface(
|
|
parent_uuid,
|
|
limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
|
else:
|
|
routes = pecan.request.dbapi.routes_get_all(
|
|
limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
|
|
|
return RouteCollection.convert_with_links(
|
|
routes, limit, url=resource_url, expand=expand,
|
|
sort_key=sort_key, sort_dir=sort_dir)
|
|
|
|
def _query_route(self, host_id, route):
|
|
try:
|
|
result = pecan.request.dbapi.route_query(host_id, route)
|
|
except exception.RouteNotFoundByName:
|
|
return None
|
|
return result
|
|
|
|
def _get_parent_id(self, interface_uuid):
|
|
interface = pecan.request.dbapi.iinterface_get(interface_uuid)
|
|
return (interface['forihostid'], interface['id'])
|
|
|
|
def _check_interface_type(self, interface_id):
|
|
interface = pecan.request.dbapi.iinterface_get(interface_id)
|
|
networktype = interface['networktype']
|
|
if networktype not in ALLOWED_NETWORK_TYPES:
|
|
raise exception.RoutesNotSupportedOnInterfaces(iftype=networktype)
|
|
return
|
|
|
|
def _check_duplicate_route(self, host_id, route):
|
|
result = self._query_route(host_id, route)
|
|
if not result:
|
|
return
|
|
raise exception.RouteAlreadyExists(network=route['network'],
|
|
prefix=route['prefix'],
|
|
gateway=route['gateway'])
|
|
|
|
def _is_same_subnet(self, a, b):
|
|
if a['prefix'] != b['prefix']:
|
|
return False
|
|
if a['metric'] != b['metric']:
|
|
return False
|
|
_a = netaddr.IPNetwork(a['network'] + "/" + str(a['prefix']))
|
|
_b = netaddr.IPNetwork(b['network'] + "/" + str(b['prefix']))
|
|
if _a.network == _b.network:
|
|
return True
|
|
return False
|
|
|
|
def _check_duplicate_subnet(self, host_id, route):
|
|
result = pecan.request.dbapi.routes_get_by_host(host_id)
|
|
count = 0
|
|
for entry in result:
|
|
if self._is_same_subnet(entry, route):
|
|
count += 1
|
|
if count >= SYSINV_ROUTE_MAX_PATHS:
|
|
raise exception.RouteMaxPathsForSubnet(
|
|
count=SYSINV_ROUTE_MAX_PATHS,
|
|
network=entry['network'],
|
|
prefix=entry['prefix'])
|
|
|
|
def _check_reachable_gateway(self, interface_id, route):
|
|
result = pecan.request.dbapi.addresses_get_by_interface(interface_id)
|
|
for address in result:
|
|
if Route.address_in_subnet(route['gateway'],
|
|
address['address'],
|
|
address['prefix']):
|
|
return
|
|
result = pecan.request.dbapi.address_pools_get_by_interface(
|
|
interface_id)
|
|
for pool in result:
|
|
if Route.address_in_subnet(route['gateway'],
|
|
pool['network'],
|
|
pool['prefix']):
|
|
return
|
|
raise exception.RouteGatewayNotReachable(gateway=route['gateway'])
|
|
|
|
def _check_local_gateway(self, host_id, route):
|
|
address = {'address': route['gateway']}
|
|
try:
|
|
result = pecan.request.dbapi.address_query(address)
|
|
# It is OK to set up a route to a gateway. Gateways are not
|
|
# local addresses.
|
|
if 'gateway' not in result.name:
|
|
raise exception.RouteGatewayCannotBeLocal(
|
|
gateway=route['gateway'])
|
|
except exception.AddressNotFoundByAddress:
|
|
pass
|
|
return
|
|
|
|
def _check_route_conflicts(self, host_id, route):
|
|
self._check_duplicate_route(host_id, route)
|
|
self._check_duplicate_subnet(host_id, route)
|
|
|
|
def _create_route(self, route):
|
|
route.validate_syntax()
|
|
route = route.as_dict()
|
|
route['uuid'] = str(uuid.uuid4())
|
|
interface_uuid = route.pop('interface_uuid')
|
|
# Query parent object references
|
|
host_id, interface_id = self._get_parent_id(interface_uuid)
|
|
# Check for semantic conflicts
|
|
self._check_interface_type(interface_id)
|
|
self._check_route_conflicts(host_id, route)
|
|
self._check_local_gateway(host_id, route)
|
|
self._check_reachable_gateway(interface_id, route)
|
|
# Attempt to create the new route record
|
|
result = pecan.request.dbapi.route_create(interface_id, route)
|
|
pecan.request.rpcapi.update_route_config(pecan.request.context)
|
|
|
|
return Route.convert_with_links(result)
|
|
|
|
def _get_one(self, route_uuid):
|
|
rpc_route = objects.route.get_by_uuid(
|
|
pecan.request.context, route_uuid)
|
|
return Route.convert_with_links(rpc_route)
|
|
|
|
@wsme_pecan.wsexpose(RouteCollection, types.uuid, types.uuid, int,
|
|
wtypes.text, wtypes.text)
|
|
def get_all(self, parent_uuid=None,
|
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
|
"""Retrieve a list of IP Routes."""
|
|
return self._get_route_collection(parent_uuid, marker, limit,
|
|
sort_key=sort_key, sort_dir=sort_dir)
|
|
|
|
@wsme_pecan.wsexpose(Route, types.uuid)
|
|
def get_one(self, route_uuid):
|
|
return self._get_one(route_uuid)
|
|
|
|
@cutils.synchronized(LOCK_NAME)
|
|
@wsme_pecan.wsexpose(Route, body=Route)
|
|
def post(self, route):
|
|
"""Create a new IP route."""
|
|
return self._create_route(route)
|
|
|
|
@cutils.synchronized(LOCK_NAME)
|
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
|
def delete(self, route_uuid):
|
|
"""Delete an IP route."""
|
|
self._get_one(route_uuid)
|
|
pecan.request.dbapi.route_destroy(route_uuid)
|
|
pecan.request.rpcapi.update_route_config(pecan.request.context)
|