ec2-api/ec2api/metadata/api.py

312 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
import six
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 six.text_type(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