Refactor running of instances

- get a single copy of key algorithms
- remove remains of detaching of network interfaces during cleaning on
crash
- use describe_instances to format run_instances result
- use describe_instances for idempotent run

Change-Id: Ica01a15f6146f39c0100902604d438968efc41a5
This commit is contained in:
Feodor Tersin 2015-03-25 20:11:48 +03:00
parent 4b2924c02e
commit 092e1947c1
2 changed files with 230 additions and 452 deletions

View File

@ -21,6 +21,7 @@ import re
from novaclient import exceptions as nova_exception
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from ec2api.api import clients
@ -31,8 +32,9 @@ from ec2api.api import security_group as security_group_api
from ec2api import context as ec2_context
from ec2api.db import api as db_api
from ec2api import exception
from ec2api.i18n import _
from ec2api.i18n import _, _LE
LOG = logging.getLogger(__name__)
ec2_opts = [
cfg.BoolOpt('ec2_private_dns_show_ip',
@ -72,24 +74,99 @@ def run_instances(context, image_id, min_count, max_count,
_check_min_max_count(min_count, max_count)
if client_token:
idempotent_run = _get_idempotent_run(context, client_token)
if idempotent_run:
return idempotent_run
reservations = describe_instances(context,
filter=[{'name': 'client-token',
'value': [client_token]}])
if reservations['reservationSet']:
if len(reservations['reservationSet']) > 1:
LOG.error(_LE('describe_instances has returned %s '
'reservations, but 1 is expected.') %
len(reservations['reservationSet']))
LOG.error(_LE('Requested instances client token: %s') %
client_token)
LOG.error(_LE('Result: %s') % reservations)
return reservations['reservationSet'][0]
os_image, os_kernel_id, os_ramdisk_id = _parse_image_parameters(
context, image_id, kernel_id, ramdisk_id)
nova = clients.nova(context)
try:
os_flavor = next(f for f in nova.flavors.list()
if f.name == instance_type)
except StopIteration:
raise exception.InvalidParameterValue(value=instance_type,
parameter='InstanceType')
bdm = _parse_block_device_mapping(context, block_device_mapping)
availability_zone = (placement or {}).get('availability_zone')
if user_data:
user_data = base64.b64decode(user_data)
return instance_engine.run_instances(
context, image_id, min_count, max_count,
key_name, security_group_id,
security_group, user_data, instance_type,
placement, kernel_id, ramdisk_id,
block_device_mapping, monitoring,
subnet_id, disable_api_termination,
instance_initiated_shutdown_behavior,
private_ip_address, client_token,
network_interface, iam_instance_profile,
ebs_optimized)
vpc_id, launch_context = instance_engine.get_vpc_and_build_launch_context(
context, security_group,
subnet_id, private_ip_address, security_group_id, network_interface,
multiple_instances=max_count > 1)
ec2_reservation_id = _generate_reservation_id()
instance_ids = []
with common.OnCrashCleaner() as cleaner:
# NOTE(ft): create Neutron's ports manually and run instances one
# by one to have a chance to:
# process individual network interface options like security_group
# or private_ip_addresses (Nova's create_instances receives only
# one fixed_ip for subnet)
# set dhcp options to port
# add corresponding OS ids of network interfaces to our DB
# TODO(ft): we should lock created network interfaces to prevent
# their usage or deleting
# TODO(ft): do correct error messages on create failures. For
# example, overlimit, ip lack, ip overlapping, etc
for launch_index in range(max_count):
if launch_index >= min_count:
cleaner.approveChanges()
extra_params = (
instance_engine.get_launch_extra_parameters(
context, cleaner, launch_context))
os_instance = nova.servers.create(
'%s-%s' % (ec2_reservation_id, launch_index),
os_image.id, os_flavor,
min_count=1, max_count=1,
kernel_id=os_kernel_id, ramdisk_id=os_ramdisk_id,
availability_zone=availability_zone,
block_device_mapping=bdm,
key_name=key_name, userdata=user_data,
**extra_params)
cleaner.addCleanup(nova.servers.delete, os_instance.id)
instance = {'os_id': os_instance.id,
'vpc_id': vpc_id,
'reservation_id': ec2_reservation_id,
'launch_index': launch_index}
if client_token:
instance['client_token'] = client_token
instance = db_api.add_item(context, 'i', instance)
cleaner.addCleanup(db_api.delete_item, context, instance['id'])
instance_ids.append(instance['id'])
nova.servers.update(os_instance, name=instance['id'])
instance_engine.post_launch_action(
context, cleaner, launch_context, instance['id'])
ec2_reservations = describe_instances(context, instance_ids)
reservation_count = len(ec2_reservations['reservationSet'])
if reservation_count != 1:
LOG.error(_LE('describe_instances has returned %s reservations, '
'but 1 is expected.') % reservation_count)
LOG.error(_LE('Requested instances IDs: %s') % instance_ids)
LOG.error(_LE('Result: %s') % ec2_reservations)
return (ec2_reservations['reservationSet'][0]
if reservation_count else None)
def terminate_instances(context, instance_id):
@ -229,9 +306,9 @@ class ReservationDescriber(common.NonOpenstackItemsDescriber):
if i['instanceId'] in self.suitable_instances]
if not formatted_instances:
return None
return _format_reservation_body(self.context, reservation,
formatted_instances,
self.os_groups.get(reservation['id']))
return _format_reservation(self.context, reservation,
formatted_instances,
self.os_groups.get(reservation['id']))
def get_db_items(self):
return self.reservations
@ -380,31 +457,7 @@ def describe_instance_attribute(context, instance_id, attribute):
return result
def _get_idempotent_run(context, client_token):
# TODO(ft): implement search in DB layer
instances = dict((i['os_id'], i) for i in db_api.get_items(context, 'i')
if i.get('client_token') == client_token)
if not instances:
return
nova = clients.nova(ec2_context.get_os_admin_context())
os_instances = _get_os_instances_by_instances(context, instances.values(),
nova=nova)
instances_info = []
instance_ids = []
for os_instance in os_instances:
instance = instances[os_instance.id]
instances_info.append((instance, os_instance,))
instance_ids.append(instance['id'])
if not instances_info:
return
ec2_network_interfaces = (
instance_engine.get_ec2_network_interfaces(context, instance_ids))
return _format_reservation(context, instance['reservation_id'],
instances_info, ec2_network_interfaces)
def _format_reservation_body(context, reservation, formatted_instances,
os_groups):
def _format_reservation(context, reservation, formatted_instances, os_groups):
return {
'reservationId': reservation['id'],
'ownerId': reservation['owner_id'],
@ -415,23 +468,6 @@ def _format_reservation_body(context, reservation, formatted_instances,
}
def _format_reservation(context, reservation_id, instances_info,
ec2_network_interfaces, image_ids={}):
formatted_instances = []
for (instance, os_instance) in instances_info:
ec2_instance = _format_instance(
context, instance, os_instance,
ec2_network_interfaces.get(instance['id']), image_ids)
formatted_instances.append(ec2_instance)
reservation = {'id': reservation_id,
'owner_id': os_instance.tenant_id}
return _format_reservation_body(
context, reservation, formatted_instances,
(None if instance['vpc_id'] else
getattr(os_instance, 'security_groups', [])))
def _format_instance(context, instance, os_instance, ec2_network_interfaces,
image_ids, volumes=None, os_volumes=None):
ec2_instance = {
@ -588,7 +624,7 @@ def _parse_image_parameters(context, image_id, kernel_id, ramdisk_id):
return os_image, os_kernel_id, os_ramdisk_id
def _parse_block_device_mapping(context, block_device_mapping, os_image):
def _parse_block_device_mapping(context, block_device_mapping):
# TODO(ft): check block_device_mapping structure
bdm = {}
for args_bd in (block_device_mapping or []):
@ -696,139 +732,64 @@ def _generate_reservation_id():
class InstanceEngineNeutron(object):
def run_instances(self, context, image_id, min_count, max_count,
key_name=None, security_group_id=None,
security_group=None, user_data=None, instance_type=None,
placement=None, kernel_id=None, ramdisk_id=None,
block_device_mapping=None, monitoring=None,
subnet_id=None, disable_api_termination=None,
instance_initiated_shutdown_behavior=None,
private_ip_address=None, client_token=None,
network_interface=None, iam_instance_profile=None,
ebs_optimized=None):
os_image, os_kernel_id, os_ramdisk_id = _parse_image_parameters(
context, image_id, kernel_id, ramdisk_id)
nova = clients.nova(context)
os_flavor = next((f for f in nova.flavors.list()
if f.name == instance_type), None)
if not os_flavor:
raise exception.InvalidParameterValue(value=instance_type,
parameter='InstanceType')
bdm = _parse_block_device_mapping(context, block_device_mapping,
os_image)
def get_vpc_and_build_launch_context(
self, context, security_group,
subnet_id, private_ip_address, security_group_id,
network_interface, multiple_instances):
# TODO(ft): support auto_assign_floating_ip
(security_groups_names,
vpc_network_parameters) = self.merge_network_interface_parameters(
vpc_network_parameters = self.merge_network_interface_parameters(
security_group,
subnet_id, private_ip_address, security_group_id,
network_interface)
self.check_network_interface_parameters(vpc_network_parameters,
max_count > 1)
multiple_instances)
(vpc_id, network_data) = self.parse_network_interface_parameters(
context, vpc_network_parameters)
launch_context = {'vpc_id': vpc_id,
'network_data': network_data,
'security_groups': security_group}
# NOTE(ft): workaround for Launchpad Bug #1384347 in Icehouse
if not security_groups_names and vpc_network_parameters:
security_groups_names = self.get_vpc_default_security_group_id(
context, vpc_id)
if not security_group and vpc_network_parameters:
launch_context['security_groups'] = (
self.get_vpc_default_security_group_id(context, vpc_id))
neutron = clients.neutron(context)
if not vpc_id:
ec2_classic_nics = [
neutron = clients.neutron(context)
launch_context['ec2_classic_nics'] = [
{'net-id': self.get_ec2_classic_os_network(context,
neutron)['id']}]
instance_ids = []
instances_info = []
ec2_reservation_id = _generate_reservation_id()
return vpc_id, launch_context
with common.OnCrashCleaner() as cleaner:
# NOTE(ft): create Neutron's ports manually and run instances one
# by one to have a chance to:
# process individual network interface options like security_group
# or private_ip_addresses (Nova's create_instances receives only
# one fixed_ip for subnet)
# set dhcp options to port
# add corresponding OS ids of network interfaces to our DB
# TODO(ft): we should lock created network interfaces to prevent
# their usage or deleting
def get_launch_extra_parameters(self, context, cleaner, launch_context):
if 'ec2_classic_nics' in launch_context:
nics = launch_context['ec2_classic_nics']
else:
network_data = launch_context['network_data']
self.create_network_interfaces(context, cleaner, network_data)
nics = [{'port-id': data['network_interface']['os_id']}
for data in network_data]
return {'security_groups': launch_context['security_groups'],
'nics': nics}
# TODO(ft): do correct error messages on create failures. For
# example, overlimit, ip lack, ip overlapping, etc
for launch_index in range(max_count):
if launch_index >= min_count:
cleaner.approveChanges()
self.create_network_interfaces(context, cleaner, network_data)
nics = ([{'port-id': data['network_interface']['os_id']}
for data in network_data]
if vpc_id else
ec2_classic_nics)
os_instance = nova.servers.create(
'%s-%s' % (ec2_reservation_id, launch_index),
os_image.id, os_flavor,
min_count=1, max_count=1,
kernel_id=os_kernel_id, ramdisk_id=os_ramdisk_id,
availability_zone=(
(placement or {}).get('availability_zone')),
block_device_mapping=bdm,
security_groups=security_groups_names,
nics=nics,
key_name=key_name, userdata=user_data)
cleaner.addCleanup(nova.servers.delete, os_instance.id)
for data in network_data:
if data.get('detach_on_crash'):
cleaner.addCleanup(neutron.update_port,
data['network_interface']['os_id'],
{'port': {'device_id': '',
'device_owner': ''}})
instance = {'os_id': os_instance.id,
'vpc_id': vpc_id,
'reservation_id': ec2_reservation_id,
'launch_index': launch_index}
if client_token:
instance['client_token'] = client_token
instance = db_api.add_item(context, 'i', instance)
cleaner.addCleanup(db_api.delete_item, context, instance['id'])
instance_ids.append(instance['id'])
nova.servers.update(os_instance, name=instance['id'])
for data in network_data:
# TODO(ft): implement update items in DB layer to prevent
# record by record modification
# Alternatively a create_network_interface sub-function can
# set attach_time at once
network_interface_api._attach_network_interface_item(
context, data['network_interface'], instance['id'],
data['device_index'],
delete_on_termination=data['delete_on_termination'])
cleaner.addCleanup(
network_interface_api._detach_network_interface_item,
context, data['network_interface'])
instances_info.append((instance, os_instance))
# NOTE(ft): we don't reuse network interface objects received from
# create_network_interfaces because they don't contain attachment info
ec2_network_interfaces = (self.get_ec2_network_interfaces(
context, instance_ids=instance_ids))
# NOTE(ft): since os_instance is created with regular Nova client,
# it doesn't contain enough info to get an instance in EC2 format
# completely, nevertheless we use it to get rid of additional requests
# and reduce code complexity
return _format_reservation(context, ec2_reservation_id, instances_info,
ec2_network_interfaces,
image_ids={os_image.id: image_id})
def post_launch_action(self, context, cleaner, launch_context,
instance_id):
for data in launch_context['network_data']:
# TODO(ft): implement update items in DB layer to prevent
# record by record modification
# Alternatively a create_network_interface sub-function can
# set attach_time at once
network_interface_api._attach_network_interface_item(
context, data['network_interface'], instance_id,
data['device_index'],
delete_on_termination=data['delete_on_termination'])
cleaner.addCleanup(
network_interface_api._detach_network_interface_item,
context, data['network_interface'])
def get_ec2_network_interfaces(self, context, instance_ids=None):
# NOTE(ft): we would be glad to use filters with this describe
@ -895,7 +856,7 @@ class InstanceEngineNeutron(object):
param['private_ip_address'] = private_ip_address
if security_group_ids:
param['security_group_id'] = security_group_ids
return None, [param]
return [param]
elif private_ip_address:
msg = _('Specifying an IP address is only valid for VPC instances '
'and thus requires a subnet in which to launch')
@ -905,7 +866,7 @@ class InstanceEngineNeutron(object):
raise exception.InvalidParameterCombination(msg)
else:
# NOTE(ft): only one of this variables is not empty
return security_group_names, network_interfaces
return network_interfaces
def check_network_interface_parameters(self, params, multiple_instances):
# NOTE(ft): we ignore associate_public_ip_address
@ -970,7 +931,6 @@ class InstanceEngineNeutron(object):
network_interface_ids.add(ec2_eni_id)
network_data.append({'device_index': param['device_index'],
'network_interface': network_interface,
'detach_on_crash': True,
'delete_on_termination': False})
else:
subnet = ec2utils.get_db_item(context, param['subnet_id'],
@ -1055,70 +1015,20 @@ class InstanceEngineNeutron(object):
class InstanceEngineNova(object):
def run_instances(self, context, image_id, min_count, max_count,
key_name=None, security_group_id=None,
security_group=None, user_data=None, instance_type=None,
placement=None, kernel_id=None, ramdisk_id=None,
block_device_mapping=None, monitoring=None,
subnet_id=None, disable_api_termination=None,
instance_initiated_shutdown_behavior=None,
private_ip_address=None, client_token=None,
network_interface=None, iam_instance_profile=None,
ebs_optimized=None):
os_image, os_kernel_id, os_ramdisk_id = _parse_image_parameters(
context, image_id, kernel_id, ramdisk_id)
def get_vpc_and_build_launch_context(
self, context, security_group,
subnet_id, private_ip_address, security_group_id,
network_interface, multiple_instances):
# TODO(ft): check emptiness of vpc related parameters
nova = clients.nova(context)
os_flavor = next((f for f in nova.flavors.list()
if f.name == instance_type), None)
if not os_flavor:
raise exception.InvalidParameterValue(value=instance_type,
parameter='InstanceType')
return None, {'security_groups': security_group}
bdm = _parse_block_device_mapping(context, block_device_mapping,
os_image)
def get_launch_extra_parameters(self, context, cleaner, launch_context):
return {'security_groups': launch_context['security_groups']}
# TODO(ft): support auto_assign_floating_ip
instances_info = []
ec2_reservation_id = _generate_reservation_id()
# TODO(ft): do correct error messages on create failures. For
# example, overlimit, ip lack, ip overlapping, etc
with common.OnCrashCleaner() as cleaner:
for index in range(max_count):
if index >= min_count:
cleaner.approveChanges()
os_instance = nova.servers.create(
'%s-%s' % (ec2_reservation_id, index),
os_image.id, os_flavor,
min_count=1, max_count=1,
kernel_id=os_kernel_id, ramdisk_id=os_ramdisk_id,
availability_zone=(
placement or {}).get('availability_zone'),
block_device_mapping=bdm,
security_groups=security_group,
key_name=key_name, userdata=user_data)
cleaner.addCleanup(nova.servers.delete, os_instance.id)
instance = {'os_id': os_instance.id,
'reservation_id': ec2_reservation_id,
'launch_index': index}
if client_token:
instance['client_token'] = client_token
instance = db_api.add_item(context, 'i', instance)
cleaner.addCleanup(db_api.delete_item, context, instance['id'])
nova.servers.update(os_instance, name=instance['id'])
instances_info.append((instance, os_instance))
# NOTE(ft): since os_instance is created with regular Nova client,
# it doesn't contain enough info to get an instance in EC2 format
# completely, nevertheless we use it to get rid of additional requests
# and reduce code complexity
return _format_reservation(context, ec2_reservation_id, instances_info,
{}, image_ids={os_image.id: image_id})
def post_launch_action(self, context, cleaner, launch_context,
instance_id):
pass
def get_ec2_network_interfaces(self, context, instance_ids=None):
return {}

View File

@ -79,12 +79,11 @@ class InstanceTestCase(base.ApiTestCase):
self.nova.flavors.get.return_value = self.fake_flavor
self.nova.flavors.list.return_value = [self.fake_flavor]
@mock.patch('ec2api.api.instance.InstanceEngineNeutron.'
'get_ec2_network_interfaces')
@mock.patch('ec2api.api.instance.describe_instances')
@mock.patch('ec2api.api.instance.InstanceEngineNeutron.'
'get_vpc_default_security_group_id')
def test_run_instances(self, get_vpc_default_security_group_id,
get_ec2_network_interfaces):
describe_instances):
"""Run instance with various network interface settings."""
instance_api.instance_engine = (
instance_api.InstanceEngineNeutron())
@ -130,8 +129,8 @@ class InstanceTestCase(base.ApiTestCase):
reservation_id=fakes.ID_EC2_RESERVATION_1),
{'privateDnsName': None},
['rootDeviceType', 'rootDeviceName'])])
get_ec2_network_interfaces.return_value = {
fakes.ID_EC2_INSTANCE_1: [eni]}
describe_instances.return_value = {
'reservationSet': [expected_reservation]}
params.update({'ImageId': fakes.ID_EC2_IMAGE_1,
'InstanceType': 'fake_flavor',
@ -161,13 +160,13 @@ class InstanceTestCase(base.ApiTestCase):
mock.ANY, fakes.DB_NETWORK_INTERFACE_1,
fakes.ID_EC2_INSTANCE_1, 0,
delete_on_termination=delete_port_on_termination))
get_ec2_network_interfaces.assert_called_once_with(
mock.ANY, instance_ids=[fakes.ID_EC2_INSTANCE_1])
describe_instances.assert_called_once_with(
mock.ANY, [fakes.ID_EC2_INSTANCE_1])
self.network_interface_api.reset_mock()
self.nova.servers.reset_mock()
self.db_api.reset_mock()
get_ec2_network_interfaces.reset_mock()
describe_instances.reset_mock()
do_check({'SubnetId': fakes.ID_EC2_SUBNET_1},
create_network_interface_kwargs={})
@ -210,13 +209,12 @@ class InstanceTestCase(base.ApiTestCase):
'NetworkInterface.1.NetworkInterfaceId': (
fakes.ID_EC2_NETWORK_INTERFACE_1)})
@mock.patch('ec2api.api.instance.InstanceEngineNeutron.'
'get_ec2_network_interfaces')
@mock.patch('ec2api.api.instance.describe_instances')
@mock.patch('ec2api.api.instance.InstanceEngineNeutron.'
'get_vpc_default_security_group_id')
def test_run_instances_multiple_networks(self,
get_vpc_default_security_group_id,
get_ec2_network_interfaces):
describe_instances):
"""Run 2 instances at once on 2 subnets in all combinations."""
instance_api.instance_engine = (
instance_api.InstanceEngineNeutron())
@ -225,11 +223,6 @@ class InstanceTestCase(base.ApiTestCase):
self.glance.images.get.return_value = fakes.OSImage(fakes.OS_IMAGE_1)
get_vpc_default_security_group_id.return_value = None
get_ec2_network_interfaces.return_value = dict(
(ec2_instance_id, list(eni_pair))
for ec2_instance_id, eni_pair in zip(
self.IDS_EC2_INSTANCE,
zip(*[iter(self.EC2_ATTACHED_ENIS)] * 2)))
ec2_instances = [
tools.patch_dict(
fakes.gen_ec2_instance(
@ -243,6 +236,7 @@ class InstanceTestCase(base.ApiTestCase):
zip(*[iter(self.EC2_ATTACHED_ENIS)] * 2)))]
ec2_reservation = fakes.gen_ec2_reservation(fakes.ID_EC2_RESERVATION_1,
ec2_instances)
describe_instances.return_value = {'reservationSet': [ec2_reservation]}
self.set_mock_db_items(
fakes.DB_IMAGE_1, fakes.DB_SUBNET_1, fakes.DB_SUBNET_2,
@ -305,11 +299,11 @@ class InstanceTestCase(base.ApiTestCase):
for db_instance in self.DB_INSTANCES])
@mock.patch('ec2api.api.instance._parse_block_device_mapping')
@mock.patch('ec2api.api.instance._format_reservation')
@mock.patch('ec2api.api.instance.describe_instances')
@mock.patch('ec2api.api.instance.InstanceEngineNeutron.'
'get_ec2_classic_os_network')
def test_run_instances_other_parameters(self, get_ec2_classic_os_network,
format_reservation,
describe_instances,
parse_block_device_mapping):
self.set_mock_db_items(
fakes.DB_IMAGE_1, fakes.DB_IMAGE_AKI_1, fakes.DB_IMAGE_ARI_1)
@ -319,13 +313,15 @@ class InstanceTestCase(base.ApiTestCase):
fakes.ID_OS_IMAGE_AKI_1: fakes.OSImage(fakes.OS_IMAGE_AKI_1),
fakes.ID_OS_IMAGE_ARI_1: fakes.OSImage(fakes.OS_IMAGE_ARI_1)}))
get_ec2_classic_os_network.return_value = {'id': fakes.random_os_id()}
format_reservation.return_value = {}
user_data = base64.b64decode(fakes.USER_DATA_INSTANCE_2)
parse_block_device_mapping.return_value = 'fake_bdm'
def do_check(engine, extra_kwargs={}, extra_db_instance={}):
instance_api.instance_engine = engine
user_data = base64.b64decode(fakes.USER_DATA_INSTANCE_2)
describe_instances.side_effect = [
{'reservationSet': []},
{'reservationSet': [{'foo': 'bar'}]}]
self.execute(
'RunInstances',
@ -352,6 +348,7 @@ class InstanceTestCase(base.ApiTestCase):
**extra_kwargs)
self.nova.servers.reset_mock()
db_instance = {'os_id': mock.ANY,
'vpc_id': None,
'reservation_id': mock.ANY,
'launch_index': 0,
'client_token': 'fake_client_token'}
@ -363,8 +360,7 @@ class InstanceTestCase(base.ApiTestCase):
mock.ANY,
[{'device_name': '/dev/vdd',
'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_1,
'delete_on_termination': False}}],
fakes.OSImage(fakes.OS_IMAGE_1))
'delete_on_termination': False}}])
parse_block_device_mapping.reset_mock()
do_check(
@ -376,27 +372,13 @@ class InstanceTestCase(base.ApiTestCase):
extra_db_instance={'vpc_id': None})
do_check(instance_api.InstanceEngineNova())
@mock.patch('ec2api.api.instance._format_reservation')
@mock.patch('ec2api.api.instance._get_os_instances_by_instances')
def test_idempotent_run(self, get_os_instances_by_instances,
format_reservation):
instance_engine = mock.MagicMock()
instance_api.instance_engine = instance_engine
get_ec2_network_interfaces = instance_engine.get_ec2_network_interfaces
instances = [{'id': fakes.random_ec2_id('i'),
'os_id': fakes.random_os_id(),
'reservation_id': fakes.random_ec2_id('r'),
'client_token': 'client-token-%s' % ind}
for ind in range(3)]
os_instances = [fakes.OSInstance_full({'id': inst['os_id']})
for inst in instances]
format_reservation.return_value = {'key': 'value'}
@mock.patch('ec2api.api.instance.describe_instances')
def test_idempotent_run(self, describe_instances):
self.set_mock_db_items()
# NOTE(ft): check select corresponding instance by client_token
self.set_mock_db_items(instances[0], instances[1])
get_os_instances_by_instances.return_value = [os_instances[1]]
get_ec2_network_interfaces.return_value = 'ec2_network_interfaces'
describe_instances.return_value = {
'reservationSet': [{'key': 'value'}]}
resp = self.execute('RunInstances',
{'MinCount': '1', 'MaxCount': '1',
@ -404,64 +386,20 @@ class InstanceTestCase(base.ApiTestCase):
'InstanceType': 'fake_flavor',
'ClientToken': 'client-token-1'})
self.assertEqual({'key': 'value'}, resp)
format_reservation.assert_called_once_with(
mock.ANY, instances[1]['reservation_id'],
[(instances[1], os_instances[1])],
'ec2_network_interfaces')
get_os_instances_by_instances.assert_called_once_with(
mock.ANY, instances[1:2], nova=self.nova_admin)
get_ec2_network_interfaces.assert_called_once_with(
mock.ANY, [instances[1]['id']])
describe_instances.assert_called_once_with(
mock.ANY, filter=[{'name': 'client-token',
'value': ['client-token-1']}])
# NOTE(ft): check pass to general run_instances logic if no
# corresponding client_token is found
instance_engine.run_instances.return_value = {}
resp = self.execute('RunInstances',
{'MinCount': '1', 'MaxCount': '1',
'ImageId': fakes.ID_EC2_IMAGE_1,
'InstanceType': 'fake_flavor',
'ClientToken': 'client-token-2'})
self.assertTrue(instance_engine.run_instances.called)
describe_instances.return_value = {'reservationSet': []}
# NOTE(ft): check pass to general run_instances logic if no more
# corresponding OS instance exists
instance_engine.reset_mock()
get_os_instances_by_instances.return_value = []
resp = self.execute('RunInstances',
{'MinCount': '1', 'MaxCount': '1',
'ImageId': fakes.ID_EC2_IMAGE_1,
'InstanceType': 'fake_flavor',
'ClientToken': 'client-token-1'})
self.assertTrue(instance_engine.run_instances.called)
# NOTE(ft): check case for several instances with same client_token,
# but one no more exists in OS
format_reservation.reset_mock()
get_os_instances_by_instances.reset_mock()
instance_engine.reset_mock()
for inst in instances:
inst['reservation_id'] = instances[0]['reservation_id']
inst['client_token'] = 'client-token'
self.set_mock_db_items(*instances)
get_os_instances_by_instances.return_value = [os_instances[0],
os_instances[2]]
get_ec2_network_interfaces.return_value = 'ec2_network_interfaces'
resp = self.execute('RunInstances',
{'MinCount': '1', 'MaxCount': '1',
'ImageId': fakes.ID_EC2_IMAGE_1,
'InstanceType': 'fake_flavor',
'ClientToken': 'client-token'})
self.assertEqual({'key': 'value'}, resp)
format_reservation.assert_called_once_with(
mock.ANY, instances[0]['reservation_id'],
[(instances[0], os_instances[0]),
(instances[2], os_instances[2])],
'ec2_network_interfaces')
self.assert_any_call(get_os_instances_by_instances, mock.ANY,
instances, nova=self.nova_admin)
get_ec2_network_interfaces.assert_called_once_with(
mock.ANY, [instances[0]['id'], instances[2]['id']])
self.assert_execution_error(
'InvalidAMIID.NotFound', 'RunInstances',
{'MinCount': '1', 'MaxCount': '1',
'ImageId': fakes.ID_EC2_IMAGE_1,
'InstanceType': 'fake_flavor',
'ClientToken': 'client-token-2'})
def test_run_instances_rollback(self):
instance_api.instance_engine = (
@ -482,7 +420,7 @@ class InstanceTestCase(base.ApiTestCase):
_attach_network_interface_item.side_effect) = Exception()
@tools.screen_unexpected_exception_logs
def do_check(params, new_port=True, delete_on_termination=None):
def do_check(params, new_port=True):
mock_manager = mock.MagicMock()
mock_manager.attach_mock(self.network_interface_api,
'network_interface_api')
@ -495,15 +433,7 @@ class InstanceTestCase(base.ApiTestCase):
self.assert_execution_error(
self.ANY_EXECUTE_ERROR, 'RunInstances', params)
calls = []
if not new_port:
calls.append(
mock.call.neutron.update_port(
fakes.ID_OS_PORT_1,
{'port': {'device_id': '',
'device_owner': ''}}))
calls.append(
mock.call.nova_servers.delete(fakes.ID_OS_INSTANCE_1))
calls = [mock.call.nova_servers.delete(fakes.ID_OS_INSTANCE_1)]
if new_port:
calls.append(
mock.call.network_interface_api.delete_network_interface(
@ -525,19 +455,15 @@ class InstanceTestCase(base.ApiTestCase):
do_check({'NetworkInterface.1.DeviceIndex': '0',
'NetworkInterface.1.SubnetId': fakes.ID_EC2_SUBNET_1,
'NetworkInterface.1.DeleteOnTermination': 'False'},
delete_on_termination=False)
'NetworkInterface.1.DeleteOnTermination': 'False'})
do_check({'NetworkInterface.1.DeviceIndex': '0',
'NetworkInterface.1.NetworkInterfaceId': (
fakes.ID_EC2_NETWORK_INTERFACE_1)},
new_port=False)
@mock.patch('ec2api.api.instance.InstanceEngineNeutron.'
'get_ec2_network_interfaces')
@mock.patch('ec2api.api.instance._format_reservation')
def test_run_instances_multiply_rollback(self, format_reservation,
get_ec2_network_interfaces):
@mock.patch('ec2api.api.instance.describe_instances')
def test_run_instances_multiply_rollback(self, describe_instances):
instances = [{'id': fakes.random_ec2_id('i'),
'os_id': fakes.random_os_id()}
for dummy in range(3)]
@ -553,7 +479,6 @@ class InstanceTestCase(base.ApiTestCase):
self.glance.images.get.return_value = fakes.OSImage(fakes.OS_IMAGE_1)
self.utils_generate_uid.return_value = fakes.ID_EC2_RESERVATION_1
get_ec2_network_interfaces.return_value = []
def do_check(engine):
instance_api.instance_engine = engine
@ -563,24 +488,19 @@ class InstanceTestCase(base.ApiTestCase):
for eni in network_interfaces]
self.db_api.add_item.side_effect = instances
self.nova.servers.create.side_effect = os_instances
format_reservation.side_effect = (
lambda _context, r_id, instance_info, *args, **kwargs: (
{'reservationId': r_id,
'instancesSet': [
{'instanceId': inst['id']}
for inst, _os_inst in instance_info]}))
expected_reservation = {
'reservationId': fakes.ID_EC2_RESERVATION_1,
'instancesSet': [{'instanceId': inst['id']}
for inst in instances[:2]]}
describe_instances.return_value = {
'reservationSet': [expected_reservation]}
resp = self.execute('RunInstances',
{'ImageId': fakes.ID_EC2_IMAGE_1,
'InstanceType': 'fake_flavor',
'MinCount': '2', 'MaxCount': '3',
'SubnetId': fakes.ID_EC2_SUBNET_1})
self.assertThat(resp,
matchers.DictMatches(
{'reservationId': fakes.ID_EC2_RESERVATION_1,
'instancesSet': [
{'instanceId': inst['id']}
for inst in instances[:2]]}))
self.assertThat(resp, matchers.DictMatches(expected_reservation))
self.nova.servers.delete.assert_called_once_with(
instances[2]['os_id'])
@ -626,10 +546,7 @@ class InstanceTestCase(base.ApiTestCase):
"""Terminate 2 instances in one request."""
instance_api.instance_engine = (
instance_api.InstanceEngineNeutron())
self.set_mock_db_items(
fakes.DB_INSTANCE_1, fakes.DB_INSTANCE_2,
fakes.DB_NETWORK_INTERFACE_1, fakes.DB_NETWORK_INTERFACE_2,
fakes.DB_ADDRESS_1, fakes.DB_ADDRESS_2)
self.set_mock_db_items(fakes.DB_INSTANCE_1, fakes.DB_INSTANCE_2)
os_instances = [fakes.OSInstance(fakes.OS_INSTANCE_1),
fakes.OSInstance(fakes.OS_INSTANCE_2)]
self.nova.servers.get.side_effect = os_instances
@ -650,13 +567,9 @@ class InstanceTestCase(base.ApiTestCase):
fake_state_change),
tools.update_dict({'instanceId': fakes.ID_EC2_INSTANCE_2},
fake_state_change)]}))
self.db_api.get_items_by_ids.assert_called_once_with(
mock.ANY, set([fakes.ID_EC2_INSTANCE_1, fakes.ID_EC2_INSTANCE_2]))
self.assertEqual(2, self.nova.servers.get.call_count)
self.nova.servers.get.assert_any_call(fakes.ID_OS_INSTANCE_1)
self.nova.servers.get.assert_any_call(fakes.ID_OS_INSTANCE_2)
self.assertEqual(
0, self.address_api.dissassociate_address_item.call_count)
self.assertFalse(self.db_api.delete_item.called)
self.assertEqual(2, os_instance_delete.call_count)
self.assertEqual(2, os_instance_get.call_count)
@ -684,27 +597,15 @@ class InstanceTestCase(base.ApiTestCase):
lambda ec2_id: fakes.OSInstance({'id': ec2_id,
'vm_state': 'active'}))
def do_check(mock_eni_list=[]):
self.set_mock_db_items(self.DB_FAKE_ENI,
*(self.DB_INSTANCES + mock_eni_list))
self.set_mock_db_items(*self.DB_INSTANCES)
resp = self.execute('TerminateInstances',
{'InstanceId.1': fakes.ID_EC2_INSTANCE_1,
'InstanceId.2': fakes.ID_EC2_INSTANCE_2})
resp = self.execute('TerminateInstances',
{'InstanceId.1': fakes.ID_EC2_INSTANCE_1,
'InstanceId.2': fakes.ID_EC2_INSTANCE_2})
self.assertThat(
resp, matchers.DictMatches(ec2_terminate_instances_result))
self.assertFalse(self.db_api.delete_item.called)
self.db_api.delete_item.reset_mock()
do_check(
mock_eni_list=[
self.DB_ATTACHED_ENIS[0], self.DB_ATTACHED_ENIS[1],
self.DB_ATTACHED_ENIS[2], self.DB_DETACHED_ENIS[3]])
do_check(
mock_eni_list=[self.DB_ATTACHED_ENIS[1]])
self.assertThat(
resp, matchers.DictMatches(ec2_terminate_instances_result))
self.assertFalse(self.db_api.delete_item.called)
def test_terminate_instances_invalid_parameters(self):
self.assert_execution_error(
@ -1150,24 +1051,11 @@ class InstanceTestCase(base.ApiTestCase):
self.IDS_OS_INSTANCE = ids_os_instance
self.IDS_EC2_INSTANCE = ids_ec2_instance
self.IDS_EC2_SUBNET_BY_PORT = ids_ec2_subnet_by_port
self.DB_ATTACHED_ENIS = db_attached_enis
self.DB_DETACHED_ENIS = db_detached_enis
self.EC2_ATTACHED_ENIS = ec2_attached_enis
self.EC2_DETACHED_ENIS = ec2_detached_enis
self.DB_INSTANCES = db_instances
# NOTE(ft): additional fake data to check filtering, etc
self.DB_FAKE_ENI = fakes.gen_db_network_interface(
fakes.random_ec2_id('eni'), fakes.random_os_id(),
fakes.ID_EC2_VPC_1, fakes.ID_EC2_SUBNET_2,
'fake_ip')
ec2_fake_eni = fakes.gen_ec2_network_interface(
self.DB_FAKE_ENI['id'],
fakes.EC2_SUBNET_2, ['fake_ip'])
self.OS_FAKE_PORT = fakes.gen_os_port(
fakes.random_os_id(), ec2_fake_eni,
fakes.ID_OS_SUBNET_2, ['fake_ip'])
# TODO(ft): add tests for get_vpc_default_security_group_id,
@ -1227,41 +1115,40 @@ class InstancePrivateTestCase(test_base.BaseTestCase):
None, None, None, ['sg-1'], None)
self.assertEqual(
(None, [{'device_index': 0,
'subnet_id': 'subnet-1'}]),
([{'device_index': 0,
'subnet_id': 'subnet-1'}]),
engine.merge_network_interface_parameters(
None, 'subnet-1', None, None, None))
self.assertEqual(
(None, [{'device_index': 0,
'subnet_id': 'subnet-1',
'private_ip_address': '10.10.10.10'}]),
([{'device_index': 0,
'subnet_id': 'subnet-1',
'private_ip_address': '10.10.10.10'}]),
engine.merge_network_interface_parameters(
None, 'subnet-1', '10.10.10.10', None, None))
self.assertEqual(
(None, [{'device_index': 0,
'subnet_id': 'subnet-1',
'private_ip_address': '10.10.10.10',
'security_group_id': ['sg-1']}]),
([{'device_index': 0,
'subnet_id': 'subnet-1',
'private_ip_address': '10.10.10.10',
'security_group_id': ['sg-1']}]),
engine.merge_network_interface_parameters(
None, 'subnet-1', '10.10.10.10', ['sg-1'], None))
self.assertEqual(
(None, [{'device_index': 0,
'subnet_id': 'subnet-1',
'security_group_id': ['sg-1']}]),
([{'device_index': 0,
'subnet_id': 'subnet-1',
'security_group_id': ['sg-1']}]),
engine.merge_network_interface_parameters(
None, 'subnet-1', None, ['sg-1'], None))
self.assertEqual(
(None, [{'device_index': 0,
'subnet_id': 'subnet-1'}]),
([{'device_index': 0,
'subnet_id': 'subnet-1'}]),
engine.merge_network_interface_parameters(
None, None, None, None,
[{'device_index': 0, 'subnet_id': 'subnet-1'}]))
self.assertEqual(
(['default'], []),
engine.merge_network_interface_parameters(
['default'], None, None, None, None))
self.assertEqual((None, []),
self.assertEqual([],
engine.merge_network_interface_parameters(
['default'], None, None, None, None))
self.assertEqual([],
engine.merge_network_interface_parameters(
None, None, None, None, None))
@ -1374,7 +1261,6 @@ class InstancePrivateTestCase(test_base.BaseTestCase):
'delete_on_termination': False},
{'device_index': 1,
'network_interface': fakes.DB_NETWORK_INTERFACE_1,
'detach_on_crash': True,
'delete_on_termination': False}]),
resp)
resp = engine.parse_network_interface_parameters(
@ -1472,14 +1358,12 @@ class InstancePrivateTestCase(test_base.BaseTestCase):
@mock.patch('ec2api.db.api.IMPL')
def test_parse_block_device_mapping(self, db_api):
fake_context = mock.Mock(service_catalog=[{'type': 'fake'}])
os_image = fakes.OSImage(fakes.OS_IMAGE_1)
db_api.get_item_by_id.side_effect = tools.get_db_api_get_item_by_id(
fakes.DB_VOLUME_1, fakes.DB_VOLUME_2, fakes.DB_VOLUME_3,
fakes.DB_SNAPSHOT_1, fakes.DB_SNAPSHOT_2)
res = instance_api._parse_block_device_mapping(
fake_context, [], os_image)
res = instance_api._parse_block_device_mapping(fake_context, [])
self.assertEqual({}, res)
res = instance_api._parse_block_device_mapping(
@ -1495,8 +1379,7 @@ class InstancePrivateTestCase(test_base.BaseTestCase):
'ebs': {'snapshot_id': fakes.ID_EC2_VOLUME_2,
'delete_on_termination': True}},
{'device_name': '/dev/sdb1',
'ebs': {'volume_size': 55}}],
os_image)
'ebs': {'volume_size': 55}}])
self.assertThat(
res,
matchers.DictMatches(
@ -1717,10 +1600,8 @@ class InstancePrivateTestCase(test_base.BaseTestCase):
@mock.patch('ec2api.api.network_interface.delete_network_interface')
@mock.patch('ec2api.api.network_interface._detach_network_interface_item')
@mock.patch('ec2api.api.address._disassociate_address_item')
@mock.patch('ec2api.db.api.IMPL')
def test_remove_instances(self, db_api, disassociate_address_item,
detach_network_interface_item,
def test_remove_instances(self, db_api, detach_network_interface_item,
delete_network_interface):
fake_context = mock.Mock(service_catalog=[{'type': 'fake'}])
@ -1735,35 +1616,22 @@ class InstancePrivateTestCase(test_base.BaseTestCase):
[[{'id': fakes.random_ec2_id('i')}] * 2])))]
network_interfaces.extend({'id': fakes.random_ec2_id('eni')}
for dummy in range(2))
addresses = [{'id': fakes.random_ec2_id('eipalloc'),
'network_interface_id': eni['id']}
for eni in network_interfaces[::2]]
addresses.extend({'id': fakes.random_ec2_id('eipalloc')}
for dummy in range(2))
instances_to_remove = instances[:2] + [instances[3]]
network_interfaces_to_delete = network_interfaces[0:2]
network_interfaces_to_detach = network_interfaces[0:4]
db_api.get_items.side_effect = tools.get_db_api_get_items(
*(network_interfaces + addresses))
def check_calls():
for eni in network_interfaces_to_detach:
detach_network_interface_item.assert_any_call(fake_context,
eni)
for eni in network_interfaces_to_delete:
delete_network_interface.assert_any_call(fake_context,
eni['id'])
detach_network_interface_item.reset_mock()
db_api.reset_mock()
disassociate_address_item.reset_mock()
*network_interfaces)
instance_api._remove_instances(fake_context, instances_to_remove)
check_calls()
instance_api._remove_instances(fake_context, instances_to_remove)
check_calls()
for eni in network_interfaces_to_detach:
detach_network_interface_item.assert_any_call(fake_context,
eni)
for eni in network_interfaces_to_delete:
delete_network_interface.assert_any_call(fake_context,
eni['id'])
@mock.patch('cinderclient.client.Client')
def test_get_os_volumes(self, cinder):