edd72883de
All Nova extensions are enabled in API v2.1, which is the only API version we support now. There's no reason to query for these things. Change-Id: Ib12b2f4fe53182e047d6264de850178909b8fd5d Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
632 lines
24 KiB
Python
632 lines
24 KiB
Python
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import ipaddress
|
|
import socket
|
|
|
|
from openstack import _log
|
|
from openstack import exceptions
|
|
from openstack import utils
|
|
|
|
|
|
NON_CALLABLES = (str, bool, dict, int, float, list, type(None))
|
|
|
|
|
|
def find_nova_interfaces(
|
|
addresses, ext_tag=None, key_name=None, version=4, mac_addr=None
|
|
):
|
|
ret = []
|
|
for k, v in iter(addresses.items()):
|
|
if key_name is not None and k != key_name:
|
|
# key_name is specified and it doesn't match the current network.
|
|
# Continue with the next one
|
|
continue
|
|
|
|
for interface_spec in v:
|
|
if ext_tag is not None:
|
|
if 'OS-EXT-IPS:type' not in interface_spec:
|
|
# ext_tag is specified, but this interface has no tag
|
|
# We could actually return right away as this means that
|
|
# this cloud doesn't support OS-EXT-IPS. Nevertheless,
|
|
# it would be better to perform an explicit check
|
|
# but this needs cloud to be passed to this function.
|
|
continue
|
|
elif interface_spec['OS-EXT-IPS:type'] != ext_tag:
|
|
# Type doesn't match, continue with next one
|
|
continue
|
|
|
|
if mac_addr is not None:
|
|
if 'OS-EXT-IPS-MAC:mac_addr' not in interface_spec:
|
|
# mac_addr is specified, but this interface has no mac_addr
|
|
# We could actually return right away as this means that
|
|
# this cloud doesn't support OS-EXT-IPS-MAC. Nevertheless,
|
|
# it would be better to perform an explicit check
|
|
# but this needs cloud to be passed to this function.
|
|
continue
|
|
elif interface_spec['OS-EXT-IPS-MAC:mac_addr'] != mac_addr:
|
|
# MAC doesn't match, continue with next one
|
|
continue
|
|
|
|
if interface_spec['version'] == version:
|
|
ret.append(interface_spec)
|
|
return ret
|
|
|
|
|
|
def find_nova_addresses(
|
|
addresses, ext_tag=None, key_name=None, version=4, mac_addr=None
|
|
):
|
|
interfaces = find_nova_interfaces(
|
|
addresses, ext_tag, key_name, version, mac_addr
|
|
)
|
|
floating_addrs = []
|
|
fixed_addrs = []
|
|
for i in interfaces:
|
|
if i.get('OS-EXT-IPS:type') == 'floating':
|
|
floating_addrs.append(i['addr'])
|
|
else:
|
|
fixed_addrs.append(i['addr'])
|
|
return floating_addrs + fixed_addrs
|
|
|
|
|
|
def get_server_ip(server, public=False, cloud_public=True, **kwargs):
|
|
"""Get an IP from the Nova addresses dict
|
|
|
|
:param server: The server to pull the address from
|
|
:param public: Whether the address we're looking for should be considered
|
|
'public' and therefore reachabiliity tests should be
|
|
used. (defaults to False)
|
|
:param cloud_public: Whether the cloud has been configured to use private
|
|
IPs from servers as the interface_ip. This inverts the
|
|
public reachability logic, as in this case it's the
|
|
private ip we expect shade to be able to reach
|
|
"""
|
|
addrs = find_nova_addresses(server['addresses'], **kwargs)
|
|
return find_best_address(addrs, public=public, cloud_public=cloud_public)
|
|
|
|
|
|
def get_server_private_ip(server, cloud=None):
|
|
"""Find the private IP address
|
|
|
|
If Neutron is available, search for a port on a network where
|
|
`router:external` is False and `shared` is False. This combination
|
|
indicates a private network with private IP addresses. This port should
|
|
have the private IP.
|
|
|
|
If Neutron is not available, or something goes wrong communicating with it,
|
|
as a fallback, try the list of addresses associated with the server dict,
|
|
looking for an IP type tagged as 'fixed' in the network named 'private'.
|
|
|
|
Last resort, ignore the IP type and just look for an IP on the 'private'
|
|
network (e.g., Rackspace).
|
|
"""
|
|
if cloud and not cloud.use_internal_network():
|
|
return None
|
|
|
|
# Try to get a floating IP interface. If we have one then return the
|
|
# private IP address associated with that floating IP for consistency.
|
|
fip_ints = find_nova_interfaces(server['addresses'], ext_tag='floating')
|
|
fip_mac = None
|
|
if fip_ints:
|
|
fip_mac = fip_ints[0].get('OS-EXT-IPS-MAC:mac_addr')
|
|
|
|
# Short circuit the ports/networks search below with a heavily cached
|
|
# and possibly pre-configured network name
|
|
if cloud:
|
|
int_nets = cloud.get_internal_ipv4_networks()
|
|
for int_net in int_nets:
|
|
int_ip = get_server_ip(
|
|
server,
|
|
key_name=int_net['name'],
|
|
ext_tag='fixed',
|
|
cloud_public=not cloud.private,
|
|
mac_addr=fip_mac,
|
|
)
|
|
if int_ip is not None:
|
|
return int_ip
|
|
# Try a second time without the fixed tag. This is for old nova-network
|
|
# results that do not have the fixed/floating tag.
|
|
for int_net in int_nets:
|
|
int_ip = get_server_ip(
|
|
server,
|
|
key_name=int_net['name'],
|
|
cloud_public=not cloud.private,
|
|
mac_addr=fip_mac,
|
|
)
|
|
if int_ip is not None:
|
|
return int_ip
|
|
|
|
ip = get_server_ip(
|
|
server, ext_tag='fixed', key_name='private', mac_addr=fip_mac
|
|
)
|
|
if ip:
|
|
return ip
|
|
|
|
# Last resort, and Rackspace
|
|
return get_server_ip(server, key_name='private')
|
|
|
|
|
|
def get_server_external_ipv4(cloud, server):
|
|
"""Find an externally routable IP for the server.
|
|
|
|
There are 5 different scenarios we have to account for:
|
|
|
|
* Cloud has externally routable IP from neutron but neutron APIs don't
|
|
work (only info available is in nova server record) (rackspace)
|
|
* Cloud has externally routable IP from neutron (runabove, ovh)
|
|
* Cloud has externally routable IP from neutron AND supports optional
|
|
private tenant networks (vexxhost, unitedstack)
|
|
* Cloud only has private tenant network provided by neutron and requires
|
|
floating-ip for external routing (dreamhost, hp)
|
|
* Cloud only has private tenant network provided by nova-network and
|
|
requires floating-ip for external routing (auro)
|
|
|
|
:param cloud: the cloud we're working with
|
|
:param server: the server dict from which we want to get an IPv4 address
|
|
:return: a string containing the IPv4 address or None
|
|
"""
|
|
|
|
if not cloud.use_external_network():
|
|
return None
|
|
|
|
if server['accessIPv4']:
|
|
return server['accessIPv4']
|
|
|
|
# Short circuit the ports/networks search below with a heavily cached
|
|
# and possibly pre-configured network name
|
|
ext_nets = cloud.get_external_ipv4_networks()
|
|
for ext_net in ext_nets:
|
|
ext_ip = get_server_ip(
|
|
server,
|
|
key_name=ext_net['name'],
|
|
public=True,
|
|
cloud_public=not cloud.private,
|
|
)
|
|
if ext_ip is not None:
|
|
return ext_ip
|
|
|
|
# Try to get a floating IP address
|
|
# Much as I might find floating IPs annoying, if it has one, that's
|
|
# almost certainly the one that wants to be used
|
|
ext_ip = get_server_ip(
|
|
server, ext_tag='floating', public=True, cloud_public=not cloud.private
|
|
)
|
|
if ext_ip is not None:
|
|
return ext_ip
|
|
|
|
# The cloud doesn't support Neutron or Neutron can't be contacted. The
|
|
# server might have fixed addresses that are reachable from outside the
|
|
# cloud (e.g. Rax) or have plain ol' floating IPs
|
|
|
|
# Try to get an address from a network named 'public'
|
|
ext_ip = get_server_ip(
|
|
server, key_name='public', public=True, cloud_public=not cloud.private
|
|
)
|
|
if ext_ip is not None:
|
|
return ext_ip
|
|
|
|
# Nothing else works, try to find a globally routable IP address
|
|
for interfaces in server['addresses'].values():
|
|
for interface in interfaces:
|
|
try:
|
|
ip = ipaddress.ip_address(interface['addr'])
|
|
except Exception:
|
|
# Skip any error, we're looking for a working ip - if the
|
|
# cloud returns garbage, it wouldn't be the first weird thing
|
|
# but it still doesn't meet the requirement of "be a working
|
|
# ip address"
|
|
continue
|
|
if ip.version == 4 and not ip.is_private:
|
|
return str(ip)
|
|
|
|
return None
|
|
|
|
|
|
def find_best_address(addresses, public=False, cloud_public=True):
|
|
do_check = public == cloud_public
|
|
if not addresses:
|
|
return None
|
|
if len(addresses) == 1:
|
|
return addresses[0]
|
|
if len(addresses) > 1 and do_check:
|
|
# We only want to do this check if the address is supposed to be
|
|
# reachable. Otherwise we're just debug log spamming on every listing
|
|
# of private ip addresses
|
|
for address in addresses:
|
|
try:
|
|
for count in utils.iterate_timeout(
|
|
5, "Timeout waiting for %s" % address, wait=0.1
|
|
):
|
|
# Return the first one that is reachable
|
|
try:
|
|
for res in socket.getaddrinfo(
|
|
address,
|
|
22,
|
|
socket.AF_UNSPEC,
|
|
socket.SOCK_STREAM,
|
|
0,
|
|
):
|
|
family, socktype, proto, _, sa = res
|
|
connect_socket = socket.socket(
|
|
family, socktype, proto
|
|
)
|
|
connect_socket.settimeout(1)
|
|
connect_socket.connect(sa)
|
|
return address
|
|
except OSError:
|
|
# Sometimes a "no route to address" type error
|
|
# will fail fast, but can often come alive
|
|
# when retried.
|
|
continue
|
|
except Exception:
|
|
pass
|
|
|
|
# Give up and return the first - none work as far as we can tell
|
|
if do_check:
|
|
log = _log.setup_logging('openstack')
|
|
log.debug(
|
|
"The cloud returned multiple addresses %s:, and we could not "
|
|
"connect to port 22 on either. That might be what you wanted, "
|
|
"but we have no clue what's going on, so we picked the first one "
|
|
"%s" % (addresses, addresses[0])
|
|
)
|
|
return addresses[0]
|
|
|
|
|
|
def get_server_external_ipv6(server):
|
|
"""Get an IPv6 address reachable from outside the cloud.
|
|
|
|
This function assumes that if a server has an IPv6 address, that address
|
|
is reachable from outside the cloud.
|
|
|
|
:param server: the server from which we want to get an IPv6 address
|
|
:return: a string containing the IPv6 address or None
|
|
"""
|
|
# Don't return ipv6 interfaces if forcing IPv4
|
|
if server['accessIPv6']:
|
|
return server['accessIPv6']
|
|
addresses = find_nova_addresses(addresses=server['addresses'], version=6)
|
|
return find_best_address(addresses, public=True)
|
|
|
|
|
|
def get_server_default_ip(cloud, server):
|
|
"""Get the configured 'default' address
|
|
|
|
It is possible in clouds.yaml to configure for a cloud a network that
|
|
is the 'default_interface'. This is the network that should be used
|
|
to talk to instances on the network.
|
|
|
|
:param cloud: the cloud we're working with
|
|
:param server: the server dict from which we want to get the default
|
|
IPv4 address
|
|
:return: a string containing the IPv4 address or None
|
|
"""
|
|
ext_net = cloud.get_default_network()
|
|
if ext_net:
|
|
if cloud._local_ipv6 and not cloud.force_ipv4:
|
|
# try 6 first, fall back to four
|
|
versions = [6, 4]
|
|
else:
|
|
versions = [4]
|
|
for version in versions:
|
|
ext_ip = get_server_ip(
|
|
server,
|
|
key_name=ext_net['name'],
|
|
version=version,
|
|
public=True,
|
|
cloud_public=not cloud.private,
|
|
)
|
|
if ext_ip is not None:
|
|
return ext_ip
|
|
return None
|
|
|
|
|
|
def _get_interface_ip(cloud, server):
|
|
"""Get the interface IP for the server
|
|
|
|
Interface IP is the IP that should be used for communicating with the
|
|
server. It is:
|
|
- the IP on the configured default_interface network
|
|
- if cloud.private, the private ip if it exists
|
|
- if the server has a public ip, the public ip
|
|
"""
|
|
default_ip = get_server_default_ip(cloud, server)
|
|
if default_ip:
|
|
return default_ip
|
|
|
|
if cloud.private and server['private_v4']:
|
|
return server['private_v4']
|
|
|
|
if server['public_v6'] and cloud._local_ipv6 and not cloud.force_ipv4:
|
|
return server['public_v6']
|
|
else:
|
|
return server['public_v4']
|
|
|
|
|
|
def get_groups_from_server(cloud, server, server_vars):
|
|
groups = []
|
|
|
|
# NOTE(efried): This is hardcoded to 'compute' because this method is only
|
|
# used from ComputeCloudMixin.
|
|
region = cloud.config.get_region_name('compute')
|
|
cloud_name = cloud.name
|
|
|
|
# Create a group for the cloud
|
|
groups.append(cloud_name)
|
|
|
|
# Create a group on region
|
|
groups.append(region)
|
|
|
|
# And one by cloud_region
|
|
groups.append(f"{cloud_name}_{region}")
|
|
|
|
# Check if group metadata key in servers' metadata
|
|
group = server['metadata'].get('group')
|
|
if group:
|
|
groups.append(group)
|
|
|
|
for extra_group in server['metadata'].get('groups', '').split(','):
|
|
if extra_group:
|
|
groups.append(extra_group)
|
|
|
|
groups.append('instance-%s' % server['id'])
|
|
|
|
for key in ('flavor', 'image'):
|
|
if 'name' in server_vars[key]:
|
|
groups.append('{}-{}'.format(key, server_vars[key]['name']))
|
|
|
|
for key, value in iter(server['metadata'].items()):
|
|
groups.append(f'meta-{key}_{value}')
|
|
|
|
az = server_vars.get('az', None)
|
|
if az:
|
|
# Make groups for az, region_az and cloud_region_az
|
|
groups.append(az)
|
|
groups.append(f'{region}_{az}')
|
|
groups.append(f'{cloud.name}_{region}_{az}')
|
|
return groups
|
|
|
|
|
|
def expand_server_vars(cloud, server):
|
|
"""Backwards compatibility function."""
|
|
return add_server_interfaces(cloud, server)
|
|
|
|
|
|
def _make_address_dict(fip, port):
|
|
address = dict(version=4, addr=fip['floating_ip_address'])
|
|
address['OS-EXT-IPS:type'] = 'floating'
|
|
address['OS-EXT-IPS-MAC:mac_addr'] = port['mac_address']
|
|
return address
|
|
|
|
|
|
def _get_supplemental_addresses(cloud, server):
|
|
fixed_ip_mapping = {}
|
|
for name, network in server['addresses'].items():
|
|
for address in network:
|
|
if address['version'] == 6:
|
|
continue
|
|
if address.get('OS-EXT-IPS:type') == 'floating':
|
|
# We have a floating IP that nova knows about, do nothing
|
|
return server['addresses']
|
|
fixed_ip_mapping[address['addr']] = name
|
|
try:
|
|
# Don't bother doing this before the server is active, it's a waste
|
|
# of an API call while polling for a server to come up
|
|
if (
|
|
cloud.has_service('network')
|
|
and cloud._has_floating_ips()
|
|
and server['status'] == 'ACTIVE'
|
|
):
|
|
for port in cloud.search_ports(
|
|
filters=dict(device_id=server['id'])
|
|
):
|
|
# This SHOULD return one and only one FIP - but doing it as a
|
|
# search/list lets the logic work regardless
|
|
for fip in cloud.search_floating_ips(
|
|
filters=dict(port_id=port['id'])
|
|
):
|
|
fixed_net = fixed_ip_mapping.get(fip['fixed_ip_address'])
|
|
if fixed_net is None:
|
|
log = _log.setup_logging('openstack')
|
|
log.debug(
|
|
"The cloud returned floating ip %(fip)s attached"
|
|
" to server %(server)s but the fixed ip associated"
|
|
" with the floating ip in the neutron listing"
|
|
" does not exist in the nova listing. Something"
|
|
" is exceptionally broken.",
|
|
dict(fip=fip['id'], server=server['id']),
|
|
)
|
|
else:
|
|
server['addresses'][fixed_net].append(
|
|
_make_address_dict(fip, port)
|
|
)
|
|
except exceptions.SDKException:
|
|
# If something goes wrong with a cloud call, that's cool - this is
|
|
# an attempt to provide additional data and should not block forward
|
|
# progress
|
|
pass
|
|
return server['addresses']
|
|
|
|
|
|
def add_server_interfaces(cloud, server):
|
|
"""Add network interface information to server.
|
|
|
|
Query the cloud as necessary to add information to the server record
|
|
about the network information needed to interface with the server.
|
|
|
|
Ensures that public_v4, public_v6, private_v4, private_v6, interface_ip,
|
|
accessIPv4 and accessIPv6 are always set.
|
|
"""
|
|
# First, add an IP address. Set it to '' rather than None if it does
|
|
# not exist to remain consistent with the pre-existing missing values
|
|
server['addresses'] = _get_supplemental_addresses(cloud, server)
|
|
server['public_v4'] = get_server_external_ipv4(cloud, server) or ''
|
|
# If we're forcing IPv4, then don't report IPv6 interfaces which
|
|
# are likely to be unconfigured.
|
|
if cloud.force_ipv4:
|
|
server['public_v6'] = ''
|
|
else:
|
|
server['public_v6'] = get_server_external_ipv6(server) or ''
|
|
server['private_v4'] = get_server_private_ip(server, cloud) or ''
|
|
server['interface_ip'] = _get_interface_ip(cloud, server) or ''
|
|
|
|
# Some clouds do not set these, but they're a regular part of the Nova
|
|
# server record. Since we know them, go ahead and set them. In the case
|
|
# where they were set previous, we use the values, so this will not break
|
|
# clouds that provide the information
|
|
if cloud.private and server.private_v4:
|
|
server['access_ipv4'] = server['private_v4']
|
|
else:
|
|
server['access_ipv4'] = server['public_v4']
|
|
server['access_ipv6'] = server['public_v6']
|
|
|
|
return server
|
|
|
|
|
|
def expand_server_security_groups(cloud, server):
|
|
try:
|
|
groups = cloud.list_server_security_groups(server)
|
|
except exceptions.SDKException:
|
|
groups = []
|
|
server['security_groups'] = groups or []
|
|
|
|
|
|
def get_hostvars_from_server(cloud, server, mounts=None):
|
|
"""Expand additional server information useful for ansible inventory.
|
|
|
|
Variables in this function may make additional cloud queries to flesh out
|
|
possibly interesting info, making it more expensive to call than
|
|
expand_server_vars if caching is not set up. If caching is set up,
|
|
the extra cost should be minimal.
|
|
"""
|
|
server_vars = obj_to_munch(add_server_interfaces(cloud, server))
|
|
|
|
flavor_id = server['flavor'].get('id')
|
|
if flavor_id:
|
|
# In newer nova, the flavor record can be kept around for flavors
|
|
# that no longer exist. The id and name are not there.
|
|
flavor_name = cloud.get_flavor_name(flavor_id)
|
|
if flavor_name:
|
|
server_vars['flavor']['name'] = flavor_name
|
|
elif 'original_name' in server['flavor']:
|
|
# Users might be have code still expecting name. That name is in
|
|
# original_name.
|
|
server_vars['flavor']['name'] = server['flavor']['original_name']
|
|
|
|
expand_server_security_groups(cloud, server)
|
|
|
|
# OpenStack can return image as a string when you've booted from volume
|
|
if str(server['image']) == server['image']:
|
|
image_id = server['image']
|
|
server_vars['image'] = dict(id=image_id)
|
|
else:
|
|
image_id = server['image'].get('id', None)
|
|
if image_id:
|
|
image_name = cloud.get_image_name(image_id)
|
|
if image_name:
|
|
server_vars['image']['name'] = image_name
|
|
|
|
# During the switch to returning sdk resource objects we need temporarily
|
|
# to force convertion to dict. This will be dropped soon.
|
|
if hasattr(server_vars['image'], 'to_dict'):
|
|
server_vars['image'] = server_vars['image'].to_dict(computed=False)
|
|
|
|
volumes = []
|
|
if cloud.has_service('volume'):
|
|
try:
|
|
for volume in cloud.get_volumes(server):
|
|
# Make things easier to consume elsewhere
|
|
volume['device'] = volume['attachments'][0]['device']
|
|
volumes.append(volume)
|
|
except exceptions.SDKException:
|
|
pass
|
|
server_vars['volumes'] = volumes
|
|
if mounts:
|
|
for mount in mounts:
|
|
for vol in server_vars['volumes']:
|
|
if vol['display_name'] == mount['display_name']:
|
|
if 'mount' in mount:
|
|
vol['mount'] = mount['mount']
|
|
|
|
return server_vars
|
|
|
|
|
|
def obj_to_munch(obj):
|
|
"""Turn an object with attributes into a dict suitable for serializing.
|
|
|
|
Some of the things that are returned in OpenStack are objects with
|
|
attributes. That's awesome - except when you want to expose them as JSON
|
|
structures. We use this as the basis of get_hostvars_from_server above so
|
|
that we can just have a plain dict of all of the values that exist in the
|
|
nova metadata for a server.
|
|
"""
|
|
if obj is None:
|
|
return None
|
|
elif isinstance(obj, utils.Munch) or hasattr(obj, 'mock_add_spec'):
|
|
# If we obj_to_munch twice, don't fail, just return the munch
|
|
# Also, don't try to modify Mock objects - that way lies madness
|
|
return obj
|
|
elif isinstance(obj, dict):
|
|
# The new request-id tracking spec:
|
|
# https://specs.openstack.org/openstack/nova-specs/specs/juno/approved/log-request-id-mappings.html
|
|
# adds a request-ids attribute to returned objects. It does this even
|
|
# with dicts, which now become dict subclasses. So we want to convert
|
|
# the dict we get, but we also want it to fall through to object
|
|
# attribute processing so that we can also get the request_ids
|
|
# data into our resulting object.
|
|
instance = utils.Munch(obj)
|
|
else:
|
|
instance = utils.Munch()
|
|
|
|
for key in dir(obj):
|
|
try:
|
|
value = getattr(obj, key)
|
|
# some attributes can be defined as a @property, so we can't assure
|
|
# to have a valid value
|
|
# e.g. id in python-novaclient/tree/novaclient/v2/quotas.py
|
|
except AttributeError:
|
|
continue
|
|
if isinstance(value, NON_CALLABLES) and not key.startswith('_'):
|
|
instance[key] = value
|
|
return instance
|
|
|
|
|
|
obj_to_dict = obj_to_munch
|
|
|
|
|
|
def obj_list_to_munch(obj_list):
|
|
"""Enumerate through lists of objects and return lists of dictonaries.
|
|
|
|
Some of the objects returned in OpenStack are actually lists of objects,
|
|
and in order to expose the data structures as JSON, we need to facilitate
|
|
the conversion to lists of dictonaries.
|
|
"""
|
|
return [obj_to_munch(obj) for obj in obj_list]
|
|
|
|
|
|
obj_list_to_dict = obj_list_to_munch
|
|
|
|
|
|
def get_and_munchify(key, data):
|
|
"""Get the value associated to key and convert it.
|
|
|
|
The value will be converted in a Munch object or a list of Munch objects
|
|
based on the type
|
|
"""
|
|
result = data.get(key, []) if key else data
|
|
if isinstance(result, list):
|
|
return obj_list_to_munch(result)
|
|
elif isinstance(result, dict):
|
|
return obj_to_munch(result)
|
|
return result
|