Extract image manipulation methods
Change-Id: Ia261740fa168f157e62ae3b9ff45861b1bd6473c
This commit is contained in:
parent
92e11cc9d8
commit
cc05d7afe9
|
@ -17,7 +17,9 @@ from keystoneclient.v2_0 import client as kc
|
|||
from novaclient import client as novaclient
|
||||
from novaclient import shell as novashell
|
||||
from oslo.config import cfg
|
||||
from oslo import messaging
|
||||
|
||||
from ec2api import context as ec2_context
|
||||
from ec2api.openstack.common.gettextutils import _
|
||||
from ec2api.openstack.common import log as logging
|
||||
|
||||
|
@ -121,6 +123,18 @@ def keystone(context):
|
|||
return _keystone
|
||||
|
||||
|
||||
def nova_cert(context):
|
||||
_cert_api = _rpcapi_CertAPI(context)
|
||||
return _cert_api
|
||||
|
||||
|
||||
def rpc_init(conf):
|
||||
global _rpc_TRANSPORT
|
||||
# NOTE(ft): set control_exchange parameter to use Nova cert topic
|
||||
messaging.set_transport_defaults('nova')
|
||||
_rpc_TRANSPORT = messaging.get_transport(conf)
|
||||
|
||||
|
||||
def _url_for(context, **kwargs):
|
||||
service_catalog = context.service_catalog
|
||||
if not service_catalog:
|
||||
|
@ -139,3 +153,39 @@ def _url_for(context, **kwargs):
|
|||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class _rpcapi_CertAPI(object):
|
||||
'''Client side of the cert rpc API.'''
|
||||
|
||||
def __init__(self, context):
|
||||
super(_rpcapi_CertAPI, self).__init__()
|
||||
target = messaging.Target(topic=CONF.cert_topic, version='2.0')
|
||||
self.client = _rpc_get_client(target)
|
||||
self.context = context
|
||||
|
||||
def decrypt_text(self, text):
|
||||
cctxt = self.client.prepare()
|
||||
return cctxt.call(self.context, 'decrypt_text',
|
||||
project_id=self.context.project_id,
|
||||
text=text)
|
||||
|
||||
|
||||
_rpc_TRANSPORT = None
|
||||
|
||||
|
||||
def _rpc_get_client(target):
|
||||
assert _rpc_TRANSPORT is not None
|
||||
serializer = _rpc_RequestContextSerializer()
|
||||
return messaging.RPCClient(_rpc_TRANSPORT,
|
||||
target,
|
||||
serializer=serializer)
|
||||
|
||||
|
||||
class _rpc_RequestContextSerializer(messaging.NoOpSerializer):
|
||||
|
||||
def serialize_context(self, context):
|
||||
return context.to_dict()
|
||||
|
||||
def deserialize_context(self, context):
|
||||
return ec2_context.RequestContext.from_dict(context)
|
||||
|
|
|
@ -1054,6 +1054,8 @@ class CloudController(object):
|
|||
volume_id (str): The ID of the volume (Nova extension).
|
||||
snapshot_id (str): The ID of the snapshot.
|
||||
volume_size (str): The size of the volume, in GiBs.
|
||||
volume_type (str): The volume type.
|
||||
Not used now.
|
||||
delete_on_termination (bool): Indicates whether to delete
|
||||
the volume on instance termination.
|
||||
iops (int): he number of IOPS to provision for the volume.
|
||||
|
@ -1262,7 +1264,7 @@ class CloudController(object):
|
|||
def get_console_output(self, context, instance_id):
|
||||
return instance.get_console_output(context, instance_id)
|
||||
|
||||
def create_volume(self, context, availability_zone, size=None,
|
||||
def create_volume(self, context, availability_zone=None, size=None,
|
||||
snapshot_id=None, volume_type=None, name=None,
|
||||
description=None, metadata=None, iops=None,
|
||||
encrypted=None, kms_key_id=None):
|
||||
|
@ -1272,6 +1274,7 @@ class CloudController(object):
|
|||
context (RequestContext): The request context.
|
||||
availability_zone (str): The Availability Zone in which to create
|
||||
the volume.
|
||||
It's required by AWS but optional for legacy Nova EC2 API.
|
||||
instance_id (str): The size of the volume, in GiBs.
|
||||
Valid values: 1-1024
|
||||
If you're creating the volume from a snapshot and don't specify
|
||||
|
@ -1420,6 +1423,121 @@ class CloudController(object):
|
|||
return snapshot.describe_snapshots(context, snapshot_id, owner,
|
||||
restorable_by, filter)
|
||||
|
||||
def create_image(self, context, instance_id, name=None, description=None,
|
||||
no_reboot=False, block_device_mapping=None):
|
||||
"""Creates an EBS-backed AMI from an EBS-backed instance.
|
||||
|
||||
Args:
|
||||
context (RequestContext): The request context.
|
||||
instance_id (str): The ID of the instance.
|
||||
name (str): A name for the new image.
|
||||
It's required by AWS but optional for legacy Nova EC2 API.
|
||||
description (str): A description for the new image.
|
||||
Not used now.
|
||||
no_reboot (boolean): When the parameter is set to false, EC2
|
||||
attempts to shut down the instance cleanly before image
|
||||
creation and then reboots the instance.
|
||||
block_device_mapping (list of dict): Dict can contain:
|
||||
device_name (str): The device name exposed to the instance
|
||||
(for example, /dev/sdh or xvdh).
|
||||
virtual_name (str): The virtual device name (ephemeral[0..3]).
|
||||
ebs (dict): Dict can contain:
|
||||
volume_id (str): The ID of the volume (Nova extension).
|
||||
snapshot_id (str): The ID of the snapshot.
|
||||
volume_size (str): The size of the volume, in GiBs.
|
||||
volume_type (str): The volume type.
|
||||
Not used now.
|
||||
delete_on_termination (bool): Indicates whether to delete
|
||||
the volume on instance termination.
|
||||
iops (int): he number of IOPS to provision for the volume.
|
||||
Not used now.
|
||||
encrypted (boolean): Whether the volume is encrypted.
|
||||
Not used now.
|
||||
no_device (str): Suppresses the device mapping.
|
||||
|
||||
Returns:
|
||||
The ID of the new AMI.
|
||||
"""
|
||||
return image.create_image(context, instance_id, name, description,
|
||||
no_reboot, block_device_mapping)
|
||||
|
||||
def register_image(self, context, name=None, image_location=None,
|
||||
description=None, architecture=None,
|
||||
root_device_name=None, block_device_mapping=None,
|
||||
virtualization_type=None, kernel_id=None,
|
||||
ramdisk_id=None, sriov_net_support=None):
|
||||
"""Registers an AMI.
|
||||
|
||||
Args:
|
||||
context (RequestContext): The request context.
|
||||
name (str): A name for your AMI.
|
||||
It's required by AWS but optional for legacy Nova EC2 API.
|
||||
image_location (str): The full path to AMI manifest in S3 storage.
|
||||
description (str): A description for your AMI.
|
||||
Not used now.
|
||||
architecture (str): The architecture of the AMI.
|
||||
Not used now.
|
||||
root_device_name (str): The name of the root device
|
||||
block_device_mapping (list of dict): Dict can contain:
|
||||
device_name (str): The device name exposed to the instance
|
||||
(for example, /dev/sdh or xvdh).
|
||||
virtual_name (str): The virtual device name (ephemeral[0..3]).
|
||||
ebs (dict): Dict can contain:
|
||||
volume_id (str): The ID of the volume (Nova extension).
|
||||
snapshot_id (str): The ID of the snapshot.
|
||||
volume_size (str): The size of the volume, in GiBs.
|
||||
volume_type (str): The volume type.
|
||||
Not used now.
|
||||
delete_on_termination (bool): Indicates whether to delete
|
||||
the volume on instance termination.
|
||||
iops (int): he number of IOPS to provision for the volume.
|
||||
Not used now.
|
||||
encrypted (boolean): Whether the volume is encrypted.
|
||||
Not used now.
|
||||
no_device (str): Suppresses the device mapping.
|
||||
virtualization_type (str): The type of virtualization.
|
||||
Not used now.
|
||||
kernel_id (str): The ID of the kernel.
|
||||
Not used now.
|
||||
ramdisk_id (str): The ID of the RAM disk.
|
||||
Not used now.
|
||||
sriov_net_support (str): SR-IOV mode for networking.
|
||||
Not used now.
|
||||
|
||||
Returns:
|
||||
The ID of the new AMI.
|
||||
"""
|
||||
return image.register_image(context, name, image_location,
|
||||
description, architecture,
|
||||
root_device_name, block_device_mapping,
|
||||
virtualization_type, kernel_id,
|
||||
ramdisk_id, sriov_net_support)
|
||||
|
||||
def deregister_image(self, context, image_id):
|
||||
"""Deregisters the specified AMI.
|
||||
|
||||
Args:
|
||||
context (RequestContext): The request context.
|
||||
image_id (str): The ID of the AMI.
|
||||
|
||||
Returns:
|
||||
true if the request succeeds.
|
||||
"""
|
||||
return image.deregister_image(context, image_id)
|
||||
|
||||
def update_image(self, context, image_id, **kwargs):
|
||||
"""Update image metadata (Nova EC2 extension).
|
||||
|
||||
Args:
|
||||
context (RequestContext): The request context.
|
||||
image_id (str): The ID of the image.
|
||||
**kwargs: Metadata key-value pairs to be added/updated.
|
||||
|
||||
Returns:
|
||||
The updated image.
|
||||
"""
|
||||
pass
|
||||
|
||||
def describe_images(self, context, executable_by=None, image_id=None,
|
||||
owner=None, filter=None):
|
||||
"""Describes one or more of the images available to you.
|
||||
|
@ -1431,6 +1549,7 @@ class CloudController(object):
|
|||
Not used now.
|
||||
image_id (list of str): One or more image IDs.
|
||||
owner (list of str): Filters the images by the owner.
|
||||
Not used now.
|
||||
filter (list of filter dict): You can specify filters so that the
|
||||
response includes information for only certain images.
|
||||
|
||||
|
@ -1439,3 +1558,50 @@ class CloudController(object):
|
|||
"""
|
||||
return image.describe_images(context, executable_by, image_id,
|
||||
owner, filter)
|
||||
|
||||
def describe_image_attribute(self, context, image_id, attribute):
|
||||
"""Describes the specified attribute of the specified AMI.
|
||||
|
||||
Args:
|
||||
context (RequestContext): The request context.
|
||||
image_id (str): The ID of the image.
|
||||
attribute (str): The attribute of the network interface.
|
||||
Valid values: description (unsupported now)| kernel | ramdisk |
|
||||
launchPermission | productCodes (unsupported now)|
|
||||
blockDeviceMapping | rootDeviceName (Nova EC2 extension)
|
||||
|
||||
Returns:
|
||||
Specified attribute.
|
||||
"""
|
||||
return image.describe_image_attribute(context, image_id, attribute)
|
||||
|
||||
def modify_image_attribute(self, context, image_id, attribute,
|
||||
user_group, operation_type,
|
||||
description=None, launch_permission=None,
|
||||
product_code=None, user_id=None, value=None):
|
||||
"""Modifies the specified attribute of the specified AMI.
|
||||
|
||||
Args:
|
||||
context (RequestContext): The request context.
|
||||
image_id (str): The ID of the image.
|
||||
attribute (str): The name of the attribute to modify.
|
||||
It's optional for AWS but required for legacy Nova EC2 API.
|
||||
Only 'launchPermission' is supported now.
|
||||
user_group (list of str): One or more user groups.
|
||||
It's optional for AWS but required for legacy Nova EC2 API.
|
||||
Only 'all' group is supported now.
|
||||
operation_type (str): The operation type.
|
||||
It's optional for AWS but required for legacy Nova EC2 API.
|
||||
Only 'add' and 'remove' operation types are supported now.
|
||||
description: Not supported now.
|
||||
launch_permission: : Not supported now.
|
||||
product_code: : Not supported now.
|
||||
user_id: : Not supported now.
|
||||
value: : Not supported now.
|
||||
Returns:
|
||||
true if the request succeeds.
|
||||
"""
|
||||
return image.modify_image_attribute(context, image_id, attribute,
|
||||
user_group, operation_type,
|
||||
description, launch_permission,
|
||||
product_code, user_id, value)
|
||||
|
|
|
@ -12,16 +12,185 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import boto.s3.connection
|
||||
import eventlet
|
||||
from glanceclient import exc as glance_exception
|
||||
from lxml import etree
|
||||
from oslo.config import cfg
|
||||
from oslo_concurrency import processutils
|
||||
|
||||
from ec2api.api import clients
|
||||
from ec2api.api import common
|
||||
from ec2api.api import ec2utils
|
||||
from ec2api.api import instance as instance_api
|
||||
from ec2api.api import utils
|
||||
from ec2api import context as ec2_context
|
||||
from ec2api.db import api as db_api
|
||||
from ec2api import exception
|
||||
from ec2api.openstack.common.gettextutils import _
|
||||
from ec2api.openstack.common import timeutils
|
||||
|
||||
|
||||
s3_opts = [
|
||||
cfg.StrOpt('image_decryption_dir',
|
||||
default='/tmp',
|
||||
help='Parent directory for tempdir used for image decryption'),
|
||||
cfg.StrOpt('s3_host',
|
||||
default='$my_ip',
|
||||
help='Hostname or IP for OpenStack to use when accessing '
|
||||
'the S3 api'),
|
||||
cfg.IntOpt('s3_port',
|
||||
default=3333,
|
||||
help='Port used when accessing the S3 api'),
|
||||
cfg.BoolOpt('s3_use_ssl',
|
||||
default=False,
|
||||
help='Whether to use SSL when talking to S3'),
|
||||
cfg.BoolOpt('s3_affix_tenant',
|
||||
default=False,
|
||||
help='Whether to affix the tenant id to the access key '
|
||||
'when downloading from S3'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(s3_opts)
|
||||
|
||||
rpcapi_opts = [
|
||||
cfg.StrOpt('cert_topic',
|
||||
default='cert',
|
||||
help='The topic cert nodes listen on'),
|
||||
]
|
||||
|
||||
CONF.register_opts(rpcapi_opts)
|
||||
|
||||
|
||||
# TODO(yamahata): race condition
|
||||
# At the moment there is no way to prevent others from
|
||||
# manipulating instances/volumes/snapshots.
|
||||
# As other code doesn't take it into consideration, here we don't
|
||||
# care of it for now. Ostrich algorithm
|
||||
def create_image(context, instance_id, name=None, description=None,
|
||||
no_reboot=False, block_device_mapping=None):
|
||||
instance = ec2utils.get_db_item(context, 'i', instance_id)
|
||||
nova = clients.nova(context)
|
||||
os_instance = nova.servers.get(instance['os_id'])
|
||||
|
||||
if not instance_api._is_ebs_instance(context, os_instance):
|
||||
# TODO(ft): Change the error code and message with the real AWS ones
|
||||
msg = _('The instance is not an EBS-backed instance.')
|
||||
raise exception.InvalidParameterValue(value=instance_id,
|
||||
parameter='InstanceId',
|
||||
reason=msg)
|
||||
|
||||
restart_instance = False
|
||||
if not no_reboot:
|
||||
vm_state = getattr(os_instance, 'OS-EXT-STS:vm_state')
|
||||
|
||||
if vm_state not in (instance_api.vm_states_ACTIVE,
|
||||
instance_api.vm_states_STOPPED):
|
||||
# TODO(ft): Change the error code and message with the real AWS
|
||||
# ones
|
||||
msg = _('Instance must be run or stopped')
|
||||
raise exception.IncorrectState(reason=msg)
|
||||
|
||||
if vm_state == instance_api.vm_states_ACTIVE:
|
||||
restart_instance = True
|
||||
os_instance.stop()
|
||||
|
||||
# wait instance for really stopped
|
||||
start_time = time.time()
|
||||
while vm_state != instance_api.vm_states_STOPPED:
|
||||
time.sleep(1)
|
||||
os_instance.get()
|
||||
vm_state = getattr(os_instance, 'OS-EXT-STS:vm_state')
|
||||
# NOTE(yamahata): timeout and error. 1 hour for now for safety.
|
||||
# Is it too short/long?
|
||||
# Or is there any better way?
|
||||
timeout = 1 * 60 * 60
|
||||
if time.time() > start_time + timeout:
|
||||
err = _("Couldn't stop instance within %d sec") % timeout
|
||||
raise exception.EC2Exception(message=err)
|
||||
|
||||
# meaningful image name
|
||||
name_map = dict(instance=instance['os_id'], now=timeutils.isotime())
|
||||
name = name or _('image of %(instance)s at %(now)s') % name_map
|
||||
|
||||
with utils.OnCrashCleaner() as cleaner:
|
||||
os_image = os_instance.create_image(name)
|
||||
cleaner.addCleanup(os_image.delete)
|
||||
image = db_api.add_item(context, 'ami', {'os_id': os_image.id,
|
||||
'is_public': False})
|
||||
|
||||
if restart_instance:
|
||||
os_instance.start()
|
||||
|
||||
return {'imageId': image['id']}
|
||||
|
||||
|
||||
def register_image(context, name=None, image_location=None,
|
||||
description=None, architecture=None,
|
||||
root_device_name=None, block_device_mapping=None,
|
||||
virtualization_type=None, kernel_id=None,
|
||||
ramdisk_id=None, sriov_net_support=None):
|
||||
if image_location is None and name:
|
||||
image_location = name
|
||||
if image_location is None:
|
||||
msg = _('imageLocation is required')
|
||||
raise exception.MissingParameter(msg)
|
||||
|
||||
metadata = {'properties': {'image_location': image_location}}
|
||||
|
||||
if name:
|
||||
metadata['name'] = name
|
||||
else:
|
||||
metadata['name'] = image_location
|
||||
|
||||
if root_device_name:
|
||||
metadata['properties']['root_device_name'] = root_device_name
|
||||
|
||||
mappings = [instance_api._cloud_parse_block_device_mapping(context, bdm)
|
||||
for bdm in block_device_mapping or []]
|
||||
if mappings:
|
||||
metadata['properties']['block_device_mapping'] = mappings
|
||||
|
||||
with utils.OnCrashCleaner() as cleaner:
|
||||
os_image = _s3_create(context, metadata)
|
||||
cleaner.addCleanup(os_image.delete)
|
||||
image_type = ec2utils.image_type(os_image.container_format)
|
||||
image = db_api.add_item(context, image_type, {'os_id': os_image.id,
|
||||
'is_public': False})
|
||||
return {'imageId': image['id']}
|
||||
|
||||
|
||||
def deregister_image(context, image_id):
|
||||
# TODO(ft): AWS returns AuthFailure for public images,
|
||||
# but we return NotFound due searching for local images only
|
||||
kind = image_id.split('-')[0]
|
||||
image = ec2utils.get_db_item(context, kind, image_id)
|
||||
glance = clients.glance(context)
|
||||
try:
|
||||
glance.images.delete(image['os_id'])
|
||||
except glance_exception.HTTPNotFound:
|
||||
pass
|
||||
db_api.delete_item(context, image['id'])
|
||||
return True
|
||||
|
||||
|
||||
def update_image(context, image_id, **kwargs):
|
||||
kind = image_id.split('-')[0]
|
||||
image = ec2utils.get_db_item(context, kind, image_id)
|
||||
glance = clients.glance(context)
|
||||
return glance.images.update(image['os_id'], **kwargs)
|
||||
|
||||
|
||||
class ImageDescriber(common.UniversalDescriber):
|
||||
|
@ -53,9 +222,8 @@ class ImageDescriber(common.UniversalDescriber):
|
|||
public_images)))
|
||||
if len(images) < len(self.ids):
|
||||
missed_ids = set(self.ids) - set(i['id']
|
||||
for i in images.itervalues())
|
||||
raise exception.InvalidAMIIDNotFound(
|
||||
{'id': next(iter(missed_ids))})
|
||||
for i in images)
|
||||
raise exception.InvalidAMIIDNotFound(id=next(iter(missed_ids)))
|
||||
self.images = images
|
||||
self.snapshot_ids = dict((s['os_id'], s['id'])
|
||||
for s in db_api.get_items(self.context, 'snap'))
|
||||
|
@ -97,6 +265,91 @@ def describe_images(context, executable_by=None, image_id=None,
|
|||
return {'imagesSet': formatted_images}
|
||||
|
||||
|
||||
def describe_image_attribute(context, image_id, attribute):
|
||||
def _block_device_mapping_attribute(image, result):
|
||||
_cloud_format_mappings(image['properties'], result)
|
||||
|
||||
def _launch_permission_attribute(image, result):
|
||||
result['launchPermission'] = []
|
||||
if image['is_public']:
|
||||
result['launchPermission'].append({'group': 'all'})
|
||||
|
||||
def _root_device_name_attribute(image, result):
|
||||
_prop_root_dev_name = _block_device_properties_root_device_name
|
||||
result['rootDeviceName'] = _prop_root_dev_name(image['properties'])
|
||||
if result['rootDeviceName'] is None:
|
||||
result['rootDeviceName'] = _block_device_DEFAULT_ROOT_DEV_NAME
|
||||
|
||||
def _kernel_attribute(image, result):
|
||||
kernel_id = image['properties'].get('kernel_id')
|
||||
if kernel_id:
|
||||
result['kernel'] = {
|
||||
'value': ec2utils.os_id_to_ec2_id(context, 'aki', kernel_id)
|
||||
}
|
||||
|
||||
def _ramdisk_attribute(image, result):
|
||||
ramdisk_id = image['properties'].get('ramdisk_id')
|
||||
if ramdisk_id:
|
||||
result['ramdisk'] = {
|
||||
'value': ec2utils.os_id_to_ec2_id(context, 'ari', ramdisk_id)
|
||||
}
|
||||
|
||||
supported_attributes = {
|
||||
'blockDeviceMapping': _block_device_mapping_attribute,
|
||||
'launchPermission': _launch_permission_attribute,
|
||||
'rootDeviceName': _root_device_name_attribute,
|
||||
'kernel': _kernel_attribute,
|
||||
'ramdisk': _ramdisk_attribute,
|
||||
}
|
||||
|
||||
# TODO(ft): AWS returns AuthFailure for public images,
|
||||
# but we return NotFound due searching for local images only
|
||||
kind = image_id.split('-')[0]
|
||||
image = ec2utils.get_db_item(context, kind, image_id)
|
||||
fn = supported_attributes.get(attribute)
|
||||
if fn is None:
|
||||
raise exception.InvalidAttribute(attr=attribute)
|
||||
glance = clients.glance(context)
|
||||
os_image = glance.images.get(image['os_id'])
|
||||
|
||||
result = {'imageId': image_id}
|
||||
fn(os_image, result)
|
||||
return result
|
||||
|
||||
|
||||
def modify_image_attribute(context, image_id, attribute,
|
||||
user_group, operation_type,
|
||||
description=None, launch_permission=None,
|
||||
product_code=None, user_id=None, value=None):
|
||||
if attribute != 'launchPermission':
|
||||
# TODO(ft): Change the error code and message with the real AWS ones
|
||||
raise exception.InvalidAttribute(attr=attribute)
|
||||
if not user_group:
|
||||
msg = _('user or group not specified')
|
||||
# TODO(ft): Change the error code and message with the real AWS ones
|
||||
raise exception.MissingParameter(msg)
|
||||
if len(user_group) != 1 and user_group[0] != 'all':
|
||||
msg = _('only group "all" is supported')
|
||||
raise exception.InvalidParameterValue(parameter='UserGroup',
|
||||
value=user_group,
|
||||
reason=msg)
|
||||
if operation_type not in ['add', 'remove']:
|
||||
msg = _('operation_type must be add or remove')
|
||||
raise exception.InvalidParameterValue(parameter='OperationType',
|
||||
value='operation_type',
|
||||
reason=msg)
|
||||
|
||||
# TODO(ft): AWS returns AuthFailure for public images,
|
||||
# but we return NotFound due searching for local images only
|
||||
kind = image_id.split('-')[0]
|
||||
image = ec2utils.get_db_item(context, kind, image_id)
|
||||
glance = clients.glance(context)
|
||||
image = glance.images.get(image['os_id'])
|
||||
|
||||
image.update(is_public=(operation_type == 'add'))
|
||||
return True
|
||||
|
||||
|
||||
def _format_image(context, image, os_image, images_dict, ids_dict,
|
||||
snapshot_ids=None):
|
||||
image_type = ec2utils.image_type(os_image.container_format)
|
||||
|
@ -288,3 +541,236 @@ _ephemeral = re.compile('^ephemeral(\d|[1-9]\d+)$')
|
|||
|
||||
def _block_device_is_ephemeral(device_name):
|
||||
return _ephemeral.match(device_name) is not None
|
||||
|
||||
|
||||
def _s3_create(context, metadata):
|
||||
"""Gets a manifest from s3 and makes an image."""
|
||||
image_path = tempfile.mkdtemp(dir=CONF.image_decryption_dir)
|
||||
|
||||
image_location = metadata['properties']['image_location'].lstrip('/')
|
||||
bucket_name = image_location.split('/')[0]
|
||||
manifest_path = image_location[len(bucket_name) + 1:]
|
||||
bucket = _s3_conn(context).get_bucket(bucket_name)
|
||||
key = bucket.get_key(manifest_path)
|
||||
manifest = key.get_contents_as_string()
|
||||
|
||||
manifest, image = _s3_parse_manifest(context, metadata, manifest)
|
||||
|
||||
def _update_image_state(image_state):
|
||||
image.update(properties={'image_state': image_state})
|
||||
|
||||
def delayed_create():
|
||||
"""This handles the fetching and decrypting of the part files."""
|
||||
context.update_store()
|
||||
|
||||
try:
|
||||
_update_image_state('downloading')
|
||||
|
||||
try:
|
||||
parts = []
|
||||
elements = manifest.find('image').getiterator('filename')
|
||||
for fn_element in elements:
|
||||
part = _s3_download_file(bucket, fn_element.text,
|
||||
image_path)
|
||||
parts.append(part)
|
||||
|
||||
# NOTE(vish): this may be suboptimal, should we use cat?
|
||||
enc_filename = os.path.join(image_path, 'image.encrypted')
|
||||
with open(enc_filename, 'w') as combined:
|
||||
for filename in parts:
|
||||
with open(filename) as part:
|
||||
shutil.copyfileobj(part, combined)
|
||||
|
||||
except Exception:
|
||||
_update_image_state('failed_download')
|
||||
return
|
||||
|
||||
_update_image_state('decrypting')
|
||||
|
||||
try:
|
||||
hex_key = manifest.find('image/ec2_encrypted_key').text
|
||||
encrypted_key = binascii.a2b_hex(hex_key)
|
||||
hex_iv = manifest.find('image/ec2_encrypted_iv').text
|
||||
encrypted_iv = binascii.a2b_hex(hex_iv)
|
||||
|
||||
dec_filename = os.path.join(image_path, 'image.tar.gz')
|
||||
_s3_decrypt_image(context, enc_filename, encrypted_key,
|
||||
encrypted_iv, dec_filename)
|
||||
except Exception:
|
||||
_update_image_state('failed_decrypt')
|
||||
return
|
||||
|
||||
_update_image_state('untarring')
|
||||
|
||||
try:
|
||||
unz_filename = _s3_untarzip_image(image_path, dec_filename)
|
||||
except Exception:
|
||||
_update_image_state('failed_untar')
|
||||
return
|
||||
|
||||
_update_image_state('uploading')
|
||||
try:
|
||||
with open(unz_filename) as image_file:
|
||||
image.update(data=image_file)
|
||||
except Exception:
|
||||
_update_image_state('failed_upload')
|
||||
return
|
||||
|
||||
_update_image_state('available')
|
||||
|
||||
shutil.rmtree(image_path)
|
||||
except glance_exception.HTTPNotFound:
|
||||
return
|
||||
|
||||
eventlet.spawn_n(delayed_create)
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def _s3_parse_manifest(context, metadata, manifest):
|
||||
manifest = etree.fromstring(manifest)
|
||||
image_format = 'ami'
|
||||
|
||||
try:
|
||||
kernel_id = manifest.find('machine_configuration/kernel_id').text
|
||||
if kernel_id == 'true':
|
||||
image_format = 'aki'
|
||||
kernel_id = None
|
||||
except Exception:
|
||||
kernel_id = None
|
||||
|
||||
try:
|
||||
ramdisk_id = manifest.find('machine_configuration/ramdisk_id').text
|
||||
if ramdisk_id == 'true':
|
||||
image_format = 'ari'
|
||||
ramdisk_id = None
|
||||
except Exception:
|
||||
ramdisk_id = None
|
||||
|
||||
try:
|
||||
arch = manifest.find('machine_configuration/architecture').text
|
||||
except Exception:
|
||||
arch = 'x86_64'
|
||||
|
||||
# NOTE(yamahata):
|
||||
# EC2 ec2-budlne-image --block-device-mapping accepts
|
||||
# <virtual name>=<device name> where
|
||||
# virtual name = {ami, root, swap, ephemeral<N>}
|
||||
# where N is no negative integer
|
||||
# device name = the device name seen by guest kernel.
|
||||
# They are converted into
|
||||
# block_device_mapping/mapping/{virtual, device}
|
||||
#
|
||||
# Do NOT confuse this with ec2-register's block device mapping
|
||||
# argument.
|
||||
mappings = []
|
||||
try:
|
||||
block_device_mapping = manifest.findall('machine_configuration/'
|
||||
'block_device_mapping/'
|
||||
'mapping')
|
||||
for bdm in block_device_mapping:
|
||||
mappings.append({'virtual': bdm.find('virtual').text,
|
||||
'device': bdm.find('device').text})
|
||||
except Exception:
|
||||
mappings = []
|
||||
|
||||
properties = metadata['properties']
|
||||
properties['architecture'] = arch
|
||||
|
||||
def _translate_dependent_image_id(image_key, image_id):
|
||||
image_uuid = ec2utils.ec2_id_to_glance_id(context, image_id)
|
||||
properties[image_key] = image_uuid
|
||||
|
||||
if kernel_id:
|
||||
_translate_dependent_image_id('kernel_id', kernel_id)
|
||||
|
||||
if ramdisk_id:
|
||||
_translate_dependent_image_id('ramdisk_id', ramdisk_id)
|
||||
|
||||
if mappings:
|
||||
properties['mappings'] = mappings
|
||||
|
||||
metadata.update({'disk_format': image_format,
|
||||
'container_format': image_format,
|
||||
'is_public': False,
|
||||
'properties': properties})
|
||||
metadata['properties']['image_state'] = 'pending'
|
||||
|
||||
# TODO(bcwaldon): right now, this removes user-defined ids
|
||||
# We need to re-enable this.
|
||||
metadata.pop('id', None)
|
||||
|
||||
glance = clients.glance(context)
|
||||
image = glance.images.create(**metadata)
|
||||
|
||||
return manifest, image
|
||||
|
||||
|
||||
def _s3_download_file(bucket, filename, local_dir):
|
||||
key = bucket.get_key(filename)
|
||||
local_filename = os.path.join(local_dir, os.path.basename(filename))
|
||||
key.get_contents_to_filename(local_filename)
|
||||
return local_filename
|
||||
|
||||
|
||||
def _s3_decrypt_image(context, encrypted_filename, encrypted_key,
|
||||
encrypted_iv, decrypted_filename):
|
||||
cert_client = clients.nova_cert(context)
|
||||
try:
|
||||
key = cert_client.decrypt_text(base64.b64encode(encrypted_key))
|
||||
except Exception as exc:
|
||||
msg = _('Failed to decrypt private key: %s') % exc
|
||||
raise exception.EC2Exception(msg)
|
||||
try:
|
||||
iv = cert_client.decrypt_text(base64.b64encode(encrypted_iv))
|
||||
except Exception as exc:
|
||||
msg = _('Failed to decrypt initialization vector: %s') % exc
|
||||
raise exception.EC2Exception(msg)
|
||||
|
||||
try:
|
||||
processutils.execute('openssl', 'enc',
|
||||
'-d', '-aes-128-cbc',
|
||||
'-in', '%s' % (encrypted_filename,),
|
||||
'-K', '%s' % (key,),
|
||||
'-iv', '%s' % (iv,),
|
||||
'-out', '%s' % (decrypted_filename,))
|
||||
except processutils.ProcessExecutionError as exc:
|
||||
raise exception.EC2Exception(_('Failed to decrypt image file '
|
||||
'%(image_file)s: %(err)s') %
|
||||
{'image_file': encrypted_filename,
|
||||
'err': exc.stdout})
|
||||
|
||||
|
||||
def _s3_untarzip_image(path, filename):
|
||||
_s3_test_for_malicious_tarball(path, filename)
|
||||
tar_file = tarfile.open(filename, 'r|gz')
|
||||
tar_file.extractall(path)
|
||||
image_file = tar_file.getnames()[0]
|
||||
tar_file.close()
|
||||
return os.path.join(path, image_file)
|
||||
|
||||
|
||||
def _s3_test_for_malicious_tarball(path, filename):
|
||||
"""Raises exception if extracting tarball would escape extract path."""
|
||||
tar_file = tarfile.open(filename, 'r|gz')
|
||||
for n in tar_file.getnames():
|
||||
if not os.path.abspath(os.path.join(path, n)).startswith(path):
|
||||
tar_file.close()
|
||||
raise exception.Invalid(_('Unsafe filenames in image'))
|
||||
tar_file.close()
|
||||
|
||||
|
||||
def _s3_conn(context):
|
||||
# NOTE(vish): access and secret keys for s3 server are not
|
||||
# checked in nova-objectstore
|
||||
access = context.access_key
|
||||
if CONF.s3_affix_tenant:
|
||||
access = '%s:%s' % (access, context.project_id)
|
||||
secret = context.secret_key
|
||||
calling = boto.s3.connection.OrdinaryCallingFormat()
|
||||
return boto.s3.connection.S3Connection(aws_access_key_id=access,
|
||||
aws_secret_access_key=secret,
|
||||
is_secure=CONF.s3_use_ssl,
|
||||
calling_format=calling,
|
||||
port=CONF.s3_port,
|
||||
host=CONF.s3_host)
|
||||
|
|
|
@ -341,6 +341,32 @@ def start_instances(context, instance_id):
|
|||
lambda instance: instance.start())
|
||||
|
||||
|
||||
def get_password_data(context, instance_id):
|
||||
# NOTE(Alex): AWS supports one and only one instance_id here
|
||||
instance = ec2utils.get_db_item(context, 'i', instance_id)
|
||||
nova = clients.nova(context)
|
||||
os_instance = nova.servers.get(instance['os_id'])
|
||||
password = os_instance.get_password()
|
||||
# NOTE(vish): this should be timestamp from the metadata fields
|
||||
# but it isn't important enough to implement properly
|
||||
now = timeutils.utcnow()
|
||||
return {"instanceId": instance_id,
|
||||
"timestamp": now,
|
||||
"passwordData": password}
|
||||
|
||||
|
||||
def get_console_output(context, instance_id):
|
||||
# NOTE(Alex): AWS supports one and only one instance_id here
|
||||
instance = ec2utils.get_db_item(context, 'i', instance_id)
|
||||
nova = clients.nova(context)
|
||||
os_instance = nova.servers.get(instance['os_id'])
|
||||
console_output = os_instance.get_console_output()
|
||||
now = timeutils.utcnow()
|
||||
return {"instanceId": instance_id,
|
||||
"timestamp": now,
|
||||
"output": console_output}
|
||||
|
||||
|
||||
def describe_instance_attribute(context, instance_id, attribute):
|
||||
instance = db_api.get_item_by_id(context, 'i', instance_id)
|
||||
nova = clients.nova(context)
|
||||
|
@ -922,18 +948,6 @@ def _get_os_instances_by_instances(context, instances, exactly=False):
|
|||
return os_instances
|
||||
|
||||
|
||||
def _auto_create_instance_extension(context, instance, novadb_instance=None):
|
||||
if not novadb_instance:
|
||||
novadb_instance = novadb.instance_get_by_uuid(context,
|
||||
instance['os_id'])
|
||||
instance['reservation_id'] = novadb_instance['reservation_id']
|
||||
instance['launch_index'] = novadb_instance['launch_index']
|
||||
|
||||
|
||||
ec2utils.register_auto_create_db_item_extension(
|
||||
'i', _auto_create_instance_extension)
|
||||
|
||||
|
||||
def _get_ec2_classic_os_network(context, neutron):
|
||||
os_subnet_ids = [eni['os_id']
|
||||
for eni in db_api.get_items(context, 'subnet')]
|
||||
|
@ -958,30 +972,35 @@ def _get_ec2_classic_os_network(context, neutron):
|
|||
return ec2_classic_os_networks[0]
|
||||
|
||||
|
||||
def get_password_data(context, instance_id):
|
||||
# NOTE(Alex): AWS supports one and only one instance_id here
|
||||
instance = ec2utils.get_db_item(context, 'i', instance_id)
|
||||
nova = clients.nova(context)
|
||||
os_instance = nova.servers.get(instance['os_id'])
|
||||
password = os_instance.get_password()
|
||||
# NOTE(vish): this should be timestamp from the metadata fields
|
||||
# but it isn't important enough to implement properly
|
||||
now = timeutils.utcnow()
|
||||
return {"instanceId": instance_id,
|
||||
"timestamp": now,
|
||||
"passwordData": password}
|
||||
def _is_ebs_instance(context, os_instance):
|
||||
novadb_instance = novadb.instance_get_by_uuid(context, os_instance.id)
|
||||
root_device_name = _cloud_format_instance_root_device_name(novadb_instance)
|
||||
root_device_short_name = _block_device_strip_dev(root_device_name)
|
||||
if root_device_name == root_device_short_name:
|
||||
root_device_name = _block_device_prepend_dev(root_device_name)
|
||||
for bdm in novadb.block_device_mapping_get_all_by_instance(context,
|
||||
os_instance.id):
|
||||
volume_id = bdm['volume_id']
|
||||
if (volume_id is None or bdm['no_device']):
|
||||
continue
|
||||
|
||||
if ((bdm['snapshot_id'] or bdm['volume_id']) and
|
||||
(bdm['device_name'] == root_device_name or
|
||||
bdm['device_name'] == root_device_short_name)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_console_output(context, instance_id):
|
||||
# NOTE(Alex): AWS supports one and only one instance_id here
|
||||
instance = ec2utils.get_db_item(context, 'i', instance_id)
|
||||
nova = clients.nova(context)
|
||||
os_instance = nova.servers.get(instance['os_id'])
|
||||
console_output = os_instance.get_console_output()
|
||||
now = timeutils.utcnow()
|
||||
return {"instanceId": instance_id,
|
||||
"timestamp": now,
|
||||
"output": console_output}
|
||||
def _auto_create_instance_extension(context, instance, novadb_instance=None):
|
||||
if not novadb_instance:
|
||||
novadb_instance = novadb.instance_get_by_uuid(context,
|
||||
instance['os_id'])
|
||||
instance['reservation_id'] = novadb_instance['reservation_id']
|
||||
instance['launch_index'] = novadb_instance['launch_index']
|
||||
|
||||
|
||||
ec2utils.register_auto_create_db_item_extension(
|
||||
'i', _auto_create_instance_extension)
|
||||
|
||||
|
||||
# NOTE(ft): following functions are copied from various parts of Nova
|
||||
|
|
|
@ -20,6 +20,7 @@ import sys
|
|||
|
||||
from oslo.config import cfg
|
||||
|
||||
from ec2api.api import clients
|
||||
from ec2api import config
|
||||
from ec2api.openstack.common import log as logging
|
||||
from ec2api import service
|
||||
|
@ -31,6 +32,7 @@ CONF.import_opt('use_ssl', 'ec2api.service')
|
|||
def main():
|
||||
config.parse_args(sys.argv)
|
||||
logging.setup('ec2api')
|
||||
clients.rpc_init(cfg.CONF)
|
||||
|
||||
server = service.WSGIService(
|
||||
'ec2api', use_ssl=CONF.use_ssl, max_url_len=16384)
|
||||
|
|
38
install.sh
38
install.sh
|
@ -186,6 +186,21 @@ function iniget() {
|
|||
echo ${line#*=}
|
||||
}
|
||||
|
||||
# Copy an option from Nova INI file or from environment if it's set
|
||||
function copynovaopt() {
|
||||
local option_name=$1
|
||||
local env_var
|
||||
local option
|
||||
env_var=${option_name^^}
|
||||
if [ ${!env_var+x} ]; then
|
||||
option=${!env_var}
|
||||
elif ini_has_option "$NOVA_CONF" DEFAULT $option_name; then
|
||||
option=$(iniget $NOVA_CONF DEFAULT $option_name)
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
iniset $CONF_FILE DEFAULT $option_name $option
|
||||
}
|
||||
|
||||
#get nova settings
|
||||
if [[ -z "$NOVA_CONNECTION" ]]; then
|
||||
|
@ -204,11 +219,7 @@ if [[ -z "$NOVA_CONNECTION" ]]; then
|
|||
NOVA_CONNECTION=$(iniget $NOVA_CONF sql connection)
|
||||
fi
|
||||
fi
|
||||
if [[ -z "$NOVA_CONNECTION" ]]; then
|
||||
echo "$reason"
|
||||
echo "Please set NOVA_CONNECTION environment variable to the connection string to Nova DB"
|
||||
exit 1
|
||||
fi
|
||||
die_if_not_set $LINENO NOVA_CONNECTION "$reason. Please set NOVA_CONNECTION environment variable to the connection string to Nova DB"
|
||||
fi
|
||||
if [[ -z "$EXTERNAL_NETWORK" ]]; then
|
||||
declare -a newtron_output
|
||||
|
@ -220,14 +231,9 @@ if [[ -z "$EXTERNAL_NETWORK" ]]; then
|
|||
else
|
||||
EXTERNAL_NETWORK=$(echo $newtron_output | awk -F '|' '{ print $3 }')
|
||||
fi
|
||||
if [[ -z "$EXTERNAL_NETWORK" ]]; then
|
||||
echo $reason
|
||||
echo "Please set PUBLIC_NETWORK environment variable to the external network dedicated to EC2 elastic IP operations"
|
||||
exit 1
|
||||
fi
|
||||
die_if_not_set $LINENO EXTERNAL_NETWORK "$reason. Please set PUBLIC_NETWORK environment variable to the external network dedicated to EC2 elastic IP operations"
|
||||
fi
|
||||
|
||||
|
||||
#create keystone user with admin privileges
|
||||
ADMIN_ROLE=$(get_data 2 admin 1 keystone role-list)
|
||||
die_if_not_set $LINENO ADMIN_ROLE "Fail to get ADMIN_ROLE by 'keystone role-list' "
|
||||
|
@ -284,6 +290,16 @@ iniset $CONF_FILE keystone_authtoken admin_tenant_name $SERVICE_TENANT
|
|||
iniset $CONF_FILE keystone_authtoken auth_protocol $AUTH_PROTO
|
||||
iniset $CONF_FILE keystone_authtoken auth_port $AUTH_PORT
|
||||
|
||||
if [[ -f "$NOVA_CONF" ]]; then
|
||||
copynovaopt s3_host
|
||||
copynovaopt s3_port
|
||||
copynovaopt s3_affix_tenant
|
||||
copynovaopt s3_use_ssl
|
||||
copynovaopt cert_topic
|
||||
copynovaopt rabbit_hosts
|
||||
copynovaopt rabbit_password
|
||||
# TODO(ft): it's necessary to support other available messaging implementations
|
||||
fi
|
||||
|
||||
#init cache dir
|
||||
echo Creating signing dir
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
anyjson>=0.3.3
|
||||
argparse
|
||||
Babel>=1.3
|
||||
boto>=2.32.1
|
||||
eventlet>=0.13.0
|
||||
greenlet>=0.3.2
|
||||
httplib2>=0.7.5
|
||||
iso8601>=0.1.9
|
||||
jsonschema>=2.0.0,<3.0.0
|
||||
lxml>=2.3
|
||||
oslo.concurrency>=0.3.0
|
||||
oslo.config>=1.4.0.0a2
|
||||
oslo.messaging>=1.4.0,!=1.5.0
|
||||
Paste
|
||||
PasteDeploy>=1.5.0
|
||||
pbr>=0.6,!=0.7,<1.0
|
||||
|
|
Loading…
Reference in New Issue