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
561 lines
19 KiB
Python
561 lines
19 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 datetime
|
|
import re
|
|
|
|
from glanceclient.common import exceptions as glance_exception
|
|
from lxml import etree
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import timeutils
|
|
|
|
from ec2api import clients
|
|
from ec2api.db import api as db_api
|
|
from ec2api import exception
|
|
from ec2api.i18n import _
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
ec2_opts = [
|
|
cfg.StrOpt('external_network',
|
|
default=None,
|
|
help='Name of the external network, which is used to connect'
|
|
'VPCs to Internet and to allocate Elastic IPs.'),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(ec2_opts)
|
|
|
|
LEGACY_BDM_FIELDS = set(['device_name', 'delete_on_termination', 'snapshot_id',
|
|
'volume_id', 'volume_size', 'no_device'])
|
|
|
|
_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
|
|
|
|
|
|
def camelcase_to_underscore(str):
|
|
return _c2u.sub(r'_\1', str).lower().strip('_')
|
|
|
|
|
|
def _try_convert(value):
|
|
"""Return a non-string from a string or unicode, if possible.
|
|
|
|
============= =====================================================
|
|
When value is returns
|
|
============= =====================================================
|
|
zero-length ''
|
|
'None' None
|
|
'True' True case insensitive
|
|
'False' False case insensitive
|
|
'0', '-0' 0
|
|
0xN, -0xN int from hex (positive) (N is any number)
|
|
0bN, -0bN int from binary (positive) (N is any number)
|
|
* try conversion to int, float, complex, fallback value
|
|
|
|
"""
|
|
def _negative_zero(value):
|
|
epsilon = 1e-7
|
|
return 0 if abs(value) < epsilon else value
|
|
|
|
if len(value) == 0:
|
|
return ''
|
|
if value == 'None':
|
|
return None
|
|
lowered_value = value.lower()
|
|
if lowered_value == 'true':
|
|
return True
|
|
if lowered_value == 'false':
|
|
return False
|
|
for prefix, base in [('0x', 16), ('0b', 2), ('0', 8), ('', 10)]:
|
|
try:
|
|
if lowered_value.startswith((prefix, "-" + prefix)):
|
|
return int(lowered_value, base)
|
|
except ValueError:
|
|
pass
|
|
try:
|
|
return _negative_zero(float(value))
|
|
except ValueError:
|
|
return value
|
|
|
|
|
|
def dict_from_dotted_str(items):
|
|
"""parse multi dot-separated argument into dict.
|
|
|
|
EBS boot uses multi dot-separated arguments like
|
|
BlockDeviceMapping.1.DeviceName=snap-id
|
|
Convert the above into
|
|
{'block_device_mapping': {'1': {'device_name': snap-id}}}
|
|
"""
|
|
args = {}
|
|
for key, value in items:
|
|
parts = key.split(".")
|
|
key = str(camelcase_to_underscore(parts[0]))
|
|
if isinstance(value, str):
|
|
# NOTE(vish): Automatically convert strings back
|
|
# into their respective values
|
|
value = _try_convert(value)
|
|
|
|
if len(parts) > 1:
|
|
d = args.get(key, {})
|
|
args[key] = d
|
|
for k in parts[1:-1]:
|
|
k = camelcase_to_underscore(k)
|
|
v = d.get(k, {})
|
|
d[k] = v
|
|
d = v
|
|
d[camelcase_to_underscore(parts[-1])] = value
|
|
else:
|
|
args[key] = value
|
|
|
|
return args
|
|
|
|
|
|
def _render_dict(el, data):
|
|
try:
|
|
for key, val in data.items():
|
|
sub_el = etree.SubElement(el, key)
|
|
_render_data(sub_el, val)
|
|
except Exception:
|
|
LOG.debug(data)
|
|
raise
|
|
|
|
|
|
def _render_data(el, data):
|
|
if isinstance(data, list):
|
|
for item in data:
|
|
sub_el = etree.SubElement(el, 'item')
|
|
_render_data(sub_el, item)
|
|
elif isinstance(data, dict):
|
|
_render_dict(el, data)
|
|
elif hasattr(data, '__dict__'):
|
|
_render_dict(el, data.__dict__)
|
|
elif isinstance(data, bool):
|
|
el.text = str(data).lower()
|
|
elif isinstance(data, datetime.datetime):
|
|
el.text = _database_to_isoformat(data)
|
|
elif isinstance(data, bytes):
|
|
el.text = data.decode("utf-8")
|
|
elif data is not None:
|
|
el.text = str(data)
|
|
|
|
|
|
def _database_to_isoformat(datetimeobj):
|
|
"""Return a xs:dateTime parsable string from datatime."""
|
|
return datetimeobj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + 'Z'
|
|
|
|
|
|
def dict_to_xml(data_dict, root_tag):
|
|
root = etree.Element(root_tag)
|
|
_render_dict(root, data_dict)
|
|
return root
|
|
|
|
|
|
_ms_time_regex = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3,6}Z$')
|
|
|
|
|
|
def is_ec2_timestamp_expired(request, expires=None):
|
|
"""Checks the timestamp or expiry time included in an EC2 request
|
|
|
|
and returns true if the request is expired
|
|
"""
|
|
query_time = None
|
|
timestamp = request.get('Timestamp')
|
|
expiry_time = request.get('Expires')
|
|
|
|
def parse_strtime(strtime):
|
|
if _ms_time_regex.match(strtime):
|
|
# NOTE(MotoKen): time format for aws-sdk-java contains millisecond
|
|
time_format = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
else:
|
|
time_format = "%Y-%m-%dT%H:%M:%SZ"
|
|
return timeutils.parse_strtime(strtime, time_format)
|
|
|
|
try:
|
|
if timestamp and expiry_time:
|
|
msg = _("Request must include either Timestamp or Expires,"
|
|
" but cannot contain both")
|
|
LOG.error(msg)
|
|
raise exception.InvalidRequest(msg)
|
|
elif expiry_time:
|
|
query_time = parse_strtime(expiry_time)
|
|
return timeutils.is_older_than(query_time, -1)
|
|
elif timestamp:
|
|
query_time = parse_strtime(timestamp)
|
|
|
|
# Check if the difference between the timestamp in the request
|
|
# and the time on our servers is larger than 5 minutes, the
|
|
# request is too old (or too new).
|
|
if query_time and expires:
|
|
return (timeutils.is_older_than(query_time, expires) or
|
|
timeutils.is_newer_than(query_time, expires))
|
|
return False
|
|
except ValueError:
|
|
LOG.exception("Timestamp is invalid: ")
|
|
return True
|
|
|
|
|
|
# NOTE(ft): extra functions to use in vpc specific code or instead of
|
|
# malformed existed functions
|
|
|
|
|
|
def get_ec2_id_kind(obj_id):
|
|
return obj_id.split('-')[0]
|
|
|
|
|
|
def change_ec2_id_kind(obj_id, new_kind):
|
|
return '%(kind)s-%(id)s' % {'kind': new_kind,
|
|
'id': obj_id.split('-')[-1]}
|
|
|
|
|
|
NOT_FOUND_EXCEPTION_MAP = {
|
|
'vpc': exception.InvalidVpcIDNotFound,
|
|
'igw': exception.InvalidInternetGatewayIDNotFound,
|
|
'subnet': exception.InvalidSubnetIDNotFound,
|
|
'eni': exception.InvalidNetworkInterfaceIDNotFound,
|
|
'dopt': exception.InvalidDhcpOptionsIDNotFound,
|
|
'eipalloc': exception.InvalidAllocationIDNotFound,
|
|
'sg': exception.InvalidGroupNotFound,
|
|
'rtb': exception.InvalidRouteTableIDNotFound,
|
|
'i': exception.InvalidInstanceIDNotFound,
|
|
'kp': exception.InvalidKeypairNotFound,
|
|
'az': exception.InvalidAvailabilityZoneNotFound,
|
|
'vol': exception.InvalidVolumeNotFound,
|
|
'snap': exception.InvalidSnapshotNotFound,
|
|
'ami': exception.InvalidAMIIDNotFound,
|
|
'aki': exception.InvalidAMIIDNotFound,
|
|
'ari': exception.InvalidAMIIDNotFound,
|
|
'vgw': exception.InvalidVpnGatewayIDNotFound,
|
|
'cgw': exception.InvalidCustomerGatewayIDNotFound,
|
|
'vpn': exception.InvalidVpnConnectionIDNotFound,
|
|
}
|
|
|
|
|
|
def get_db_item(context, ec2_id, expected_kind=None):
|
|
"""Get an DB item, raise AWS compliant exception if it's not found.
|
|
|
|
Args:
|
|
context (RequestContext): The request context.
|
|
ec2_id (str): The ID of the requested item.
|
|
expected_kind (str): The expected kind of the requested item.
|
|
It should be specified for a kind of ec2_id to be validated,
|
|
if you need it.
|
|
|
|
Returns:
|
|
The DB item.
|
|
"""
|
|
item = db_api.get_item_by_id(context, ec2_id)
|
|
if (item is None or
|
|
expected_kind and get_ec2_id_kind(ec2_id) != expected_kind):
|
|
kind = expected_kind or get_ec2_id_kind(ec2_id)
|
|
params = {'id': ec2_id}
|
|
raise NOT_FOUND_EXCEPTION_MAP[kind](**params)
|
|
return item
|
|
|
|
|
|
def get_db_items(context, kind, ec2_ids):
|
|
if not ec2_ids:
|
|
return db_api.get_items(context, kind)
|
|
|
|
if not isinstance(ec2_ids, set):
|
|
ec2_ids = set(ec2_ids)
|
|
items = db_api.get_items_by_ids(context, ec2_ids)
|
|
if len(items) < len(ec2_ids):
|
|
missed_ids = ec2_ids - set((item['id'] for item in items))
|
|
params = {'id': next(iter(missed_ids))}
|
|
raise NOT_FOUND_EXCEPTION_MAP[kind](**params)
|
|
return items
|
|
|
|
|
|
_auto_create_db_item_extensions = {}
|
|
|
|
|
|
def register_auto_create_db_item_extension(kind, extension):
|
|
_auto_create_db_item_extensions[kind] = extension
|
|
|
|
|
|
def auto_create_db_item(context, kind, os_id, **extension_kwargs):
|
|
item = {'os_id': os_id}
|
|
extension = _auto_create_db_item_extensions.get(kind)
|
|
if extension:
|
|
extension(context, item, **extension_kwargs)
|
|
return db_api.add_item(context, kind, item)
|
|
|
|
|
|
def get_db_item_by_os_id(context, kind, os_id, items_by_os_id=None,
|
|
**extension_kwargs):
|
|
"""Get DB item by OS id (create if it doesn't exist).
|
|
|
|
Args:
|
|
context (RequestContext): The request context.
|
|
kind (str): The kind of item.
|
|
os_id (str): OS id of an object.
|
|
items_by_os_id (dict of items): The dict of known DB items,
|
|
OS id is used as a key.
|
|
extension_kwargs (dict): Additional parameters passed to
|
|
a registered extension at creating item.
|
|
|
|
Returns:
|
|
A found or created item.
|
|
|
|
Search item in passed dict. If it's not found - create a new item, and
|
|
add it to the dict (if it's passed).
|
|
If an extension is registered on corresponding item kind, call it
|
|
passing extension_kwargs to it.
|
|
"""
|
|
if os_id is None:
|
|
return None
|
|
if items_by_os_id is not None:
|
|
item = items_by_os_id.get(os_id)
|
|
if item:
|
|
return item
|
|
else:
|
|
item = next((i for i in db_api.get_items(context, kind)
|
|
if i['os_id'] == os_id), None)
|
|
if not item:
|
|
item = auto_create_db_item(context, kind, os_id, **extension_kwargs)
|
|
if items_by_os_id is not None:
|
|
items_by_os_id[os_id] = item
|
|
return item
|
|
|
|
|
|
# TODO(Alex): The project_id passing mechanism can be potentially
|
|
# reconsidered in future.
|
|
def os_id_to_ec2_id(context, kind, os_id, items_by_os_id=None,
|
|
ids_by_os_id=None, project_id=None):
|
|
if os_id is None:
|
|
return None
|
|
if ids_by_os_id is not None:
|
|
item_id = ids_by_os_id.get(os_id)
|
|
if item_id:
|
|
return item_id
|
|
if items_by_os_id is not None:
|
|
item = items_by_os_id.get(os_id)
|
|
if item:
|
|
return item['id']
|
|
ids = db_api.get_items_ids(context, kind, item_os_ids=(os_id,))
|
|
if len(ids):
|
|
item_id, _os_id = ids[0]
|
|
else:
|
|
item_id = db_api.add_item_id(context, kind, os_id,
|
|
project_id=project_id)
|
|
if ids_by_os_id is not None:
|
|
ids_by_os_id[os_id] = item_id
|
|
return item_id
|
|
|
|
|
|
def get_os_image(context, ec2_image_id):
|
|
kind = get_ec2_id_kind(ec2_image_id)
|
|
ids = db_api.get_items_ids(context, kind, item_ids=(ec2_image_id,))
|
|
if not ids:
|
|
raise exception.InvalidAMIIDNotFound(id=ec2_image_id)
|
|
_id, os_id = ids[0]
|
|
if not os_id:
|
|
return None
|
|
glance = clients.glance(context)
|
|
try:
|
|
return glance.images.get(os_id)
|
|
except glance_exception.HTTPNotFound:
|
|
raise exception.InvalidAMIIDNotFound(id=ec2_image_id)
|
|
|
|
|
|
def deserialize_os_image_properties(os_image):
|
|
def prepare_property(property_name):
|
|
if property_name in os_image_dict:
|
|
os_image_dict[property_name] = jsonutils.loads(
|
|
os_image_dict[property_name])
|
|
|
|
os_image_dict = dict(os_image)
|
|
prepare_property('mappings')
|
|
prepare_property('block_device_mapping')
|
|
return os_image_dict
|
|
|
|
|
|
def create_virtual_bdm(device_name, virtual_name):
|
|
bdm = {'device_name': device_name,
|
|
'source_type': 'blank',
|
|
'destination_type': 'local',
|
|
'device_type': 'disk',
|
|
'delete_on_termination': True,
|
|
'boot_index': -1,
|
|
'virtual_name': virtual_name}
|
|
if virtual_name == 'swap':
|
|
bdm['guest_format'] = 'swap'
|
|
return bdm
|
|
|
|
|
|
def get_os_image_mappings(os_image_properties):
|
|
mappings = []
|
|
names = set()
|
|
# TODO(ft): validate device names for both virtual and block device
|
|
# mappings
|
|
|
|
def is_virtual(virtual_name):
|
|
return virtual_name == 'swap' or (virtual_name and
|
|
_ephemeral.match(virtual_name))
|
|
|
|
# NOTE(ft): substitute mapping if the same device name is specified
|
|
def add_mapping(mapping):
|
|
device_name = block_device_strip_dev(mapping.get('device_name'))
|
|
if device_name in names:
|
|
for i, m in enumerate(mappings):
|
|
if (device_name ==
|
|
block_device_strip_dev(m.get('device_name'))):
|
|
mappings[i] = mapping
|
|
break
|
|
else:
|
|
if device_name:
|
|
names.add(device_name)
|
|
mappings.append(mapping)
|
|
|
|
# TODO(ft): From Juno virtual device mapping has precedence of block one
|
|
# in boot logic. This function should do the same, despite Nova EC2
|
|
# behavior.
|
|
|
|
# NOTE(ft): Nova EC2 prepended device names for virtual device mappings.
|
|
# But AWS doesn't do it.
|
|
for vdm in os_image_properties.get('mappings', []):
|
|
if is_virtual(vdm.get('virtual')):
|
|
add_mapping(create_virtual_bdm(
|
|
block_device_prepend_dev(vdm.get('device')), vdm['virtual']))
|
|
|
|
legacy_mapping = not os_image_properties.get('bdm_v2', False)
|
|
for bdm in os_image_properties.get('block_device_mapping', []):
|
|
if legacy_mapping:
|
|
virtual_name = bdm.get('virtual_name')
|
|
if is_virtual(virtual_name):
|
|
new_bdm = create_virtual_bdm(bdm.get('device_name'),
|
|
virtual_name)
|
|
else:
|
|
new_bdm = {key: val for key, val in bdm.items()
|
|
if key in LEGACY_BDM_FIELDS}
|
|
if bdm.get('snapshot_id'):
|
|
new_bdm.update({'source_type': 'snapshot',
|
|
'destination_type': 'volume'})
|
|
elif bdm.get('volume_id'):
|
|
new_bdm.update({'source_type': 'volume',
|
|
'destination_type': 'volume'})
|
|
bdm = new_bdm
|
|
|
|
bdm.setdefault('delete_on_termination', False)
|
|
add_mapping(bdm)
|
|
|
|
return mappings
|
|
|
|
|
|
def get_os_public_network(context):
|
|
neutron = clients.neutron(context)
|
|
search_opts = {'router:external': True, 'name': CONF.external_network}
|
|
os_networks = neutron.list_networks(**search_opts)['networks']
|
|
if len(os_networks) != 1:
|
|
if CONF.external_network:
|
|
if len(os_networks) == 0:
|
|
msg = "No external network with name '%s' is found"
|
|
else:
|
|
msg = "More than one external network with name '%s' is found"
|
|
LOG.error(msg, CONF.external_network)
|
|
else:
|
|
if len(os_networks) == 0:
|
|
msg = 'No external network is found'
|
|
else:
|
|
msg = 'More than one external network is found'
|
|
LOG.error(msg)
|
|
raise exception.Unsupported(_('Feature is restricted by OS admin'))
|
|
return os_networks[0]
|
|
|
|
|
|
def get_attached_gateway(context, vpc_id, gateway_kind):
|
|
# TODO(ft): move search by vpc_id to DB api
|
|
return next((gw for gw in db_api.get_items(context, gateway_kind)
|
|
if gw['vpc_id'] == vpc_id), None)
|
|
|
|
|
|
_check_and_create_default_vpc = None
|
|
|
|
|
|
def check_and_create_default_vpc(context):
|
|
return _check_and_create_default_vpc(context)
|
|
|
|
|
|
def set_check_and_create_default_vpc(check_and_create_default_vpc):
|
|
global _check_and_create_default_vpc
|
|
_check_and_create_default_vpc = check_and_create_default_vpc
|
|
|
|
|
|
def get_default_vpc(context):
|
|
default_vpc = check_and_create_default_vpc(context)
|
|
if not default_vpc:
|
|
raise exception.VPCIdNotSpecified()
|
|
return default_vpc
|
|
|
|
|
|
# NOTE(ft): following functions are copied from various parts of Nova
|
|
|
|
_ephemeral = re.compile(r'^ephemeral(\d|[1-9]\d+)$')
|
|
|
|
_dev = re.compile(r'^/dev/')
|
|
|
|
|
|
def block_device_strip_dev(device_name):
|
|
"""remove leading '/dev/'."""
|
|
return _dev.sub('', device_name) if device_name else device_name
|
|
|
|
|
|
def block_device_prepend_dev(device_name):
|
|
"""Make sure there is a leading '/dev/'."""
|
|
return device_name and '/dev/' + block_device_strip_dev(device_name)
|
|
|
|
|
|
def block_device_properties_root_device_name(properties):
|
|
"""get root device name from image meta data.
|
|
|
|
If it isn't specified, return None.
|
|
"""
|
|
if 'root_device_name' in properties:
|
|
return properties.get('root_device_name')
|
|
elif 'mappings' in properties:
|
|
return next((bdm['device'] for bdm in properties['mappings']
|
|
if bdm['virtual'] == 'root'), None)
|
|
else:
|
|
return None
|
|
|
|
|
|
_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f'
|
|
_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
|
|
|
|
|
|
def isotime(at=None, subsecond=False):
|
|
"""Stringify time in ISO 8601 format."""
|
|
|
|
# Python provides a similar instance method for datetime.datetime objects
|
|
# called isoformat(). The format of the strings generated by isoformat()
|
|
# have a couple of problems:
|
|
# 1) The strings generated by isotime are used in tokens and other public
|
|
# APIs that we can't change without a deprecation period. The strings
|
|
# generated by isoformat are not the same format, so we can't just
|
|
# change to it.
|
|
# 2) The strings generated by isoformat do not include the microseconds if
|
|
# the value happens to be 0. This will likely show up as random failures
|
|
# as parsers may be written to always expect microseconds, and it will
|
|
# parse correctly most of the time.
|
|
|
|
if not at:
|
|
at = timeutils.utcnow()
|
|
st = at.strftime(_ISO8601_TIME_FORMAT
|
|
if not subsecond
|
|
else _ISO8601_TIME_FORMAT_SUBSECOND)
|
|
tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
|
|
st += ('Z' if tz == 'UTC' else tz)
|
|
return st
|