44f22cf2a5
Replace the following items with Python 3 style code. - six.PY2 - six.text_type - six.string_type - six.binary_type - six.iterkeys - six.moves - six.itervalues Implements: blueprint six-removal Change-Id: I6195ceeeed2ebe1586973eaeef7fa9f29698fec2
311 lines
12 KiB
Python
311 lines
12 KiB
Python
# Copyright 2014
|
|
# The Cloudscaling Group, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import base64
|
|
import itertools
|
|
|
|
from novaclient import exceptions as nova_exception
|
|
from oslo_cache import core as cache_core
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
|
|
from ec2api.api import clients
|
|
from ec2api.api import ec2utils
|
|
from ec2api.api import instance as instance_api
|
|
from ec2api import exception
|
|
|
|
|
|
CONF = cfg.CONF
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
VERSIONS = [
|
|
'1.0',
|
|
'2007-01-19',
|
|
'2007-03-01',
|
|
'2007-08-29',
|
|
'2007-10-10',
|
|
'2007-12-15',
|
|
'2008-02-01',
|
|
'2008-09-01',
|
|
'2009-04-04',
|
|
]
|
|
|
|
VERSION_DATA = {
|
|
'1.0': ['ami-id',
|
|
'ami-launch-index',
|
|
'ami-manifest-path',
|
|
'hostname',
|
|
'instance-id',
|
|
'local-ipv4',
|
|
'public-keys',
|
|
'reservation-id',
|
|
'security-groups'],
|
|
'2007-01-19': ['local-hostname',
|
|
'public-hostname',
|
|
'public-ipv4'],
|
|
'2007-03-01': ['product-codes'],
|
|
'2007-08-29': ['instance-type'],
|
|
'2007-10-10': ['ancestor-ami-ids',
|
|
'ramdisk-id'],
|
|
'2007-12-15': ['block-device-mapping'],
|
|
'2008-02-01': ['kernel-id',
|
|
'placement'],
|
|
'2008-09-01': ['instance-action'],
|
|
'2009-04-04': [],
|
|
}
|
|
|
|
|
|
def get_version_list():
|
|
return _format_metadata_item(VERSIONS + ["latest"])
|
|
|
|
|
|
def get_os_instance_and_project_id_by_provider_id(context, provider_id,
|
|
fixed_ip):
|
|
neutron = clients.neutron(context)
|
|
os_subnets = neutron.list_subnets(advanced_service_providers=[provider_id],
|
|
fields=['network_id'])
|
|
if not os_subnets:
|
|
raise exception.EC2MetadataNotFound()
|
|
os_networks = [subnet['network_id']
|
|
for subnet in os_subnets['subnets']]
|
|
try:
|
|
os_port = neutron.list_ports(
|
|
fixed_ips='ip_address=' + fixed_ip,
|
|
network_id=os_networks,
|
|
fields=['device_id', 'tenant_id'])['ports'][0]
|
|
except IndexError:
|
|
raise exception.EC2MetadataNotFound()
|
|
os_instance_id = os_port['device_id']
|
|
project_id = os_port['tenant_id']
|
|
return os_instance_id, project_id
|
|
|
|
|
|
def get_metadata_item(context, path_tokens, os_instance_id, remote_ip,
|
|
cache_region):
|
|
version = path_tokens[0]
|
|
if version == "latest":
|
|
version = VERSIONS[-1]
|
|
elif version not in VERSIONS:
|
|
raise exception.EC2MetadataNotFound()
|
|
|
|
cache_key = 'ec2api-metadata-%s' % os_instance_id
|
|
cache = cache_region.get(
|
|
cache_key, expiration_time=CONF.metadata.cache_expiration)
|
|
if cache and cache != cache_core.NO_VALUE:
|
|
_check_instance_owner(context, os_instance_id, cache['owner_id'])
|
|
LOG.debug("Using cached metadata for instance %s", os_instance_id)
|
|
else:
|
|
ec2_instance, ec2_reservation = (
|
|
_get_ec2_instance_and_reservation(context, os_instance_id))
|
|
|
|
_check_instance_owner(context, os_instance_id,
|
|
ec2_reservation['ownerId'])
|
|
|
|
metadata = _build_metadata(context, ec2_instance, ec2_reservation,
|
|
os_instance_id, remote_ip)
|
|
LOG.debug('get_metadata_item: result %s', str(metadata))
|
|
cache = {'metadata': metadata,
|
|
'owner_id': ec2_reservation['ownerId']}
|
|
|
|
cache_region.set(cache_key, cache)
|
|
|
|
metadata = cache['metadata']
|
|
metadata = _cut_down_to_version(metadata, version)
|
|
metadata_item = _find_path_in_tree(metadata, path_tokens[1:])
|
|
return _format_metadata_item(metadata_item)
|
|
|
|
|
|
def _get_ec2_instance_and_reservation(context, os_instance_id):
|
|
instance_id = ec2utils.os_id_to_ec2_id(context, 'i', os_instance_id)
|
|
LOG.debug('_get_ec2_instance_and_reservation(%s)', os_instance_id)
|
|
try:
|
|
ec2_reservations = instance_api.describe_instances(
|
|
context, [instance_id])
|
|
LOG.debug('_get_ec2_instance_and_reservation: result by id %s',
|
|
str(ec2_reservations))
|
|
except exception.InvalidInstanceIDNotFound:
|
|
ec2_reservations = instance_api.describe_instances(
|
|
context, filter=[{'name': 'instance-id',
|
|
'value': [instance_id]}])
|
|
LOG.debug('_get_ec2_instance_and_reservation: result by name %s',
|
|
str(ec2_reservations))
|
|
if (len(ec2_reservations['reservationSet']) != 1 or
|
|
len(ec2_reservations['reservationSet'][0]['instancesSet']) != 1):
|
|
LOG.error('Failed to get metadata for instance id: %s',
|
|
os_instance_id)
|
|
raise exception.EC2MetadataNotFound()
|
|
|
|
ec2_reservation = ec2_reservations['reservationSet'][0]
|
|
ec2_instance = ec2_reservation['instancesSet'][0]
|
|
|
|
return ec2_instance, ec2_reservation
|
|
|
|
|
|
def _check_instance_owner(context, os_instance_id, owner_id):
|
|
# NOTE(ft): check for case of Neutron metadata proxy.
|
|
# It sends project_id as X-Tenant-ID HTTP header.
|
|
# We make sure it's correct
|
|
if context.project_id != owner_id:
|
|
LOG.warning('Tenant_id %(tenant_id)s does not match tenant_id '
|
|
'of instance %(instance_id)s.',
|
|
{'tenant_id': context.project_id,
|
|
'instance_id': os_instance_id})
|
|
raise exception.EC2MetadataNotFound()
|
|
|
|
|
|
def _build_metadata(context, ec2_instance, ec2_reservation,
|
|
os_instance_id, remote_ip):
|
|
metadata = {
|
|
'ami-id': ec2_instance['imageId'],
|
|
'ami-launch-index': ec2_instance['amiLaunchIndex'],
|
|
# NOTE (ft): the fake value as it is in Nova EC2 metadata
|
|
'ami-manifest-path': 'FIXME',
|
|
# NOTE (ft): empty value as it is in Nova EC2 metadata
|
|
'ancestor-ami-ids': [],
|
|
'block-device-mapping': _build_block_device_mappings(context,
|
|
ec2_instance,
|
|
os_instance_id),
|
|
# NOTE(ft): Nova EC2 metadata returns instance's hostname with
|
|
# dhcp_domain suffix if it's set in config.
|
|
# But i don't see any reason to return a hostname differs from EC2
|
|
# describe output one. If we need to consider dhcp_domain suffix
|
|
# then we should do it in the describe operation
|
|
'hostname': ec2_instance['privateDnsName'],
|
|
# NOTE (ft): the fake value as it is in Nova EC2 metadata
|
|
'instance-action': 'none',
|
|
'instance-id': ec2_instance['instanceId'],
|
|
'instance-type': ec2_instance['instanceType'],
|
|
'local-hostname': ec2_instance['privateDnsName'],
|
|
'local-ipv4': ec2_instance['privateIpAddress'] or remote_ip,
|
|
'placement': {
|
|
'availability-zone': ec2_instance['placement']['availabilityZone']
|
|
},
|
|
# NOTE (ft): empty value as it is in Nova EC2 metadata
|
|
'product-codes': [],
|
|
'public-hostname': ec2_instance['dnsName'],
|
|
'public-ipv4': ec2_instance.get('ipAddress', ''),
|
|
'reservation-id': ec2_reservation['reservationId'],
|
|
'security-groups': [sg['groupName']
|
|
for sg in ec2_reservation.get('groupSet', [])],
|
|
}
|
|
if 'kernelId' in ec2_instance:
|
|
metadata['kernel-id'] = ec2_instance['kernelId']
|
|
if 'ramdiskId' in ec2_instance:
|
|
metadata['ramdisk-id'] = ec2_instance['ramdiskId']
|
|
# public keys are strangely rendered in ec2 metadata service
|
|
# meta-data/public-keys/ returns '0=keyname' (with no trailing /)
|
|
# and only if there is a public key given.
|
|
# '0=keyname' means there is a normally rendered dict at
|
|
# meta-data/public-keys/0
|
|
#
|
|
# meta-data/public-keys/ : '0=%s' % keyname
|
|
# meta-data/public-keys/0/ : 'openssh-key'
|
|
# meta-data/public-keys/0/openssh-key : '%s' % publickey
|
|
if ec2_instance['keyName']:
|
|
metadata['public-keys'] = {
|
|
'0': {'_name': "0=" + ec2_instance['keyName']}}
|
|
nova = clients.nova(context)
|
|
os_instance = nova.servers.get(os_instance_id)
|
|
try:
|
|
keypair = nova.keypairs._get(
|
|
'/%s/%s?user_id=%s' % (nova.keypairs.keypair_prefix,
|
|
ec2_instance['keyName'],
|
|
os_instance.user_id),
|
|
'keypair')
|
|
except nova_exception.NotFound:
|
|
pass
|
|
else:
|
|
metadata['public-keys']['0']['openssh-key'] = keypair.public_key
|
|
|
|
full_metadata = {'meta-data': metadata}
|
|
|
|
userdata = instance_api.describe_instance_attribute(
|
|
context, ec2_instance['instanceId'], 'userData')
|
|
if 'userData' in userdata:
|
|
userdata = userdata['userData']['value']
|
|
userdata = base64.b64decode(userdata)
|
|
try:
|
|
userdata = userdata.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
pass
|
|
full_metadata['user-data'] = userdata
|
|
|
|
return full_metadata
|
|
|
|
|
|
def _build_block_device_mappings(context, ec2_instance, os_instance_id):
|
|
mappings = {'root': ec2_instance.get('rootDeviceName', ''),
|
|
'ami': ec2utils.block_device_strip_dev(
|
|
ec2_instance.get('rootDeviceName', ''))}
|
|
if 'blockDeviceMapping' in ec2_instance:
|
|
# NOTE(yamahata): I'm not sure how ebs device should be numbered.
|
|
# Right now sort by device name for deterministic
|
|
# result.
|
|
ebs_devices = [ebs['deviceName']
|
|
for ebs in ec2_instance['blockDeviceMapping']]
|
|
ebs_devices.sort()
|
|
ebs_devices = {'ebs%d' % num: ebs
|
|
for num, ebs in enumerate(ebs_devices)}
|
|
mappings.update(ebs_devices)
|
|
|
|
# TODO(ft): extend Nova API to get ephemerals and swap
|
|
return mappings
|
|
|
|
|
|
def _cut_down_to_version(metadata, version):
|
|
version_number = VERSIONS.index(version) + 1
|
|
if version_number == len(VERSIONS):
|
|
return metadata
|
|
return {attr: metadata[attr]
|
|
for attr in itertools.chain(
|
|
*(VERSION_DATA[ver] for ver in VERSIONS[:version_number]))
|
|
if attr in metadata}
|
|
|
|
|
|
def _format_metadata_item(data):
|
|
if isinstance(data, dict):
|
|
output = ''
|
|
for key in sorted(data.keys()):
|
|
if key == '_name':
|
|
continue
|
|
if isinstance(data[key], dict):
|
|
if '_name' in data[key]:
|
|
output += str(data[key]['_name'])
|
|
else:
|
|
output += key + '/'
|
|
else:
|
|
output += key
|
|
|
|
output += '\n'
|
|
return output[:-1]
|
|
elif isinstance(data, list):
|
|
return '\n'.join(data)
|
|
else:
|
|
return str(data)
|
|
|
|
|
|
def _find_path_in_tree(data, path_tokens):
|
|
# given a dict/list tree, and a path in that tree, return data found there.
|
|
for i in range(0, len(path_tokens)):
|
|
if isinstance(data, dict) or isinstance(data, list):
|
|
if path_tokens[i] in data:
|
|
data = data[path_tokens[i]]
|
|
else:
|
|
raise exception.EC2MetadataNotFound()
|
|
else:
|
|
if i != len(path_tokens) - 1:
|
|
raise exception.EC2MetadataNotFound()
|
|
data = data[path_tokens[i]]
|
|
return data
|