956 lines
31 KiB
Python
956 lines
31 KiB
Python
# Copyright 2014 DreamHost, LLC
|
|
#
|
|
# Author: DreamHost, LLC
|
|
#
|
|
# 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 collections
|
|
import itertools
|
|
import socket
|
|
import time
|
|
import uuid
|
|
|
|
import netaddr
|
|
import six
|
|
|
|
from neutronclient.v2_0 import client
|
|
from neutronclient.common import exceptions as neutron_exc
|
|
|
|
from oslo_config import cfg
|
|
from oslo_context import context
|
|
from oslo_log import log as logging
|
|
from oslo_utils import importutils
|
|
|
|
from astara.common.i18n import _, _LI, _LW
|
|
from astara.common.linux import ip_lib
|
|
from astara.api import keystone
|
|
from astara.common import rpc
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
|
|
|
|
neutron_opts = [
|
|
cfg.StrOpt('management_network_id'),
|
|
cfg.StrOpt('external_network_id'),
|
|
cfg.StrOpt('management_subnet_id'),
|
|
cfg.StrOpt('external_subnet_id'),
|
|
cfg.StrOpt('management_prefix', default='fdca:3ba5:a17a:acda::/64'),
|
|
cfg.StrOpt('external_prefix', default='172.16.77.0/24'),
|
|
cfg.IntOpt('astara_mgt_service_port', default=5000),
|
|
cfg.StrOpt('default_instance_flavor', default=1),
|
|
cfg.StrOpt('interface_driver',
|
|
default='astara.common.linux.interface.OVSInterfaceDriver'),
|
|
cfg.BoolOpt('neutron_port_security_extension_enabled', default=True),
|
|
|
|
# legacy_fallback option is deprecated and will be removed in the N-release
|
|
cfg.BoolOpt('legacy_fallback_mode', default=True,
|
|
help=_('Check for resources using the Liberty naming scheme '
|
|
'when the modern name does not exist.'))
|
|
]
|
|
CONF.register_opts(neutron_opts)
|
|
|
|
|
|
# copied from Neutron source
|
|
DEVICE_OWNER_ROUTER_MGT = "network:router_management"
|
|
DEVICE_OWNER_ROUTER_INT = "network:router_interface"
|
|
DEVICE_OWNER_ROUTER_GW = "network:router_gateway"
|
|
DEVICE_OWNER_FLOATINGIP = "network:floatingip"
|
|
DEVICE_OWNER_RUG = "network:astara"
|
|
|
|
PLUGIN_ROUTER_RPC_TOPIC = 'q-l3-plugin'
|
|
|
|
STATUS_ACTIVE = 'ACTIVE'
|
|
STATUS_BUILD = 'BUILD'
|
|
STATUS_DOWN = 'DOWN'
|
|
STATUS_ERROR = 'ERROR'
|
|
|
|
# Service operation status constants
|
|
# Copied from neutron.plugings.common.constants.py
|
|
# prefaced here with PLUGIN_
|
|
PLUGIN_ACTIVE = "ACTIVE"
|
|
PLUGIN_DOWN = "DOWN"
|
|
PLUGIN_CREATED = "CREATED"
|
|
PLUGIN_PENDING_CREATE = "PENDING_CREATE"
|
|
PLUGIN_PENDING_UPDATE = "PENDING_UPDATE"
|
|
PLUGIN_PENDING_DELETE = "PENDING_DELETE"
|
|
PLUGIN_INACTIVE = "INACTIVE"
|
|
PLUGIN_ERROR = "ERROR"
|
|
|
|
# XXX not sure these are needed?
|
|
ACTIVE_PENDING_STATUSES = (
|
|
PLUGIN_ACTIVE,
|
|
PLUGIN_PENDING_CREATE,
|
|
PLUGIN_PENDING_UPDATE
|
|
)
|
|
|
|
|
|
class RouterGone(Exception):
|
|
pass
|
|
|
|
|
|
class LoadBalancerGone(Exception):
|
|
pass
|
|
|
|
|
|
class RouterGatewayMissing(Exception):
|
|
pass
|
|
|
|
|
|
class MissingIPAllocation(Exception):
|
|
|
|
def __init__(self, port_id, missing=None):
|
|
self.port_id = port_id
|
|
self.missing = missing
|
|
msg = 'Port %s missing expected IPs ' % port_id
|
|
if missing:
|
|
ip_msg = ' and '.join(
|
|
('IPv%s address from one of %s' %
|
|
(mv, missing_subnets))
|
|
for mv, missing_subnets in missing
|
|
)
|
|
msg = msg + ip_msg
|
|
super(MissingIPAllocation, self).__init__(msg)
|
|
|
|
|
|
class DictModelBase(object):
|
|
DICT_ATTRS = ()
|
|
|
|
def __repr__(self):
|
|
return '<%s (%s:%s)>' % (self.__class__.__name__,
|
|
getattr(self, 'name', ''),
|
|
getattr(self, 'tenant_id', ''))
|
|
|
|
def __eq__(self, other):
|
|
return type(self) == type(other) and vars(self) == vars(other)
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def to_dict(self):
|
|
"""Serialize the object into a dict, handy for building config"""
|
|
d = {}
|
|
for attr in self.DICT_ATTRS:
|
|
val = getattr(self, attr)
|
|
if isinstance(val, list):
|
|
# this'll eventually break something and you can find this
|
|
# comment and hurt me.
|
|
val = [v.to_dict() for v in val]
|
|
if hasattr(val, 'to_dict'):
|
|
val = val.to_dict()
|
|
if isinstance(val, netaddr.IPAddress):
|
|
val = str(val)
|
|
d[attr] = val
|
|
return d
|
|
|
|
|
|
class Router(object):
|
|
def __init__(self, id_, tenant_id, name, admin_state_up, status,
|
|
external_port=None, internal_ports=None, floating_ips=None):
|
|
self.id = id_
|
|
self.tenant_id = tenant_id
|
|
self.name = name
|
|
self.admin_state_up = admin_state_up
|
|
self.status = status
|
|
self.external_port = external_port
|
|
self.internal_ports = internal_ports or []
|
|
self.floating_ips = floating_ips or []
|
|
|
|
def __repr__(self):
|
|
return '<%s (%s:%s)>' % (self.__class__.__name__,
|
|
self.name,
|
|
self.tenant_id)
|
|
|
|
def __eq__(self, other):
|
|
return type(self) == type(other) and vars(self) == vars(other)
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
external_port = None
|
|
internal_ports = []
|
|
|
|
if d.get('gw_port'):
|
|
external_port = Port.from_dict(d.get('gw_port'))
|
|
|
|
for port_dict in d.get('_interfaces', []):
|
|
port = Port.from_dict(port_dict)
|
|
if port.device_owner == DEVICE_OWNER_ROUTER_INT:
|
|
internal_ports.append(port)
|
|
|
|
fips = [FloatingIP.from_dict(fip) for fip in d.get('_floatingips', [])]
|
|
|
|
return cls(
|
|
d['id'],
|
|
d['tenant_id'],
|
|
d['name'],
|
|
d['admin_state_up'],
|
|
d['status'],
|
|
external_port,
|
|
internal_ports,
|
|
floating_ips=fips,
|
|
)
|
|
|
|
@property
|
|
def ports(self):
|
|
return itertools.chain(
|
|
[self.external_port],
|
|
self.internal_ports
|
|
)
|
|
|
|
|
|
class Network(DictModelBase):
|
|
DICT_ATTRS = ('id', 'name', 'tenant_id', 'status', 'shared',
|
|
'admin_state_up', 'mtu', 'port_security_enabled')
|
|
|
|
def __init__(self, id_, name, tenant_id, status, shared, admin_state_up,
|
|
mtu=None, port_security_enabled=False, subnets=()):
|
|
self.id = id_
|
|
self.name = name
|
|
self.tenant_id = tenant_id
|
|
self.shared = shared
|
|
self.admin_state_up = admin_state_up
|
|
self.mtu = mtu
|
|
self.port_security_enabled = port_security_enabled
|
|
self.subnets = subnets
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
optional = {}
|
|
|
|
for opt in ['mtu', 'port_security_enabled']:
|
|
if opt in d:
|
|
optional[opt] = d[opt]
|
|
|
|
return cls(
|
|
d['id'],
|
|
d['name'],
|
|
d['tenant_id'],
|
|
d['status'],
|
|
d['shared'],
|
|
d['admin_state_up'],
|
|
**optional
|
|
)
|
|
|
|
|
|
class Subnet(DictModelBase):
|
|
DICT_ATTRS = ('id', 'name', 'tenant_id', 'network_id', 'ip_version',
|
|
'cidr', 'gateway_ip', 'enable_dhcp', 'dns_nameservers',
|
|
'host_routes', 'ipv6_ra_mode')
|
|
|
|
def __init__(self, id_, name, tenant_id, network_id, ip_version, cidr,
|
|
gateway_ip, enable_dhcp, dns_nameservers, host_routes,
|
|
ipv6_ra_mode):
|
|
self.id = id_
|
|
self.name = name
|
|
self.tenant_id = tenant_id
|
|
self.network_id = network_id
|
|
self.ip_version = ip_version
|
|
try:
|
|
self.cidr = netaddr.IPNetwork(cidr)
|
|
except (TypeError, netaddr.AddrFormatError) as e:
|
|
raise ValueError(
|
|
_('Invalid CIDR %r for subnet %s of network %s: %s') % (
|
|
cidr, id_, network_id, e,
|
|
)
|
|
)
|
|
try:
|
|
self.gateway_ip = netaddr.IPAddress(gateway_ip)
|
|
except (TypeError, netaddr.AddrFormatError) as e:
|
|
self.gateway_ip = None
|
|
LOG.info(_LI(
|
|
'Bad gateway_ip on subnet %s: %r (%s)'),
|
|
id_, gateway_ip, e)
|
|
self.enable_dhcp = enable_dhcp
|
|
self.dns_nameservers = dns_nameservers
|
|
self.host_routes = host_routes
|
|
self.ipv6_ra_mode = ipv6_ra_mode
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(
|
|
d['id'],
|
|
d['name'],
|
|
d['tenant_id'],
|
|
d['network_id'],
|
|
d['ip_version'],
|
|
d['cidr'],
|
|
d['gateway_ip'],
|
|
d['enable_dhcp'],
|
|
d['dns_nameservers'],
|
|
d['host_routes'],
|
|
d['ipv6_ra_mode'])
|
|
|
|
|
|
class Port(DictModelBase):
|
|
DICT_ATTRS = ('id', 'device_id', 'fixed_ips', 'mac_address', 'network_id',
|
|
'device_owner', 'name')
|
|
|
|
def __init__(self, id_, device_id='', fixed_ips=None, mac_address='',
|
|
network_id='', device_owner='', name='',
|
|
neutron_port_dict=None):
|
|
self.id = id_
|
|
self.device_id = device_id
|
|
self.fixed_ips = fixed_ips or []
|
|
self.mac_address = mac_address
|
|
self.network_id = network_id
|
|
self.device_owner = device_owner
|
|
self.name = name
|
|
|
|
# Unlike instance ports, management ports are created at boot and
|
|
# could be created on the Pez side. We need to pass that info
|
|
# back to Rug via RPC so hang on to the original port data for
|
|
# easier serialization, allowing Rug to re-create (via from_dict).
|
|
# without another neutron call.
|
|
self._neutron_port_dict = neutron_port_dict or {}
|
|
|
|
def __eq__(self, other):
|
|
return type(self) == type(other) and vars(self) == vars(other)
|
|
|
|
@property
|
|
def first_v4(self):
|
|
for fixed_ip in self.fixed_ips:
|
|
ip = netaddr.IPAddress(fixed_ip.ip_address)
|
|
if ip.version == 4:
|
|
return str(ip)
|
|
return None
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(
|
|
d['id'],
|
|
d['device_id'],
|
|
fixed_ips=[FixedIp.from_dict(fip) for fip in d['fixed_ips']],
|
|
mac_address=d['mac_address'],
|
|
network_id=d['network_id'],
|
|
device_owner=d['device_owner'],
|
|
name=d['name'],
|
|
neutron_port_dict=d)
|
|
|
|
def to_dict(self):
|
|
return self._neutron_port_dict
|
|
|
|
|
|
class FixedIp(DictModelBase):
|
|
DICT_ATTRS = ('subnet_id', 'ip_address')
|
|
|
|
def __init__(self, subnet_id, ip_address):
|
|
self.subnet_id = subnet_id
|
|
self.ip_address = netaddr.IPAddress(ip_address)
|
|
|
|
def __eq__(self, other):
|
|
return type(self) == type(other) and vars(self) == vars(other)
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(d['subnet_id'], d['ip_address'])
|
|
|
|
|
|
class FloatingIP(object):
|
|
def __init__(self, id_, floating_ip, fixed_ip):
|
|
self.id = id_
|
|
self.floating_ip = netaddr.IPAddress(floating_ip)
|
|
self.fixed_ip = netaddr.IPAddress(fixed_ip)
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(
|
|
d['id'],
|
|
d['floating_ip_address'],
|
|
d['fixed_ip_address']
|
|
)
|
|
|
|
|
|
class LoadBalancer(DictModelBase):
|
|
DICT_ATTRS = ('id', 'tenant_id', 'name', 'admin_state_up', 'status',
|
|
'listeners', 'vip_address', 'vip_port')
|
|
|
|
def __init__(self, id_, tenant_id, name, admin_state_up, status,
|
|
vip_address=None, vip_port=None, listeners=()):
|
|
self.id = id_
|
|
self.tenant_id = tenant_id
|
|
self.name = name
|
|
self.admin_state_up = admin_state_up
|
|
self.status = status
|
|
self.vip_address = vip_address
|
|
self.vip_port = vip_port
|
|
self.listeners = listeners
|
|
|
|
@property
|
|
def ports(self):
|
|
if self.vip_port:
|
|
return [self.vip_port]
|
|
else:
|
|
return []
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
if d.get('vip_port'):
|
|
vip_port = Port.from_dict(d.get('vip_port'))
|
|
vip_address = d['vip_address']
|
|
else:
|
|
vip_port = None
|
|
vip_address = None
|
|
return cls(
|
|
d['id'],
|
|
d['tenant_id'],
|
|
d['name'],
|
|
d['admin_state_up'],
|
|
d['provisioning_status'],
|
|
vip_address,
|
|
vip_port,
|
|
[Listener.from_dict(l) for l in d.get('listeners')],
|
|
)
|
|
|
|
|
|
class Listener(DictModelBase):
|
|
DICT_ATTRS = ('id', 'tenant_id', 'name', 'admin_state_up', 'protocol',
|
|
'protocol_port', 'default_pool')
|
|
|
|
def __init__(self, id_, tenant_id, name, admin_state_up, protocol,
|
|
protocol_port, default_pool=None):
|
|
self.id = id_
|
|
self.tenant_id = tenant_id
|
|
self.name = name
|
|
self.admin_state_up = admin_state_up
|
|
self.protocol = protocol
|
|
self.protocol_port = protocol_port
|
|
self.default_pool = default_pool
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
# NOTE: we may be constructing a loadbalancer without the full
|
|
# details during pre-populate. To avoid having to do more neutron
|
|
# calls to find the additional data, support instantiation without
|
|
# full details.
|
|
return cls(
|
|
d['id'],
|
|
d.get('tenant_id'),
|
|
d.get('name'),
|
|
d.get('admin_state_up'),
|
|
d.get('protocol'),
|
|
d.get('protocol_port'),
|
|
)
|
|
|
|
|
|
class Pool(DictModelBase):
|
|
DICT_ATTRS = (
|
|
'id', 'tenant_id', 'name', 'admin_state_up', 'lb_algorithm',
|
|
'protocol', 'healthmonitor', 'session_persistence', 'members'
|
|
)
|
|
|
|
def __init__(self, id_, tenant_id, name, admin_state_up, lb_algorithm,
|
|
protocol, healthmonitor=None, session_persistence=None,
|
|
members=()):
|
|
self.id = id_
|
|
self.tenant_id = tenant_id
|
|
self.name = name
|
|
self.admin_state_up = admin_state_up
|
|
self.lb_algorithm = lb_algorithm
|
|
self.protocol = protocol
|
|
self.healthmonitor = healthmonitor
|
|
self.session_persistence = session_persistence
|
|
self.members = members
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(
|
|
d['id'],
|
|
d['tenant_id'],
|
|
d['name'],
|
|
d['admin_state_up'],
|
|
d['lb_algorithm'],
|
|
d['protocol'],
|
|
)
|
|
|
|
|
|
class Member(DictModelBase):
|
|
DICT_ATTRS = ('id', 'tenant_id', 'admin_state_up', 'address',
|
|
'protocol_port', 'weight', 'subnet')
|
|
|
|
def __init__(self, id_, tenant_id, admin_state_up, address, protocol_port,
|
|
weight, subnet=None):
|
|
self.id = id_
|
|
self.tenant_id = tenant_id
|
|
self.admin_state_up = admin_state_up
|
|
self.address = netaddr.IPAddress(address)
|
|
self.protocol_port = protocol_port
|
|
self.weight = weight
|
|
self.subnet = subnet
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
return cls(
|
|
d['id'],
|
|
d['tenant_id'],
|
|
d['admin_state_up'],
|
|
d['address'],
|
|
d['protocol_port'],
|
|
d['weight'],
|
|
)
|
|
|
|
|
|
class AstaraExtClientWrapper(client.Client):
|
|
"""Add client support for Astara Extensions. """
|
|
|
|
routerstatus_path = '/dhrouterstatus'
|
|
lbstatus_path = '/akloadbalancerstatus'
|
|
|
|
@client.APIParamsCall
|
|
def update_router_status(self, router, status):
|
|
return self.put(
|
|
'%s/%s' % (self.routerstatus_path, router),
|
|
body={'routerstatus': {'status': status}}
|
|
)
|
|
|
|
@client.APIParamsCall
|
|
def update_loadbalancer_status(self, load_balancer, status):
|
|
return self.put(
|
|
'%s/%s' % (self.lbstatus_path, load_balancer),
|
|
# XXX We should be differentiating between these 2 states
|
|
body={
|
|
'loadbalancerstatus': {
|
|
'provisioning_status': status,
|
|
'operating_status': status,
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
class L3PluginApi(object):
|
|
|
|
"""Agent side of the Qunatum l3 agent RPC API."""
|
|
|
|
BASE_RPC_API_VERSION = '1.0'
|
|
|
|
def __init__(self, topic, host):
|
|
self.host = host
|
|
self._client = rpc.get_rpc_client(
|
|
topic=topic,
|
|
exchange=cfg.CONF.neutron_control_exchange,
|
|
version=self.BASE_RPC_API_VERSION)
|
|
|
|
def get_routers(self, router_id=None):
|
|
"""Make a remote process call to retrieve the sync data for routers."""
|
|
router_id = [router_id] if router_id else None
|
|
# yes the plural is intended for havana compliance
|
|
retval = self._client.call(
|
|
context.get_admin_context().to_dict(),
|
|
'sync_routers', host=self.host, router_ids=router_id) # plural
|
|
return retval
|
|
|
|
|
|
class Neutron(object):
|
|
def __init__(self, conf):
|
|
self.conf = conf
|
|
ks_session = keystone.KeystoneSession()
|
|
self.api_client = AstaraExtClientWrapper(
|
|
session=ks_session.session,
|
|
)
|
|
self.l3_rpc_client = L3PluginApi(PLUGIN_ROUTER_RPC_TOPIC,
|
|
cfg.CONF.host)
|
|
|
|
def update_loadbalancer_status(self, loadbalancer_id, status):
|
|
try:
|
|
self.api_client.update_loadbalancer_status(loadbalancer_id, status)
|
|
except Exception as e:
|
|
# We don't want to die just because we can't tell neutron
|
|
# what the status of the router should be. Log the error
|
|
# but otherwise ignore it.
|
|
LOG.info(_LI(
|
|
'ignoring failure to update status for %s to %s: %s'),
|
|
id, status, e,
|
|
)
|
|
|
|
def get_loadbalancers(self, tenant_id=None):
|
|
if tenant_id:
|
|
res = self.api_client.list_loadbalancers(tenant_id=tenant_id)
|
|
else:
|
|
res = self.api_client.list_loadbalancers()
|
|
return [
|
|
LoadBalancer.from_dict(lb_data) for lb_data in
|
|
res.get('loadbalancers', [])
|
|
]
|
|
|
|
def get_loadbalancer_detail(self, lb_id):
|
|
try:
|
|
lb_data = self.api_client.show_loadbalancer(lb_id)['loadbalancer']
|
|
except neutron_exc.NotFound:
|
|
raise LoadBalancerGone(
|
|
'No load balancer with id %s found.' % lb_id)
|
|
|
|
lb = LoadBalancer.from_dict(lb_data)
|
|
|
|
lb.vip_port = Port.from_dict(
|
|
self.api_client.show_port(lb_data['vip_port_id'])['port']
|
|
)
|
|
lb.vip_address = lb_data.get('vip_address')
|
|
lb.listeners = [
|
|
self.get_listener_detail(l['id']) for l in lb_data['listeners']
|
|
]
|
|
|
|
return lb
|
|
|
|
def get_listener_detail(self, listener_id):
|
|
data = self.api_client.show_listener(listener_id)['listener']
|
|
listener = Listener.from_dict(data)
|
|
if data.get('default_pool_id'):
|
|
listener.default_pool = self.\
|
|
get_pool_detail(data['default_pool_id'])
|
|
return listener
|
|
|
|
def get_pool_detail(self, pool_id):
|
|
data = self.api_client.show_lbaas_pool(pool_id)['pool']
|
|
pool = Pool.from_dict(data)
|
|
if data.get('members'):
|
|
pool.members = [self.get_member_detail(pool_id, m['id'])
|
|
for m in data['members']]
|
|
return pool
|
|
|
|
def get_loadbalancer_by_listener(self, listener_id, tenant_id=None):
|
|
for lb in self.get_loadbalancers(tenant_id):
|
|
lbd = self.get_loadbalancer_detail(lb.id)
|
|
if listener_id in [l.id for l in lbd.listeners]:
|
|
return lbd
|
|
|
|
def get_loadbalancer_by_member(self, member_id, tenant_id=None):
|
|
for lb in self.get_loadbalancers(tenant_id):
|
|
lbd = self.get_loadbalancer_detail(lb.id)
|
|
for listener in lbd.listeners:
|
|
pd = self.get_pool_detail(listener.default_pool.id)
|
|
if member_id in [m.id for m in pd.members]:
|
|
return lbd
|
|
|
|
def get_member_detail(self, pool_id, member_id):
|
|
data = self.api_client.show_lbaas_member(member_id, pool_id)['member']
|
|
member = Member.from_dict(data)
|
|
return member
|
|
|
|
def get_routers(self, detailed=True):
|
|
"""Return a list of routers."""
|
|
if detailed:
|
|
return [Router.from_dict(r) for r in
|
|
self.l3_rpc_client.get_routers()]
|
|
|
|
routers = self.api_client.list_routers().get('routers', [])
|
|
return [Router.from_dict(r) for r in routers]
|
|
|
|
def get_router_detail(self, router_id):
|
|
"""Return detailed information about a router and it's networks."""
|
|
router = self.l3_rpc_client.get_routers(router_id=router_id)
|
|
try:
|
|
return Router.from_dict(router[0])
|
|
except IndexError:
|
|
raise RouterGone(_('the router is no longer available'))
|
|
|
|
def get_router_for_tenant(self, tenant_id):
|
|
response = self.api_client.list_routers(tenant_id=tenant_id)
|
|
routers = response.get('routers', [])
|
|
|
|
if routers:
|
|
return self.get_router_detail(routers[0]['id'])
|
|
else:
|
|
LOG.debug('found no router for tenant %s', tenant_id)
|
|
LOG.debug('query response: %r', response)
|
|
return None
|
|
|
|
def get_network_ports(self, network_id):
|
|
return [Port.from_dict(p) for p in
|
|
self.api_client.list_ports(network_id=network_id)['ports']]
|
|
|
|
def get_network_subnets(self, network_id):
|
|
response = []
|
|
subnet_response = self.api_client.list_subnets(network_id=network_id)
|
|
subnets = subnet_response['subnets']
|
|
for s in subnets:
|
|
try:
|
|
response.append(Subnet.from_dict(s))
|
|
except Exception as e:
|
|
LOG.info(_LI('ignoring subnet %s (%s) on network %s: %s'),
|
|
s.get('id'), s.get('cidr'),
|
|
network_id, e)
|
|
return response
|
|
|
|
def get_network_detail(self, network_id):
|
|
network_response = self.api_client.show_network(network_id)['network']
|
|
network = Network.from_dict(network_response)
|
|
network.subnets = self.get_network_subnets(network_id)
|
|
|
|
return network
|
|
|
|
def get_ports_for_instance(self, instance_id):
|
|
ports = self.api_client.list_ports(device_id=instance_id)['ports']
|
|
|
|
mgt_port = None
|
|
intf_ports = []
|
|
|
|
for port in (Port.from_dict(p) for p in ports):
|
|
if port.network_id == self.conf.management_network_id:
|
|
mgt_port = port
|
|
else:
|
|
intf_ports.append(port)
|
|
return mgt_port, intf_ports
|
|
|
|
def create_management_port(self, object_id):
|
|
return self.create_vrrp_port(
|
|
object_id,
|
|
self.conf.management_network_id,
|
|
'MGT'
|
|
)
|
|
|
|
def create_vrrp_port(self, object_id, network_id, label='VRRP'):
|
|
port_dict = dict(
|
|
admin_state_up=True,
|
|
network_id=network_id,
|
|
name='ASTARA:%s:%s' % (label, object_id),
|
|
security_groups=[]
|
|
)
|
|
|
|
if label in ['VRRP', 'LB']:
|
|
port_dict['fixed_ips'] = []
|
|
# disable port_securty on VRRP
|
|
if self.conf.neutron_port_security_extension_enabled:
|
|
port_dict['port_security_enabled'] = False
|
|
|
|
response = self.api_client.create_port(dict(port=port_dict))
|
|
port_data = response.get('port')
|
|
if not port_data:
|
|
raise ValueError(_(
|
|
'Unable to create %s port for %s on network %s') %
|
|
(label, object_id, network_id)
|
|
)
|
|
port = Port.from_dict(port_data)
|
|
|
|
return port
|
|
|
|
def delete_vrrp_port(self, object_id, label='VRRP'):
|
|
name = 'ASTARA:%s:%s' % (label, object_id)
|
|
response = self.api_client.list_ports(name=name)
|
|
port_data = response.get('ports')
|
|
|
|
if not port_data and self.conf.legacy_fallback_mode:
|
|
name = name.replace('ASTARA', 'AKANDA')
|
|
LOG.info(_LI('Attempting legacy query for %s.'), name)
|
|
response = self.api_client.list_ports(name=name)
|
|
port_data = response.get('ports')
|
|
|
|
if not port_data:
|
|
LOG.warning(_LW(
|
|
'Unable to find VRRP port to delete with name %s.'), name)
|
|
for port in port_data:
|
|
self.api_client.delete_port(port['id'])
|
|
|
|
def create_router_external_port(self, router):
|
|
# FIXME: Need to make this smarter in case the switch is full.
|
|
network_args = {'network_id': self.conf.external_network_id}
|
|
update_args = {
|
|
'name': router.name,
|
|
'admin_state_up': router.admin_state_up,
|
|
'external_gateway_info': network_args
|
|
}
|
|
|
|
self.api_client.update_router(
|
|
router.id,
|
|
body=dict(router=update_args)
|
|
)
|
|
new_port = self.get_router_external_port(router)
|
|
|
|
# Make sure the port has enough IPs.
|
|
subnets = self.get_network_subnets(self.conf.external_network_id)
|
|
sn_by_id = {
|
|
sn.id: sn
|
|
for sn in subnets
|
|
}
|
|
sn_by_version = collections.defaultdict(list)
|
|
for sn in subnets:
|
|
sn_by_version[sn.ip_version].append(sn)
|
|
versions_needed = set(sn_by_version.keys())
|
|
found = set(sn_by_id[fip.subnet_id].ip_version
|
|
for fip in new_port.fixed_ips)
|
|
if found != versions_needed:
|
|
missing_versions = list(sorted(versions_needed - found))
|
|
raise MissingIPAllocation(
|
|
new_port.id,
|
|
[(mv, [sn.id for sn in sn_by_version[mv]])
|
|
for mv in missing_versions]
|
|
)
|
|
return new_port
|
|
|
|
def get_router_external_port(self, router):
|
|
for i in six.moves.range(self.conf.max_retries):
|
|
LOG.debug(
|
|
'Looking for router external port. Attempt %d of %d',
|
|
i,
|
|
cfg.CONF.max_retries,
|
|
)
|
|
query_dict = {
|
|
'device_owner': DEVICE_OWNER_ROUTER_GW,
|
|
'device_id': router.id,
|
|
'network_id': self.conf.external_network_id
|
|
}
|
|
ports = self.api_client.list_ports(**query_dict)['ports']
|
|
|
|
if len(ports):
|
|
port = Port.from_dict(ports[0])
|
|
LOG.debug('Found router external port: %s', port.id)
|
|
return port
|
|
time.sleep(self.conf.retry_delay)
|
|
raise RouterGatewayMissing()
|
|
|
|
def _ensure_local_port(self, network_id, subnet_id, prefix,
|
|
network_type):
|
|
driver = importutils.import_object(self.conf.interface_driver,
|
|
self.conf)
|
|
|
|
host_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname()))
|
|
|
|
name = 'ASTARA:RUG:%s' % network_type.upper()
|
|
|
|
query_dict = dict(device_owner=DEVICE_OWNER_RUG,
|
|
device_id=host_id,
|
|
name=name,
|
|
network_id=network_id)
|
|
|
|
ports = self.api_client.list_ports(**query_dict)['ports']
|
|
|
|
if not ports and self.conf.legacy_fallback_mode:
|
|
LOG.info(_LI('Attempting legacy query for %s.'), name)
|
|
query_dict.update({
|
|
'name': name.replace('ASTARA', 'AKANDA'),
|
|
'device_owner': DEVICE_OWNER_RUG.replace('astara', 'akanda')
|
|
})
|
|
ports = self.api_client.list_ports(**query_dict)['ports']
|
|
|
|
if ports and 'AKANDA' in ports[0]['name']:
|
|
port = Port.from_dict(ports[0])
|
|
LOG.info(
|
|
_LI('migrating port to ASTARA for port %r and using local %s'),
|
|
port,
|
|
network_type
|
|
)
|
|
self.api_client.update_port(
|
|
port.id,
|
|
{
|
|
'port': {
|
|
'name': port.name.replace('AKANDA', 'ASTARA'),
|
|
'device_owner': DEVICE_OWNER_RUG
|
|
}
|
|
}
|
|
)
|
|
elif ports:
|
|
port = Port.from_dict(ports[0])
|
|
LOG.info(_LI('already have local %s port, using %r'),
|
|
network_type, port)
|
|
else:
|
|
LOG.info(_LI('creating a new local %s port'), network_type)
|
|
port_dict = {
|
|
'admin_state_up': True,
|
|
'network_id': network_id,
|
|
'device_owner': DEVICE_OWNER_ROUTER_INT, # lying here for IP
|
|
'name': name,
|
|
'device_id': host_id,
|
|
'fixed_ips': [{
|
|
'subnet_id': subnet_id
|
|
}],
|
|
'binding:host_id': socket.gethostname()
|
|
}
|
|
port = Port.from_dict(
|
|
self.api_client.create_port(dict(port=port_dict))['port'])
|
|
|
|
# remove lie that enabled us pick IP on slaac subnet
|
|
self.api_client.update_port(
|
|
port.id,
|
|
{'port': {'device_owner': DEVICE_OWNER_RUG}}
|
|
)
|
|
port.device_owner = DEVICE_OWNER_RUG
|
|
|
|
LOG.info(_LI('new local %s port: %r'), network_type, port)
|
|
|
|
# create the tap interface if it doesn't already exist
|
|
if not ip_lib.device_exists(driver.get_device_name(port)):
|
|
driver.plug(
|
|
port.network_id,
|
|
port.id,
|
|
driver.get_device_name(port),
|
|
port.mac_address)
|
|
|
|
# add sleep to ensure that port is setup before use
|
|
time.sleep(1)
|
|
|
|
try:
|
|
fixed_ip = [fip for fip in port.fixed_ips
|
|
if fip.subnet_id == subnet_id][0]
|
|
except IndexError:
|
|
raise MissingIPAllocation(port.id)
|
|
|
|
ip_cidr = '%s/%s' % (fixed_ip.ip_address, prefix.split('/')[1])
|
|
driver.init_l3(driver.get_device_name(port), [ip_cidr])
|
|
return ip_cidr
|
|
|
|
def ensure_local_external_port(self):
|
|
return self._ensure_local_port(
|
|
self.conf.external_network_id,
|
|
self.conf.external_subnet_id,
|
|
self.conf.external_prefix,
|
|
'external')
|
|
|
|
def ensure_local_service_port(self):
|
|
return self._ensure_local_port(
|
|
self.conf.management_network_id,
|
|
self.conf.management_subnet_id,
|
|
self.conf.management_prefix,
|
|
'service')
|
|
|
|
def purge_management_interface(self):
|
|
driver = importutils.import_object(
|
|
self.conf.interface_driver,
|
|
self.conf
|
|
)
|
|
host_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname()))
|
|
query_dict = dict(
|
|
device_owner=DEVICE_OWNER_RUG,
|
|
name='ASTARA:RUG:MANAGEMENT',
|
|
device_id=host_id
|
|
)
|
|
ports = self.api_client.list_ports(**query_dict)['ports']
|
|
|
|
if not ports and self.conf.legacy_fallback_mode:
|
|
query_dict.update({
|
|
'name': 'AKANDA:RUG:MANAGEMENT',
|
|
'device_owner': DEVICE_OWNER_RUG.replace('astara', 'akanda')
|
|
})
|
|
ports = self.api_client.list_ports(**query_dict)['ports']
|
|
|
|
if ports:
|
|
port = Port.from_dict(ports[0])
|
|
device_name = driver.get_device_name(port)
|
|
driver.unplug(device_name)
|
|
|
|
def update_router_status(self, router_id, status):
|
|
try:
|
|
self.api_client.update_router_status(router_id, status)
|
|
except Exception as e:
|
|
# We don't want to die just because we can't tell neutron
|
|
# what the status of the router should be. Log the error
|
|
# but otherwise ignore it.
|
|
LOG.info(_LI(
|
|
'ignoring failure to update status for %s to %s: %s'),
|
|
id, status, e,
|
|
)
|
|
|
|
def clear_device_id(self, port):
|
|
self.api_client.update_port(port.id, {'port': {'device_id': ''}})
|