config/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/address.py

526 lines
20 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 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 route
from sysinv.api.controllers.v1 import types
from sysinv.api.controllers.v1 import utils
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import utils as cutils
from sysinv import objects
from sysinv.openstack.common.gettextutils import _
from sysinv.openstack.common import log
LOG = log.getLogger(__name__)
# Defines the list of interface network types that support addresses
ALLOWED_NETWORK_TYPES = [constants.NETWORK_TYPE_MGMT,
constants.NETWORK_TYPE_INFRA,
constants.NETWORK_TYPE_OAM,
constants.NETWORK_TYPE_CLUSTER_HOST,
constants.NETWORK_TYPE_DATA]
class Address(base.APIBase):
"""API representation of an IP address.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of an IP
address.
"""
id = int
"Unique ID for this address"
uuid = types.uuid
"Unique UUID for this address"
interface_uuid = types.uuid
"Unique UUID of the parent interface"
ifname = wtypes.text
"User defined name of the interface"
address = types.ipaddress
"IP address"
prefix = int
"IP address prefix length"
name = wtypes.text
"User defined name of the address"
enable_dad = bool
"Enables or disables duplicate address detection"
forihostid = int
"The ID of the host this interface belongs to"
pool_uuid = wtypes.text
"The UUID of the address pool from which this address was allocated"
def __init__(self, **kwargs):
self.fields = objects.address.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.address)
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(Address, self).as_dict()
data['family'] = self._get_family()
return data
@classmethod
def convert_with_links(cls, rpc_address, expand=True):
address = Address(**rpc_address.as_dict())
if not expand:
address.unset_fields_except(['uuid', 'address',
'prefix', 'interface_uuid', 'ifname',
'forihostid', 'enable_dad',
'pool_uuid'])
return address
def _validate_prefix(self):
if self.prefix < 1:
raise ValueError(_("Address prefix must be greater than 1 for "
"data network type"))
def _validate_zero_address(self):
data = netaddr.IPAddress(self.address)
if data.value == 0:
raise ValueError(_("Address must not be null"))
def _validate_zero_network(self):
data = netaddr.IPNetwork(self.address + "/" + str(self.prefix))
network = data.network
if network.value == 0:
raise ValueError(_("Network must not be null"))
def _validate_address(self):
"""
Validates that the prefix is valid for the IP address family.
"""
try:
value = netaddr.IPNetwork(self.address + "/" + str(self.prefix))
except netaddr.core.AddrFormatError:
raise ValueError(_("Invalid IP address and prefix"))
mask = value.hostmask
host = value.ip & mask
if host.value == 0:
raise ValueError(_("Host bits must not be zero"))
if host == mask:
raise ValueError(_("Address cannot be the network "
"broadcast address"))
def _validate_address_type(self):
address = netaddr.IPAddress(self.address)
if not address.is_unicast():
raise ValueError(_("Address must be a unicast address"))
def _validate_name(self):
if self.name:
# follows the same naming convention as a host name since it
# typically contains the hostname with a network type suffix
utils.is_valid_hostname(self.name)
def validate_syntax(self):
"""
Validates the syntax of each field.
"""
self._validate_prefix()
self._validate_zero_address()
self._validate_zero_network()
self._validate_address()
self._validate_address_type()
self._validate_name()
class AddressCollection(collection.Collection):
"""API representation of a collection of IP addresses."""
addresses = [Address]
"A list containing IP Address objects"
def __init__(self, **kwargs):
self._type = 'addresses'
@classmethod
def convert_with_links(cls, rpc_addresses, limit, url=None,
expand=False, **kwargs):
collection = AddressCollection()
collection.addresses = [Address.convert_with_links(a, expand)
for a in rpc_addresses]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
LOCK_NAME = 'AddressController'
class AddressController(rest.RestController):
"""REST controller for Addresses."""
def __init__(self, parent=None, **kwargs):
self._parent = parent
def _get_address_collection(self, parent_uuid,
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.address.get_by_uuid(
pecan.request.context, marker)
if self._parent == "ihosts":
addresses = pecan.request.dbapi.addresses_get_by_host(
parent_uuid, family=0,
limit=limit, marker=marker_obj,
sort_key=sort_key, sort_dir=sort_dir)
elif self._parent == "iinterfaces":
addresses = pecan.request.dbapi.addresses_get_by_interface(
parent_uuid, family=0,
limit=limit, marker=marker_obj,
sort_key=sort_key, sort_dir=sort_dir)
else:
addresses = pecan.request.dbapi.addresses_get_all(
family=0, limit=limit, marker=marker_obj,
sort_key=sort_key, sort_dir=sort_dir)
return AddressCollection.convert_with_links(
addresses, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
def _query_address(self, address):
try:
result = pecan.request.dbapi.address_query(address)
except exception.AddressNotFoundByAddress:
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)
if not interface.networktype:
raise exception.InterfaceNetworkTypeNotSet()
if interface.networktype not in ALLOWED_NETWORK_TYPES:
raise exception.UnsupportedInterfaceNetworkType(
networktype=interface.networktype)
return
def _check_infra_address(self, interface_id, address):
# Check that infra network is configured
try:
infra = pecan.request.dbapi.iinfra_get_one()
except exception.NetworkTypeNotFound:
raise exception.InfrastructureNetworkNotConfigured()
subnet = netaddr.IPNetwork(infra.infra_subnet)
# Check that the correct prefix was entered
prefix = subnet.prefixlen
if address['prefix'] != prefix:
raise exception.IncorrectPrefix(length=prefix)
# Check for existing on the infra subnet and between low/high
low = infra.infra_start
high = infra.infra_end
if netaddr.IPAddress(address['address']) not in \
netaddr.IPRange(low, high):
raise exception.IpAddressOutOfRange(address=address['address'],
low=low, high=high)
return
def _check_address_mode(self, interface_id, family):
interface = pecan.request.dbapi.iinterface_get(interface_id)
if family == constants.IPV4_FAMILY:
if interface['ipv4_mode'] != constants.IPV4_STATIC:
raise exception.AddressModeMustBeStatic(
family=constants.IP_FAMILIES[family])
elif family == constants.IPV6_FAMILY:
if interface['ipv6_mode'] != constants.IPV6_STATIC:
raise exception.AddressModeMustBeStatic(
family=constants.IP_FAMILIES[family])
return
def _check_duplicate_address(self, address):
result = self._query_address(address)
if not result:
return
raise exception.AddressAlreadyExists(address=address['address'],
prefix=address['prefix'])
def _is_same_subnet(self, a, b):
if a['prefix'] != b['prefix']:
return False
_a = netaddr.IPNetwork(a['address'] + "/" + str(a['prefix']))
_b = netaddr.IPNetwork(b['address'] + "/" + str(b['prefix']))
if _a.network == _b.network:
return True
return False
def _check_duplicate_subnet(self, host_id, address):
result = pecan.request.dbapi.addresses_get_by_host(host_id)
for entry in result:
if self._is_same_subnet(entry, address):
raise exception.AddressInSameSubnetExists(
**{'address': entry['address'],
'prefix': entry['prefix'],
'interface': entry['interface_uuid']})
def _check_address_count(self, interface_id, host_id):
interface = pecan.request.dbapi.iinterface_get(interface_id)
networktype = interface['networktype']
sdn_enabled = utils.get_sdn_enabled()
if networktype == constants.NETWORK_TYPE_DATA and not sdn_enabled:
# Is permitted to add multiple addresses only
# if SDN L3 mode is not enabled.
return
# There can only be one 'data' interface with an IP address
# where SDN is enabled
if (sdn_enabled):
iface_list = pecan.request.dbapi.iinterface_get_all(host_id)
for iface in iface_list:
uuid = iface['uuid']
# skip the one we came in with
if uuid == interface_id:
continue
if iface['ifclass'] == constants.NETWORK_TYPE_DATA:
addresses = (
pecan.request.dbapi.addresses_get_by_interface(uuid))
if len(addresses) != 0:
raise exception.\
AddressLimitedToOneWithSDN(iftype=networktype)
def _check_address_conflicts(self, host_id, interface_id, address):
self._check_address_count(interface_id, host_id)
self._check_duplicate_address(address)
self._check_duplicate_subnet(host_id, address)
def _check_host_state(self, host_id):
host = pecan.request.dbapi.ihost_get(host_id)
if utils.is_aio_simplex_host_unlocked(host):
raise exception.HostMustBeLocked(host=host['hostname'])
elif host['administrative'] != constants.ADMIN_LOCKED and not \
utils.is_host_simplex_controller(host):
raise exception.HostMustBeLocked(host=host['hostname'])
def _check_from_pool(self, pool_uuid):
if pool_uuid:
raise exception.AddressAllocatedFromPool()
def _check_orphaned_routes(self, interface_id, address):
routes = pecan.request.dbapi.routes_get_by_interface(interface_id)
for r in routes:
if route.Route.address_in_subnet(r['gateway'],
address['address'],
address['prefix']):
raise exception.AddressInUseByRouteGateway(
address=address['address'],
network=r['network'], prefix=r['prefix'],
gateway=r['gateway'])
def _check_dad_state(self, address):
if address['family'] == constants.IPV4_FAMILY:
if address['enable_dad']:
raise exception.DuplicateAddressDetectionNotSupportedOnIpv4()
else:
if not address['enable_dad']:
raise exception.DuplicateAddressDetectionRequiredOnIpv6()
def _check_managed_addr(self, host_id, interface_id):
# Check that static address alloc is enabled
interface = pecan.request.dbapi.iinterface_get(interface_id)
networktype = interface['networktype']
if networktype not in [constants.NETWORK_TYPE_MGMT,
constants.NETWORK_TYPE_INFRA,
constants.NETWORK_TYPE_OAM]:
return
network = pecan.request.dbapi.network_get_by_type(networktype)
if network.dynamic:
raise exception.StaticAddressNotConfigured()
host = pecan.request.dbapi.ihost_get(host_id)
if host['personality'] in [constants.STORAGE]:
raise exception.ManagedIPAddress()
def _check_managed_infra_addr(self, host_id):
# Check that static address alloc is enabled
network = pecan.request.dbapi.network_get_by_type(
constants.NETWORK_TYPE_INFRA)
if network.dynamic:
raise exception.StaticAddressNotConfigured()
host = pecan.request.dbapi.ihost_get(host_id)
if host['personality'] in [constants.STORAGE]:
raise exception.ManagedIPAddress()
def _check_name_conflict(self, address):
name = address.get('name', None)
if name is None:
return
try:
pecan.request.dbapi.address_get_by_name(name)
raise exception.AddressNameExists(name=name)
except exception.AddressNotFoundByName:
pass
def _check_subnet_valid(self, pool, address):
network = {'address': pool.network, 'prefix': pool.prefix}
if not self._is_same_subnet(network, address):
raise exception.AddressNetworkInvalid(**address)
def _set_defaults(self, address):
address['uuid'] = str(uuid.uuid4())
if 'enable_dad' not in address:
family = address['family']
address['enable_dad'] = constants.IP_DAD_STATES[family]
def _create_infra_addr(self, address_dict, host_id, interface_id):
self._check_duplicate_address(address_dict)
self._check_managed_addr(host_id, interface_id)
self._check_infra_address(interface_id, address_dict)
# Inform conductor of the change
LOG.info("calling rpc with addr %s ihostid %s " % (
address_dict['address'], host_id))
return pecan.request.rpcapi.infra_ip_set_by_ihost(
pecan.request.context, host_id, address_dict['address'])
def _create_interface_addr(self, address_dict, host_id, interface_id):
self._check_address_conflicts(host_id, interface_id, address_dict)
self._check_dad_state(address_dict)
self._check_managed_addr(host_id, interface_id)
address_dict['interface_id'] = interface_id
# Attempt to create the new address record
return pecan.request.dbapi.address_create(address_dict)
def _create_pool_addr(self, pool_id, address_dict):
self._check_duplicate_address(address_dict)
address_dict['address_pool_id'] = pool_id
# Attempt to create the new address record
return pecan.request.dbapi.address_create(address_dict)
def _create_address(self, address):
address.validate_syntax()
address_dict = address.as_dict()
self._set_defaults(address_dict)
interface_uuid = address_dict.pop('interface_uuid', None)
pool_uuid = address_dict.pop('pool_uuid', None)
if interface_uuid is not None:
# Query parent object references
host_id, interface_id = self._get_parent_id(interface_uuid)
interface = pecan.request.dbapi.iinterface_get(interface_id)
# Check for semantic conflicts
self._check_interface_type(interface_id)
self._check_host_state(host_id)
self._check_address_mode(interface_id, address_dict['family'])
if (interface['networktype'] == constants.NETWORK_TYPE_INFRA):
result = self._create_infra_addr(
address_dict, host_id, interface_id)
else:
result = self._create_interface_addr(
address_dict, host_id, interface_id)
elif pool_uuid is not None:
pool = pecan.request.dbapi.address_pool_get(pool_uuid)
self._check_subnet_valid(pool, address_dict)
self._check_name_conflict(address_dict)
result = self._create_pool_addr(pool.id, address_dict)
else:
raise ValueError(_("Address must provide an interface or pool"))
return Address.convert_with_links(result)
def _get_one(self, address_uuid):
rpc_address = objects.address.get_by_uuid(
pecan.request.context, address_uuid)
return Address.convert_with_links(rpc_address)
@wsme_pecan.wsexpose(AddressCollection, 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 Addresses."""
return self._get_address_collection(parent_uuid, marker, limit,
sort_key, sort_dir)
@wsme_pecan.wsexpose(Address, types.uuid)
def get_one(self, address_uuid):
return self._get_one(address_uuid)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(Address, body=Address)
def post(self, addr):
"""Create a new IP address."""
return self._create_address(addr)
def _delete_infra_addr(self, address):
# Check if it's a config-managed infra ip address
self._check_managed_infra_addr(getattr(address, 'forihostid'))
# Inform conductor of removal (handles dnsmasq + address object)
pecan.request.rpcapi.infra_ip_set_by_ihost(
pecan.request.context,
getattr(address, 'forihostid'),
None)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
def delete(self, address_uuid):
"""Delete an IP address."""
address = self._get_one(address_uuid)
interface_uuid = getattr(address, 'interface_uuid')
self._check_orphaned_routes(interface_uuid, address.as_dict())
self._check_host_state(getattr(address, 'forihostid'))
self._check_from_pool(getattr(address, 'pool_uuid'))
interface = pecan.request.dbapi.iinterface_get(interface_uuid)
if (interface['networktype'] == constants.NETWORK_TYPE_INFRA):
self._delete_infra_addr(address)
else:
pecan.request.dbapi.address_destroy(address_uuid)