Files
zaza-openstack-tests/zaza/openstack/utilities/openstack.py
2019-04-25 17:46:03 +02:00

2048 lines
71 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)
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60),
reraise=True, stop=tenacity.stop_after_attempt(2))
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 msy: 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 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')