Retire astara repo

Retire repository, following
https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project

Change-Id: If5f7d284bd107a93edd9272ac0ed8e6d20ba5c51
This commit is contained in:
Andreas Jaeger 2018-10-14 12:50:35 +02:00
parent e7f8940fa0
commit 7759e2fd82
204 changed files with 10 additions and 27927 deletions

53
.gitignore vendored
View File

@ -1,53 +0,0 @@
*.py[co]
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
# Packaging output
*.deb
# pbr output
AUTHORS
ChangeLog
orchestrator.ini.sample
astara/test/functional/test.conf.sample
*.swp
#pycharm cruft
.idea/*
*.db
*.db_clean
#macos hidden files
.DS_Store
._.DS_Store
# Vagrant
vagrant/.vagrant
vagrant/user_local.conf

View File

@ -1,10 +0,0 @@
language: python
python:
- "2.7"
install:
- pip install -r test_requirements.txt --use-mirrors
- pip install flake8 --use-mirrors
- pip install -q . --use-mirrors
before_script:
- flake8 setup.py akanda --ignore=E123,E133,E226,E241,E242,E731
script: nosetests -d

175
LICENSE
View File

@ -1,175 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

View File

@ -1,70 +0,0 @@
# Astara
A service with an open plugin architecture that manages Neutron advanced
services such as routers and load balancers within an OpenStack environment.
## The Name
Astara is the sanskrit word for carpet. So why name our project carpet?
The original code name for this project was simply "The RUG" which was a
reference to a line from the popular film "The Big Lebowski":
**That rug really tied the room together, did it not?**
The idea is that "The Rug" really ties OpenStack neutron together nicely. We
felt it was an apt description so we kept the name.
## Related Projects
The code for the Astara project lives in several separate repositories to ease
packaging and management:
* [Astara](https://github.com/openstack/astara) -
Contains the Orchestration service for managing the creation, configuration,
and health of neutron advanced services as virtual network functions.
* [Astara Appliance](https://github.com/openstack/astara-appliance)
Supporting software for the Astara virtual network appliance, which is
a Linux-based service VM that provides routing and L3+ services in
a virtualized network environment. This includes a REST API for managing
the appliance via the Astara orchestration service.
* [Astara Neutron](https://github.com/openstack/astara-neutron)  
Ancillary subclasses of several OpenStack Neutron plugins and supporting
code.
* [Astara Horizon](https://github.com/openstack/astara-horizon) -
OpenStack Horizon Dashboard code.
## Project Details
Astara is publicly managed through the [Astara Launchpad project](https://launchpad.net/astara)
## Code Review
The code goes to get reviewed by collaborators and merged at
[OpenStack Gerrit review](https://review.openstack.org)
## Documentation
Can be found at [docs.akanda.io](http://docs.akanda.io)
Developer quick start guides for making this all work in Devstack `Here
<docs/source/developer_quickstart.rst>`_
## Community
Talk to the developers through IRC [#openstack-astara channel on freenode.net]
(http://webchat.freenode.net/?randomnick=1&channels=%23openstack-astara&prompt=1&uio=d4)
## License and Copyright
Astara is licensed under the Apache-2.0 license and is Copyright 2015,
OpenStack Foundation

10
README.rst Normal file
View File

@ -0,0 +1,10 @@
This project is no longer maintained.
The contents of this repository are still available in the Git
source code management system. To see the contents of this
repository before it reached its end of life, please check out the
previous commit with "git checkout HEAD^1".
For any further questions, please email
openstack-dev@lists.openstack.org or join #openstack-dev on
Freenode.

View File

View File

@ -1,33 +0,0 @@
# 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.
from oslo_config import cfg
CONF = cfg.CONF
api_opts = [
cfg.StrOpt('admin_user'),
cfg.StrOpt('admin_password', secret=True),
cfg.StrOpt('admin_tenant_name'),
cfg.StrOpt('auth_url'),
cfg.StrOpt('auth_strategy', default='keystone'),
cfg.StrOpt('auth_region'),
cfg.IntOpt('max_retries', default=3),
cfg.IntOpt('retry_delay', default=1),
cfg.StrOpt('endpoint_type', default='publicURL'),
]
CONF.register_opts(api_opts)

View File

@ -1,91 +0,0 @@
# 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 requests
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
ASTARA_MGT_SERVICE_PORT = 5000
ASTARA_BASE_PATH = '/v1/'
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
AK_CLIENT_OPTS = [
cfg.IntOpt('alive_timeout', default=3),
cfg.IntOpt('config_timeout', default=90),
]
CONF.register_opts(AK_CLIENT_OPTS)
def _mgt_url(host, port, path):
if ':' in host:
host = '[%s]' % host
return 'http://%s:%s%s' % (host, port, path)
def _get_proxyless_session():
s = requests.Session()
# ignore any proxy setting because we should have a direct connection
s.trust_env = False
return s
def is_alive(host, port, timeout=None):
timeout = timeout or cfg.CONF.alive_timeout
path = ASTARA_BASE_PATH + 'firewall/rules'
try:
s = _get_proxyless_session()
r = s.get(_mgt_url(host, port, path), timeout=timeout)
if r.status_code == 200:
return True
except Exception as e:
LOG.debug('is_alive for %s failed: %s', host, str(e))
return False
def get_interfaces(host, port):
path = ASTARA_BASE_PATH + 'system/interfaces'
s = _get_proxyless_session()
r = s.get(_mgt_url(host, port, path), timeout=30)
return r.json().get('interfaces', [])
def update_config(host, port, config_dict):
path = ASTARA_BASE_PATH + 'system/config'
headers = {'Content-type': 'application/json'}
s = _get_proxyless_session()
r = s.put(
_mgt_url(host, port, path),
data=jsonutils.dump_as_bytes(config_dict),
headers=headers,
timeout=cfg.CONF.config_timeout)
if r.status_code != 200:
raise Exception('Config update failed: %s' % r.text)
else:
return r.json()
def read_labels(host, port):
path = ASTARA_BASE_PATH + 'firewall/labels'
s = _get_proxyless_session()
r = s.post(_mgt_url(host, port, path), timeout=30)
return r.json().get('labels', [])

View File

@ -1,105 +0,0 @@
# 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 re
from astara.common import constants
SERVICE_STATIC = 'static'
def network_config(client, port, ifname, network_type, network_ports=[]):
network = client.get_network_detail(port.network_id)
subnets_dict = dict((s.id, s) for s in network.subnets)
return _make_network_config_dict(
_interface_config(ifname, port, subnets_dict, network.mtu),
network_type,
port.network_id,
mtu=network.mtu,
subnets_dict=subnets_dict,
network_ports=network_ports)
def _make_network_config_dict(interface, network_type, network_id, mtu=None,
v4_conf=SERVICE_STATIC, v6_conf=SERVICE_STATIC,
subnets_dict={}, network_ports=[]):
return {'interface': interface,
'network_id': network_id,
'mtu': mtu,
'v4_conf_service': v4_conf,
'v6_conf_service': v6_conf,
'network_type': network_type,
'subnets': [_subnet_config(s) for s in subnets_dict.values()],
'allocations': _allocation_config(network_ports, subnets_dict)}
def _interface_config(ifname, port, subnets_dict, mtu):
def fmt(fixed):
return '%s/%s' % (fixed.ip_address,
subnets_dict[fixed.subnet_id].cidr.prefixlen)
retval = {'ifname': ifname,
'addresses': [fmt(fixed) for fixed in port.fixed_ips]}
if mtu:
retval['mtu'] = mtu
return retval
def _subnet_config(subnet):
return {
'id': str(subnet.id),
'cidr': str(subnet.cidr),
'dhcp_enabled': subnet.enable_dhcp and subnet.ipv6_ra_mode != 'slaac',
'dns_nameservers': subnet.dns_nameservers,
'host_routes': subnet.host_routes,
'gateway_ip': (str(subnet.gateway_ip)
if subnet.gateway_ip is not None
else ''),
}
def _allocation_config(ports, subnets_dict):
r = re.compile('[:.]')
service_ports_re = re.compile(
'^ASTARA:(' + '|'.join(constants.ASTARA_SERVICE_PORT_TYPES) + '):.*$'
)
allocations = []
for port in ports:
if service_ports_re.match(port.name):
continue
addrs = {
str(fixed.ip_address): subnets_dict[fixed.subnet_id].enable_dhcp
for fixed in port.fixed_ips
}
if not addrs:
continue
allocations.append(
{
'ip_addresses': addrs,
'device_id': port.device_id,
'hostname': '%s.local' % r.sub('-', sorted(addrs.keys())[0]),
'mac_address': port.mac_address
}
)
return allocations

View File

@ -1,49 +0,0 @@
# 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.
from oslo_log import log as logging
from astara.api.config import common
LOG = logging.getLogger(__name__)
def build_config(client, loadbalancer, management_port, iface_map):
LOG.debug('Generating configuration for loadbalancer %s', loadbalancer.id)
network_config = [
common.network_config(
client,
loadbalancer.vip_port,
iface_map[loadbalancer.vip_port.network_id],
'loadbalancer'),
common.network_config(
client,
management_port,
iface_map[management_port.network_id],
'management'),
]
out = {
'hostname': 'ak-loadbalancer-%s' % loadbalancer.tenant_id,
'tenant_id': loadbalancer.tenant_id,
'networks': network_config,
'services': {
'loadbalancer': loadbalancer.to_dict()
}
}
return out

View File

@ -1,171 +0,0 @@
# 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 netaddr
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from astara.common.i18n import _LI, _LW
from astara.api.config import common
LOG = logging.getLogger(__name__)
DEFAULT_AS = 64512
OPTIONS = [
cfg.StrOpt('provider_rules_path',
default='/etc/astara/provider_rules.json'),
cfg.IntOpt('asn', default=DEFAULT_AS),
cfg.IntOpt('neighbor_asn', default=DEFAULT_AS),
]
cfg.CONF.register_opts(OPTIONS)
EXTERNAL_NET = 'external'
INTERNAL_NET = 'internal'
MANAGEMENT_NET = 'management'
SERVICE_STATIC = 'static'
SERVICE_DHCP = 'dhcp'
SERVICE_RA = 'ra'
def build_config(worker_context, router, management_port, interfaces):
provider_rules = load_provider_rules(cfg.CONF.provider_rules_path)
networks = generate_network_config(
worker_context.neutron,
router,
management_port,
interfaces
)
gateway = get_default_v4_gateway(
worker_context.neutron, router, networks)
return {
'asn': cfg.CONF.asn,
'neighbor_asn': cfg.CONF.neighbor_asn,
'default_v4_gateway': gateway,
'networks': networks,
'labels': provider_rules.get('labels', {}),
'floating_ips': generate_floating_config(router),
'tenant_id': router.tenant_id,
'hostname': 'ak-%s' % router.tenant_id,
'orchestrator': worker_context.config,
'ha_resource': router.ha,
'vpn': generate_vpn_config(router, worker_context.neutron),
}
def get_default_v4_gateway(client, router, networks):
"""Find the IPv4 default gateway for the router.
"""
LOG.debug('networks = %r', networks)
if router.external_port:
LOG.debug('external interface = %s', router.external_port.mac_address)
# Now find the subnet that our external IP is on, and return its
# gateway.
for n in networks:
if n['network_type'] == EXTERNAL_NET:
v4_addresses = [
addr
for addr in (netaddr.IPAddress(ip.partition('/')[0])
for ip in n['interface']['addresses'])
if addr.version == 4
]
for s in n['subnets']:
subnet = netaddr.IPNetwork(s['cidr'])
if subnet.version != 4:
continue
LOG.debug(
'%s: checking if subnet %s should have the default route',
router.id, s['cidr'])
for addr in v4_addresses:
if addr in subnet:
LOG.debug(
'%s: found gateway %s for subnet %s on network %s',
router.id,
s['gateway_ip'],
s['cidr'],
n['network_id'],
)
return s['gateway_ip']
# Sometimes we are asked to build a configuration for the server
# when the external interface is still marked as "down". We can
# report that case, but we don't treat it as an error here because
# we'll be asked to do it again when the interface comes up.
LOG.info(_LI('%s: no default gateway was found'), router.id)
return ''
def load_provider_rules(path):
try:
return jsonutils.load(open(path))
except: # pragma nocover
LOG.warning(_LW('unable to open provider rules: %s'), path)
return {}
def generate_network_config(client, router, management_port, iface_map):
retval = [
common.network_config(
client,
management_port,
iface_map[management_port.network_id],
MANAGEMENT_NET
)
]
if router.external_port:
retval.extend([
common.network_config(
client,
router.external_port,
iface_map[router.external_port.network_id],
EXTERNAL_NET)])
retval.extend(
common.network_config(
client,
p,
iface_map[p.network_id],
INTERNAL_NET,
client.get_network_ports(p.network_id))
for p in router.internal_ports)
return retval
def generate_floating_config(router):
return [
{'floating_ip': str(fip.floating_ip), 'fixed_ip': str(fip.fixed_ip)}
for fip in router.floating_ips
]
def generate_vpn_config(router, client):
if not cfg.CONF.router.ipsec_vpn:
return {}
return {
'ipsec': [
v.to_dict() for v in client.get_vpnservices_for_router(router.id)
]
}

View File

@ -1,38 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
from keystoneclient import auth as ksauth
from keystoneclient import session as kssession
from oslo_config import cfg
CONF = cfg.CONF
class KeystoneSession(object):
def __init__(self):
self._session = None
self.region_name = CONF.auth_region
ksauth.register_conf_options(CONF, 'keystone_authtoken')
@property
def session(self):
if not self._session:
# Construct a Keystone session for configured auth_plugin
# and credentials
auth_plugin = ksauth.load_from_conf_options(
cfg.CONF, 'keystone_authtoken')
self._session = kssession.Session(auth=auth_plugin)
return self._session

File diff suppressed because it is too large Load Diff

View File

@ -1,453 +0,0 @@
# 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.
from datetime import datetime
import time
import netaddr
from novaclient import client
from novaclient import exceptions as novaclient_exceptions
from oslo_config import cfg
from oslo_log import log as logging
from astara.common.i18n import _LW, _LE, _LI
from astara.api import keystone
from astara.api import neutron
from astara.common import config
from astara.pez import rpcapi as pez_api
LOG = logging.getLogger(__name__)
OPTIONS = [
cfg.StrOpt(
'ssh_public_key',
help="Path to the SSH public key for the 'astara' user within "
"appliance instances",
default='/etc/astara/astara.pub'),
cfg.StrOpt(
'instance_provider', default='on_demand',
help='Which instance provider to use (on_demand, pez)'),
cfg.StrOpt(
'astara_boot_command', default='astara-configure-management',
help='The boot command to run to configure the appliance'),
]
cfg.CONF.register_opts(OPTIONS)
class NovaInstanceDeleteTimeout(Exception):
pass
class InstanceInfo(object):
def __init__(self, instance_id, name, management_port=None, ports=(),
image_uuid=None, status=None, last_boot=None):
self.id_ = instance_id
self.name = name
self.image_uuid = image_uuid
self.nova_status = status
self.management_port = management_port
self._ports = ports
self.last_boot = last_boot
@property
def booting(self):
return 'BUILD' in self.nova_status
@property
def management_address(self):
if self.management_port:
return str(self.management_port.fixed_ips[0].ip_address)
@property
def time_since_boot(self):
if self.last_boot:
return datetime.utcnow() - self.last_boot
@property
def ports(self):
return self._ports
@ports.setter
def ports(self, port_list):
self._ports = [p for p in port_list if p != self.management_port]
@classmethod
def from_nova(cls, instance):
"""
Returns an instantiated InstanceInfo object with data gathered from
an existing Nova server.
:param instance: novaclient.v2.servers.Server object for an existing
nova instance.
:returns: InstanceInfo instance
"""
# NOTE(adam_g): We do not yet actually rebuild any instances.
# A rug REBUILD is actually a delete/create, so it
# should be safe to track last_boot as the timestamp
# the instance was last booted.
last_boot = datetime.strptime(
instance.created, "%Y-%m-%dT%H:%M:%SZ")
return cls(
instance_id=instance.id,
name=instance.name,
image_uuid=instance.image['id'],
status=instance.status,
last_boot=last_boot,
)
class InstanceProvider(object):
def __init__(self, client):
self.nova_client = client
LOG.debug(_LI(
'Initialized %s with novaclient %s'),
self.__class__.__name__, self.nova_client)
def create_instance(self, driver, name, image_uuid, flavor,
make_ports_callback):
"""Create or get an instance
:param router_id: UUID of the resource that the instance will host
:returns: InstanceInfo object with at least id, name and image_uuid
set.
"""
class PezInstanceProvider(InstanceProvider):
def __init__(self, client):
super(PezInstanceProvider, self).__init__(client)
self.rpc_client = pez_api.AstaraPezAPI(rpc_topic='astara-pez')
LOG.debug(_LI(
'Initialized %s with rpc client %s'),
self.__class__.__name__, self.rpc_client)
def create_instance(self, resource_type, name, image_uuid, flavor,
make_ports_callback):
# TODO(adam_g): pez already creates the mgt port on boot and the one
# we create here is wasted. callback needs to be adjusted
mgt_port, instance_ports = make_ports_callback()
mgt_port_dict = {
'id': mgt_port.id,
'network_id': mgt_port.network_id,
}
instance_ports_dicts = [{
'id': p.id, 'network_id': p.network_id,
} for p in instance_ports]
LOG.debug('Requesting new %s instance from Pez.', resource_type)
pez_instance = self.rpc_client.get_instance(
resource_type, name, mgt_port_dict, instance_ports_dicts)
LOG.debug('Got %s instance %s from Pez.',
resource_type, pez_instance['id'])
server = self.nova_client.servers.get(pez_instance['id'])
# deserialize port data
mgt_port = neutron.Port.from_dict(pez_instance['management_port'])
instance_ports = [
neutron.Port.from_dict(p)
for p in pez_instance['instance_ports']]
boot_time = datetime.strptime(
server.created, "%Y-%m-%dT%H:%M:%SZ")
instance_info = InstanceInfo(
instance_id=server.id,
name=server.name,
management_port=mgt_port,
ports=instance_ports,
image_uuid=image_uuid,
status=server.status,
last_boot=boot_time)
return instance_info
class OnDemandInstanceProvider(InstanceProvider):
def create_instance(self, resource_type, name, image_uuid, flavor,
make_ports_callback):
mgt_port, instance_ports = make_ports_callback()
nics = [{'net-id': p.network_id,
'v4-fixed-ip': '',
'port-id': p.id}
for p in ([mgt_port] + instance_ports)]
LOG.debug('creating instance %s with image %s',
name, image_uuid)
server = self.nova_client.servers.create(
name,
image=image_uuid,
flavor=flavor,
nics=nics,
config_drive=True,
userdata=format_userdata(mgt_port)
)
server_status = None
for i in range(1, 10):
try:
# novaclient loads attributes lazily and we need to wait until
# the client object is populated. moving to keystone sessions
# exposes this race.
server_status = server.status
except AttributeError:
time.sleep(.5)
assert server_status
boot_time = datetime.strptime(
server.created, "%Y-%m-%dT%H:%M:%SZ")
instance_info = InstanceInfo(
instance_id=server.id,
name=name,
management_port=mgt_port,
ports=instance_ports,
image_uuid=image_uuid,
status=server.status,
last_boot=boot_time)
return instance_info
INSTANCE_PROVIDERS = {
'on_demand': OnDemandInstanceProvider,
'pez': PezInstanceProvider,
'default': OnDemandInstanceProvider,
}
def get_instance_provider(provider):
try:
return INSTANCE_PROVIDERS[provider]
except KeyError:
default = INSTANCE_PROVIDERS['default']
LOG.error(_LE('Could not find %s instance provider, using default %s'),
provider, default)
return default
class Nova(object):
def __init__(self, conf):
self.conf = conf
ks_session = keystone.KeystoneSession()
self.client = client.Client(
version='2',
session=ks_session.session,
region_name=conf.auth_region,
endpoint_type=conf.endpoint_type)
try:
self.instance_provider = get_instance_provider(
conf.instance_provider)(self.client)
except AttributeError:
default = INSTANCE_PROVIDERS['default']
LOG.error(_LE('Could not find provider config, using default %s'),
default)
self.instance_provider = default(self.client)
def get_instances_for_obj(self, name):
"""Retrieves all nova servers for a given instance name.
:param name: name of the instance being queried
:returns: a list of novaclient.v2.servers.Server objects or []
"""
search_opt = '^' + name + '.*$'
instances = self.client.servers.list(
search_opts=dict(name=search_opt)
)
if not instances:
return []
return [InstanceInfo.from_nova(i) for i in instances]
def get_instance_for_obj(self, name):
"""Retrieves a nova server for a given instance name.
:param name: name of the instance being queried
:returns: a novaclient.v2.servers.Server object or None
"""
instances = self.client.servers.list(
search_opts=dict(name=name)
)
if instances:
return instances[0]
else:
return None
def get_instance_by_id(self, instance_id):
"""Retrieves a nova server for a given instance_id.
:param instance_id: Nova instance ID of instance being queried
:returns: a novaclient.v2.servers.Server object
"""
try:
return self.client.servers.get(instance_id)
except novaclient_exceptions.NotFound:
return None
def destroy_instance(self, instance_info):
if instance_info:
LOG.debug('deleting instance %s', instance_info.name)
self.client.servers.delete(instance_info.id_)
def boot_instance(self,
resource_type,
prev_instance_info,
name,
image_uuid,
flavor,
make_ports_callback):
if not prev_instance_info:
instance = self.get_instance_for_obj(name)
else:
instance = self.get_instance_by_id(prev_instance_info.id_)
# check to make sure this instance isn't pre-existing
if instance:
if 'BUILD' in instance.status:
if prev_instance_info:
# if we had previous instance, return the same instance
# with updated status
prev_instance_info.nova_status = instance.status
instance_info = prev_instance_info
else:
instance_info = InstanceInfo.from_nova(instance)
return instance_info
self.client.servers.delete(instance.id)
return None
# it is now safe to attempt boot
instance_info = self.instance_provider.create_instance(
resource_type=resource_type,
name=name,
image_uuid=image_uuid,
flavor=flavor,
make_ports_callback=make_ports_callback
)
return instance_info
def update_instance_info(self, instance_info):
"""Used primarily for updating tracked instance status"""
instance = self.get_instance_by_id(instance_info.id_)
if not instance:
return None
instance_info.nova_status = instance.status
return instance_info
def delete_instances_and_wait(self, instance_infos):
"""Deletes the nova instance and waits for its deletion to complete"""
to_poll = list(instance_infos)
for inst in instance_infos:
try:
self.destroy_instance(inst)
except novaclient_exceptions.NotFound:
pass
except Exception:
LOG.exception(
_LE('Error deleting instance %s' % inst.id_))
to_poll.remove(inst)
# XXX parallelize this
timed_out = []
for inst in to_poll:
start = time.time()
i = 0
while time.time() - start < cfg.CONF.boot_timeout:
i += 1
if not self.get_instance_by_id(inst.id_):
LOG.debug('Instance %s has been deleted', inst.id_)
break
LOG.debug(
'Instance %s has not finished stopping', inst.id_)
time.sleep(cfg.CONF.retry_delay)
else:
timed_out.append(inst)
LOG.error(_LE(
'Instance %s failed to stop within %d secs'),
inst.id_, cfg.CONF.boot_timeout)
if timed_out:
raise NovaInstanceDeleteTimeout()
# TODO(mark): Convert this to dynamic yaml, proper network prefix and ssh-keys
TEMPLATE = """#cloud-config
cloud_config_modules:
- emit_upstart
- set_hostname
- locale
- set-passwords
- timezone
- disable-ec2-metadata
- runcmd
output: {all: '| tee -a /var/log/cloud-init-output.log'}
debug:
- verbose: true
bootcmd:
- /usr/local/bin/%(boot_command)s %(mac_address)s %(ip_address)s/%(prefix)d
users:
- name: astara
gecos: Astara
groups: users
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
lock-passwd: true
ssh-authorized-keys:
- %(ssh_public_key)s
final_message: "Astara appliance is running"
""" # noqa
def _ssh_key():
key = config.get_best_config_path(cfg.CONF.ssh_public_key)
if not key:
return ''
try:
with open(key) as out:
return out.read().strip()
except IOError:
LOG.warning(_LW('Could not load router ssh public key from %s'), key)
return ''
def format_userdata(mgt_port):
mgt_net = netaddr.IPNetwork(cfg.CONF.management_prefix)
ctxt = {
'ssh_public_key': _ssh_key(),
'mac_address': mgt_port.mac_address,
'ip_address': mgt_port.fixed_ips[0].ip_address,
'boot_command': cfg.CONF.astara_boot_command,
'prefix': mgt_net.prefixlen
}
out = TEMPLATE % ctxt
LOG.debug('Rendered cloud-init for instance: %s' % out)
return out

View File

@ -1,125 +0,0 @@
# Copyright 2015 Akanda, Inc
#
# Author: Akanda, Inc
#
# 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 socket
import eventlet
import eventlet.wsgi
import webob
import webob.dec
import webob.exc
import six
from oslo_config import cfg
from oslo_log import log as logging
from astara.cli import app
from astara.common.i18n import _, _LE, _LI, _LW
LOG = logging.getLogger(__name__)
RUG_API_OPTS = [
cfg.IntOpt('api_port', default=44250,
help='Astara administrative API listening port',
deprecated_opts=[
cfg.DeprecatedOpt('rug_api_port',
group='DEFAULT')]),
cfg.StrOpt('api_listen', default='0.0.0.0',
help='Astara administrative API listening address')
]
cfg.CONF.register_opts(RUG_API_OPTS)
class RugAPI(object):
def __init__(self, ctl=app.RugController):
self.ctl = ctl()
@webob.dec.wsgify(RequestClass=webob.Request)
def __call__(self, req):
try:
if req.method != 'PUT':
return webob.exc.HTTPMethodNotAllowed()
args = filter(None, req.path.split('/'))
if not args:
return webob.exc.HTTPNotFound()
command, _, _ = self.ctl.command_manager.find_command(args)
if command.interactive:
return webob.exc.HTTPNotImplemented()
return str(self.ctl.run(['--debug'] + args))
except SystemExit:
# cliff invokes -h (help) on argparse failure
# (which in turn results in sys.exit call)
return webob.exc.HTTPBadRequest()
except ValueError:
return webob.exc.HTTPNotFound()
except Exception:
LOG.exception(_LE("Unexpected error."))
msg = _('An unknown error has occurred. '
'Please try your request again.')
return webob.exc.HTTPInternalServerError(
explanation=six.text_type(msg))
class RugAPIServer(object):
def __init__(self):
self.pool = eventlet.GreenPool(1000)
def run(self, ip_address, port):
app = RugAPI()
try:
socket.inet_pton(socket.AF_INET6, ip_address)
family = socket.AF_INET6
except Exception:
family = socket.AF_INET
for i in six.moves.range(5):
LOG.info(_LI(
'Starting the rug-api on %s:%s'),
ip_address, port,
)
try:
sock = eventlet.listen(
(ip_address, port),
family=family,
backlog=128
)
except socket.error as err:
if err.errno != 99: # EADDRNOTAVAIL
raise
LOG.warning(_LW('Could not create rug-api socket: %s'), err)
LOG.warning(_LW('Sleeping %s before trying again'), i + 1)
eventlet.sleep(i + 1)
else:
break
else:
raise RuntimeError(_(
'Could not establish rug-api socket on %s:%s') %
(ip_address, port)
)
eventlet.wsgi.server(
sock,
app,
custom_pool=self.pool,
log=LOG)
def serve():
RugAPIServer().run(cfg.CONF.api_listen, cfg.CONF.api_port)

View File

@ -1,15 +0,0 @@
# 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.

View File

@ -1,49 +0,0 @@
# 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 logging
from cliff import app
from cliff import commandmanager
from oslo_config import cfg
import pkg_resources
from astara.common import config
class RugController(app.App):
log = logging.getLogger(__name__)
def __init__(self):
dist = pkg_resources.get_distribution('astara')
super(RugController, self).__init__(
description='controller for the Astara Orchestrator service',
version=dist.version,
command_manager=commandmanager.CommandManager('astara.cli'),
)
def initialize_app(self, argv):
# Quiet logging for some request library
logging.getLogger('requests').setLevel(logging.WARN)
# Don't pass argv here because cfg.CONF will intercept the
# help options and exit.
cfg.CONF(['--config-file', config.get_best_config_path()],
project='astara-orchestrator')
self.rug_ini = cfg.CONF
return super(RugController, self).initialize_app(argv)

View File

@ -1,377 +0,0 @@
# 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.
"""Interactive CLI for rebuilding routers
"""
from __future__ import print_function
import logging
import os
import Queue
import sqlite3
import tempfile
import threading
import six
from contextlib import closing
from datetime import datetime
try:
from blessed import Terminal
except ImportError:
# blessed is not part of openstack global-requirements.
raise Exception("The 'blessed' python module is required to browse"
" Astara routers. Please install and try again.")
from oslo_config import cfg
from astara import commands
from astara.api import nova as nova_api
from astara.api import neutron as neutron_api
from astara.cli import message
logging.getLogger("urllib3").setLevel(logging.ERROR)
cfg.CONF.import_opt('host', 'astara.main')
class FakeConfig(object):
def __init__(self, admin_user, admin_password, tenant_name, auth_url,
auth_strategy, auth_region, instance_provider):
self.admin_user = admin_user
self.admin_password = admin_password
self.tenant_name = tenant_name
self.admin_tenant_name = tenant_name
self.auth_url = auth_url
self.auth_strategy = auth_strategy
self.auth_region = auth_region
self.instance_provider = instance_provider
class RouterRow(object):
id = None
name = None
status = None
latest = None
image_name = None
booted_at = None
last_fetch = None
nova_status = None
def __init__(self, **kw):
for k, v in kw.items():
setattr(self, k, v)
self.image_name = self.image_name or ''
self.booted_at = self.booted_at or ''
self.nova_status = self.nova_status or ''
self.tenant_id = '*'
if self.name and self.name.startswith('ak-'):
self.tenant_id = self.name.replace('ak-', '')
@classmethod
def from_cursor(cls, cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return cls(**d)
class RouterFetcher(object):
def __init__(self, conf, db, workers):
self.db = db
self.conn = sqlite3.connect(self.db)
self.conn.row_factory = RouterRow.from_cursor
self.nova = nova_api.Nova(conf)
self.neutron = neutron_api.Neutron(conf)
self.nova_queue = Queue.Queue()
self.save_queue = Queue.Queue()
# Create X threads to perform Nova calls and put results into a queue
threads = [
threading.Thread(
name='fetcher-t%02d' % i,
target=self.fetch_router_metadata,
)
for i in six.moves.range(workers)
]
for t in threads:
t.setDaemon(True)
t.start()
def fetch(self):
routers = self.neutron.get_routers(detailed=False)
routers.sort(key=lambda x: x.id)
for router in routers:
sql = ''.join([
"INSERT OR IGNORE INTO routers ",
"('id', 'name', 'latest') VALUES (",
', '.join("?" * 3),
");"
])
with closing(self.conn.cursor()) as cursor:
cursor.execute(
'SELECT * FROM routers WHERE id=?;',
(router.id,)
)
current_router = cursor.fetchone()
if router.status not in ('BUILD', 'ACTIVE') and \
current_router and current_router.status == 'BOOT':
continue
cursor.execute(sql, (router.id, router.name, None))
cursor.execute(
'UPDATE routers SET status=? WHERE id=?',
(router.status, router.id)
)
self.conn.commit()
self.nova_queue.put(router.id)
# SQLite databases have global database-wide lock for writes, so
# we can't split the writes across threads. That's okay, though, the
# slowness isn't the DB writes, it's the Nova API calls
while True:
try:
router, latest, name, booted_at, nova_status = \
self.save_queue.get(False)
with closing(self.conn.cursor()) as cursor:
cursor.execute(
'UPDATE routers SET latest=?, image_name=?, '
'last_fetch=?, booted_at=? WHERE id=?',
(latest, name, datetime.utcnow(), booted_at, router)
)
if nova_status == 'BUILD':
cursor.execute(
'UPDATE routers SET status=? WHERE id=?',
('BOOT', router)
)
self.conn.commit()
self.save_queue.task_done()
except Queue.Empty:
# the queue *might* be empty, and that's okay
break
def fetch_router_metadata(self):
conn = sqlite3.connect(self.db)
conn.row_factory = RouterRow.from_cursor
while True:
router = RouterRow(id=self.nova_queue.get())
image = None
try:
instance = self.nova.get_instance(router)
image = self.nova.client.images.get(instance.image['id'])
except:
pass
if image:
self.save_queue.put((
router.id,
image.id == cfg.CONF.router_image_uuid,
image.name,
instance.created,
instance.status
))
else:
self.save_queue.put((
router.id,
None,
None,
None,
None
))
self.nova_queue.task_done()
def populate_routers(db, conf, workers):
conf = FakeConfig(*conf)
client = RouterFetcher(conf, db, workers)
while True:
try:
client.fetch()
except (KeyboardInterrupt, SystemExit):
print("Killing background worker...")
break
class BrowseRouters(message.MessageSending):
"""browse the state of every Astara appliance"""
log = logging.getLogger(__name__)
interactive = True
SCHEMA = '''CREATE TABLE routers (
id TEXT PRIMARY KEY,
name TEXT,
status TEXT,
latest INTEGER,
image_name TEXT,
last_fetch TIMESTAMP,
booted_at TIMESTAMP
);'''
def __init__(self, *a, **kw):
self.term = Terminal()
self.position = 0
self.routers = []
super(BrowseRouters, self).__init__(*a, **kw)
def init_database(self):
self.fh = tempfile.NamedTemporaryFile(delete=False)
self.conn = sqlite3.connect(self.fh.name)
self.conn.row_factory = RouterRow.from_cursor
with closing(self.conn.cursor()) as cursor:
cursor.execute(self.SCHEMA)
def get_parser(self, prog_name):
parser = super(BrowseRouters, self).get_parser(prog_name)
parser.add_argument('--dump', dest='interactive', action='store_false')
parser.add_argument('--threads', type=int, default=16)
parser.set_defaults(interactive=True)
return parser
def take_action(self, parsed_args):
self.interactive = parsed_args.interactive
self.init_database()
credentials = [
cfg.CONF.admin_user,
cfg.CONF.admin_password,
cfg.CONF.admin_tenant_name,
cfg.CONF.auth_url,
cfg.CONF.auth_strategy,
cfg.CONF.auth_region,
cfg.CONF.instance_provider
]
populate = threading.Thread(
name='router-populater',
target=populate_routers,
args=(self.fh.name, credentials, parsed_args.threads)
)
populate.setDaemon(True)
populate.start()
self.handle_loop()
def handle_loop(self):
try:
with self.term.fullscreen():
with self.term.cbreak():
val = None
while val != u'q':
if not val:
self.fetch_routers()
elif val.is_sequence:
if val.code == self.term.KEY_DOWN:
self.move_down()
if val.code == self.term.KEY_UP:
self.move_up()
elif val == u'j':
self.move_down()
elif val == u'k':
self.move_up()
elif val == u'r':
self.rebuild_router()
if self.interactive:
self.print_routers()
val = self.term.inkey(timeout=3)
elif len(self.routers) and all(map(
lambda x: x.last_fetch, self.routers
)):
self.print_routers()
val = u'q'
self._exit()
except KeyboardInterrupt:
self._exit()
raise
def fetch_routers(self):
with self.conn:
cursor = self.conn.cursor()
cursor.execute('SELECT * FROM routers ORDER BY id ASC;')
self.routers = cursor.fetchall()
@property
def window(self):
offset = 0
routers = self.routers
visible_height = self.term.height - 2
if len(routers) > visible_height:
offset = self.position
offset = min(offset, len(routers) - visible_height - 1)
return offset, routers[offset:(offset+visible_height+1)]
def print_routers(self):
offset, routers = self.window
with self.term.location():
for i, r in enumerate(routers):
if r.latest is None:
age = '<loading>'.ljust(11)
elif r.latest:
age = self.term.green('LATEST'.ljust(11))
elif not r.latest:
age = self.term.red('OUT-OF-DATE')
args = [
r.id,
r.name,
self.router_states[r.status](r.status.ljust(7)),
age,
r.image_name,
'at',
r.booted_at
]
if i + offset == self.position:
args = map(self.term.reverse, args[:-3]) + args[-3:]
print(self.term.move(i, 0) + ' '.join(args))
def make_message(self, router):
return {
'command': commands.ROUTER_REBUILD,
'router_id': router.id,
'tenant_id': router.tenant_id
}
def rebuild_router(self):
offset, routers = self.window
r = routers[self.position-offset]
r.status = 'REBUILD'
self.send_message(self.make_message(r))
def move_up(self):
self.position = max(0, self.position-1)
def move_down(self):
self.position = min(len(self.routers)-1, self.position+1)
@property
def router_states(self):
return {
'ACTIVE': self.term.green,
'BUILD': self.term.yellow,
'BOOT': self.term.yellow,
'REBUILD': self.term.yellow,
'DOWN': self.term.red,
'ERROR': self.term.red
}
def _exit(self):
if self.interactive:
print('Deleting %s...' % self.fh.name)
self.fh.close()
os.remove(self.fh.name)
if self.interactive:
print('Exiting...')

View File

@ -1,37 +0,0 @@
# 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.
"""Commands related to the application configuration
"""
import logging
from astara import commands
from astara.cli import message
class ConfigReload(message.MessageSending):
"""reload the configuration file(s)"""
log = logging.getLogger(__name__)
def make_message(self, parsed_args):
self.log.info(
'sending config reload instruction',
)
return {
'command': commands.CONFIG_RELOAD,
}

View File

@ -1,56 +0,0 @@
# Copyright 2015 Akanda, Inc.
#
# Author: Akanda, Inc.
#
# 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.
"""Commands related to tenants.
"""
import logging
from astara import commands
from astara.cli import message
class GlobalDebug(message.MessageSending):
"""Enable or disable global debug mode"""
_COMMAND = commands.GLOBAL_DEBUG
log = logging.getLogger(__name__)
def get_parser(self, prog_name):
p = super(GlobalDebug, self).get_parser(prog_name)
p.add_argument(
'status',
)
p.add_argument(
'--reason',
)
return p
def make_message(self, parsed_args):
status = parsed_args.status.lower()
if status not in ['enable', 'disable']:
m = "Invalid global-debug command, must 'enable' or 'disable'"
raise ValueError(m)
self.log.info(
"sending instruction to %s global debug mode" % status
)
return {
'command': self._COMMAND,
'enabled': 1 if status == "enable" else 0,
'reason': parsed_args.reason,
}

View File

@ -1,24 +0,0 @@
# 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 sys
from astara.cli import app
def main(args=sys.argv[1:]):
return app.RugController().run(args)

View File

@ -1,52 +0,0 @@
# 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.
"""Base class for command that sends a message to the rug
"""
import abc
import logging
import os
import sys
from cliff import command
from astara import notifications
class MessageSending(command.Command):
__metaclass__ = abc.ABCMeta
log = logging.getLogger(__name__)
interactive = False
@abc.abstractmethod
def make_message(self, parsed_args):
"""Return a dictionary containing the message contents
"""
return {}
def take_action(self, parsed_args):
self.send_message(self.make_message(parsed_args))
def send_message(self, payload):
sender = notifications.Sender()
cmd = payload.get('command')
argv = os.path.basename(sys.argv[0])
self.log.info('%s: sending %s instruction.' % (argv, cmd))
self.log.debug('payload: %r', payload)
sender.send(event_type='astara.command', message=payload)

View File

@ -1,33 +0,0 @@
# 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.
"""Commands related to tenants.
"""
import logging
from astara import commands
from astara.cli import message
class Poll(message.MessageSending):
log = logging.getLogger(__name__)
def make_message(self, parsed_args):
return {
'command': commands.POLL,
}

View File

@ -1,102 +0,0 @@
# 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.
"""Commands related to astara resources.
"""
from astara.api import nova
from astara.cli import message
from astara import commands
from novaclient import exceptions
from oslo_config import cfg
class _TenantResourceCmd(message.MessageSending):
def get_parser(self, prog_name):
p = super(_TenantResourceCmd, self).get_parser(prog_name)
p.add_argument(
'resource_id',
)
p.add_argument(
'--reason',
)
return p
def make_message(self, parsed_args):
resource_id = parsed_args.resource_id.lower()
reason = parsed_args.reason
self.log.info(
'sending %s instruction for resource %r',
self._COMMAND,
resource_id,
)
return {
'command': self._COMMAND,
'resource_id': resource_id,
'tenant_id': '*',
'reason': reason,
}
class ResourceUpdate(_TenantResourceCmd):
"""force-update a resource"""
_COMMAND = commands.RESOURCE_UPDATE
class ResourceRebuild(_TenantResourceCmd):
"""force-rebuild a resource"""
_COMMAND = commands.RESOURCE_REBUILD
def get_parser(self, prog_name):
p = super(ResourceRebuild, self).get_parser(prog_name)
p.add_argument(
'--image_uuid',
)
return p
def take_action(self, parsed_args):
uuid = parsed_args.image_uuid
if uuid:
nova_client = nova.Nova(cfg.CONF).client
try:
nova_client.images.get(uuid)
except exceptions.NotFound:
self.log.exception(
'could not retrieve custom image %s from Glance:' % uuid
)
raise
return super(ResourceRebuild, self).take_action(parsed_args)
def make_message(self, parsed_args):
message = super(ResourceRebuild, self).make_message(parsed_args)
message['resource_image_uuid'] = parsed_args.image_uuid
return message
class ResourceDebug(_TenantResourceCmd):
"""debug a single resource"""
_COMMAND = commands.RESOURCE_DEBUG
class ResourceManage(_TenantResourceCmd):
"""manage a single resource"""
_COMMAND = commands.RESOURCE_MANAGE

View File

@ -1,169 +0,0 @@
# 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.
"""Commands related to routers.
"""
import argparse
import subprocess
import sys
from astara.common.i18n import _LW
from astara import commands
from astara.cli import message
from astara.api import keystone, nova
from novaclient import exceptions
from oslo_config import cfg
from oslo_log import log as logging
from neutronclient.v2_0 import client
LOG = logging.getLogger(__name__)
class _TenantRouterCmd(message.MessageSending):
def get_parser(self, prog_name):
new_cmd = str(prog_name).replace('router', 'resource')
LOG.warning(_LW(
"WARNING: '%s' is deprecated in favor of '%s' and will be removed "
"in the Mitaka release.") % (prog_name, new_cmd))
# Bypass the direct base class to let us put the tenant id
# argument first
p = super(_TenantRouterCmd, self).get_parser(prog_name)
p.add_argument(
'router_id',
)
p.add_argument(
'--reason',
)
return p
def make_message(self, parsed_args):
router_id = parsed_args.router_id.lower()
reason = parsed_args.reason
if router_id in ['error', '*']:
tenant_id = router_id
else:
# Look up the tenant for a given router so we can send the
# command using both and the rug can route it to the correct
# worker. We do the lookup here instead of in the rug to avoid
# having to teach the rug notification and dispatching code
# about how to find the owner of a router, and to shift the
# burden of the neutron API call to the client so the server
# doesn't block. It also gives us a chance to report an error
# when we can't find the router.
ks_session = keystone.KeystoneSession()
n_c = client.Client(session=ks_session.session)
response = n_c.list_routers(retrieve_all=True, id=router_id)
try:
router_details = response['routers'][0]
except (KeyError, IndexError):
raise ValueError('No router with id %r found.' %
(router_id))
assert router_details['id'] == router_id
tenant_id = router_details['tenant_id']
self.log.info(
'sending %s instruction for tenant %r, router %r',
self._COMMAND,
tenant_id,
router_id,
)
return {
'command': self._COMMAND,
'router_id': router_id,
'tenant_id': tenant_id,
'reason': reason,
}
class RouterUpdate(_TenantRouterCmd):
"""force-update a router"""
_COMMAND = commands.ROUTER_UPDATE
class RouterRebuild(_TenantRouterCmd):
"""force-rebuild a router"""
_COMMAND = commands.ROUTER_REBUILD
def get_parser(self, prog_name):
p = super(RouterRebuild, self).get_parser(prog_name)
p.add_argument(
'--router_image_uuid',
)
return p
def take_action(self, parsed_args):
uuid = parsed_args.router_image_uuid
if uuid:
nova_client = nova.Nova(cfg.CONF).client
try:
nova_client.images.get(uuid)
except exceptions.NotFound:
self.log.exception(
'could not retrieve custom image %s from Glance:' % uuid
)
raise
return super(RouterRebuild, self).take_action(parsed_args)
def make_message(self, parsed_args):
message = super(RouterRebuild, self).make_message(parsed_args)
message['router_image_uuid'] = parsed_args.router_image_uuid
return message
class RouterDebug(_TenantRouterCmd):
"""debug a single router"""
_COMMAND = commands.ROUTER_DEBUG
class RouterManage(_TenantRouterCmd):
"""manage a single router"""
_COMMAND = commands.ROUTER_MANAGE
class RouterSSH(_TenantRouterCmd):
"""ssh into a router over the management network"""
interactive = True
def get_parser(self, prog_name):
p = super(RouterSSH, self).get_parser(prog_name)
p.add_argument('remainder', nargs=argparse.REMAINDER)
return p
def take_action(self, parsed_args):
ks_session = keystone.KeystoneSession()
n_c = client.Client(session=ks_session.session)
router_id = parsed_args.router_id.lower()
port = n_c.list_ports(name="ASTARA:MGT:%s" % router_id)
try:
mgmt_ip_addr = port['ports'][0]['fixed_ips'].pop()['ip_address']
except (KeyError, IndexError):
raise ValueError(
"No router management address found for router with id %s"
% router_id)
try:
cmd = ["ssh", "astara@%s" % mgmt_ip_addr] + parsed_args.remainder
subprocess.check_call(cmd)
except subprocess.CalledProcessError as e:
sys.exit(e.returncode)

View File

@ -1,62 +0,0 @@
# 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.
"""Commands related to tenants.
"""
import logging
from astara import commands
from astara.cli import message
class _TenantCmd(message.MessageSending):
log = logging.getLogger(__name__)
def get_parser(self, prog_name):
p = super(_TenantCmd, self).get_parser(prog_name)
p.add_argument(
'tenant_id',
)
p.add_argument(
'--reason',
)
return p
def make_message(self, parsed_args):
self.log.info(
'sending %s instruction for tenant with uuid %r',
self._COMMAND,
parsed_args.tenant_id,
)
return {
'command': self._COMMAND,
'tenant_id': parsed_args.tenant_id,
'reason': parsed_args.reason,
}
class TenantDebug(_TenantCmd):
"""debug a single tenant"""
_COMMAND = commands.TENANT_DEBUG
class TenantManage(_TenantCmd):
"""manage a single tenant"""
_COMMAND = commands.TENANT_MANAGE

View File

@ -1,37 +0,0 @@
# 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.
"""Commands related to workers.
"""
import logging
from astara import commands
from astara.cli import message
class WorkerDebug(message.MessageSending):
"""debug all workers"""
log = logging.getLogger(__name__)
def make_message(self, parsed_args):
self.log.info(
'sending worker debug instruction',
)
return {
'command': commands.WORKERS_DEBUG,
}

View File

@ -1,55 +0,0 @@
# 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.
"""Constants for the commands
"""
# Special values for dispatching
WILDCARDS = ('*', 'error')
# Dump debugging details about the worker processes and threads
WORKERS_DEBUG = 'workers-debug'
# Router commands expect a 'router_id' argument in the payload with
# the UUID of the router
# Put a resource in debug/manage mode
RESOURCE_DEBUG = 'resource-debug'
RESOURCE_MANAGE = 'resource-manage'
# Send an updated config to the resource whether it is needed or not
RESOURCE_UPDATE = 'resource-update'
# Rebuild a resource from scratch
RESOURCE_REBUILD = 'resource-rebuild'
# These are the deprecated versions of the above, to be removed in M.
ROUTER_DEBUG = 'router-debug'
ROUTER_MANAGE = 'router-manage'
ROUTER_UPDATE = 'router-update'
ROUTER_REBUILD = 'router-rebuild'
# Put a tenant in debug/manage mode
# Expects a 'tenant_id' argument in the payload with the UUID of the tenant
TENANT_DEBUG = 'tenant-debug'
TENANT_MANAGE = 'tenant-manage'
# Configuration commands
CONFIG_RELOAD = 'config-reload'
# Force a poll of all resources right now
POLL = 'poll'
GLOBAL_DEBUG = 'global-debug'

View File

@ -1,15 +0,0 @@
# 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.

View File

@ -1,96 +0,0 @@
# Copyright 2015 Akanda, Inc.
#
# 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 os
from oslo_config import cfg
from oslo_log import log
PREFERRED_CONFIG_FILEPATH = '/etc/astara/orchestrator.ini'
SEARCH_DIRS = ['/etc/astara', '/etc/akanda-rug', '/etc/akanda']
LEGACY_FILE_MAP = {
'orchestrator.ini': 'rug.ini',
'astara.pub': 'akanda.pub'
}
DEFAULT_CONFIG_FILES = [
PREFERRED_CONFIG_FILEPATH
]
def get_best_config_path(filepath=PREFERRED_CONFIG_FILEPATH):
if os.path.isfile(filepath):
return filepath
# now begin attemp to fallback for compatibility
dirname, basename = os.path.split(filepath)
if dirname and dirname not in SEARCH_DIRS:
return filepath # retain the non-standard location
for searchdir in SEARCH_DIRS:
candidate_path = os.path.join(searchdir, basename)
if os.path.isfile(candidate_path):
return candidate_path
if basename in LEGACY_FILE_MAP:
candidate_path = os.path.join(searchdir, LEGACY_FILE_MAP[basename])
if os.path.isfile(candidate_path):
return candidate_path
return filepath
def parse_config(argv, default_config_files=DEFAULT_CONFIG_FILES):
log.register_options(cfg.CONF)
# Set the logging format to include the process and thread, since
# those aren't included in standard openstack logs but are useful
# for the rug
extended = ':'.join('%(' + n + ')s'
for n in ['name',
'process',
'processName',
'threadName'])
log_format = ('%(asctime)s.%(msecs)03d %(levelname)s ' +
extended + ' %(message)s')
# Configure the default log levels for some third-party packages
# that are chatty
log_levels = [
'amqp=WARN',
'amqplib=WARN',
'qpid.messaging=INFO',
'sqlalchemy=WARN',
'keystoneclient=INFO',
'stevedore=INFO',
'eventlet.wsgi.server=WARN',
'requests=WARN',
'astara.openstack.common.rpc.amqp=INFO',
'neutronclient.client=INFO',
'oslo.messaging=INFO',
'iso8601=INFO',
'cliff.commandmanager=INFO',
]
cfg.CONF.set_default('logging_default_format_string', log_format)
log.set_defaults(default_log_levels=log_levels)
# For legacy compatibility
default_config_files = map(get_best_config_path, default_config_files)
# remove default config files that do not exist
default_config_files = filter(os.path.isfile, default_config_files)
cfg.CONF(argv,
project='astara-orchestrator',
default_config_files=default_config_files)

View File

@ -1,24 +0,0 @@
# Copyright (c) 2016 Akanda, 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.
ASTARA_SERVICE_PORT_TYPES = [
'VRRP',
'LB',
]
ASTARA_MGT_PORT_TYPES = [
'MGT',
]

View File

@ -1,70 +0,0 @@
# Copyright (c) 2016 Akanda, 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.
import collections
import threading
class ResourceContainer(object):
def __init__(self):
self.resources = {}
self.deleted = collections.deque(maxlen=50)
self.lock = threading.Lock()
def __delitem__(self, item):
with self.lock:
del self.resources[item]
self.deleted.append(item)
def items(self):
"""Get all state machines.
:returns: all state machines in this RouterContainer
"""
with self.lock:
return list(self.resources.items())
def values(self):
with self.lock:
return list(self.resources.values())
def has_been_deleted(self, resource_id):
"""Check if a resource has been deleted.
:param resource_id: The resource's id to check against the deleted list
:returns: Returns True if the resource_id has been deleted.
"""
with self.lock:
return resource_id in self.deleted
def __getitem__(self, item):
with self.lock:
return self.resources[item]
def __setitem__(self, key, value):
with self.lock:
self.resources[key] = value
def __contains__(self, item):
with self.lock:
return item in self.resources
def __bool__(self):
if self.values():
return True
else:
return False
def __nonzero__(self):
return self.__bool__()

View File

@ -1,204 +0,0 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# Copyright 2015 Akanda, 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.
import bisect
import hashlib
import threading
from oslo_config import cfg
import six
from six.moves import range
from astara.common.i18n import _
hash_opts = [
cfg.IntOpt('hash_partition_exponent',
default=5,
help='Exponent to determine number of hash partitions to use '
'when distributing load across Rugs. Larger values '
'will result in more even distribution of load and less '
'load when rebalancing the ring, but more memory usage. '
'Number of partitions per rug is '
'(2^hash_partition_exponent). This determines the '
'granularity of rebalancing: given 10 hosts, and an '
'exponent of the 2, there are 40 partitions in the ring.'
'A few thousand partitions should make rebalancing '
'smooth in most cases. The default is suitable for up to '
'a few hundred rugs. Too many partitions has a CPU '
'impact.'),
]
CONF = cfg.CONF
CONF.register_opts(hash_opts)
# A static key that can be used to choose a single host when from the
# ring we have no other data to hash with.
DC_KEY = 'astara_designated_coordinator'
class Invalid(Exception):
pass
# Lifted from ironic with some modifications.
class HashRing(object):
"""A stable hash ring.
We map item N to a host Y based on the closest lower hash:
- hash(item) -> partition
- hash(host) -> divider
- closest lower divider is the host to use
- we hash each host many times to spread load more finely
as otherwise adding a host gets (on average) 50% of the load of
just one other host assigned to it.
"""
def __init__(self, hosts, replicas=1):
"""Create a new hash ring across the specified hosts.
:param hosts: an iterable of hosts which will be mapped.
:param replicas: number of hosts to map to each hash partition,
or len(hosts), which ever is lesser.
Default: 1
"""
try:
self.hosts = set(hosts)
self.replicas = replicas if replicas <= len(hosts) else len(hosts)
except TypeError:
raise Invalid(
_("Invalid hosts supplied when building HashRing."))
self._host_hashes = {}
for host in hosts:
key = str(host).encode('utf8')
key_hash = hashlib.md5(key)
for p in range(2 ** CONF.hash_partition_exponent):
key_hash.update(key)
hashed_key = self._hash2int(key_hash)
self._host_hashes[hashed_key] = host
# Gather the (possibly colliding) resulting hashes into a bisectable
# list.
self._partitions = sorted(self._host_hashes.keys())
def _hash2int(self, key_hash):
"""Convert the given hash's digest to a numerical value for the ring.
:returns: An integer equivalent value of the digest.
"""
return int(key_hash.hexdigest(), 16)
def _get_partition(self, data):
try:
if six.PY3 and data is not None:
data = data.encode('utf-8')
key_hash = hashlib.md5(data)
hashed_key = self._hash2int(key_hash)
position = bisect.bisect(self._partitions, hashed_key)
return position if position < len(self._partitions) else 0
except TypeError:
raise Invalid(
_("Invalid data supplied to HashRing.get_hosts."))
def get_hosts(self, data, ignore_hosts=None):
"""Get the list of hosts which the supplied data maps onto.
:param data: A string identifier to be mapped across the ring.
:param ignore_hosts: A list of hosts to skip when performing the hash.
Useful to temporarily skip down hosts without
performing a full rebalance.
Default: None.
:returns: a list of hosts.
The length of this list depends on the number of replicas
this `HashRing` was created with. It may be less than this
if ignore_hosts is not None.
"""
hosts = []
if ignore_hosts is None:
ignore_hosts = set()
else:
ignore_hosts = set(ignore_hosts)
ignore_hosts.intersection_update(self.hosts)
partition = self._get_partition(data)
for replica in range(0, self.replicas):
if len(hosts) + len(ignore_hosts) == len(self.hosts):
# prevent infinite loop - cannot allocate more fallbacks.
break
# Linear probing: partition N, then N+1 etc.
host = self._get_host(partition)
while host in hosts or host in ignore_hosts:
partition += 1
if partition >= len(self._partitions):
partition = 0
host = self._get_host(partition)
hosts.append(host)
return hosts
def _get_host(self, partition):
"""Find what host is serving a partition.
:param partition: The index of the partition in the partition map.
e.g. 0 is the first partition, 1 is the second.
:return: The host object the ring was constructed with.
"""
return self._host_hashes[self._partitions[partition]]
class HashRingManager(object):
_hash_ring = None
_lock = threading.Lock()
def __init__(self):
self._hosts = []
self._balanced = False
@property
def balanced(self):
return self._balanced
@property
def ring(self):
# Hot path, no lock
if self._hash_ring is not None:
return self._hash_ring
with self._lock:
if self._hash_ring is None:
ring = self._load_hash_ring()
self.__class__._hash_ring = ring
return self._hash_ring
@property
def hosts(self):
return self.ring.hosts
def _load_hash_ring(self):
return HashRing(self._hosts)
@classmethod
def reset(cls):
with cls._lock:
cls._hash_ring = None
cls._balanced = False
def rebalance(self, hosts):
self.reset()
with self._lock:
self._hosts = hosts
self._balanced = True

View File

@ -1,24 +0,0 @@
# Copyright 2014 Akanda, Inc.
#
# Author: Akanda, Inc.
#
# 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 oslo_i18n
_translators = oslo_i18n.TranslatorFactory(domain='astara')
_ = _translators.primary
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error

View File

@ -1,15 +0,0 @@
# 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.

View File

@ -1,269 +0,0 @@
# 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.
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack Foundation
# 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.
import abc
import netaddr
from oslo_config import cfg
from oslo_log import log as logging
from astara.common.i18n import _, _LE, _LW
from astara.common.linux import ip_lib
from astara.common.linux import ovs_lib
from astara.common.linux import utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
OPTS = [
cfg.StrOpt('ovs_integration_bridge',
default='br-int',
help=_('Name of Open vSwitch bridge to use')),
cfg.BoolOpt('ovs_use_veth',
default=False,
help=_('Uses veth for an interface or not')),
cfg.StrOpt('network_device_mtu',
help=_('MTU setting for device.')),
]
CONF.register_opts(OPTS)
AGENT_OPTIONS = [
cfg.StrOpt('root_helper',
default='sudo astara-rootwrap /etc/astara/rootwrap.conf'),
]
CONF.register_group(cfg.OptGroup(name='AGENT'))
CONF.register_opts(AGENT_OPTIONS, 'AGENT')
class LinuxInterfaceDriver(object):
__metaclass__ = abc.ABCMeta
# from linux IF_NAMESIZE
DEV_NAME_LEN = 14
DEV_NAME_PREFIX = 'tap'
def __init__(self, conf):
self.conf = conf
self.root_helper = conf.AGENT.root_helper
def init_l3(self, device_name, ip_cidrs, namespace=None):
"""Set the L3 settings for the interface using data from the port.
ip_cidrs: list of 'X.X.X.X/YY' strings
"""
device = ip_lib.IPDevice(device_name,
self.root_helper,
namespace=namespace)
previous = {}
for address in device.addr.list(scope='global', filters=['permanent']):
previous[address['cidr']] = address['ip_version']
# add new addresses
for ip_cidr in ip_cidrs:
net = netaddr.IPNetwork(ip_cidr)
if ip_cidr in previous:
del previous[ip_cidr]
continue
device.addr.add(net.version, ip_cidr, str(net.broadcast))
# clean up any old addresses
for ip_cidr, ip_version in previous.items():
device.addr.delete(ip_version, ip_cidr)
def check_bridge_exists(self, bridge):
if not ip_lib.device_exists(bridge):
raise Exception(_('Bridge %s does not exist') % bridge)
def get_device_name(self, port):
return (self.DEV_NAME_PREFIX + port.id)[:self.DEV_NAME_LEN]
@abc.abstractmethod
def plug(self, network_id, port_id, device_name, mac_address,
bridge=None, namespace=None, prefix=None):
"""Plug in the interface."""
@abc.abstractmethod
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
"""Unplug the interface."""
class NullDriver(LinuxInterfaceDriver):
def plug(self, network_id, port_id, device_name, mac_address,
bridge=None, namespace=None, prefix=None):
pass
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
pass
class OVSInterfaceDriver(LinuxInterfaceDriver):
"""Driver for creating an internal interface on an OVS bridge."""
DEV_NAME_PREFIX = 'tap'
def __init__(self, conf):
super(OVSInterfaceDriver, self).__init__(conf)
if self.conf.ovs_use_veth:
self.DEV_NAME_PREFIX = 'ns-'
def _get_tap_name(self, dev_name, prefix=None):
if self.conf.ovs_use_veth:
dev_name = dev_name.replace(prefix or self.DEV_NAME_PREFIX, 'tap')
return dev_name
def _ovs_add_port(self, bridge, device_name, port_id, mac_address,
internal=True):
cmd = ['ovs-vsctl', '--', '--may-exist',
'add-port', bridge, device_name]
if internal:
cmd += ['--', 'set', 'Interface', device_name, 'type=internal']
cmd += ['--', 'set', 'Interface', device_name,
'external-ids:iface-id=%s' % port_id,
'--', 'set', 'Interface', device_name,
'external-ids:iface-status=active',
'--', 'set', 'Interface', device_name,
'external-ids:attached-mac=%s' % mac_address]
utils.execute(cmd, self.root_helper)
def plug(self, network_id, port_id, device_name, mac_address,
bridge=None, namespace=None, prefix=None):
"""Plug in the interface."""
if not bridge:
bridge = self.conf.ovs_integration_bridge
self.check_bridge_exists(bridge)
if not ip_lib.device_exists(device_name,
self.root_helper,
namespace=namespace):
ip = ip_lib.IPWrapper(self.root_helper)
tap_name = self._get_tap_name(device_name, prefix)
if self.conf.ovs_use_veth:
root_dev, ns_dev = ip.add_veth(tap_name, device_name)
internal = not self.conf.ovs_use_veth
self._ovs_add_port(bridge, tap_name, port_id, mac_address,
internal=internal)
ns_dev = ip.device(device_name)
ns_dev.link.set_address(mac_address)
if self.conf.network_device_mtu:
ns_dev.link.set_mtu(self.conf.network_device_mtu)
if self.conf.ovs_use_veth:
root_dev.link.set_mtu(self.conf.network_device_mtu)
if namespace:
namespace_obj = ip.ensure_namespace(namespace)
namespace_obj.add_device_to_namespace(ns_dev)
ns_dev.link.set_up()
if self.conf.ovs_use_veth:
root_dev.link.set_up()
else:
LOG.warning(_LW("Device %s already exists"), device_name)
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
"""Unplug the interface."""
if not bridge:
bridge = self.conf.ovs_integration_bridge
tap_name = self._get_tap_name(device_name, prefix)
self.check_bridge_exists(bridge)
ovs = ovs_lib.OVSBridge(bridge, self.root_helper)
try:
ovs.delete_port(tap_name)
if self.conf.ovs_use_veth:
device = ip_lib.IPDevice(device_name,
self.root_helper,
namespace)
device.link.delete()
LOG.debug(_("Unplugged interface '%s'"), device_name)
except RuntimeError:
LOG.exception(_LE("Failed unplugging interface '%s'"), device_name)
class BridgeInterfaceDriver(LinuxInterfaceDriver):
"""Driver for creating bridge interfaces."""
DEV_NAME_PREFIX = 'ns-'
def plug(self, network_id, port_id, device_name, mac_address,
bridge=None, namespace=None, prefix=None):
"""Plugin the interface."""
if not ip_lib.device_exists(device_name,
self.root_helper,
namespace=namespace):
ip = ip_lib.IPWrapper(self.root_helper)
# Enable agent to define the prefix
if prefix:
tap_name = device_name.replace(prefix, 'tap')
else:
tap_name = device_name.replace(self.DEV_NAME_PREFIX, 'tap')
root_veth, ns_veth = ip.add_veth(tap_name, device_name)
ns_veth.link.set_address(mac_address)
if self.conf.network_device_mtu:
root_veth.link.set_mtu(self.conf.network_device_mtu)
ns_veth.link.set_mtu(self.conf.network_device_mtu)
if namespace:
namespace_obj = ip.ensure_namespace(namespace)
namespace_obj.add_device_to_namespace(ns_veth)
root_veth.link.set_up()
ns_veth.link.set_up()
else:
LOG.warning(_LW("Device %s already exists"), device_name)
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
"""Unplug the interface."""
device = ip_lib.IPDevice(device_name, self.root_helper, namespace)
try:
device.link.delete()
LOG.debug("Unplugged interface '%s'", device_name)
except RuntimeError:
LOG.exception(_LE(
"Failed unplugging interface '%s'"), device_name)

View File

@ -1,450 +0,0 @@
# 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.
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack Foundation
# 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.
import netaddr
from astara.common.linux import utils
from astara.common.i18n import _
from oslo_log import log
LOOPBACK_DEVNAME = 'lo'
LOG = log.getLogger(__name__)
class SubProcessBase(object):
def __init__(self, root_helper=None, namespace=None):
self.root_helper = root_helper
self.namespace = namespace
def _run(self, options, command, args):
if self.namespace:
return self._as_root(options, command, args)
else:
return self._execute(options, command, args)
def _as_root(self, options, command, args, use_root_namespace=False):
if not self.root_helper:
raise Exception(_('Sudo is required to run this command'))
namespace = self.namespace if not use_root_namespace else None
return self._execute(options,
command,
args,
self.root_helper,
namespace)
@classmethod
def _execute(cls, options, command, args, root_helper=None,
namespace=None):
opt_list = ['-%s' % o for o in options]
if namespace:
ip_cmd = ['ip', 'netns', 'exec', namespace, 'ip']
else:
ip_cmd = ['ip']
return utils.execute(ip_cmd + opt_list + [command] + list(args),
root_helper=root_helper)
class IPWrapper(SubProcessBase):
def __init__(self, root_helper=None, namespace=None):
super(IPWrapper, self).__init__(root_helper=root_helper,
namespace=namespace)
self.netns = IpNetnsCommand(self)
def device(self, name):
return IPDevice(name, self.root_helper, self.namespace)
def get_devices(self, exclude_loopback=False):
retval = []
output = self._execute('o', 'link', ('list',),
self.root_helper, self.namespace)
for line in output.split('\n'):
if '<' not in line:
continue
tokens = line.split(':', 2)
if len(tokens) >= 3:
name = tokens[1].strip()
if exclude_loopback and name == LOOPBACK_DEVNAME:
continue
retval.append(IPDevice(name,
self.root_helper,
self.namespace))
return retval
def add_tuntap(self, name, mode='tap'):
self._as_root('', 'tuntap', ('add', name, 'mode', mode))
return IPDevice(name, self.root_helper, self.namespace)
def add_veth(self, name1, name2):
self._as_root('', 'link',
('add', name1, 'type', 'veth', 'peer', 'name', name2))
return (IPDevice(name1, self.root_helper, self.namespace),
IPDevice(name2, self.root_helper, self.namespace))
def ensure_namespace(self, name):
if not self.netns.exists(name):
ip = self.netns.add(name)
lo = ip.device(LOOPBACK_DEVNAME)
lo.link.set_up()
else:
ip = IPWrapper(self.root_helper, name)
return ip
def namespace_is_empty(self):
return not self.get_devices(exclude_loopback=True)
def garbage_collect_namespace(self):
"""Conditionally destroy the namespace if it is empty."""
if self.namespace and self.netns.exists(self.namespace):
if self.namespace_is_empty():
self.netns.delete(self.namespace)
return True
return False
def add_device_to_namespace(self, device):
if self.namespace:
device.link.set_netns(self.namespace)
@classmethod
def get_namespaces(cls, root_helper):
output = cls._execute('', 'netns', ('list',), root_helper=root_helper)
return [l.strip() for l in output.split('\n')]
class IPDevice(SubProcessBase):
def __init__(self, name, root_helper=None, namespace=None):
super(IPDevice, self).__init__(root_helper=root_helper,
namespace=namespace)
self.name = name
self.link = IpLinkCommand(self)
self.addr = IpAddrCommand(self)
self.route = IpRouteCommand(self)
def __eq__(self, other):
return (other is not None and self.name == other.name and
self.namespace == other.namespace)
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
return self.name
class IpCommandBase(object):
COMMAND = ''
def __init__(self, parent):
self._parent = parent
def _run(self, *args, **kwargs):
return self._parent._run(kwargs.get('options', []), self.COMMAND, args)
def _as_root(self, *args, **kwargs):
return self._parent._as_root(kwargs.get('options', []),
self.COMMAND,
args,
kwargs.get('use_root_namespace', False))
class IpDeviceCommandBase(IpCommandBase):
@property
def name(self):
return self._parent.name
class IpLinkCommand(IpDeviceCommandBase):
COMMAND = 'link'
def set_address(self, mac_address):
self._as_root('set', self.name, 'address', mac_address)
def set_mtu(self, mtu_size):
self._as_root('set', self.name, 'mtu', mtu_size)
def set_up(self):
self._as_root('set', self.name, 'up')
def set_down(self):
self._as_root('set', self.name, 'down')
def set_netns(self, namespace):
self._as_root('set', self.name, 'netns', namespace)
self._parent.namespace = namespace
def set_name(self, name):
self._as_root('set', self.name, 'name', name)
self._parent.name = name
def set_alias(self, alias_name):
self._as_root('set', self.name, 'alias', alias_name)
def delete(self):
self._as_root('delete', self.name)
@property
def address(self):
return self.attributes.get('link/ether')
@property
def state(self):
return self.attributes.get('state')
@property
def mtu(self):
return self.attributes.get('mtu')
@property
def qdisc(self):
return self.attributes.get('qdisc')
@property
def qlen(self):
return self.attributes.get('qlen')
@property
def alias(self):
return self.attributes.get('alias')
@property
def attributes(self):
return self._parse_line(self._run('show', self.name, options='o'))
def _parse_line(self, value):
if not value:
return {}
device_name, settings = value.replace("\\", '').split('>', 1)
tokens = settings.split()
keys = tokens[::2]
values = [int(v) if v.isdigit() else v for v in tokens[1::2]]
retval = dict(zip(keys, values))
return retval
class IpAddrCommand(IpDeviceCommandBase):
COMMAND = 'addr'
def add(self, ip_version, cidr, broadcast, scope='global'):
self._as_root('add',
cidr,
'brd',
broadcast,
'scope',
scope,
'dev',
self.name,
options=[ip_version])
def delete(self, ip_version, cidr):
self._as_root('del',
cidr,
'dev',
self.name,
options=[ip_version])
def flush(self):
self._as_root('flush', self.name)
def list(self, scope=None, to=None, filters=None):
if filters is None:
filters = []
retval = []
if scope:
filters += ['scope', scope]
if to:
filters += ['to', to]
for line in self._run('show', self.name, *filters).split('\n'):
line = line.strip()
if not line.startswith('inet'):
continue
parts = line.split()
if parts[0] == 'inet6':
version = 6
scope = parts[3]
broadcast = '::'
else:
version = 4
if parts[2] == 'brd':
broadcast = parts[3]
scope = parts[5]
else:
# sometimes output of 'ip a' might look like:
# inet 192.168.100.100/24 scope global eth0
# and broadcast needs to be calculated from CIDR
broadcast = str(netaddr.IPNetwork(parts[1]).broadcast)
scope = parts[3]
retval.append(dict(cidr=parts[1],
broadcast=broadcast,
scope=scope,
ip_version=version,
dynamic=('dynamic' == parts[-1])))
return retval
class IpRouteCommand(IpDeviceCommandBase):
COMMAND = 'route'
def add_gateway(self, gateway, metric=None):
args = ['replace', 'default', 'via', gateway]
if metric:
args += ['metric', metric]
args += ['dev', self.name]
self._as_root(*args)
def delete_gateway(self, gateway):
self._as_root('del',
'default',
'via',
gateway,
'dev',
self.name)
def get_gateway(self, scope=None, filters=None):
if filters is None:
filters = []
retval = None
if scope:
filters += ['scope', scope]
route_list_lines = self._run('list', 'dev', self.name,
*filters).split('\n')
default_route_line = next((x.strip() for x in
route_list_lines if
x.strip().startswith('default')), None)
if default_route_line:
gateway_index = 2
parts = default_route_line.split()
retval = dict(gateway=parts[gateway_index])
metric_index = 4
parts_has_metric = (len(parts) > metric_index)
if parts_has_metric:
retval.update(metric=int(parts[metric_index]))
return retval
def pullup_route(self, interface_name):
"""
Ensures that the route entry for the interface is before all
others on the same subnet.
"""
device_list = []
device_route_list_lines = self._run('list', 'proto', 'kernel',
'dev', interface_name).split('\n')
for device_route_line in device_route_list_lines:
try:
subnet = device_route_line.split()[0]
except:
continue
subnet_route_list_lines = self._run('list', 'proto', 'kernel',
'match', subnet).split('\n')
for subnet_route_line in subnet_route_list_lines:
i = iter(subnet_route_line.split())
while(next(i) != 'dev'):
pass
device = next(i)
try:
while(next(i) != 'src'):
pass
src = next(i)
except:
src = ''
if device != interface_name:
device_list.append((device, src))
else:
break
for (device, src) in device_list:
self._as_root('del', subnet, 'dev', device)
if (src != ''):
self._as_root('append', subnet, 'proto', 'kernel',
'src', src, 'dev', device)
else:
self._as_root('append', subnet, 'proto', 'kernel',
'dev', device)
class IpNetnsCommand(IpCommandBase):
COMMAND = 'netns'
def add(self, name):
self._as_root('add', name, use_root_namespace=True)
return IPWrapper(self._parent.root_helper, name)
def delete(self, name):
self._as_root('delete', name, use_root_namespace=True)
def execute(self, cmds, addl_env={}, check_exit_code=True):
if not self._parent.root_helper:
m = _('sudo is required to run this command')
LOG.error(m)
raise Exception(m)
elif not self._parent.namespace:
m = _('No namespace defined for parent')
LOG.error(m)
raise Exception(m)
else:
return utils.execute(
['%s=%s' % pair for pair in addl_env.items()] +
['ip', 'netns', 'exec', self._parent.namespace] + list(cmds),
root_helper=self._parent.root_helper,
check_exit_code=check_exit_code)
def exists(self, name):
output = self._as_root('list', options='o', use_root_namespace=True)
for line in output.split('\n'):
if name == line.strip():
return True
return False
def device_exists(device_name, root_helper=None, namespace=None):
try:
address = IPDevice(device_name, root_helper, namespace).link.address
except RuntimeError:
return False
return bool(address)

View File

@ -1,321 +0,0 @@
# 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.
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nicira Networks, 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.
# @author: Somik Behera, Nicira Networks, Inc.
# @author: Brad Hall, Nicira Networks, Inc.
# @author: Dan Wendlandt, Nicira Networks, Inc.
# @author: Dave Lapsley, Nicira Networks, Inc.
import re
from astara.common.linux import utils
from astara.common.i18n import _, _LE, _LW
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
class VifPort:
def __init__(self, port_name, ofport, vif_id, vif_mac, switch):
self.port_name = port_name
self.ofport = ofport
self.vif_id = vif_id
self.vif_mac = vif_mac
self.switch = switch
def __str__(self):
return ("iface-id=" + self.vif_id + ", vif_mac=" +
self.vif_mac + ", port_name=" + self.port_name +
", ofport=" + str(self.ofport) + ", bridge_name =" +
self.switch.br_name)
class OVSBridge:
def __init__(self, br_name, root_helper):
self.br_name = br_name
self.root_helper = root_helper
self.re_id = self.re_compile_id()
def re_compile_id(self):
external = 'external_ids\s*'
mac = 'attached-mac="(?P<vif_mac>([a-fA-F\d]{2}:){5}([a-fA-F\d]{2}))"'
iface = 'iface-id="(?P<vif_id>[^"]+)"'
name = 'name\s*:\s"(?P<port_name>[^"]*)"'
port = 'ofport\s*:\s(?P<ofport>-?\d+)'
_re = ('%(external)s:\s{ ( %(mac)s,? | %(iface)s,? | . )* }'
' \s+ %(name)s \s+ %(port)s' % locals())
return re.compile(_re, re.M | re.X)
def run_vsctl(self, args):
full_args = ["ovs-vsctl", "--timeout=2"] + args
try:
return utils.execute(full_args, root_helper=self.root_helper)
except Exception, e:
LOG.error(_LE(
"Unable to execute %(cmd)s. Exception: %(exception)s"),
{'cmd': full_args, 'exception': e})
def reset_bridge(self):
self.run_vsctl(["--", "--if-exists", "del-br", self.br_name])
self.run_vsctl(["add-br", self.br_name])
def add_port(self, port_name):
self.run_vsctl(["--", "--may-exist", "add-port", self.br_name,
port_name])
return self.get_port_ofport(port_name)
def delete_port(self, port_name):
self.run_vsctl(["--", "--if-exists", "del-port", self.br_name,
port_name])
def set_db_attribute(self, table_name, record, column, value):
args = ["set", table_name, record, "%s=%s" % (column, value)]
self.run_vsctl(args)
def clear_db_attribute(self, table_name, record, column):
args = ["clear", table_name, record, column]
self.run_vsctl(args)
def run_ofctl(self, cmd, args):
full_args = ["ovs-ofctl", cmd, self.br_name] + args
try:
return utils.execute(full_args, root_helper=self.root_helper)
except Exception, e:
LOG.error(_LE(
"Unable to execute %(cmd)s. Exception: %(exception)s"),
{'cmd': full_args, 'exception': e})
def count_flows(self):
flow_list = self.run_ofctl("dump-flows", []).split("\n")[1:]
return len(flow_list) - 1
def remove_all_flows(self):
self.run_ofctl("del-flows", [])
def get_port_ofport(self, port_name):
return self.db_get_val("Interface", port_name, "ofport")
def get_datapath_id(self):
return self.db_get_val('Bridge',
self.br_name, 'datapath_id').strip('"')
def _build_flow_expr_arr(self, **kwargs):
flow_expr_arr = []
is_delete_expr = kwargs.get('delete', False)
if not is_delete_expr:
prefix = ("hard_timeout=%s,idle_timeout=%s,priority=%s" %
(kwargs.get('hard_timeout', '0'),
kwargs.get('idle_timeout', '0'),
kwargs.get('priority', '1')))
flow_expr_arr.append(prefix)
elif 'priority' in kwargs:
raise Exception(_("Cannot match priority on flow deletion"))
in_port = ('in_port' in kwargs and ",in_port=%s" %
kwargs['in_port'] or '')
dl_type = ('dl_type' in kwargs and ",dl_type=%s" %
kwargs['dl_type'] or '')
dl_vlan = ('dl_vlan' in kwargs and ",dl_vlan=%s" %
kwargs['dl_vlan'] or '')
dl_src = 'dl_src' in kwargs and ",dl_src=%s" % kwargs['dl_src'] or ''
dl_dst = 'dl_dst' in kwargs and ",dl_dst=%s" % kwargs['dl_dst'] or ''
nw_src = 'nw_src' in kwargs and ",nw_src=%s" % kwargs['nw_src'] or ''
nw_dst = 'nw_dst' in kwargs and ",nw_dst=%s" % kwargs['nw_dst'] or ''
tun_id = 'tun_id' in kwargs and ",tun_id=%s" % kwargs['tun_id'] or ''
proto = 'proto' in kwargs and ",%s" % kwargs['proto'] or ''
ip = ('nw_src' in kwargs or 'nw_dst' in kwargs) and ',ip' or ''
match = (in_port + dl_type + dl_vlan + dl_src + dl_dst +
(ip or proto) + nw_src + nw_dst + tun_id)
if match:
match = match[1:] # strip leading comma
flow_expr_arr.append(match)
return flow_expr_arr
def add_flow(self, **kwargs):
if "actions" not in kwargs:
raise Exception(_("Must specify one or more actions"))
if "priority" not in kwargs:
kwargs["priority"] = "0"
flow_expr_arr = self._build_flow_expr_arr(**kwargs)
flow_expr_arr.append("actions=%s" % (kwargs["actions"]))
flow_str = ",".join(flow_expr_arr)
self.run_ofctl("add-flow", [flow_str])
def delete_flows(self, **kwargs):
kwargs['delete'] = True
flow_expr_arr = self._build_flow_expr_arr(**kwargs)
if "actions" in kwargs:
flow_expr_arr.append("actions=%s" % (kwargs["actions"]))
flow_str = ",".join(flow_expr_arr)
self.run_ofctl("del-flows", [flow_str])
def add_tunnel_port(self, port_name, remote_ip):
self.run_vsctl(["add-port", self.br_name, port_name])
self.set_db_attribute("Interface", port_name, "type", "gre")
self.set_db_attribute("Interface", port_name, "options:remote_ip",
remote_ip)
self.set_db_attribute("Interface", port_name, "options:in_key", "flow")
self.set_db_attribute("Interface", port_name, "options:out_key",
"flow")
return self.get_port_ofport(port_name)
def add_patch_port(self, local_name, remote_name):
self.run_vsctl(["add-port", self.br_name, local_name])
self.set_db_attribute("Interface", local_name, "type", "patch")
self.set_db_attribute("Interface", local_name, "options:peer",
remote_name)
return self.get_port_ofport(local_name)
def db_get_map(self, table, record, column):
output = self.run_vsctl(["get", table, record, column])
if output:
str = output.rstrip("\n\r")
return self.db_str_to_map(str)
return {}
def db_get_val(self, table, record, column):
output = self.run_vsctl(["get", table, record, column])
if output:
return output.rstrip("\n\r")
def db_str_to_map(self, full_str):
list = full_str.strip("{}").split(", ")
ret = {}
for e in list:
if e.find("=") == -1:
continue
arr = e.split("=")
ret[arr[0]] = arr[1].strip("\"")
return ret
def get_port_name_list(self):
res = self.run_vsctl(["list-ports", self.br_name])
if res:
return res.strip().split("\n")
return []
def get_port_stats(self, port_name):
return self.db_get_map("Interface", port_name, "statistics")
def get_xapi_iface_id(self, xs_vif_uuid):
args = ["xe", "vif-param-get", "param-name=other-config",
"param-key=nicira-iface-id", "uuid=%s" % xs_vif_uuid]
try:
return utils.execute(args, root_helper=self.root_helper).strip()
except Exception, e:
LOG.error(_LE(
"Unable to execute %(cmd)s. Exception: %(exception)s"),
{'cmd': args, 'exception': e})
# returns a VIF object for each VIF port
def get_vif_ports(self):
edge_ports = []
port_names = self.get_port_name_list()
for name in port_names:
external_ids = self.db_get_map("Interface", name, "external_ids")
ofport = self.db_get_val("Interface", name, "ofport")
if "iface-id" in external_ids and "attached-mac" in external_ids:
p = VifPort(name, ofport, external_ids["iface-id"],
external_ids["attached-mac"], self)
edge_ports.append(p)
elif ("xs-vif-uuid" in external_ids and
"attached-mac" in external_ids):
# if this is a xenserver and iface-id is not automatically
# synced to OVS from XAPI, we grab it from XAPI directly
iface_id = self.get_xapi_iface_id(external_ids["xs-vif-uuid"])
p = VifPort(name, ofport, iface_id,
external_ids["attached-mac"], self)
edge_ports.append(p)
return edge_ports
def get_vif_port_set(self):
edge_ports = set()
port_names = self.get_port_name_list()
for name in port_names:
external_ids = self.db_get_map("Interface", name, "external_ids")
if "iface-id" in external_ids and "attached-mac" in external_ids:
edge_ports.add(external_ids['iface-id'])
elif ("xs-vif-uuid" in external_ids and
"attached-mac" in external_ids):
# if this is a xenserver and iface-id is not automatically
# synced to OVS from XAPI, we grab it from XAPI directly
iface_id = self.get_xapi_iface_id(external_ids["xs-vif-uuid"])
edge_ports.add(iface_id)
return edge_ports
def get_vif_port_by_id(self, port_id):
args = ['--', '--columns=external_ids,name,ofport',
'find', 'Interface',
'external_ids:iface-id="%s"' % port_id]
result = self.run_vsctl(args)
if not result:
return
match = self.re_id.search(result)
try:
vif_mac = match.group('vif_mac')
vif_id = match.group('vif_id')
port_name = match.group('port_name')
ofport = int(match.group('ofport'))
return VifPort(port_name, ofport, vif_id, vif_mac, self)
except Exception, e:
LOG.warning(_LW("Unable to parse regex results. Exception: %s"), e)
return
def delete_ports(self, all_ports=False):
if all_ports:
port_names = self.get_port_name_list()
else:
port_names = (port.port_name for port in self.get_vif_ports())
for port_name in port_names:
self.delete_port(port_name)
def get_bridge_for_iface(root_helper, iface):
args = ["ovs-vsctl", "--timeout=2", "iface-to-br", iface]
try:
return utils.execute(args, root_helper=root_helper).strip()
except Exception:
LOG.exception(_LE("Interface %s not found."), iface)
return None
def get_bridges(root_helper):
args = ["ovs-vsctl", "--timeout=2", "list-br"]
try:
return utils.execute(args, root_helper=root_helper).strip().split("\n")
except Exception:
LOG.exception(_LE("Unable to retrieve bridges."))
return []

View File

@ -1,123 +0,0 @@
# 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.
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Locaweb.
# 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.
#
# @author: Juliano Martinez, Locaweb.
import fcntl
import os
import shlex
import signal
import socket
import struct
import tempfile
from eventlet.green import subprocess
from astara.common.i18n import _
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
def _subprocess_setup():
# Python installs a SIGPIPE handler by default. This is usually not what
# non-Python subprocesses expect.
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
def subprocess_popen(args, stdin=None, stdout=None, stderr=None, shell=False,
env=None):
return subprocess.Popen(args, shell=shell, stdin=stdin, stdout=stdout,
stderr=stderr, preexec_fn=_subprocess_setup,
close_fds=True, env=env)
def execute(cmd, root_helper=None, process_input=None, addl_env=None,
check_exit_code=True, return_stderr=False):
if root_helper:
cmd = shlex.split(root_helper) + cmd
cmd = map(str, cmd)
LOG.debug("Running command: %s", cmd)
env = os.environ.copy()
if addl_env:
env.update(addl_env)
obj = subprocess_popen(cmd, shell=False,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env)
_stdout, _stderr = (process_input and
obj.communicate(process_input) or
obj.communicate())
obj.stdin.close()
m = _("\nCommand: %(cmd)s\nExit code: %(code)s\nStdout: %(stdout)r\n"
"Stderr: %(stderr)r") % {'cmd': cmd, 'code': obj.returncode,
'stdout': _stdout, 'stderr': _stderr}
LOG.debug(m)
if obj.returncode and check_exit_code:
raise RuntimeError(m)
return return_stderr and (_stdout, _stderr) or _stdout
def get_interface_mac(interface):
DEVICE_NAME_LEN = 15
MAC_START = 18
MAC_END = 24
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = fcntl.ioctl(s.fileno(), 0x8927,
struct.pack('256s', interface[:DEVICE_NAME_LEN]))
return ''.join(['%02x:' % ord(char)
for char in info[MAC_START:MAC_END]])[:-1]
def replace_file(file_name, data):
"""Replaces the contents of file_name with data in a safe manner.
First write to a temp file and then rename. Since POSIX renames are
atomic, the file is unlikely to be corrupted by competing writes.
We create the tempfile on the same device to ensure that it can be renamed.
"""
base_dir = os.path.dirname(os.path.abspath(file_name))
tmp_file = tempfile.NamedTemporaryFile('w+', dir=base_dir, delete=False)
tmp_file.write(data)
tmp_file.close()
os.chmod(tmp_file.name, 0644)
os.rename(tmp_file.name, file_name)

View File

@ -1,153 +0,0 @@
# Copyright 2015 Akanda, Inc
#
# Author: Akanda, Inc
#
# 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.
from six.moves.urllib import parse as urlparse
from oslo_log import log as logging
from oslo_config import cfg
from oslo_service import service
import oslo_messaging
from astara.common.i18n import _LW
LOG = logging.getLogger(__name__)
def _deprecated_amqp_url():
"""Allow for deprecating amqp_url setting over time.
This warns and attempts to translate an amqp_url to something
oslo_messaging can use to load a driver.
"""
url = cfg.CONF.amqp_url
if not url:
return
LOG.warning(_LW(
'Use of amqp_url is deprecated. Please instead use options defined in '
'oslo_messaging_rabbit to declare your AMQP connection.'))
url = urlparse.urlsplit(url)
if url.scheme == 'amqp':
scheme = 'rabbit'
else:
scheme = url.scheme
port = str(url.port or 5672)
netloc = url.netloc
if netloc.endswith(':'):
netloc = netloc[:-1]
out = urlparse.urlunsplit((
scheme,
'%s:%s' % (netloc, port),
url.path,
'', ''
))
return out
def get_transport():
url = _deprecated_amqp_url()
return oslo_messaging.get_transport(conf=cfg.CONF, url=url)
def get_server(target, endpoints):
return oslo_messaging.get_rpc_server(
transport=get_transport(),
target=target,
endpoints=endpoints,
)
def get_target(topic, fanout=True, exchange=None, version=None, server=None):
return oslo_messaging.Target(
topic=topic, fanout=fanout, exchange=exchange, version=version,
server=server)
def get_rpc_client(topic, exchange=None, version='1.0'):
"""Creates an RPC client to be used to request methods be
executed on remote RPC servers
"""
target = get_target(topic=topic, exchange=exchange,
version=version, fanout=False)
return oslo_messaging.rpc.client.RPCClient(
get_transport(), target
)
def get_rpc_notifier(topic='notifications'):
return oslo_messaging.notify.Notifier(
transport=get_transport(),
# TODO(adam_g): driver should be specified in oslo.messaging's cfg
driver='messaging',
topic=topic,
)
class MessagingService(service.Service):
"""Used to create objects that can manage multiple RPC connections"""
def __init__(self):
super(MessagingService, self).__init__()
self._servers = set()
def _add_server(self, server):
self._servers.add(server)
def create_rpc_consumer(self, topic, endpoints):
"""Creates an RPC server for this host that will execute RPCs requested
by clients. Adds the resulting consumer to the pool of messaging
servers.
:param topic: Topic on which to listen for RPC requests
:param endpoints: List of endpoint objects that define methods that
the server will execute.
"""
target = get_target(topic=topic, fanout=True, server=cfg.CONF.host)
server = get_server(target, endpoints)
LOG.debug('Created RPC server on topic %s', topic)
self._add_server(server)
def create_notification_listener(self, endpoints, exchange=None,
topic='notifications'):
"""Creates an oslo.messaging notification listener associated with
provided endpoints. Adds the resulting listener to the pool of
messaging servers.
:param endpoints: list of endpoint objects that define methods for
processing prioritized notifications
:param exchange: Optional control exchange to listen on. If not
specified, oslo_messaging defaults to 'openstack'
:param topic: Topic on which to listen for notification events
"""
transport = get_transport()
target = get_target(topic=topic, fanout=False,
exchange=exchange)
pool = 'astara.' + topic + '.' + cfg.CONF.host
server = oslo_messaging.get_notification_listener(
transport, [target], endpoints, pool=pool, executor='threading')
LOG.debug(
'Created RPC notification listener on topic:%s/exchange:%s.',
topic, exchange)
self._add_server(server)
def start(self):
LOG.info('Astara notification listener service starting...')
super(MessagingService, self).start()
[s.start() for s in self._servers]
LOG.info('Astara notification listener service started.')
def stop(self):
LOG.info('Astara notification listener service stopping...')
super(MessagingService, self).stop()
[s.wait() for s in self._servers]
LOG.info('Astara notification listener service stopped.')

View File

@ -1,186 +0,0 @@
# Copyright 2015 Akanda, Inc.
#
# Author: Akanda, Inc.
#
# 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 signal
import time
from oslo_config import cfg
from oslo_log import log
import tooz
from tooz import coordination as tz_coordination
from astara import event as ak_event
from astara.common.i18n import _, _LI
LOG = log.getLogger(__name__)
CONF = cfg.CONF
COORD_OPTS = [
cfg.BoolOpt('enabled', default=False,
help=_('Whether to use an external coordination service to '
'a cluster of astara-orchestrator nodes. This may be '
'disabled for astara-orchestrator node environments.')),
cfg.StrOpt('url',
default='memcached://localhost:11211',
help=_('URL of supported coordination service')),
cfg.StrOpt('group_id', default='astara.orchestrator',
help=_('ID of coordination group to join.')),
cfg.IntOpt('heartbeat_interval', default=1,
help=_('Interval (in seconds) for cluster heartbeats')),
]
CONF.register_group(cfg.OptGroup(name='coordination'))
CONF.register_opts(COORD_OPTS, group='coordination')
class InvalidEventType(Exception):
pass
class CoordinatorDone(Exception):
pass
class RugCoordinator(object):
def __init__(self, notifications_queue):
self._queue = notifications_queue
self.host = CONF.host
self.url = CONF.coordination.url
self.group = CONF.coordination.group_id
self.heartbeat_interval = CONF.coordination.heartbeat_interval
self._coordinator = None
signal.signal(signal.SIGTERM, self.stop)
self.start()
def start(self):
"""Brings up coordination service online
This connects the coordination service to its tooz backend. This
involves:
- connecting to the cluster
- creating the coordination group (if required)
- joining the coordination group
- registering callbacks to respond to join/leave membership
events
After the local node has joined the cluster and knows its remote
peers, it fires off an initial rebalance event to the workers
so they can seed their hash ring with the current membership.
"""
LOG.info(_LI('Starting RUG coordinator process for host %s on %s'),
self.host, self.url)
self._coordinator = tz_coordination.get_coordinator(
self.url, self.host)
self._coordinator.start()
try:
self._coordinator.create_group(self.group).get()
except tooz.coordination.GroupAlreadyExist:
pass
try:
self._coordinator.join_group(self.group).get()
self._coordinator.heartbeat()
except tooz.coordination.MemberAlreadyExist:
pass
self._coordinator.watch_join_group(self.group, self.cluster_changed)
self._coordinator.watch_leave_group(self.group, self.cluster_changed)
self._coordinator.heartbeat()
LOG.debug("Sending initial event changed for members: %s" %
self.members)
self.cluster_changed(event=None, node_bootstrap=True)
def run(self):
try:
while True:
self._coordinator.heartbeat()
self._coordinator.run_watchers()
time.sleep(self.heartbeat_interval)
except CoordinatorDone:
LOG.info(_LI('Stopping RUG coordinator.'))
return
def stop(self, signal=None, frame=None):
"""Stop the coordinator service.
This ensures a clean shutdown of the coordinator service and attemps to
advertise its departure to the rest of the cluster. Note this is
registered as a signal handler for SIGINT so that its run when the main
shutdowns and subprocesses receive the signal.
"""
self._coordinator.unwatch_join_group(self.group, self.cluster_changed)
self._coordinator.unwatch_leave_group(self.group, self.cluster_changed)
if self.is_leader:
try:
self._coordinator.stand_down_group_leader(self.group)
except tooz.NotImplemented:
pass
self._coordinator.leave_group(self.group).get()
raise CoordinatorDone()
@property
def members(self):
"""Returns the current cluster membership list"""
members = self._coordinator.get_members(self.group).get()
# tooz ZK driver reports 'leader' as a member, which can screw with
# hashing.
try:
members.remove('leader')
except ValueError:
pass
return members
@property
def is_leader(self):
"""Returns true if the local cluster member is the leader"""
return self._coordinator.get_leader(self.group).get() == self.host
def cluster_changed(self, event, node_bootstrap=False):
"""Event callback to be called by tooz on membership changes"""
LOG.debug('Broadcasting cluster changed event to trigger rebalance. '
'members=%s' % self.members)
body = {
'members': self.members
}
# Flag this as a local bootstrap rebalance rather than one in reaction
# to a cluster event.
if node_bootstrap:
body['node_bootstrap'] = True
r = ak_event.Resource(
tenant_id='*',
id='*',
driver='*',
)
e = ak_event.Event(
resource=r,
crud=ak_event.REBALANCE,
body=body,
)
self._queue.put(('*', e))
def start(notification_queue):
return RugCoordinator(notification_queue).run()

View File

@ -1,44 +0,0 @@
# 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.
"""Utilities for managing ourselves as a daemon.
"""
import signal
from oslo_log import log as logging
from astara.common.i18n import _LI
def ignore_signals():
"""Ignore signals that might interrupt processing
Since the RUG doesn't want to be asynchronously interrupted,
various signals received needs to be ignored. The registered
signals including SIGHUP, SIGALRM, and default signals
SIGUSR1 and SIGUSR2 are captured and ignored through the SIG_IGN
action.
:param: None
:returns: None
"""
for s in [signal.SIGHUP, signal.SIGUSR1, signal.SIGUSR2, signal.SIGALRM]:
logging.getLogger(__name__).info(_LI('ignoring signal %s'), s)
signal.signal(s, signal.SIG_IGN)

View File

View File

@ -1,128 +0,0 @@
# Copyright 2015 Akanda, Inc.
#
# Author: Akanda, Inc.
#
# 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 abc
import six
from oslo_config import cfg
from oslo_db import api as db_api
_BACKEND_MAPPING = {
'sqlalchemy': 'astara.db.sqlalchemy.api'
}
IMPL = db_api.DBAPI.from_config(
cfg.CONF, backend_mapping=_BACKEND_MAPPING, lazy=True)
def get_instance():
return IMPL
@six.add_metaclass(abc.ABCMeta)
class Connection(object):
@abc.abstractmethod
def __init__(self):
pass
@abc.abstractmethod
def enable_resource_debug(self, resource_uuid, reason=None):
"""Enter a resource into debug mode
:param resource_uuid: str uuid of the resource to be placed into debug
mode
:param reason: str (optional) reason for entering resource into debug
mode
"""
@abc.abstractmethod
def disable_resource_debug(self, resource_uuid):
"""Remove a resource into debug mode
:param resource_uuid: str uuid of the resource to be removed from debug
mode
"""
@abc.abstractmethod
def resource_in_debug(self, resource_uuid):
"""Determines if a resource is in debug mode
:param resource_uuid: str the uuid of the resource to query
:returns: tuple (False, None) if resource is not in debug mode or
(True, "reason") if it is.
"""
@abc.abstractmethod
def resources_in_debug(self):
"""Queries all resources in debug mode
:returns: a set of (resource_uuid, reason) tuples
"""
@abc.abstractmethod
def enable_tenant_debug(self, tenant_uuid, reason=None):
"""Enter a tenant into debug mode
:param tenant_uuid: str uuid of the tenant to be placed into debug
mode
:param reason: str (optional) reason for entering tenant into debug
mode
"""
@abc.abstractmethod
def disable_tenant_debug(self, tenant_uuid):
"""Remove a tenant into debug mode
:param tenant_uuid: str uuid of the tenant to be removed from debug
mode
"""
@abc.abstractmethod
def tenant_in_debug(self, tenant_uuid):
"""Determines if a tenant is in debug mode
:param tenant_uuid: str the uuid of the tenant to query
:returns: tuple (False, None) if tenant is not in debug mode or
(True, "reason") if it is.
"""
@abc.abstractmethod
def tenants_in_debug(self):
"""Queries all tenants in debug mode
:returns: a set of (tenant_uuid, reason) tuples
"""
@abc.abstractmethod
def enable_global_debug(self, reason=None):
"""Enter the entire system into debug mode
:param reason: str (optional) reason for entering cluster into global
debug mode.
"""
@abc.abstractmethod
def disable_global_debug(self):
"""Remove the entire system from global debug mode"""
@abc.abstractmethod
def global_debug(self):
"""Determine whether cluster is in global debug mode
:returns: bool True if cluster is in debug mode
:returns: tuple (False, None) if cluster is not in global debug mode or
(True, "reason") if it is.
"""

View File

@ -1,56 +0,0 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
"""Database setup and migration commands."""
from oslo_config import cfg
from stevedore import driver
_IMPL = None
def get_backend():
global _IMPL
if not _IMPL:
cfg.CONF.import_opt('backend', 'oslo_db.options', group='database')
_IMPL = driver.DriverManager("astara.database.migration_backend",
cfg.CONF.database.backend).driver
return _IMPL
def upgrade(version=None):
"""Migrate the database to `version` or the most recent version."""
return get_backend().upgrade(version)
def downgrade(version=None):
return get_backend().downgrade(version)
def version():
return get_backend().version()
def stamp(version):
return get_backend().stamp(version)
def revision(message, autogenerate):
return get_backend().revision(message, autogenerate)
def create_schema():
return get_backend().create_schema()

View File

@ -1,54 +0,0 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = %(here)s/alembic
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
#sqlalchemy.url = driver://user:pass@localhost/dbname
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -1,61 +0,0 @@
# 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.
from logging import config as log_config
from alembic import context
try:
# NOTE(whaom): This is to register the DB2 alembic code which
# is an optional runtime dependency.
from ibm_db_alembic.ibm_db import IbmDbImpl # noqa
except ImportError:
pass
from astara.db.sqlalchemy import api as sqla_api
from astara.db.sqlalchemy import models
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
log_config.fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
target_metadata = models.Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
engine = sqla_api.get_engine()
with engine.connect() as connection:
context.configure(connection=connection,
target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
run_migrations_online()

View File

@ -1,22 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -1,69 +0,0 @@
# Copyright 2015 Akanda, Inc.
#
# Author: Akanda, Inc
#
# 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.
"""initial_migration
Revision ID: 4f695b725637
Revises: None
Create Date: 2015-07-02 12:29:50.243891
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '4f695b725637'
down_revision = None
def upgrade():
op.create_table(
'resource_debug',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=False),
sa.Column('reason', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_debug_resource0uuid'),
)
op.create_table(
'tenant_debug',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=False),
sa.Column('reason', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_debug_tenant0uuid'),
)
op.create_table(
'global_debug',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('status', sa.Integer(), nullable=False),
sa.Column('reason', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('status', name='uniq_global_debug0status'),
)
def downgrade():
raise NotImplementedError(('Downgrade from initial migration is'
' unsupported.'))

View File

@ -1,166 +0,0 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# Copyright 2015 Akanda, Inc.
#
# 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.
"""SQLAlchemy storage backend."""
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import session as db_session
from astara.db import api
from astara.db.sqlalchemy import models
CONF = cfg.CONF
_FACADE = None
def _create_facade_lazily():
global _FACADE
if _FACADE is None:
_FACADE = db_session.EngineFacade.from_config(CONF)
return _FACADE
def get_engine():
facade = _create_facade_lazily()
return facade.get_engine()
def get_session(**kwargs):
facade = _create_facade_lazily()
return facade.get_session(**kwargs)
def get_backend():
"""The backend is this module itself."""
return Connection()
def model_query(model, *args, **kwargs):
"""Query helper for simpler session usage.
:param session: if present, the session to use
"""
session = kwargs.get('session') or get_session()
query = session.query(model, *args)
return query
class Connection(api.Connection):
"""SqlAlchemy connection."""
def __init__(self):
pass
def _enable_debug(self, model, uuid, reason=None):
model.update({
'uuid': uuid,
'reason': reason,
})
try:
model.save()
except db_exc.DBDuplicateEntry:
pass
def _disable_debug(self, model=None, uuid=None):
query = model_query(model)
query.filter_by(uuid=uuid).delete()
def _check_debug(self, model, uuid):
query = model_query(model)
res = query.filter_by(uuid=uuid).all()
if not res:
return (False, None)
return (True, res[0].reason)
def _list_debug(self, model):
res = model_query(model).all()
return set((r.uuid, r.reason) for r in res)
def enable_resource_debug(self, resource_uuid, reason=None):
self._enable_debug(
model=models.ResourceDebug(),
uuid=resource_uuid,
reason=reason,
)
def disable_resource_debug(self, resource_uuid):
self._disable_debug(
model=models.ResourceDebug,
uuid=resource_uuid,
)
def resource_in_debug(self, resource_uuid):
return self._check_debug(models.ResourceDebug, resource_uuid)
def resources_in_debug(self):
return self._list_debug(models.ResourceDebug)
def enable_tenant_debug(self, tenant_uuid, reason=None):
self._enable_debug(
model=models.TenantDebug(),
uuid=tenant_uuid,
reason=reason,
)
def disable_tenant_debug(self, tenant_uuid):
self._disable_debug(
model=models.TenantDebug,
uuid=tenant_uuid,
)
def tenant_in_debug(self, tenant_uuid):
return self._check_debug(models.TenantDebug, tenant_uuid)
def tenants_in_debug(self):
return self._list_debug(models.TenantDebug)
def _set_global_debug(self, status, reason=None):
query = model_query(models.GlobalDebug)
res = query.first()
if not res:
gdb = models.GlobalDebug()
gdb.update({
'status': status,
'reason': reason,
})
gdb.save()
def enable_global_debug(self, reason=None):
gdb = models.GlobalDebug()
gdb.update({
'status': 1,
'reason': reason,
})
try:
gdb.save()
except db_exc.DBDuplicateEntry:
pass
def disable_global_debug(self):
query = model_query(models.GlobalDebug)
query.filter_by(status=1).delete()
def global_debug(self):
query = model_query(models.GlobalDebug)
res = query.filter_by(status=1).all()
if not res:
return (False, None)
return (True, res[0].reason)

View File

@ -1,93 +0,0 @@
# Copyright 2012 New Dream Network, LLC (DreamHost)
# Copyright 2015 Akanda, Inc.
#
# 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 os
import six
from alembic import command as alembic_command
from alembic import config as alembic_config
from alembic import util as alembic_util
from oslo_config import cfg
from astara.common.i18n import _
import pkg_resources
CONF = cfg.CONF
def do_alembic_command(config, cmd, *args, **kwargs):
try:
getattr(alembic_command, cmd)(config, *args, **kwargs)
except alembic_util.CommandError as e:
alembic_util.err(six.text_type(e))
def add_alembic_subparser(sub, cmd):
return sub.add_parser(cmd, help=getattr(alembic_command, cmd).__doc__)
def do_upgrade(config, cmd):
revision = CONF.command.revision or 'head'
do_alembic_command(config, cmd, revision, sql=CONF.command.sql)
def do_stamp(config, cmd):
do_alembic_command(config, cmd,
CONF.command.revision,
sql=CONF.command.sql)
def add_command_parsers(subparsers):
for name in ['current', 'history', 'branches']:
parser = add_alembic_subparser(subparsers, name)
parser.set_defaults(func=do_alembic_command)
parser = add_alembic_subparser(subparsers, 'upgrade')
parser.add_argument('--delta', type=int)
parser.add_argument('--sql', action='store_true')
parser.add_argument('revision', nargs='?')
parser.add_argument('--mysql-engine',
default='',
help='Change MySQL storage engine of current '
'existing tables')
parser.set_defaults(func=do_upgrade)
parser = add_alembic_subparser(subparsers, 'stamp')
parser.add_argument('--sql', action='store_true')
parser.add_argument('revision')
parser.set_defaults(func=do_stamp)
command_opt = cfg.SubCommandOpt('command',
title='Command',
help=_('Available commands'),
handler=add_command_parsers)
CONF.register_cli_opt(command_opt)
def get_alembic_config():
config = alembic_config.Config(os.path.join(os.path.dirname(__file__),
'alembic.ini'))
return config
def main():
dist = pkg_resources.get_distribution('astara')
CONF(project='astara-orchestrator', version=dist.version)
config = get_alembic_config()
config.astara_config = CONF
CONF.command.func(config, CONF.command.name)

View File

@ -1,113 +0,0 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
import os
import alembic
from alembic import config as alembic_config
import alembic.migration as alembic_migration
from oslo_db import exception as db_exc
from astara.db.sqlalchemy import api as sqla_api
from astara.db.sqlalchemy import models
def _alembic_config():
path = os.path.join(os.path.dirname(__file__), 'alembic.ini')
config = alembic_config.Config(path)
return config
def version(config=None, engine=None):
"""Current database version.
:returns: Database version
:rtype: string
"""
if engine is None:
engine = sqla_api.get_engine()
with engine.connect() as conn:
context = alembic_migration.MigrationContext.configure(conn)
return context.get_current_revision()
def upgrade(revision, config=None):
"""Used for upgrading database.
:param version: Desired database version
:type version: string
"""
revision = revision or 'head'
config = config or _alembic_config()
alembic.command.upgrade(config, revision or 'head')
def create_schema(config=None, engine=None):
"""Create database schema from models description.
Can be used for initial installation instead of upgrade('head').
"""
if engine is None:
engine = sqla_api.get_engine()
# NOTE(viktors): If we will use metadata.create_all() for non empty db
# schema, it will only add the new tables, but leave
# existing as is. So we should avoid of this situation.
if version(engine=engine) is not None:
raise db_exc.DbMigrationError("DB schema is already under version"
" control. Use upgrade() instead")
models.Base.metadata.create_all(engine)
stamp('head', config=config)
def downgrade(revision, config=None):
"""Used for downgrading database.
:param version: Desired database version
:type version: string
"""
revision = revision or 'base'
config = config or _alembic_config()
return alembic.command.downgrade(config, revision)
def stamp(revision, config=None):
"""Stamps database with provided revision.
Don't run any migrations.
:param revision: Should match one from repository or head - to stamp
database with most recent revision
:type revision: string
"""
config = config or _alembic_config()
return alembic.command.stamp(config, revision=revision)
def revision(message=None, autogenerate=False, config=None):
"""Creates template for migration.
:param message: Text that will be used for migration title
:type message: string
:param autogenerate: If True - generates diff based on current database
state
:type autogenerate: bool
"""
config = config or _alembic_config()
return alembic.command.revision(config, message=message,
autogenerate=autogenerate)

View File

@ -1,113 +0,0 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# Copyright 2015 Akanda, Inc.
#
# 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.
"""
SQLAlchemy models for baremetal data.
"""
from astara.common.i18n import _
from oslo_config import cfg
from oslo_db import options as db_options
from oslo_db.sqlalchemy import models
import six.moves.urllib.parse as urlparse
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import schema, String
from sqlalchemy.ext.declarative import declarative_base
sql_opts = [
cfg.StrOpt('mysql_engine',
default='InnoDB',
help=_('MySQL engine to use.'))
]
_DEFAULT_SQL_CONNECTION = 'sqlite:///astara.db'
cfg.CONF.register_opts(sql_opts, 'database')
db_options.set_defaults(cfg.CONF, _DEFAULT_SQL_CONNECTION, 'ironic.sqlite')
def table_args():
engine_name = urlparse.urlparse(cfg.CONF.database.connection).scheme
if engine_name == 'mysql':
return {'mysql_engine': cfg.CONF.database.mysql_engine,
'mysql_charset': "utf8"}
return None
class AstaraBase(models.TimestampMixin,
models.ModelBase):
metadata = None
def as_dict(self):
d = {}
for c in self.__table__.columns:
d[c.name] = self[c.name]
return d
def save(self, session=None):
import astara.db.sqlalchemy.api as db_api
if session is None:
session = db_api.get_session()
super(AstaraBase, self).save(session)
Base = declarative_base(cls=AstaraBase)
class ResourceDebug(Base):
"""Represents a resource in debug mode."""
__tablename__ = 'resource_debug'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_debug_resource0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
reason = Column(String(255), nullable=True)
class TenantDebug(Base):
"""Represents a tenant in debug mode."""
__tablename__ = 'tenant_debug'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_debug_tenant0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
reason = Column(String(255), nullable=True)
class GlobalDebug(Base):
"""Stores a single row that serves as a status flag for global debug"""
__tablename__ = 'global_debug'
__table_args__ = (
schema.UniqueConstraint('status', name='uniq_global_debug0status'),
table_args()
)
id = Column(Integer, primary_key=True)
status = Column(Integer)
reason = Column(String(255), nullable=True)

View File

@ -1,80 +0,0 @@
# 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.
from __future__ import print_function
import os
import sys
from oslo_config import cfg
from oslo_log import log as logging
from astara import drivers
from astara import state
from astara import worker
from astara.common import config
DEBUG_OPTS = [
cfg.StrOpt(
'router-id', required=True,
help='The UUID for the router to debug')
]
class Fake(object):
def __init__(self, crud):
self.crud = crud
def delete_callback():
print('DELETE')
def bandwidth_callback(*args, **kwargs):
print('BANDWIDTH:', args, kwargs)
def debug_one_router(args=sys.argv[1:]):
# Add our extra option for specifying the router-id to debug
cfg.CONF.register_cli_opts(DEBUG_OPTS)
cfg.CONF.set_override('boot_timeout', 60000)
cfg.CONF.import_opt('host', 'astara.main')
config.parse_config(args)
logging.setup(cfg.CONF, __name__)
log = logging.getLogger(__name__)
log.debug('Proxy settings: %r', os.getenv('no_proxy'))
context = worker.WorkerContext()
driver = drivers.get('router')(context, cfg.CONF.router_id)
a = state.Automaton(
resource=driver,
tenant_id=driver._router.tenant_id,
delete_callback=delete_callback,
bandwidth_callback=bandwidth_callback,
worker_context=context,
queue_warning_threshold=100,
reboot_error_threshold=1,
)
a.send_message(Fake('update'))
import pdb
pdb.set_trace()
a.update(context)

View File

@ -1,91 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
from oslo_config import cfg
from oslo_log import log as logging
from astara.drivers.router import Router
from astara.drivers.loadbalancer import LoadBalancer
DRIVER_OPTS = [
cfg.ListOpt('enabled_drivers',
default=['router', ],
help='list of drivers the rug process will load'),
]
cfg.CONF.register_opts(DRIVER_OPTS)
ASTARA_APP_OPTS = [
cfg.IntOpt('max_sleep', default=15,
help='The max sleep seconds between each attempt by'
' neutron client for fetching resource.'),
]
cfg.CONF.register_group(cfg.OptGroup(name='astara_appliance'))
cfg.CONF.register_opts(ASTARA_APP_OPTS, 'astara_appliance')
LOG = logging.getLogger(__name__)
AVAILABLE_DRIVERS = {
Router.RESOURCE_NAME: Router,
LoadBalancer.RESOURCE_NAME: LoadBalancer,
}
class InvalidDriverException(Exception):
"""Triggered when driver is not available in AVAILABLE_DRIVERS"""
pass
def get(requested_driver):
"""Returns driver class based on the requested_driver param
will raise InvalidDriverException if not listed in the config option
cfg.CONF.available_drivers.
:param requested_driver: name of desired driver
:return: returns driver object
"""
if requested_driver in AVAILABLE_DRIVERS:
return AVAILABLE_DRIVERS[requested_driver]
raise InvalidDriverException(
'Failed loading driver: %s' % requested_driver
)
def load_from_byonf(worker_context, byonf_result, resource_id):
""""Returns a loaded driver based on astara-neutron BYONF response
:param worker_context: Worker context with clients
:param byonf_result: dict response from neutron API describing
user-provided NF info (specifically image_uuid and
driver)
:param resource_id: The UUID of the logical resource derived from the
notification message
Responsible for also setting correct driver attributes based on BYONF
specs.
"""
driver_obj = get(byonf_result['driver'])(worker_context, resource_id)
if byonf_result.get('image_uuid'):
driver_obj.image_uuid = byonf_result['image_uuid']
return driver_obj
def enabled_drivers():
for driver in cfg.CONF.enabled_drivers:
try:
d = get(driver)
yield d
except InvalidDriverException as e:
LOG.exception(e)
pass

View File

@ -1,194 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
from oslo_log import log as logging
class BaseDriver(object):
RESOURCE_NAME = 'BaseDriver'
def __init__(self, worker_context, id, log=None):
"""This is the abstract for rug drivers.
:param id: logical resource id
:param log: override default log
"""
self.id = id
self.external_port = None
self.details = []
self.flavor = None
self.image_uuid = None
self.name = 'ak-%s-%s' % (self.RESOURCE_NAME, self.id)
if log:
self.log = log
else:
self.log = logging.getLogger(self.name)
self.post_init(worker_context)
def post_init(self, worker_context):
"""post init hook
:param worker_context:
:returns: None
"""
pass
def pre_boot(self, worker_context):
"""pre boot hook
:param worker_context:
:returns: None
"""
pass
def post_boot(self, worker_context):
"""post boot hook
:param worker_context:
:returns: None
"""
pass
def update_state(self, worker_context, silent=False):
"""returns state of logical resource.
:param worker_context:
:param silent:
:returns: None
"""
pass
def build_config(self, worker_context, mgt_port, iface_map):
"""gets config of logical resource attached to worker_context.
:param worker_context:
:returns: None
"""
pass
def update_config(self, management_address, config):
"""Updates appliance configuration
This is responsible for pushing configuration to the managed
appliance
"""
pass
def synchronize_state(self, worker_context, state):
"""sometimes a driver will need to update a service behind it with a
new state.
:param state: a valid state
"""
pass
def make_ports(self, worker_context):
"""Make ports call back for the nova client.
This is expected to create the management port for the instance
and any required instance ports.
:param worker_context:
:returns: A tuple (management_port, [instance_ports])
"""
def _make_ports():
pass
return _make_ports
def delete_ports(self, worker_context):
"""Delete all created ports.
:param worker_context:
:returns: None
"""
@staticmethod
def pre_populate_hook():
"""called in populate.py during driver loading loop.
"""
pass
def pre_plug(self, worker_context):
"""pre-plug hook
:param worker_context:
:returns: None
"""
@staticmethod
def get_resource_id_for_tenant(worker_context, tenant_id, message):
"""Find the id of a resource for a given tenant id and message.
For some resources simply searching by tenant_id is enough, for
others some context from the message payload may be necessary.
:param worker_context: A worker context with instantiated clients
:param tenant_id: The tenant uuid to search for
:param message: The message associated with the request
:returns: uuid of the resource owned by the tenant
"""
pass
@staticmethod
def process_notification(tenant_id, event_type, payload):
"""Process an incoming notification event
This gets called from the notifications layer to determine whether
a driver should process an incoming notification event. It is
responsible for translating an incoming notification to an Event
object appropriate for that driver.
:param tenant_id: str The UUID tenant_id for the incoming event
:param event_type: str event type, for example router.create.end
:param payload: The payload body of the incoming event
:returns: A populated Event object if it should process, or None if not
"""
pass
@property
def ports(self):
"""Lists ports associated with the resource.
:returns: A list of astara.api.neutron.Port objects or []
"""
def get_interfaces(self, management_address):
"""Lists interfaces attached to the resource.
This lists the interfaces attached to the resource from the POV
of the resource itself.
:returns: A list of interfaces
"""
pass
def is_alive(self, management_address):
"""Determines whether the managed resource is alive
:returns: bool True if alive, False if not
"""
def get_state(self, worker_context):
"""Returns the state of the managed resource"""
@property
def is_ha(self):
"""Returns True if logical resource is set to be highly-available"""
return False

View File

@ -1,352 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
import time
from oslo_config import cfg
from oslo_log import log as logging
from neutronclient.common import exceptions as q_exceptions
from astara.common.i18n import _
from astara.api import astara_client
from astara.api.config import loadbalancer as config
from astara import event
from astara.api import neutron
from astara.drivers.base import BaseDriver
from astara.drivers import states
from astara.common.i18n import _LW
LOG = logging.getLogger(__name__)
LOADBALANCER_OPTS = [
cfg.StrOpt('image_uuid',
help='The image_uuid for loadbalancer instances.'),
cfg.StrOpt('instance_flavor',
help='The nova flavor id to use for loadbalancer instances'),
cfg.IntOpt('mgt_service_port', default=5000,
help='The port on which the loadbalancer API service listens '
'on loadbalancer appliances'),
]
cfg.CONF.register_group(cfg.OptGroup(name='loadbalancer'))
cfg.CONF.register_opts(LOADBALANCER_OPTS, 'loadbalancer')
STATUS_MAP = {
states.DOWN: neutron.PLUGIN_DOWN,
states.BOOTING: neutron.PLUGIN_PENDING_CREATE,
states.UP: neutron.PLUGIN_PENDING_CREATE,
states.CONFIGURED: neutron.PLUGIN_ACTIVE,
states.ERROR: neutron.PLUGIN_ERROR,
states.REPLUG: neutron.PLUGIN_PENDING_UPDATE,
}
class LoadBalancer(BaseDriver):
RESOURCE_NAME = 'loadbalancer'
_last_synced_status = None
def post_init(self, worker_context):
"""Called at end of __init__ in BaseDriver.
Populates the details object from neutron and sets image_uuid and
flavor from cfg.
:param worker_context:
"""
self.image_uuid = cfg.CONF.loadbalancer.image_uuid
self.flavor = cfg.CONF.loadbalancer.instance_flavor
self.mgt_port = cfg.CONF.loadbalancer.mgt_service_port
self._ensure_cache(worker_context)
def _ensure_cache(self, worker_context):
try:
lb = worker_context.neutron.get_loadbalancer_detail(self.id)
self._loadbalancer = lb
except neutron.LoadBalancerGone:
self._loadbalancer = None
@property
def ports(self):
"""Lists ports associated with the resource.
:returns: A list of astara.api.neutron.Port objects or []
"""
if self._loadbalancer:
return [p for p in self._loadbalancer.ports]
else:
return []
def pre_boot(self, worker_context):
"""pre boot hook
Calls self.pre_plug().
:param worker_context:
:returns: None
"""
pass
def post_boot(self, worker_context):
"""post boot hook
:param worker_context:
:returns: None
"""
pass
def build_config(self, worker_context, mgt_port, iface_map):
"""Builds / rebuilds config
:param worker_context:
:param mgt_port:
:param iface_map:
:returns: configuration object
"""
self._ensure_cache(worker_context)
return config.build_config(
worker_context.neutron,
self._loadbalancer,
mgt_port,
iface_map)
def update_config(self, management_address, config):
"""Updates appliance configuration
This is responsible for pushing configuration to the managed
appliance
"""
self.log.info(_('Updating config for %s'), self.name)
astara_client.update_config(management_address, self.mgt_port, config)
def pre_plug(self, worker_context):
"""pre-plug hook
Sets up the external port.
:param worker_context:
:returs: None
"""
def make_ports(self, worker_context):
"""make ports call back for the nova client.
:param worker_context:
:returns: A tuple (managment_port, [instance_ports])
"""
def _make_ports():
self._ensure_cache(worker_context)
mgt_port = worker_context.neutron.create_management_port(
self.id
)
# allocate a port on the same net as the LB VIP
lb_port = worker_context.neutron.create_vrrp_port(
object_id=self.id,
network_id=self._loadbalancer.vip_port.network_id,
label='LB',
)
return mgt_port, [lb_port]
return _make_ports
def delete_ports(self, worker_context):
"""Delete all ports.
:param worker_context:
:returns: None
"""
worker_context.neutron.delete_vrrp_port(self.id, label='LB')
worker_context.neutron.delete_vrrp_port(self.id, label='MGT')
@staticmethod
def pre_populate_hook():
"""Fetch the existing LBs from neutron then and returns list back
to populate to be distributed to workers.
Wait for neutron to return the list of the existing LBs.
Pause up to max_sleep seconds between each attempt and ignore
neutron client exceptions.
"""
nap_time = 1
neutron_client = neutron.Neutron(cfg.CONF)
while True:
try:
resources = []
for lb in neutron_client.get_loadbalancers():
resources.append(
event.Resource(driver=LoadBalancer.RESOURCE_NAME,
id=lb.id,
tenant_id=lb.tenant_id))
return resources
except (q_exceptions.Unauthorized, q_exceptions.Forbidden) as err:
LOG.warning(_LW('PrePopulateWorkers thread failed: %s'), err)
return
except Exception as err:
LOG.warning(
_LW('Could not fetch loadbalancers from neutron: %s'), err)
LOG.warning(_LW(
'sleeping %s seconds before retrying'), nap_time)
time.sleep(nap_time)
nap_time = min(nap_time * 2,
cfg.CONF.astara_appliance.max_sleep)
@staticmethod
def get_resource_id_for_tenant(worker_context, tenant_id, message):
"""Find the id of the loadbalancer owned by tenant
Some events (ie, member.create.end) give us no context about which
LB the event is associated and only show us the tenant_id and member
id, so we for those we need to some resolution here.
:param tenant_id: The tenant uuid to search for
:param message: Message associated /w the request
:returns: uuid of the loadbalancer owned by the tenant
"""
lb_id = None
# loadbalancer.create.end contains the id in the payload
if message.body.get('loadbalancer'):
lb_id = message.body['loadbalancer'].get('id')
# listener.create.end references the loadbalancer directly
elif message.body.get('listener'):
lb_id = message.body['listener'].get('loadbalancer_id')
# pool.create.end references by listener
elif message.body.get('pool'):
listener_id = message.body['pool'].get('listener_id')
if listener_id:
lb = worker_context.neutron.get_loadbalancer_by_listener(
listener_id, tenant_id)
if lb:
lb_id = lb.id
# member.crate.end only gives us the member id itself.
elif message.body.get('member') or message.body.get('member_id'):
member_id = (message.body.get('member', {}).get('id') or
message.body.get('member_id'))
if member_id:
lb = worker_context.neutron.get_loadbalancer_by_member(
member_id=member_id, tenant_id=tenant_id)
if lb:
lb_id = lb.id
return lb_id
@staticmethod
def process_notification(tenant_id, event_type, payload):
"""Process an incoming notification event
This gets called from the notifications layer to determine whether
this driver should process an incoming notification event. It is
responsible for translating an incoming notificatino to an Event
object appropriate for this driver.
:param tenant_id: str The UUID tenant_id for the incoming event
:param event_type: str event type, for example loadbalancer.create.end
:param payload: The payload body of the incoming event
:returns: A populated Event objet if it should process, or None if not
"""
if event_type.startswith('loadbalancerstatus.update'):
# these are generated when we sync state
return
lb_id = (
payload.get('loadbalancer', {}).get('id') or
payload.get('listener', {}).get('loadbalancer_id') or
payload.get('loadbalancer_id')
)
update_notifications = [
'listener.create.start',
'pool.create.start',
'member.create.end',
'member.delete.end',
]
# some events do not contain a lb id.
if not lb_id and event_type not in update_notifications:
return
if event_type == 'loadbalancer.create.end':
crud = event.CREATE
elif event_type == 'loadbalancer.delete.end':
crud = event.DELETE
elif event_type in update_notifications:
crud = event.UPDATE
else:
crud = None
if not crud:
LOG.info('Could not determine CRUD for event: %s ', event_type)
return
resource = event.Resource(driver=LoadBalancer.RESOURCE_NAME,
id=lb_id,
tenant_id=tenant_id)
e = event.Event(
resource=resource,
crud=crud,
body=payload,
)
return e
def get_state(self, worker_context):
self._ensure_cache(worker_context)
if not self._loadbalancer:
return states.GONE
else:
# NOTE(adam_g): We probably want to map this status back to
# an internal astara status
return self._loadbalancer.status
def synchronize_state(self, worker_context, state):
self._ensure_cache(worker_context)
if not self._loadbalancer:
LOG.debug('Not synchronizing state with missing loadbalancer %s',
self.id)
return
new_status = STATUS_MAP.get(state)
old_status = self._last_synced_status
LOG.debug('Synchronizing loadbalancer %s state %s->%s',
self.id, old_status, new_status)
worker_context.neutron.update_loadbalancer_status(
self.id, new_status)
self._last_synced_status = new_status
def get_interfaces(self, management_address):
"""Lists interfaces attached to the resource.
This lists the interfaces attached to the resource from the POV
of the resource iteslf.
:returns: A list of interfaces
"""
return astara_client.get_interfaces(management_address,
self.mgt_port)
def is_alive(self, management_address):
"""Determines whether the managed resource is alive
:returns: bool True if alive, False if not
"""
return astara_client.is_alive(management_address, self.mgt_port)

View File

@ -1,368 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
import time
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from neutronclient.common import exceptions as q_exceptions
from astara.common.i18n import _
from astara.api import astara_client
from astara.api.config import router as configuration
from astara import event
from astara.api import neutron
from astara.drivers.base import BaseDriver
from astara.drivers import states
from astara.common.i18n import _LW
LOG = logging.getLogger(__name__)
ROUTER_OPTS = [
cfg.StrOpt('image_uuid',
help='The image_uuid for router instances.',
deprecated_opts=[
cfg.DeprecatedOpt('router_image_uuid',
group='DEFAULT')]),
cfg.StrOpt('instance_flavor',
help='The nova id flavor to use for router instances',
deprecated_opts=[
cfg.DeprecatedOpt('router_instance_flavor',
group='DEFAULT')]),
cfg.IntOpt('mgt_service_port', default=5000,
help='The port on which the router API service listens on '
'router appliances',
deprecated_opts=[
cfg.DeprecatedOpt('akanda_mgt_service_port',
group='DEFAULT')]),
cfg.BoolOpt('ipsec_vpn', default=False,
help='Enable ipsec vpn support'),
]
cfg.CONF.register_group(cfg.OptGroup(name='router'))
cfg.CONF.register_opts(ROUTER_OPTS, 'router')
STATUS_MAP = {
states.DOWN: neutron.STATUS_DOWN,
states.BOOTING: neutron.STATUS_BUILD,
states.UP: neutron.STATUS_BUILD,
states.CONFIGURED: neutron.STATUS_ACTIVE,
states.ERROR: neutron.STATUS_ERROR,
states.DEGRADED: neutron.STATUS_BUILD,
}
_ROUTER_INTERFACE_NOTIFICATIONS = set([
'router.interface.create',
'router.interface.delete',
])
_ROUTER_INTERESTING_NOTIFICATIONS = set([
'router.change.end',
'subnet.create.end',
'subnet.change.end',
'subnet.delete.end',
'port.create.end',
'port.change.end',
'port.delete.end',
'floatingip.create.end',
'floatingip.update.end',
# NOTE(adam_g): Not certain this floatingip.change.* is ever broadcast?
'floatingip.change.end',
'floatingip.delete.end'
])
_VPN_NOTIFICATIONS = set([
'vpnservice.change.end',
'vpnservice.delete.end',
'ipsec_site_connection.create.end',
'ipsec_site_connection.change.end',
'ipsec_site_connection.delete.end',
'ikepolicy.change.end',
'ipsecpolicy.change.end'
])
DRIVER_NAME = 'router'
class Router(BaseDriver):
RESOURCE_NAME = DRIVER_NAME
_last_synced_status = None
def post_init(self, worker_context):
"""Called at end of __init__ in BaseDriver.
Populates the _router object from neutron and sets image_uuid and
flavor from cfg.
:param worker_context:
"""
self.image_uuid = cfg.CONF.router.image_uuid
self.flavor = cfg.CONF.router.instance_flavor
self.mgt_port = cfg.CONF.router.mgt_service_port
self._ensure_cache(worker_context)
def _ensure_cache(self, worker_context):
try:
self._router = worker_context.neutron.get_router_detail(self.id)
except neutron.RouterGone:
self._router = None
@property
def ports(self):
"""Lists ports associated with the resource.
:returns: A list of astara.api.neutron.Port objects or []
"""
if self._router:
return [p for p in self._router.ports]
else:
return []
def pre_boot(self, worker_context):
"""pre boot hook
Calls self.pre_plug().
:param worker_context:
:returns: None
"""
self.pre_plug(worker_context)
def post_boot(self, worker_context):
"""post boot hook
:param worker_context:
:returns: None
"""
pass
def build_config(self, worker_context, mgt_port, iface_map):
"""Builds / rebuilds config
:param worker_context:
:param mgt_port:
:param iface_map:
:returns: configuration object
"""
self._ensure_cache(worker_context)
return configuration.build_config(
worker_context,
self._router,
mgt_port,
iface_map
)
def update_config(self, management_address, config):
"""Updates appliance configuration
This is responsible for pushing configuration to the managed
appliance
"""
self.log.info(_('Updating config for %s'), self.name)
start_time = timeutils.utcnow()
astara_client.update_config(
management_address, self.mgt_port, config)
delta = timeutils.delta_seconds(start_time, timeutils.utcnow())
self.log.info(_('Config updated for %s after %s seconds'),
self.name, round(delta, 2))
def make_ports(self, worker_context):
"""make ports call back for the nova client.
:param worker_context:
:returns: A tuple (managment_port, [instance_ports])
"""
def _make_ports():
self._ensure_cache(worker_context)
mgt_port = worker_context.neutron.create_management_port(
self.id
)
# FIXME(mark): ideally this should be ordered and de-duped
instance_ports = [
worker_context.neutron.create_vrrp_port(self.id, n)
for n in (p.network_id for p in self._router.ports)
]
return mgt_port, instance_ports
return _make_ports
def delete_ports(self, worker_context):
"""Delete all ports.
:param worker_context:
:returns: None
"""
worker_context.neutron.delete_vrrp_port(self.id)
worker_context.neutron.delete_vrrp_port(self.id, label='MGT')
@staticmethod
def pre_populate_hook():
"""Fetch the existing routers from neutrom then and returns list back
to populate to be distributed to workers.
Wait for neutron to return the list of the existing routers.
Pause up to max_sleep seconds between each attempt and ignore
neutron client exceptions.
"""
nap_time = 1
neutron_client = neutron.Neutron(cfg.CONF)
while True:
try:
neutron_routers = neutron_client.get_routers(detailed=False)
resources = []
for router in neutron_routers:
resources.append(
event.Resource(driver=DRIVER_NAME,
id=router.id,
tenant_id=router.tenant_id)
)
return resources
except (q_exceptions.Unauthorized, q_exceptions.Forbidden) as err:
LOG.warning(_LW('PrePopulateWorkers thread failed: %s'), err)
return
except Exception as err:
LOG.warning(
_LW('Could not fetch routers from neutron: %s'), err)
LOG.warning(_LW(
'sleeping %s seconds before retrying'), nap_time)
time.sleep(nap_time)
nap_time = min(nap_time * 2,
cfg.CONF.astara_appliance.max_sleep)
@staticmethod
def get_resource_id_for_tenant(worker_context, tenant_id, message):
"""Find the id of the router owned by tenant
:param tenant_id: The tenant uuid to search for
:param message: message associated /w request (unused here)
:returns: uuid of the router owned by the tenant
"""
router = worker_context.neutron.get_router_for_tenant(tenant_id)
if not router:
LOG.debug('Router not found for tenant %s.',
tenant_id)
return None
return router.id
@staticmethod
def process_notification(tenant_id, event_type, payload):
"""Process an incoming notification event
This gets called from the notifications layer to determine whether
this driver should process an incoming notification event. It is
responsible for translating an incoming notificatino to an Event
object appropriate for this driver.
:param tenant_id: str The UUID tenant_id for the incoming event
:param event_type: str event type, for example router.create.end
:param payload: The payload body of the incoming event
:returns: A populated Event objet if it should process, or None if not
"""
router_id = payload.get('router', {}).get('id')
crud = event.UPDATE
if event_type.startswith('routerstatus.update'):
# We generate these events ourself, so ignore them.
return
if event_type == 'router.create.end':
crud = event.CREATE
elif event_type == 'router.delete.end':
crud = event.DELETE
router_id = payload.get('router_id')
elif event_type in _ROUTER_INTERFACE_NOTIFICATIONS:
crud = event.UPDATE
router_id = payload.get('router.interface', {}).get('id')
elif event_type in _ROUTER_INTERESTING_NOTIFICATIONS:
crud = event.UPDATE
elif cfg.CONF.router.ipsec_vpn and event_type in _VPN_NOTIFICATIONS:
crud = event.UPDATE
else:
LOG.debug('Not processing event: %s' % event_type)
return
resource = event.Resource(driver=DRIVER_NAME,
id=router_id,
tenant_id=tenant_id)
e = event.Event(
resource=resource,
crud=crud,
body=payload,
)
return e
def get_state(self, worker_context):
self._ensure_cache(worker_context)
if not self._router:
return states.GONE
else:
# NOTE(adam_g): We probably want to map this status back to
# an internal astara status
return self._router.status
def synchronize_state(self, worker_context, state):
self._ensure_cache(worker_context)
if not self._router:
LOG.debug('Not synchronizing state with missing router %s',
self.id)
return
new_status = STATUS_MAP.get(state)
old_status = self._last_synced_status
if not old_status or old_status != new_status:
LOG.debug('Synchronizing router %s state %s->%s',
self.id, old_status, new_status)
worker_context.neutron.update_router_status(self.id, new_status)
self._last_synced_status = new_status
def get_interfaces(self, management_address):
"""Lists interfaces attached to the resource.
This lists the interfaces attached to the resource from the POV
of the resource iteslf.
:returns: A list of interfaces
"""
return astara_client.get_interfaces(management_address,
self.mgt_port)
def is_alive(self, management_address):
"""Determines whether the managed resource is alive
:returns: bool True if alive, False if not
"""
return astara_client.is_alive(management_address, self.mgt_port)
@property
def is_ha(self):
"""Returns True if logical resource is set to be highly-available"""
if not self._router:
return False
return self._router.ha

View File

@ -1,30 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
"""Reference states.
Each driver maps these to which ever neutron or other
services state.
"""
DOWN = 'down'
BOOTING = 'booting'
UP = 'up'
CONFIGURED = 'configured'
RESTART = 'restart'
REPLUG = 'replug'
GONE = 'gone'
ERROR = 'error'
DEGRADED = 'degraded'
# base list of ready states, driver can use its own list.
READY_STATES = (UP, CONFIGURED, DEGRADED)

View File

@ -1,98 +0,0 @@
# 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.
# CRUD operations tracked in Event.crud
CREATE = 'create'
READ = 'read'
UPDATE = 'update'
DELETE = 'delete'
POLL = 'poll'
COMMAND = 'command' # an external command to be processed
REBUILD = 'rebuild'
REBALANCE = 'rebalance'
CLUSTER_REBUILD = 'cluster_rebuild'
class Event(object):
"""Rug Event object
Events are constructed from incoming messages accepted by the Rug.
They are responsible for holding the message payload (body), the
correpsonding CRUD operation and the logical resource that the
event affects.
"""
def __init__(self, resource, crud, body):
"""
:param resource: Resource instance holding context about the logical
resource that is affected by the Event.
:param crud: CRUD operation that is to be completed by the
correpsonding state machine when it is delivered.
:param body: The original message payload dict.
"""
self.resource = resource
self.crud = crud
self.body = body
def __eq__(self, other):
if not type(self) == type(other):
return False
for k, v in vars(self).items():
if k not in vars(other):
return False
if vars(other)[k] != v:
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return '<%s (resource=%s, crud=%s, body=%s)>' % (
self.__class__.__name__,
self.resource,
self.crud,
self.body)
class Resource(object):
"""Rug Resource object
A Resource object represents one instance of a logical resource
that is to be managed by the rug (ie, a router).
"""
def __init__(self, driver, id, tenant_id):
"""
:param driver: str name of the driver that corresponds to the resource
type.
:param id: ID of the resource (ie, the Neutron router's UUID).
:param tenant_id: The UUID of the tenant that owns this resource.
"""
self.driver = driver
self.id = id
self.tenant_id = 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 __repr__(self):
return '<%s (driver=%s, id=%s, tenant_id=%s)>' % (
self.__class__.__name__,
self.driver,
self.id,
self.tenant_id)

View File

@ -1,87 +0,0 @@
# 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.
"""Periodic health check code.
"""
import threading
import time
from oslo_config import cfg
from astara import event
from astara.api import neutron
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
HEALTH_INSPECTOR_OPTS = [
cfg.IntOpt('health_check_period',
default=60,
help='seconds between health checks'),
]
CONF.register_opts(HEALTH_INSPECTOR_OPTS)
def _health_inspector(scheduler):
"""Runs in the thread.
"""
period = CONF.health_check_period
while True:
time.sleep(period)
LOG.debug('waking up')
r = event.Resource(
id='*',
tenant_id='*',
driver='*',
)
e = event.Event(
resource=r,
crud=event.POLL,
body={},
)
scheduler.handle_message('*', e)
def start_inspector(period, scheduler):
"""Start a health check thread.
"""
t = threading.Thread(
target=_health_inspector,
args=(scheduler,),
name='HealthInspector',
)
t.setDaemon(True)
t.start()
return t
def start_reporter():
"""Start a agent report thread.
"""
reporter = neutron.NeutronAgentReporter()
t = threading.Thread(
target=reporter.report_forever,
args=(),
name='AgentReporter',
)
t.setDaemon(True)
t.start()
return t

View File

@ -1,824 +0,0 @@
# 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.
from datetime import datetime
from functools import wraps
import time
import six
from oslo_config import cfg
from astara.drivers import states
from astara.common.i18n import _LE, _LI
from astara.common import container
CONF = cfg.CONF
INSTANCE_MANAGER_OPTS = [
cfg.IntOpt(
'hotplug_timeout',
default=10,
help='The amount of time to wait for nova to hotplug/unplug '
'networks from the instances.'),
cfg.IntOpt(
'boot_timeout', default=600),
cfg.IntOpt(
'error_state_cooldown',
default=30,
help='Number of seconds to ignore new events when an instance goes '
'into ERROR state.',
),
]
CONF.register_opts(INSTANCE_MANAGER_OPTS)
def _generate_interface_map(instance, interfaces):
# TODO(mark): We're in the first phase of VRRP, so we need
# map the interface to the network ID.
# Eventually we'll send VRRP data and real interface data
port_mac_to_net = {
p.mac_address: p.network_id
for p in instance.ports
}
# Add in the management port
mgt_port = instance.management_port
port_mac_to_net[mgt_port.mac_address] = mgt_port.network_id
# this is a network to logical interface id
return {
port_mac_to_net[i['lladdr']]: i['ifname']
for i in interfaces if i['lladdr'] in port_mac_to_net
}
def synchronize_driver_state(f):
"""Wrapper that triggers a driver's synchronize_state function"""
def wrapper(self, *args, **kw):
state = f(self, *args, **kw)
self.resource.synchronize_state(*args, state=state)
return state
return wrapper
def ensure_cache(f):
"""Decorator to wrap around any function that uses self.instance_info.
Ensures that self.instance_info is up to date and catches instances in a
GONE or missing state before wasting cycles trying to do something with it.
NOTE: This replaces the old function called _ensure_cache made a Decorator
rather than calling it explicitly at the start of all those functions.
"""
@wraps(f)
def wrapper(self, worker_context, *args, **kw):
self.instances.refresh(worker_context)
instances = worker_context.nova_client.get_instances_for_obj(
self.resource.name)
for inst_info in instances:
self.instances[inst_info.id_] = inst_info
self.instances.update_ports(worker_context)
return f(self, worker_context, *args, **kw)
return wrapper
class BootAttemptCounter(object):
def __init__(self):
self._attempts = 0
def start(self):
self._attempts += 1
def reset(self):
self._attempts = 0
@property
def count(self):
return self._attempts
class InstanceGroupManager(container.ResourceContainer):
def __init__(self, log, resource):
super(InstanceGroupManager, self).__init__()
self.log = log
self.resource = resource
self._alive = set()
@property
def instances(self):
"""Returns the managed instances sorted by name"""
return sorted(self.resources.values(), key=lambda i: i.name)
def validate_ports(self):
"""Checks whether instance have management ports attached
:returns: tuple containing two lists:
(instances that have ports, instances that don't)
"""
has_ports = set()
for inst_info in set(self.resources.values()):
if inst_info.management_address:
has_ports.add(inst_info)
return has_ports, set(self.resources.values()) - has_ports
def are_alive(self):
"""Calls the check_check function all instances to ensure liveliness
:returns: tuple containing two lists (alive_instances, dead_instances)
"""
alive = set()
for i in six.moves.range(cfg.CONF.max_retries):
for inst_info in set(self.instances) - alive:
if (inst_info.management_address and
self.resource.is_alive(inst_info.management_address)):
self.log.debug(
'Instance %s found alive after %s of %s attempts',
inst_info.id_, i, cfg.CONF.max_retries)
alive.add(inst_info)
else:
self.log.debug(
'Alive check failed for instance %s. Attempt %d of %d',
inst_info.id_, i, cfg.CONF.max_retries)
# all managed instances report alive
if alive == set(self.instances):
self._alive = [i.id_ for i in alive]
return alive, []
# zero managed instances report alive
if not alive:
self.log.debug(
'Alive check failed for all instances after %s attempts.',
cfg.CONF.max_retries)
return [], self.instances
dead = set(self.resources.values()) - alive
self._alive = [i.id_ for i in alive - dead]
return list(alive), list(dead)
def update_ports(self, worker_context):
"""Refresh ports on all managed instance info objects"""
for instance_info in self.instances:
if not instance_info:
continue
(
instance_info.management_port,
instance_info.ports
) = worker_context.neutron.get_ports_for_instance(
instance_info.id_
)
def get_interfaces(self):
"""Obtain a list of interfaces from each managed instance
Skips any instance that has not already been verified as being alive.
:returns: dict of {instance: [interfaces_dict]}
"""
interfaces = {}
for inst in self.instances:
if inst.id_ not in self._alive:
self.log.debug(
'Skipping interfaces on query on instance %s that '
'is not yet alive.', inst.id_)
continue
else:
interfaces[inst] = self.resource.get_interfaces(
inst.management_address)
return interfaces
def verify_interfaces(self, ports):
"""Verify at least one instance in group has correct ports plugged"""
for inst, interfaces in self.get_interfaces().items():
actual_macs = set((iface['lladdr'] for iface in interfaces))
self.log.debug(
'MACs found on %s: %s', inst.id_,
', '.join(sorted(actual_macs)))
if not all(
getattr(p, 'mac_address', None) for p in ports
):
return False
num_instance_ports = len(list(inst.ports))
num_logical_ports = len(list(ports))
if num_logical_ports != num_instance_ports:
self.log.debug(
'Expected %s instance ports but found %s',
num_logical_ports, num_instance_ports)
return False
expected_macs = set(p.mac_address
for p in inst.ports)
expected_macs.add(inst.management_port.mac_address)
self.log.debug(
'MACs expected on: %s, %s',
inst.id_, ', '.join(sorted(expected_macs)))
if actual_macs == expected_macs:
self.log.debug('Found all expected MACs on %s', inst.id_)
return True
self.log.debug(
'Did not find all expected MACs on instance %s, '
'actual MACs: %s', inst.id_, ', '.join(actual_macs))
return False
def _update_config(self, instance, config):
self.log.debug(
'Updating config for instance %s on resource %s',
instance.id_, self.resource.id)
self.log.debug('New config: %r', config)
attempts = cfg.CONF.max_retries
for i in six.moves.range(attempts):
try:
self.resource.update_config(
instance.management_address,
config)
except Exception:
if i == attempts - 1:
# Only log the traceback if we encounter it many times.
self.log.exception(_LE('failed to update config'))
else:
self.log.debug(
'failed to update config, attempt %d',
i
)
time.sleep(cfg.CONF.retry_delay)
else:
self.log.info('Instance config updated')
return True
else:
return False
def _ha_config(self, instance):
"""Builds configuration describing the HA cluster
This informs the instance about any configuration relating to the HA
cluster it should be joining. ATM this is primarily used to inform
an instance about the management addresses of its peers.
:param instance: InstanceInfo object
:returns: dict of HA configuration
"""
peers = [
i.management_address for i in self.instances
if i.management_address != instance.management_address]
# determine cluster priority by instance age. the older instance
# gets the higher priority
sorted_by_age = sorted(
self.instances, key=lambda i: i.time_since_boot,
reverse=True)
if sorted_by_age.index(instance) == 0:
priority = 100
else:
priority = 50
return {
'peers': peers,
'priority': priority,
}
def configure(self, worker_context):
# XXX config update can be dispatched to threads to speed
# things up across multiple instances
failed = []
# get_interfaces() return returns only instances that are up and ready
# for config
instances_interfaces = self.get_interfaces()
for inst, interfaces in instances_interfaces.items():
# sending all the standard config over to the driver for
# final updates
config = self.resource.build_config(
worker_context,
inst.management_port,
_generate_interface_map(inst, interfaces)
)
# while drivers are free to express their own ha config
# requirements, the instance manager is the only one with
# high level view of the cluster, ie knowledge of membership
if self.resource.is_ha:
config['ha_config'] = config.get('ha') or {}
config['ha_config'].update(self._ha_config(inst))
self.log.debug(
'preparing to update config for instance %s on %s resource '
'to %r', inst.id_, self.resource.RESOURCE_NAME, config)
if self._update_config(inst, config) is not True:
failed.append(inst)
if set(failed) == set(self.instances):
# all updates have failed
self.log.error(
'Could not update config for any instances on %s resource %s, '
'marking resource state %s',
self.resource.id, self.resource.RESOURCE_NAME, states.RESTART)
return states.RESTART
elif failed:
# some updates to instances we thought to be alive have failed
self.log.error(
'Could not update config for some instances on %s '
'resource %s marking %s resource state',
self.resource.RESOURCE_NAME, self.resource.id, states.DEGRADED)
return states.DEGRADED
elif len(instances_interfaces.keys()) != len(self.instances):
# instance_interfaces contains only instances that are alive
# if we're still waiting on instances, remain degraded
self.log.debug(
'Config updated on %s of %s instances',
len(instances_interfaces.keys()), len(self.instances))
return states.DEGRADED
else:
self.log.debug(
'Config updated across all instances on %s resource %s',
self.resource.RESOURCE_NAME, self.resource.id)
return states.CONFIGURED
def delete(self, instance):
"""Removes nova server reference from manager"""
del self.resources[instance.id_]
def refresh(self, worker_context):
"""Update nova server reference for all managed instances"""
for i in self.instances:
if not worker_context.nova_client.update_instance_info(i):
self.delete(i)
def destroy(self, worker_context):
"""Destroys all nova instances and blocks until deletion"""
worker_context.nova_client.delete_instances_and_wait(
self.instances)
def remove(self, worker_context, instance):
"""Destroys the nova instance, removes instance from group manager"""
worker_context.nova_client.destroy_instance(instance)
self.delete(instance)
@property
def next_instance_index(self):
ids = [
int(i.name.split('_')[1]) for i in
self.instances]
try:
return max(ids) + 1
except ValueError:
return 0
def create(self, worker_context):
to_boot = self.required_instance_count - len(self.instances)
self.log.debug(
'Booting an additional %s instance(s) for resource %s',
to_boot, self.resource.id)
for i in six.moves.range(to_boot):
name = '%s_%s' % (self.resource.name, self.next_instance_index)
instance = worker_context.nova_client.boot_instance(
resource_type=self.resource.RESOURCE_NAME,
prev_instance_info=None,
name=name,
image_uuid=self.resource.image_uuid,
flavor=self.resource.flavor,
make_ports_callback=self.resource.make_ports(worker_context)
)
self.add_instance(instance)
@property
def required_instance_count(self):
if self.resource.is_ha is True:
return 2
else:
return 1
@property
def instance_count(self):
return len(self.instances)
@property
def cluster_degraded(self):
return self.instance_count < self.required_instance_count
def add_instance(self, instance):
"""Adds a new instance or updates existing"""
self.resources[instance.id_] = instance
class InstanceManager(object):
def __init__(self, resource, worker_context):
"""The instance manager is your interface to the running instance.
wether it be virtual, container or physical.
Service specific code lives in the driver which is passed in here.
:param resource: An driver instance for the managed resource
:param resource_id: UUID of logical resource
:param worker_context:
"""
self.resource = resource
self.log = self.resource.log
self.state = states.DOWN
self.instance_info = None
self.instances = InstanceGroupManager(self.log, self.resource)
self.last_error = None
self._boot_counter = BootAttemptCounter()
self._boot_logged = []
self._last_synced_status = None
self.state = self.update_state(worker_context, silent=True)
@property
def attempts(self):
"""Property which returns the boot count.
:returns Int:
"""
return self._boot_counter.count
def reset_boot_counter(self):
"""Resets the boot counter.
:returns None:
"""
self._boot_counter.reset()
@synchronize_driver_state
@ensure_cache
def update_state(self, worker_context, silent=False):
"""Updates state of the instance and, by extension, its logical resource
:param worker_context:
:param silent:
:returns: state
"""
if self.resource.get_state(worker_context) == states.GONE:
self.log.debug('%s driver reported its state is %s',
self.resource.RESOURCE_NAME, states.GONE)
self.state = states.GONE
return self.state
if not self.instances:
self.log.info(_LI('no backing instance(s), marking as %s'),
states.DOWN)
self.state = states.DOWN
return self.state
elif self.instances.cluster_degraded is True:
self.log.info(_LI(
'instance cluster for resource %s reports degraded'),
self.resource.id)
self.state = states.DEGRADED
return self.state
has_ports, no_ports = self.instances.validate_ports()
# ports_state=None means no instances have ports
if not has_ports:
self.log.debug('waiting for instance ports to be attached')
self.state = states.BOOTING
return self.state
# XXX TODO need to account for when only a subset of the cluster have
# correct ports, kick back to Replug
alive, dead = self.instances.are_alive()
if not alive:
# alive checked failed on all instances for an already configured
# resource, mark it down.
# XXX need to track timeouts per instance
# self._check_boot_timeout()
if self.state == states.CONFIGURED:
self.log.debug('No instance(s) alive, marking it as %s',
states.DOWN)
self.state = states.DOWN
return self.state
elif dead:
# some subset of instances reported not alive, mark it degraded.
if self.state == states.CONFIGURED:
for i in dead:
instance = worker_context.nova_client.get_instance_by_id(
i.id_)
if instance is None and self.state != states.ERROR:
self.log.info(
'Instance %s was found; rebooting', i.id_)
self.instances.delete(i)
self.state = states.DEGRADED
return self.state
self.instances.refresh(worker_context)
if self.state == states.CONFIGURED:
for i in alive:
if not i.booting and i not in self._boot_logged:
self.log.info(
'%s booted in %s seconds after %s attempts',
self.resource.RESOURCE_NAME,
i.time_since_boot.total_seconds(),
self._boot_counter.count)
self._boot_logged.append(i)
self.reset_boot_counter()
else:
if alive:
self.state = states.UP
return self.state
@ensure_cache
def boot(self, worker_context):
"""Boots the instances with driver pre/post boot hooks.
:returns: None
"""
self.log.info('Booting %s' % self.resource.RESOURCE_NAME)
if self.state != states.DEGRADED:
self.state = states.DOWN
self._boot_counter.start()
# driver preboot hook
self.resource.pre_boot(worker_context)
try:
self.instances.create(worker_context)
if not self.instances:
self.log.info(_LI('Previous instances are still deleting'))
# Reset the boot counter, causing the state machine to start
# again with a new Instance.
self.reset_boot_counter()
return
except:
self.log.exception(_LE('Instances failed to start boot'))
else:
self.state = states.BOOTING
# driver post boot hook
self.resource.post_boot(worker_context)
@synchronize_driver_state
@ensure_cache
def set_error(self, worker_context, silent=False):
"""Set the internal and neutron status for the router to states.ERROR.
This is called from outside when something notices the router
is "broken". We don't use it internally because this class is
supposed to do what it's told and not make decisions about
whether or not the router is fatally broken.
"""
self.state = states.ERROR
self.last_error = datetime.utcnow()
return self.state
@synchronize_driver_state
@ensure_cache
def clear_error(self, worker_context, silent=False):
"""Clear the internal error state.
This is called from outside when something wants to force a
router rebuild, so that the state machine that checks our
status won't think we are broken unless we actually break
again.
"""
# Clear the boot counter.
self._boot_counter.reset()
self.state = states.DOWN
return self.state
@property
def error_cooldown(self):
"""Returns True if the instance was recently set to states.ERROR state.
"""
if self.last_error and self.state == states.ERROR:
seconds_since_error = (
datetime.utcnow() - self.last_error
).total_seconds()
if seconds_since_error < cfg.CONF.error_state_cooldown:
return True
return False
@synchronize_driver_state
@ensure_cache
def stop(self, worker_context):
"""Attempts to destroy the instance cluster
:param worker_context:
:returns:
"""
self.log.info(_LI('Destroying instance'))
self.resource.delete_ports(worker_context)
if not self.instances:
self.log.info(_LI('Instance(s) already destroyed.'))
if self.state != states.GONE:
self.state = states.DOWN
return self.state
try:
self.instances.destroy(worker_context)
if self.state != states.GONE:
self.state = states.DOWN
except Exception:
self.log.exception(_LE('Failed to stop instance(s)'))
@synchronize_driver_state
@ensure_cache
def configure(self, worker_context):
"""Pushes config to instance
:param worker_context:
:param failure_state:
:param attempts:
:returns:
"""
self.log.debug('Begin instance config')
self.state = states.UP
if self.resource.get_state(worker_context) == states.GONE:
return states.GONE
if not self.instances:
return states.DOWN
if not self.instances.verify_interfaces(self.resource.ports):
# XXX Need to acct for degraded cluster /w subset of nodes
# having incorrect plugging.
self.log.debug("Interfaces aren't plugged as expected.")
self.state = states.REPLUG
return self.state
self.state = self.instances.configure(worker_context)
return self.state
def replug(self, worker_context):
"""Attempts to replug the network ports for an instance.
:param worker_context:
:returns:
"""
self.log.debug('Attempting to replug...')
self.resource.pre_plug(worker_context)
for instance, interfaces in self.instances.get_interfaces().items():
actual_macs = set((iface['lladdr'] for iface in interfaces))
instance_macs = set(p.mac_address for p in instance.ports)
instance_macs.add(instance.management_port.mac_address)
if instance_macs != actual_macs:
# our cached copy of the ports is wrong reboot and clean up
self.log.warning((
'Instance macs(%s) do not match actual macs (%s). Instance'
' cache appears out-of-sync'),
instance_macs, actual_macs
)
self.state = states.RESTART
return
instance_ports = {p.network_id: p for p in instance.ports}
instance_networks = set(instance_ports.keys())
logical_networks = set(p.network_id for p in self.resource.ports)
if logical_networks != instance_networks:
nova_instance = worker_context.nova_client.get_instance_by_id(
instance.id_
)
# For each port that doesn't have a mac address on the instance
for network_id in logical_networks - instance_networks:
port = worker_context.neutron.create_vrrp_port(
self.resource.id,
network_id
)
self.log.debug(
'Net %s is missing from the appliance instance %s, '
'plugging: %s', network_id, instance.id_, port.id
)
try:
nova_instance.interface_attach(port.id, None, None)
instance.ports.append(port)
except:
self.log.exception(
'Interface attach failed on instance %s',
instance.id_)
self.instances.remove(worker_context, instance)
# instance has been removed for failure, do not continue with
# plugging
if instance not in self.instances.values():
continue
ports_to_delete = []
for network_id in instance_networks - logical_networks:
port = instance_ports[network_id]
self.log.debug(
'Net %s is detached from the router, unplugging: %s',
network_id, port.id
)
try:
nova_instance.interface_detach(port.id)
instance.ports.remove(port)
ports_to_delete.append(port)
except:
self.log.exception(
'Interface detach failed on instance %s',
instance.id_)
self.instances.remove(worker_context, instance)
# instance has been removed for failure, do not continue with
# plugging
if instance not in self.instances.values():
continue
if self._wait_for_interface_hotplug(instance) is not True:
self.instances.remove(worker_context, instance)
if not self.instances:
# all instances were destroyed for plugging failure
self.state = states.RESTART
elif self.instances.cluster_degraded:
# some instances were destroyed for plugging failure
self.state = states.DEGRADED
else:
# plugging was successful
for p in ports_to_delete:
worker_context.neutron.api_client.delete_port(port.id)
return
def _wait_for_interface_hotplug(self, instance):
"""Waits for instance to report interfaces for all expected ports"""
# The action of attaching/detaching interfaces in Nova happens via
# the message bus and is *not* blocking. We need to wait a few
# seconds to if the list of tap devices on the appliance actually
# changed. If not, assume the hotplug failed, and reboot the
# Instance.
for i in six.moves.range(1, cfg.CONF.hotplug_timeout):
self.log.debug(
"Waiting for interface attachments to take effect..."
)
interfaces = self.resource.get_interfaces(
instance.management_address)
actual_macs = set((iface['lladdr'] for iface in interfaces))
instance_macs = set(p.mac_address for p in instance.ports)
instance_macs.add(instance.management_port.mac_address)
if actual_macs == instance_macs:
return True
time.sleep(1)
else:
self.log.debug(
"Interfaces aren't plugged as expected on instance %s, ",
"marking for rebooting.", instance.id_)
return False
def _check_boot_timeout(self):
"""If the instance was created more than `boot_timeout` seconds
ago, log an error and set the state set to states.DOWN
"""
time_since_boot = self.instance_info.time_since_boot
if time_since_boot:
if time_since_boot.seconds < cfg.CONF.boot_timeout:
# Do not reset the state if we have an error
# condition already. The state will be reset when
# the router starts responding again, or when the
# error is cleared from a forced rebuild.
if self.state != states.ERROR:
self.state = states.BOOTING
else:
# If the instance was created more than `boot_timeout` seconds
# ago, log an error and set the state set to states.DOWN
self.log.info(
'Router is DOWN. Created over %d secs ago.',
cfg.CONF.boot_timeout)
# Do not reset the state if we have an error condition
# already. The state will be reset when the router starts
# responding again, or when the error is cleared from a
# forced rebuild.
if self.state != states.ERROR:
self.state = states.DOWN

View File

@ -1,218 +0,0 @@
# 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 functools
import logging
import multiprocessing
import signal
import socket
import sys
import threading
from oslo_config import cfg
from oslo_log import log
from astara.common.i18n import _LE, _LI
from astara.common import config as ak_cfg
from astara import coordination
from astara import daemon
from astara import health
from astara import metadata
from astara import notifications
from astara import scheduler
from astara import populate
from astara import worker
from astara.api import neutron as neutron_api
LOG = log.getLogger(__name__)
CONF = cfg.CONF
MAIN_OPTS = [
cfg.StrOpt('host',
default=socket.gethostname(),
help="The hostname Astara is running on"),
]
CONF.register_opts(MAIN_OPTS)
CEILOMETER_OPTS = [
cfg.BoolOpt('enabled', default=False,
help='Enable reporting metrics to ceilometer.'),
cfg.StrOpt('topic', default='notifications.info',
help='The name of the topic queue ceilometer consumes events '
'from.')
]
CONF.register_group(cfg.OptGroup(name='ceilometer',
title='Ceilometer Reporting Options'))
CONF.register_opts(CEILOMETER_OPTS, group='ceilometer')
def shuffle_notifications(notification_queue, sched):
"""Copy messages from the notification queue into the scheduler.
"""
while True:
try:
target, message = notification_queue.get()
if target is None:
break
sched.handle_message(target, message)
except IOError:
# FIXME(rods): if a signal arrive during an IO operation
# an IOError is raised. We catch the exceptions in
# meantime waiting for a better solution.
pass
except KeyboardInterrupt:
LOG.info(_LI('got Ctrl-C'))
break
except:
LOG.exception(_LE('unhandled exception processing message'))
def main(argv=sys.argv[1:]):
"""Main Entry point into the astara-orchestrator
This is the main entry point into the astara-orchestrator. On invocation of
this method, logging, local network connectivity setup is performed.
This information is obtained through the 'ak-config' file, passed as
arguement to this method. Worker threads are spawned for handling
various tasks that are associated with processing as well as
responding to different Neutron events prior to starting a notification
dispatch loop.
:param argv: list of Command line arguments
:returns: None
:raises: None
"""
# TODO(rama) Error Handling to be added as part of the docstring
# description
# Change the process and thread name so the logs are cleaner.
p = multiprocessing.current_process()
p.name = 'pmain'
t = threading.current_thread()
t.name = 'tmain'
ak_cfg.parse_config(argv)
log.setup(cfg.CONF, 'astara-orchestrator')
cfg.CONF.log_opt_values(LOG, logging.INFO)
neutron = neutron_api.Neutron(cfg.CONF)
# TODO(mark): develop better way restore after machine reboot
# neutron.purge_management_interface()
# bring the mgt tap interface up
mgt_ip_address = neutron.ensure_local_service_port().split('/')[0]
# Set up the queue to move messages between the eventlet-based
# listening process and the scheduler.
notification_queue = multiprocessing.Queue()
# Ignore signals that might interrupt processing.
daemon.ignore_signals()
# If we see a SIGINT, stop processing.
def _stop_processing(*args):
notification_queue.put((None, None))
signal.signal(signal.SIGINT, _stop_processing)
# Listen for notifications.
notification_proc = multiprocessing.Process(
target=notifications.listen,
kwargs={
'notification_queue': notification_queue
},
name='notification-listener',
)
notification_proc.start()
if CONF.coordination.enabled:
coordinator_proc = multiprocessing.Process(
target=coordination.start,
kwargs={
'notification_queue': notification_queue
},
name='coordinator',
)
coordinator_proc.start()
else:
coordinator_proc = None
metadata_proc = multiprocessing.Process(
target=metadata.serve,
args=(mgt_ip_address,),
name='metadata-proxy'
)
metadata_proc.start()
from astara.api import rug as rug_api
rug_api_proc = multiprocessing.Process(
target=rug_api.serve,
name='rug-api'
)
rug_api_proc.start()
# Set up the notifications publisher
Publisher = (notifications.Publisher if cfg.CONF.ceilometer.enabled
else notifications.NoopPublisher)
publisher = Publisher(
topic=cfg.CONF.ceilometer.topic,
)
# Set up a factory to make Workers that know how many threads to
# run.
worker_factory = functools.partial(
worker.Worker,
notifier=publisher,
management_address=mgt_ip_address,
)
# Set up the scheduler that knows how to manage the routers and
# dispatch messages.
sched = scheduler.Scheduler(
worker_factory=worker_factory,
)
# Prepopulate the workers with existing routers on startup
populate.pre_populate_workers(sched)
# Set up the periodic health check
health.start_inspector(cfg.CONF.health_check_period, sched)
# Set up the periodic neutron agent report
health.start_reporter()
# Block the main process, copying messages from the notification
# listener to the scheduler
try:
shuffle_notifications(notification_queue, sched)
finally:
LOG.info(_LI('Stopping scheduler.'))
sched.stop()
LOG.info(_LI('Stopping notification publisher.'))
publisher.stop()
# Terminate the subprocesses
for subproc in [notification_proc, coordinator_proc, metadata_proc,
rug_api_proc]:
if not subproc:
continue
LOG.info(_LI('Stopping %s.'), subproc.name)
subproc.terminate()

View File

@ -1,229 +0,0 @@
# 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.
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012 New Dream Network, LLC (DreamHost)
#
# 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.
#
# @author: Mark McClain, DreamHost
"""Proxy requests to Nova's metadata server.
Used by main.py
"""
import hashlib
import hmac
from six.moves.urllib import parse as urlparse
import socket
import eventlet
import eventlet.wsgi
import httplib2
from oslo_config import cfg
import webob
import webob.dec
import webob.exc
import six
from oslo_log import log as logging
from astara.common.i18n import _, _LE, _LI, _LW
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
METADATA_OPTS = [
cfg.StrOpt('nova_metadata_ip', default='127.0.0.1',
help="IP address used by Nova metadata server."),
cfg.IntOpt('nova_metadata_port',
default=8775,
help="TCP Port used by Nova metadata server."),
cfg.IntOpt('astara_metadata_port',
default=9697,
help="TCP listening port used by Astara metadata proxy."),
cfg.StrOpt('neutron_metadata_proxy_shared_secret',
default='',
help='Shared secret to sign instance-id request',
deprecated_name='quantum_metadata_proxy_shared_secret')
]
CONF.register_opts(METADATA_OPTS)
class MetadataProxyHandler(object):
"""The actual handler for proxy requests."""
@webob.dec.wsgify(RequestClass=webob.Request)
def __call__(self, req):
"""Inital handler for an incoming `webob.Request`.
:param req: The webob.Request to handle
:returns: returns a valid HTTP Response or Error
"""
try:
LOG.debug("Request: %s", req)
instance_id = self._get_instance_id(req)
if instance_id:
return self._proxy_request(instance_id, req)
else:
return webob.exc.HTTPNotFound()
except Exception:
LOG.exception(_LE("Unexpected error."))
msg = ('An unknown error has occurred. '
'Please try your request again.')
return webob.exc.HTTPInternalServerError(
explanation=six.text_type(msg))
def _get_instance_id(self, req):
"""Pull the X-Instance-ID out of a request.
:param req: The webob.Request to handle
:returns: returns the X-Instance-ID HTTP header
"""
return req.headers.get('X-Instance-ID')
def _proxy_request(self, instance_id, req):
"""Proxy a signed HTTP request to an instance.
:param instance_id: ID of the Instance being proxied to
:param req: The webob.Request to handle
:returns: returns a valid HTTP Response or Error
"""
headers = {
'X-Forwarded-For': req.headers.get('X-Forwarded-For'),
'X-Instance-ID': instance_id,
'X-Instance-ID-Signature': self._sign_instance_id(instance_id),
'X-Tenant-ID': req.headers.get('X-Tenant-ID')
}
url = urlparse.urlunsplit((
'http',
'%s:%s' % (cfg.CONF.nova_metadata_ip,
cfg.CONF.nova_metadata_port),
req.path_info,
req.query_string,
''))
h = httplib2.Http()
resp, content = h.request(url, headers=headers)
if resp.status == 200:
LOG.debug(str(resp))
return content
elif resp.status == 403:
msg = _LW(
'The remote metadata server responded with Forbidden. This '
'response usually occurs when shared secrets do not match.'
)
LOG.warning(msg)
return webob.exc.HTTPForbidden()
elif resp.status == 404:
return webob.exc.HTTPNotFound()
elif resp.status == 500:
msg = _LW('Remote metadata server experienced an'
' internal server error.')
LOG.warning(msg)
return webob.exc.HTTPInternalServerError(
explanation=six.text_type(msg))
else:
raise Exception(_('Unexpected response code: %s') % resp.status)
def _sign_instance_id(self, instance_id):
"""Get an HMAC based on the instance_id and Neutron shared secret.
:param instance_id: ID of the Instance being proxied to
:returns: returns a hexadecimal string HMAC for a specific instance_id
"""
return hmac.new(cfg.CONF.neutron_metadata_proxy_shared_secret,
instance_id,
hashlib.sha256).hexdigest()
class MetadataProxy(object):
"""The proxy service."""
def __init__(self):
"""Initialize the MetadataProxy.
:returns: returns nothing
"""
self.pool = eventlet.GreenPool(1000)
def run(self, ip_address, port=cfg.CONF.astara_metadata_port):
"""Run the MetadataProxy.
:param ip_address: the ip address to bind to for incoming requests
:param port: the port to bind to for incoming requests
:returns: returns nothing
"""
app = MetadataProxyHandler()
for i in six.moves.range(5):
LOG.info(_LI(
'Starting the metadata proxy on %s:%s'),
ip_address, port
)
try:
sock = eventlet.listen(
(ip_address, port),
family=socket.AF_INET6,
backlog=128
)
except socket.error as err:
if err.errno != 99:
raise
LOG.warning(
_LW('Could not create metadata proxy socket: %s'), err)
LOG.warning(_LW('Sleeping %s before trying again'), i + 1)
eventlet.sleep(i + 1)
else:
break
else:
raise RuntimeError(
_('Could not establish metadata proxy socket on %s:%s') %
(ip_address, port)
)
eventlet.wsgi.server(
sock,
app,
custom_pool=self.pool,
log=LOG)
def serve(ip_address):
"""Initialize the MetaData proxy.
:param ip_address: the ip address to bind to for incoming requests
:returns: returns nothing
"""
MetadataProxy().run(ip_address)

View File

@ -1,58 +0,0 @@
# Copyright 2016 Mark McClain
#
# 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.
from astara_neutron.plugins import ml2_neutron_plugin as as_plugin
from neutron.plugins.ml2 import plugin as ml2_plugin
from neutron.services.l3_router.service_providers import base
class SingleNodeDriver(base.L3ServiceProvider):
"""Provider for single L3 agent routers."""
use_integrated_agent_scheduler = False
class HaNodeDriver(base.L3ServiceProvider):
"""Provider for HA L3 agent routers."""
use_integrated_agent_schedule = False
ha_support = base.MANDATORY
class Ml2Plugin(as_plugin.Ml2Plugin):
_supported_extension_aliases = (
as_plugin.Ml2Plugin._supported_extension_aliases +
['ip_allocation']
)
disabled_extensions = [
"dhrouterstatus",
"byonf"
]
for ext in disabled_extensions:
try:
_supported_extension_aliases.remove(ext)
except ValueError:
pass
def _make_port_dict(self, port, fields=None, process_extensions=True):
res = ml2_plugin.Ml2Plugin._make_port_dict(
self,
port,
fields,
process_extensions
)
if not res.get('fixed_ips') and res.get('mac_address'):
res['ip_allocation'] = 'deferred'
return res

View File

@ -1,264 +0,0 @@
# 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.
"""Listen for notifications.
"""
import Queue
import threading
from astara import commands
from astara import drivers
from astara import event
from astara.common import rpc
from oslo_config import cfg
from oslo_context import context
from oslo_log import log as logging
from astara.common.i18n import _LE
from oslo_service import service
NOTIFICATIONS_OPTS = [
cfg.StrOpt('amqp-url',
help='connection for AMQP server'),
cfg.StrOpt('incoming-notifications-exchange',
default='neutron',
help='name of the exchange where we receive notifications'),
cfg.StrOpt('rpc-exchange',
default='l3_agent_fanout',
help='name of the exchange where we receive RPC calls'),
cfg.StrOpt('neutron-control-exchange',
default='neutron',
help='The name of the exchange used by Neutron for RPCs')
]
cfg.CONF.register_opts(NOTIFICATIONS_OPTS)
LOG = logging.getLogger(__name__)
def _get_tenant_id_for_message(context, payload=None):
"""Find the tenant id in the incoming message."""
# give priority to the tenant_id in the router dict if one
# exists in the message
if payload:
for key in ('router', 'port', 'subnet'):
if key in payload and payload[key].get('tenant_id'):
val = payload[key]['tenant_id']
return val
for key in ['tenant_id', 'project_id']:
if key in context:
val = context[key]
# Some notifications have None as the tenant id, but we
# can't shard on None in the dispatcher, so treat those as
# invalid.
if val is not None:
return val
return None
_ROUTER_INTERFACE_NOTIFICATIONS = set([
'router.interface.create',
'router.interface.delete',
])
_ROUTER_INTERESTING_NOTIFICATIONS = set([
'subnet.create.end',
'subnet.change.end',
'subnet.delete.end',
'port.create.end',
'port.change.end',
'port.delete.end',
])
L3_AGENT_TOPIC = 'l3_agent'
class L3RPCEndpoint(object):
"""A RPC endpoint for servicing L3 Agent RPC requests"""
def __init__(self, notification_queue):
self.notification_queue = notification_queue
def router_deleted(self, ctxt, router_id):
tenant_id = _get_tenant_id_for_message(ctxt)
resource = event.Resource('router', router_id, tenant_id)
crud = event.DELETE
e = event.Event(resource, crud, None)
self.notification_queue.put((e.resource.tenant_id, e))
class NotificationsEndpoint(object):
"""A RPC endpoint for processing notification"""
def __init__(self, notification_queue):
self.notification_queue = notification_queue
def info(self, ctxt, publisher_id, event_type, payload, metadata):
tenant_id = _get_tenant_id_for_message(ctxt, payload)
crud = event.UPDATE
e = None
events = []
if event_type.startswith('astara.command'):
LOG.debug('received a command: %r', payload)
crud = event.COMMAND
if payload.get('command') == commands.POLL:
r = event.Resource(driver='*', id='*', tenant_id='*')
e = event.Event(
resource=r,
crud=event.POLL,
body={})
self.notification_queue.put(('*', e))
return
else:
# If the message does not specify a tenant, send it to everyone
tenant_id = payload.get('tenant_id', '*')
router_id = payload.get('router_id')
resource = event.Resource(
driver='*',
id=router_id,
tenant_id=tenant_id)
events.append(event.Event(resource, crud, payload))
else:
for driver in drivers.enabled_drivers():
driver_event = driver.process_notification(
tenant_id, event_type, payload)
if driver_event:
events.append(driver_event)
if not events:
LOG.debug('Could not construct any events from %s /w payload: %s',
event_type, payload)
return
LOG.debug('Generated %s events from %s /w payload: %s',
len(events), event_type, payload)
for e in events:
self.notification_queue.put((e.resource.tenant_id, e))
def listen(notification_queue):
"""Create and launch the messaging service"""
connection = rpc.MessagingService()
connection.create_notification_listener(
endpoints=[NotificationsEndpoint(notification_queue)],
exchange=cfg.CONF.neutron_control_exchange,
)
connection.create_rpc_consumer(
topic=L3_AGENT_TOPIC,
endpoints=[L3RPCEndpoint(notification_queue)]
)
launcher = service.ServiceLauncher(cfg.CONF)
launcher.launch_service(service=connection, workers=1)
launcher.wait()
class Sender(object):
"Send notification messages"
def __init__(self, topic=None):
self._notifier = None
self.topic = topic
def get_notifier(self):
if not self._notifier:
self._notifier = rpc.get_rpc_notifier(topic=self.topic)
def send(self, event_type, message):
self.get_notifier()
ctxt = context.get_admin_context().to_dict()
self._notifier.info(ctxt, event_type, message)
class Publisher(Sender):
def __init__(self, topic=None):
super(Publisher, self).__init__(topic)
self._q = Queue.Queue()
self._t = None
def start(self):
ready = threading.Event()
self._t = threading.Thread(
name='notification-publisher',
target=self._send,
args=(ready,),
)
self._t.setDaemon(True)
self._t.start()
# Block until the thread is ready for work, but use a timeout
# in case of error in the thread.
ready.wait(10)
LOG.debug('started %s', self._t.getName())
def stop(self):
if self._t:
LOG.debug('stopping %s', self._t.getName())
self._q.put(None)
self._t.join(timeout=1)
self._t = None
def publish(self, incoming):
self._q.put(incoming)
def _send(self, ready):
"""Deliver notification messages from the in-process queue
to the appropriate topic via the AMQP service.
"""
# setup notifier driver ahead a time
self.get_notifier()
# Tell the start() method that we have set up the AMQP
# communication stuff and are ready to do some work.
ready.set()
while True:
msg = self._q.get()
if msg is None:
break
LOG.debug('sending notification %r', msg)
try:
self.send(event_type=msg['event_type'], message=msg['payload'])
except Exception:
LOG.exception(_LE('could not publish notification'))
class NoopPublisher(Publisher):
"""A Publisher that doesn't do anything.
The code that publishes notifications is spread across several
classes and cannot be easily disabled in configurations that do
not require sending metrics to ceilometer.
This class is used in place of the Publisher class to disable
sending metrics without explicitly checking in various places
across the code base.
"""
def start(self):
pass
def stop(self):
pass
def publish(self, incoming):
pass

View File

@ -1,84 +0,0 @@
# 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 itertools
import astara.api.nova
import astara.drivers
import astara.main
import astara.common.linux.interface
import astara.notifications
import astara.coordination
import astara.pez.manager
import astara.drivers.router
import astara.api.rug
import astara.debug
def list_opts():
return [
('DEFAULT',
itertools.chain(
astara.api.api_opts,
astara.api.rug.RUG_API_OPTS,
astara.api.nova.OPTIONS,
astara.api.neutron.neutron_opts,
astara.api.astara_client.AK_CLIENT_OPTS,
astara.drivers.DRIVER_OPTS,
astara.main.MAIN_OPTS,
astara.common.linux.interface.OPTS,
astara.common.hash_ring.hash_opts,
astara.api.config.router.OPTIONS,
astara.notifications.NOTIFICATIONS_OPTS,
astara.debug.DEBUG_OPTS,
astara.scheduler.SCHEDULER_OPTS,
astara.worker.WORKER_OPTS,
astara.metadata.METADATA_OPTS,
astara.health.HEALTH_INSPECTOR_OPTS,
astara.instance_manager.INSTANCE_MANAGER_OPTS
))
]
def list_agent_opts():
return [
('AGENT', astara.common.linux.interface.AGENT_OPTIONS)
]
def list_coordination_opts():
return [
('coordination', astara.coordination.COORD_OPTS)
]
def list_ceilometer_opts():
return [
('ceilometer', astara.main.CEILOMETER_OPTS)
]
def list_router_opts():
return [
('router', astara.drivers.router.ROUTER_OPTS)
]
def list_loadbalancer_opts():
return [
('loadbalancer', astara.drivers.loadbalancer.LOADBALANCER_OPTS)
]
def list_pez_opts():
return [
('pez', astara.pez.manager.PEZ_OPTIONS)
]

View File

View File

@ -1,109 +0,0 @@
# Copyright 2015 Akanda, Inc
#
# Author: Akanda, Inc
#
# 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 threading
from oslo_config import cfg
from astara.common.i18n import _
from astara.pez import pool
CONF = cfg.CONF
PEZ_OPTIONS = [
cfg.IntOpt('pool_size', default=1,
help=_('How many pre-allocated hot standby nodes to keep '
'in the pez pool.')),
# NOTE(adam_g): We should consider how these get configured for when
# we support multiple drivers. {router, lbaas}_image_uuid?
cfg.StrOpt('image_uuid',
help=_('Image uuid to boot.')),
cfg.StrOpt('flavor',
help=_('Nova flavor to boot')),
cfg.StrOpt('rpc_topic', default='astara-pez'),
]
CONF.register_group(cfg.OptGroup(name='pez'))
CONF.register_opts(PEZ_OPTIONS, group='pez')
CONF.import_opt('host', 'astara.main')
CONF.import_opt('management_network_id', 'astara.api.neutron')
class PezManager(object):
"""The RPC server-side of the Pez service"""
def __init__(self):
self.image_uuid = CONF.pez.image_uuid
self.flavor = CONF.pez.flavor
self.mgt_net_id = CONF.management_network_id
self.pool_size = CONF.pez.pool_size
self.pool_mgr = pool.PezPoolManager(
self.image_uuid,
self.flavor,
self.pool_size,
self.mgt_net_id)
def start(self):
pooler_thread = threading.Thread(target=self.pool_mgr.start)
pooler_thread.start()
def get_instance(self, context, resource_type, name, management_port,
instance_ports):
"""Obtains an instance from the pool for client
This obtains an instance from the pool manager and returns enough data
about it to the client that the client can create an InstanceInfo
object. We purposely avoid the need to introduce versioned object (for
now) by serializing everything into a dict. This may change in the
future.
:param context: oslo_context admin context object
:param resource_type: The str driver name of the resource
:param name: The requested name of the instance
:param managment_port: The management port dict that was created for
the instance by the RUG.
:param instance_ports: A list of dicts of ports to be attached to
instance upon reservation.
:returns: A dict containing the following:
- 'id': The id of the reserved instance
- 'name': The name of the reserved instance
- 'image_uuid': The image id of the reserved instance
- 'management_port': A serialized dict representing the
management Neutron port.
- 'instance_port': A list of serialized instance port
dicts that the caller requested be
attached.
"""
instance, mgt_port, instance_ports = self.pool_mgr.get_instance(
resource_type=resource_type, name=name,
management_port=management_port, instance_ports=instance_ports)
return {
'id': instance.id,
'resource_type': resource_type,
'name': instance.name,
'image_uuid': instance.image['id'],
'management_port': mgt_port.to_dict(),
'instance_ports': [
p.to_dict() for p in instance_ports
],
}

View File

@ -1,327 +0,0 @@
# Copyright 2015 Akanda, Inc
#
# Author: Akanda, Inc
#
# 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 copy
import time
from six.moves import range
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from astara.common.i18n import _LE, _LI
from astara import drivers
from astara.api import neutron
from astara.api import nova
LOG = logging.getLogger(__name__)
# Unused instances are launched with a known name
INSTANCE_FREE = 'ASTARA:UNUSED:%(resource_name)s'
# When an instance is reserved, its renamed accordingly
# TODO(adam_g): We ideally want to make all instance naming template-able
IN_USE_TEMPLATE = 'ak-%(resource_name)s-%(resource_id)s'
# Nova states
ACTIVE = 'active'
ERROR = 'error'
DELETING = 'deleting'
PEZ_LOCK = 'astara-pez'
class PezPoolExhausted(Exception):
pass
class WorkerContext(object):
"""Holds resources owned by the worker and used by the Automaton.
"""
def __init__(self):
self.nova_client = nova.Nova(cfg.CONF)
self.neutron_client = neutron.Neutron(cfg.CONF)
class PezPoolManager(object):
"""Astara Pez Pool Manager
This manages a pool of instances of a configurable size. All instance
state is managed and tracked in Nova itself.
Each iteratino of the manager's main loop will scan the service tenant's
booted instances. Instances named INSTANCE_FREE (defined above) will be
considered unused. If any of these instances are in ERROR state or are
out dated in some way (ie, its image is not the currently configured
image), they will be deleted from the pool and the manager will replenish
the deficit on its next tick.
Instances may be reserved for use via the get_instance() method. This
simply renames the instance according to the ID of the thing that it will
host and returns it to the caller. At this point, Pez no longer cares about
the instance and will refill its position in the pool on next its next
tick.
The calling service is responsible for managing the lifecycle of the
returned instance. This includes attaching required ports, ensuring
deletion/cleanup, etc. The instance will not be returned to the pool when
it is no longer in use.
"""
def __init__(self, image_uuid, flavor, pool_size, mgt_net_id):
"""
:param image_uuid: UUID of backing image for managed instances.
:param flavor: nova flavor id to be used for managed instances.
:param mgt_net_id: UUID of management network. Each instance in the
pool is initially booted with a single port on this
network
:param pool_size: The size of the pool
"""
self.image_uuid = image_uuid
self.flavor = flavor
self.mgt_net_id = mgt_net_id
self.pool_size = int(pool_size)
self.poll_interval = 3
self.ctxt = WorkerContext()
self.boot_timeout = 120
self.delete_timeout = 30
# used to track boot/delete timeouts
self._delete_counters = {}
self._boot_counters = {}
self.load_driver_config()
def load_driver_config(self):
self.images = {}
self.flavors = {}
self.drivers = [d for d in drivers.enabled_drivers()]
for driver in self.drivers:
self.images[driver.RESOURCE_NAME] = getattr(
cfg.CONF, driver.RESOURCE_NAME).image_uuid
self.flavors[driver.RESOURCE_NAME] = getattr(
cfg.CONF, driver.RESOURCE_NAME).instance_flavor
@lockutils.synchronized(PEZ_LOCK)
def delete_instance(self, instance_uuid):
LOG.info(_LI('Deleting instance %s.'), instance_uuid)
self.ctxt.nova_client.client.servers.delete(instance_uuid)
self._delete_counters[instance_uuid] = timeutils.utcnow()
def _check_err_instances(self, pools):
"""Scans the pool and deletes any instances in error state"""
for resource, pool in copy.copy(pools).items():
err_instances = [i for i in pool if i.status == ERROR]
for err_inst in err_instances:
LOG.error(_LE(
'Instance %s is in %s state, deleting.'),
i.id, ERROR)
del_instance = self.delete_instance(err_inst.id)
i = pool.index(err_inst)
pools[resource][i] = del_instance
def _check_del_instances(self, pools):
"""Scans the pool for deleted instances and checks deletion timers"""
# XXX: What do we do with instances stuck in deleting?
# For now, just return stuck instances to caller and we can figure
# out what to do with them later.
stuck_instances = []
del_instances = []
for resource, pool in pools.items():
del_instances += [i for i in pool if i.status == DELETING]
# clean out counters for old instances that have been deleted entirely
if self._delete_counters:
del_instance_ids = [i.id for i in del_instances]
for inst_id in copy.copy(self._delete_counters):
if inst_id not in del_instance_ids:
self._delete_counters.pop(inst_id)
for del_inst in del_instances:
if del_inst.id not in self._delete_counters:
self._delete_counters[del_inst.id] = timeutils.utcnow()
else:
if timeutils.is_older_than(self._delete_counters[del_inst.id],
self.delete_timeout):
LOG.error(_LE(
'Instance %s is stuck in %s for more than %s '
'seconds.'), i.id, DELETING, self.delete_timeout)
stuck_instances.append(del_inst)
return stuck_instances
def _check_outdated_instances(self, pools):
outdated_instances = []
for resource, pool in pools.items():
for server in pool:
if server.image['id'] != str(self.images[resource]):
LOG.info(_LI(
'Deleting instance %s with outdated image, '
'%s != %s'),
server.id, server.image['id'], self.image_uuid)
outdated_instances.append(server)
elif server.flavor['id'] != str(self.flavors[resource]):
LOG.info(_LI(
'Deleting instance %s with outdated flavor, '
'%s != %s'),
server.id, server.flavor['id'], self.flavor)
outdated_instances.append(server)
if outdated_instances:
[self.delete_instance(i.id) for i in outdated_instances]
def load_pools(self):
pools = {}
[pools.update({d.RESOURCE_NAME: []}) for d in self.drivers]
for server in self.ctxt.nova_client.client.servers.list():
for d in self.drivers:
instance_free = INSTANCE_FREE % {
'resource_name': d.RESOURCE_NAME
}
if server.name.startswith(instance_free):
pools[d.RESOURCE_NAME].append(server)
return pools
@property
def unused_instances(self):
"""Determines the size and contents of the current instance pool
We list all nova servers according to the naming template.
Any instances in an error state are deleted and will be replenished on
the next run of the main loop.
We time instance deletion and any servers that appear to be stuck in a
deleted state will be reported as such. TODO(adam_g): We should figure
out what to do with stuck instances?
Any instances that appear to be outdated (ie, the server's image or
flavor does not match whats configured) will be deleted and replenished
on the next tick of hte main loop.
:returns: a dict keyed by driver name, each value a list of nova server
objects that represents the current resources pool.
"""
pools = self.load_pools()
self._check_err_instances(pools)
self._check_del_instances(pools)
self._check_outdated_instances(pools)
return pools
def launch_instances(self, count, driver):
LOG.info(_LI(
'Launching %s %s instances.'), driver.RESOURCE_NAME, count)
for i in range(0, count):
# NOTE: Use a fake UUID so astara-neutron's name matching still
# catches this port as an astara port. This can be avoided if
# we use a mgt security group in the future.
mgt_port = self.ctxt.neutron_client.create_management_port(
'00000000-0000-0000-0000-000000000000')
nics = [{
'net-id': mgt_port.network_id,
'v4-fixed-ip': '',
'port-id': mgt_port.id}]
instance_name = INSTANCE_FREE % {
'resource_name': driver.RESOURCE_NAME
}
image = self.images[driver.RESOURCE_NAME]
flavor = self.flavors[driver.RESOURCE_NAME]
self.ctxt.nova_client.client.servers.create(
name=instance_name,
image=image,
flavor=flavor,
nics=nics,
config_drive=True,
userdata=nova.format_userdata(mgt_port),
)
@lockutils.synchronized(PEZ_LOCK)
def get_instance(self, resource_type, name, management_port=None,
instance_ports=None):
"""Get an instance from the pool.
This involves popping it out of the pool, updating its name and
attaching
any ports.
:param resource_type: The str driver name of the resource
:param name: The requested name of the instance
:param managment_port: The management port dict that was created for
the instance by the RUG.
:param instance_ports: A list of dicts of ports to be attached to
instance upon reservation.
:returns: A tuple containing (novaclient server object for the
reserved server, a port object for the management port,
a list of port objects that were attached the server)
"""
instance_ports = instance_ports or []
try:
server = self.unused_instances[resource_type][0]
except IndexError:
raise PezPoolExhausted()
LOG.info(_LI('Renaming instance %s to %s'), server.name, name)
server = self.ctxt.nova_client.client.servers.update(
server, name=name)
for port in instance_ports:
LOG.info(_LI('Attaching instance port %s to %s (%s)'),
port['id'], server.name, server.id)
self.ctxt.nova_client.client.servers.interface_attach(
server=server, port_id=port['id'], net_id=None, fixed_ip=None)
mgt_port, instance_ports = (
self.ctxt.neutron_client.get_ports_for_instance(server.id)
)
return (
self.ctxt.nova_client.client.servers.get(server.id),
mgt_port,
instance_ports,
)
def start(self):
"""The pool manager main loop.
The bulk of the algorithm exists in the 'unused_instances' property.
This main loop simply checks for a deficit in the pool and dispatches
a 'launch_instances' call when a deficit needs to be filled.
"""
while True:
cur_pools = self.unused_instances
report = []
for driver in self.drivers:
report.append(
'%s:%s/%s' %
(driver.RESOURCE_NAME,
len(cur_pools[driver.RESOURCE_NAME]),
self.pool_size))
LOG.debug('Current pools: %s' % ' '.join(report))
for driver in self.drivers:
cur_pool = cur_pools[driver.RESOURCE_NAME]
deficit = self.pool_size - len(cur_pool)
if deficit:
LOG.info(_LI(
'Need to launch %s more %s instance(s).'),
deficit, driver.RESOURCE_NAME)
self.launch_instances(
driver=driver, count=deficit)
time.sleep(self.poll_interval)

View File

@ -1,53 +0,0 @@
# Copyright 2015 Akanda, Inc
#
# Author: Akanda, Inc
#
# 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.
from astara.common import rpc
from oslo_context import context
class AstaraPezAPI(object):
""""Client side of the Astara Pez RPC API.
"""
def __init__(self, rpc_topic):
self.topic = rpc_topic
self.client = rpc.get_rpc_client(
topic=self.topic)
self.context = context.get_admin_context().to_dict()
def get_instance(self, resource_type, name, management_port,
instance_ports):
"""Reserves an instance from the Pez service. We can instruct Pez to
attach any required instance ports during the reservation process.
The dict returned here should be enough for the caller to construct
a InstanceInfo object. Note that the port information are serialized
astara.api.neutron.Port objects that can be deserialized by the
caller during creation of InstanceInfo.
:param resource_type: The str name of the driver that manages the
resource (ie, loadbalancer)
:param name: The requested name of the instance
:param managment_port: The management port dict that was created for
the instance by the RUG.
:param instance_ports: A list of dicts of ports to be attached to
instance upon reservation.
"""
cctxt = self.client.prepare(topic=self.topic)
return cctxt.call(
self.context, 'get_instance', resource_type=resource_type,
name=name, management_port=management_port,
instance_ports=instance_ports)

View File

@ -1,65 +0,0 @@
# Copyright 2015 Akanda, Inc
#
# Author: Akanda, Inc
#
# 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 logging
import sys
from astara.common.i18n import _LI
from astara.common import config as ak_cfg
from astara.common import rpc
from astara.pez import manager
from oslo_config import cfg
from oslo_log import log
from oslo_service import service
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class PezService(service.Service):
"""Bootstraps a connection for the manager to the messaging
queue and launches the pez service
"""
def __init__(self):
super(PezService, self).__init__()
self.manager = manager.PezManager()
self.manager.start()
self._rpc_connection = None
self.rpcserver = None
def start(self):
super(PezService, self).start()
self._rpc_connection = rpc.Connection()
self._rpc_connection.create_rpc_consumer(
topic=cfg.CONF.pez.rpc_topic,
endpoints=[self.manager])
self._rpc_connection.consume_in_threads()
self._rpc_connection.close()
def main(argv=sys.argv[1:]):
ak_cfg.parse_config(argv)
log.setup(CONF, 'astara-pez')
CONF.log_opt_values(LOG, logging.INFO)
LOG.info(_LI("Starting Astara Pez service."))
mgr = PezService()
launcher = service.launch(CONF, mgr)
launcher.wait()

View File

@ -1,79 +0,0 @@
# 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.
"""Populate the workers with the existing routers
"""
import threading
from oslo_log import log as logging
from astara import event
from astara import drivers
LOG = logging.getLogger(__name__)
def repopulate():
"""Called from workers after a rebalance to find newly owned resources"""
resources = []
for driver in drivers.enabled_drivers():
resources += driver.pre_populate_hook()
return resources
def _pre_populate_workers(scheduler):
"""Loops through enabled drivers triggering each drivers pre_populate_hook
which is a static method for each driver.
"""
for driver in drivers.enabled_drivers():
resources = driver.pre_populate_hook()
if not resources:
# just skip to the next one the drivers pre_populate_hook already
# handled the exception or error and outputs to logs
LOG.debug('No %s resources found to pre-populate',
driver.RESOURCE_NAME)
continue
LOG.debug('Start pre-populating %d workers for the %s driver',
len(resources),
driver.RESOURCE_NAME)
for resource in resources:
message = event.Event(
resource=resource,
crud=event.POLL,
body={}
)
scheduler.handle_message(resource.tenant_id, message)
def pre_populate_workers(scheduler):
"""Start the pre-populating task
"""
t = threading.Thread(
target=_pre_populate_workers,
args=(scheduler,),
name='PrePopulateWorkers'
)
t.setDaemon(True)
t.start()
return t

View File

@ -1,166 +0,0 @@
# 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.
"""Scheduler to send messages for a given router to the correct worker.
"""
import six
import multiprocessing
import uuid
from six.moves import range
from oslo_config import cfg
from oslo_log import log as logging
from astara import commands
from astara.common.i18n import _, _LE, _LI, _LW
from astara import daemon
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
SCHEDULER_OPTS = [
cfg.IntOpt('num_worker_processes',
default=16,
help='the number of worker processes to run'),
]
CONF.register_opts(SCHEDULER_OPTS)
def _worker(inq, worker_factory, scheduler, proc_name):
"""Scheduler's worker process main function.
"""
daemon.ignore_signals()
LOG.debug('starting worker process')
worker = worker_factory(scheduler=scheduler, proc_name=proc_name)
while True:
try:
data = inq.get()
except IOError:
# NOTE(dhellmann): Likely caused by a signal arriving
# during processing, especially SIGCHLD.
data = None
if data is None:
target, message = None, None
else:
target, message = data
try:
worker.handle_message(target, message)
except Exception:
LOG.exception(_LE('Error processing data %s'), six.text_type(data))
if data is None:
break
LOG.debug('exiting')
class Dispatcher(object):
"""Choose one of the workers to receive a message.
The current implementation uses the least significant bits of the
UUID as an integer to shard across the worker pool.
"""
def __init__(self, workers):
self.workers = workers
def pick_workers(self, target):
"""Returns the workers that match the target.
"""
target = target.strip() if target else None
# If we get any wildcard target, send the message to all of
# the workers.
if target in commands.WILDCARDS:
return self.workers[:]
try:
idx = uuid.UUID(target).int % len(self.workers)
except (TypeError, ValueError) as e:
LOG.warning(_LW(
'Could not determine UUID from %r: %s, ignoring message'),
target, e,
)
return []
else:
LOG.debug('target %s maps to worker %s', target, idx)
return [self.workers[idx]]
class Scheduler(object):
"""Manages a worker pool and redistributes messages.
"""
def __init__(self, worker_factory):
"""
:param num_workers: The number of worker processes to create.
:type num_workers: int
:param worker_func: Callable for the worker processes to use
when a notification is received.
:type worker_factory: Callable to create Worker instances.
"""
self.num_workers = cfg.CONF.num_worker_processes
if self.num_workers < 1:
raise ValueError(_('Need at least one worker process'))
self.workers = []
# Create several worker processes, each with its own queue for
# sending it instructions based on the notifications we get
# when someone calls our handle_message() method.
for i in range(self.num_workers):
wq = multiprocessing.JoinableQueue()
name = 'p%02d' % i
worker = multiprocessing.Process(
target=_worker,
kwargs={
'inq': wq,
'worker_factory': worker_factory,
'scheduler': self,
'proc_name': name,
},
name=name,
)
self.workers.append({
'queue': wq,
'worker': worker,
})
self.dispatcher = Dispatcher(self.workers)
for w in self.workers:
w['worker'].start()
def stop(self):
"""Shutdown all workers cleanly.
"""
LOG.info('shutting down scheduler')
# Send a poison pill to all of the workers
for w in self.workers:
LOG.debug('sending stop message to %s', w['worker'].name)
w['queue'].put(None)
# Wait for the workers to finish and be ready to exit.
for w in self.workers:
LOG.debug('waiting for queue for %s', w['worker'].name)
w['queue'].close()
LOG.debug('waiting for worker %s', w['worker'].name)
w['worker'].join()
LOG.info(_LI('scheduler shutdown'))
def handle_message(self, target, message):
"""Call this method when a new notification message is delivered. The
scheduler will distribute it to the appropriate worker.
:param target: UUID of the resource that needs to get the message.
:type target: uuid
:param message: Dictionary full of data to send to the target.
:type message: dict
"""
for w in self.dispatcher.pick_workers(target):
w['queue'].put((target, message))

View File

@ -1,565 +0,0 @@
# 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.
"""State machine for managing a router.
"""
# See state machine diagram and description:
# http://akanda.readthedocs.org/en/latest/rug.html#state-machine-workers-and-router-lifecycle
import collections
import itertools
from astara.common.i18n import _LE, _LI, _LW
from astara.event import (POLL, CREATE, READ, UPDATE, DELETE, REBUILD,
CLUSTER_REBUILD)
from astara import instance_manager
from astara.drivers import states
class StateParams(object):
def __init__(self, driver, instance, queue, bandwidth_callback,
reboot_error_threshold):
self.resource = driver
self.instance = instance
self.log = driver.log
self.queue = queue
self.bandwidth_callback = bandwidth_callback
self.reboot_error_threshold = reboot_error_threshold
self.image_uuid = driver.image_uuid
class State(object):
def __init__(self, params):
self.params = params
@property
def log(self):
return self.params.log
@property
def queue(self):
return self.params.queue
@property
def instance(self):
return self.params.instance
@property
def image_uuid(self):
return self.params.image_uuid
@property
def name(self):
return self.__class__.__name__
def __str__(self):
return self.name
def execute(self, action, worker_context):
return action
def transition(self, action, worker_context):
return self
class CalcAction(State):
def execute(self, action, worker_context):
queue = self.queue
if DELETE in queue:
self.params.resource.log.debug('shortcutting to delete')
return DELETE
if (self.params.instance.state == states.DEGRADED and
CLUSTER_REBUILD not in queue):
self.params.resource.log.debug(
'Scheduling a rebuild on degraded cluster')
queue.append(CLUSTER_REBUILD)
while queue:
self.params.resource.log.debug(
'action = %s, len(queue) = %s, queue = %s',
action,
len(queue),
list(itertools.islice(queue, 0, 60))
)
if action == UPDATE and queue[0] == CREATE:
# upgrade to CREATE from UPDATE by taking the next
# item from the queue
self.params.resource.log.debug(
'upgrading from update to create')
action = queue.popleft()
continue
elif (action in (CREATE, UPDATE, CLUSTER_REBUILD) and
queue[0] == REBUILD):
# upgrade to REBUILD from CREATE/UPDATE by taking the next
# item from the queue
self.params.resource.log.debug('upgrading from %s to rebuild',
action)
action = queue.popleft()
continue
elif action == CREATE and queue[0] == UPDATE:
# CREATE implies an UPDATE so eat the update event
# without changing the action
self.params.resource.log.debug('merging create and update')
queue.popleft()
continue
elif action and queue[0] == POLL:
# Throw away a poll following any other valid action,
# because a create or update will automatically handle
# the poll and repeated polls are not needed.
self.params.resource.log.debug(
'discarding poll event following action %s',
action)
queue.popleft()
continue
elif action and action != POLL and action != queue[0]:
# We are not polling and the next action is something
# different from what we are doing, so just do the
# current action.
self.params.resource.log.debug('done collapsing events')
break
self.params.resource.log.debug('popping action from queue')
action = queue.popleft()
return action
def transition(self, action, worker_context):
if self.instance.state == states.GONE:
next_action = StopInstance(self.params)
elif action == DELETE:
next_action = StopInstance(self.params)
elif action == REBUILD:
next_action = RebuildInstance(self.params)
elif (action == CLUSTER_REBUILD and
self.instance.state in (states.DEGRADED, states.DOWN)):
next_action = CreateInstance(self.params)
elif self.instance.state == states.BOOTING:
next_action = CheckBoot(self.params)
elif self.instance.state in (states.DOWN, states.DEGRADED):
next_action = CreateInstance(self.params)
else:
next_action = Alive(self.params)
if self.instance.state == states.ERROR:
if action == POLL:
# If the selected action is to poll, and we are in an
# error state, then an event slipped through the
# filter in send_message() and we should ignore it
# here.
next_action = self
elif self.instance.error_cooldown:
self.params.resource.log.debug(
'Resource is in ERROR cooldown, '
'ignoring event.'
)
next_action = self
else:
# If this isn't a POLL, and the configured `error_cooldown`
# has passed, clear the error status before doing what we
# really want to do.
next_action = ClearError(self.params, next_action)
return next_action
class PushUpdate(State):
"""Put an update instruction on the queue for the state machine.
"""
def execute(self, action, worker_context):
# Put the action back on the front of the queue.
self.queue.appendleft(UPDATE)
return action
def transition(self, action, worker_context):
return CalcAction(self.params)
class ClearError(State):
"""Remove the error state from the instance.
"""
def __init__(self, params, next_state=None):
super(ClearError, self).__init__(params)
self._next_state = next_state
def execute(self, action, worker_context):
# If we are being told explicitly to update the instance, we should
# ignore any error status.
self.instance.clear_error(worker_context)
return action
def transition(self, action, worker_context):
if self._next_state:
return self._next_state
return CalcAction(self.params)
class Alive(State):
def execute(self, action, worker_context):
self.instance.update_state(worker_context)
return action
def transition(self, action, worker_context):
if self.instance.state == states.GONE:
return StopInstance(self.params)
elif self.instance.state in (states.DOWN, states.DEGRADED):
return CreateInstance(self.params)
elif action == POLL and \
self.instance.state == states.CONFIGURED:
return CalcAction(self.params)
elif action == READ and \
self.instance.state == states.CONFIGURED:
return ReadStats(self.params)
else:
return ConfigureInstance(self.params)
class CreateInstance(State):
def execute(self, action, worker_context):
# Check for a loop where the resource keeps failing to boot or
# accept the configuration.
if (not self.instance.state == states.DEGRADED and
self.instance.attempts >= self.params.reboot_error_threshold):
self.params.resource.log.info(_LI(
'Dropping out of boot loop after %s trials'),
self.instance.attempts)
self.instance.set_error(worker_context)
return action
self.instance.boot(worker_context)
self.params.resource.log.debug('CreateInstance attempt %s/%s',
self.instance.attempts,
self.params.reboot_error_threshold)
return action
def transition(self, action, worker_context):
if self.instance.state == states.GONE:
return StopInstance(self.params)
elif self.instance.state == states.ERROR:
return CalcAction(self.params)
elif self.instance.state == states.DOWN:
return CreateInstance(self.params)
return CheckBoot(self.params)
class CheckBoot(State):
def execute(self, action, worker_context):
self.instance.update_state(worker_context)
self.params.resource.log.debug(
'Instance is %s' % self.instance.state.upper())
# Put the action back on the front of the queue so that we can yield
# and handle it in another state machine traversal (which will proceed
# from CalcAction directly to CheckBoot).
if self.instance.state not in (states.DOWN,
states.GONE):
self.queue.appendleft(action)
return action
def transition(self, action, worker_context):
if self.instance.state == states.REPLUG:
return ReplugInstance(self.params)
if self.instance.state in (states.DOWN,
states.GONE):
return StopInstance(self.params)
if self.instance.state == states.UP:
return ConfigureInstance(self.params)
return CalcAction(self.params)
class ReplugInstance(State):
def execute(self, action, worker_context):
self.instance.replug(worker_context)
return action
def transition(self, action, worker_context):
if self.instance.state == states.RESTART:
return StopInstance(self.params)
return ConfigureInstance(self.params)
class StopInstance(State):
def execute(self, action, worker_context):
self.instance.stop(worker_context)
if self.instance.state == states.GONE:
# Force the action to delete since the router isn't there
# any more.
return DELETE
return action
def transition(self, action, worker_context):
if self.instance.state not in (states.DOWN,
states.GONE):
return self
if self.instance.state == states.GONE:
return Exit(self.params)
if action == DELETE:
return Exit(self.params)
return CreateInstance(self.params)
class RebuildInstance(State):
def execute(self, action, worker_context):
self.instance.stop(worker_context)
if self.instance.state == states.GONE:
# Force the action to delete since the router isn't there
# any more.
return DELETE
# Re-create the instance
self.instance.reset_boot_counter()
return CREATE
def transition(self, action, worker_context):
if self.instance.state not in (states.DOWN,
states.GONE):
return self
if self.instance.state == states.GONE:
return Exit(self.params)
return CreateInstance(self.params)
class Exit(State):
pass
class ConfigureInstance(State):
def execute(self, action, worker_context):
self.instance.configure(worker_context)
if self.instance.state == states.CONFIGURED:
if action == READ:
return READ
else:
return POLL
else:
return action
def transition(self, action, worker_context):
if self.instance.state == states.REPLUG:
return ReplugInstance(self.params)
if self.instance.state in (states.RESTART,
states.DOWN,
states.GONE):
return StopInstance(self.params)
if self.instance.state == states.UP:
return PushUpdate(self.params)
# Below here, assume instance.state == states.CONFIGURED
if action == READ:
return ReadStats(self.params)
return CalcAction(self.params)
class ReadStats(State):
def execute(self, action, worker_context):
stats = self.instance.read_stats()
self.params.bandwidth_callback(stats)
return POLL
def transition(self, action, worker_context):
return CalcAction(self.params)
class Automaton(object):
def __init__(self, resource, tenant_id,
delete_callback, bandwidth_callback,
worker_context, queue_warning_threshold,
reboot_error_threshold):
"""
:param resource: An instantiated driver object for the managed resource
:param tenant_id: UUID of the tenant being managed
:type tenant_id: str
:param delete_callback: Invoked when the Automaton decides
the router should be deleted.
:type delete_callback: callable
:param bandwidth_callback: To be invoked when the Automaton needs to
report how much bandwidth a router has used.
:type bandwidth_callback: callable taking router_id and bandwidth
info dict
:param worker_context: a WorkerContext
:type worker_context: WorkerContext
:param queue_warning_threshold: Limit after which adding items
to the queue triggers a warning.
:type queue_warning_threshold: int
:param reboot_error_threshold: Limit after which trying to reboot
the router puts it into an error state.
:type reboot_error_threshold: int
"""
self.resource = resource
self.tenant_id = tenant_id
self._delete_callback = delete_callback
self._queue_warning_threshold = queue_warning_threshold
self._reboot_error_threshold = reboot_error_threshold
self.deleted = False
self.bandwidth_callback = bandwidth_callback
self._queue = collections.deque()
self.action = POLL
self.instance = instance_manager.InstanceManager(self.resource,
worker_context)
self._state_params = StateParams(
self.resource,
self.instance,
self._queue,
self.bandwidth_callback,
self._reboot_error_threshold,
)
self.state = CalcAction(self._state_params)
@property
def resource_id(self):
"""Returns the ID of the managed resource"""
return self.resource.id
def service_shutdown(self):
"Called when the parent process is being stopped"
def _do_delete(self):
if self._delete_callback is not None:
self.resource.log.debug('calling delete callback')
self._delete_callback()
# Avoid calling the delete callback more than once.
self._delete_callback = None
# Remember that this router has been deleted
self.deleted = True
def update(self, worker_context):
"Called when the router config should be changed"
while self._queue:
while True:
if self.deleted:
self.resource.log.debug(
'skipping update because the router is being deleted'
)
return
try:
self.resource.log.debug(
'%s.execute(%s) instance.state=%s',
self.state,
self.action,
self.instance.state)
self.action = self.state.execute(
self.action,
worker_context,
)
self.resource.log.debug(
'%s.execute -> %s instance.state=%s',
self.state,
self.action,
self.instance.state)
except:
self.resource.log.exception(
_LE('%s.execute() failed for action: %s'),
self.state,
self.action
)
old_state = self.state
self.state = self.state.transition(
self.action,
worker_context,
)
self.resource.log.debug(
'%s.transition(%s) -> %s instance.state=%s',
old_state,
self.action,
self.state,
self.instance.state
)
# Yield control each time we stop to figure out what
# to do next.
if isinstance(self.state, CalcAction):
return # yield
# We have reached the exit state, so the router has
# been deleted somehow.
if isinstance(self.state, Exit):
self._do_delete()
return
def send_message(self, message):
"Called when the worker put a message in the state machine queue"
if self.deleted:
# Ignore any more incoming messages
self.resource.log.debug(
'deleted state machine, ignoring incoming message %s',
message)
return False
# NOTE(dhellmann): This check is largely redundant with the
# one in CalcAction.transition() but it may allow us to avoid
# adding poll events to the queue at all, and therefore cut
# down on the number of times a worker thread wakes up to
# process something on a router that isn't going to actually
# do any work.
if message.crud == POLL and \
self.instance.state == states.ERROR:
self.resource.log.info(_LI(
'Resource status is ERROR, ignoring POLL message: %s'),
message,
)
return False
if message.crud == REBUILD:
if message.body.get('image_uuid'):
self.resource.log.info(_LI(
'Resource is being REBUILT with custom image %s'),
message.body['image_uuid']
)
self.image_uuid = message.body['image_uuid']
else:
self.image_uuid = self.resource.image_uuid
self._queue.append(message.crud)
queue_len = len(self._queue)
if queue_len > self._queue_warning_threshold:
logger = self.resource.log.warning
else:
logger = self.resource.log.debug
logger(_LW('incoming message brings queue length to %s'), queue_len)
return True
@property
def image_uuid(self):
return self.state.params.image_uuid
@image_uuid.setter
def image_uuid(self, value):
self.state.params.image_uuid = value
def has_more_work(self):
"Called to check if there are more messages in the state machine queue"
return (not self.deleted) and bool(self._queue)
def has_error(self):
return self.instance.state == states.ERROR
def drop_queue(self):
"""Drop all pending actions from the local state machine's work queue.
This is used after a ring rebalance if this state machine no longer
maps to the local Rug process.
"""
self.resource.log.info(
'Dropping %s pending actions from queue', len(self._queue))
self._queue.clear()

View File

@ -1,224 +0,0 @@
# 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.
"""Manage the resources for a given tenant.
"""
import datetime
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from astara.common.i18n import _LE
from astara import state
from astara import drivers
from astara.common import container
LOG = logging.getLogger(__name__)
tenant_opts = [
cfg.BoolOpt('enable_byonf', default=False,
help='Whether to enable bring-your-own-network-function '
'support via operator supplied drivers and images.'),
]
cfg.CONF.register_opts(tenant_opts)
class InvalidIncomingMessage(Exception):
pass
class StateMachineContainer(container.ResourceContainer):
def unmanage(self, resource_id):
"""Used to delete a state machine from local management
Removes the local state machine from orchestrator management during
cluster events. This is different than deleting the resource in that
it does not tag the resource as also deleted from Neutron, which would
prevent us from recreating its state machine if the resource later ends
up back under this orchestrators control.
:param resource_id: The resource id to unmanage
"""
try:
with self.lock:
sm = self.resources.pop(resource_id)
sm.drop_queue()
LOG.debug('unmanaged tenant state machine for resource %s',
resource_id)
except KeyError:
pass
class TenantResourceManager(object):
"""Keep track of the state machines for the logical resources for a given
tenant.
"""
def __init__(self, tenant_id, delete_callback, notify_callback,
queue_warning_threshold,
reboot_error_threshold):
self.tenant_id = tenant_id
self.delete = delete_callback
self.notify = notify_callback
self._queue_warning_threshold = queue_warning_threshold
self._reboot_error_threshold = reboot_error_threshold
self.state_machines = StateMachineContainer()
self._default_resource_id = None
def _delete_resource(self, resource):
"Called when the Automaton decides the resource can be deleted"
if resource.id in self.state_machines:
LOG.debug('deleting state machine for %s', resource.id)
del self.state_machines[resource.id]
if self._default_resource_id == resource.id:
self._default_resource_id = None
self.delete(resource)
def unmanage_resource(self, resource_id):
self.state_machines.unmanage(resource_id)
def shutdown(self):
LOG.info('shutting down')
for resource_id, sm in self.state_machines.items():
try:
sm.service_shutdown()
except Exception:
LOG.exception(_LE(
'Failed to shutdown state machine for %s'), resource_id
)
def _report_bandwidth(self, resource_id, bandwidth):
LOG.debug('reporting bandwidth for %s', resource_id)
msg = {
'tenant_id': self.tenant_id,
'timestamp': datetime.datetime.isoformat(timeutils.utcnow()),
'event_type': 'astara.bandwidth.used',
'payload': dict((b.pop('name'), b) for b in bandwidth),
'uuid': resource_id,
}
self.notify(msg)
def get_all_state_machines(self):
return self.state_machines.values()
def get_state_machines(self, message, worker_context):
"""Return the state machines and the queue for sending it messages for
the logical resource being addressed by the message.
"""
if (not message.resource or
(message.resource and not message.resource.id)):
LOG.error(_LE(
'Cannot get state machine for message with '
'no message.resource'))
raise InvalidIncomingMessage()
state_machines = []
# Send to all of our resources.
if message.resource.id == '*':
LOG.debug('routing to all state machines')
state_machines = self.state_machines.values()
# Ignore messages to deleted resources.
elif self.state_machines.has_been_deleted(message.resource.id):
LOG.debug('dropping message for deleted resource')
return []
# Send to resources that have an ERROR status
elif message.resource.id == 'error':
state_machines = [
sm for sm in self.state_machines.values()
if sm.has_error()
]
LOG.debug('routing to %d errored state machines',
len(state_machines))
# Create a new state machine for this router.
elif message.resource.id not in self.state_machines:
LOG.debug('creating state machine for %s', message.resource.id)
# load the driver
if not message.resource.driver:
LOG.error(_LE('cannot create state machine without specifying'
'a driver.'))
return []
resource_obj = self._load_resource_from_message(
worker_context, message)
if not resource_obj:
# this means the driver didn't load for some reason..
# this might not be needed at all.
LOG.debug('for some reason loading the driver failed')
return []
def deleter():
self._delete_resource(message.resource)
new_state_machine = state.Automaton(
resource=resource_obj,
tenant_id=self.tenant_id,
delete_callback=deleter,
bandwidth_callback=self._report_bandwidth,
worker_context=worker_context,
queue_warning_threshold=self._queue_warning_threshold,
reboot_error_threshold=self._reboot_error_threshold,
)
self.state_machines[message.resource.id] = new_state_machine
state_machines = [new_state_machine]
# Send directly to an existing router.
elif message.resource.id:
state_machines = [self.state_machines[message.resource.id]]
# Filter out any deleted state machines.
return [
machine
for machine in state_machines
if (not machine.deleted and
not self.state_machines.has_been_deleted(machine.resource.id))
]
def get_state_machine_by_resource_id(self, resource_id):
try:
return self.state_machines[resource_id]
except KeyError:
return None
def _load_resource_from_message(self, worker_context, message):
if cfg.CONF.enable_byonf:
byonf_res = worker_context.neutron.tenant_has_byo_for_function(
tenant_id=self.tenant_id.replace('-', ''),
function_type=message.resource.driver)
if byonf_res:
try:
return drivers.load_from_byonf(
worker_context,
byonf_res,
message.resource.id)
except drivers.InvalidDriverException:
LOG.exception(_LE(
'Could not load BYONF driver, falling back to '
'configured image'))
pass
return drivers.get(message.resource.driver)(
worker_context, message.resource.id)

View File

@ -1,15 +0,0 @@
# 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.

View File

@ -1,629 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
import netaddr
import os
import six
import subprocess
import testtools
import time
from oslo_config import cfg
from oslo_log import log as logging
from astara.api import astara_client
from keystoneclient import client as _keystoneclient
from keystoneclient import auth as ksauth
from keystoneclient import session as kssession
from neutronclient.v2_0 import client as _neutronclient
from novaclient import client as _novaclient
from keystoneclient import exceptions as ksc_exceptions
from neutronclient.common import exceptions as neutron_exceptions
from tempest_lib.common.utils import data_utils
from astara.test.functional import config
DEFAULT_CONFIG = os.path.join(os.path.dirname(__file__), 'test.conf')
DEFAULT_ACTIVE_TIMEOUT = 340
DEFAULT_DELETE_TIMEOUT = 60
DEFAULT_DOMAIN = 'default'
config.register_opts()
CONF = cfg.CONF
logging.register_options(CONF)
LOG = None
def parse_config():
config_file = os.environ.get('AKANDA_TEST_CONFIG',
DEFAULT_CONFIG)
cfg.CONF(
[], project='astara-orchestrator-functional',
default_config_files=[config_file])
logging.set_defaults(default_log_levels=[
'paramiko.transport=INFO',
'neutronclient=WARN',
'keystoneclient=WARN',
])
logging.setup(CONF, 'astara_functional')
global LOG
LOG = logging.getLogger(__name__)
class ClientManager(object):
"""A client manager using specified credentials"""
def __init__(self, username, password, tenant_name, auth_url):
parse_config()
self.username = username
self.password = password
self.tenant_name = tenant_name
self.auth_url = auth_url
self._keystoneclient = None
self._neutronclient = None
self._novaclient = None
@property
def auth_version(self):
if self.auth_url.endswith('v3') or self.auth_url.endswith('identity'):
return 3
else:
return 2.0
@property
def keystone_session(self):
auth_plugin = ksauth.get_plugin_class('password')
_args = {
'auth_url': self.auth_url,
'username': self.username,
'password': self.password,
}
if self.auth_version == 3:
_args.update({
'user_domain_name': DEFAULT_DOMAIN,
'project_domain_name': DEFAULT_DOMAIN,
'project_name': self.tenant_name,
})
else:
_args.update({
'tenant_name': self.tenant_name,
})
_auth = auth_plugin(**_args)
return kssession.Session(auth=_auth)
@property
def novaclient(self):
if not self._novaclient:
self._novaclient = _novaclient.Client(
version=2,
session=self.keystone_session,
)
return self._novaclient
@property
def neutronclient(self):
if not self._neutronclient:
self._neutronclient = _neutronclient.Client(
session=self.keystone_session,
)
return self._neutronclient
@property
def keystoneclient(self):
if not self._keystoneclient:
client = _keystoneclient.Client(session=self.keystone_session)
self._keystoneclient = client
return self._keystoneclient
@property
def tenant_id(self):
return self.keystoneclient.tenant_id
class ApplianceServerNotFound(Exception):
pass
class ApplianceServerTimeout(Exception):
pass
class AdminClientManager(ClientManager):
"""A client manager using credentials loaded from test.conf, which
are assumed to be admin.
"""
def __init__(self):
parse_config()
super(AdminClientManager, self).__init__(
username=CONF.os_username,
password=CONF.os_password,
tenant_name=CONF.os_tenant_name,
auth_url=CONF.os_auth_url,
)
def get_router_appliance_server(self, router_uuid, retries=10,
wait_for_active=False, ha_router=False):
"""Returns a Nova server object for router"""
LOG.debug(
'Looking for nova backing instance for resource %s',
router_uuid)
if ha_router:
exp_instances = 2
else:
exp_instances = 1
for i in six.moves.range(retries):
service_instances = \
[instance for instance in
self.novaclient.servers.list(
search_opts={
'all_tenants': 1,
'tenant_id': CONF.service_tenant_id}
) if router_uuid in instance.name]
if service_instances and len(service_instances) == exp_instances:
LOG.debug(
'Found %s backing instance for resource %s: %s',
exp_instances, router_uuid, service_instances)
break
LOG.debug('%s backing instance not found, will retry %s/%s',
exp_instances, i, retries)
time.sleep(1)
else:
raise ApplianceServerNotFound(
'Could not get nova %s server(s) for router %s' %
(exp_instances, router_uuid))
def _wait_for_active(instance):
LOG.debug('Waiting for backing instance %s to become ACTIVE',
instance)
for i in six.moves.range(CONF.appliance_active_timeout):
instance = self.novaclient.servers.get(
instance.id)
if instance.status == 'ACTIVE':
LOG.debug('Instance %s status==ACTIVE', instance)
return
else:
LOG.debug('Instance %s status==%s, will wait',
instance, instance.status)
time.sleep(1)
raise ApplianceServerTimeout(
('Timed out waiting for backing instance of %s %s to become '
'ACTIVE') % router_uuid)
if wait_for_active:
LOG.debug('Waiting for %s backing instances to become ACTIVE',
exp_instances)
[_wait_for_active(i) for i in service_instances]
LOG.debug('Waiting for backing instance %s to become ACTIVE',
exp_instances)
if ha_router:
return sorted(service_instances, key=lambda i: i.name)
else:
return service_instances[0]
def get_network_info(self, network_name):
net_response = self.neutronclient.list_networks(name=network_name)
network = net_response.get('networks', [None])[0]
return network
class TestTenant(object):
def __init__(self):
parse_config()
self.username = data_utils.rand_name(name='user', prefix='akanda')
self.user_id = None
self.password = data_utils.rand_password()
self.tenant_name = data_utils.rand_name(name='tenant', prefix='akanda')
self.tenant_id = None
self.role_name = data_utils.rand_name(name='role', prefix='akanda')
self._admin_clients = AdminClientManager()
self._admin_ks_client = self._admin_clients.keystoneclient
self.auth_url = self._admin_ks_client.auth_url
# create the tenant before creating its clients.
self._create_tenant()
self.clients = ClientManager(self.username, self.password,
self.tenant_name, self.auth_url)
self.tester = ClientManager('demo', 'akanda', 'demo', self.auth_url)
self._subnets = []
self._routers = []
def _create_tenant(self):
if self._admin_clients.auth_version == 3:
tenant = self._admin_ks_client.projects.create(
name=self.tenant_name,
domain=DEFAULT_DOMAIN)
user = self._admin_ks_client.users.create(
name=self.username,
password=self.password,
project_domain_name=DEFAULT_DOMAIN,
default_project=self.tenant_name)
role = self._admin_ks_client.roles.create(name=self.role_name)
self._admin_ks_client.roles.grant(
role=role, user=user, project=tenant)
else:
tenant = self._admin_ks_client.tenants.create(self.tenant_name)
self.tenant_id = tenant.id
user = self._admin_ks_client.users.create(
name=self.username,
password=self.password,
tenant_id=self.tenant_id)
self.user_id = user.id
self.tenant_id = tenant.id
LOG.debug('Created new test tenant: %s (%s)',
self.tenant_id, self.user_id)
def setup_networking(self, ha_router=False):
""""Create a network + subnet for the tenant. Also creates a router
if required, and attaches the subnet to it.
:returns: a (network dict, router dict) tuple
"""
# NOTE(adam_g): I didn't expect simply creating a network
# to also create a subnet and router automatically, but this
# does?
net_body = {
'network': {
'name': data_utils.rand_name(name='network', prefix='ak'),
'admin_state_up': False,
'tenant_id': self.tenant_id
}}
LOG.debug('Creating network: %s', net_body)
network = self.clients.neutronclient.create_network(net_body)
network = network.get('network')
if not network:
raise Exception('Failed to create default tenant network')
LOG.debug('Created network: %s', network)
if not CONF.astara_auto_add_resources:
addr = netaddr.IPNetwork(CONF.test_subnet_cidr)
subnet_body = {
'subnet': {
'name': data_utils.rand_name(name='subnet', prefix='ak'),
'network_id': network['id'],
'cidr': CONF.test_subnet_cidr,
'ip_version': addr.version,
}
}
LOG.debug('Creating subnet: %s', subnet_body)
subnet = self.clients.neutronclient.create_subnet(
body=subnet_body)['subnet']
LOG.debug('Created subnet: %s', subnet)
router_body = {
'router': {
'name': data_utils.rand_name(name='router', prefix='ak'),
'admin_state_up': True,
'tenant_id': self.tenant_id,
'ha': ha_router,
}
}
LOG.debug('Creating router: %s', router_body)
router = self._admin_clients.neutronclient.create_router(
body=router_body)['router']
LOG.debug('Created router: %s', router)
LOG.debug(
'Attaching interface on subnet %s to router %s',
subnet['id'], router['id'])
self.clients.neutronclient.add_interface_router(
router['id'], {'subnet_id': subnet['id']}
)
LOG.debug(
'Attached interface on subnet %s to router %s',
subnet['id'], router['id'])
else:
# routers report as ACTIVE initially (LP: #1491673)
time.sleep(2)
LOG.debug('Waiting for astara auto-created router')
for i in six.moves.range(CONF.appliance_active_timeout):
routers = self.clients.neutronclient.list_routers()
routers = routers.get('routers')
if routers:
router = routers[0]
LOG.debug('Found astara auto-created router: %s', router)
break
else:
LOG.debug(
'Still waiting for auto-created router. %s/%s',
i, CONF.appliance_active_timeout)
time.sleep(1)
else:
raise Exception('Timed out waiting for default router.')
# routers report as ACTIVE initially (LP: #1491673)
time.sleep(2)
return network, router
def _wait_for_backing_instance_delete(self, resource_id):
i = 1
LOG.debug(
'Waiting on deletion of backing instance for resource %s',
resource_id)
for i in six.moves.range(DEFAULT_DELETE_TIMEOUT):
try:
self._admin_clients.get_router_appliance_server(
resource_id, retries=1)
except ApplianceServerNotFound:
LOG.debug('Backing instance for resource %s deleted',
resource_id)
return
LOG.debug(
'Still waiting for deletion of backing instance for %s'
' , will wait (%s/%s)',
resource_id, i, DEFAULT_DELETE_TIMEOUT)
time.sleep(1)
m = ('Timed out waiting on deletion of backing instance for %s '
'after %s sec.' % (resource_id, DEFAULT_DELETE_TIMEOUT))
LOG.debug(m)
raise ApplianceServerTimeout(m)
def _wait_for_neutron_delete(self, thing, ids):
show = getattr(self.clients.neutronclient, 'show_' + thing)
attempt = 0
max_attempts = 10
for i in ids:
LOG.debug('Waiting for deletion of %s %s', thing, i)
while True:
try:
show(i)
except neutron_exceptions.NeutronClientException as e:
if e.status_code == 404:
LOG.debug('Deletion of %s %s complete', thing, i)
break
if attempt == max_attempts:
raise Exception(
'Timed out waiting for deletion of %s %s after %s sec.'
% (thing, i, max_attempts))
LOG.debug(
'Still waiting for deletion of %s %s, will wait (%s/%s)',
thing, i, attempt, max_attempts)
attempt += 1
time.sleep(1)
# also wait for nova backing instance to delete after routers
if thing in ['router']:
[self._wait_for_backing_instance_delete(i) for i in ids]
def cleanup_neutron(self):
"""Clean tenant environment of neutron resources"""
LOG.debug('Cleaning up created neutron resources')
router_interface_ports = [
p for p in self.clients.neutronclient.list_ports()['ports']
if (
'router_interface' in p['device_owner'] or
'ha_router_replicated_interface' in p['device_owner']
)]
for rip in router_interface_ports:
LOG.debug('Deleting router interface port: %s', rip)
self.clients.neutronclient.remove_interface_router(
rip['device_id'],
body=dict(port_id=router_interface_ports[0]['id']))
astara_router_ports = []
router_ids = [
r['id'] for r in
self.clients.neutronclient.list_routers().get('routers')
]
for rid in router_ids:
for p in ['MGT', 'VRRP']:
name = 'ASTARA:%s:%s' % (p, rid)
astara_router_ports += [
p['id'] for p in
self._admin_clients.neutronclient.list_ports(
name=name).get('ports')]
LOG.debug('Deleting router %s' % rid)
try:
self.clients.neutronclient.delete_router(r['id'])
except neutron_exceptions.NeutronClientException as e:
if e.status_code == 404:
router_ids.remove(rid)
else:
raise
self._wait_for_neutron_delete('router', router_ids)
time.sleep(2)
port_ids = [
p['id'] for p in
self.clients.neutronclient.list_ports().get('ports')]
for pid in port_ids:
LOG.debug('Deleting port: %s', pid)
try:
self.clients.neutronclient.delete_port(pid)
except neutron_exceptions.NeutronClientException as e:
if e.status_code == 404:
port_ids.remove(pid)
else:
raise
self._wait_for_neutron_delete('port', port_ids)
subnet_ids = [
s['id']
for s in self.clients.neutronclient.list_subnets().get('subnets')]
for sid in subnet_ids:
LOG.debug('Deleting subnet: %s', sid)
try:
self.clients.neutronclient.delete_subnet(sid)
except neutron_exceptions.NeutronClientException as e:
if e.status_code == 404:
subnet_ids.remove(sid)
else:
raise
self._wait_for_neutron_delete('subnet', subnet_ids)
# need to make sure the vrrp and mgt ports get deleted
# in time before the delete_network()
for p in astara_router_ports:
try:
self._admin_clients.neutronclient.delete_port(p)
except neutron_exceptions.NeutronClientException as e:
if e.status_code == 404:
astara_router_ports.remove(p)
else:
raise
self._wait_for_neutron_delete('port', astara_router_ports)
networks = self.clients.neutronclient.list_networks().get('networks')
net_ids = [
n['id'] for n in networks if n['tenant_id'] == self.tenant_id]
for nid in net_ids:
LOG.debug('Deleting network: %s', nid)
try:
self.clients.neutronclient.delete_network(nid)
except neutron_exceptions.NeutronClientException as e:
if e.status_code == 404:
net_ids.remove(nid)
else:
raise
self._wait_for_neutron_delete('network', net_ids)
def cleanUp(self):
self.cleanup_neutron()
self._admin_ks_client.users.delete(self.user_id)
if self._admin_clients.auth_version == 3:
self._admin_ks_client.projects.delete(self.tenant_id)
else:
self._admin_ks_client.tenants.delete(self.tenant_id)
class AstaraFunctionalBase(testtools.TestCase):
_test_tenants = []
def setUp(self):
super(AstaraFunctionalBase, self).setUp()
log_format = '%(asctime)s.%(msecs)03d ' + self.id() + ' %(message)s'
cfg.CONF.set_default('logging_default_format_string', log_format)
parse_config()
self.ak_client = astara_client
self.admin_clients = AdminClientManager()
@classmethod
def setUpClass(cls):
cls._test_tenants = []
@classmethod
def tearDownClass(cls):
try:
[t.cleanUp() for t in cls._test_tenants]
except ksc_exceptions.NotFound:
pass
@classmethod
def get_tenant(cls):
"""Creates a new test tenant
This tenant is assumed to be empty of any cloud resources
and will be destroyed on test class teardown.
"""
tenant = TestTenant()
cls._test_tenants.append(tenant)
return tenant
def get_router_appliance_server(self, router_uuid, retries=10,
wait_for_active=False, ha_router=False):
"""Returns a Nova server object for router"""
return self.admin_clients.get_router_appliance_server(
router_uuid, retries, wait_for_active, ha_router)
def get_management_address(self, router_uuid, retries=10):
LOG.debug('Getting management address for resource %s', router_uuid)
service_instance = self.get_router_appliance_server(
router_uuid,
retries=retries,
wait_for_active=True
)
mgt_network = self.admin_clients.get_network_info(
CONF.management_network_name
)
for interface in service_instance.interface_list():
if interface.net_id == mgt_network['id']:
addr = interface.fixed_ips[0]['ip_address']
LOG.debug(
'Got management address %s for resource %s',
addr,
router_uuid
)
return addr
else:
raise Exception(
'"mgt" port not found on service instance %s (%s)' %
(service_instance.id, service_instance.name))
def assert_router_is_active(self, router_uuid, ha_router=False):
LOG.debug('Waiting for resource %s to become ACTIVE', router_uuid)
for i in six.moves.range(CONF.appliance_active_timeout):
res = self.admin_clients.neutronclient.show_router(router_uuid)
router = res['router']
if router['status'] == 'ACTIVE':
LOG.debug('Router %s ACTIVE after %s sec.', router_uuid, i)
return
service_instances = self.get_router_appliance_server(
router_uuid, wait_for_active=True, ha_router=ha_router)
if not ha_router:
service_instances = [service_instances]
for instance in service_instances:
if instance.status == 'ERROR':
raise Exception(
'Backing instance %s for router %s in ERROR state',
instance.id, router_uuid)
LOG.debug(
'Resource %s not active. Status==%s, will wait, %s/%s sec.',
router_uuid, router['status'], i,
CONF.appliance_active_timeout)
time.sleep(1)
raise Exception(
'Timed out waiting for router %s to become ACTIVE, '
'current status=%s' % (router_uuid, router['status']))
def ping_router_mgt_address(self, router_uuid):
mgt_address = self.get_management_address(router_uuid)
program = {4: 'ping', 6: 'ping6'}
mgt_ip_version = netaddr.IPNetwork(mgt_address).version
cmd = [program[mgt_ip_version], '-c30', mgt_address]
LOG.debug('Pinging resource %s: %s', router_uuid, ' '.join(cmd))
try:
subprocess.check_call(cmd)
except:
raise Exception('Failed to ping router with command: %s' % cmd)

View File

@ -1,67 +0,0 @@
# Copyright (c) 2016 Akanda, 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.
import itertools
from oslo_config import cfg
functional_test_opts = [
cfg.StrOpt(
'os_auth_url', required=True,
help='Keystone auth URL'),
cfg.StrOpt(
'os_username', required=True,
help='Username of admin user'),
cfg.StrOpt(
'os_password', required=True,
help='Password of admin user'),
cfg.StrOpt(
'os_tenant_name', required=True,
help='Tenant name of admin user'),
cfg.StrOpt(
'service_tenant_id', required=True,
help='Tenant ID for the astara service user'),
cfg.StrOpt(
'service_tenant_name', required=True,
help='Tenant name of the astara service user'),
cfg.StrOpt(
'appliance_api_port', required=True,
help='The port on which appliance API servers listen'),
cfg.BoolOpt(
'astara_auto_add_resources', required=False, default=True,
help='Whether astara-neutron is configured to auto-add resources'),
cfg.IntOpt(
'appliance_active_timeout', required=False, default=340,
help='Timeout (sec) for an appliance to become ACTIVE'),
cfg.StrOpt(
'test_subnet_cidr', required=False, default='10.1.1.0/24'),
cfg.IntOpt(
'health_check_period', required=False, default=60,
help='Time health_check_period astara-orchestrator is configured to '
'use'),
cfg.StrOpt(
'management_network_name', required=False, default='mgt',
help='The name of the management network')
]
def list_opts():
return [
('functional',
itertools.chain(functional_test_opts))]
def register_opts():
cfg.CONF.register_opts(functional_test_opts)

View File

@ -1,42 +0,0 @@
[DEFAULT]
[functional]
#
# From astara.test.functional
#
# Password of admin user (string value)
#os_password = <None>
# The port on which appliance API servers listen (string value)
#appliance_api_port = <None>
# Timeout (sec) for an appliance to become ACTIVE (integer value)
#appliance_active_timeout = 340
# Time health_check_period astara-orchestrator is configured to use (integer
# value)
#health_check_period = 60
# Tenant ID for the astara service user (string value)
#service_tenant_id = <None>
# Tenant name of admin user (string value)
#os_tenant_name = <None>
# Keystone auth URL (string value)
#os_auth_url = <None>
# Whether astara-neutron is configured to auto-add resources (boolean value)
#astara_auto_add_resources = true
# (string value)
#test_subnet_cidr = 10.1.1.0/24
# Username of admin user (string value)
#os_username = <None>
# Tenant name of the astara service user (string value)
#service_tenant_name = <None>

View File

@ -1,134 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
import time
from oslo_config import cfg
from oslo_log import log as logging
from astara.test.functional import base
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class AstaraRouterTestBase(base.AstaraFunctionalBase):
HA_ROUTER = False
@classmethod
def setUpClass(cls):
super(AstaraRouterTestBase, cls).setUpClass()
cls.tenant = cls.get_tenant()
cls.neutronclient = cls.tenant.clients.neutronclient
cls.network, cls.router = cls.tenant.setup_networking(
ha_router=cls.HA_ROUTER)
def setUp(self):
super(AstaraRouterTestBase, self).setUp()
self.assert_router_is_active(self.router['id'], self.HA_ROUTER)
# refresh router ref now that its active
router = self.neutronclient.show_router(self.router['id'])
self.router = router['router']
@property
def router_ha(self):
router = self.admin_clients.neutronclient.show_router(
self.router['id'])['router']
return router.get('ha', False)
class TestAstaraHARouter(AstaraRouterTestBase):
HA_ROUTER = True
def test_ha_router_servers(self):
service_instances = self.get_router_appliance_server(
self.router['id'], ha_router=self.HA_ROUTER)
self.assertEqual(2, len(service_instances))
# kill the master and ensure it is backfilled with a new instance
master, backup = service_instances
self.admin_clients.novaclient.servers.delete(master.id)
LOG.debug('Waiting %s seconds for astara health check to tick',
CONF.health_check_period)
time.sleep(CONF.health_check_period)
service_instances = self.get_router_appliance_server(
self.router['id'], retries=600, ha_router=self.HA_ROUTER)
self.assertEqual(2, len(service_instances))
self.assertEqual(backup, service_instances[0])
class TestAstaraRouter(AstaraRouterTestBase):
HA_ROUTER = False
def test_router_recovery(self):
"""
Test that creation of network/subnet/router results in a
correctly plugged appliance, and that manually destroying the
Nova instance results in a new appliance being booted.
"""
self.skipTest("Race condition makes this test too unstable")
# for each subnet that was created during setup, ensure we have a
# router interface added
ports = self.neutronclient.list_ports(
device_owner='network:router_interface')['ports']
subnets = self.neutronclient.list_subnets(
network_id=self.network['id'])
subnets = subnets['subnets']
self.assertEqual(len(ports), len(subnets))
for port in ports:
self.assertEqual(port['device_id'], self.router['id'])
self.assertEqual(
sorted([subnet['id'] for subnet in subnets]),
sorted([fip['subnet_id'] for fip in port['fixed_ips']])
)
self.ping_router_mgt_address(self.router['id'])
# Ensure that if we destroy the nova instance, the RUG will rebuild
# the router with a new instance.
# This could live in a separate test case but it'd require the
# above as setup, so just piggyback on it.
old_server = self.get_router_appliance_server(self.router['id'])
LOG.debug('Original server: %s', old_server)
# NOTE(adam_g): In the gate, sometimes the appliance hangs on the
# first config update and health checks get queued up behind the
# hanging config update. If thats the case, we need to wait a while
# before deletion for the first to timeout.
time.sleep(30)
LOG.debug('Deleting original nova server: %s', old_server.id)
self.admin_clients.novaclient.servers.delete(old_server.id)
LOG.debug('Waiting %s seconds for astara health check to tick',
CONF.health_check_period)
time.sleep(CONF.health_check_period)
# look for the new server, retry giving rug time to do its thing.
new_server = self.get_router_appliance_server(
self.router['id'], retries=600, wait_for_active=True)
LOG.debug('Rebuilt new server found: %s', new_server)
self.assertNotEqual(old_server.id, new_server.id)
# routers report as ACTIVE initially (LP: #1491673)
time.sleep(2)
self.assert_router_is_active(self.router['id'])
self.ping_router_mgt_address(self.router['id'])

View File

@ -1,15 +0,0 @@
# 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.

View File

@ -1,15 +0,0 @@
# 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.

View File

@ -1,15 +0,0 @@
# 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.

View File

@ -1,127 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
import netaddr
from astara.api.neutron import Subnet
class FakeModel(object):
def __init__(self, id_, **kwargs):
self.id = id_
self.__dict__.update(kwargs)
fake_ext_port = FakeModel(
'1',
mac_address='aa:bb:cc:dd:ee:ff',
network_id='ext-net',
fixed_ips=[FakeModel('', ip_address='9.9.9.9', subnet_id='s2')],
first_v4='9.9.9.9',
device_id='e-e-e-e')
fake_mgt_port = FakeModel(
'2',
name='ASTARA:MGT:foo',
mac_address='aa:bb:cc:cc:bb:aa',
network_id='mgt-net',
device_id='m-m-m-m')
fake_int_port = FakeModel(
'3',
name='ASTARA:RUG:foo',
mac_address='aa:aa:aa:aa:aa:aa',
network_id='int-net',
fixed_ips=[FakeModel('', ip_address='192.168.1.1', subnet_id='s1')],
device_id='i-i-i-i')
fake_instance_port = FakeModel(
'4',
name='foo',
mac_address='aa:aa:aa:aa:aa:bb',
network_id='int-net',
fixed_ips=[FakeModel('', ip_address='192.168.1.2', subnet_id='s1')],
first_v4='192.168.1.2',
device_id='v-v-v-v')
fake_instance_mgt_port = FakeModel(
'4',
name='ASTARA:MGT:foo',
mac_address='aa:aa:aa:aa:aa:bb',
network_id='int-net',
fixed_ips=[FakeModel('', ip_address='192.168.1.2', subnet_id='s1')],
first_v4='192.168.1.2',
device_id='v-v-v-v')
fake_instance_vrrp_port = FakeModel(
'4',
name='ASTARA:VRRP:foo',
mac_address='aa:aa:aa:aa:aa:bb',
network_id='int-net',
fixed_ips=[FakeModel('', ip_address='192.168.1.2', subnet_id='s1')],
first_v4='192.168.1.2',
device_id='v-v-v-v')
fake_instance_lb_port = FakeModel(
'4',
name='ASTARA:LB:foo',
mac_address='aa:aa:aa:aa:aa:bb',
network_id='int-net',
fixed_ips=[FakeModel('', ip_address='192.168.1.2', subnet_id='s1')],
first_v4='192.168.1.2',
device_id='v-v-v-v')
fake_subnet = FakeModel(
's1',
cidr=netaddr.IPNetwork('192.168.1.0/24'),
gateway_ip='192.168.1.1',
enable_dhcp=True,
dns_nameservers=['8.8.8.8'],
ipv6_ra_mode=None,
host_routes={})
fake_subnet_with_slaac = Subnet(
id_='fake_id',
name='s1',
tenant_id='fake_tenant_id',
network_id='fake_network_id',
ip_version=6,
cidr='fdee:9f85:83be::/48',
gateway_ip='fdee:9f85:83be::1',
enable_dhcp=True,
dns_nameservers=['8.8.8.8'],
ipv6_ra_mode='slaac',
host_routes={})
fake_network = FakeModel(
'fake_network_id',
name='thenet',
tenant_id='tenant_id',
status='ACTIVE',
shared=False,
admin_statue_up=True,
mtu=1280,
port_security_enabled=False,
subnets=[fake_subnet]
)
fake_router = FakeModel(
'router_id',
tenant_id='tenant_id',
name='router_name',
external_port=fake_ext_port,
management_port=fake_mgt_port,
internal_ports=[fake_int_port],
ha=False)

View File

@ -1,202 +0,0 @@
# 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 mock
import netaddr
import unittest2 as unittest
from astara.api.config import common
from astara.test.unit.api.config import config_fakes as fakes
class TestCommonConfig(unittest.TestCase):
def test_network_config(self):
mock_client = mock.Mock()
mock_client.get_network_detail.return_value = fakes.fake_network
subnets_dict = {fakes.fake_subnet.id: fakes.fake_subnet}
with mock.patch.object(common, '_make_network_config_dict') as nc:
with mock.patch.object(common, '_interface_config') as ic:
mock_interface = mock.Mock()
ic.return_value = mock_interface
common.network_config(
mock_client,
fakes.fake_int_port,
'ge1',
'internal',
[])
ic.assert_called_once_with(
'ge1', fakes.fake_int_port, subnets_dict, 1280)
nc.assert_called_once_with(
mock_interface,
'internal',
'int-net',
mtu=1280,
subnets_dict=subnets_dict,
network_ports=[]),
def test_make_network_config(self):
interface = {'ifname': 'ge2'}
result = common._make_network_config_dict(
interface,
'internal',
fakes.fake_int_port.network_id,
1280,
'dhcp',
'ra',
subnets_dict={fakes.fake_subnet.id: fakes.fake_subnet},
network_ports=[fakes.fake_instance_port])
expected = {
'interface': interface,
'network_id': fakes.fake_int_port.network_id,
'v4_conf_service': 'dhcp',
'v6_conf_service': 'ra',
'network_type': 'internal',
'mtu': 1280,
'subnets': [{'cidr': '192.168.1.0/24',
'dhcp_enabled': True,
'dns_nameservers': ['8.8.8.8'],
'gateway_ip': '192.168.1.1',
'host_routes': {},
'id': 's1'}],
'allocations': [
{
'mac_address': 'aa:aa:aa:aa:aa:bb',
'ip_addresses': {'192.168.1.2': True},
'hostname': '192-168-1-2.local',
'device_id': 'v-v-v-v'
}
]
}
self.assertEqual(expected, result)
def test_interface_config(self):
expected = {
'addresses': ['192.168.1.1/24'],
'ifname': 'ge1',
'mtu': 1280
}
subnets_dict = {fakes.fake_subnet.id: fakes.fake_subnet}
self.assertEqual(
expected,
common._interface_config(
'ge1',
fakes.fake_int_port,
subnets_dict,
1280
)
)
def test_subnet_config(self):
expected = {
'cidr': '192.168.1.0/24',
'dhcp_enabled': True,
'dns_nameservers': ['8.8.8.8'],
'gateway_ip': '192.168.1.1',
'host_routes': {},
'id': 's1',
}
self.assertEqual(expected, common._subnet_config(fakes.fake_subnet))
def test_subnet_config_with_slaac_enabled(self):
expected = {
'cidr': 'fdee:9f85:83be::/48',
'dhcp_enabled': False,
'dns_nameservers': ['8.8.8.8'],
'gateway_ip': 'fdee:9f85:83be::1',
'host_routes': {},
'id': 'fake_id',
}
self.assertEqual(
expected, common._subnet_config(fakes.fake_subnet_with_slaac))
def test_subnet_config_no_gateway(self):
expected = {
'cidr': '192.168.1.0/24',
'dhcp_enabled': True,
'dns_nameservers': ['8.8.8.8'],
'gateway_ip': '',
'host_routes': {},
'id': 's1',
}
sn = fakes.FakeModel(
's1',
cidr=netaddr.IPNetwork('192.168.1.0/24'),
gateway_ip='',
enable_dhcp=True,
dns_nameservers=['8.8.8.8'],
ipv6_ra_mode='',
host_routes={})
self.assertEqual(expected, common._subnet_config(sn))
def test_subnet_config_gateway_none(self):
expected = {
'cidr': '192.168.1.0/24',
'dhcp_enabled': True,
'dns_nameservers': ['8.8.8.8'],
'gateway_ip': '',
'host_routes': {},
'id': 's1',
}
sn = fakes.FakeModel(
's1',
cidr=netaddr.IPNetwork('192.168.1.0/24'),
gateway_ip=None,
enable_dhcp=True,
dns_nameservers=['8.8.8.8'],
ipv6_ra_mode='',
host_routes={})
self.assertEqual(expected, common._subnet_config(sn))
def test_allocation_config_vrrp(self):
subnets_dict = {fakes.fake_subnet.id: fakes.fake_subnet}
self.assertEqual(
[],
common._allocation_config(
[fakes.fake_instance_vrrp_port],
subnets_dict)
)
def test_allocation_config_lb(self):
subnets_dict = {fakes.fake_subnet.id: fakes.fake_subnet}
self.assertEqual(
[],
common._allocation_config(
[fakes.fake_instance_lb_port],
subnets_dict)
)
def test_allocation_config_mgt(self):
subnets_dict = {fakes.fake_subnet.id: fakes.fake_subnet}
expected = [
{'mac_address': 'aa:aa:aa:aa:aa:bb',
'ip_addresses': {'192.168.1.2': True},
'hostname': '192-168-1-2.local',
'device_id': 'v-v-v-v'}
]
self.assertEqual(
expected,
common._allocation_config([
fakes.fake_instance_mgt_port],
subnets_dict)
)

View File

@ -1,51 +0,0 @@
# 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 mock
from astara.api.config import loadbalancer as lb_config
from astara.test.unit import base, fakes
class TestLoadbalancerConfigAPI(base.RugTestBase):
@mock.patch('astara.api.config.common.network_config')
def test_build_config(self, fake_network_config):
fake_client = mock.Mock()
fake_lb = fakes.fake_loadbalancer()
fake_lb_net = mock.Mock()
fake_mgt_net = mock.Mock()
fake_mgt_port = mock.Mock(
network_id='fake_mgt_network_id',
)
fake_iface_map = {
fake_lb.vip_port.network_id: fake_lb_net,
fake_mgt_port.network_id: fake_mgt_net,
}
fake_network_config.side_effect = [
'fake_lb_net_dict', 'fake_mgt_net_dict'
]
res = lb_config.build_config(
fake_client, fake_lb, fake_mgt_port, fake_iface_map)
expected = {
'hostname': 'ak-loadbalancer-%s' % fake_lb.tenant_id,
'tenant_id': fake_lb.tenant_id,
'networks': ['fake_lb_net_dict', 'fake_mgt_net_dict'],
'services': {
'loadbalancer': fake_lb.to_dict(),
}
}
self.assertEqual(expected, res)

View File

@ -1,292 +0,0 @@
# 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 mock
import netaddr
from oslo_config import cfg
import unittest2 as unittest
from six.moves import builtins as __builtins__
from astara.api.config import router as conf_mod
from astara.test.unit.api.config import config_fakes as fakes
class TestAstaraClient(unittest.TestCase):
def setUp(self):
cfg.CONF.set_override('provider_rules_path', '/the/path')
def tearDown(self):
cfg.CONF.reset()
def test_build_config(self):
methods = {
'load_provider_rules': mock.DEFAULT,
'generate_network_config': mock.DEFAULT,
'generate_floating_config': mock.DEFAULT,
'get_default_v4_gateway': mock.DEFAULT,
}
fake_orchestrator = {
'host': 'foohost',
'adddress': '10.0.0.1',
'metadata_port': 80,
}
mock_client = mock.Mock()
mock_context = mock.Mock(
neutron=mock_client,
config=fake_orchestrator,
)
ifaces = []
provider_rules = {'labels': {'ext': ['192.168.1.1']}}
network_config = [
{'interface': 1,
'network_id': 2,
'v4_conf_service': 'static',
'v6_conf_service': 'static',
'network_type': 'external',
'subnets': [
{'cidr': '192.168.1.0/24',
'dhcp_enabled': True,
'dns_nameservers': [],
'host_routes': [],
'gateway_ip': '192.168.1.1',
},
{'cidr': '10.0.0.0/24',
'dhcp_enabled': True,
'dns_nameservers': [],
'host_routes': [],
'gateway_ip': '10.0.0.1',
}, ],
'allocations': []}
]
with mock.patch.multiple(conf_mod, **methods) as mocks:
mocks['load_provider_rules'].return_value = provider_rules
mocks['generate_network_config'].return_value = network_config
mocks['generate_floating_config'].return_value = 'floating_config'
mocks['get_default_v4_gateway'].return_value = 'default_gw'
config = conf_mod.build_config(mock_context, fakes.fake_router,
fakes.fake_mgt_port, ifaces)
expected = {
'default_v4_gateway': 'default_gw',
'networks': network_config,
'labels': {'ext': ['192.168.1.1']},
'floating_ips': 'floating_config',
'asn': 64512,
'neighbor_asn': 64512,
'tenant_id': 'tenant_id',
'ha_resource': False,
'hostname': 'ak-tenant_id',
'orchestrator': {
'host': 'foohost',
'adddress': '10.0.0.1',
'metadata_port': 80,
},
'vpn': {}
}
self.assertEqual(expected, config)
mocks['load_provider_rules'].assert_called_once_with('/the/path')
mocks['generate_network_config'].assert_called_once_with(
mock_client, fakes.fake_router, fakes.fake_mgt_port, ifaces)
def test_load_provider_rules(self):
rules_dict = {'labels': {}, 'preanchors': [], 'postanchors': []}
with mock.patch('oslo_serialization.jsonutils.load') as load:
load.return_value = rules_dict
with mock.patch('six.moves.builtins.open') as mock_open:
r = conf_mod.load_provider_rules('/the/path')
mock_open.assert_called_once_with('/the/path')
load.assert_called_once_with(mock_open.return_value)
self.assertEqual(rules_dict, r)
@mock.patch.object(__builtins__, 'open', autospec=True)
def test_load_provider_rules_not_found(self, mock_open):
mock_open.side_effect = IOError()
res = conf_mod.load_provider_rules('/tmp/path')
self.assertEqual({}, res)
@mock.patch('astara.api.config.common.network_config')
def test_generate_network_config(self, mock_net_conf):
mock_client = mock.Mock()
iface_map = {
fakes.fake_mgt_port.network_id: 'ge0',
fakes.fake_ext_port.network_id: 'ge1',
fakes.fake_int_port.network_id: 'ge2'
}
mock_net_conf.return_value = 'configured_network'
result = conf_mod.generate_network_config(
mock_client, fakes.fake_router, fakes.fake_mgt_port, iface_map)
expected = [
'configured_network',
'configured_network',
'configured_network'
]
self.assertEqual(expected, result)
expected_calls = [
mock.call(
mock_client, fakes.fake_router.management_port,
'ge0', 'management'),
mock.call(
mock_client, fakes.fake_router.external_port,
'ge1', 'external'),
mock.call(
mock_client, fakes.fake_int_port,
'ge2', 'internal', mock.ANY)]
for c in expected_calls:
self.assertIn(c, mock_net_conf.call_args_list)
mock_net_conf.assert_has_calls(expected_calls)
def test_generate_floating_config(self):
fip = fakes.FakeModel(
'id',
floating_ip=netaddr.IPAddress('9.9.9.9'),
fixed_ip=netaddr.IPAddress('192.168.1.1')
)
rtr = fakes.FakeModel('rtr_id', floating_ips=[fip])
result = conf_mod.generate_floating_config(rtr)
expected = [{'floating_ip': '9.9.9.9', 'fixed_ip': '192.168.1.1'}]
self.assertEqual(expected, result)
class TestAstaraClientGateway(unittest.TestCase):
def setUp(self):
cfg.CONF.set_override('provider_rules_path', '/the/path')
# Sample data taken from a real devstack-created system, with
# the external MAC address modified to match the fake port in
# use for the mocked router.
self.networks = [
{'subnets': [
{'host_routes': [],
'cidr': '172.16.77.0/24',
'gateway_ip': '172.16.77.1',
'dns_nameservers': [],
'dhcp_enabled': True,
'network_type': 'external'},
{'host_routes': [],
'cidr': 'fdee:9f85:83be::/48',
'gateway_ip': 'fdee:9f85:83be::1',
'dns_nameservers': [],
'dhcp_enabled': True}],
'v6_conf_service': 'static',
'network_id': u'1e109e80-4a6a-483e-9dd4-2ff31adf25f5',
'allocations': [],
'interface': {'ifname': u'ge1',
'addresses': [
'172.16.77.2/24',
'fdee:9f85:83be:0:f816:3eff:fee5:1742/48',
]},
'v4_conf_service': 'static',
'network_type': 'external'},
{'subnets': [],
'v6_conf_service': 'static',
'network_id': u'698ef1d1-1089-48ab-80b0-f994a962891c',
'allocations': [],
'interface': {
u'addresses': [
u'fe80::f816:3eff:fe4d:bf12/64',
u'fdca:3ba5:a17a:acda:f816:3eff:fe4d:bf12/64',
],
u'media': u'Ethernet autoselect',
u'lladdr': u'fa:16:3e:4d:bf:12',
u'state': u'up',
u'groups': [],
u'ifname': u'ge0',
u'mtu': 1500,
u'description': u''},
'v4_conf_service': 'static',
'network_type': 'management'},
{'subnets': [
{'host_routes': [],
'cidr': 'fdd6:a1fa:cfa8:6c94::/64',
'gateway_ip': 'fdd6:a1fa:cfa8:6c94::1',
'dns_nameservers': [],
'dhcp_enabled': False},
{'host_routes': [],
'cidr': '192.168.0.0/24',
'gateway_ip': '192.168.0.1',
'dns_nameservers': [],
'dhcp_enabled': True}],
'v6_conf_service': 'static',
'network_id': u'a1ea2256-5e57-4e9e-8b7a-8bf17eb76b73',
'allocations': [
{'mac_address': u'fa:16:3e:1b:93:76',
'ip_addresses': {
'fdd6:a1fa:cfa8:6c94::1': False,
'192.168.0.1': True},
'hostname': '192-168-0-1.local',
'device_id': u'c72a34fb-fb56-4ee7-b9b2-6467eb1c45d6'}],
'interface': {'ifname': u'ge2',
'addresses': ['192.168.0.1/24',
'fdd6:a1fa:cfa8:6c94::1/64']},
'v4_conf_service': 'static',
'network_type': 'internal'}]
def tearDown(self):
cfg.CONF.reset()
def test_with_interfaces(self):
mock_client = mock.Mock()
result = conf_mod.get_default_v4_gateway(
mock_client,
fakes.fake_router,
self.networks,
)
self.assertEqual('172.16.77.1', result)
def test_without_ipv4_on_external_port(self):
# Only set a V6 address
self.networks[0]['interface']['addresses'] = [
'fdee:9f85:83be:0:f816:3eff:fee5:1742/48',
]
mock_client = mock.Mock()
result = conf_mod.get_default_v4_gateway(
mock_client,
fakes.fake_router,
self.networks,
)
self.assertEqual('', result)
def test_extra_ipv4_on_external_port(self):
self.networks[0]['interface']['addresses'] = [
u'fe80::f816:3eff:fe4d:bf12/64',
u'fdca:3ba5:a17a:acda:f816:3eff:fe4d:bf12/64',
u'192.168.1.1',
u'172.16.77.2',
]
mock_client = mock.Mock()
result = conf_mod.get_default_v4_gateway(
mock_client,
fakes.fake_router,
self.networks,
)
self.assertEqual('172.16.77.1', result)

View File

@ -1,140 +0,0 @@
# 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 mock
import unittest2 as unittest
from astara.api import astara_client
class TestAstaraClient(unittest.TestCase):
def setUp(self):
self.mock_create_session = mock.patch.object(
astara_client,
'_get_proxyless_session'
).start()
self.mock_get = self.mock_create_session.return_value.get
self.mock_put = self.mock_create_session.return_value.put
self.mock_post = self.mock_create_session.return_value.post
self.addCleanup(mock.patch.stopall)
def test_mgt_url(self):
self.assertEqual('http://[fe80::2]:5000/',
astara_client._mgt_url('fe80::2', 5000, '/'))
self.assertEqual('http://192.168.1.1:5000/',
astara_client._mgt_url('192.168.1.1', 5000, '/'))
def test_is_alive_success(self):
self.mock_get.return_value.status_code = 200
self.assertTrue(astara_client.is_alive('fe80::2', 5000))
self.mock_get.assert_called_once_with(
'http://[fe80::2]:5000/v1/firewall/rules',
timeout=3.0
)
def test_is_alive_bad_status(self):
self.mock_get.return_value.status_code = 500
self.assertFalse(astara_client.is_alive('fe80::2', 5000))
self.mock_get.assert_called_once_with(
'http://[fe80::2]:5000/v1/firewall/rules',
timeout=3.0
)
def test_is_alive_exception(self):
self.mock_get.side_effect = Exception
self.assertFalse(astara_client.is_alive('fe80::2', 5000))
self.mock_get.assert_called_once_with(
'http://[fe80::2]:5000/v1/firewall/rules',
timeout=3.0
)
def test_get_interfaces(self):
self.mock_get.return_value.status_code = 200
self.mock_get.return_value.json.return_value = {
'interfaces': 'the_interfaces'
}
self.assertEqual('the_interfaces',
astara_client.get_interfaces('fe80::2', 5000))
self.mock_get.assert_called_once_with(
'http://[fe80::2]:5000/v1/system/interfaces',
timeout=30
)
def test_update_config(self):
config = {'foo': 'bar'}
self.mock_put.return_value.status_code = 200
self.mock_put.return_value.json.return_value = config
resp = astara_client.update_config('fe80::2', 5000, config)
self.mock_put.assert_called_once_with(
'http://[fe80::2]:5000/v1/system/config',
data=b'{"foo": "bar"}',
headers={'Content-type': 'application/json'},
timeout=90)
self.assertEqual(config, resp)
def test_update_config_with_custom_config(self):
config = {'foo': 'bar'}
self.mock_put.return_value.status_code = 200
self.mock_put.return_value.json.return_value = config
with mock.patch.object(astara_client.cfg, 'CONF') as cfg:
cfg.config_timeout = 5
resp = astara_client.update_config('fe80::2', 5000, config)
self.mock_put.assert_called_once_with(
'http://[fe80::2]:5000/v1/system/config',
data=b'{"foo": "bar"}',
headers={'Content-type': 'application/json'},
timeout=5)
self.assertEqual(config, resp)
def test_update_config_failure(self):
config = {'foo': 'bar'}
self.mock_put.return_value.status_code = 500
self.mock_put.return_value.text = 'error_text'
with self.assertRaises(Exception):
astara_client.update_config('fe80::2', 5000, config)
self.mock_put.assert_called_once_with(
'http://[fe80::2]:5000/v1/system/config',
data=b'{"foo": "bar"}',
headers={'Content-type': 'application/json'},
timeout=90
)
def test_read_labels(self):
self.mock_post.return_value.status_code = 200
self.mock_post.return_value.json.return_value = {
'labels': ['label1', 'label2']
}
resp = astara_client.read_labels('fe80::2', 5000)
self.mock_post.assert_called_once_with(
'http://[fe80::2]:5000/v1/firewall/labels',
timeout=30
)
self.assertEqual(['label1', 'label2'], resp)

View File

@ -1,38 +0,0 @@
# Copyright (c) 2015 Akanda, 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.
import mock
from astara.api import keystone
from astara.test.unit import base
from oslo_config import cfg
class KeystoneTest(base.RugTestBase):
def setUp(self):
super(KeystoneTest, self).setUp()
self.config(auth_region='foo_regin')
@mock.patch('keystoneclient.session.Session')
@mock.patch('keystoneclient.auth.load_from_conf_options')
def test_session(self, mock_load_auth, mock_session):
fake_auth = mock.Mock()
mock_load_auth.return_value = fake_auth
fake_session = mock.Mock()
mock_session.return_value = fake_session
ks_session = keystone.KeystoneSession().session
mock_load_auth.assert_called_with(cfg.CONF, 'keystone_authtoken')
mock_session.assert_called_with(auth=fake_auth)
self.assertEqual(fake_session, ks_session)

View File

@ -1,518 +0,0 @@
# 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 six
import mock
import netaddr
from oslo_config import cfg
from astara.test.unit import base, fakes
from astara.api import neutron
class TestuNeutronModels(base.RugTestBase):
def test_router(self):
r = neutron.Router(
'1', 'tenant_id', 'name', True, 'ACTIVE', 'ext', ['int'], ['fip'])
self.assertEqual('1', r.id)
self.assertEqual('tenant_id', r.tenant_id)
self.assertEqual('name', r.name)
self.assertTrue(r.admin_state_up)
self.assertEqual('ACTIVE', r.status)
self.assertEqual('ext', r.external_port)
self.assertEqual(['fip'], r.floating_ips)
self.assertEqual(['int'], r.internal_ports)
self.assertEqual(set(['ext', 'int']), set(r.ports))
def test_router_from_dict(self):
p = {
'name': 'ext',
'id': 'ext',
'device_id': 'device_id',
'fixed_ips': [],
'mac_address': 'aa:bb:cc:dd:ee:ff',
'network_id': 'net_id',
'device_owner': 'network:router_gateway'
}
int_p = {
'name': 'int',
'id': 'int',
'device_id': 'device_id',
'fixed_ips': [],
'mac_address': 'aa:bb:cc:dd:ee:ee',
'network_id': 'net_id',
'device_owner': 'network:router_interface'
}
int_ha_p = {
'name': 'ha_int',
'id': 'ha_int',
'device_id': 'device_id',
'fixed_ips': [],
'mac_address': 'aa:bb:cc:dd:ee:ee',
'network_id': 'net_id',
'device_owner': 'network:ha_router_replicated_interface'
}
fip = {
'id': 'fip',
'floating_ip_address': '9.9.9.9',
'fixed_ip_address': '192.168.1.1'
}
d = {
'id': '1',
'tenant_id': 'tenant_id',
'name': 'name',
'admin_state_up': True,
'status': 'ACTIVE',
'ports': [p],
'_floatingips': [fip],
'_interfaces': [int_p, int_ha_p],
}
r = neutron.Router.from_dict(d)
self.assertEqual('1', r.id)
self.assertEqual('tenant_id', r.tenant_id)
self.assertEqual('name', r.name)
self.assertTrue(r.admin_state_up)
self.assertTrue(r.floating_ips) # just make sure this exists
self.assertEqual(
['ha_int', 'int'],
sorted([ip.id for ip in r.internal_ports]))
def test_router_eq(self):
r1 = neutron.Router(
'1', 'tenant_id', 'name', True, 'ext', ['int'], 'mgt')
r2 = neutron.Router(
'1', 'tenant_id', 'name', True, 'ext', ['int'], 'mgt')
self.assertEqual(r1, r2)
def test_router_ne(self):
r1 = neutron.Router(
'1', 'tenant_id', 'name', True, 'ext', ['int'], 'mgt')
r2 = neutron.Router(
'2', 'tenant_id', 'name', True, 'ext', ['int'], 'mgt')
self.assertNotEqual(r1, r2)
def test_subnet_model(self):
d = {
'id': '1',
'tenant_id': 'tenant_id',
'name': 'name',
'network_id': 'network_id',
'ip_version': 6,
'cidr': 'fe80::/64',
'gateway_ip': 'fe80::1',
'enable_dhcp': True,
'dns_nameservers': ['8.8.8.8', '8.8.4.4'],
'ipv6_ra_mode': 'slaac',
'host_routes': []
}
s = neutron.Subnet.from_dict(d)
self.assertEqual('1', s.id)
self.assertEqual('tenant_id', s.tenant_id)
self.assertEqual('name', s.name)
self.assertEqual('network_id', s.network_id)
self.assertEqual(6, s.ip_version)
self.assertEqual(netaddr.IPNetwork('fe80::/64'), s.cidr)
self.assertEqual(netaddr.IPAddress('fe80::1'), s.gateway_ip)
self.assertTrue(s.enable_dhcp, True)
self.assertEqual(['8.8.8.8', '8.8.4.4'], s.dns_nameservers)
self.assertEqual([], s.host_routes)
def test_subnet_gateway_none(self):
d = {
'id': '1',
'tenant_id': 'tenant_id',
'name': 'name',
'network_id': 'network_id',
'ip_version': 6,
'cidr': 'fe80::/64',
'gateway_ip': None,
'enable_dhcp': True,
'dns_nameservers': ['8.8.8.8', '8.8.4.4'],
'ipv6_ra_mode': 'slaac',
'host_routes': []
}
s = neutron.Subnet.from_dict(d)
self.assertEqual(netaddr.IPNetwork('fe80::/64'), s.cidr)
self.assertIsNone(s.gateway_ip)
def test_subnet_gateway_not_ip(self):
d = {
'id': '1',
'tenant_id': 'tenant_id',
'name': 'name',
'network_id': 'network_id',
'ip_version': 6,
'cidr': 'fe80::/64',
'gateway_ip': 'something-that-is-not-an-ip',
'enable_dhcp': True,
'dns_nameservers': ['8.8.8.8', '8.8.4.4'],
'ipv6_ra_mode': 'slaac',
'host_routes': []
}
s = neutron.Subnet.from_dict(d)
self.assertEqual(netaddr.IPNetwork('fe80::/64'), s.cidr)
self.assertIsNone(s.gateway_ip)
def test_subnet_cidr_none(self):
d = {
'id': '1',
'tenant_id': 'tenant_id',
'name': 'name',
'network_id': 'network_id',
'ip_version': 6,
'cidr': None,
'gateway_ip': 'fe80::1',
'enable_dhcp': True,
'dns_nameservers': ['8.8.8.8', '8.8.4.4'],
'ipv6_ra_mode': 'slaac',
'host_routes': []
}
try:
neutron.Subnet.from_dict(d)
except ValueError as e:
self.assertIn('Invalid CIDR', six.text_type(e))
def test_subnet_cidr_not_valid(self):
d = {
'id': '1',
'tenant_id': 'tenant_id',
'name': 'name',
'network_id': 'network_id',
'ip_version': 6,
'cidr': 'something-that-is-not-an-ip',
'gateway_ip': 'fe80::1',
'enable_dhcp': True,
'dns_nameservers': ['8.8.8.8', '8.8.4.4'],
'ipv6_ra_mode': 'slaac',
'host_routes': []
}
try:
neutron.Subnet.from_dict(d)
except ValueError as e:
self.assertIn('Invalid CIDR', six.text_type(e))
def test_port_model(self):
d = {
'id': '1',
'name': 'name',
'device_id': 'device_id',
'fixed_ips': [{'ip_address': '192.168.1.1', 'subnet_id': 'sub1'}],
'mac_address': 'aa:bb:cc:dd:ee:ff',
'network_id': 'net_id',
'device_owner': 'test'
}
p = neutron.Port.from_dict(d)
self.assertEqual('1', p.id)
self.assertEqual('device_id', p.device_id)
self.assertEqual('aa:bb:cc:dd:ee:ff', p.mac_address)
self.assertEqual('test', p.device_owner)
self.assertEqual(1, len(p.fixed_ips))
def test_fixed_ip_model(self):
d = {
'subnet_id': 'sub1',
'ip_address': '192.168.1.1'
}
fip = neutron.FixedIp.from_dict(d)
self.assertEqual('sub1', fip.subnet_id)
self.assertEqual(netaddr.IPAddress('192.168.1.1'), fip.ip_address)
def test_floating_ip_model(self):
d = {
'id': 'a-b-c-d',
'floating_ip_address': '9.9.9.9',
'fixed_ip_address': '192.168.1.1'
}
fip = neutron.FloatingIP.from_dict(d)
self.assertEqual('a-b-c-d', fip.id)
self.assertEqual(netaddr.IPAddress('9.9.9.9'), fip.floating_ip)
self.assertEqual(netaddr.IPAddress('192.168.1.1'), fip.fixed_ip)
class FakeConf:
admin_user = 'admin'
admin_password = 'password'
admin_tenant_name = 'admin'
auth_url = 'http://127.0.0.1/'
auth_strategy = 'keystone'
auth_region = 'RegionOne'
class TestNeutronWrapper(base.RugTestBase):
@mock.patch('astara.api.neutron.cfg')
@mock.patch('astara.api.neutron.AstaraExtClientWrapper')
@mock.patch('astara.api.neutron.importutils')
def test_purge_management_interface(self, import_utils, ak_wrapper, cfg):
conf = mock.Mock()
driver = mock.Mock()
import_utils.import_object.return_value = driver
neutron_wrapper = neutron.Neutron(conf)
neutron_wrapper.purge_management_interface()
self.assertEqual(1, driver.get_device_name.call_count)
self.assertEqual(1, driver.unplug.call_count)
def test_clear_device_id(self):
neutron_wrapper = neutron.Neutron(mock.Mock())
neutron_wrapper.api_client.update_port = mock.Mock()
neutron_wrapper.clear_device_id(mock.Mock(id='PORT1'))
neutron_wrapper.api_client.update_port.assert_called_once_with(
'PORT1', {'port': {'device_id': ''}}
)
@mock.patch('astara.api.neutron.AstaraExtClientWrapper')
def test_neutron_router_status_update_error(self, client_wrapper):
urs = client_wrapper.return_value.update_status
urs.side_effect = RuntimeError('should be caught')
conf = mock.Mock()
neutron_wrapper = neutron.Neutron(conf)
neutron_wrapper.update_router_status('router-id', 'new-status')
@mock.patch('astara.api.neutron.AstaraExtClientWrapper')
def _test_create_vrrp_port_success_hlpr(self, ext_enabled, client_wrapper):
conf = mock.Mock()
conf.neutron_port_security_extension_enabled = ext_enabled
expected_port_data = {
'port': {
'name': 'ASTARA:VRRP:obj_id',
'admin_state_up': True,
'network_id': 'the_net_id',
'fixed_ips': [],
'security_groups': []
}
}
if ext_enabled:
expected_port_data['port']['port_security_enabled'] = False
neutron_wrapper = neutron.Neutron(conf)
api_client = neutron_wrapper.api_client
with mock.patch.object(api_client, 'create_port') as create_port:
with mock.patch.object(neutron.Port, 'from_dict') as port_from_d:
retval = neutron_wrapper.create_vrrp_port(
'obj_id',
'the_net_id'
)
self.assertIs(retval, port_from_d.return_value)
port_from_d.assert_called_once_with(
create_port.return_value.get()
)
create_port.assert_called_once_with(
expected_port_data
)
def test_create_vrrp_port_success(self):
self._test_create_vrrp_port_success_hlpr(True)
def test_create_vrrp_port_success_port_security_disabled(self):
self._test_create_vrrp_port_success_hlpr(False)
@mock.patch('astara.api.neutron.AstaraExtClientWrapper')
def test_create_vrrp_port_error(self, client_wrapper):
neutron_wrapper = neutron.Neutron(mock.Mock())
api_client = neutron_wrapper.api_client
with mock.patch.object(api_client, 'create_port') as create_port:
create_port.return_value.get.return_value = None
self.assertRaises(
ValueError,
neutron_wrapper.create_vrrp_port,
'obj_id',
'the_net_id'
)
@mock.patch('astara.api.neutron.AstaraExtClientWrapper')
def test_delete_vrrp_ports(self, client_wrapper):
conf = mock.Mock()
neutron_wrapper = neutron.Neutron(conf)
neutron_wrapper.api_client.list_ports = mock.Mock(
return_value={
'ports': [{'id': 'fake_port_id'}]
}
)
neutron_wrapper.api_client.delete_port = mock.Mock()
neutron_wrapper.delete_vrrp_port(object_id='foo')
neutron_wrapper.api_client.list_ports.assert_called_with(
name='ASTARA:VRRP:foo'
)
neutron_wrapper.api_client.delete_port.assert_called_with(
'fake_port_id')
@mock.patch('astara.api.neutron.AstaraExtClientWrapper')
def test_delete_vrrp_ports_not_found(self, client_wrapper):
conf = mock.Mock()
neutron_wrapper = neutron.Neutron(conf)
neutron_wrapper.api_client.list_ports = mock.Mock(
return_value={'ports': []}
)
neutron_wrapper.api_client.delete_port = mock.Mock()
neutron_wrapper.delete_vrrp_port(object_id='foo')
neutron_wrapper.api_client.list_ports.assert_has_calls(
[
mock.call(name='ASTARA:VRRP:foo'),
mock.call(name='AKANDA:VRRP:foo'),
]
)
self.assertFalse(neutron_wrapper.api_client.delete_port.called)
class TestLocalServicePorts(base.RugTestBase):
def setUp(self):
super(TestLocalServicePorts, self).setUp()
self.config(management_network_id='fake_mgtnet_network_id')
self.config(management_subnet_id='fake_mgtnet_subnet_id')
self.config(management_prefix='172.16.77.0/24')
self.config(management_prefix='fdca:3ba5:a17a:acda::/64')
self.neutron_wrapper = neutron.Neutron(cfg.CONF)
self.fake_interface_driver = mock.Mock(
plug=mock.Mock(),
init_l3=mock.Mock(),
get_device_name=mock.Mock())
def test_ensure_local_service_port(self):
with mock.patch.object(self.neutron_wrapper,
'_ensure_local_port') as ep:
self.neutron_wrapper.ensure_local_service_port()
ep.assert_called_with(
'fake_mgtnet_network_id',
'fake_mgtnet_subnet_id',
'fdca:3ba5:a17a:acda::/64',
'service',
)
@mock.patch('astara.api.neutron.ip_lib')
@mock.patch('astara.api.neutron.uuid')
@mock.patch('astara.api.neutron.importutils')
def test__ensure_local_port_neutron_port_exists(self, fake_import,
fake_uuid, fake_ip_lib):
fake_ip_lib.device_exists.return_value = True
fake_uuid.uuid5.return_value = 'fake_host_id'
fake_import.import_object.return_value = self.fake_interface_driver
fake_port = fakes.fake_port()
fake_port_dict = {
'ports': [fake_port._neutron_port_dict],
}
fake_client = mock.Mock(
list_ports=mock.Mock(return_value=fake_port_dict)
)
self.neutron_wrapper.api_client = fake_client
self.fake_interface_driver.get_device_name.return_value = 'fake_dev'
self.neutron_wrapper._ensure_local_port(
'fake_network_id',
'fake_subnet_id',
'fdca:3ba5:a17a:acda:f816:3eff:fe2b::1/64',
'service')
exp_query = {
'network_id': 'fake_network_id',
'device_owner': 'network:astara',
'name': 'ASTARA:RUG:SERVICE',
'device_id': 'fake_host_id'
}
fake_client.list_ports.assert_called_with(**exp_query)
self.fake_interface_driver.init_l3.assert_called_with(
'fake_dev', ['fdca:3ba5:a17a:acda:f816:3eff:fe2b:ced0/64']
)
@mock.patch('astara.api.neutron.socket')
@mock.patch('astara.api.neutron.ip_lib')
@mock.patch('astara.api.neutron.uuid')
@mock.patch('astara.api.neutron.importutils')
def test__ensure_local_port_no_neutron_port(self, fake_import, fake_uuid,
fake_ip_lib, fake_socket):
fake_socket.gethostname.return_value = 'foo_hostname'
fake_ip_lib.device_exists.return_value = True
fake_uuid.uuid5.return_value = 'fake_host_id'
fake_import.import_object.return_value = self.fake_interface_driver
fake_created_port = {'port': fakes.fake_port().to_dict()}
fake_client = mock.Mock(
list_ports=mock.Mock(return_value={'ports': []}),
create_port=mock.Mock(return_value=fake_created_port))
self.neutron_wrapper.api_client = fake_client
self.fake_interface_driver.get_device_name.return_value = 'fake_dev'
self.neutron_wrapper._ensure_local_port(
'fake_network_id',
'fake_subnet_id',
'fdca:3ba5:a17a:acda:f816:3eff:fe2b::1/64',
'service')
exp_port_create_dict = {'port': {
'admin_state_up': True,
'binding:host_id': 'foo_hostname',
'device_id': 'fake_host_id',
'device_owner': 'network:router_interface',
'fixed_ips': [{'subnet_id': 'fake_subnet_id'}],
'name': 'ASTARA:RUG:SERVICE',
'network_id': 'fake_network_id'
}}
fake_client.create_port.assert_called_with(exp_port_create_dict)
self.fake_interface_driver.init_l3.assert_called_with(
'fake_dev', ['fdca:3ba5:a17a:acda:f816:3eff:fe2b:ced0/64']
)
@mock.patch('time.sleep')
@mock.patch('astara.api.neutron.ip_lib')
@mock.patch('astara.api.neutron.uuid')
@mock.patch('astara.api.neutron.importutils')
def test__ensure_local_port_plug(self, fake_import,
fake_uuid, fake_ip_lib, fake_sleep):
fake_ip_lib.device_exists.return_value = False
fake_uuid.uuid5.return_value = 'fake_host_id'
fake_import.import_object.return_value = self.fake_interface_driver
fake_port = fakes.fake_port()
fake_port_dict = {
'ports': [fake_port._neutron_port_dict],
}
fake_client = mock.Mock(
list_ports=mock.Mock(return_value=fake_port_dict)
)
self.neutron_wrapper.api_client = fake_client
self.fake_interface_driver.get_device_name.return_value = 'fake_dev'
self.neutron_wrapper._ensure_local_port(
'fake_network_id',
'fake_subnet_id',
'fdca:3ba5:a17a:acda:f816:3eff:fe2b::1/64',
'service')
self.fake_interface_driver.plug.assert_called_with(
'fake_network_id',
fake_port.id,
'fake_dev',
fake_port.mac_address)

View File

@ -1,481 +0,0 @@
# 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.
from datetime import datetime, timedelta
import mock
import copy
from novaclient import exceptions as novaclient_exceptions
from six.moves import builtins as __builtins__
from astara.api import nova
from astara.test.unit import base
class FakeNovaServer(object):
id = '6f05906e-4538-11e5-bb22-5254003ff1ae'
name = 'ak-796aafbc-4538-11e5-88e0-5254003ff1ae'
image = {'id': '83031410-4538-11e5-abd2-5254003ff1ae'}
status = 'ACTIVE'
created = '2012-08-20T21:11:09Z'
class FakeModel(object):
def __init__(self, id_, **kwargs):
self.id = id_
self.__dict__.update(kwargs)
fake_ext_port = FakeModel(
'1',
mac_address='aa:bb:cc:dd:ee:ff',
network_id='ext-net',
fixed_ips=[FakeModel('', ip_address='9.9.9.9', subnet_id='s2')])
fake_mgt_port = FakeModel(
'2',
mac_address='aa:bb:cc:cc:bb:aa',
network_id='mgt-net')
fake_int_port = FakeModel(
'3',
mac_address='aa:aa:aa:aa:aa:aa',
network_id='int-net',
fixed_ips=[FakeModel('', ip_address='192.168.1.1', subnet_id='s1')])
fake_router = FakeModel(
'router_id',
tenant_id='tenant_id',
external_port=fake_ext_port,
management_port=fake_mgt_port,
internal_ports=[fake_int_port],
ports=[fake_mgt_port, fake_ext_port, fake_int_port])
fake_nova_instance = FakeModel(
'instance_id',
name='ak-appliance',
status=None,
image={'id': 'fake_image_uuid'},
created='2012-08-20T21:11:09Z'
)
class FakeConf:
admin_user = 'admin'
admin_password = 'password'
admin_tenant_name = 'admin'
auth_url = 'http://127.0.0.1/'
auth_strategy = 'keystone'
auth_region = 'RegionOne'
router_image_uuid = 'astara-image'
router_instance_flavor = 1
instance_provider = 'foo'
endpoint_type = 'publicURL'
EXPECTED_USERDATA = """
#cloud-config
cloud_config_modules:
- emit_upstart
- set_hostname
- locale
- set-passwords
- timezone
- disable-ec2-metadata
- runcmd
output: {all: '| tee -a /var/log/cloud-init-output.log'}
debug:
- verbose: true
bootcmd:
- /usr/local/bin/astara-configure-management aa:aa:aa:aa:aa:aa 192.168.1.1/64
users:
- name: astara
gecos: Astara
groups: users
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
lock-passwd: true
ssh-authorized-keys:
- fake_key
final_message: "Astara appliance is running"
"""
def fake_make_ports_callback():
return (fake_mgt_port, [fake_ext_port, fake_int_port])
class TestNovaWrapper(base.RugTestBase):
def setUp(self):
super(TestNovaWrapper, self).setUp()
self.addCleanup(mock.patch.stopall)
patch = mock.patch('novaclient.client.Client')
self.client = mock.Mock()
self.client_cls = patch.start()
self.client_cls.return_value = self.client
self.fake_instance_provider = mock.Mock(create_instance=mock.Mock())
fake_instance_provider_cls = mock.Mock(name='fake_provider_class')
fake_instance_provider_cls.return_value = \
self.fake_instance_provider
get_instance_provider_p = mock.patch.object(
nova, 'get_instance_provider').start()
get_instance_provider_p.return_value = fake_instance_provider_cls
self.nova = nova.Nova(FakeConf)
self.INSTANCE_INFO = nova.InstanceInfo(
instance_id='fake_instance_id',
name='fake_name',
image_uuid='fake_image_id',
status='ACTIVE',
last_boot=(datetime.utcnow() - timedelta(minutes=15)),
ports=[fake_int_port, fake_ext_port, fake_mgt_port],
management_port=fake_mgt_port,
)
def test_get_instance_for_obj(self):
instance = mock.Mock()
self.client.servers.list.return_value = [instance]
expected = [
mock.call.servers.list(search_opts={'name': 'foo_instance_name'})
]
result = self.nova.get_instance_for_obj('foo_instance_name')
self.client.assert_has_calls(expected)
self.assertEqual(instance, result)
def test_get_instance_for_obj_not_found(self):
self.client.servers.list.return_value = []
expected = [
mock.call.servers.list(search_opts={'name': 'foo_instance_name'})
]
result = self.nova.get_instance_for_obj('foo_instance_name')
self.client.assert_has_calls(expected)
self.assertIsNone(result)
def test_get_instance_by_id(self):
self.client.servers.get.return_value = 'fake_instance'
expected = [
mock.call.servers.get('instance_id')
]
result = self.nova.get_instance_by_id('instance_id')
self.client.servers.get.assert_has_calls(expected)
self.assertEqual('fake_instance', result)
def test_get_instance_by_id_not_found(self):
not_found = novaclient_exceptions.NotFound('instance_id')
self.client.servers.get.side_effect = not_found
result = self.nova.get_instance_by_id('instance_id')
self.assertIsNone(result)
def test_destroy_instance(self):
self.nova.destroy_instance(self.INSTANCE_INFO)
self.client.servers.delete.assert_called_with(self.INSTANCE_INFO.id_)
@mock.patch.object(nova, '_ssh_key')
def test_format_userdata(self, fake_ssh_key):
fake_ssh_key.return_value = 'fake_key'
result = nova.format_userdata(fake_int_port)
self.assertEqual(EXPECTED_USERDATA.strip(), result.strip())
@mock.patch.object(__builtins__, 'open', autospec=True)
def test_ssh_key(self, fake_open):
mock_key_file = mock.MagicMock(spec=file)
mock_key_file.read.return_value = 'fake-key'
mock_key_file.__enter__.return_value = mock_key_file
fake_open.return_value = mock_key_file
result = nova._ssh_key()
self.assertEqual('fake-key', result)
@mock.patch.object(__builtins__, 'open', autospec=True)
def test_ssh_key_sanitize(self, fake_open):
mock_key_file = mock.MagicMock(spec=file)
mock_key_file.read.return_value = ('''
fake-key with some newlines
''')
mock_key_file.__enter__.return_value = mock_key_file
fake_open.return_value = mock_key_file
result = nova._ssh_key()
self.assertEqual('fake-key with some newlines', result)
@mock.patch.object(nova, 'LOG', autospec=True)
@mock.patch.object(__builtins__, 'open', autospec=True)
def test_ssh_key_not_found(self, fake_open, fake_log):
fake_open.side_effect = IOError
result = nova._ssh_key()
self.assertEqual('', result)
self.assertTrue(fake_log.warning.called)
@mock.patch.object(nova.Nova, 'get_instance_for_obj', return_value=None)
def test_boot_instance(self, fake_get):
self.fake_instance_provider.create_instance.return_value = \
'fake_new_instance_info'
res = self.nova.boot_instance(
resource_type='router',
prev_instance_info=None,
name='foo_instance_name',
image_uuid='foo_image',
flavor='foo_flavor',
make_ports_callback='foo_callback',
)
self.fake_instance_provider.create_instance.assert_called_with(
resource_type='router',
name='foo_instance_name',
image_uuid='foo_image',
flavor='foo_flavor',
make_ports_callback='foo_callback',
)
fake_get.assert_called_with('foo_instance_name')
self.assertEqual('fake_new_instance_info', res)
@mock.patch.object(nova.Nova, 'get_instance_for_obj')
def test_boot_instance_exists(self, fake_get):
fake_instance = fake_nova_instance
fake_instance.id = 'existing_instance_id'
fake_instance.status = 'SHUTOFF'
fake_get.return_value = fake_instance
self.fake_instance_provider.create_instance.return_value = \
'fake_new_instance_info'
res = self.nova.boot_instance(
resource_type='router',
prev_instance_info=None,
name='foo_instance_name',
image_uuid='foo_image',
flavor='foo_flavor',
make_ports_callback='foo_callback',
)
fake_get.assert_called_with('foo_instance_name')
self.client.servers.delete.assert_called_with('existing_instance_id')
self.assertIsNone(res)
@mock.patch.object(nova.Nova, 'get_instance_for_obj')
def test_boot_instance_exists_build(self, fake_get):
fake_instance = fake_nova_instance
fake_instance.id = 'existing_instance_id'
fake_instance.status = 'BUILD'
fake_get.return_value = fake_instance
self.fake_instance_provider.create_instance.return_value = \
'fake_new_instance_info'
res = self.nova.boot_instance(
resource_type='router',
prev_instance_info=None,
name='foo_instance_name',
image_uuid='foo_image',
flavor='foo_flavor',
make_ports_callback='foo_callback',
)
fake_get.assert_called_with('foo_instance_name')
self.assertIsInstance(res, nova.InstanceInfo)
self.assertEqual('existing_instance_id', res.id_)
self.assertEqual('ak-appliance', res.name)
self.assertEqual('fake_image_uuid', res.image_uuid)
@mock.patch.object(nova.Nova, 'get_instance_by_id', return_value=None)
def test_boot_instance_prev_inst(self, fake_get):
self.fake_instance_provider.create_instance.return_value = \
'fake_new_instance_info'
res = self.nova.boot_instance(
resource_type='router',
prev_instance_info=self.INSTANCE_INFO,
name='foo_instance_name',
image_uuid='foo_image',
flavor='foo_flavor',
make_ports_callback='foo_callback',
)
fake_get.assert_called_with(self.INSTANCE_INFO.id_)
self.fake_instance_provider.create_instance.assert_called_with(
resource_type='router',
name='foo_instance_name',
image_uuid='foo_image',
flavor='foo_flavor',
make_ports_callback='foo_callback',
)
self.assertEqual('fake_new_instance_info', res)
@mock.patch.object(nova.Nova, 'get_instance_by_id')
def test_boot_instance_exists_prev_inst(self, fake_get):
fake_instance = fake_nova_instance
fake_instance.id = 'existing_instance_id'
fake_instance.status = 'SHUTOFF'
fake_get.return_value = fake_instance
self.fake_instance_provider.create_instance.return_value = \
'fake_new_instance_info'
res = self.nova.boot_instance(
resource_type='router',
prev_instance_info=self.INSTANCE_INFO,
name='foo_instance_name',
image_uuid='foo_image',
flavor='foo_flavor',
make_ports_callback='foo_callback',
)
fake_get.assert_called_with(self.INSTANCE_INFO.id_)
self.client.servers.delete.assert_called_with('existing_instance_id')
self.assertIsNone(res)
@mock.patch.object(nova.Nova, 'get_instance_for_obj')
def test_boot_instance_exists_build_prev_inst(self, fake_get):
fake_instance = fake_nova_instance
fake_instance.id = 'existing_instance_id'
fake_instance.status = 'BUILD'
fake_get.return_value = fake_instance
self.fake_instance_provider.create_instance.return_value = \
'fake_new_instance_info'
res = self.nova.boot_instance(
resource_type='router',
prev_instance_info=None,
name='foo_instance_name',
image_uuid='foo_image',
flavor='foo_flavor',
make_ports_callback='foo_callback',
)
# assert we get back the same instance_info but with updated status
self.assertEqual('BUILD', res.nova_status)
self.assertEqual(fake_instance.id, res.id_)
self.assertIsInstance(res, nova.InstanceInfo)
def test_from_nova(self):
fake_server = FakeNovaServer()
last_boot = datetime.strptime(
fake_server.created, "%Y-%m-%dT%H:%M:%SZ")
instance_info = nova.InstanceInfo.from_nova(fake_server)
self.assertEqual(fake_server.id, instance_info.id_)
self.assertEqual(fake_server.name, instance_info.name)
self.assertEqual(fake_server.image['id'], instance_info.image_uuid)
self.assertEqual(last_boot, instance_info.last_boot)
def test_booting_false(self):
self.INSTANCE_INFO.nova_status = 'ACTIVE'
self.assertFalse(self.INSTANCE_INFO.booting)
def test_booting_true(self):
self.INSTANCE_INFO.nova_status = 'BUILDING'
self.assertTrue(self.INSTANCE_INFO.booting)
def test_no_provider_not_none(self):
NoProviderConf = copy.deepcopy(FakeConf)
del NoProviderConf.instance_provider
self.nova = nova.Nova(NoProviderConf)
self.assertIsNotNone(self.nova.instance_provider.create_instance)
class TestOnDemandInstanceProvider(base.RugTestBase):
def setUp(self):
super(TestOnDemandInstanceProvider, self).setUp()
self.addCleanup(mock.patch.stopall)
patch = mock.patch('novaclient.client.Client')
self.client = mock.Mock()
self.client_cls = patch.start()
self.client_cls.return_value = self.client
@mock.patch.object(nova, 'format_userdata')
def test_create_instance(self, mock_userdata):
provider = nova.OnDemandInstanceProvider(self.client)
self.client.servers.create.return_value = fake_nova_instance
mock_userdata.return_value = 'fake_userdata'
expected = [
mock.call.servers.create(
'ak-instance-name',
nics=[{'port-id': '2',
'net-id': 'mgt-net',
'v4-fixed-ip': ''},
{'port-id': '1',
'net-id': 'ext-net',
'v4-fixed-ip': ''},
{'port-id': '3',
'net-id': 'int-net',
'v4-fixed-ip': ''}],
flavor=1,
image='GLANCE-IMAGE-123',
config_drive=True,
userdata='fake_userdata',
)
]
provider.create_instance(
'router', 'ak-instance-name', 'GLANCE-IMAGE-123',
1, fake_make_ports_callback)
self.client.assert_has_calls(expected)
class TestPezInstanceProvider(base.RugTestBase):
def setUp(self):
super(TestPezInstanceProvider, self).setUp()
self.addCleanup(mock.patch.stopall)
patch = mock.patch('novaclient.client.Client')
self.nova_client = mock.Mock()
self.nova_client_cls = patch.start()
self.nova_client_cls.return_value = self.nova_client
patch = mock.patch('astara.pez.rpcapi.AstaraPezAPI')
self.rpc_client = mock.Mock()
self.rpc_client_cls = patch.start()
self.rpc_client_cls.return_value = self.rpc_client
@mock.patch.object(nova, 'format_userdata')
def test_create_instance(self, mock_userdata):
provider = nova.PezInstanceProvider(self.nova_client)
inst_port = {
'id': '1',
'name': 'name',
'device_id': 'device_id',
'fixed_ips': [{'ip_address': '192.168.1.1', 'subnet_id': 'sub1'}],
'mac_address': 'aa:bb:cc:dd:ee:ff',
'network_id': 'net_id',
'device_owner': 'test'
}
mgt_port = {
'id': '2',
'name': 'name',
'device_id': 'device_id',
'fixed_ips': [{'ip_address': '192.168.1.10', 'subnet_id': 'sub1'}],
'mac_address': 'aa:bb:cc:dd:ee:fa',
'network_id': 'net_id2',
'device_owner': 'test'
}
fake_server = FakeNovaServer()
self.nova_client.servers.get.return_value = fake_server
fake_pez_instance = {
'id': fake_server.id,
'management_port': mgt_port,
'instance_ports': [inst_port],
}
self.rpc_client.get_instance.return_value = fake_pez_instance
res = provider.create_instance(
'router', 'ak-instance-name', 'GLANCE-IMAGE-123',
1, fake_make_ports_callback)
self.rpc_client.get_instance.assert_called_with(
'router', 'ak-instance-name',
{'network_id': 'mgt-net', 'id': '2'},
[{'network_id': 'ext-net', 'id': '1'},
{'network_id': 'int-net', 'id': '3'}])
self.nova_client.servers.get.assert_called_with(fake_server.id)
exp_instance_info = nova.InstanceInfo.from_nova(fake_server)
self.assertEqual(exp_instance_info.id_, res.id_)

View File

@ -1,242 +0,0 @@
import unittest
import socket
from six.moves import range
from cliff import commandmanager
import mock
from oslo_log import log
import webob
from astara.api import rug
try:
import blessed # noqa
HAS_BLESSED = True
except ImportError:
HAS_BLESSED = False
class TestRugAPI(unittest.TestCase):
def setUp(self):
ctl = mock.Mock()
ctl.return_value.command_manager = commandmanager.CommandManager(
'astara.cli'
)
self.api = rug.RugAPI(ctl)
self.ctl = ctl.return_value
@unittest.skipUnless(HAS_BLESSED, "blessed not available")
def test_browse(self):
resp = self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/browse/'
}))
assert isinstance(resp, webob.exc.HTTPNotImplemented)
assert not self.ctl.run.called
def test_ssh(self):
resp = self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/ssh/ROUTER123/'
}))
assert isinstance(resp, webob.exc.HTTPNotImplemented)
assert not self.ctl.run.called
def test_poll(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/poll/'
}))
self.ctl.run.assert_called_with(
['--debug', 'poll']
)
def test_missing_argument(self):
# argparse failures (e.g., a missing router ID) raise a SystemExit
# because cliff's behavior is to print a help message and sys.exit()
self.ctl.run.side_effect = SystemExit
resp = self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/debug/'
}))
assert isinstance(resp, webob.exc.HTTPBadRequest)
self.ctl.run.assert_called_with(
['--debug', 'router', 'debug']
)
def test_router_debug(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/debug/ROUTER123'
}))
self.ctl.run.assert_called_with(
['--debug', 'router', 'debug', 'ROUTER123']
)
def test_router_manage(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/manage/ROUTER123'
}))
self.ctl.run.assert_called_with(
['--debug', 'router', 'manage', 'ROUTER123']
)
def test_router_update(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/update/ROUTER123'
}))
self.ctl.run.assert_called_with(
['--debug', 'router', 'update', 'ROUTER123']
)
def test_router_rebuild(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/rebuild/ROUTER123'
}))
self.ctl.run.assert_called_with(
['--debug', 'router', 'rebuild', 'ROUTER123']
)
def test_tenant_debug(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/tenant/debug/TENANT123'
}))
self.ctl.run.assert_called_with(
['--debug', 'tenant', 'debug', 'TENANT123']
)
def test_tenant_manage(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/tenant/manage/TENANT123'
}))
self.ctl.run.assert_called_with(
['--debug', 'tenant', 'manage', 'TENANT123']
)
def test_workers_debug(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/workers/debug/'
}))
self.ctl.run.assert_called_with(
['--debug', 'workers', 'debug']
)
def test_invalid_router_action(self):
resp = self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/breakdance/ROUTER123'
}))
assert isinstance(resp, webob.exc.HTTPNotFound)
assert not self.ctl.run.called
def test_multiple_calls(self):
for i in range(10):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/poll/'
}))
assert self.ctl.run.call_args_list == [
mock.call(['--debug', 'poll'])
for _ in range(10)
]
def test_invalid_request_method(self):
resp = self.api(webob.Request({
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/poll/'
}))
assert isinstance(resp, webob.exc.HTTPMethodNotAllowed)
assert not self.ctl.run.called
class TestRugAPIServer(unittest.TestCase):
@mock.patch('eventlet.listen')
@mock.patch('eventlet.wsgi')
def test_bind_and_serve_ipv4(self, wsgi, listen):
sock = listen.return_value
server = rug.RugAPIServer()
server.run('10.0.0.250', 44250)
listen.assert_called_with(
('10.0.0.250', 44250),
family=socket.AF_INET,
backlog=128
)
args, kwargs = wsgi.server.call_args
assert all([
args[0] == sock,
isinstance(args[1], rug.RugAPI),
kwargs['custom_pool'] == server.pool,
isinstance(kwargs['log'], log.KeywordArgumentAdapter)
])
@mock.patch('eventlet.listen')
@mock.patch('eventlet.wsgi')
def test_bind_and_serve_ipv6(self, wsgi, listen):
sock = listen.return_value
server = rug.RugAPIServer()
server.run('fdca:3ba5:a17a:acda::1', 44250)
listen.assert_called_with(
('fdca:3ba5:a17a:acda::1', 44250),
family=socket.AF_INET6,
backlog=128
)
args, kwargs = wsgi.server.call_args
assert all([
args[0] == sock,
isinstance(args[1], rug.RugAPI),
kwargs['custom_pool'] == server.pool,
isinstance(kwargs['log'], log.KeywordArgumentAdapter)
])
@mock.patch('eventlet.listen')
@mock.patch('eventlet.sleep', lambda x: None)
def test_fail_to_bind(self, listen):
listen.side_effect = socket.error(
99, "Can't assign requested address"
)
server = rug.RugAPIServer()
self.assertRaises(
RuntimeError,
server.run,
'fdca:3ba5:a17a:acda::1',
44250,
)
assert listen.call_args_list == [
mock.call(('fdca:3ba5:a17a:acda::1', 44250),
family=socket.AF_INET6, backlog=128)
for i in range(5)
]
@mock.patch('eventlet.listen')
@mock.patch('eventlet.wsgi')
@mock.patch('eventlet.sleep', lambda x: None)
def test_bind_fails_on_first_attempt(self, wsgi, listen):
sock = mock.Mock()
listen.side_effect = [
socket.error(99, "Can't assign requested address"),
sock
]
server = rug.RugAPIServer()
server.run('fdca:3ba5:a17a:acda::1', 44250)
assert listen.call_args_list == [
mock.call(('fdca:3ba5:a17a:acda::1', 44250),
family=socket.AF_INET6, backlog=128)
for i in range(2) # fails the first time, succeeds the second
]
args, kwargs = wsgi.server.call_args
assert all([
args[0] == sock,
isinstance(args[1], rug.RugAPI),
kwargs['custom_pool'] == server.pool,
isinstance(kwargs['log'], log.KeywordArgumentAdapter)
])

View File

@ -1,40 +0,0 @@
# Copyright 2015 Akanda, Inc.
#
# Author: Akanda, Inc.
#
# 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 mock
import testtools
from oslo_config import cfg
from oslo_config import fixture as config_fixture
class RugTestBase(testtools.TestCase):
def setUp(self):
super(RugTestBase, self).setUp()
self.test_config = self.useFixture(config_fixture.Config(cfg.CONF))
self.argv = []
cfg.CONF.import_opt('host', 'astara.main')
self.time_patch = mock.patch('time.sleep')
self.time_mock = self.time_patch.start()
self.addCleanup(mock.patch.stopall)
def config(self, **kw):
"""Override config options for a test."""
group = kw.pop('group', None)
for k, v in kw.items():
cfg.CONF.set_override(k, v, group)

View File

@ -1,15 +0,0 @@
# 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.

View File

@ -1,109 +0,0 @@
# 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.
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012, Nicira, Inc.
#
# 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.
# @author: Dan Wendlandt, Nicira, Inc.
import fixtures
import mock
import testtools
from astara.common.linux import utils
class AgentUtilsExecuteTest(testtools.TestCase):
def setUp(self):
super(AgentUtilsExecuteTest, self).setUp()
self.root_helper = "echo"
self.test_file = self.useFixture(
fixtures.TempDir()).join("test_execute.tmp")
open(self.test_file, 'w').close()
def test_without_helper(self):
result = utils.execute(["ls", self.test_file])
self.assertEqual("%s\n" % self.test_file, result)
def test_with_helper(self):
result = utils.execute(["ls", self.test_file],
self.root_helper)
self.assertEqual("ls %s\n" % self.test_file, result)
def test_stderr(self):
stdout, stderr = utils.execute(["ls", self.test_file],
return_stderr=True)
self.assertEqual("%s\n" % self.test_file, stdout)
self.assertEqual("", stderr)
def test_check_exit_code(self):
stdout = utils.execute(["ls", self.test_file[:-1]],
check_exit_code=False)
self.assertEqual("", stdout)
self.assertRaises(RuntimeError, utils.execute,
["ls", self.test_file[:-1]])
def test_process_input(self):
result = utils.execute(["cat"], process_input="%s\n" %
self.test_file[:-1])
self.assertEqual("%s\n" % self.test_file[:-1], result)
def test_with_addl_env(self):
result = utils.execute(["ls", self.test_file],
addl_env={'foo': 'bar'})
self.assertEqual("%s\n" % self.test_file, result)
class AgentUtilsGetInterfaceMAC(testtools.TestCase):
def test_get_interface_mac(self):
expect_val = '01:02:03:04:05:06'
with mock.patch('fcntl.ioctl') as ioctl:
ioctl.return_value = ''.join(['\x00' * 18,
'\x01\x02\x03\x04\x05\x06',
'\x00' * 232])
actual_val = utils.get_interface_mac('eth0')
self.assertEqual(expect_val, actual_val)
class AgentUtilsReplaceFile(testtools.TestCase):
def test_replace_file(self):
# make file to replace
with mock.patch('tempfile.NamedTemporaryFile') as ntf:
ntf.return_value.name = '/baz'
with mock.patch('os.chmod') as chmod:
with mock.patch('os.rename') as rename:
utils.replace_file('/foo', 'bar')
expected = [mock.call('w+', dir='/', delete=False),
mock.call().write('bar'),
mock.call().close()]
ntf.assert_has_calls(expected)
chmod.assert_called_once_with('/baz', 0644)
rename.assert_called_once_with('/baz', '/foo')

View File

@ -1,44 +0,0 @@
# Copyright 2015 Akanda, 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.
import mock
from astara.common import config
from astara.test.unit import base
class TestConfig(base.RugTestBase):
def _test_get_best_config_path(self, original, expected, files_exist=()):
def mock_isfile_f(f):
return f in files_exist
with mock.patch('os.path.isfile', side_effect=mock_isfile_f):
self.assertEqual(
expected,
config.get_best_config_path(original)
)
def test_get_best_config_path_preferred(self):
self._test_get_best_config_path(
config.PREFERRED_CONFIG_FILEPATH,
config.PREFERRED_CONFIG_FILEPATH
)
def test_get_best_config_path_legacy(self):
self._test_get_best_config_path(
config.PREFERRED_CONFIG_FILEPATH,
'/etc/akanda/rug.ini',
('/etc/akanda/rug.ini',)
)

Some files were not shown because too many files have changed in this diff Show More