resource_removed is used in many places to monitor removal of different kinds of OpenStack resource. It is reasonable to expect a flavor to be removed in a few seconds where as an image based instance may take minutes to be removed. With that in mind this change exposes the retry options used by tenacity allowing the caller to set reasonable expectations for the resource removal.
2126 lines
74 KiB
Python
2126 lines
74 KiB
Python
# Copyright 2018 Canonical Ltd.
|
|
#
|
|
# 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.
|
|
|
|
"""Module for interacting with OpenStack.
|
|
|
|
This module contains a number of functions for interacting with Openstack.
|
|
"""
|
|
from .os_versions import (
|
|
OPENSTACK_CODENAMES,
|
|
SWIFT_CODENAMES,
|
|
PACKAGE_CODENAMES,
|
|
OPENSTACK_RELEASES_PAIRS,
|
|
)
|
|
|
|
from openstack import connection
|
|
|
|
from cinderclient import client as cinderclient
|
|
from glanceclient import Client as GlanceClient
|
|
|
|
from keystoneclient.v2_0 import client as keystoneclient_v2
|
|
from keystoneclient.v3 import client as keystoneclient_v3
|
|
from keystoneauth1 import session
|
|
from keystoneauth1.identity import (
|
|
v3,
|
|
v2,
|
|
)
|
|
import zaza.openstack.utilities.cert as cert
|
|
from novaclient import client as novaclient_client
|
|
from neutronclient.v2_0 import client as neutronclient
|
|
from neutronclient.common import exceptions as neutronexceptions
|
|
from octaviaclient.api.v2 import octavia as octaviaclient
|
|
from swiftclient import client as swiftclient
|
|
|
|
import datetime
|
|
import io
|
|
import juju_wait
|
|
import logging
|
|
import os
|
|
import paramiko
|
|
import re
|
|
import six
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import tenacity
|
|
import urllib
|
|
|
|
from zaza import model
|
|
from zaza.openstack.utilities import (
|
|
exceptions,
|
|
generic as generic_utils,
|
|
juju as juju_utils,
|
|
)
|
|
|
|
CIRROS_RELEASE_URL = 'http://download.cirros-cloud.net/version/released'
|
|
CIRROS_IMAGE_URL = 'http://download.cirros-cloud.net'
|
|
UBUNTU_IMAGE_URLS = {
|
|
'bionic': ('http://cloud-images.ubuntu.com/{release}/current/'
|
|
'{release}-server-cloudimg-{arch}.img')
|
|
}
|
|
|
|
CHARM_TYPES = {
|
|
'neutron': {
|
|
'pkg': 'neutron-common',
|
|
'origin_setting': 'openstack-origin'
|
|
},
|
|
'nova': {
|
|
'pkg': 'nova-common',
|
|
'origin_setting': 'openstack-origin'
|
|
},
|
|
'glance': {
|
|
'pkg': 'glance-common',
|
|
'origin_setting': 'openstack-origin'
|
|
},
|
|
'cinder': {
|
|
'pkg': 'cinder-common',
|
|
'origin_setting': 'openstack-origin'
|
|
},
|
|
'keystone': {
|
|
'pkg': 'keystone',
|
|
'origin_setting': 'openstack-origin'
|
|
},
|
|
'openstack-dashboard': {
|
|
'pkg': 'openstack-dashboard',
|
|
'origin_setting': 'openstack-origin'
|
|
},
|
|
'ceilometer': {
|
|
'pkg': 'ceilometer-common',
|
|
'origin_setting': 'openstack-origin'
|
|
},
|
|
}
|
|
UPGRADE_SERVICES = [
|
|
{'name': 'keystone', 'type': CHARM_TYPES['keystone']},
|
|
{'name': 'nova-cloud-controller', 'type': CHARM_TYPES['nova']},
|
|
{'name': 'nova-compute', 'type': CHARM_TYPES['nova']},
|
|
{'name': 'neutron-api', 'type': CHARM_TYPES['neutron']},
|
|
{'name': 'neutron-gateway', 'type': CHARM_TYPES['neutron']},
|
|
{'name': 'glance', 'type': CHARM_TYPES['glance']},
|
|
{'name': 'cinder', 'type': CHARM_TYPES['cinder']},
|
|
{'name': 'openstack-dashboard',
|
|
'type': CHARM_TYPES['openstack-dashboard']},
|
|
{'name': 'ceilometer', 'type': CHARM_TYPES['ceilometer']},
|
|
]
|
|
|
|
|
|
WORKLOAD_STATUS_EXCEPTIONS = {
|
|
'vault': {
|
|
'workload-status': 'blocked',
|
|
'workload-status-message': 'Vault needs to be initialized'},
|
|
'easyrsa': {
|
|
'workload-status-message': 'Certificate Authority connected.'},
|
|
'etcd': {
|
|
'workload-status-message': 'Healthy'},
|
|
'memcached': {
|
|
'workload-status': 'unknown',
|
|
'workload-status-message': ''},
|
|
'mongodb': {
|
|
'workload-status': 'unknown',
|
|
'workload-status-message': ''},
|
|
'postgresql': {
|
|
'workload-status-message': 'Live'}}
|
|
|
|
# For vault TLS certificates
|
|
KEYSTONE_CACERT = "keystone_juju_ca_cert.crt"
|
|
KEYSTONE_REMOTE_CACERT = (
|
|
"/usr/local/share/ca-certificates/{}".format(KEYSTONE_CACERT))
|
|
KEYSTONE_LOCAL_CACERT = ("/tmp/{}".format(KEYSTONE_CACERT))
|
|
|
|
|
|
# Openstack Client helpers
|
|
def get_ks_creds(cloud_creds, scope='PROJECT'):
|
|
"""Return the credentials for authenticating against keystone.
|
|
|
|
:param cloud_creds: Openstack RC environment credentials
|
|
:type cloud_creds: dict
|
|
:param scope: Authentication scope: PROJECT or DOMAIN
|
|
:type scope: string
|
|
:returns: Credentials dictionary
|
|
:rtype: dict
|
|
"""
|
|
if cloud_creds.get('API_VERSION', 2) == 2:
|
|
auth = {
|
|
'username': cloud_creds['OS_USERNAME'],
|
|
'password': cloud_creds['OS_PASSWORD'],
|
|
'auth_url': cloud_creds['OS_AUTH_URL'],
|
|
'tenant_name': (cloud_creds.get('OS_PROJECT_NAME') or
|
|
cloud_creds['OS_TENANT_NAME']),
|
|
}
|
|
else:
|
|
if scope == 'DOMAIN':
|
|
auth = {
|
|
'username': cloud_creds['OS_USERNAME'],
|
|
'password': cloud_creds['OS_PASSWORD'],
|
|
'auth_url': cloud_creds['OS_AUTH_URL'],
|
|
'user_domain_name': cloud_creds['OS_USER_DOMAIN_NAME'],
|
|
'domain_name': cloud_creds['OS_DOMAIN_NAME'],
|
|
}
|
|
else:
|
|
auth = {
|
|
'username': cloud_creds['OS_USERNAME'],
|
|
'password': cloud_creds['OS_PASSWORD'],
|
|
'auth_url': cloud_creds['OS_AUTH_URL'],
|
|
'user_domain_name': cloud_creds['OS_USER_DOMAIN_NAME'],
|
|
'project_domain_name': cloud_creds['OS_PROJECT_DOMAIN_NAME'],
|
|
'project_name': cloud_creds['OS_PROJECT_NAME'],
|
|
}
|
|
return auth
|
|
|
|
|
|
def get_glance_session_client(session):
|
|
"""Return glanceclient authenticated by keystone session.
|
|
|
|
:param session: Keystone session object
|
|
:type session: keystoneauth1.session.Session object
|
|
:returns: Authenticated glanceclient
|
|
:rtype: glanceclient.Client
|
|
"""
|
|
return GlanceClient('2', session=session)
|
|
|
|
|
|
def get_nova_session_client(session):
|
|
"""Return novaclient authenticated by keystone session.
|
|
|
|
:param session: Keystone session object
|
|
:type session: keystoneauth1.session.Session object
|
|
:returns: Authenticated novaclient
|
|
:rtype: novaclient.Client object
|
|
"""
|
|
return novaclient_client.Client(2, session=session)
|
|
|
|
|
|
def get_neutron_session_client(session):
|
|
"""Return neutronclient authenticated by keystone session.
|
|
|
|
:param session: Keystone session object
|
|
:type session: keystoneauth1.session.Session object
|
|
:returns: Authenticated neutronclient
|
|
:rtype: neutronclient.Client object
|
|
"""
|
|
return neutronclient.Client(session=session)
|
|
|
|
|
|
def get_swift_session_client(session,
|
|
region_name='RegionOne'):
|
|
"""Return swiftclient authenticated by keystone session.
|
|
|
|
:param session: Keystone session object
|
|
:type session: keystoneauth1.session.Session object
|
|
:param region_name: Optional region name to use
|
|
:type region_name: str
|
|
:returns: Authenticated swiftclient
|
|
:rtype: swiftclient.Client object
|
|
"""
|
|
return swiftclient.Connection(session=session,
|
|
os_options={'region_name': region_name})
|
|
|
|
|
|
def get_octavia_session_client(session, service_type='load-balancer',
|
|
interface='internal'):
|
|
"""Return octavia client authenticated by keystone session.
|
|
|
|
:param session: Keystone session object
|
|
:type session: keystoneauth1.session.Session object
|
|
:param service_type: Service type to look for in catalog
|
|
:type service_type: str
|
|
:param interface: Interface to look for in catalog
|
|
:type interface: str
|
|
:returns: Authenticated octaviaclient
|
|
:rtype: octaviaclient.OctaviaAPI object
|
|
"""
|
|
keystone_client = get_keystone_session_client(session)
|
|
lbaas_service = keystone_client.services.list(type=service_type)
|
|
for service in lbaas_service:
|
|
lbaas_endpoint = keystone_client.endpoints.list(service=service,
|
|
interface='internal')
|
|
for endpoint in lbaas_endpoint:
|
|
break
|
|
return octaviaclient.OctaviaAPI(session=session,
|
|
service_type=service_type,
|
|
endpoint=endpoint.url)
|
|
|
|
|
|
def get_cinder_session_client(session, version=2):
|
|
"""Return cinderclient authenticated by keystone session.
|
|
|
|
:param session: Keystone session object
|
|
:type session: keystoneauth1.session.Session object
|
|
:param version: Cinder API version
|
|
:type version: int
|
|
:returns: Authenticated cinderclient
|
|
:rtype: cinderclient.Client object
|
|
"""
|
|
return cinderclient.Client(session=session, version=version)
|
|
|
|
|
|
def get_masakari_session_client(session, interface='internal',
|
|
region_name='RegionOne'):
|
|
"""Return masakari client authenticated by keystone session.
|
|
|
|
:param session: Keystone session object
|
|
:type session: keystoneauth1.session.Session object
|
|
:param interface: Interface to look for in catalog
|
|
:type interface: str
|
|
:param region_name: Region name to use in catalogue lookup
|
|
:type region_name: str
|
|
:returns: Authenticated masakari client
|
|
:rtype: openstack.instance_ha.v1._proxy.Proxy
|
|
"""
|
|
conn = connection.Connection(session=session,
|
|
interface=interface,
|
|
region_name=region_name)
|
|
return conn.instance_ha
|
|
|
|
|
|
def get_keystone_scope():
|
|
"""Return Keystone scope based on OpenStack release of the overcloud.
|
|
|
|
:returns: String keystone scope
|
|
:rtype: string
|
|
"""
|
|
os_version = get_current_os_versions("keystone")["keystone"]
|
|
# Keystone policy.json shipped the charm with liberty requires a domain
|
|
# scoped token. Bug #1649106
|
|
if os_version == "liberty":
|
|
scope = "DOMAIN"
|
|
else:
|
|
scope = "PROJECT"
|
|
return scope
|
|
|
|
|
|
def get_keystone_session(openrc_creds, scope='PROJECT', verify=None):
|
|
"""Return keystone session.
|
|
|
|
:param openrc_creds: Openstack RC credentials
|
|
:type openrc_creds: dict
|
|
:param verify: Control TLS certificate verification behaviour
|
|
:type verify: any (True - use system certs,
|
|
False - do not verify,
|
|
None - defer to requests library to find certs,
|
|
str - path to a CA cert bundle)
|
|
:param scope: Authentication scope: PROJECT or DOMAIN
|
|
:type scope: string
|
|
:returns: Keystone session object
|
|
:rtype: keystoneauth1.session.Session object
|
|
"""
|
|
keystone_creds = get_ks_creds(openrc_creds, scope=scope)
|
|
if not verify and openrc_creds.get('OS_CACERT'):
|
|
verify = openrc_creds['OS_CACERT']
|
|
if openrc_creds.get('API_VERSION', 2) == 2:
|
|
auth = v2.Password(**keystone_creds)
|
|
else:
|
|
auth = v3.Password(**keystone_creds)
|
|
return session.Session(auth=auth, verify=verify)
|
|
|
|
|
|
def get_overcloud_keystone_session(verify=None):
|
|
"""Return Over cloud keystone session.
|
|
|
|
:param verify: Control TLS certificate verification behaviour
|
|
:type verify: any
|
|
:returns keystone_session: keystoneauth1.session.Session object
|
|
:rtype: keystoneauth1.session.Session
|
|
"""
|
|
return get_keystone_session(get_overcloud_auth(),
|
|
scope=get_keystone_scope(),
|
|
verify=verify)
|
|
|
|
|
|
def get_undercloud_keystone_session(verify=None):
|
|
"""Return Under cloud keystone session.
|
|
|
|
:param verify: Control TLS certificate verification behaviour
|
|
:type verify: any
|
|
:returns keystone_session: keystoneauth1.session.Session object
|
|
:rtype: keystoneauth1.session.Session
|
|
"""
|
|
return get_keystone_session(get_undercloud_auth(),
|
|
verify=verify)
|
|
|
|
|
|
def get_keystone_session_client(session, client_api_version=3):
|
|
"""Return keystoneclient authenticated by keystone session.
|
|
|
|
:param session: Keystone session object
|
|
:type session: keystoneauth1.session.Session object
|
|
:param client_api_version: Whether you want a v2 or v3 Keystone Client
|
|
:type client_api_version: int
|
|
:returns: Authenticated keystoneclient
|
|
:rtype: keystoneclient.v3.Client object
|
|
"""
|
|
if client_api_version == 2:
|
|
return keystoneclient_v2.Client(session=session)
|
|
else:
|
|
return keystoneclient_v3.Client(session=session)
|
|
|
|
|
|
def get_keystone_client(openrc_creds, verify=None):
|
|
"""Return authenticated keystoneclient and set auth_ref for service_catalog.
|
|
|
|
:param openrc_creds: Openstack RC credentials
|
|
:type openrc_creds: dict
|
|
:param verify: Control TLS certificate verification behaviour
|
|
:type verify: any
|
|
:returns: Authenticated keystoneclient
|
|
:rtype: keystoneclient.v3.Client object
|
|
"""
|
|
session = get_keystone_session(openrc_creds, verify=verify)
|
|
client = get_keystone_session_client(session)
|
|
keystone_creds = get_ks_creds(openrc_creds)
|
|
if openrc_creds.get('API_VERSION', 2) == 2:
|
|
auth = v2.Password(**keystone_creds)
|
|
else:
|
|
auth = v3.Password(**keystone_creds)
|
|
# This populates the client.service_catalog
|
|
client.auth_ref = auth.get_access(session)
|
|
return client
|
|
|
|
|
|
def get_project_id(ks_client, project_name, api_version=2, domain_name=None):
|
|
"""Return project ID.
|
|
|
|
:param ks_client: Authenticated keystoneclient
|
|
:type ks_client: keystoneclient.v3.Client object
|
|
:param project_name: Name of the project
|
|
:type project_name: string
|
|
:param api_version: API version number
|
|
:type api_version: int
|
|
:param domain_name: Name of the domain
|
|
:type domain_name: string or None
|
|
:returns: Project ID
|
|
:rtype: string or None
|
|
"""
|
|
domain_id = None
|
|
if domain_name:
|
|
domain_id = ks_client.domains.list(name=domain_name)[0].id
|
|
all_projects = ks_client.projects.list(domain=domain_id)
|
|
for p in all_projects:
|
|
if p._info['name'] == project_name:
|
|
return p._info['id']
|
|
return None
|
|
|
|
|
|
# Neutron Helpers
|
|
def get_gateway_uuids():
|
|
"""Return machine uuids for neutron-gateway(s).
|
|
|
|
:returns: List of uuids
|
|
:rtype: list
|
|
"""
|
|
return juju_utils.get_machine_uuids_for_application('neutron-gateway')
|
|
|
|
|
|
def get_ovs_uuids():
|
|
"""Return machine uuids for neutron-openvswitch(s).
|
|
|
|
:returns: List of uuids
|
|
:rtype: list
|
|
"""
|
|
return (juju_utils
|
|
.get_machine_uuids_for_application('neutron-openvswitch'))
|
|
|
|
|
|
BRIDGE_MAPPINGS = 'bridge-mappings'
|
|
NEW_STYLE_NETWORKING = 'physnet1:br-ex'
|
|
|
|
|
|
def deprecated_external_networking(dvr_mode=False):
|
|
"""Determine whether deprecated external network mode is in use.
|
|
|
|
:param dvr_mode: Using DVR mode or not
|
|
:type dvr_mode: boolean
|
|
:returns: True or False
|
|
:rtype: boolean
|
|
"""
|
|
bridge_mappings = None
|
|
if dvr_mode:
|
|
bridge_mappings = get_application_config_option('neutron-openvswitch',
|
|
BRIDGE_MAPPINGS)
|
|
else:
|
|
bridge_mappings = get_application_config_option('neutron-gateway',
|
|
BRIDGE_MAPPINGS)
|
|
|
|
if bridge_mappings == NEW_STYLE_NETWORKING:
|
|
return False
|
|
return True
|
|
|
|
|
|
def get_net_uuid(neutron_client, net_name):
|
|
"""Determine whether deprecated external network mode is in use.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param net_name: Network name
|
|
:type net_name: string
|
|
:returns: Network ID
|
|
:rtype: string
|
|
"""
|
|
network = neutron_client.list_networks(name=net_name)['networks'][0]
|
|
return network['id']
|
|
|
|
|
|
def get_admin_net(neutron_client):
|
|
"""Return admin netowrk.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:returns: Admin network object
|
|
:rtype: dict
|
|
"""
|
|
for net in neutron_client.list_networks()['networks']:
|
|
if net['name'].endswith('_admin_net'):
|
|
return net
|
|
|
|
|
|
def configure_gateway_ext_port(novaclient, neutronclient,
|
|
dvr_mode=None, net_id=None):
|
|
"""Configure the neturong-gateway external port.
|
|
|
|
:param novaclient: Authenticated novaclient
|
|
:type novaclient: novaclient.Client object
|
|
:param neutronclient: Authenticated neutronclient
|
|
:type neutronclient: neutronclient.Client object
|
|
:param dvr_mode: Using DVR mode or not
|
|
:type dvr_mode: boolean
|
|
:param net_id: Network ID
|
|
:type net_id: string
|
|
"""
|
|
if dvr_mode:
|
|
uuids = get_ovs_uuids()
|
|
else:
|
|
uuids = get_gateway_uuids()
|
|
|
|
deprecated_extnet_mode = deprecated_external_networking(dvr_mode)
|
|
|
|
config_key = 'data-port'
|
|
if deprecated_extnet_mode:
|
|
config_key = 'ext-port'
|
|
|
|
if not net_id:
|
|
net_id = get_admin_net(neutronclient)['id']
|
|
|
|
for uuid in uuids:
|
|
server = novaclient.servers.get(uuid)
|
|
ext_port_name = "{}_ext-port".format(server.name)
|
|
for port in neutronclient.list_ports(device_id=server.id)['ports']:
|
|
if port['name'] == ext_port_name:
|
|
logging.warning('Neutron Gateway already has additional port')
|
|
break
|
|
else:
|
|
logging.info('Attaching additional port to instance, '
|
|
'connected to net id: {}'.format(net_id))
|
|
body_value = {
|
|
"port": {
|
|
"admin_state_up": True,
|
|
"name": ext_port_name,
|
|
"network_id": net_id,
|
|
"port_security_enabled": False,
|
|
}
|
|
}
|
|
port = neutronclient.create_port(body=body_value)
|
|
server.interface_attach(port_id=port['port']['id'],
|
|
net_id=None, fixed_ip=None)
|
|
ext_br_macs = []
|
|
for port in neutronclient.list_ports(network_id=net_id)['ports']:
|
|
if 'ext-port' in port['name']:
|
|
if deprecated_extnet_mode:
|
|
ext_br_macs.append(port['mac_address'])
|
|
else:
|
|
ext_br_macs.append('br-ex:{}'.format(port['mac_address']))
|
|
ext_br_macs.sort()
|
|
ext_br_macs_str = ' '.join(ext_br_macs)
|
|
if dvr_mode:
|
|
application_name = 'neutron-openvswitch'
|
|
else:
|
|
application_name = 'neutron-gateway'
|
|
|
|
if ext_br_macs:
|
|
logging.info('Setting {} on {} external port to {}'.format(
|
|
config_key, application_name, ext_br_macs_str))
|
|
current_data_port = get_application_config_option(application_name,
|
|
config_key)
|
|
if current_data_port == ext_br_macs_str:
|
|
logging.info('Config already set to value')
|
|
return
|
|
model.set_application_config(
|
|
application_name,
|
|
configuration={config_key: ext_br_macs_str})
|
|
juju_wait.wait(wait_for_workload=True)
|
|
|
|
|
|
def create_project_network(neutron_client, project_id, net_name='private',
|
|
shared=False, network_type='gre', domain=None):
|
|
"""Create the project network.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param project_id: Project ID
|
|
:type project_id: string
|
|
:param net_name: Network name
|
|
:type net_name: string
|
|
:param shared: The network should be shared between projects
|
|
:type shared: boolean
|
|
:param net_type: Network type: GRE, VXLAN, local, VLAN
|
|
:type net_type: string
|
|
:param domain_name: Name of the domain
|
|
:type domain_name: string or None
|
|
:returns: Network object
|
|
:rtype: dict
|
|
"""
|
|
networks = neutron_client.list_networks(name=net_name)
|
|
if len(networks['networks']) == 0:
|
|
logging.info('Creating network: %s',
|
|
net_name)
|
|
network_msg = {
|
|
'network': {
|
|
'name': net_name,
|
|
'shared': shared,
|
|
'tenant_id': project_id,
|
|
}
|
|
}
|
|
if network_type == 'vxlan':
|
|
network_msg['network']['provider:segmentation_id'] = 1233
|
|
network_msg['network']['provider:network_type'] = network_type
|
|
network = neutron_client.create_network(network_msg)['network']
|
|
else:
|
|
logging.warning('Network %s already exists.', net_name)
|
|
network = networks['networks'][0]
|
|
return network
|
|
|
|
|
|
def create_external_network(neutron_client, project_id, dvr_mode,
|
|
net_name='ext_net'):
|
|
"""Create the external network.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param project_id: Project ID
|
|
:type project_id: string
|
|
:param dvr_mode: Using DVR mode or not
|
|
:type dvr_mode: boolean
|
|
:param net_name: Network name
|
|
:type net_name: string
|
|
:returns: Network object
|
|
:rtype: dict
|
|
"""
|
|
networks = neutron_client.list_networks(name=net_name)
|
|
if len(networks['networks']) == 0:
|
|
logging.info('Configuring external network')
|
|
network_msg = {
|
|
'name': net_name,
|
|
'router:external': True,
|
|
'tenant_id': project_id,
|
|
'provider:physical_network': 'physnet1',
|
|
'provider:network_type': 'flat',
|
|
}
|
|
|
|
logging.info('Creating new external network definition: %s',
|
|
net_name)
|
|
network = neutron_client.create_network(
|
|
{'network': network_msg})['network']
|
|
logging.info('New external network created: %s', network['id'])
|
|
else:
|
|
logging.warning('Network %s already exists.', net_name)
|
|
network = networks['networks'][0]
|
|
return network
|
|
|
|
|
|
def create_project_subnet(neutron_client, project_id, network, cidr, dhcp=True,
|
|
subnet_name='private_subnet', domain=None,
|
|
subnetpool=None, ip_version=4, prefix_len=24):
|
|
"""Create the project subnet.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param project_id: Project ID
|
|
:type project_id: string
|
|
:param network: Network object
|
|
:type network: dict
|
|
:param cidr: Network CIDR
|
|
:type cidr: string
|
|
:param dhcp: Run DHCP on this subnet
|
|
:type dhcp: boolean
|
|
:param subnet_name: Subnet name
|
|
:type subnet_name: string
|
|
:param domain_name: Name of the domain
|
|
:type domain_name: string or None
|
|
:param subnet_pool: Subnetpool object
|
|
:type subnet_pool: dict or None
|
|
:param ip_version: IP version: 4 or 6
|
|
:type ip_version: int
|
|
:param prefix_len: Prefix lenghths of subnets derived from subnet pools
|
|
:type prefix_len: int
|
|
:returns: Subnet object
|
|
:rtype: dict
|
|
"""
|
|
# Create subnet
|
|
subnets = neutron_client.list_subnets(name=subnet_name)
|
|
if len(subnets['subnets']) == 0:
|
|
logging.info('Creating subnet')
|
|
subnet_msg = {
|
|
'subnet': {
|
|
'name': subnet_name,
|
|
'network_id': network['id'],
|
|
'enable_dhcp': dhcp,
|
|
'ip_version': ip_version,
|
|
'tenant_id': project_id
|
|
}
|
|
}
|
|
if subnetpool:
|
|
subnet_msg['subnet']['subnetpool_id'] = subnetpool['id']
|
|
subnet_msg['subnet']['prefixlen'] = prefix_len
|
|
else:
|
|
subnet_msg['subnet']['cidr'] = cidr
|
|
subnet = neutron_client.create_subnet(subnet_msg)['subnet']
|
|
else:
|
|
logging.warning('Subnet %s already exists.', subnet_name)
|
|
subnet = subnets['subnets'][0]
|
|
return subnet
|
|
|
|
|
|
def create_external_subnet(neutron_client, project_id, network,
|
|
default_gateway=None, cidr=None,
|
|
start_floating_ip=None, end_floating_ip=None,
|
|
subnet_name='ext_net_subnet'):
|
|
"""Create the external subnet.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param project_id: Project ID
|
|
:type project_id: string
|
|
:param network: Network object
|
|
:type network: dict
|
|
:param default_gateway: Deafault gateway IP address
|
|
:type default_gateway: string
|
|
:param cidr: Network CIDR
|
|
:type cidr: string
|
|
:param start_floating_ip: Start of floating IP range: IP address
|
|
:type start_floating_ip: string or None
|
|
:param end_floating_ip: End of floating IP range: IP address
|
|
:type end_floating_ip: string or None
|
|
:param subnet_name: Subnet name
|
|
:type subnet_name: string
|
|
:returns: Subnet object
|
|
:rtype: dict
|
|
"""
|
|
subnets = neutron_client.list_subnets(name=subnet_name)
|
|
if len(subnets['subnets']) == 0:
|
|
subnet_msg = {
|
|
'name': subnet_name,
|
|
'network_id': network['id'],
|
|
'enable_dhcp': False,
|
|
'ip_version': 4,
|
|
'tenant_id': project_id
|
|
}
|
|
|
|
if default_gateway:
|
|
subnet_msg['gateway_ip'] = default_gateway
|
|
if cidr:
|
|
subnet_msg['cidr'] = cidr
|
|
if (start_floating_ip and end_floating_ip):
|
|
allocation_pool = {
|
|
'start': start_floating_ip,
|
|
'end': end_floating_ip,
|
|
}
|
|
subnet_msg['allocation_pools'] = [allocation_pool]
|
|
|
|
logging.info('Creating new subnet')
|
|
subnet = neutron_client.create_subnet({'subnet': subnet_msg})['subnet']
|
|
logging.info('New subnet created: %s', subnet['id'])
|
|
else:
|
|
logging.warning('Subnet %s already exists.', subnet_name)
|
|
subnet = subnets['subnets'][0]
|
|
return subnet
|
|
|
|
|
|
def update_subnet_dns(neutron_client, subnet, dns_servers):
|
|
"""Update subnet DNS servers.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param subnet: Subnet object
|
|
:type subnet: dict
|
|
:param dns_servers: Comma separted list of IP addresses
|
|
:type project_id: string
|
|
"""
|
|
msg = {
|
|
'subnet': {
|
|
'dns_nameservers': dns_servers.split(',')
|
|
}
|
|
}
|
|
logging.info('Updating dns_nameservers (%s) for subnet',
|
|
dns_servers)
|
|
neutron_client.update_subnet(subnet['id'], msg)
|
|
|
|
|
|
def create_provider_router(neutron_client, project_id):
|
|
"""Create the provider router.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param project_id: Project ID
|
|
:type project_id: string
|
|
:returns: Router object
|
|
:rtype: dict
|
|
"""
|
|
routers = neutron_client.list_routers(name='provider-router')
|
|
if len(routers['routers']) == 0:
|
|
logging.info('Creating provider router for external network access')
|
|
router_info = {
|
|
'router': {
|
|
'name': 'provider-router',
|
|
'tenant_id': project_id
|
|
}
|
|
}
|
|
router = neutron_client.create_router(router_info)['router']
|
|
logging.info('New router created: %s', (router['id']))
|
|
else:
|
|
logging.warning('Router provider-router already exists.')
|
|
router = routers['routers'][0]
|
|
return router
|
|
|
|
|
|
def plug_extnet_into_router(neutron_client, router, network):
|
|
"""Add external interface to virtual router.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param router: Router object
|
|
:type router: dict
|
|
:param network: Network object
|
|
:type network: dict
|
|
"""
|
|
ports = neutron_client.list_ports(device_owner='network:router_gateway',
|
|
network_id=network['id'])
|
|
if len(ports['ports']) == 0:
|
|
logging.info('Plugging router into ext_net')
|
|
router = neutron_client.add_gateway_router(
|
|
router=router['id'],
|
|
body={'network_id': network['id']})
|
|
logging.info('Router connected')
|
|
else:
|
|
logging.warning('Router already connected')
|
|
|
|
|
|
def plug_subnet_into_router(neutron_client, router, network, subnet):
|
|
"""Add subnet interface to virtual router.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param router: Router object
|
|
:type router: dict
|
|
:param network: Network object
|
|
:type network: dict
|
|
:param subnet: Subnet object
|
|
:type subnet: dict
|
|
"""
|
|
routers = neutron_client.list_routers(name=router)
|
|
if len(routers['routers']) == 0:
|
|
logging.error('Unable to locate provider router %s', router)
|
|
sys.exit(1)
|
|
else:
|
|
# Check to see if subnet already plugged into router
|
|
ports = neutron_client.list_ports(
|
|
device_owner='network:router_interface',
|
|
network_id=network['id'])
|
|
if len(ports['ports']) == 0:
|
|
logging.info('Adding interface from subnet to %s' % (router))
|
|
router = routers['routers'][0]
|
|
neutron_client.add_interface_router(router['id'],
|
|
{'subnet_id': subnet['id']})
|
|
else:
|
|
logging.warning('Router already connected to subnet')
|
|
|
|
|
|
def create_address_scope(neutron_client, project_id, name, ip_version=4):
|
|
"""Create address scope.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param project_id: Project ID
|
|
:type project_id: string
|
|
:param name: Address scope name
|
|
:type name: string
|
|
:param ip_version: IP version: 4 or 6
|
|
:type ip_version: int
|
|
:returns: Address scope object
|
|
:rtype: dict
|
|
"""
|
|
address_scopes = neutron_client.list_address_scopes(name=name)
|
|
if len(address_scopes['address_scopes']) == 0:
|
|
logging.info('Creating {} address scope'.format(name))
|
|
address_scope_info = {
|
|
'address_scope': {
|
|
'name': name,
|
|
'shared': True,
|
|
'ip_version': ip_version,
|
|
'tenant_id': project_id,
|
|
}
|
|
}
|
|
address_scope = neutron_client.create_address_scope(
|
|
address_scope_info)['address_scope']
|
|
logging.info('New address scope created: %s', (address_scope['id']))
|
|
else:
|
|
logging.warning('Address scope {} already exists.'.format(name))
|
|
address_scope = address_scopes['address_scopes'][0]
|
|
return address_scope
|
|
|
|
|
|
def create_subnetpool(neutron_client, project_id, name, subnetpool_prefix,
|
|
address_scope, shared=True):
|
|
"""Create subnet pool.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param project_id: Project ID
|
|
:type project_id: string
|
|
:param name: Subnet pool name
|
|
:type name: string
|
|
:param subnetpool_prefix: CIDR network
|
|
:type subnetpool_prefix: string
|
|
:param address_scope: Address scope object
|
|
:type address_scope: dict
|
|
:param shared: The subnet pool should be shared between projects
|
|
:type shared: boolean
|
|
:returns: Subnetpool object
|
|
:rtype: dict
|
|
"""
|
|
subnetpools = neutron_client.list_subnetpools(name=name)
|
|
if len(subnetpools['subnetpools']) == 0:
|
|
logging.info('Creating subnetpool: %s',
|
|
name)
|
|
subnetpool_msg = {
|
|
'subnetpool': {
|
|
'name': name,
|
|
'shared': shared,
|
|
'tenant_id': project_id,
|
|
'prefixes': [subnetpool_prefix],
|
|
'address_scope_id': address_scope['id'],
|
|
}
|
|
}
|
|
subnetpool = neutron_client.create_subnetpool(
|
|
subnetpool_msg)['subnetpool']
|
|
else:
|
|
logging.warning('Network %s already exists.', name)
|
|
subnetpool = subnetpools['subnetpools'][0]
|
|
return subnetpool
|
|
|
|
|
|
def create_bgp_speaker(neutron_client, local_as=12345, ip_version=4,
|
|
name='bgp-speaker'):
|
|
"""Create BGP speaker.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param local_as: Autonomous system number of the OpenStack cloud
|
|
:type local_as: int
|
|
:param remote_as: Autonomous system number of the BGP peer
|
|
:type local_as: int
|
|
:param name: BGP speaker name
|
|
:type name: string
|
|
:returns: BGP speaker object
|
|
:rtype: dict
|
|
"""
|
|
bgp_speakers = neutron_client.list_bgp_speakers(name=name)
|
|
if len(bgp_speakers['bgp_speakers']) == 0:
|
|
logging.info('Creating BGP Speaker')
|
|
bgp_speaker_msg = {
|
|
'bgp_speaker': {
|
|
'name': name,
|
|
'local_as': local_as,
|
|
'ip_version': ip_version,
|
|
}
|
|
}
|
|
bgp_speaker = neutron_client.create_bgp_speaker(
|
|
bgp_speaker_msg)['bgp_speaker']
|
|
else:
|
|
logging.warning('BGP Speaker %s already exists.', name)
|
|
bgp_speaker = bgp_speakers['bgp_speakers'][0]
|
|
return bgp_speaker
|
|
|
|
|
|
def add_network_to_bgp_speaker(neutron_client, bgp_speaker, network_name):
|
|
"""Advertise network on BGP Speaker.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param bpg_speaker: BGP speaker object
|
|
:type bgp_speaker: dict
|
|
:param network_name: Name of network to advertise
|
|
:type network_name: string
|
|
"""
|
|
network_id = get_net_uuid(neutron_client, network_name)
|
|
# There is no direct way to determine which networks have already
|
|
# been advertised. For example list_route_advertised_from_bgp_speaker shows
|
|
# ext_net as FIP /32s.
|
|
# Handle the expected exception if the route is already advertised
|
|
try:
|
|
logging.info('Advertising {} network on BGP Speaker {}'
|
|
.format(network_name, bgp_speaker['name']))
|
|
neutron_client.add_network_to_bgp_speaker(bgp_speaker['id'],
|
|
{'network_id': network_id})
|
|
except neutronexceptions.InternalServerError:
|
|
logging.warning('{} network already advertised.'.format(network_name))
|
|
|
|
|
|
def create_bgp_peer(neutron_client, peer_application_name='quagga',
|
|
remote_as=10000, auth_type='none'):
|
|
"""Create BGP peer.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param peer_application_name: Application name of the BGP peer
|
|
:type peer_application_name: string
|
|
:param remote_as: Autonomous system number of the BGP peer
|
|
:type local_as: int
|
|
:param auth_type: BGP authentication type
|
|
:type auth_type: string or None
|
|
:returns: BGP peer object
|
|
:rtype: dict
|
|
"""
|
|
peer_unit = model.get_units(peer_application_name)[0]
|
|
peer_ip = peer_unit.public_address
|
|
bgp_peers = neutron_client.list_bgp_peers(name=peer_application_name)
|
|
if len(bgp_peers['bgp_peers']) == 0:
|
|
logging.info('Creating BGP Peer')
|
|
bgp_peer_msg = {
|
|
'bgp_peer': {
|
|
'name': peer_application_name,
|
|
'peer_ip': peer_ip,
|
|
'remote_as': remote_as,
|
|
'auth_type': auth_type,
|
|
}
|
|
}
|
|
bgp_peer = neutron_client.create_bgp_peer(bgp_peer_msg)['bgp_peer']
|
|
else:
|
|
logging.warning('BGP Peer %s already exists.', peer_ip)
|
|
bgp_peer = bgp_peers['bgp_peers'][0]
|
|
return bgp_peer
|
|
|
|
|
|
def add_peer_to_bgp_speaker(neutron_client, bgp_speaker, bgp_peer):
|
|
"""Add BGP peer relationship to BGP speaker.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param bpg_speaker: BGP speaker object
|
|
:type bgp_speaker: dict
|
|
:param bpg_peer: BGP peer object
|
|
:type bgp_peer: dict
|
|
"""
|
|
# Handle the expected exception if the peer is already on the
|
|
# speaker
|
|
try:
|
|
logging.info('Adding peer {} on BGP Speaker {}'
|
|
.format(bgp_peer['name'], bgp_speaker['name']))
|
|
neutron_client.add_peer_to_bgp_speaker(bgp_speaker['id'],
|
|
{'bgp_peer_id': bgp_peer['id']})
|
|
except neutronexceptions.Conflict:
|
|
logging.warning('{} peer already on BGP speaker.'
|
|
.format(bgp_peer['name']))
|
|
|
|
|
|
def add_neutron_secgroup_rules(neutron_client, project_id):
|
|
"""Add neutron security group rules.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param project_id: Project ID
|
|
:type project_id: string
|
|
"""
|
|
secgroup = None
|
|
for group in neutron_client.list_security_groups().get('security_groups'):
|
|
if (group.get('name') == 'default' and
|
|
(group.get('project_id') == project_id or
|
|
(group.get('tenant_id') == project_id))):
|
|
secgroup = group
|
|
if not secgroup:
|
|
raise Exception("Failed to find default security group")
|
|
# Using presence of a 22 rule to indicate whether secgroup rules
|
|
# have been added
|
|
port_rules = [rule['port_range_min'] for rule in
|
|
secgroup.get('security_group_rules')]
|
|
protocol_rules = [rule['protocol'] for rule in
|
|
secgroup.get('security_group_rules')]
|
|
if 22 in port_rules:
|
|
logging.warn('Security group rules for ssh already added')
|
|
else:
|
|
logging.info('Adding ssh security group rule')
|
|
neutron_client.create_security_group_rule(
|
|
{'security_group_rule':
|
|
{'security_group_id': secgroup.get('id'),
|
|
'protocol': 'tcp',
|
|
'port_range_min': 22,
|
|
'port_range_max': 22,
|
|
'direction': 'ingress',
|
|
}
|
|
})
|
|
|
|
if 'icmp' in protocol_rules:
|
|
logging.warn('Security group rules for ping already added')
|
|
else:
|
|
logging.info('Adding ping security group rule')
|
|
neutron_client.create_security_group_rule(
|
|
{'security_group_rule':
|
|
{'security_group_id': secgroup.get('id'),
|
|
'protocol': 'icmp',
|
|
'direction': 'ingress',
|
|
}
|
|
})
|
|
|
|
|
|
def create_port(neutron_client, name, network_name):
|
|
"""Create port on network.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param name: Port name
|
|
:type name: string
|
|
:param network_name: Network name the port is on
|
|
:type network_name: string
|
|
:returns: Port object
|
|
:rtype: dict
|
|
"""
|
|
ports = neutron_client.list_ports(name=name)
|
|
if len(ports['ports']) == 0:
|
|
logging.info('Creating port: {}'.format(name))
|
|
network_id = get_net_uuid(neutron_client, network_name)
|
|
port_msg = {
|
|
'port': {
|
|
'name': name,
|
|
'network_id': network_id,
|
|
}
|
|
}
|
|
port = neutron_client.create_port(port_msg)['port']
|
|
else:
|
|
logging.debug('Port {} already exists.'.format(name))
|
|
port = ports['ports'][0]
|
|
|
|
return port
|
|
|
|
|
|
def create_floating_ip(neutron_client, network_name, port=None):
|
|
"""Create floating IP on network and optionally associate to a port.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param network_name: Name of external netowrk for FIPs
|
|
:type network_name: string
|
|
:param port: Port object
|
|
:type port: dict
|
|
:returns: Floating IP object
|
|
:rtype: dict
|
|
"""
|
|
floatingips = neutron_client.list_floatingips()
|
|
if len(floatingips['floatingips']) > 0:
|
|
if port:
|
|
for floatingip in floatingips['floatingips']:
|
|
if floatingip.get('port_id') == port['id']:
|
|
logging.debug('Floating IP with port, {}, already'
|
|
'exists.'.format(port['name']))
|
|
return floatingip
|
|
logging.warning('A floating IP already exists but ports do not match '
|
|
'Potentially creating more than one.')
|
|
|
|
logging.info('Creating floatingip')
|
|
network_id = get_net_uuid(neutron_client, network_name)
|
|
floatingip_msg = {
|
|
'floatingip': {
|
|
'floating_network_id': network_id,
|
|
}
|
|
}
|
|
if port:
|
|
floatingip_msg['floatingip']['port_id'] = port['id']
|
|
floatingip = neutron_client.create_floatingip(
|
|
floatingip_msg)['floatingip']
|
|
return floatingip
|
|
|
|
|
|
# Codename and package versions
|
|
def get_swift_codename(version):
|
|
"""Determine OpenStack codename that corresponds to swift version.
|
|
|
|
:param version: Version of Swift
|
|
:type version: string
|
|
:returns: Codename for swift
|
|
:rtype: string
|
|
"""
|
|
codenames = [k for k, v in six.iteritems(SWIFT_CODENAMES) if version in v]
|
|
return codenames[0]
|
|
|
|
|
|
def get_os_code_info(package, pkg_version):
|
|
"""Determine OpenStack codename that corresponds to package version.
|
|
|
|
:param package: Package name
|
|
:type package: string
|
|
:param pkg_version: Package version
|
|
:type pkg_version: string
|
|
:returns: Codename for package
|
|
:rtype: string
|
|
"""
|
|
# {'code_num': entry, 'code_name': OPENSTACK_CODENAMES[entry]}
|
|
# Remove epoch if it exists
|
|
if ':' in pkg_version:
|
|
pkg_version = pkg_version.split(':')[1:][0]
|
|
if 'swift' in package:
|
|
# Fully x.y.z match for swift versions
|
|
match = re.match('^(\d+)\.(\d+)\.(\d+)', pkg_version)
|
|
else:
|
|
# x.y match only for 20XX.X
|
|
# and ignore patch level for other packages
|
|
match = re.match('^(\d+)\.(\d+)', pkg_version)
|
|
|
|
if match:
|
|
vers = match.group(0)
|
|
# Generate a major version number for newer semantic
|
|
# versions of openstack projects
|
|
major_vers = vers.split('.')[0]
|
|
if (package in PACKAGE_CODENAMES and
|
|
major_vers in PACKAGE_CODENAMES[package]):
|
|
return PACKAGE_CODENAMES[package][major_vers]
|
|
else:
|
|
# < Liberty co-ordinated project versions
|
|
if 'swift' in package:
|
|
return get_swift_codename(vers)
|
|
else:
|
|
return OPENSTACK_CODENAMES[vers]
|
|
|
|
|
|
def get_current_os_versions(deployed_applications):
|
|
"""Determine OpenStack codename of deployed applications.
|
|
|
|
:param deployed_applications: List of deployed applications
|
|
:type deployed_applications: list
|
|
:returns: List of aplication to codenames dictionaries
|
|
:rtype: list
|
|
"""
|
|
versions = {}
|
|
for application in UPGRADE_SERVICES:
|
|
if application['name'] not in deployed_applications:
|
|
continue
|
|
|
|
version = generic_utils.get_pkg_version(application['name'],
|
|
application['type']['pkg'])
|
|
versions[application['name']] = (
|
|
get_os_code_info(application['type']['pkg'], version))
|
|
return versions
|
|
|
|
|
|
def get_application_config_keys(application):
|
|
"""Return application configuration keys.
|
|
|
|
:param application: Name of application
|
|
:type application: string
|
|
:returns: List of aplication configuration keys
|
|
:rtype: list
|
|
"""
|
|
application_config = model.get_application_config(application)
|
|
return list(application_config.keys())
|
|
|
|
|
|
def get_current_os_release_pair(application='keystone'):
|
|
"""Return OpenStack Release pair name.
|
|
|
|
:param application: Name of application
|
|
:type application: string
|
|
:returns: Name of the OpenStack release pair
|
|
:rtype: str
|
|
:raises: exceptions.ApplicationNotFound
|
|
:raises: exceptions.SeriesNotFound
|
|
:raises: exceptions.OSVersionNotFound
|
|
"""
|
|
machines = juju_utils.get_machines_for_application(application)
|
|
if len(machines) >= 1:
|
|
machine = machines[0]
|
|
else:
|
|
raise exceptions.ApplicationNotFound(application)
|
|
|
|
series = juju_utils.get_machine_series(machine)
|
|
if not series:
|
|
raise exceptions.SeriesNotFound()
|
|
|
|
os_version = get_current_os_versions([application]).get(application)
|
|
if not os_version:
|
|
raise exceptions.OSVersionNotFound()
|
|
|
|
return '{}_{}'.format(series, os_version)
|
|
|
|
|
|
def get_os_release(release_pair=None):
|
|
"""Return index of release in OPENSTACK_RELEASES_PAIRS.
|
|
|
|
:returns: Index of the release
|
|
:rtype: int
|
|
:raises: exceptions.ReleasePairNotFound
|
|
"""
|
|
if release_pair is None:
|
|
release_pair = get_current_os_release_pair()
|
|
try:
|
|
index = OPENSTACK_RELEASES_PAIRS.index(release_pair)
|
|
except ValueError:
|
|
msg = 'Release pair: {} not found in {}'.format(
|
|
release_pair,
|
|
OPENSTACK_RELEASES_PAIRS
|
|
)
|
|
raise exceptions.ReleasePairNotFound(msg)
|
|
return index
|
|
|
|
|
|
def get_application_config_option(application, option):
|
|
"""Return application configuration.
|
|
|
|
:param application: Name of application
|
|
:type application: string
|
|
:param option: Specific configuration option
|
|
:type option: string
|
|
:returns: Value of configuration option
|
|
:rtype: Configuration option value type
|
|
"""
|
|
application_config = model.get_application_config(application)
|
|
try:
|
|
return application_config.get(option).get('value')
|
|
except AttributeError:
|
|
return None
|
|
|
|
|
|
def get_undercloud_auth():
|
|
"""Get undercloud OpenStack authentication settings from environment.
|
|
|
|
:returns: Dictionary of authentication settings
|
|
:rtype: dict
|
|
"""
|
|
os_auth_url = os.environ.get('OS_AUTH_URL')
|
|
if os_auth_url:
|
|
api_version = os_auth_url.split('/')[-1].replace('v', '')
|
|
else:
|
|
logging.error('Missing OS authentication setting: OS_AUTH_URL')
|
|
raise exceptions.MissingOSAthenticationException(
|
|
'One or more OpenStack authetication variables could '
|
|
'be found in the environment. Please export the OS_* '
|
|
'settings into the environment.')
|
|
|
|
logging.info('AUTH_URL: {}, api_ver: {}'.format(os_auth_url, api_version))
|
|
|
|
if api_version == '2.0':
|
|
# V2
|
|
logging.info('Using keystone API V2 for undercloud auth')
|
|
auth_settings = {
|
|
'OS_AUTH_URL': os.environ.get('OS_AUTH_URL'),
|
|
'OS_TENANT_NAME': os.environ.get('OS_TENANT_NAME'),
|
|
'OS_USERNAME': os.environ.get('OS_USERNAME'),
|
|
'OS_PASSWORD': os.environ.get('OS_PASSWORD'),
|
|
'OS_REGION_NAME': os.environ.get('OS_REGION_NAME'),
|
|
'API_VERSION': 2,
|
|
}
|
|
elif api_version >= '3':
|
|
# V3 or later
|
|
logging.info('Using keystone API V3 (or later) for undercloud auth')
|
|
domain = os.environ.get('OS_DOMAIN_NAME')
|
|
auth_settings = {
|
|
'OS_AUTH_URL': os.environ.get('OS_AUTH_URL'),
|
|
'OS_USERNAME': os.environ.get('OS_USERNAME'),
|
|
'OS_PASSWORD': os.environ.get('OS_PASSWORD'),
|
|
'OS_REGION_NAME': os.environ.get('OS_REGION_NAME'),
|
|
'API_VERSION': 3,
|
|
}
|
|
if domain:
|
|
auth_settings['OS_DOMAIN_NAME': 'admin_domain'] = domain
|
|
else:
|
|
auth_settings['OS_USER_DOMAIN_NAME'] = (
|
|
os.environ.get('OS_USER_DOMAIN_NAME'))
|
|
auth_settings['OS_PROJECT_NAME'] = (
|
|
os.environ.get('OS_PROJECT_NAME'))
|
|
auth_settings['OS_PROJECT_DOMAIN_NAME'] = (
|
|
os.environ.get('OS_PROJECT_DOMAIN_NAME'))
|
|
os_project_id = os.environ.get('OS_PROJECT_ID')
|
|
if os_project_id is not None:
|
|
auth_settings['OS_PROJECT_ID'] = os_project_id
|
|
|
|
# Validate settings
|
|
for key, settings in list(auth_settings.items()):
|
|
if settings is None:
|
|
logging.error('Missing OS authentication setting: {}'
|
|
''.format(key))
|
|
raise exceptions.MissingOSAthenticationException(
|
|
'One or more OpenStack authetication variables could '
|
|
'be found in the environment. Please export the OS_* '
|
|
'settings into the environment.')
|
|
|
|
return auth_settings
|
|
|
|
|
|
# Openstack Client helpers
|
|
def get_keystone_ip():
|
|
"""Return the IP address to use when communicating with keystone api.
|
|
|
|
:returns: IP address
|
|
:rtype: str
|
|
"""
|
|
if get_application_config_option('keystone', 'vip'):
|
|
return get_application_config_option('keystone', 'vip')
|
|
unit = model.get_units('keystone')[0]
|
|
return unit.public_address
|
|
|
|
|
|
def get_keystone_api_version():
|
|
"""Return the keystone api version.
|
|
|
|
:returns: Keystone's api version
|
|
:rtype: int
|
|
"""
|
|
os_version = get_current_os_versions('keystone')['keystone']
|
|
api_version = get_application_config_option('keystone',
|
|
'preferred-api-version')
|
|
if os_version >= 'queens':
|
|
api_version = 3
|
|
elif api_version is None:
|
|
api_version = 2
|
|
|
|
return int(api_version)
|
|
|
|
|
|
def get_overcloud_auth(address=None):
|
|
"""Get overcloud OpenStack authentication from the environment.
|
|
|
|
:returns: Dictionary of authentication settings
|
|
:rtype: dict
|
|
"""
|
|
tls_rid = model.get_relation_id('keystone', 'vault',
|
|
remote_interface_name='certificates')
|
|
ssl_config = get_application_config_option('keystone', 'ssl_cert')
|
|
if tls_rid or ssl_config:
|
|
transport = 'https'
|
|
port = 35357
|
|
else:
|
|
transport = 'http'
|
|
port = 5000
|
|
|
|
if not address:
|
|
address = get_keystone_ip()
|
|
|
|
password = juju_utils.leader_get('keystone', 'admin_passwd')
|
|
|
|
if get_keystone_api_version() == 2:
|
|
# V2 Explicitly, or None when charm does not possess the config key
|
|
logging.info('Using keystone API V2 for overcloud auth')
|
|
auth_settings = {
|
|
'OS_AUTH_URL': '%s://%s:%i/v2.0' % (transport, address, port),
|
|
'OS_TENANT_NAME': 'admin',
|
|
'OS_USERNAME': 'admin',
|
|
'OS_PASSWORD': password,
|
|
'OS_REGION_NAME': 'RegionOne',
|
|
'API_VERSION': 2,
|
|
}
|
|
else:
|
|
# V3 or later
|
|
logging.info('Using keystone API V3 (or later) for overcloud auth')
|
|
auth_settings = {
|
|
'OS_AUTH_URL': '%s://%s:%i/v3' % (transport, address, port),
|
|
'OS_USERNAME': 'admin',
|
|
'OS_PASSWORD': password,
|
|
'OS_REGION_NAME': 'RegionOne',
|
|
'OS_DOMAIN_NAME': 'admin_domain',
|
|
'OS_USER_DOMAIN_NAME': 'admin_domain',
|
|
'OS_PROJECT_NAME': 'admin',
|
|
'OS_PROJECT_DOMAIN_NAME': 'admin_domain',
|
|
'API_VERSION': 3,
|
|
}
|
|
if tls_rid:
|
|
unit = model.get_first_unit_name('keystone')
|
|
model.scp_from_unit(
|
|
unit,
|
|
KEYSTONE_REMOTE_CACERT,
|
|
KEYSTONE_LOCAL_CACERT)
|
|
|
|
if os.path.exists(KEYSTONE_LOCAL_CACERT):
|
|
os.chmod(KEYSTONE_LOCAL_CACERT, 0o644)
|
|
auth_settings['OS_CACERT'] = KEYSTONE_LOCAL_CACERT
|
|
|
|
return auth_settings
|
|
|
|
|
|
def get_urllib_opener():
|
|
"""Create a urllib opener taking into account proxy settings.
|
|
|
|
Using urllib.request.urlopen will automatically handle proxies so none
|
|
of this function is needed except we are currently specifying proxies
|
|
via AMULET_HTTP_PROXY rather than http_proxy so a ProxyHandler is needed
|
|
explicitly stating the proxies.
|
|
|
|
:returns: An opener which opens URLs via BaseHandlers chained together
|
|
:rtype: urllib.request.OpenerDirector
|
|
"""
|
|
http_proxy = os.getenv('AMULET_HTTP_PROXY')
|
|
logging.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
|
|
|
|
if http_proxy:
|
|
handler = urllib.request.ProxyHandler({'http': http_proxy})
|
|
else:
|
|
handler = urllib.request.HTTPHandler()
|
|
return urllib.request.build_opener(handler)
|
|
|
|
|
|
def get_images_by_name(glance, image_name):
|
|
"""Get all glance image objects with the given name.
|
|
|
|
:param glance: Authenticated glanceclient
|
|
:type glance: glanceclient.Client
|
|
:param image_name: Name of image
|
|
:type image_name: str
|
|
|
|
:returns: List of glance images
|
|
:rtype: [glanceclient.v2.image, ...]
|
|
"""
|
|
return [i for i in glance.images.list() if image_name == i.name]
|
|
|
|
|
|
def find_cirros_image(arch):
|
|
"""Return the url for the latest cirros image for the given architecture.
|
|
|
|
:param arch: aarch64, arm, i386, amd64, x86_64 etc
|
|
:type arch: str
|
|
:returns: URL for latest cirros image
|
|
:rtype: str
|
|
"""
|
|
opener = get_urllib_opener()
|
|
f = opener.open(CIRROS_RELEASE_URL)
|
|
version = f.read().strip().decode()
|
|
cirros_img = 'cirros-{}-{}-disk.img'.format(version, arch)
|
|
return '{}/{}/{}'.format(CIRROS_IMAGE_URL, version, cirros_img)
|
|
|
|
|
|
def find_ubuntu_image(release, arch):
|
|
"""Return url for image."""
|
|
return UBUNTU_IMAGE_URLS[release].format(release=release, arch=arch)
|
|
|
|
|
|
def download_image(image_url, target_file):
|
|
"""Download the image from the given url to the specified file.
|
|
|
|
:param image_url: URL to download image from
|
|
:type image_url: str
|
|
:param target_file: Local file to savee image to
|
|
:type target_file: str
|
|
"""
|
|
opener = get_urllib_opener()
|
|
urllib.request.install_opener(opener)
|
|
urllib.request.urlretrieve(image_url, target_file)
|
|
|
|
|
|
def _resource_reaches_status(resource, resource_id,
|
|
expected_status='available',
|
|
msg='resource'):
|
|
"""Wait for an openstack resources status to reach an expected status.
|
|
|
|
Wait for an openstack resources status to reach an expected status
|
|
within a specified time. Useful to confirm that nova instances, cinder
|
|
vols, snapshots, glance images, heat stacks and other resources
|
|
eventually reach the expected status.
|
|
|
|
:param resource: pointer to os resource type, ex: heat_client.stacks
|
|
:type resource: str
|
|
:param resource_id: unique id for the openstack resource
|
|
:type resource_id: str
|
|
:param expected_status: status to expect resource to reach
|
|
:type expected_status: str
|
|
:param msg: text to identify purpose in logging
|
|
:type msy: str
|
|
:raises: AssertionError
|
|
"""
|
|
resource_status = resource.get(resource_id).status
|
|
logging.info(resource_status)
|
|
assert resource_status == expected_status, (
|
|
"Resource in {} state, waiting for {}" .format(resource_status,
|
|
expected_status,))
|
|
|
|
|
|
def resource_reaches_status(resource,
|
|
resource_id,
|
|
expected_status='available',
|
|
msg='resource',
|
|
wait_exponential_multiplier=1,
|
|
wait_iteration_max_time=60,
|
|
stop_after_attempt=8,
|
|
):
|
|
"""Wait for an openstack resources status to reach an expected status.
|
|
|
|
Wait for an openstack resources status to reach an expected status
|
|
within a specified time. Useful to confirm that nova instances, cinder
|
|
vols, snapshots, glance images, heat stacks and other resources
|
|
eventually reach the expected status.
|
|
|
|
:param resource: pointer to os resource type, ex: heat_client.stacks
|
|
:type resource: str
|
|
:param resource_id: unique id for the openstack resource
|
|
:type resource_id: str
|
|
:param expected_status: status to expect resource to reach
|
|
:type expected_status: str
|
|
:param msg: text to identify purpose in logging
|
|
:type msg: str
|
|
:param wait_exponential_multiplier: Wait 2^x * wait_exponential_multiplier
|
|
seconds between each retry
|
|
:type wait_exponential_multiplier: int
|
|
:param wait_iteration_max_time: Wait a max of wait_iteration_max_time
|
|
between retries.
|
|
:type wait_iteration_max_time: int
|
|
:param stop_after_attempt: Stop after stop_after_attempt retires.
|
|
:type stop_after_attempt: int
|
|
:raises: AssertionError
|
|
"""
|
|
retryer = tenacity.Retrying(
|
|
wait=tenacity.wait_exponential(
|
|
multiplier=wait_exponential_multiplier,
|
|
max=wait_iteration_max_time),
|
|
reraise=True,
|
|
stop=tenacity.stop_after_attempt(stop_after_attempt))
|
|
retryer(
|
|
_resource_reaches_status,
|
|
resource,
|
|
resource_id,
|
|
expected_status,
|
|
msg)
|
|
|
|
|
|
def _resource_removed(resource, resource_id, msg="resource"):
|
|
"""Wait for an openstack resource to no longer be present.
|
|
|
|
:param resource: pointer to os resource type, ex: heat_client.stacks
|
|
:type resource: str
|
|
:param resource_id: unique id for the openstack resource
|
|
:type resource_id: str
|
|
:param msg: text to identify purpose in logging
|
|
:type msg: str
|
|
:raises: AssertionError
|
|
"""
|
|
matching = [r for r in resource.list() if r.id == resource_id]
|
|
logging.debug("Resource {} still present".format(resource_id))
|
|
assert len(matching) == 0, "Resource {} still present".format(resource_id)
|
|
|
|
|
|
def resource_removed(resource,
|
|
resource_id,
|
|
msg='resource',
|
|
wait_exponential_multiplier=1,
|
|
wait_iteration_max_time=60,
|
|
stop_after_attempt=8):
|
|
"""Wait for an openstack resource to no longer be present.
|
|
|
|
:param resource: pointer to os resource type, ex: heat_client.stacks
|
|
:type resource: str
|
|
:param resource_id: unique id for the openstack resource
|
|
:type resource_id: str
|
|
:param msg: text to identify purpose in logging
|
|
:type msg: str
|
|
:param wait_exponential_multiplier: Wait 2^x * wait_exponential_multiplier
|
|
seconds between each retry
|
|
:type wait_exponential_multiplier: int
|
|
:param wait_iteration_max_time: Wait a max of wait_iteration_max_time
|
|
between retries.
|
|
:type wait_iteration_max_time: int
|
|
:param stop_after_attempt: Stop after stop_after_attempt retires.
|
|
:type stop_after_attempt: int
|
|
:raises: AssertionError
|
|
"""
|
|
retryer = tenacity.Retrying(
|
|
wait=tenacity.wait_exponential(
|
|
multiplier=wait_exponential_multiplier,
|
|
max=wait_iteration_max_time),
|
|
reraise=True,
|
|
stop=tenacity.stop_after_attempt(stop_after_attempt))
|
|
print(retryer)
|
|
retryer(
|
|
_resource_removed,
|
|
resource,
|
|
resource_id,
|
|
msg)
|
|
|
|
|
|
def delete_resource(resource, resource_id, msg="resource"):
|
|
"""Delete an openstack resource.
|
|
|
|
Delete an openstack resource, such as one instance, keypair,
|
|
image, volume, stack, etc., and confirm deletion within max wait time.
|
|
|
|
:param resource: pointer to os resource type, ex:glance_client.images
|
|
:type resource: str
|
|
:param resource_id: unique name or id for the openstack resource
|
|
:type resource_id: str
|
|
:param msg: text to identify purpose in logging
|
|
:type msg: str
|
|
"""
|
|
logging.debug('Deleting OpenStack resource '
|
|
'{} ({})'.format(resource_id, msg))
|
|
resource.delete(resource_id)
|
|
resource_removed(resource, resource_id, msg)
|
|
|
|
|
|
def delete_image(glance, img_id):
|
|
"""Delete the given image from glance.
|
|
|
|
:param glance: Authenticated glanceclient
|
|
:type glance: glanceclient.Client
|
|
:param img_id: unique name or id for the openstack resource
|
|
:type img_id: str
|
|
"""
|
|
delete_resource(glance.images, img_id, msg="glance image")
|
|
|
|
|
|
def upload_image_to_glance(glance, local_path, image_name, disk_format='qcow2',
|
|
visibility='public', container_format='bare'):
|
|
"""Upload the given image to glance and apply the given label.
|
|
|
|
:param glance: Authenticated glanceclient
|
|
:type glance: glanceclient.Client
|
|
:param local_path: Path to local image
|
|
:type local_path: str
|
|
:param image_name: The label to give the image in glance
|
|
:type image_name: str
|
|
:param disk_format: The of the underlying disk image.
|
|
:type disk_format: str
|
|
:param visibility: Who can access image
|
|
:type visibility: str (public, private, shared or community)
|
|
:param container_format: Whether the virtual machine image is in a file
|
|
format that also contains metadata about the
|
|
actual virtual machine.
|
|
:type container_format: str
|
|
:returns: glance image pointer
|
|
:rtype: glanceclient.common.utils.RequestIdProxy
|
|
"""
|
|
# Create glance image
|
|
image = glance.images.create(
|
|
name=image_name,
|
|
disk_format=disk_format,
|
|
visibility=visibility,
|
|
container_format=container_format)
|
|
glance.images.upload(image.id, open(local_path, 'rb'))
|
|
|
|
resource_reaches_status(
|
|
glance.images,
|
|
image.id,
|
|
expected_status='active',
|
|
msg='Image status wait')
|
|
|
|
return image
|
|
|
|
|
|
def create_image(glance, image_url, image_name, image_cache_dir=None, tags=[]):
|
|
"""Download the image and upload it to glance.
|
|
|
|
Download an image from image_url and upload it to glance labelling
|
|
the image with image_url, validate and return a resource pointer.
|
|
|
|
:param glance: Authenticated glanceclient
|
|
:type glance: glanceclient.Client
|
|
:param image_url: URL to download image from
|
|
:type image_url: str
|
|
:param image_name: display name for new image
|
|
:type image_name: str
|
|
:param image_cache_dir: Directory to store image in before uploading. If it
|
|
is not passed, or is None, then a tmp directory is used.
|
|
:type image_cache_dir: Option[str, None]
|
|
:param tags: Tags to add to image
|
|
:type tags: list of str
|
|
:returns: glance image pointer
|
|
:rtype: glanceclient.common.utils.RequestIdProxy
|
|
"""
|
|
if image_cache_dir is None:
|
|
image_cache_dir = tempfile.gettempdir()
|
|
|
|
logging.debug('Creating glance cirros image '
|
|
'({})...'.format(image_name))
|
|
|
|
img_name = os.path.basename(urllib.parse.urlparse(image_url).path)
|
|
local_path = os.path.join(image_cache_dir, img_name)
|
|
|
|
if not os.path.exists(local_path):
|
|
download_image(image_url, local_path)
|
|
|
|
image = upload_image_to_glance(glance, local_path, image_name)
|
|
for tag in tags:
|
|
result = glance.image_tags.update(image.id, tag)
|
|
logging.debug(
|
|
'applying tag to image: glance.image_tags.update({}, {}) = {}'
|
|
.format(image.id, tags, result))
|
|
return image
|
|
|
|
|
|
def create_ssh_key(nova_client, keypair_name, replace=False):
|
|
"""Create ssh key.
|
|
|
|
:param nova_client: Authenticated nova client
|
|
:type nova_client: novaclient.v2.client.Client
|
|
:param keypair_name: Label to apply to keypair in Openstack.
|
|
:type keypair_name: str
|
|
:param replace: Whether to replace the existing keypair if it already
|
|
exists.
|
|
:type replace: str
|
|
:returns: The keypair
|
|
:rtype: nova.objects.keypair
|
|
"""
|
|
existing_keys = nova_client.keypairs.findall(name=keypair_name)
|
|
if existing_keys:
|
|
if replace:
|
|
logging.info('Deleting key(s) {}'.format(keypair_name))
|
|
for key in existing_keys:
|
|
nova_client.keypairs.delete(key)
|
|
else:
|
|
return existing_keys[0]
|
|
logging.info('Creating key %s' % (keypair_name))
|
|
return nova_client.keypairs.create(name=keypair_name)
|
|
|
|
|
|
def get_private_key_file(keypair_name):
|
|
"""Location of the file containing the private key with the given label.
|
|
|
|
:param keypair_name: Label of keypair in Openstack.
|
|
:type keypair_name: str
|
|
:returns: Path to file containing key
|
|
:rtype: str
|
|
"""
|
|
return 'tests/id_rsa_{}'.format(keypair_name)
|
|
|
|
|
|
def write_private_key(keypair_name, key):
|
|
"""Store supplied private key in file.
|
|
|
|
:param keypair_name: Label of keypair in Openstack.
|
|
:type keypair_name: str
|
|
:param key: PEM Encoded Private Key
|
|
:type key: str
|
|
"""
|
|
with open(get_private_key_file(keypair_name), 'w') as key_file:
|
|
key_file.write(key)
|
|
|
|
|
|
def get_private_key(keypair_name):
|
|
"""Return private key.
|
|
|
|
:param keypair_name: Label of keypair in Openstack.
|
|
:type keypair_name: str
|
|
:returns: PEM Encoded Private Key
|
|
:rtype: str
|
|
"""
|
|
key_file = get_private_key_file(keypair_name)
|
|
if not os.path.isfile(key_file):
|
|
return None
|
|
with open(key_file, 'r') as key_file:
|
|
key = key_file.read()
|
|
return key
|
|
|
|
|
|
def get_public_key(nova_client, keypair_name):
|
|
"""Return public key from Openstack.
|
|
|
|
:param nova_client: Authenticated nova client
|
|
:type nova_client: novaclient.v2.client.Client
|
|
:param keypair_name: Label of keypair in Openstack.
|
|
:type keypair_name: str
|
|
:returns: OpenSSH Encoded Public Key
|
|
:rtype: str or None
|
|
"""
|
|
keys = nova_client.keypairs.findall(name=keypair_name)
|
|
if keys:
|
|
return keys[0].public_key
|
|
else:
|
|
return None
|
|
|
|
|
|
def valid_key_exists(nova_client, keypair_name):
|
|
"""Check if a valid public/private keypair exists for keypair_name.
|
|
|
|
:param nova_client: Authenticated nova client
|
|
:type nova_client: novaclient.v2.client.Client
|
|
:param keypair_name: Label of keypair in Openstack.
|
|
:type keypair_name: str
|
|
"""
|
|
pub_key = get_public_key(nova_client, keypair_name)
|
|
priv_key = get_private_key(keypair_name)
|
|
if not all([pub_key, priv_key]):
|
|
return False
|
|
return cert.is_keys_valid(pub_key, priv_key)
|
|
|
|
|
|
def get_ports_from_device_id(neutron_client, device_id):
|
|
"""Return the ports associated with a given device.
|
|
|
|
:param neutron_client: Authenticated neutronclient
|
|
:type neutron_client: neutronclient.Client object
|
|
:param device_id: The id of the device to look for
|
|
:type device_id: str
|
|
:returns: List of port objects
|
|
:rtype: []
|
|
"""
|
|
ports = []
|
|
for _port in neutron_client.list_ports().get('ports'):
|
|
if device_id in _port.get('device_id'):
|
|
ports.append(_port)
|
|
return ports
|
|
|
|
|
|
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60),
|
|
reraise=True, stop=tenacity.stop_after_attempt(8))
|
|
def cloud_init_complete(nova_client, vm_id, bootstring):
|
|
"""Wait for cloud init to complete on the given vm.
|
|
|
|
If cloud init does not complete in the alloted time then
|
|
exceptions.CloudInitIncomplete is raised.
|
|
|
|
:param nova_client: Authenticated nova client
|
|
:type nova_client: novaclient.v2.client.Client
|
|
:param vm_id,: The id of the server to monitor.
|
|
:type vm_id: str (uuid)
|
|
:param bootstring: The string to look for in the console output that will
|
|
indicate cloud init is complete.
|
|
:type bootstring: str
|
|
:raises: exceptions.CloudInitIncomplete
|
|
"""
|
|
instance = nova_client.servers.find(id=vm_id)
|
|
console_log = instance.get_console_output()
|
|
if bootstring not in console_log:
|
|
raise exceptions.CloudInitIncomplete()
|
|
|
|
|
|
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60),
|
|
reraise=True, stop=tenacity.stop_after_attempt(8))
|
|
def ping_response(ip):
|
|
"""Wait for ping to respond on the given IP.
|
|
|
|
:param ip: IP address to ping
|
|
:type ip: str
|
|
:raises: subprocess.CalledProcessError
|
|
"""
|
|
cmd = ['ping', '-c', '1', '-W', '1', ip]
|
|
subprocess.check_call(cmd, stdout=subprocess.DEVNULL)
|
|
|
|
|
|
def ssh_test(username, ip, vm_name, password=None, privkey=None):
|
|
"""SSH to given ip using supplied credentials.
|
|
|
|
:param username: Username to connect with
|
|
:type username: str
|
|
:param ip: IP address to ssh to.
|
|
:type ip: str
|
|
:param vm_name: Name of VM.
|
|
:type vm_name: str
|
|
:param password: Password to authenticate with. If supplied it is used
|
|
rather than privkey.
|
|
:type password: str
|
|
:param privkey: Private key to authenticate with. If a password is
|
|
supplied it is used rather than the private key.
|
|
:type privkey: str
|
|
:raises: exceptions.SSHFailed
|
|
"""
|
|
logging.info('Attempting to ssh to %s(%s)' % (vm_name, ip))
|
|
ssh = paramiko.SSHClient()
|
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
if password:
|
|
ssh.connect(ip, username=username, password=password)
|
|
else:
|
|
key = paramiko.RSAKey.from_private_key(io.StringIO(privkey))
|
|
ssh.connect(ip, username=username, password='', pkey=key)
|
|
stdin, stdout, stderr = ssh.exec_command('uname -n')
|
|
return_string = stdout.readlines()[0].strip()
|
|
ssh.close()
|
|
if return_string == vm_name:
|
|
logging.info('SSH to %s(%s) succesfull' % (vm_name, ip))
|
|
else:
|
|
logging.info('SSH to %s(%s) failed (%s != %s)' % (vm_name, ip,
|
|
return_string,
|
|
vm_name))
|
|
raise exceptions.SSHFailed()
|
|
|
|
|
|
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=0.01),
|
|
reraise=True, stop=tenacity.stop_after_delay(60) |
|
|
tenacity.stop_after_attempt(100))
|
|
def neutron_agent_appears(neutron_client, binary):
|
|
"""Wait for Neutron agent to appear and return agent_id.
|
|
|
|
:param neutron_client: Neutron client
|
|
:type neutron_client: Pointer to Neutron client object
|
|
:param binary: Name of agent we want to appear
|
|
:type binary: str
|
|
:returns: result set from Neutron list_agents call
|
|
:rtype: dict
|
|
:raises: exceptions.NeutronAgentMissing
|
|
"""
|
|
result = neutron_client.list_agents(binary=binary)
|
|
for agent in result.get('agents', []):
|
|
agent_id = agent.get('id', None)
|
|
if agent_id:
|
|
break
|
|
else:
|
|
raise exceptions.NeutronAgentMissing(
|
|
'no agents for binary "{}"'.format(binary))
|
|
return result
|
|
|
|
|
|
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=0.01),
|
|
reraise=True,
|
|
stop=tenacity.stop_after_delay(60) |
|
|
tenacity.stop_after_attempt(100))
|
|
def neutron_bgp_speaker_appears_on_agent(neutron_client, agent_id):
|
|
"""Wait for Neutron BGP speaker to appear on agent.
|
|
|
|
:param neutron_client: Neutron client
|
|
:type neutron_client: Pointer to Neutron client object
|
|
:param agent_id: Neutron agent UUID
|
|
:type agent_id: str
|
|
:param speaker_id: Neutron BGP speaker UUID
|
|
:type speaker_id: str
|
|
:returns: result set from Neutron list_bgp_speaker_on_dragent call
|
|
:rtype: dict
|
|
:raises: exceptions.NeutronBGPSpeakerMissing
|
|
"""
|
|
result = neutron_client.list_bgp_speaker_on_dragent(agent_id)
|
|
for bgp_speaker in result.get('bgp_speakers', []):
|
|
bgp_speaker_id = bgp_speaker.get('id', None)
|
|
if bgp_speaker_id:
|
|
break
|
|
else:
|
|
raise exceptions.NeutronBGPSpeakerMissing(
|
|
'No BGP Speaker appeared on agent "{}"'
|
|
''.format(agent_id))
|
|
return result
|
|
|
|
|
|
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60),
|
|
reraise=True, stop=tenacity.stop_after_attempt(80))
|
|
def wait_for_server_migration(nova_client, vm_name, original_hypervisor):
|
|
"""Wait for guest to migrate to a different hypervisor.
|
|
|
|
:param nova_client: Authenticated nova client
|
|
:type nova_client: novaclient.v2.client.Client
|
|
:param vm_name: Name of guest to monitor
|
|
:type vm_name: str
|
|
:param original_hypervisor: Name of hypervisor that was hosting guest
|
|
prior to migration.
|
|
:type original_hypervisor: str
|
|
:raises: exceptions.NovaGuestMigrationFailed
|
|
"""
|
|
server = nova_client.servers.find(name=vm_name)
|
|
current_hypervisor = getattr(server, 'OS-EXT-SRV-ATTR:host')
|
|
logging.info('{} is on {} in state {}'.format(
|
|
vm_name,
|
|
current_hypervisor,
|
|
server.status))
|
|
if original_hypervisor == current_hypervisor or server.status != 'ACTIVE':
|
|
raise exceptions.NovaGuestMigrationFailed(
|
|
'Migration of {} away from {} timed out or failed'.format(
|
|
vm_name,
|
|
original_hypervisor))
|
|
else:
|
|
logging.info('SUCCESS {} has migrated to {}'.format(
|
|
vm_name,
|
|
current_hypervisor))
|
|
|
|
|
|
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60),
|
|
reraise=True, stop=tenacity.stop_after_attempt(80),
|
|
retry=tenacity.retry_if_exception_type(
|
|
exceptions.NovaGuestRestartFailed))
|
|
def wait_for_server_update_and_active(nova_client, vm_name,
|
|
original_updatetime):
|
|
"""Wait for guests metadata to be updated and for status to become active.
|
|
|
|
:param nova_client: Authenticated nova client
|
|
:type nova_client: novaclient.v2.client.Client
|
|
:param vm_name: Name of guest to monitor
|
|
:type vm_name: str
|
|
:param original_updatetime: The time the metadata was previously updated.
|
|
:type original_updatetime: datetime
|
|
:raises: exceptions.NovaGuestMigrationFailed
|
|
"""
|
|
server = nova_client.servers.find(name=vm_name)
|
|
current_updatetime = datetime.datetime.strptime(
|
|
server.updated,
|
|
"%Y-%m-%dT%H:%M:%SZ")
|
|
if current_updatetime <= original_updatetime or server.status != 'ACTIVE':
|
|
logging.info('{} Updated: {} Satus: {})'.format(
|
|
vm_name,
|
|
current_updatetime,
|
|
server.status))
|
|
raise exceptions.NovaGuestRestartFailed(
|
|
'Restart of {} after crash failed'.format(vm_name))
|
|
else:
|
|
logging.info('SUCCESS {} has restarted'.format(vm_name))
|
|
|
|
|
|
def enable_all_nova_services(nova_client):
|
|
"""Enable all nova services.
|
|
|
|
:param nova_client: Authenticated nova client
|
|
:type nova_client: novaclient.v2.client.Client
|
|
"""
|
|
for svc in nova_client.services.list():
|
|
if svc.status == 'disabled':
|
|
logging.info("Enabling {} on {}".format(svc.binary, svc.host))
|
|
nova_client.services.enable(svc.host, svc.binary)
|
|
|
|
|
|
def get_hypervisor_for_guest(nova_client, guest_name):
|
|
"""Return the name of the hypervisor hosting a guest.
|
|
|
|
:param nova_client: Authenticated nova client
|
|
:type nova_client: novaclient.v2.client.Client
|
|
:param vm_name: Name of guest to loohup
|
|
:type vm_name: str
|
|
"""
|
|
logging.info('Finding hosting hypervisor')
|
|
server = nova_client.servers.find(name=guest_name)
|
|
return getattr(server, 'OS-EXT-SRV-ATTR:host')
|
|
|
|
|
|
def get_keystone_session_from_relation(client_app,
|
|
identity_app='keystone',
|
|
relation_name='identity-service',
|
|
scope='PROJECT',
|
|
verify=None):
|
|
"""Extract credentials information from a relation & return a session.
|
|
|
|
:param client_app: Name of application receiving credentials.
|
|
:type client_app: string
|
|
:param identity_app: Name of application providing credentials.
|
|
:type identity_app: string
|
|
:param relation_name: Name of relation between applications.
|
|
:type relation_name: string
|
|
:param scope: Authentication scope: PROJECT or DOMAIN
|
|
:type scope: string
|
|
:param verify: Control TLS certificate verification behaviour
|
|
:type verify: any (True - use system certs,
|
|
False - do not verify,
|
|
None - defer to requests library to find certs,
|
|
str - path to a CA cert bundle)
|
|
:returns: Keystone session object
|
|
:rtype: keystoneauth1.session.Session object
|
|
"""
|
|
relation = juju_utils.get_relation_from_unit(
|
|
client_app,
|
|
identity_app,
|
|
relation_name)
|
|
|
|
api_version = int(relation.get('api_version', 2))
|
|
creds = get_overcloud_auth()
|
|
creds['OS_USERNAME'] = relation['service_username']
|
|
creds['OS_PASSWORD'] = relation['service_password']
|
|
creds['OS_PROJECT_NAME'] = relation['service_tenant']
|
|
creds['OS_TENANT_NAME'] = relation['service_tenant']
|
|
if api_version == 3:
|
|
creds['OS_DOMAIN_NAME'] = relation['service_domain']
|
|
creds['OS_USER_DOMAIN_NAME'] = relation['service_domain']
|
|
creds['OS_PROJECT_DOMAIN_NAME'] = relation['service_domain']
|
|
|
|
return get_keystone_session(creds, scope=scope, verify=verify)
|