#!/usr/bin/env python from .os_versions import ( OPENSTACK_CODENAMES, SWIFT_CODENAMES, PACKAGE_CODENAMES, ) from keystoneclient.v3 import client as keystoneclient_v3 from keystoneauth1 import session from keystoneauth1.identity import ( v3, v2, ) from novaclient import client as novaclient_client from neutronclient.v2_0 import client as neutronclient from neutronclient.common import exceptions as neutronexceptions import logging import os import re import six import sys import juju_wait from zaza import model from zaza.charm_lifecycle import utils as lifecycle_utils from zaza.utilities import ( exceptions, _local_utils, ) 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']}, ] # 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_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_keystone_scope(): """Return Keystone scope based on OpenStack release :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(opentackrc_creds, insecure=True, scope='PROJECT'): """Return keystone session :param openrc_creds: Openstack RC credentials :type openrc_creds: dict :param insecure: Allow insecure HTTPS connections :type insecure: boolean :param scope: Authentication scope: PROJECT or DOMAIN :type scope: string :returns: Keystone session object :rtype: keystoneauth1.session.Session object """ keystone_creds = get_ks_creds(opentackrc_creds, scope=scope) if opentackrc_creds.get('API_VERSION', 2) == 2: auth = v2.Password(**keystone_creds) else: auth = v3.Password(**keystone_creds) return session.Session(auth=auth, verify=not insecure) def get_overcloud_keystone_session(): """Return Over cloud keystone session :returns keystone_session: keystoneauth1.session.Session object :rtype: keystoneauth1.session.Session """ return get_keystone_session(get_overcloud_auth(), scope=get_keystone_scope()) def get_undercloud_keystone_session(): """Return Under cloud keystone session :returns keystone_session: keystoneauth1.session.Session object :rtype: keystoneauth1.session.Session """ return get_keystone_session(get_undercloud_auth(), scope=get_keystone_scope()) def get_keystone_session_client(session): """Return keystoneclient authenticated by keystone session :param session: Keystone session object :type session: keystoneauth1.session.Session object :returns: Authenticated keystoneclient :rtype: keystoneclient.v3.Client object """ return keystoneclient_v3.Client(session=session) def get_keystone_client(opentackrc_creds, insecure=True): """Return authenticated keystoneclient and set auth_ref for service_catalog :param openrc_creds: Openstack RC credentials :type openrc_creds: dict :param insecure: Allow insecure HTTPS connections :type insecure: boolean :returns: Authenticated keystoneclient :rtype: keystoneclient.v3.Client object """ session = get_keystone_session(opentackrc_creds, insecure) client = get_keystone_session_client(session) keystone_creds = get_ks_creds(opentackrc_creds) if opentackrc_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 _local_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 (_local_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 :returns: Nothing: This fucntion is executed for its sideffect :rtype: None """ 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( lifecycle_utils.get_juju_model(), 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 :returns: Nothing: This fucntion is executed for its sideffect :rtype: None """ 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 :returns: Nothing: This fucntion is executed for its sideffect :rtype: None """ 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 :returns: Nothing: This fucntion is executed for its sideffect :rtype: None """ 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 :returns: Nothing: This fucntion is executed for its sideffect :rtype: None """ 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( lifecycle_utils.get_juju_model(), 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 :returns: Nothing: This fucntion is executed for its sideffect :rtype: None """ # 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 :returns: Nothing: This fucntion is executed for its sideffect :rtype: None """ 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 = _local_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( lifecycle_utils.get_juju_model(), application) return list(application_config.keys()) 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( lifecycle_utils.get_juju_model(), application) try: return application_config.get(option).get('value') except AttributeError: return None def get_undercloud_auth(): """Get the undercloud OpenStack authentication settings from the 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(): if get_application_config_option('keystone', 'vip'): return get_application_config_option('keystone', 'vip') unit = model.get_units( lifecycle_utils.get_juju_model(), 'keystone')[0] return unit.public_address def get_overcloud_auth(): """Get the overcloud OpenStack authentication settings from the environment. :returns: Dictionary of authentication settings :rtype: dict """ if get_application_config_option('keystone', 'use-https').lower() == 'yes': transport = 'https' port = 35357 else: transport = 'http' port = 5000 address = get_keystone_ip() 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 if 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': 'openstack', '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': 'openstack', '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, } return auth_settings