summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorRyan Lane <rlane@wikimedia.org>2011-01-05 23:31:12 +0000
committerRyan Lane <rlane@wikimedia.org>2011-01-05 23:31:12 +0000
commit32fb1e59948f0bf1940712e36c671e8d74ede4d0 (patch)
tree248539077f2a3047da5e5570a585b36caa6cb14b /nova
parent6a068e60bf6d84acb7bbdf728c5f018d17d9f897 (diff)
parentceeb00765ed3fe03c0c2ad1583e1fe4b3421f0bf (diff)
Merge from trunk
Diffstat (limited to 'nova')
-rw-r--r--nova/adminclient.py7
-rw-r--r--nova/api/ec2/__init__.py55
-rw-r--r--nova/api/ec2/cloud.py315
-rw-r--r--nova/api/ec2/metadatarequesthandler.py4
-rw-r--r--nova/api/openstack/__init__.py25
-rw-r--r--nova/api/openstack/auth.py9
-rw-r--r--nova/api/openstack/images.py13
-rw-r--r--nova/api/openstack/ratelimiting/__init__.py6
-rw-r--r--nova/api/openstack/servers.py37
-rw-r--r--nova/compute/__init__.py15
-rw-r--r--nova/compute/api.py194
-rw-r--r--nova/compute/manager.py50
-rw-r--r--nova/db/api.py18
-rw-r--r--nova/db/sqlalchemy/__init__.py21
-rw-r--r--nova/db/sqlalchemy/api.py83
-rw-r--r--nova/db/sqlalchemy/models.py49
-rw-r--r--nova/db/sqlalchemy/session.py4
-rw-r--r--nova/flags.py8
-rw-r--r--nova/network/__init__.py15
-rw-r--r--nova/network/api.py87
-rw-r--r--nova/quota.py6
-rw-r--r--nova/scheduler/driver.py5
-rw-r--r--nova/scheduler/simple.py26
-rw-r--r--nova/service.py12
-rw-r--r--nova/tests/api/openstack/fakes.py9
-rw-r--r--nova/tests/api/openstack/test_images.py11
-rw-r--r--nova/tests/api/openstack/test_servers.py37
-rw-r--r--nova/tests/test_cloud.py8
-rw-r--r--nova/tests/test_compute.py18
-rw-r--r--nova/tests/test_quota.py10
-rw-r--r--nova/tests/test_scheduler.py90
-rw-r--r--nova/tests/test_service.py26
-rw-r--r--nova/tests/test_xenapi.py122
-rw-r--r--nova/tests/xenapi/stubs.py66
-rw-r--r--nova/virt/fake.py17
-rw-r--r--nova/virt/libvirt_conn.py10
-rw-r--r--nova/virt/xenapi/fake.py64
-rw-r--r--nova/virt/xenapi/vm_utils.py144
-rw-r--r--nova/virt/xenapi/vmops.py288
-rw-r--r--nova/virt/xenapi/volume_utils.py12
-rw-r--r--nova/virt/xenapi_conn.py44
-rw-r--r--nova/volume/__init__.py14
-rw-r--r--nova/volume/api.py101
43 files changed, 1571 insertions, 584 deletions
diff --git a/nova/adminclient.py b/nova/adminclient.py
index 6ae9f0c0f..b2609c8c4 100644
--- a/nova/adminclient.py
+++ b/nova/adminclient.py
@@ -23,12 +23,9 @@ import base64
import boto
import httplib
-from nova import flags
from boto.ec2.regioninfo import RegionInfo
-FLAGS = flags.FLAGS
-
DEFAULT_CLC_URL = 'http://127.0.0.1:8773'
DEFAULT_REGION = 'nova'
@@ -199,8 +196,8 @@ class NovaAdminClient(object):
self,
clc_url=DEFAULT_CLC_URL,
region=DEFAULT_REGION,
- access_key=FLAGS.aws_access_key_id,
- secret_key=FLAGS.aws_secret_access_key,
+ access_key=None,
+ secret_key=None,
**kwargs):
parts = self.split_clc_url(clc_url)
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index 51d33bcc6..aa3bfaeb4 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -294,10 +294,9 @@ class Executor(wsgi.Application):
args = req.environ['ec2.action_args']
api_request = apirequest.APIRequest(controller, action)
+ result = None
try:
result = api_request.send(context, **args)
- req.headers['Content-Type'] = 'text/xml'
- return result
except exception.ApiError as ex:
if ex.code:
@@ -307,6 +306,12 @@ class Executor(wsgi.Application):
# TODO(vish): do something more useful with unknown exceptions
except Exception as ex:
return self._error(req, type(ex).__name__, str(ex))
+ else:
+ resp = webob.Response()
+ resp.status = 200
+ resp.headers['Content-Type'] = 'text/xml'
+ resp.body = str(result)
+ return resp
def _error(self, req, code, message):
logging.error("%s: %s", code, message)
@@ -318,3 +323,49 @@ class Executor(wsgi.Application):
'<Message>%s</Message></Error></Errors>'
'<RequestID>?</RequestID></Response>' % (code, message))
return resp
+
+
+class Versions(wsgi.Application):
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ """Respond to a request for all EC2 versions."""
+ # available api versions
+ versions = [
+ '1.0',
+ '2007-01-19',
+ '2007-03-01',
+ '2007-08-29',
+ '2007-10-10',
+ '2007-12-15',
+ '2008-02-01',
+ '2008-09-01',
+ '2009-04-04',
+ ]
+ return ''.join('%s\n' % v for v in versions)
+
+
+def authenticate_factory(global_args, **local_args):
+ def authenticator(app):
+ return Authenticate(app)
+ return authenticator
+
+
+def router_factory(global_args, **local_args):
+ def router(app):
+ return Router(app)
+ return router
+
+
+def authorizer_factory(global_args, **local_args):
+ def authorizer(app):
+ return Authorizer(app)
+ return authorizer
+
+
+def executor_factory(global_args, **local_args):
+ return Executor()
+
+
+def versions_factory(global_args, **local_args):
+ return Versions()
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index e09261f00..0c0027287 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -31,19 +31,19 @@ import os
from nova import context
import IPy
+from nova import compute
from nova import crypto
from nova import db
from nova import exception
from nova import flags
-from nova import quota
+from nova import network
from nova import rpc
from nova import utils
-from nova.compute import api as compute_api
+from nova import volume
from nova.compute import instance_types
FLAGS = flags.FLAGS
-flags.DECLARE('storage_availability_zone', 'nova.volume.manager')
InvalidInputException = exception.InvalidInputException
@@ -71,16 +71,16 @@ def _gen_key(context, user_id, key_name):
return {'private_key': private_key, 'fingerprint': fingerprint}
-def ec2_id_to_internal_id(ec2_id):
- """Convert an ec2 ID (i-[base 36 number]) to an internal id (int)"""
+def ec2_id_to_id(ec2_id):
+ """Convert an ec2 ID (i-[base 36 number]) to an instance id (int)"""
return int(ec2_id[2:], 36)
-def internal_id_to_ec2_id(internal_id):
- """Convert an internal ID (int) to an ec2 ID (i-[base 36 number])"""
+def id_to_ec2_id(instance_id):
+ """Convert an instance ID (int) to an ec2 ID (i-[base 36 number])"""
digits = []
- while internal_id != 0:
- internal_id, remainder = divmod(internal_id, 36)
+ while instance_id != 0:
+ instance_id, remainder = divmod(instance_id, 36)
digits.append('0123456789abcdefghijklmnopqrstuvwxyz'[remainder])
return "i-%s" % ''.join(reversed(digits))
@@ -91,10 +91,11 @@ class CloudController(object):
sent to the other nodes.
"""
def __init__(self):
- self.network_manager = utils.import_object(FLAGS.network_manager)
self.image_service = utils.import_object(FLAGS.image_service)
- self.compute_api = compute_api.ComputeAPI(self.network_manager,
- self.image_service)
+ self.network_api = network.API()
+ self.volume_api = volume.API()
+ self.compute_api = compute.API(self.image_service, self.network_api,
+ self.volume_api)
self.setup()
def __str__(self):
@@ -118,7 +119,8 @@ class CloudController(object):
def _get_mpi_data(self, context, project_id):
result = {}
- for instance in self.compute_api.get_instances(context, project_id):
+ for instance in self.compute_api.get_all(context,
+ project_id=project_id):
if instance['fixed_ip']:
line = '%s slots=%d' % (instance['fixed_ip']['address'],
instance['vcpus'])
@@ -140,7 +142,7 @@ class CloudController(object):
def get_metadata(self, address):
ctxt = context.get_admin_context()
- instance_ref = db.fixed_ip_get_instance(ctxt, address)
+ instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address)
if instance_ref is None:
return None
mpi = self._get_mpi_data(ctxt, instance_ref['project_id'])
@@ -152,7 +154,7 @@ class CloudController(object):
hostname = instance_ref['hostname']
floating_ip = db.instance_get_floating_address(ctxt,
instance_ref['id'])
- ec2_id = internal_id_to_ec2_id(instance_ref['internal_id'])
+ ec2_id = id_to_ec2_id(instance_ref['id'])
data = {
'user-data': base64.b64decode(instance_ref['user_data']),
'meta-data': {
@@ -188,9 +190,46 @@ class CloudController(object):
return data
def describe_availability_zones(self, context, **kwargs):
+ if ('zone_name' in kwargs and
+ 'verbose' in kwargs['zone_name'] and
+ context.is_admin):
+ return self._describe_availability_zones_verbose(context,
+ **kwargs)
+ else:
+ return self._describe_availability_zones(context, **kwargs)
+
+ def _describe_availability_zones(self, context, **kwargs):
return {'availabilityZoneInfo': [{'zoneName': 'nova',
'zoneState': 'available'}]}
+ def _describe_availability_zones_verbose(self, context, **kwargs):
+ rv = {'availabilityZoneInfo': [{'zoneName': 'nova',
+ 'zoneState': 'available'}]}
+
+ services = db.service_get_all(context)
+ now = db.get_time()
+ hosts = []
+ for host in [service['host'] for service in services]:
+ if not host in hosts:
+ hosts.append(host)
+ for host in hosts:
+ rv['availabilityZoneInfo'].append({'zoneName': '|- %s' % host,
+ 'zoneState': ''})
+ hsvcs = [service for service in services \
+ if service['host'] == host]
+ for svc in hsvcs:
+ delta = now - (svc['updated_at'] or svc['created_at'])
+ alive = (delta.seconds <= FLAGS.service_down_time)
+ art = (alive and ":-)") or "XXX"
+ active = 'enabled'
+ if svc['disabled']:
+ active = 'disabled'
+ rv['availabilityZoneInfo'].append({
+ 'zoneName': '| |- %s' % svc['binary'],
+ 'zoneState': '%s %s %s' % (active, art,
+ svc['updated_at'])})
+ return rv
+
def describe_regions(self, context, region_name=None, **kwargs):
if FLAGS.region_list:
regions = []
@@ -441,8 +480,8 @@ class CloudController(object):
def get_console_output(self, context, instance_id, **kwargs):
# instance_id is passed in as a list of instances
ec2_id = instance_id[0]
- internal_id = ec2_id_to_internal_id(ec2_id)
- instance_ref = self.compute_api.get_instance(context, internal_id)
+ instance_id = ec2_id_to_id(ec2_id)
+ instance_ref = self.compute_api.get(context, instance_id)
output = rpc.call(context,
'%s.%s' % (FLAGS.compute_topic,
instance_ref['host']),
@@ -455,27 +494,22 @@ class CloudController(object):
"output": base64.b64encode(output)}
def describe_volumes(self, context, volume_id=None, **kwargs):
- if context.user.is_admin():
- volumes = db.volume_get_all(context)
- else:
- volumes = db.volume_get_all_by_project(context, context.project_id)
-
+ volumes = self.volume_api.get_all(context)
# NOTE(vish): volume_id is an optional list of volume ids to filter by.
volumes = [self._format_volume(context, v) for v in volumes
- if volume_id is None or v['ec2_id'] in volume_id]
-
+ if volume_id is None or v['id'] in volume_id]
return {'volumeSet': volumes}
def _format_volume(self, context, volume):
instance_ec2_id = None
instance_data = None
if volume.get('instance', None):
- internal_id = volume['instance']['internal_id']
- instance_ec2_id = internal_id_to_ec2_id(internal_id)
+ instance_id = volume['instance']['id']
+ instance_ec2_id = id_to_ec2_id(instance_id)
instance_data = '%s[%s]' % (instance_ec2_id,
volume['instance']['host'])
v = {}
- v['volumeId'] = volume['ec2_id']
+ v['volumeId'] = volume['id']
v['status'] = volume['status']
v['size'] = volume['size']
v['availabilityZone'] = volume['availability_zone']
@@ -502,88 +536,47 @@ class CloudController(object):
return v
def create_volume(self, context, size, **kwargs):
- # check quota
- if quota.allowed_volumes(context, 1, size) < 1:
- logging.warn("Quota exceeeded for %s, tried to create %sG volume",
- context.project_id, size)
- raise quota.QuotaError("Volume quota exceeded. You cannot "
- "create a volume of size %s" % size)
- vol = {}
- vol['size'] = size
- vol['user_id'] = context.user.id
- vol['project_id'] = context.project_id
- vol['availability_zone'] = FLAGS.storage_availability_zone
- vol['status'] = "creating"
- vol['attach_status'] = "detached"
- vol['display_name'] = kwargs.get('display_name')
- vol['display_description'] = kwargs.get('display_description')
- volume_ref = db.volume_create(context, vol)
-
- rpc.cast(context,
- FLAGS.scheduler_topic,
- {"method": "create_volume",
- "args": {"topic": FLAGS.volume_topic,
- "volume_id": volume_ref['id']}})
-
+ volume = self.volume_api.create(context, size,
+ kwargs.get('display_name'),
+ kwargs.get('display_description'))
# TODO(vish): Instance should be None at db layer instead of
# trying to lazy load, but for now we turn it into
# a dict to avoid an error.
return {'volumeSet': [self._format_volume(context, dict(volume_ref))]}
+ def delete_volume(self, context, volume_id, **kwargs):
+ self.volume_api.delete(context, volume_id)
+ return True
+
+ def update_volume(self, context, volume_id, **kwargs):
+ updatable_fields = ['display_name', 'display_description']
+ changes = {}
+ for field in updatable_fields:
+ if field in kwargs:
+ changes[field] = kwargs[field]
+ if changes:
+ self.volume_api.update(context, volume_id, kwargs)
+ return True
+
def attach_volume(self, context, volume_id, instance_id, device, **kwargs):
- volume_ref = db.volume_get_by_ec2_id(context, volume_id)
- if not re.match("^/dev/[a-z]d[a-z]+$", device):
- raise exception.ApiError(_("Invalid device specified: %s. "
- "Example device: /dev/vdb") % device)
- # TODO(vish): abstract status checking?
- if volume_ref['status'] != "available":
- raise exception.ApiError(_("Volume status must be available"))
- if volume_ref['attach_status'] == "attached":
- raise exception.ApiError(_("Volume is already attached"))
- internal_id = ec2_id_to_internal_id(instance_id)
- instance_ref = self.compute_api.get_instance(context, internal_id)
- host = instance_ref['host']
- rpc.cast(context,
- db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "attach_volume",
- "args": {"volume_id": volume_ref['id'],
- "instance_id": instance_ref['id'],
- "mountpoint": device}})
- return {'attachTime': volume_ref['attach_time'],
- 'device': volume_ref['mountpoint'],
- 'instanceId': instance_ref['id'],
+ self.compute_api.attach_volume(context, instance_id, volume_id, device)
+ volume = self.volume_api.get(context, volume_id)
+ return {'attachTime': volume['attach_time'],
+ 'device': volume['mountpoint'],
+ 'instanceId': instance_id,
'requestId': context.request_id,
- 'status': volume_ref['attach_status'],
- 'volumeId': volume_ref['id']}
+ 'status': volume['attach_status'],
+ 'volumeId': volume_id}
def detach_volume(self, context, volume_id, **kwargs):
- volume_ref = db.volume_get_by_ec2_id(context, volume_id)
- instance_ref = db.volume_get_instance(context.elevated(),
- volume_ref['id'])
- if not instance_ref:
- raise exception.ApiError(_("Volume isn't attached to anything!"))
- # TODO(vish): abstract status checking?
- if volume_ref['status'] == "available":
- raise exception.ApiError(_("Volume is already detached"))
- try:
- host = instance_ref['host']
- rpc.cast(context,
- db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "detach_volume",
- "args": {"instance_id": instance_ref['id'],
- "volume_id": volume_ref['id']}})
- except exception.NotFound:
- # If the instance doesn't exist anymore,
- # then we need to call detach blind
- db.volume_detached(context)
- internal_id = instance_ref['internal_id']
- ec2_id = internal_id_to_ec2_id(internal_id)
- return {'attachTime': volume_ref['attach_time'],
- 'device': volume_ref['mountpoint'],
- 'instanceId': internal_id,
+ volume = self.volume_api.get(context, volume_id)
+ instance = self.compute_api.detach_volume(context, volume_id)
+ return {'attachTime': volume['attach_time'],
+ 'device': volume['mountpoint'],
+ 'instanceId': id_to_ec2_id(instance['id']),
'requestId': context.request_id,
- 'status': volume_ref['attach_status'],
- 'volumeId': volume_ref['id']}
+ 'status': volume['attach_status'],
+ 'volumeId': volume_id}
def _convert_to_set(self, lst, label):
if lst == None or lst == []:
@@ -592,16 +585,6 @@ class CloudController(object):
lst = [lst]
return [{label: x} for x in lst]
- def update_volume(self, context, volume_id, **kwargs):
- updatable_fields = ['display_name', 'display_description']
- changes = {}
- for field in updatable_fields:
- if field in kwargs:
- changes[field] = kwargs[field]
- if changes:
- db.volume_update(context, volume_id, kwargs)
- return True
-
def describe_instances(self, context, **kwargs):
return self._format_describe_instances(context)
@@ -613,20 +596,16 @@ class CloudController(object):
assert len(i) == 1
return i[0]
- def _format_instances(self, context, reservation_id=None):
+ def _format_instances(self, context, **kwargs):
reservations = {}
- if reservation_id:
- instances = db.instance_get_all_by_reservation(context,
- reservation_id)
- else:
- instances = self.compute_api.get_instances(context)
+ instances = self.compute_api.get_all(context, **kwargs)
for instance in instances:
if not context.user.is_admin():
if instance['image_id'] == FLAGS.vpn_image_id:
continue
i = {}
- internal_id = instance['internal_id']
- ec2_id = internal_id_to_ec2_id(internal_id)
+ instance_id = instance['id']
+ ec2_id = id_to_ec2_id(instance_id)
i['instanceId'] = ec2_id
i['imageId'] = instance['image_id']
i['instanceState'] = {
@@ -679,8 +658,8 @@ class CloudController(object):
ec2_id = None
if (floating_ip_ref['fixed_ip']
and floating_ip_ref['fixed_ip']['instance']):
- internal_id = floating_ip_ref['fixed_ip']['instance']['ec2_id']
- ec2_id = internal_id_to_ec2_id(internal_id)
+ instance_id = floating_ip_ref['fixed_ip']['instance']['ec2_id']
+ ec2_id = id_to_ec2_id(instance_id)
address_rv = {'public_ip': address,
'instance_id': ec2_id}
if context.user.is_admin():
@@ -691,69 +670,25 @@ class CloudController(object):
return {'addressesSet': addresses}
def allocate_address(self, context, **kwargs):
- # check quota
- if quota.allowed_floating_ips(context, 1) < 1:
- logging.warn(_("Quota exceeeded for %s, tried to allocate "
- "address"),
- context.project_id)
- raise quota.QuotaError(_("Address quota exceeded. You cannot "
- "allocate any more addresses"))
- # NOTE(vish): We don't know which network host should get the ip
- # when we allocate, so just send it to any one. This
- # will probably need to move into a network supervisor
- # at some point.
- public_ip = rpc.call(context,
- FLAGS.network_topic,
- {"method": "allocate_floating_ip",
- "args": {"project_id": context.project_id}})
+ public_ip = self.network_api.allocate_floating_ip(context)
return {'addressSet': [{'publicIp': public_ip}]}
def release_address(self, context, public_ip, **kwargs):
- floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
- # NOTE(vish): We don't know which network host should get the ip
- # when we deallocate, so just send it to any one. This
- # will probably need to move into a network supervisor
- # at some point.
- rpc.cast(context,
- FLAGS.network_topic,
- {"method": "deallocate_floating_ip",
- "args": {"floating_address": floating_ip_ref['address']}})
+ self.network_api.release_floating_ip(context, public_ip)
return {'releaseResponse': ["Address released."]}
def associate_address(self, context, instance_id, public_ip, **kwargs):
- internal_id = ec2_id_to_internal_id(instance_id)
- instance_ref = self.compute_api.get_instance(context, internal_id)
- fixed_address = db.instance_get_fixed_address(context,
- instance_ref['id'])
- floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
- # NOTE(vish): Perhaps we should just pass this on to compute and
- # let compute communicate with network.
- network_topic = self.compute_api.get_network_topic(context,
- internal_id)
- rpc.cast(context,
- network_topic,
- {"method": "associate_floating_ip",
- "args": {"floating_address": floating_ip_ref['address'],
- "fixed_address": fixed_address}})
+ instance_id = ec2_id_to_id(instance_id)
+ self.compute_api.associate_floating_ip(context, instance_id, public_ip)
return {'associateResponse': ["Address associated."]}
def disassociate_address(self, context, public_ip, **kwargs):
- floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
- # NOTE(vish): Get the topic from the host name of the network of
- # the associated fixed ip.
- if not floating_ip_ref.get('fixed_ip'):
- raise exception.ApiError('Address is not associated.')
- host = floating_ip_ref['fixed_ip']['network']['host']
- topic = db.queue_get_for(context, FLAGS.network_topic, host)
- rpc.cast(context,
- topic,
- {"method": "disassociate_floating_ip",
- "args": {"floating_address": floating_ip_ref['address']}})
+ self.network_api.disassociate_floating_ip(context, public_ip)
return {'disassociateResponse': ["Address disassociated."]}
def run_instances(self, context, **kwargs):
max_count = int(kwargs.get('max_count', 1))
- instances = self.compute_api.create_instances(context,
+ instances = self.compute_api.create(context,
instance_types.get_by_type(kwargs.get('instance_type', None)),
kwargs['image_id'],
min_count=int(kwargs.get('min_count', max_count)),
@@ -761,11 +696,13 @@ class CloudController(object):
kernel_id=kwargs.get('kernel_id', None),
ramdisk_id=kwargs.get('ramdisk_id'),
display_name=kwargs.get('display_name'),
- description=kwargs.get('display_description'),
+ display_description=kwargs.get('display_description'),
key_name=kwargs.get('key_name'),
user_data=kwargs.get('user_data'),
security_group=kwargs.get('security_group'),
- generate_hostname=internal_id_to_ec2_id)
+ availability_zone=kwargs.get('placement', {}).get(
+ 'AvailabilityZone'),
+ generate_hostname=id_to_ec2_id)
return self._format_run_instances(context,
instances[0]['reservation_id'])
@@ -774,27 +711,27 @@ class CloudController(object):
instance_id is a kwarg so its name cannot be modified."""
logging.debug("Going to start terminating instances")
for ec2_id in instance_id:
- internal_id = ec2_id_to_internal_id(ec2_id)
- self.compute_api.delete_instance(context, internal_id)
+ instance_id = ec2_id_to_id(ec2_id)
+ self.compute_api.delete(context, instance_id)
return True
def reboot_instances(self, context, instance_id, **kwargs):
"""instance_id is a list of instance ids"""
for ec2_id in instance_id:
- internal_id = ec2_id_to_internal_id(ec2_id)
- self.compute_api.reboot(context, internal_id)
+ instance_id = ec2_id_to_id(ec2_id)
+ self.compute_api.reboot(context, instance_id)
return True
def rescue_instance(self, context, instance_id, **kwargs):
"""This is an extension to the normal ec2_api"""
- internal_id = ec2_id_to_internal_id(instance_id)
- self.compute_api.rescue(context, internal_id)
+ instance_id = ec2_id_to_id(instance_id)
+ self.compute_api.rescue(context, instance_id)
return True
def unrescue_instance(self, context, instance_id, **kwargs):
"""This is an extension to the normal ec2_api"""
- internal_id = ec2_id_to_internal_id(instance_id)
- self.compute_api.unrescue(context, internal_id)
+ instance_id = ec2_id_to_id(instance_id)
+ self.compute_api.unrescue(context, instance_id)
return True
def update_instance(self, context, ec2_id, **kwargs):
@@ -804,24 +741,8 @@ class CloudController(object):
if field in kwargs:
changes[field] = kwargs[field]
if changes:
- internal_id = ec2_id_to_internal_id(ec2_id)
- inst = self.compute_api.get_instance(context, internal_id)
- db.instance_update(context, inst['id'], kwargs)
- return True
-
- def delete_volume(self, context, volume_id, **kwargs):
- # TODO: return error if not authorized
- volume_ref = db.volume_get_by_ec2_id(context, volume_id)
- if volume_ref['status'] != "available":
- raise exception.ApiError(_("Volume status must be available"))
- now = datetime.datetime.utcnow()
- db.volume_update(context, volume_ref['id'], {'status': 'deleting',
- 'terminated_at': now})
- host = volume_ref['host']
- rpc.cast(context,
- db.queue_get_for(context, FLAGS.volume_topic, host),
- {"method": "delete_volume",
- "args": {"volume_id": volume_ref['id']}})
+ instance_id = ec2_id_to_id(ec2_id)
+ self.compute_api.update(context, instance_id, **kwargs)
return True
def describe_images(self, context, image_id=None, **kwargs):
diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py
index f832863a9..a57a6698a 100644
--- a/nova/api/ec2/metadatarequesthandler.py
+++ b/nova/api/ec2/metadatarequesthandler.py
@@ -79,3 +79,7 @@ class MetadataRequestHandler(object):
if data is None:
raise webob.exc.HTTPNotFound()
return self.print_data(data)
+
+
+def metadata_factory(global_args, **local_args):
+ return MetadataRequestHandler()
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 66aceee2d..a1430caed 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -20,7 +20,6 @@
WSGI middleware for OpenStack API controllers.
"""
-import json
import time
import logging
@@ -41,7 +40,6 @@ from nova.api.openstack import images
from nova.api.openstack import ratelimiting
from nova.api.openstack import servers
from nova.api.openstack import sharedipgroups
-from nova.auth import manager
FLAGS = flags.FLAGS
@@ -93,6 +91,8 @@ class APIRouter(wsgi.Router):
logging.debug("Including admin operations in API.")
server_members['pause'] = 'POST'
server_members['unpause'] = 'POST'
+ server_members["diagnostics"] = "GET"
+ server_members["actions"] = "GET"
server_members['suspend'] = 'POST'
server_members['resume'] = 'POST'
@@ -113,3 +113,24 @@ class APIRouter(wsgi.Router):
controller=sharedipgroups.Controller())
super(APIRouter, self).__init__(mapper)
+
+
+class Versions(wsgi.Application):
+ @webob.dec.wsgify
+ def __call__(self, req):
+ """Respond to a request for all OpenStack API versions."""
+ response = {
+ "versions": [
+ dict(status="CURRENT", id="v1.0")]}
+ metadata = {
+ "application/xml": {
+ "attributes": dict(version=["status", "id"])}}
+ return wsgi.Serializer(req.environ, metadata).to_content_type(response)
+
+
+def router_factory(global_cof, **local_conf):
+ return APIRouter()
+
+
+def versions_factory(global_conf, **local_conf):
+ return Versions()
diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py
index e24e58fd3..00e817c8d 100644
--- a/nova/api/openstack/auth.py
+++ b/nova/api/openstack/auth.py
@@ -55,7 +55,8 @@ class AuthMiddleware(wsgi.Middleware):
if not user:
return faults.Fault(webob.exc.HTTPUnauthorized())
- req.environ['nova.context'] = context.RequestContext(user, user)
+ project = self.auth.get_project(FLAGS.default_project)
+ req.environ['nova.context'] = context.RequestContext(user, project)
return self.application
def has_authentication(self, req):
@@ -133,3 +134,9 @@ class AuthMiddleware(wsgi.Middleware):
token = self.db.auth_create_token(ctxt, token_dict)
return token, user
return None, None
+
+
+def auth_factory(global_conf, **local_conf):
+ def auth(app):
+ return AuthMiddleware(app)
+ return auth
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index ba35fbc78..0b239aab8 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -17,15 +17,14 @@
from webob import exc
+from nova import compute
from nova import flags
from nova import utils
from nova import wsgi
import nova.api.openstack
-import nova.image.service
-
from nova.api.openstack import common
from nova.api.openstack import faults
-
+import nova.image.service
FLAGS = flags.FLAGS
@@ -127,9 +126,11 @@ class Controller(wsgi.Controller):
raise faults.Fault(exc.HTTPNotFound())
def create(self, req):
- # Only public images are supported for now, so a request to
- # make a backup of a server cannot be supproted.
- raise faults.Fault(exc.HTTPNotFound())
+ context = req.environ['nova.context']
+ env = self._deserialize(req.body, req)
+ instance_id = env["image"]["serverId"]
+ name = env["image"]["name"]
+ return compute.API().snapshot(context, instance_id, name)
def update(self, req, id):
# Users may not modify public images, and that's all that
diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py
index cbb4b897e..81b83142f 100644
--- a/nova/api/openstack/ratelimiting/__init__.py
+++ b/nova/api/openstack/ratelimiting/__init__.py
@@ -219,3 +219,9 @@ class WSGIAppProxy(object):
# No delay
return None
return float(resp.getheader('X-Wait-Seconds'))
+
+
+def ratelimit_factory(global_conf, **local_conf):
+ def rl(app):
+ return RateLimitingMiddleware(app)
+ return rl
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 845183258..ce64ac7ad 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -20,12 +20,12 @@ import traceback
from webob import exc
+from nova import compute
from nova import exception
from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
from nova.auth import manager as auth_manager
-from nova.compute import api as compute_api
from nova.compute import instance_types
from nova.compute import power_state
import nova.api.openstack
@@ -51,7 +51,7 @@ def _translate_detail_keys(inst):
inst_dict = {}
mapped_keys = dict(status='state', imageId='image_id',
- flavorId='instance_type', name='display_name', id='internal_id')
+ flavorId='instance_type', name='display_name', id='id')
for k, v in mapped_keys.iteritems():
inst_dict[k] = inst[v]
@@ -67,7 +67,7 @@ def _translate_detail_keys(inst):
def _translate_keys(inst):
""" Coerces into dictionary format, excluding all model attributes
save for id and name """
- return dict(server=dict(id=inst['internal_id'], name=inst['display_name']))
+ return dict(server=dict(id=inst['id'], name=inst['display_name']))
class Controller(wsgi.Controller):
@@ -80,7 +80,7 @@ class Controller(wsgi.Controller):
"status", "progress"]}}}
def __init__(self):
- self.compute_api = compute_api.ComputeAPI()
+ self.compute_api = compute.API()
super(Controller, self).__init__()
def index(self, req):
@@ -96,8 +96,7 @@ class Controller(wsgi.Controller):
entity_maker - either _translate_detail_keys or _translate_keys
"""
- instance_list = self.compute_api.get_instances(
- req.environ['nova.context'])
+ instance_list = self.compute_api.get_all(req.environ['nova.context'])
limited_list = common.limited(instance_list, req)
res = [entity_maker(inst)['server'] for inst in limited_list]
return dict(servers=res)
@@ -105,8 +104,7 @@ class Controller(wsgi.Controller):
def show(self, req, id):
""" Returns server details by server id """
try:
- instance = self.compute_api.get_instance(
- req.environ['nova.context'], int(id))
+ instance = self.compute_api.get(req.environ['nova.context'], id)
return _translate_detail_keys(instance)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
@@ -114,8 +112,7 @@ class Controller(wsgi.Controller):
def delete(self, req, id):
""" Destroys a server """
try:
- self.compute_api.delete_instance(req.environ['nova.context'],
- int(id))
+ self.compute_api.delete(req.environ['nova.context'], id)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPAccepted()
@@ -128,12 +125,12 @@ class Controller(wsgi.Controller):
key_pair = auth_manager.AuthManager.get_key_pairs(
req.environ['nova.context'])[0]
- instances = self.compute_api.create_instances(
+ instances = self.compute_api.create(
req.environ['nova.context'],
instance_types.get_by_flavor_id(env['server']['flavorId']),
env['server']['imageId'],
display_name=env['server']['name'],
- description=env['server']['name'],
+ display_description=env['server']['name'],
key_name=key_pair['name'],
key_data=key_pair['public_key'])
return _translate_keys(instances[0])
@@ -151,10 +148,8 @@ class Controller(wsgi.Controller):
update_dict['display_name'] = inst_dict['server']['name']
try:
- ctxt = req.environ['nova.context']
- self.compute_api.update_instance(ctxt,
- id,
- **update_dict)
+ self.compute_api.update(req.environ['nova.context'], id,
+ **update_dict)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPNoContent()
@@ -218,3 +213,13 @@ class Controller(wsgi.Controller):
logging.error(_("compute.api::resume %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+
+ def diagnostics(self, req, id):
+ """Permit Admins to retrieve server diagnostics."""
+ ctxt = req.environ["nova.context"]
+ return self.compute_api.get_diagnostics(ctxt, id)
+
+ def actions(self, req, id):
+ """Permit Admins to retrieve server actions."""
+ ctxt = req.environ["nova.context"]
+ return self.compute_api.get_actions(ctxt, id)
diff --git a/nova/compute/__init__.py b/nova/compute/__init__.py
index a5df2ec1a..b94f971d1 100644
--- a/nova/compute/__init__.py
+++ b/nova/compute/__init__.py
@@ -16,17 +16,4 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-:mod:`nova.compute` -- Compute Nodes using LibVirt
-=====================================================
-
-.. automodule:: nova.compute
- :platform: Unix
- :synopsis: Thin wrapper around libvirt for VM mgmt.
-.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com>
-.. moduleauthor:: Devin Carlen <devin.carlen@gmail.com>
-.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com>
-.. moduleauthor:: Joshua McKenty <joshua@cognition.ca>
-.. moduleauthor:: Manish Singh <yosh@gimp.org>
-.. moduleauthor:: Andy Smith <andy@anarkystic.com>
-"""
+from nova.compute.api import API
diff --git a/nova/compute/api.py b/nova/compute/api.py
index a47703461..64d47b1ce 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -17,7 +17,7 @@
# under the License.
"""
-Handles all API requests relating to instances (guest vms).
+Handles all requests relating to instances (guest vms).
"""
import datetime
@@ -27,36 +27,41 @@ import time
from nova import db
from nova import exception
from nova import flags
+from nova import network
from nova import quota
from nova import rpc
from nova import utils
+from nova import volume
from nova.compute import instance_types
from nova.db import base
FLAGS = flags.FLAGS
-def generate_default_hostname(internal_id):
+def generate_default_hostname(instance_id):
"""Default function to generate a hostname given an instance reference."""
- return str(internal_id)
+ return str(instance_id)
-class ComputeAPI(base.Base):
+class API(base.Base):
"""API for interacting with the compute manager."""
- def __init__(self, network_manager=None, image_service=None, **kwargs):
- if not network_manager:
- network_manager = utils.import_object(FLAGS.network_manager)
- self.network_manager = network_manager
+ def __init__(self, image_service=None, network_api=None, volume_api=None,
+ **kwargs):
if not image_service:
image_service = utils.import_object(FLAGS.image_service)
self.image_service = image_service
- super(ComputeAPI, self).__init__(**kwargs)
+ if not network_api:
+ network_api = network.API()
+ self.network_api = network_api
+ if not volume_api:
+ volume_api = volume.API()
+ self.volume_api = volume_api
+ super(API, self).__init__(**kwargs)
def get_network_topic(self, context, instance_id):
try:
- instance = self.db.instance_get_by_internal_id(context,
- instance_id)
+ instance = self.get(context, instance_id)
except exception.NotFound as e:
logging.warning("Instance %d was not found in get_network_topic",
instance_id)
@@ -70,17 +75,18 @@ class ComputeAPI(base.Base):
topic,
{"method": "get_network_topic", "args": {'fake': 1}})
- def create_instances(self, context, instance_type, image_id, min_count=1,
- max_count=1, kernel_id=None, ramdisk_id=None,
- display_name='', description='', key_name=None,
- key_data=None, security_group='default',
- user_data=None,
- generate_hostname=generate_default_hostname):
- """Create the number of instances requested if quote and
+ def create(self, context, instance_type,
+ image_id, kernel_id=None, ramdisk_id=None,
+ min_count=1, max_count=1,
+ display_name='', display_description='',
+ key_name=None, key_data=None, security_group='default',
+ availability_zone=None, user_data=None,
+ generate_hostname=generate_default_hostname):
+ """Create the number of instances requested if quota and
other arguments check out ok."""
- num_instances = quota.allowed_instances(context, max_count,
- instance_type)
+ type_data = instance_types.INSTANCE_TYPES[instance_type]
+ num_instances = quota.allowed_instances(context, max_count, type_data)
if num_instances < min_count:
logging.warn("Quota exceeeded for %s, tried to run %s instances",
context.project_id, min_count)
@@ -95,7 +101,7 @@ class ComputeAPI(base.Base):
kernel_id = image.get('kernelId', None)
if ramdisk_id is None:
ramdisk_id = image.get('ramdiskId', None)
- #No kernel and ramdisk for raw images
+ # No kernel and ramdisk for raw images
if kernel_id == str(FLAGS.null_kernel):
kernel_id = None
ramdisk_id = None
@@ -123,7 +129,6 @@ class ComputeAPI(base.Base):
key_pair = db.key_pair_get(context, context.user_id, key_name)
key_data = key_pair['public_key']
- type_data = instance_types.INSTANCE_TYPES[instance_type]
base_options = {
'reservation_id': utils.generate_uid('r'),
'image_id': image_id,
@@ -138,10 +143,11 @@ class ComputeAPI(base.Base):
'vcpus': type_data['vcpus'],
'local_gb': type_data['local_gb'],
'display_name': display_name,
- 'display_description': description,
+ 'display_description': display_description,
'user_data': user_data or '',
'key_name': key_name,
- 'key_data': key_data}
+ 'key_data': key_data,
+ 'availability_zone': availability_zone}
elevated = context.elevated()
instances = []
@@ -152,7 +158,6 @@ class ComputeAPI(base.Base):
**base_options)
instance = self.db.instance_create(context, instance)
instance_id = instance['id']
- internal_id = instance['internal_id']
elevated = context.elevated()
if not security_groups:
@@ -163,11 +168,11 @@ class ComputeAPI(base.Base):
security_group_id)
# Set sane defaults if not specified
- updates = dict(hostname=generate_hostname(internal_id))
+ updates = dict(hostname=generate_hostname(instance_id))
if 'display_name' not in instance:
- updates['display_name'] = "Server %s" % internal_id
+ updates['display_name'] = "Server %s" % instance_id
- instance = self.update_instance(context, instance_id, **updates)
+ instance = self.update(context, instance_id, **updates)
instances.append(instance)
logging.debug(_("Casting to scheduler for %s/%s's instance %s"),
@@ -197,7 +202,7 @@ class ComputeAPI(base.Base):
'project_id': context.project_id}
db.security_group_create(context, values)
- def update_instance(self, context, instance_id, **kwargs):
+ def update(self, context, instance_id, **kwargs):
"""Updates the instance in the datastore.
:param context: The security context
@@ -211,111 +216,172 @@ class ComputeAPI(base.Base):
"""
return self.db.instance_update(context, instance_id, kwargs)
- def delete_instance(self, context, instance_id):
- logging.debug("Going to try and terminate %d" % instance_id)
+ def delete(self, context, instance_id):
+ logging.debug("Going to try and terminate %s" % instance_id)
try:
- instance = self.db.instance_get_by_internal_id(context,
- instance_id)
+ instance = self.get(context, instance_id)
except exception.NotFound as e:
- logging.warning(_("Instance %d was not found during terminate"),
+ logging.warning(_("Instance %s was not found during terminate"),
instance_id)
raise e
if (instance['state_description'] == 'terminating'):
- logging.warning(_("Instance %d is already being terminated"),
+ logging.warning(_("Instance %s is already being terminated"),
instance_id)
return
- self.update_instance(context,
- instance['id'],
- state_description='terminating',
- state=0,
- terminated_at=datetime.datetime.utcnow())
+ self.update(context,
+ instance['id'],
+ state_description='terminating',
+ state=0,
+ terminated_at=datetime.datetime.utcnow())
host = instance['host']
if host:
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "terminate_instance",
- "args": {"instance_id": instance['id']}})
+ "args": {"instance_id": instance_id}})
else:
- self.db.instance_destroy(context, instance['id'])
-
- def get_instances(self, context, project_id=None):
- """Get all instances, possibly filtered by project ID or
- user ID. If there is no filter and the context is an admin,
- it will retreive all instances in the system."""
+ self.db.instance_destroy(context, instance_id)
+
+ def get(self, context, instance_id):
+ """Get a single instance with the given ID."""
+ return self.db.instance_get_by_id(context, instance_id)
+
+ def get_all(self, context, project_id=None, reservation_id=None,
+ fixed_ip=None):
+ """Get all instances, possibly filtered by one of the
+ given parameters. If there is no filter and the context is
+ an admin, it will retreive all instances in the system."""
+ if reservation_id is not None:
+ return self.db.instance_get_all_by_reservation(context,
+ reservation_id)
+ if fixed_ip is not None:
+ return self.db.fixed_ip_get_instance(context, fixed_ip)
if project_id or not context.is_admin:
if not context.project:
return self.db.instance_get_all_by_user(context,
context.user_id)
if project_id is None:
project_id = context.project_id
- return self.db.instance_get_all_by_project(context, project_id)
+ return self.db.instance_get_all_by_project(context,
+ project_id)
return self.db.instance_get_all(context)
- def get_instance(self, context, instance_id):
- return self.db.instance_get_by_internal_id(context, instance_id)
+ def snapshot(self, context, instance_id, name):
+ """Snapshot the given instance."""
+ instance = self.get(context, instance_id)
+ host = instance['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "snapshot_instance",
+ "args": {"instance_id": instance_id, "name": name}})
def reboot(self, context, instance_id):
"""Reboot the given instance."""
- instance = self.db.instance_get_by_internal_id(context, instance_id)
+ instance = self.get(context, instance_id)
host = instance['host']
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "reboot_instance",
- "args": {"instance_id": instance['id']}})
+ "args": {"instance_id": instance_id}})
def pause(self, context, instance_id):
"""Pause the given instance."""
- instance = self.db.instance_get_by_internal_id(context, instance_id)
+ instance = self.get(context, instance_id)
host = instance['host']
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "pause_instance",
- "args": {"instance_id": instance['id']}})
+ "args": {"instance_id": instance_id}})
def unpause(self, context, instance_id):
"""Unpause the given instance."""
- instance = self.db.instance_get_by_internal_id(context, instance_id)
+ instance = self.get(context, instance_id)
host = instance['host']
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "unpause_instance",
- "args": {"instance_id": instance['id']}})
+ "args": {"instance_id": instance_id}})
+
+ def get_diagnostics(self, context, instance_id):
+ """Retrieve diagnostics for the given instance."""
+ instance = self.get(context, instance_id)
+ host = instance["host"]
+ return rpc.call(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "get_diagnostics",
+ "args": {"instance_id": instance_id}})
+
+ def get_actions(self, context, instance_id):
+ """Retrieve actions for the given instance."""
+ return self.db.instance_get_actions(context, instance_id)
def suspend(self, context, instance_id):
"""suspend the instance with instance_id"""
- instance = self.db.instance_get_by_internal_id(context, instance_id)
+ instance = self.get(context, instance_id)
host = instance['host']
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "suspend_instance",
- "args": {"instance_id": instance['id']}})
+ "args": {"instance_id": instance_id}})
def resume(self, context, instance_id):
"""resume the instance with instance_id"""
- instance = self.db.instance_get_by_internal_id(context, instance_id)
+ instance = self.get(context, instance_id)
host = instance['host']
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "resume_instance",
- "args": {"instance_id": instance['id']}})
+ "args": {"instance_id": instance_id}})
def rescue(self, context, instance_id):
"""Rescue the given instance."""
- instance = self.db.instance_get_by_internal_id(context, instance_id)
+ instance = self.get(context, instance_id)
host = instance['host']
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "rescue_instance",
- "args": {"instance_id": instance['id']}})
+ "args": {"instance_id": instance_id}})
def unrescue(self, context, instance_id):
"""Unrescue the given instance."""
- instance = self.db.instance_get_by_internal_id(context, instance_id)
+ instance = self.get(context, instance_id)
host = instance['host']
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "unrescue_instance",
- "args": {"instance_id": instance['id']}})
+ "args": {"instance_id": instance_id}})
+
+ def attach_volume(self, context, instance_id, volume_id, device):
+ if not re.match("^/dev/[a-z]d[a-z]+$", device):
+ raise exception.ApiError(_("Invalid device specified: %s. "
+ "Example device: /dev/vdb") % device)
+ self.volume_api.check_attach(context, volume_id)
+ instance = self.get(context, instance_id)
+ host = instance['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "attach_volume",
+ "args": {"volume_id": volume_id,
+ "instance_id": instance_id,
+ "mountpoint": device}})
+
+ def detach_volume(self, context, volume_id):
+ instance = self.db.volume_get_instance(context.elevated(), volume_id)
+ if not instance:
+ raise exception.ApiError(_("Volume isn't attached to anything!"))
+ self.volume_api.check_detach(context, volume_id)
+ host = instance['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "detach_volume",
+ "args": {"instance_id": instance['id'],
+ "volume_id": volume_id}})
+ return instance
+
+ def associate_floating_ip(self, context, instance_id, address):
+ instance = self.get(context, instance_id)
+ self.network_api.associate_floating_ip(context, address,
+ instance['fixed_ip'])
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 70b175e7c..ca6065890 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -211,7 +211,7 @@ class ComputeManager(manager.Manager):
if instance_ref['state'] != power_state.RUNNING:
logging.warn(_('trying to reboot a non-running '
'instance: %s (state: %s excepted: %s)'),
- instance_ref['internal_id'],
+ instance_id,
instance_ref['state'],
power_state.RUNNING)
@@ -225,13 +225,33 @@ class ComputeManager(manager.Manager):
self._update_state(context, instance_id)
@exception.wrap_exception
+ def snapshot_instance(self, context, instance_id, name):
+ """Snapshot an instance on this server."""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ #NOTE(sirp): update_state currently only refreshes the state field
+ # if we add is_snapshotting, we will need this refreshed too,
+ # potentially?
+ self._update_state(context, instance_id)
+
+ logging.debug(_('instance %s: snapshotting'), instance_ref['name'])
+ if instance_ref['state'] != power_state.RUNNING:
+ logging.warn(_('trying to snapshot a non-running '
+ 'instance: %s (state: %s excepted: %s)'),
+ instance_id,
+ instance_ref['state'],
+ power_state.RUNNING)
+
+ self.driver.snapshot(instance_ref, name)
+
+ @exception.wrap_exception
def rescue_instance(self, context, instance_id):
"""Rescue an instance on this server."""
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
- logging.debug(_('instance %s: rescuing'),
- instance_ref['internal_id'])
+ logging.debug(_('instance %s: rescuing'), instance_id)
self.db.instance_set_state(context,
instance_id,
power_state.NOSTATE,
@@ -246,8 +266,7 @@ class ComputeManager(manager.Manager):
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
- logging.debug(_('instance %s: unrescuing'),
- instance_ref['internal_id'])
+ logging.debug(_('instance %s: unrescuing'), instance_id)
self.db.instance_set_state(context,
instance_id,
power_state.NOSTATE,
@@ -266,8 +285,7 @@ class ComputeManager(manager.Manager):
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
- logging.debug('instance %s: pausing',
- instance_ref['internal_id'])
+ logging.debug('instance %s: pausing', instance_id)
self.db.instance_set_state(context,
instance_id,
power_state.NOSTATE,
@@ -284,8 +302,7 @@ class ComputeManager(manager.Manager):
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
- logging.debug('instance %s: unpausing',
- instance_ref['internal_id'])
+ logging.debug('instance %s: unpausing', instance_id)
self.db.instance_set_state(context,
instance_id,
power_state.NOSTATE,
@@ -297,13 +314,22 @@ class ComputeManager(manager.Manager):
result))
@exception.wrap_exception
+ def get_diagnostics(self, context, instance_id):
+ """Retrieve diagnostics for an instance on this server."""
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ if instance_ref["state"] == power_state.RUNNING:
+ logging.debug(_("instance %s: retrieving diagnostics"),
+ instance_id)
+ return self.driver.get_diagnostics(instance_ref)
+
+ @exception.wrap_exception
def suspend_instance(self, context, instance_id):
"""suspend the instance with instance_id"""
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
- logging.debug(_('instance %s: suspending'),
- instance_ref['internal_id'])
+ logging.debug(_('instance %s: suspending'), instance_id)
self.db.instance_set_state(context, instance_id,
power_state.NOSTATE,
'suspending')
@@ -319,7 +345,7 @@ class ComputeManager(manager.Manager):
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
- logging.debug(_('instance %s: resuming'), instance_ref['internal_id'])
+ logging.debug(_('instance %s: resuming'), instance_id)
self.db.instance_set_state(context, instance_id,
power_state.NOSTATE,
'resuming')
diff --git a/nova/db/api.py b/nova/db/api.py
index fde3f0852..0fa5eb1e8 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -27,6 +27,9 @@ The underlying driver is loaded as a :class:`LazyPluggable`.
:sql_connection: string specifying the sqlalchemy connection to use, like:
`sqlite:///var/lib/nova/nova.sqlite`.
+
+:enable_new_services: when adding a new service to the database, is it in the
+ pool of available hardware (Default: True)
"""
from nova import exception
@@ -37,6 +40,8 @@ from nova import utils
FLAGS = flags.FLAGS
flags.DEFINE_string('db_backend', 'sqlalchemy',
'The backend to use for db')
+flags.DEFINE_boolean('enable_new_services', True,
+ 'Services to be added to the available pool on create')
IMPL = utils.LazyPluggable(FLAGS['db_backend'],
@@ -348,9 +353,9 @@ def instance_get_project_vpn(context, project_id):
return IMPL.instance_get_project_vpn(context, project_id)
-def instance_get_by_internal_id(context, internal_id):
- """Get an instance by internal id."""
- return IMPL.instance_get_by_internal_id(context, internal_id)
+def instance_get_by_id(context, instance_id):
+ """Get an instance by id."""
+ return IMPL.instance_get_by_id(context, instance_id)
def instance_is_vpn(context, instance_id):
@@ -383,6 +388,11 @@ def instance_action_create(context, values):
return IMPL.instance_action_create(context, values)
+def instance_get_actions(context, instance_id):
+ """Get instance actions by instance id."""
+ return IMPL.instance_get_actions(context, instance_id)
+
+
###################
@@ -709,7 +719,7 @@ def security_group_get_all(context):
def security_group_get(context, security_group_id):
- """Get security group by its internal id."""
+ """Get security group by its id."""
return IMPL.security_group_get(context, security_group_id)
diff --git a/nova/db/sqlalchemy/__init__.py b/nova/db/sqlalchemy/__init__.py
index 3288ebd20..22aa1cfe6 100644
--- a/nova/db/sqlalchemy/__init__.py
+++ b/nova/db/sqlalchemy/__init__.py
@@ -19,6 +19,25 @@
"""
SQLAlchemy database backend
"""
+import logging
+import time
+
+from sqlalchemy.exc import OperationalError
+
+from nova import flags
from nova.db.sqlalchemy import models
-models.register_models()
+
+FLAGS = flags.FLAGS
+
+
+for i in xrange(FLAGS.sql_max_retries):
+ if i > 0:
+ time.sleep(FLAGS.sql_retry_interval)
+
+ try:
+ models.register_models()
+ break
+ except OperationalError:
+ logging.exception(_("Data store is unreachable."
+ " Trying again in %d seconds.") % FLAGS.sql_retry_interval)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 7e945e4cb..aaa07e3c9 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -19,7 +19,6 @@
Implementation of SQLAlchemy backend.
"""
-import random
import warnings
from nova import db
@@ -236,6 +235,8 @@ def service_get_by_args(context, host, binary):
def service_create(context, values):
service_ref = models.Service()
service_ref.update(values)
+ if not FLAGS.enable_new_services:
+ service_ref.disabled = True
service_ref.save()
return service_ref
@@ -604,30 +605,18 @@ def fixed_ip_update(context, address, values):
###################
-#TODO(gundlach): instance_create and volume_create are nearly identical
-#and should be refactored. I expect there are other copy-and-paste
-#functions between the two of them as well.
-
-
@require_context
def instance_create(context, values):
"""Create a new Instance record in the database.
context - request context object
values - dict containing column values.
- 'internal_id' is auto-generated and should not be specified.
"""
instance_ref = models.Instance()
instance_ref.update(values)
session = get_session()
with session.begin():
- while instance_ref.internal_id == None:
- # Instances have integer internal ids.
- internal_id = random.randint(0, 2 ** 31 - 1)
- if not instance_internal_id_exists(context, internal_id,
- session=session):
- instance_ref.internal_id = internal_id
instance_ref.save(session=session)
return instance_ref
@@ -749,38 +738,29 @@ def instance_get_project_vpn(context, project_id):
@require_context
-def instance_get_by_internal_id(context, internal_id):
+def instance_get_by_id(context, instance_id):
session = get_session()
if is_admin_context(context):
result = session.query(models.Instance).\
options(joinedload('security_groups')).\
- filter_by(internal_id=internal_id).\
+ filter_by(id=instance_id).\
filter_by(deleted=can_read_deleted(context)).\
first()
elif is_user_context(context):
result = session.query(models.Instance).\
options(joinedload('security_groups')).\
filter_by(project_id=context.project_id).\
- filter_by(internal_id=internal_id).\
+ filter_by(id=instance_id).\
filter_by(deleted=False).\
first()
if not result:
- raise exception.NotFound(_('Instance %s not found') % (internal_id))
+ raise exception.NotFound(_('Instance %s not found') % (instance_id))
return result
@require_context
-def instance_internal_id_exists(context, internal_id, session=None):
- if not session:
- session = get_session()
- return session.query(exists().\
- where(models.Instance.internal_id == internal_id)).\
- one()[0]
-
-
-@require_context
def instance_get_fixed_address(context, instance_id):
session = get_session()
with session.begin():
@@ -856,6 +836,18 @@ def instance_action_create(context, values):
return action_ref
+@require_admin_context
+def instance_get_actions(context, instance_id):
+ """Return the actions associated to the given instance id"""
+ session = get_session()
+ actions = {}
+ for action in session.query(models.InstanceActions).\
+ filter_by(instance_id=instance_id).\
+ all():
+ actions[action.action] = action.error
+ return actions
+
+
###################
@@ -1303,10 +1295,6 @@ def volume_create(context, values):
session = get_session()
with session.begin():
- while volume_ref.ec2_id == None:
- ec2_id = utils.generate_uid('vol')
- if not volume_ec2_id_exists(context, ec2_id, session=session):
- volume_ref.ec2_id = ec2_id
volume_ref.save(session=session)
return volume_ref
@@ -1404,41 +1392,6 @@ def volume_get_all_by_project(context, project_id):
all()
-@require_context
-def volume_get_by_ec2_id(context, ec2_id):
- session = get_session()
- result = None
-
- if is_admin_context(context):
- result = session.query(models.Volume).\
- filter_by(ec2_id=ec2_id).\
- filter_by(deleted=can_read_deleted(context)).\
- first()
- elif is_user_context(context):
- result = session.query(models.Volume).\
- filter_by(project_id=context.project_id).\
- filter_by(ec2_id=ec2_id).\
- filter_by(deleted=False).\
- first()
- else:
- raise exception.NotAuthorized()
-
- if not result:
- raise exception.NotFound(_('Volume %s not found') % ec2_id)
-
- return result
-
-
-@require_context
-def volume_ec2_id_exists(context, ec2_id, session=None):
- if not session:
- session = get_session()
-
- return session.query(exists().\
- where(models.Volume.id == ec2_id)).\
- one()[0]
-
-
@require_admin_context
def volume_get_instance(context, volume_id):
session = get_session()
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 693db8d23..62bb1780d 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -22,7 +22,7 @@ SQLAlchemy models for nova data.
import datetime
from sqlalchemy.orm import relationship, backref, object_mapper
-from sqlalchemy import Column, Integer, Float, String, schema
+from sqlalchemy import Column, Integer, String, schema
from sqlalchemy import ForeignKey, DateTime, Boolean, Text
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
@@ -164,11 +164,13 @@ class Certificate(BASE, NovaBase):
class Instance(BASE, NovaBase):
"""Represents a guest vm."""
__tablename__ = 'instances'
- id = Column(Integer, primary_key=True)
- internal_id = Column(Integer, unique=True)
+ id = Column(Integer, primary_key=True, autoincrement=True)
- admin_pass = Column(String(255))
+ @property
+ def name(self):
+ return "instance-%08x" % self.id
+ admin_pass = Column(String(255))
user_id = Column(String(255))
project_id = Column(String(255))
@@ -180,10 +182,6 @@ class Instance(BASE, NovaBase):
def project(self):
return auth.manager.AuthManager().get_project(self.project_id)
- @property
- def name(self):
- return "instance-%d" % self.internal_id
-
image_id = Column(String(255))
kernel_id = Column(String(255))
ramdisk_id = Column(String(255))
@@ -220,6 +218,8 @@ class Instance(BASE, NovaBase):
launched_at = Column(DateTime)
terminated_at = Column(DateTime)
+ availability_zone = Column(String(255))
+
# User editable field for display in user-facing UIs
display_name = Column(String(255))
display_description = Column(String(255))
@@ -236,21 +236,6 @@ class Instance(BASE, NovaBase):
# 'shutdown', 'shutoff', 'crashed'])
-class InstanceDiagnostics(BASE, NovaBase):
- """Represents a guest VM's diagnostics"""
- __tablename__ = "instance_diagnostics"
- id = Column(Integer, primary_key=True)
- instance_id = Column(Integer, ForeignKey('instances.id'))
-
- memory_available = Column(Float)
- memory_free = Column(Float)
- cpu_load = Column(Float)
- disk_read = Column(Float)
- disk_write = Column(Float)
- net_tx = Column(Float)
- net_rx = Column(Float)
-
-
class InstanceActions(BASE, NovaBase):
"""Represents a guest VM's actions and results"""
__tablename__ = "instance_actions"
@@ -264,8 +249,11 @@ class InstanceActions(BASE, NovaBase):
class Volume(BASE, NovaBase):
"""Represents a block storage device that can be attached to a vm."""
__tablename__ = 'volumes'
- id = Column(Integer, primary_key=True)
- ec2_id = Column(String(12), unique=True)
+ id = Column(Integer, primary_key=True, autoincrement=True)
+
+ @property
+ def name(self):
+ return "volume-%08x" % self.id
user_id = Column(String(255))
project_id = Column(String(255))
@@ -291,10 +279,6 @@ class Volume(BASE, NovaBase):
display_name = Column(String(255))
display_description = Column(String(255))
- @property
- def name(self):
- return self.ec2_id
-
class Quota(BASE, NovaBase):
"""Represents quota overrides for a project."""
@@ -452,7 +436,7 @@ class AuthToken(BASE, NovaBase):
"""
__tablename__ = 'auth_tokens'
token_hash = Column(String(255), primary_key=True)
- user_id = Column(Integer)
+ user_id = Column(String(255))
server_manageent_url = Column(String(255))
storage_url = Column(String(255))
cdn_management_url = Column(String(255))
@@ -558,10 +542,11 @@ def register_models():
"""Register Models and create metadata.
Called from nova.db.sqlalchemy.__init__ as part of loading the driver,
- it will never need to be called explicitly elsewhere.
+ it will never need to be called explicitly elsewhere unless the
+ connection is lost and needs to be reestablished.
"""
from sqlalchemy import create_engine
- models = (Service, Instance, InstanceDiagnostics, InstanceActions,
+ models = (Service, Instance, InstanceActions,
Volume, ExportDevice, IscsiTarget, FixedIp, FloatingIp,
Network, SecurityGroup, SecurityGroupIngressRule,
SecurityGroupInstanceAssociation, AuthToken, User,
diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py
index e0d84c107..c3876c02a 100644
--- a/nova/db/sqlalchemy/session.py
+++ b/nova/db/sqlalchemy/session.py
@@ -36,7 +36,9 @@ def get_session(autocommit=True, expire_on_commit=False):
global _MAKER
if not _MAKER:
if not _ENGINE:
- _ENGINE = create_engine(FLAGS.sql_connection, echo=False)
+ _ENGINE = create_engine(FLAGS.sql_connection,
+ pool_recycle=FLAGS.sql_idle_timeout,
+ echo=False)
_MAKER = (sessionmaker(bind=_ENGINE,
autocommit=autocommit,
expire_on_commit=expire_on_commit))
diff --git a/nova/flags.py b/nova/flags.py
index 76a98d35a..4b7334927 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -212,6 +212,8 @@ DEFINE_list('region_list',
DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake')
DEFINE_string('aws_access_key_id', 'admin', 'AWS Access ID')
DEFINE_string('aws_secret_access_key', 'admin', 'AWS Access Key')
+DEFINE_integer('glance_port', 9292, 'glance port')
+DEFINE_string('glance_host', utils.get_my_ip(), 'glance host')
DEFINE_integer('s3_port', 3333, 's3 port')
DEFINE_string('s3_host', utils.get_my_ip(), 's3 host (for infrastructure)')
DEFINE_string('s3_dmz', utils.get_my_ip(), 's3 dmz ip (for instances)')
@@ -239,6 +241,7 @@ DEFINE_string('cc_dmz', utils.get_my_ip(), 'internal ip of api server')
DEFINE_integer('cc_port', 8773, 'cloud controller port')
DEFINE_string('ec2_suffix', '/services/Cloud', 'suffix for ec2')
+DEFINE_string('default_project', 'openstack', 'default project for openstack')
DEFINE_string('default_image', 'ami-11111',
'default image to use, testing only')
DEFINE_string('default_instance_type', 'm1.small',
@@ -260,6 +263,11 @@ DEFINE_string('state_path', os.path.join(os.path.dirname(__file__), '../'),
DEFINE_string('sql_connection',
'sqlite:///$state_path/nova.sqlite',
'connection string for sql database')
+DEFINE_string('sql_idle_timeout',
+ '3600',
+ 'timeout for idle sql database connections')
+DEFINE_integer('sql_max_retries', 12, 'sql connection attempts')
+DEFINE_integer('sql_retry_interval', 10, 'sql connection retry interval')
DEFINE_string('compute_manager', 'nova.compute.manager.ComputeManager',
'Manager for compute')
diff --git a/nova/network/__init__.py b/nova/network/__init__.py
index dcc54db09..6eb3e3ef6 100644
--- a/nova/network/__init__.py
+++ b/nova/network/__init__.py
@@ -16,17 +16,4 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-:mod:`nova.network` -- Network Nodes
-=====================================================
-
-.. automodule:: nova.network
- :platform: Unix
- :synopsis: Network is responsible for managing networking
-.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com>
-.. moduleauthor:: Devin Carlen <devin.carlen@gmail.com>
-.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com>
-.. moduleauthor:: Joshua McKenty <joshua@cognition.ca>
-.. moduleauthor:: Manish Singh <yosh@gimp.org>
-.. moduleauthor:: Andy Smith <andy@anarkystic.com>
-"""
+from nova.network.api import API
diff --git a/nova/network/api.py b/nova/network/api.py
new file mode 100644
index 000000000..cbd912047
--- /dev/null
+++ b/nova/network/api.py
@@ -0,0 +1,87 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# 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.
+
+"""
+Handles all requests relating to instances (guest vms).
+"""
+
+import logging
+
+from nova import db
+from nova import flags
+from nova import quota
+from nova import rpc
+from nova.db import base
+
+FLAGS = flags.FLAGS
+
+
+class API(base.Base):
+ """API for interacting with the network manager."""
+
+ def allocate_floating_ip(self, context):
+ if quota.allowed_floating_ips(context, 1) < 1:
+ logging.warn(_("Quota exceeeded for %s, tried to allocate "
+ "address"),
+ context.project_id)
+ raise quota.QuotaError(_("Address quota exceeded. You cannot "
+ "allocate any more addresses"))
+ # NOTE(vish): We don't know which network host should get the ip
+ # when we allocate, so just send it to any one. This
+ # will probably need to move into a network supervisor
+ # at some point.
+ return rpc.call(context,
+ FLAGS.network_topic,
+ {"method": "allocate_floating_ip",
+ "args": {"project_id": context.project_id}})
+
+ def release_floating_ip(self, context, address):
+ floating_ip = self.db.floating_ip_get_by_address(context, address)
+ # NOTE(vish): We don't know which network host should get the ip
+ # when we deallocate, so just send it to any one. This
+ # will probably need to move into a network supervisor
+ # at some point.
+ rpc.cast(context,
+ FLAGS.network_topic,
+ {"method": "deallocate_floating_ip",
+ "args": {"floating_address": floating_ip['address']}})
+
+ def associate_floating_ip(self, context, floating_ip, fixed_ip):
+ if isinstance(fixed_ip, str) or isinstance(fixed_ip, unicode):
+ fixed_ip = self.db.fixed_ip_get_by_address(context, fixed_ip)
+ floating_ip = self.db.floating_ip_get_by_address(context, floating_ip)
+ # NOTE(vish): Perhaps we should just pass this on to compute and
+ # let compute communicate with network.
+ host = fixed_ip['network']['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.network_topic, host),
+ {"method": "associate_floating_ip",
+ "args": {"floating_address": floating_ip['address'],
+ "fixed_address": fixed_ip['address']}})
+
+ def disassociate_floating_ip(self, context, address):
+ floating_ip = self.db.floating_ip_get_by_address(context, address)
+ if not floating_ip.get('fixed_ip'):
+ raise exception.ApiError('Address is not associated.')
+ # NOTE(vish): Get the topic from the host name of the network of
+ # the associated fixed ip.
+ host = floating_ip['fixed_ip']['network']['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.network_topic, host),
+ {"method": "disassociate_floating_ip",
+ "args": {"floating_address": floating_ip['address']}})
diff --git a/nova/quota.py b/nova/quota.py
index f6ca9f77c..3884eb308 100644
--- a/nova/quota.py
+++ b/nova/quota.py
@@ -22,7 +22,6 @@ Quotas for instances, volumes, and floating ips
from nova import db
from nova import exception
from nova import flags
-from nova.compute import instance_types
FLAGS = flags.FLAGS
@@ -63,10 +62,9 @@ def allowed_instances(context, num_instances, instance_type):
quota = get_quota(context, project_id)
allowed_instances = quota['instances'] - used_instances
allowed_cores = quota['cores'] - used_cores
- type_cores = instance_types.INSTANCE_TYPES[instance_type]['vcpus']
- num_cores = num_instances * type_cores
+ num_cores = num_instances * instance_type['vcpus']
allowed_instances = min(allowed_instances,
- int(allowed_cores // type_cores))
+ int(allowed_cores // instance_type['vcpus']))
return min(num_instances, allowed_instances)
diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py
index 08d7033f5..66e46c1b9 100644
--- a/nova/scheduler/driver.py
+++ b/nova/scheduler/driver.py
@@ -37,6 +37,11 @@ class NoValidHost(exception.Error):
pass
+class WillNotSchedule(exception.Error):
+ """The specified host is not up or doesn't exist."""
+ pass
+
+
class Scheduler(object):
"""The base class that all Scheduler clases should inherit from."""
diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py
index f9171ab35..47baf0d73 100644
--- a/nova/scheduler/simple.py
+++ b/nova/scheduler/simple.py
@@ -43,6 +43,19 @@ class SimpleScheduler(chance.ChanceScheduler):
def schedule_run_instance(self, context, instance_id, *_args, **_kwargs):
"""Picks a host that is up and has the fewest running instances."""
instance_ref = db.instance_get(context, instance_id)
+ if instance_ref['availability_zone'] and context.is_admin:
+ zone, _x, host = instance_ref['availability_zone'].partition(':')
+ service = db.service_get_by_args(context.elevated(), host,
+ 'nova-compute')
+ if not self.service_is_up(service):
+ raise driver.WillNotSchedule("Host %s is not alive" % host)
+
+ # TODO(vish): this probably belongs in the manager, if we
+ # can generalize this somehow
+ now = datetime.datetime.utcnow()
+ db.instance_update(context, instance_id, {'host': host,
+ 'scheduled_at': now})
+ return host
results = db.service_get_all_compute_sorted(context)
for result in results:
(service, instance_cores) = result
@@ -62,6 +75,19 @@ class SimpleScheduler(chance.ChanceScheduler):
def schedule_create_volume(self, context, volume_id, *_args, **_kwargs):
"""Picks a host that is up and has the fewest volumes."""
volume_ref = db.volume_get(context, volume_id)
+ if (':' in volume_ref['availability_zone']) and context.is_admin:
+ zone, _x, host = volume_ref['availability_zone'].partition(':')
+ service = db.service_get_by_args(context.elevated(), host,
+ 'nova-volume')
+ if not self.service_is_up(service):
+ raise driver.WillNotSchedule("Host %s not available" % host)
+
+ # TODO(vish): this probably belongs in the manager, if we
+ # can generalize this somehow
+ now = datetime.datetime.utcnow()
+ db.volume_update(context, volume_id, {'host': host,
+ 'scheduled_at': now})
+ return host
results = db.service_get_all_volume_sorted(context)
for result in results:
(service, volume_gigabytes) = result
diff --git a/nova/service.py b/nova/service.py
index f1f90742f..7203430c6 100644
--- a/nova/service.py
+++ b/nova/service.py
@@ -24,17 +24,21 @@ import inspect
import logging
import os
import sys
+import time
from eventlet import event
from eventlet import greenthread
from eventlet import greenpool
+from sqlalchemy.exc import OperationalError
+
from nova import context
from nova import db
from nova import exception
from nova import flags
from nova import rpc
from nova import utils
+from nova.db.sqlalchemy import models
FLAGS = flags.FLAGS
@@ -204,6 +208,14 @@ class Service(object):
self.model_disconnected = True
logging.exception(_("model server went away"))
+ try:
+ models.register_models()
+ except OperationalError:
+ logging.exception(_("Data store is unreachable."
+ " Trying again in %d seconds.") %
+ FLAGS.sql_retry_interval)
+ time.sleep(FLAGS.sql_retry_interval)
+
def serve(*services):
argv = FLAGS(sys.argv)
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 79663e43a..291a0e468 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -110,6 +110,12 @@ def stub_out_networking(stubs):
stubs.Set(nova.utils, 'get_my_ip', get_my_ip)
+def stub_out_compute_api_snapshot(stubs):
+ def snapshot(self, context, instance_id, name):
+ return 123
+ stubs.Set(nova.compute.API, 'snapshot', snapshot)
+
+
def stub_out_glance(stubs, initial_fixtures=[]):
class FakeParallaxClient:
@@ -213,6 +219,9 @@ class FakeAuthManager(object):
return v
return None
+ def get_project(self, pid):
+ return None
+
def get_user_from_access_key(self, key):
return FakeAuthManager.auth_data.get(key, None)
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 1b4031217..0f274bd15 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -50,7 +50,7 @@ class BaseImageServiceTests(object):
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None}
num_images = len(self.service.index(self.context))
@@ -67,7 +67,7 @@ class BaseImageServiceTests(object):
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None}
num_images = len(self.service.index(self.context))
@@ -87,7 +87,7 @@ class BaseImageServiceTests(object):
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None}
id = self.service.create(self.context, fixture)
@@ -105,13 +105,13 @@ class BaseImageServiceTests(object):
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None},
{'name': 'test image 2',
'updated': None,
'created': None,
'status': None,
- 'serverId': None,
+ 'instance_id': None,
'progress': None}]
num_images = len(self.service.index(self.context))
@@ -155,6 +155,7 @@ class GlanceImageServiceTest(unittest.TestCase,
def setUp(self):
self.stubs = stubout.StubOutForTesting()
fakes.stub_out_glance(self.stubs)
+ fakes.stub_out_compute_api_snapshot(self.stubs)
service_class = 'nova.image.glance.GlanceImageService'
self.service = utils.import_object(service_class)
self.context = context.RequestContext(None, None)
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 5d23db588..6e611a55d 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -56,8 +56,8 @@ def instance_address(context, instance_id):
def stub_instance(id, user_id=1):
- return Instance(id=int(id) + 123456, state=0, image_id=10, user_id=user_id,
- display_name='server%s' % id, internal_id=id)
+ return Instance(id=id, state=0, image_id=10, user_id=user_id,
+ display_name='server%s' % id)
def fake_compute_api(cls, req, id):
@@ -76,8 +76,7 @@ class ServersTest(unittest.TestCase):
fakes.stub_out_key_pair_funcs(self.stubs)
fakes.stub_out_image_service(self.stubs)
self.stubs.Set(nova.db.api, 'instance_get_all', return_servers)
- self.stubs.Set(nova.db.api, 'instance_get_by_internal_id',
- return_server)
+ self.stubs.Set(nova.db.api, 'instance_get_by_id', return_server)
self.stubs.Set(nova.db.api, 'instance_get_all_by_user',
return_servers)
self.stubs.Set(nova.db.api, 'instance_add_security_group',
@@ -87,14 +86,12 @@ class ServersTest(unittest.TestCase):
instance_address)
self.stubs.Set(nova.db.api, 'instance_get_floating_address',
instance_address)
- self.stubs.Set(nova.compute.api.ComputeAPI, 'pause',
- fake_compute_api)
- self.stubs.Set(nova.compute.api.ComputeAPI, 'unpause',
- fake_compute_api)
- self.stubs.Set(nova.compute.api.ComputeAPI, 'suspend',
- fake_compute_api)
- self.stubs.Set(nova.compute.api.ComputeAPI, 'resume',
- fake_compute_api)
+ self.stubs.Set(nova.compute.API, 'pause', fake_compute_api)
+ self.stubs.Set(nova.compute.API, 'unpause', fake_compute_api)
+ self.stubs.Set(nova.compute.API, 'suspend', fake_compute_api)
+ self.stubs.Set(nova.compute.API, 'resume', fake_compute_api)
+ self.stubs.Set(nova.compute.API, "get_diagnostics", fake_compute_api)
+ self.stubs.Set(nova.compute.API, "get_actions", fake_compute_api)
self.allow_admin = FLAGS.allow_admin_api
def tearDown(self):
@@ -105,7 +102,7 @@ class ServersTest(unittest.TestCase):
req = webob.Request.blank('/v1.0/servers/1')
res = req.get_response(nova.api.API('os'))
res_dict = json.loads(res.body)
- self.assertEqual(res_dict['server']['id'], 1)
+ self.assertEqual(res_dict['server']['id'], '1')
self.assertEqual(res_dict['server']['name'], 'server1')
def test_get_server_list(self):
@@ -122,7 +119,7 @@ class ServersTest(unittest.TestCase):
def test_create_instance(self):
def instance_create(context, inst):
- return {'id': 1, 'internal_id': 1, 'display_name': ''}
+ return {'id': '1', 'display_name': ''}
def server_update(context, id, params):
return instance_create(context, id)
@@ -274,6 +271,18 @@ class ServersTest(unittest.TestCase):
res = req.get_response(nova.api.API('os'))
self.assertEqual(res.status_int, 202)
+ def test_server_diagnostics(self):
+ req = webob.Request.blank("/v1.0/servers/1/diagnostics")
+ req.method = "GET"
+ res = req.get_response(nova.api.API("os"))
+ self.assertEqual(res.status_int, 404)
+
+ def test_server_actions(self):
+ req = webob.Request.blank("/v1.0/servers/1/actions")
+ req.method = "GET"
+ res = req.get_response(nova.api.API("os"))
+ self.assertEqual(res.status_int, 404)
+
def test_server_reboot(self):
body = dict(server=dict(
name='server_test', imageId=2, flavorId=2, metadata={},
diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py
index 70d2c44da..42344af1c 100644
--- a/nova/tests/test_cloud.py
+++ b/nova/tests/test_cloud.py
@@ -106,7 +106,7 @@ class CloudTestCase(test.TestCase):
self.cloud.allocate_address(self.context)
inst = db.instance_create(self.context, {'host': FLAGS.host})
fixed = self.network.allocate_fixed_ip(self.context, inst['id'])
- ec2_id = cloud.internal_id_to_ec2_id(inst['internal_id'])
+ ec2_id = cloud.id_to_ec2_id(inst['id'])
self.cloud.associate_address(self.context,
instance_id=ec2_id,
public_ip=address)
@@ -127,9 +127,9 @@ class CloudTestCase(test.TestCase):
result = self.cloud.describe_volumes(self.context)
self.assertEqual(len(result['volumeSet']), 2)
result = self.cloud.describe_volumes(self.context,
- volume_id=[vol2['ec2_id']])
+ volume_id=[vol2['id']])
self.assertEqual(len(result['volumeSet']), 1)
- self.assertEqual(result['volumeSet'][0]['volumeId'], vol2['ec2_id'])
+ self.assertEqual(result['volumeSet'][0]['volumeId'], vol2['id'])
db.volume_destroy(self.context, vol1['id'])
db.volume_destroy(self.context, vol2['id'])
@@ -296,7 +296,7 @@ class CloudTestCase(test.TestCase):
def test_update_of_instance_display_fields(self):
inst = db.instance_create(self.context, {})
- ec2_id = cloud.internal_id_to_ec2_id(inst['internal_id'])
+ ec2_id = cloud.id_to_ec2_id(inst['id'])
self.cloud.update_instance(self.context, ec2_id,
display_name='c00l 1m4g3')
inst = db.instance_get(self.context, inst['id'])
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index bcb8a1526..1d527b8f0 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -22,6 +22,7 @@ Tests For Compute
import datetime
import logging
+from nova import compute
from nova import context
from nova import db
from nova import exception
@@ -29,7 +30,6 @@ from nova import flags
from nova import test
from nova import utils
from nova.auth import manager
-from nova.compute import api as compute_api
FLAGS = flags.FLAGS
@@ -44,7 +44,7 @@ class ComputeTestCase(test.TestCase):
stub_network=True,
network_manager='nova.network.manager.FlatManager')
self.compute = utils.import_object(FLAGS.compute_manager)
- self.compute_api = compute_api.ComputeAPI()
+ self.compute_api = compute.API()
self.manager = manager.AuthManager()
self.user = self.manager.create_user('fake', 'fake', 'fake')
self.project = self.manager.create_project('fake', 'fake', 'fake')
@@ -72,7 +72,7 @@ class ComputeTestCase(test.TestCase):
"""Verify that an instance cannot be created without a display_name."""
cases = [dict(), dict(display_name=None)]
for instance in cases:
- ref = self.compute_api.create_instances(self.context,
+ ref = self.compute_api.create(self.context,
FLAGS.default_instance_type, None, **instance)
try:
self.assertNotEqual(ref[0].display_name, None)
@@ -80,13 +80,13 @@ class ComputeTestCase(test.TestCase):
db.instance_destroy(self.context, ref[0]['id'])
def test_create_instance_associates_security_groups(self):
- """Make sure create_instances associates security groups"""
+ """Make sure create associates security groups"""
values = {'name': 'default',
'description': 'default',
'user_id': self.user.id,
'project_id': self.project.id}
group = db.security_group_create(self.context, values)
- ref = self.compute_api.create_instances(self.context,
+ ref = self.compute_api.create(self.context,
FLAGS.default_instance_type, None, security_group=['default'])
try:
self.assertEqual(len(ref[0]['security_groups']), 1)
@@ -151,6 +151,14 @@ class ComputeTestCase(test.TestCase):
self.compute.reboot_instance(self.context, instance_id)
self.compute.terminate_instance(self.context, instance_id)
+ def test_snapshot(self):
+ """Ensure instance can be snapshotted"""
+ instance_id = self._create_instance()
+ name = "myfakesnapshot"
+ self.compute.run_instance(self.context, instance_id)
+ self.compute.snapshot_instance(self.context, instance_id, name)
+ self.compute.terminate_instance(self.context, instance_id)
+
def test_console_output(self):
"""Make sure we can get console output from instance"""
instance_id = self._create_instance()
diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py
index 8cf2a5e54..b5f9f30ef 100644
--- a/nova/tests/test_quota.py
+++ b/nova/tests/test_quota.py
@@ -27,6 +27,7 @@ from nova import test
from nova import utils
from nova.auth import manager
from nova.api.ec2 import cloud
+from nova.compute import instance_types
FLAGS = flags.FLAGS
@@ -78,14 +79,17 @@ class QuotaTestCase(test.TestCase):
def test_quota_overrides(self):
"""Make sure overriding a projects quotas works"""
- num_instances = quota.allowed_instances(self.context, 100, 'm1.small')
+ num_instances = quota.allowed_instances(self.context, 100,
+ instance_types.INSTANCE_TYPES['m1.small'])
self.assertEqual(num_instances, 2)
db.quota_create(self.context, {'project_id': self.project.id,
'instances': 10})
- num_instances = quota.allowed_instances(self.context, 100, 'm1.small')
+ num_instances = quota.allowed_instances(self.context, 100,
+ instance_types.INSTANCE_TYPES['m1.small'])
self.assertEqual(num_instances, 4)
db.quota_update(self.context, self.project.id, {'cores': 100})
- num_instances = quota.allowed_instances(self.context, 100, 'm1.small')
+ num_instances = quota.allowed_instances(self.context, 100,
+ instance_types.INSTANCE_TYPES['m1.small'])
self.assertEqual(num_instances, 10)
db.quota_destroy(self.context, self.project.id)
diff --git a/nova/tests/test_scheduler.py b/nova/tests/test_scheduler.py
index 78e4a1c77..a9937d797 100644
--- a/nova/tests/test_scheduler.py
+++ b/nova/tests/test_scheduler.py
@@ -19,6 +19,8 @@
Tests For Scheduler
"""
+import datetime
+
from nova import context
from nova import db
from nova import flags
@@ -95,7 +97,7 @@ class SimpleDriverTestCase(test.TestCase):
self.manager.delete_user(self.user)
self.manager.delete_project(self.project)
- def _create_instance(self):
+ def _create_instance(self, **kwargs):
"""Create a test instance"""
inst = {}
inst['image_id'] = 'ami-test'
@@ -106,6 +108,7 @@ class SimpleDriverTestCase(test.TestCase):
inst['mac_address'] = utils.generate_mac()
inst['ami_launch_index'] = 0
inst['vcpus'] = 1
+ inst['availability_zone'] = kwargs.get('availability_zone', None)
return db.instance_create(self.context, inst)['id']
def _create_volume(self):
@@ -114,9 +117,33 @@ class SimpleDriverTestCase(test.TestCase):
vol['image_id'] = 'ami-test'
vol['reservation_id'] = 'r-fakeres'
vol['size'] = 1
+ vol['availability_zone'] = 'test'
return db.volume_create(self.context, vol)['id']
- def test_hosts_are_up(self):
+ def test_doesnt_report_disabled_hosts_as_up(self):
+ """Ensures driver doesn't find hosts before they are enabled"""
+ # NOTE(vish): constructing service without create method
+ # because we are going to use it without queue
+ compute1 = service.Service('host1',
+ 'nova-compute',
+ 'compute',
+ FLAGS.compute_manager)
+ compute1.start()
+ compute2 = service.Service('host2',
+ 'nova-compute',
+ 'compute',
+ FLAGS.compute_manager)
+ compute2.start()
+ s1 = db.service_get_by_args(self.context, 'host1', 'nova-compute')
+ s2 = db.service_get_by_args(self.context, 'host2', 'nova-compute')
+ db.service_update(self.context, s1['id'], {'disabled': True})
+ db.service_update(self.context, s2['id'], {'disabled': True})
+ hosts = self.scheduler.driver.hosts_up(self.context, 'compute')
+ self.assertEqual(0, len(hosts))
+ compute1.kill()
+ compute2.kill()
+
+ def test_reports_enabled_hosts_as_up(self):
"""Ensures driver can find the hosts that are up"""
# NOTE(vish): constructing service without create method
# because we are going to use it without queue
@@ -131,7 +158,7 @@ class SimpleDriverTestCase(test.TestCase):
FLAGS.compute_manager)
compute2.start()
hosts = self.scheduler.driver.hosts_up(self.context, 'compute')
- self.assertEqual(len(hosts), 2)
+ self.assertEqual(2, len(hosts))
compute1.kill()
compute2.kill()
@@ -158,6 +185,63 @@ class SimpleDriverTestCase(test.TestCase):
compute1.kill()
compute2.kill()
+ def test_specific_host_gets_instance(self):
+ """Ensures if you set availability_zone it launches on that zone"""
+ compute1 = service.Service('host1',
+ 'nova-compute',
+ 'compute',
+ FLAGS.compute_manager)
+ compute1.start()
+ compute2 = service.Service('host2',
+ 'nova-compute',
+ 'compute',
+ FLAGS.compute_manager)
+ compute2.start()
+ instance_id1 = self._create_instance()
+ compute1.run_instance(self.context, instance_id1)
+ instance_id2 = self._create_instance(availability_zone='nova:host1')
+ host = self.scheduler.driver.schedule_run_instance(self.context,
+ instance_id2)
+ self.assertEqual('host1', host)
+ compute1.terminate_instance(self.context, instance_id1)
+ db.instance_destroy(self.context, instance_id2)
+ compute1.kill()
+ compute2.kill()
+
+ def test_wont_sechedule_if_specified_host_is_down(self):
+ compute1 = service.Service('host1',
+ 'nova-compute',
+ 'compute',
+ FLAGS.compute_manager)
+ compute1.start()
+ s1 = db.service_get_by_args(self.context, 'host1', 'nova-compute')
+ now = datetime.datetime.utcnow()
+ delta = datetime.timedelta(seconds=FLAGS.service_down_time * 2)
+ past = now - delta
+ db.service_update(self.context, s1['id'], {'updated_at': past})
+ instance_id2 = self._create_instance(availability_zone='nova:host1')
+ self.assertRaises(driver.WillNotSchedule,
+ self.scheduler.driver.schedule_run_instance,
+ self.context,
+ instance_id2)
+ db.instance_destroy(self.context, instance_id2)
+ compute1.kill()
+
+ def test_will_schedule_on_disabled_host_if_specified(self):
+ compute1 = service.Service('host1',
+ 'nova-compute',
+ 'compute',
+ FLAGS.compute_manager)
+ compute1.start()
+ s1 = db.service_get_by_args(self.context, 'host1', 'nova-compute')
+ db.service_update(self.context, s1['id'], {'disabled': True})
+ instance_id2 = self._create_instance(availability_zone='nova:host1')
+ host = self.scheduler.driver.schedule_run_instance(self.context,
+ instance_id2)
+ self.assertEqual('host1', host)
+ db.instance_destroy(self.context, instance_id2)
+ compute1.kill()
+
def test_too_many_cores(self):
"""Ensures we don't go over max cores"""
compute1 = service.Service('host1',
diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py
index b30838ad7..9f1a181a0 100644
--- a/nova/tests/test_service.py
+++ b/nova/tests/test_service.py
@@ -22,6 +22,8 @@ Unit Tests for remote procedure calls using queue
import mox
+from nova import context
+from nova import db
from nova import exception
from nova import flags
from nova import rpc
@@ -72,6 +74,30 @@ class ServiceManagerTestCase(test.TestCase):
self.assertEqual(serv.test_method(), 'service')
+class ServiceFlagsTestCase(test.TestCase):
+ def test_service_enabled_on_create_based_on_flag(self):
+ self.flags(enable_new_services=True)
+ host = 'foo'
+ binary = 'nova-fake'
+ app = service.Service.create(host=host, binary=binary)
+ app.start()
+ app.stop()
+ ref = db.service_get(context.get_admin_context(), app.service_id)
+ db.service_destroy(context.get_admin_context(), app.service_id)
+ self.assert_(not ref['disabled'])
+
+ def test_service_disabled_on_create_based_on_flag(self):
+ self.flags(enable_new_services=False)
+ host = 'foo'
+ binary = 'nova-fake'
+ app = service.Service.create(host=host, binary=binary)
+ app.start()
+ app.stop()
+ ref = db.service_get(context.get_admin_context(), app.service_id)
+ db.service_destroy(context.get_admin_context(), app.service_id)
+ self.assert_(ref['disabled'])
+
+
class ServiceTestCase(test.TestCase):
"""Test cases for Services"""
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index ed2e4ffde..ec9462ada 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -29,9 +29,9 @@ from nova.auth import manager
from nova.compute import instance_types
from nova.compute import power_state
from nova.virt import xenapi_conn
-from nova.virt.xenapi import fake
+from nova.virt.xenapi import fake as xenapi_fake
from nova.virt.xenapi import volume_utils
-from nova.tests.db import fakes
+from nova.tests.db import fakes as db_fakes
from nova.tests.xenapi import stubs
FLAGS = flags.FLAGS
@@ -47,9 +47,9 @@ class XenAPIVolumeTestCase(test.TestCase):
FLAGS.target_host = '127.0.0.1'
FLAGS.xenapi_connection_url = 'test_url'
FLAGS.xenapi_connection_password = 'test_pass'
- fakes.stub_out_db_instance_api(self.stubs)
+ db_fakes.stub_out_db_instance_api(self.stubs)
stubs.stub_out_get_target(self.stubs)
- fake.reset()
+ xenapi_fake.reset()
self.values = {'name': 1, 'id': 1,
'project_id': 'fake',
'user_id': 'fake',
@@ -79,11 +79,11 @@ class XenAPIVolumeTestCase(test.TestCase):
helper = volume_utils.VolumeHelper
helper.XenAPI = session.get_imported_xenapi()
vol = self._create_volume()
- info = helper.parse_volume_info(vol['ec2_id'], '/dev/sdc')
- label = 'SR-%s' % vol['ec2_id']
+ info = helper.parse_volume_info(vol['id'], '/dev/sdc')
+ label = 'SR-%s' % vol['id']
description = 'Test-SR'
sr_ref = helper.create_iscsi_storage(session, info, label, description)
- srs = fake.get_all('SR')
+ srs = xenapi_fake.get_all('SR')
self.assertEqual(sr_ref, srs[0])
db.volume_destroy(context.get_admin_context(), vol['id'])
@@ -97,7 +97,7 @@ class XenAPIVolumeTestCase(test.TestCase):
# oops, wrong mount point!
self.assertRaises(volume_utils.StorageError,
helper.parse_volume_info,
- vol['ec2_id'],
+ vol['id'],
'/dev/sd')
db.volume_destroy(context.get_admin_context(), vol['id'])
@@ -107,17 +107,16 @@ class XenAPIVolumeTestCase(test.TestCase):
conn = xenapi_conn.get_connection(False)
volume = self._create_volume()
instance = db.instance_create(self.values)
- fake.create_vm(instance.name, 'Running')
- result = conn.attach_volume(instance.name, volume['ec2_id'],
- '/dev/sdc')
+ xenapi_fake.create_vm(instance.name, 'Running')
+ result = conn.attach_volume(instance.name, volume['id'], '/dev/sdc')
def check():
# check that the VM has a VBD attached to it
# Get XenAPI reference for the VM
- vms = fake.get_all('VM')
+ vms = xenapi_fake.get_all('VM')
# Get XenAPI record for VBD
- vbds = fake.get_all('VBD')
- vbd = fake.get_record('VBD', vbds[0])
+ vbds = xenapi_fake.get_all('VBD')
+ vbd = xenapi_fake.get_record('VBD', vbds[0])
vm_ref = vbd['VM']
self.assertEqual(vm_ref, vms[0])
@@ -130,11 +129,11 @@ class XenAPIVolumeTestCase(test.TestCase):
conn = xenapi_conn.get_connection(False)
volume = self._create_volume()
instance = db.instance_create(self.values)
- fake.create_vm(instance.name, 'Running')
+ xenapi_fake.create_vm(instance.name, 'Running')
self.assertRaises(Exception,
conn.attach_volume,
instance.name,
- volume['ec2_id'],
+ volume['id'],
'/dev/sdc')
def tearDown(self):
@@ -156,41 +155,70 @@ class XenAPIVMTestCase(test.TestCase):
self.stubs = stubout.StubOutForTesting()
FLAGS.xenapi_connection_url = 'test_url'
FLAGS.xenapi_connection_password = 'test_pass'
- fake.reset()
- fakes.stub_out_db_instance_api(self.stubs)
- fake.create_network('fake', FLAGS.flat_network_bridge)
+ xenapi_fake.reset()
+ db_fakes.stub_out_db_instance_api(self.stubs)
+ xenapi_fake.create_network('fake', FLAGS.flat_network_bridge)
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
+ self.conn = xenapi_conn.get_connection(False)
def test_list_instances_0(self):
- stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
- conn = xenapi_conn.get_connection(False)
- instances = conn.list_instances()
+ instances = self.conn.list_instances()
self.assertEquals(instances, [])
+ def test_get_diagnostics(self):
+ instance = self._create_instance()
+ self.conn.get_diagnostics(instance)
+
+ def test_instance_snapshot(self):
+ stubs.stubout_instance_snapshot(self.stubs)
+ instance = self._create_instance()
+
+ name = "MySnapshot"
+ template_vm_ref = self.conn.snapshot(instance, name)
+
+ def ensure_vm_was_torn_down():
+ vm_labels = []
+ for vm_ref in xenapi_fake.get_all('VM'):
+ vm_rec = xenapi_fake.get_record('VM', vm_ref)
+ if not vm_rec["is_control_domain"]:
+ vm_labels.append(vm_rec["name_label"])
+
+ self.assertEquals(vm_labels, [1])
+
+ def ensure_vbd_was_torn_down():
+ vbd_labels = []
+ for vbd_ref in xenapi_fake.get_all('VBD'):
+ vbd_rec = xenapi_fake.get_record('VBD', vbd_ref)
+ vbd_labels.append(vbd_rec["vm_name_label"])
+
+ self.assertEquals(vbd_labels, [1])
+
+ def ensure_vdi_was_torn_down():
+ for vdi_ref in xenapi_fake.get_all('VDI'):
+ vdi_rec = xenapi_fake.get_record('VDI', vdi_ref)
+ name_label = vdi_rec["name_label"]
+ self.assert_(not name_label.endswith('snapshot'))
+
+ def check():
+ ensure_vm_was_torn_down()
+ ensure_vbd_was_torn_down()
+ ensure_vdi_was_torn_down()
+
+ check()
+
def test_spawn(self):
- stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
- values = {'name': 1, 'id': 1,
- 'project_id': self.project.id,
- 'user_id': self.user.id,
- 'image_id': 1,
- 'kernel_id': 2,
- 'ramdisk_id': 3,
- 'instance_type': 'm1.large',
- 'mac_address': 'aa:bb:cc:dd:ee:ff',
- }
- conn = xenapi_conn.get_connection(False)
- instance = db.instance_create(values)
- conn.spawn(instance)
+ instance = self._create_instance()
def check():
- instances = conn.list_instances()
+ instances = self.conn.list_instances()
self.assertEquals(instances, [1])
# Get Nova record for VM
- vm_info = conn.get_info(1)
+ vm_info = self.conn.get_info(1)
# Get XenAPI record for VM
- vms = fake.get_all('VM')
- vm = fake.get_record('VM', vms[0])
+ vms = xenapi_fake.get_all('VM')
+ vm = xenapi_fake.get_record('VM', vms[0])
# Check that m1.large above turned into the right thing.
instance_type = instance_types.INSTANCE_TYPES['m1.large']
@@ -218,3 +246,19 @@ class XenAPIVMTestCase(test.TestCase):
self.manager.delete_project(self.project)
self.manager.delete_user(self.user)
self.stubs.UnsetAll()
+
+ def _create_instance(self):
+ """Creates and spawns a test instance"""
+ values = {
+ 'name': 1,
+ 'id': 1,
+ 'project_id': self.project.id,
+ 'user_id': self.user.id,
+ 'image_id': 1,
+ 'kernel_id': 2,
+ 'ramdisk_id': 3,
+ 'instance_type': 'm1.large',
+ 'mac_address': 'aa:bb:cc:dd:ee:ff'}
+ instance = db.instance_create(values)
+ self.conn.spawn(instance)
+ return instance
diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py
index a7e592fee..55f751f11 100644
--- a/nova/tests/xenapi/stubs.py
+++ b/nova/tests/xenapi/stubs.py
@@ -19,6 +19,54 @@
from nova.virt import xenapi_conn
from nova.virt.xenapi import fake
from nova.virt.xenapi import volume_utils
+from nova.virt.xenapi import vm_utils
+
+
+def stubout_instance_snapshot(stubs):
+ @classmethod
+ def fake_fetch_image(cls, session, instance_id, image, user, project,
+ type):
+ # Stubout wait_for_task
+ def fake_wait_for_task(self, id, task):
+ class FakeEvent:
+
+ def send(self, value):
+ self.rv = value
+
+ def wait(self):
+ return self.rv
+
+ done = FakeEvent()
+ self._poll_task(id, task, done)
+ rv = done.wait()
+ return rv
+
+ stubs.Set(xenapi_conn.XenAPISession, 'wait_for_task',
+ fake_wait_for_task)
+
+ from nova.virt.xenapi.fake import create_vdi
+ name_label = "instance-%s" % instance_id
+ #TODO: create fake SR record
+ sr_ref = "fakesr"
+ vdi_ref = create_vdi(name_label=name_label, read_only=False,
+ sr_ref=sr_ref, sharable=False)
+ vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref)
+ vdi_uuid = vdi_rec['uuid']
+ return vdi_uuid
+
+ stubs.Set(vm_utils.VMHelper, 'fetch_image', fake_fetch_image)
+
+ def fake_parse_xmlrpc_value(val):
+ return val
+
+ stubs.Set(xenapi_conn, '_parse_xmlrpc_value', fake_parse_xmlrpc_value)
+
+ def fake_wait_for_vhd_coalesce(session, instance_id, sr_ref, vdi_ref,
+ original_parent_uuid):
+ #TODO(sirp): Should we actually fake out the data here
+ return "fakeparent"
+
+ stubs.Set(vm_utils, 'wait_for_vhd_coalesce', fake_wait_for_vhd_coalesce)
def stubout_session(stubs, cls):
@@ -63,6 +111,24 @@ class FakeSessionForVMTests(fake.SessionBase):
vm['is_a_template'] = False
vm['is_control_domain'] = False
+ def VM_snapshot(self, session_ref, vm_ref, label):
+ status = "Running"
+ template_vm_ref = fake.create_vm(label, status, is_a_template=True,
+ is_control_domain=False)
+
+ sr_ref = "fakesr"
+ template_vdi_ref = fake.create_vdi(label, read_only=True,
+ sr_ref=sr_ref, sharable=False)
+
+ template_vbd_ref = fake.create_vbd(template_vm_ref, template_vdi_ref)
+ return template_vm_ref
+
+ def VDI_destroy(self, session_ref, vdi_ref):
+ fake.destroy_vdi(vdi_ref)
+
+ def VM_destroy(self, session_ref, vm_ref):
+ fake.destroy_vm(vm_ref)
+
class FakeSessionForVolumeTests(fake.SessionBase):
""" Stubs out a XenAPISession for Volume tests """
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 706888b0d..32541f5b4 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -112,6 +112,20 @@ class FakeConnection(object):
self.instances[instance.name] = fake_instance
fake_instance._state = power_state.RUNNING
+ def snapshot(self, instance, name):
+ """
+ Snapshots the specified instance.
+
+ The given parameter is an instance of nova.compute.service.Instance,
+ and so the instance is being specified as instance.name.
+
+ The second parameter is the name of the snapshot.
+
+ The work will be done asynchronously. This function returns a
+ Deferred that allows the caller to detect when it is complete.
+ """
+ pass
+
def reboot(self, instance):
"""
Reboot the specified instance.
@@ -202,6 +216,9 @@ class FakeConnection(object):
'num_cpu': 2,
'cpu_time': 0}
+ def get_diagnostics(self, instance_name):
+ pass
+
def list_disks(self, instance_name):
"""
Return the IDs of all the virtual disks attached to the specified
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index 39215c4e1..00edfbdc8 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -260,6 +260,13 @@ class LibvirtConnection(object):
virt_dom.detachDevice(xml)
@exception.wrap_exception
+ def snapshot(self, instance, name):
+ """ Create snapshot from a running VM instance """
+ raise NotImplementedError(
+ _("Instance snapshotting is not supported for libvirt"
+ "at this time"))
+
+ @exception.wrap_exception
def reboot(self, instance):
self.destroy(instance, False)
xml = self.to_xml(instance)
@@ -580,6 +587,9 @@ class LibvirtConnection(object):
'num_cpu': num_cpu,
'cpu_time': cpu_time}
+ def get_diagnostics(self, instance_name):
+ raise exception.APIError("diagnostics are not supported for libvirt")
+
def get_disks(self, instance_name):
"""
Note that this function takes an instance name, not an Instance, so
diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py
index 1eaf31c25..aa4026f97 100644
--- a/nova/virt/xenapi/fake.py
+++ b/nova/virt/xenapi/fake.py
@@ -55,6 +55,8 @@ import datetime
import logging
import uuid
+from pprint import pformat
+
from nova import exception
@@ -64,6 +66,10 @@ _CLASSES = ['host', 'network', 'session', 'SR', 'VBD',\
_db_content = {}
+def log_db_contents(msg=None):
+ logging.debug(_("%s: _db_content => %s"), msg or "", pformat(_db_content))
+
+
def reset():
for c in _CLASSES:
_db_content[c] = {}
@@ -93,6 +99,24 @@ def create_vm(name_label, status,
})
+def destroy_vm(vm_ref):
+ vm_rec = _db_content['VM'][vm_ref]
+
+ vbd_refs = vm_rec['VBDs']
+ for vbd_ref in vbd_refs:
+ destroy_vbd(vbd_ref)
+
+ del _db_content['VM'][vm_ref]
+
+
+def destroy_vbd(vbd_ref):
+ del _db_content['VBD'][vbd_ref]
+
+
+def destroy_vdi(vdi_ref):
+ del _db_content['VDI'][vdi_ref]
+
+
def create_vdi(name_label, read_only, sr_ref, sharable):
return _create_object('VDI', {
'name_label': name_label,
@@ -109,6 +133,23 @@ def create_vdi(name_label, read_only, sr_ref, sharable):
})
+def create_vbd(vm_ref, vdi_ref):
+ vbd_rec = {'VM': vm_ref, 'VDI': vdi_ref}
+ vbd_ref = _create_object('VBD', vbd_rec)
+ after_VBD_create(vbd_ref, vbd_rec)
+ return vbd_ref
+
+
+def after_VBD_create(vbd_ref, vbd_rec):
+ """Create backref from VM to VBD when VBD is created"""
+ vm_ref = vbd_rec['VM']
+ vm_rec = _db_content['VM'][vm_ref]
+ vm_rec['VBDs'] = [vbd_ref]
+
+ vm_name_label = _db_content['VM'][vm_ref]['name_label']
+ vbd_rec['vm_name_label'] = vm_name_label
+
+
def create_pbd(config, sr_ref, attached):
return _create_object('PBD', {
'device-config': config,
@@ -277,11 +318,12 @@ class SessionBase(object):
self._check_arg_count(params, 2)
return get_record(cls, params[1])
- if (func == 'get_by_name_label' or
- func == 'get_by_uuid'):
+ if func in ('get_by_name_label', 'get_by_uuid'):
self._check_arg_count(params, 2)
+ return_singleton = (func == 'get_by_uuid')
return self._get_by_field(
- _db_content[cls], func[len('get_by_'):], params[1])
+ _db_content[cls], func[len('get_by_'):], params[1],
+ return_singleton=return_singleton)
if len(params) == 2:
field = func[len('get_'):]
@@ -324,6 +366,13 @@ class SessionBase(object):
(cls, _) = name.split('.')
ref = is_sr_create and \
_create_sr(cls, params) or _create_object(cls, params[1])
+
+ # Call hook to provide any fixups needed (ex. creating backrefs)
+ try:
+ globals()["after_%s_create" % cls](ref, params[1])
+ except KeyError:
+ pass
+
obj = get_record(cls, ref)
# Add RO fields
@@ -359,11 +408,18 @@ class SessionBase(object):
raise Failure(['MESSAGE_PARAMETER_COUNT_MISMATCH',
expected, actual])
- def _get_by_field(self, recs, k, v):
+ def _get_by_field(self, recs, k, v, return_singleton):
result = []
for ref, rec in recs.iteritems():
if rec.get(k) == v:
result.append(ref)
+
+ if return_singleton:
+ try:
+ return result[0]
+ except IndexError:
+ return None
+
return result
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 47fb6db53..9d1b51848 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -20,11 +20,14 @@ their attributes like VDIs, VIFs, as well as their lookup functions.
"""
import logging
+import pickle
import urllib
from xml.dom import minidom
+from eventlet import event
from nova import exception
from nova import flags
+from nova import utils
from nova.auth.manager import AuthManager
from nova.compute import instance_types
from nova.compute import power_state
@@ -204,7 +207,54 @@ class VMHelper(HelperBase):
return vif_ref
@classmethod
- def fetch_image(cls, session, image, user, project, type):
+ def create_snapshot(cls, session, instance_id, vm_ref, label):
+ """ Creates Snapshot (Template) VM, Snapshot VBD, Snapshot VDI,
+ Snapshot VHD
+ """
+ #TODO(sirp): Add quiesce and VSS locking support when Windows support
+ # is added
+ logging.debug(_("Snapshotting VM %s with label '%s'..."),
+ vm_ref, label)
+
+ vm_vdi_ref, vm_vdi_rec = get_vdi_for_vm_safely(session, vm_ref)
+ vm_vdi_uuid = vm_vdi_rec["uuid"]
+ sr_ref = vm_vdi_rec["SR"]
+
+ original_parent_uuid = get_vhd_parent_uuid(session, vm_vdi_ref)
+
+ task = session.call_xenapi('Async.VM.snapshot', vm_ref, label)
+ template_vm_ref = session.wait_for_task(instance_id, task)
+ template_vdi_rec = get_vdi_for_vm_safely(session, template_vm_ref)[1]
+ template_vdi_uuid = template_vdi_rec["uuid"]
+
+ logging.debug(_('Created snapshot %s from VM %s.'), template_vm_ref,
+ vm_ref)
+
+ parent_uuid = wait_for_vhd_coalesce(
+ session, instance_id, sr_ref, vm_vdi_ref, original_parent_uuid)
+
+ #TODO(sirp): we need to assert only one parent, not parents two deep
+ return template_vm_ref, [template_vdi_uuid, parent_uuid]
+
+ @classmethod
+ def upload_image(cls, session, instance_id, vdi_uuids, image_name):
+ """ Requests that the Glance plugin bundle the specified VDIs and
+ push them into Glance using the specified human-friendly name.
+ """
+ logging.debug(_("Asking xapi to upload %s as '%s'"),
+ vdi_uuids, image_name)
+
+ params = {'vdi_uuids': vdi_uuids,
+ 'image_name': image_name,
+ 'glance_host': FLAGS.glance_host,
+ 'glance_port': FLAGS.glance_port}
+
+ kwargs = {'params': pickle.dumps(params)}
+ task = session.async_call_plugin('glance', 'put_vdis', kwargs)
+ session.wait_for_task(instance_id, task)
+
+ @classmethod
+ def fetch_image(cls, session, instance_id, image, user, project, type):
"""
type is interpreted as an ImageType instance
"""
@@ -223,9 +273,7 @@ class VMHelper(HelperBase):
if type == ImageType.DISK_RAW:
args['raw'] = 'true'
task = session.async_call_plugin('objectstore', fn, args)
- #FIXME(armando): find a solution to missing instance_id
- #with Josh Kearney
- uuid = session.wait_for_task(0, task)
+ uuid = session.wait_for_task(instance_id, task)
return uuid
@classmethod
@@ -299,6 +347,10 @@ class VMHelper(HelperBase):
try:
host = session.get_xenapi_host()
host_ip = session.get_xenapi().host.get_record(host)["address"]
+ except (cls.XenAPI.Failure, KeyError) as e:
+ return {"Unable to retrieve diagnostics": e}
+
+ try:
diags = {}
xml = get_rrd(host_ip, record["uuid"])
if xml:
@@ -325,3 +377,87 @@ def get_rrd(host, uuid):
return xml.read()
except IOError:
return None
+
+
+#TODO(sirp): This code comes from XS5.6 pluginlib.py, we should refactor to
+# use that implmenetation
+def get_vhd_parent(session, vdi_rec):
+ """
+ Returns the VHD parent of the given VDI record, as a (ref, rec) pair.
+ Returns None if we're at the root of the tree.
+ """
+ if 'vhd-parent' in vdi_rec['sm_config']:
+ parent_uuid = vdi_rec['sm_config']['vhd-parent']
+ #NOTE(sirp): changed xenapi -> get_xenapi()
+ parent_ref = session.get_xenapi().VDI.get_by_uuid(parent_uuid)
+ parent_rec = session.get_xenapi().VDI.get_record(parent_ref)
+ #NOTE(sirp): changed log -> logging
+ logging.debug(_("VHD %s has parent %s"), vdi_rec['uuid'], parent_ref)
+ return parent_ref, parent_rec
+ else:
+ return None
+
+
+def get_vhd_parent_uuid(session, vdi_ref):
+ vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref)
+ ret = get_vhd_parent(session, vdi_rec)
+ if ret:
+ parent_ref, parent_rec = ret
+ return parent_rec["uuid"]
+ else:
+ return None
+
+
+def scan_sr(session, instance_id, sr_ref):
+ logging.debug(_("Re-scanning SR %s"), sr_ref)
+ task = session.call_xenapi('Async.SR.scan', sr_ref)
+ session.wait_for_task(instance_id, task)
+
+
+def wait_for_vhd_coalesce(session, instance_id, sr_ref, vdi_ref,
+ original_parent_uuid):
+ """ Spin until the parent VHD is coalesced into its parent VHD
+
+ Before coalesce:
+ * original_parent_vhd
+ * parent_vhd
+ snapshot
+
+ Atter coalesce:
+ * parent_vhd
+ snapshot
+ """
+ #TODO(sirp): we need to timeout this req after a while
+
+ def _poll_vhds():
+ scan_sr(session, instance_id, sr_ref)
+ parent_uuid = get_vhd_parent_uuid(session, vdi_ref)
+ if original_parent_uuid and (parent_uuid != original_parent_uuid):
+ logging.debug(
+ _("Parent %s doesn't match original parent %s, "
+ "waiting for coalesce..."),
+ parent_uuid, original_parent_uuid)
+ else:
+ done.send(parent_uuid)
+
+ done = event.Event()
+ loop = utils.LoopingCall(_poll_vhds)
+ loop.start(FLAGS.xenapi_vhd_coalesce_poll_interval, now=True)
+ parent_uuid = done.wait()
+ loop.stop()
+ return parent_uuid
+
+
+def get_vdi_for_vm_safely(session, vm_ref):
+ vdi_refs = VMHelper.lookup_vm_vdis(session, vm_ref)
+ if vdi_refs is None:
+ raise Exception(_("No VDIs found for VM %s") % vm_ref)
+ else:
+ num_vdis = len(vdi_refs)
+ if num_vdis != 1:
+ raise Exception(_("Unexpected number of VDIs (%s) found for "
+ "VM %s") % (num_vdis, vm_ref))
+
+ vdi_ref = vdi_refs[0]
+ vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref)
+ return vdi_ref, vdi_rec
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index ba502ffa2..b6d620782 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2010 Citrix Systems, Inc.
+# Copyright 2010 OpenStack LLC.
#
# 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
@@ -18,6 +19,7 @@
Management class for VM-related functions (spawn, reboot, etc).
"""
+import json
import logging
from nova import db
@@ -36,7 +38,6 @@ class VMOps(object):
"""
Management class for VM-related tasks
"""
-
def __init__(self, session):
self.XenAPI = session.get_imported_xenapi()
self._session = session
@@ -70,7 +71,7 @@ class VMOps(object):
disk_image_type = ImageType.DISK
else:
disk_image_type = ImageType.DISK_RAW
- vdi_uuid = VMHelper.fetch_image(self._session,
+ vdi_uuid = VMHelper.fetch_image(self._session, instance.id,
instance.image_id, user, project, disk_image_type)
vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid)
#Have a look at the VDI and see if it has a PV kernel
@@ -79,11 +80,11 @@ class VMOps(object):
pv_kernel = VMHelper.lookup_image(self._session, vdi_ref)
kernel = None
if instance.kernel_id:
- kernel = VMHelper.fetch_image(self._session,
+ kernel = VMHelper.fetch_image(self._session, instance.id,
instance.kernel_id, user, project, ImageType.KERNEL_RAMDISK)
ramdisk = None
if instance.ramdisk_id:
- ramdisk = VMHelper.fetch_image(self._session,
+ ramdisk = VMHelper.fetch_image(self._session, instance.id,
instance.ramdisk_id, user, project, ImageType.KERNEL_RAMDISK)
vm_ref = VMHelper.create_vm(self._session,
instance, kernel, ramdisk, pv_kernel)
@@ -120,71 +121,124 @@ class VMOps(object):
timer.f = _wait_for_boot
return timer.start(interval=0.5, now=True)
+ def _get_vm_opaque_ref(self, instance_or_vm):
+ """Refactored out the common code of many methods that receive either
+ a vm name or a vm instance, and want a vm instance in return.
+ """
+ try:
+ instance_name = instance_or_vm.name
+ vm = VMHelper.lookup(self._session, instance_name)
+ except AttributeError:
+ # A vm opaque ref was passed
+ vm = instance_or_vm
+ if vm is None:
+ raise Exception(_('Instance not present %s') % instance_name)
+ return vm
+
+ def snapshot(self, instance, name):
+ """ Create snapshot from a running VM instance
+
+ :param instance: instance to be snapshotted
+ :param name: name/label to be given to the snapshot
+
+ Steps involved in a XenServer snapshot:
+
+ 1. XAPI-Snapshot: Snapshotting the instance using XenAPI. This
+ creates: Snapshot (Template) VM, Snapshot VBD, Snapshot VDI,
+ Snapshot VHD
+
+ 2. Wait-for-coalesce: The Snapshot VDI and Instance VDI both point to
+ a 'base-copy' VDI. The base_copy is immutable and may be chained
+ with other base_copies. If chained, the base_copies
+ coalesce together, so, we must wait for this coalescing to occur to
+ get a stable representation of the data on disk.
+
+ 3. Push-to-glance: Once coalesced, we call a plugin on the XenServer
+ that will bundle the VHDs together and then push the bundle into
+ Glance.
+ """
+
+ #TODO(sirp): Add quiesce and VSS locking support when Windows support
+ # is added
+
+ logging.debug(_("Starting snapshot for VM %s"), instance)
+ vm_ref = VMHelper.lookup(self._session, instance.name)
+
+ label = "%s-snapshot" % instance.name
+ try:
+ template_vm_ref, template_vdi_uuids = VMHelper.create_snapshot(
+ self._session, instance.id, vm_ref, label)
+ except self.XenAPI.Failure, exc:
+ logging.error(_("Unable to Snapshot %s: %s"), vm_ref, exc)
+ return
+
+ try:
+ # call plugin to ship snapshot off to glance
+ VMHelper.upload_image(
+ self._session, instance.id, template_vdi_uuids, name)
+ finally:
+ self._destroy(instance, template_vm_ref, shutdown=False)
+
+ logging.debug(_("Finished snapshot and upload for VM %s"), instance)
+
def reboot(self, instance):
"""Reboot VM instance"""
- instance_name = instance.name
- vm = VMHelper.lookup(self._session, instance_name)
- if vm is None:
- raise exception.NotFound(_('instance not'
- ' found %s') % instance_name)
+ vm = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.clean_reboot', vm)
self._session.wait_for_task(instance.id, task)
def destroy(self, instance):
"""Destroy VM instance"""
vm = VMHelper.lookup(self._session, instance.name)
+ return self._destroy(instance, vm, shutdown=True)
+
+ def _destroy(self, instance, vm, shutdown=True):
+ """ Destroy VM instance """
if vm is None:
# Don't complain, just return. This lets us clean up instances
# that have already disappeared from the underlying platform.
return
# Get the VDIs related to the VM
vdis = VMHelper.lookup_vm_vdis(self._session, vm)
- try:
- task = self._session.call_xenapi('Async.VM.hard_shutdown',
- vm)
- self._session.wait_for_task(instance.id, task)
- except XenAPI.Failure, exc:
- logging.warn(exc)
+ if shutdown:
+ try:
+ task = self._session.call_xenapi('Async.VM.hard_shutdown', vm)
+ self._session.wait_for_task(instance.id, task)
+ except self.XenAPI.Failure, exc:
+ logging.warn(exc)
+
# Disk clean-up
if vdis:
for vdi in vdis:
try:
task = self._session.call_xenapi('Async.VDI.destroy', vdi)
self._session.wait_for_task(instance.id, task)
- except XenAPI.Failure, exc:
+ except self.XenAPI.Failure, exc:
logging.warn(exc)
# VM Destroy
try:
task = self._session.call_xenapi('Async.VM.destroy', vm)
self._session.wait_for_task(instance.id, task)
- except XenAPI.Failure, exc:
+ except self.XenAPI.Failure, exc:
logging.warn(exc)
def _wait_with_callback(self, instance_id, task, callback):
ret = None
try:
ret = self._session.wait_for_task(instance_id, task)
- except XenAPI.Failure, exc:
+ except self.XenAPI.Failure, exc:
logging.warn(exc)
callback(ret)
def pause(self, instance, callback):
"""Pause VM instance"""
- instance_name = instance.name
- vm = VMHelper.lookup(self._session, instance_name)
- if vm is None:
- raise exception.NotFound(_('Instance not'
- ' found %s') % instance_name)
+ vm = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.pause', vm)
self._wait_with_callback(instance.id, task, callback)
def unpause(self, instance, callback):
"""Unpause VM instance"""
- instance_name = instance.name
- vm = VMHelper.lookup(self._session, instance_name)
- if vm is None:
- raise exception.NotFound(_('Instance not'
- ' found %s') % instance_name)
+ vm = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.unpause', vm)
self._wait_with_callback(instance.id, task, callback)
@@ -217,11 +271,9 @@ class VMOps(object):
rec = self._session.get_xenapi().VM.get_record(vm)
return VMHelper.compile_info(rec)
- def get_diagnostics(self, instance_id):
+ def get_diagnostics(self, instance):
"""Return data about VM diagnostics"""
- vm = VMHelper.lookup(self._session, instance_id)
- if vm is None:
- raise exception.NotFound(_("Instance not found %s") % instance_id)
+ vm = self._get_vm_opaque_ref(instance)
rec = self._session.get_xenapi().VM.get_record(vm)
return VMHelper.compile_diagnostics(self._session, rec)
@@ -229,3 +281,175 @@ class VMOps(object):
"""Return snapshot of console"""
# TODO: implement this to fix pylint!
return 'FAKE CONSOLE OUTPUT of instance'
+
+ def list_from_xenstore(self, vm, path):
+ """Runs the xenstore-ls command to get a listing of all records
+ from 'path' downward. Returns a dict with the sub-paths as keys,
+ and the value stored in those paths as values. If nothing is
+ found at that path, returns None.
+ """
+ ret = self._make_xenstore_call('list_records', vm, path)
+ return json.loads(ret)
+
+ def read_from_xenstore(self, vm, path):
+ """Returns the value stored in the xenstore record for the given VM
+ at the specified location. A XenAPIPlugin.PluginError will be raised
+ if any error is encountered in the read process.
+ """
+ try:
+ ret = self._make_xenstore_call('read_record', vm, path,
+ {'ignore_missing_path': 'True'})
+ except self.XenAPI.Failure, e:
+ return None
+ ret = json.loads(ret)
+ if ret == "None":
+ # Can't marshall None over RPC calls.
+ return None
+ return ret
+
+ def write_to_xenstore(self, vm, path, value):
+ """Writes the passed value to the xenstore record for the given VM
+ at the specified location. A XenAPIPlugin.PluginError will be raised
+ if any error is encountered in the write process.
+ """
+ return self._make_xenstore_call('write_record', vm, path,
+ {'value': json.dumps(value)})
+
+ def clear_xenstore(self, vm, path):
+ """Deletes the VM's xenstore record for the specified path.
+ If there is no such record, the request is ignored.
+ """
+ self._make_xenstore_call('delete_record', vm, path)
+
+ def _make_xenstore_call(self, method, vm, path, addl_args={}):
+ """Handles calls to the xenstore xenapi plugin."""
+ return self._make_plugin_call('xenstore.py', method=method, vm=vm,
+ path=path, addl_args=addl_args)
+
+ def _make_plugin_call(self, plugin, method, vm, path, addl_args={}):
+ """Abstracts out the process of calling a method of a xenapi plugin.
+ Any errors raised by the plugin will in turn raise a RuntimeError here.
+ """
+ vm = self._get_vm_opaque_ref(vm)
+ rec = self._session.get_xenapi().VM.get_record(vm)
+ args = {'dom_id': rec['domid'], 'path': path}
+ args.update(addl_args)
+ # If the 'testing_mode' attribute is set, add that to the args.
+ if getattr(self, 'testing_mode', False):
+ args['testing_mode'] = 'true'
+ try:
+ task = self._session.async_call_plugin(plugin, method, args)
+ ret = self._session.wait_for_task(0, task)
+ except self.XenAPI.Failure, e:
+ raise RuntimeError("%s" % e.details[-1])
+ return ret
+
+ def add_to_xenstore(self, vm, path, key, value):
+ """Adds the passed key/value pair to the xenstore record for
+ the given VM at the specified location. A XenAPIPlugin.PluginError
+ will be raised if any error is encountered in the write process.
+ """
+ current = self.read_from_xenstore(vm, path)
+ if not current:
+ # Nothing at that location
+ current = {key: value}
+ else:
+ current[key] = value
+ self.write_to_xenstore(vm, path, current)
+
+ def remove_from_xenstore(self, vm, path, key_or_keys):
+ """Takes either a single key or a list of keys and removes
+ them from the xenstoreirecord data for the given VM.
+ If the key doesn't exist, the request is ignored.
+ """
+ current = self.list_from_xenstore(vm, path)
+ if not current:
+ return
+ if isinstance(key_or_keys, basestring):
+ keys = [key_or_keys]
+ else:
+ keys = key_or_keys
+ keys.sort(lambda x, y: cmp(y.count('/'), x.count('/')))
+ for key in keys:
+ if path:
+ keypath = "%s/%s" % (path, key)
+ else:
+ keypath = key
+ self._make_xenstore_call('delete_record', vm, keypath)
+
+ ########################################################################
+ ###### The following methods interact with the xenstore parameter
+ ###### record, not the live xenstore. They were created before I
+ ###### knew the difference, and are left in here in case they prove
+ ###### to be useful. They all have '_param' added to their method
+ ###### names to distinguish them. (dabo)
+ ########################################################################
+ def read_partial_from_param_xenstore(self, instance_or_vm, key_prefix):
+ """Returns a dict of all the keys in the xenstore parameter record
+ for the given instance that begin with the key_prefix.
+ """
+ data = self.read_from_param_xenstore(instance_or_vm)
+ badkeys = [k for k in data.keys()
+ if not k.startswith(key_prefix)]
+ for badkey in badkeys:
+ del data[badkey]
+ return data
+
+ def read_from_param_xenstore(self, instance_or_vm, keys=None):
+ """Returns the xenstore parameter record data for the specified VM
+ instance as a dict. Accepts an optional key or list of keys; if a
+ value for 'keys' is passed, the returned dict is filtered to only
+ return the values for those keys.
+ """
+ vm = self._get_vm_opaque_ref(instance_or_vm)
+ data = self._session.call_xenapi_request('VM.get_xenstore_data',
+ (vm, ))
+ ret = {}
+ if keys is None:
+ keys = data.keys()
+ elif isinstance(keys, basestring):
+ keys = [keys]
+ for key in keys:
+ raw = data.get(key)
+ if raw:
+ ret[key] = json.loads(raw)
+ else:
+ ret[key] = raw
+ return ret
+
+ def add_to_param_xenstore(self, instance_or_vm, key, val):
+ """Takes a key/value pair and adds it to the xenstore parameter
+ record for the given vm instance. If the key exists in xenstore,
+ it is overwritten"""
+ vm = self._get_vm_opaque_ref(instance_or_vm)
+ self.remove_from_param_xenstore(instance_or_vm, key)
+ jsonval = json.dumps(val)
+ self._session.call_xenapi_request('VM.add_to_xenstore_data',
+ (vm, key, jsonval))
+
+ def write_to_param_xenstore(self, instance_or_vm, mapping):
+ """Takes a dict and writes each key/value pair to the xenstore
+ parameter record for the given vm instance. Any existing data for
+ those keys is overwritten.
+ """
+ for k, v in mapping.iteritems():
+ self.add_to_param_xenstore(instance_or_vm, k, v)
+
+ def remove_from_param_xenstore(self, instance_or_vm, key_or_keys):
+ """Takes either a single key or a list of keys and removes
+ them from the xenstore parameter record data for the given VM.
+ If the key doesn't exist, the request is ignored.
+ """
+ vm = self._get_vm_opaque_ref(instance_or_vm)
+ if isinstance(key_or_keys, basestring):
+ keys = [key_or_keys]
+ else:
+ keys = key_or_keys
+ for key in keys:
+ self._session.call_xenapi_request('VM.remove_from_xenstore_data',
+ (vm, key))
+
+ def clear_param_xenstore(self, instance_or_vm):
+ """Removes all data from the xenstore parameter record for this VM."""
+ self.write_to_param_xenstore(instance_or_vm, {})
+ ########################################################################
diff --git a/nova/virt/xenapi/volume_utils.py b/nova/virt/xenapi/volume_utils.py
index 1ca813bcf..4bbc41b03 100644
--- a/nova/virt/xenapi/volume_utils.py
+++ b/nova/virt/xenapi/volume_utils.py
@@ -200,15 +200,19 @@ class VolumeHelper(HelperBase):
return -1
-def _get_volume_id(path):
+def _get_volume_id(path_or_id):
"""Retrieve the volume id from device_path"""
+ # If we have the ID and not a path, just return it.
+ if isinstance(path_or_id, int):
+ return path_or_id
# n must contain at least the volume_id
# /vol- is for remote volumes
# -vol- is for local volumes
# see compute/manager->setup_compute_volume
- volume_id = path[path.find('/vol-') + 1:]
- if volume_id == path:
- volume_id = path[path.find('-vol-') + 1:].replace('--', '-')
+ volume_id = path_or_id[path_or_id.find('/vol-') + 1:]
+ if volume_id == path_or_id:
+ volume_id = path_or_id[path_or_id.find('-vol-') + 1:]
+ volume_id = volume_id.replace('--', '-')
return volume_id
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 7f03d6c2b..c48f5b7cb 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2010 Citrix Systems, Inc.
+# Copyright 2010 OpenStack LLC.
#
# 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
@@ -19,15 +20,15 @@ A connection to XenServer or Xen Cloud Platform.
The concurrency model for this class is as follows:
-All XenAPI calls are on a thread (using t.i.t.deferToThread, via the decorator
-deferredToThread). They are remote calls, and so may hang for the usual
-reasons. They should not be allowed to block the reactor thread.
+All XenAPI calls are on a green thread (using eventlet's "tpool"
+thread pool). They are remote calls, and so may hang for the usual
+reasons.
All long-running XenAPI calls (VM.start, VM.reboot, etc) are called async
-(using XenAPI.VM.async_start etc). These return a task, which can then be
-polled for completion. Polling is handled using reactor.callLater.
+(using XenAPI.VM.async_start etc). These return a task, which can then be
+polled for completion.
-This combination of techniques means that we don't block the reactor thread at
+This combination of techniques means that we don't block the main thread at
all, and at the same time we don't hold lots of threads waiting for
long-running operations.
@@ -81,8 +82,12 @@ flags.DEFINE_string('xenapi_connection_password',
flags.DEFINE_float('xenapi_task_poll_interval',
0.5,
'The interval used for polling of remote tasks '
- '(Async.VM.start, etc). Used only if '
+ '(Async.VM.start, etc). Used only if '
'connection_type=xenapi.')
+flags.DEFINE_float('xenapi_vhd_coalesce_poll_interval',
+ 5.0,
+ 'The interval used for polling of coalescing vhds.'
+ ' Used only if connection_type=xenapi.')
flags.DEFINE_string('target_host',
None,
'iSCSI Target Host')
@@ -131,6 +136,10 @@ class XenAPIConnection(object):
"""Create VM instance"""
self._vmops.spawn(instance)
+ def snapshot(self, instance, name):
+ """ Create snapshot from a running VM instance """
+ self._vmops.snapshot(instance, name)
+
def reboot(self, instance):
"""Reboot VM instance"""
self._vmops.reboot(instance)
@@ -159,9 +168,9 @@ class XenAPIConnection(object):
"""Return data about VM instance"""
return self._vmops.get_info(instance_id)
- def get_diagnostics(self, instance_id):
+ def get_diagnostics(self, instance):
"""Return data about VM diagnostics"""
- return self._vmops.get_diagnostics(instance_id)
+ return self._vmops.get_diagnostics(instance)
def get_console_output(self, instance):
"""Return snapshot of console"""
@@ -205,6 +214,14 @@ class XenAPISession(object):
f = f.__getattr__(m)
return tpool.execute(f, *args)
+ def call_xenapi_request(self, method, *args):
+ """Some interactions with dom0, such as interacting with xenstore's
+ param record, require using the xenapi_request method of the session
+ object. This wraps that call on a background thread.
+ """
+ f = self._session.xenapi_request
+ return tpool.execute(f, method, *args)
+
def async_call_plugin(self, plugin, fn, args):
"""Call Async.host.call_plugin on a background thread."""
return tpool.execute(self._unwrap_plugin_exceptions,
@@ -214,7 +231,6 @@ class XenAPISession(object):
def wait_for_task(self, id, task):
"""Return the result of the given task. The task is polled
until it completes."""
-
done = event.Event()
loop = utils.LoopingCall(self._poll_task, id, task, done)
loop.start(FLAGS.xenapi_task_poll_interval, now=True)
@@ -227,14 +243,14 @@ class XenAPISession(object):
return self.XenAPI.Session(url)
def _poll_task(self, id, task, done):
- """Poll the given XenAPI task, and fire the given Deferred if we
+ """Poll the given XenAPI task, and fire the given action if we
get a result."""
try:
name = self._session.xenapi.task.get_name_label(task)
status = self._session.xenapi.task.get_status(task)
action = dict(
- id=int(id),
- action=name,
+ instance_id=int(id),
+ action=name[0:255], # Ensure action is never > 255
error=None)
if status == "pending":
return
@@ -282,7 +298,7 @@ class XenAPISession(object):
def _parse_xmlrpc_value(val):
- """Parse the given value as if it were an XML-RPC value. This is
+ """Parse the given value as if it were an XML-RPC value. This is
sometimes used as the format for the task.result field."""
if not val:
return val
diff --git a/nova/volume/__init__.py b/nova/volume/__init__.py
index d6e944fc0..56ef9332e 100644
--- a/nova/volume/__init__.py
+++ b/nova/volume/__init__.py
@@ -16,16 +16,4 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-:mod:`nova.volume` -- Nova Block Storage
-=====================================================
-
-.. automodule:: nova.volume
- :platform: Unix
-.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com>
-.. moduleauthor:: Devin Carlen <devin.carlen@gmail.com>
-.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com>
-.. moduleauthor:: Joshua McKenty <joshua@cognition.ca>
-.. moduleauthor:: Manish Singh <yosh@gimp.org>
-.. moduleauthor:: Andy Smith <andy@anarkystic.com>
-"""
+from nova.volume.api import API
diff --git a/nova/volume/api.py b/nova/volume/api.py
new file mode 100644
index 000000000..2d7fe3762
--- /dev/null
+++ b/nova/volume/api.py
@@ -0,0 +1,101 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# 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.
+
+"""
+Handles all requests relating to volumes.
+"""
+
+import datetime
+import logging
+
+from nova import db
+from nova import exception
+from nova import flags
+from nova import quota
+from nova import rpc
+from nova.db import base
+
+FLAGS = flags.FLAGS
+flags.DECLARE('storage_availability_zone', 'nova.volume.manager')
+
+
+class API(base.Base):
+ """API for interacting with the volume manager."""
+
+ def create(self, context, size, name, description):
+ if quota.allowed_volumes(context, 1, size) < 1:
+ logging.warn("Quota exceeeded for %s, tried to create %sG volume",
+ context.project_id, size)
+ raise quota.QuotaError("Volume quota exceeded. You cannot "
+ "create a volume of size %s" % size)
+
+ options = {
+ 'size': size,
+ 'user_id': context.user.id,
+ 'project_id': context.project_id,
+ 'availability_zone': FLAGS.storage_availability_zone,
+ 'status': "creating",
+ 'attach_status': "detached",
+ 'display_name': name,
+ 'display_description': description}
+
+ volume = self.db.volume_create(context, options)
+ rpc.cast(context,
+ FLAGS.scheduler_topic,
+ {"method": "create_volume",
+ "args": {"topic": FLAGS.volume_topic,
+ "volume_id": volume['id']}})
+ return volume
+
+ def delete(self, context, volume_id):
+ volume = self.get(context, volume_id)
+ if volume['status'] != "available":
+ raise exception.ApiError(_("Volume status must be available"))
+ now = datetime.datetime.utcnow()
+ self.db.volume_update(context, volume_id, {'status': 'deleting',
+ 'terminated_at': now})
+ host = volume['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.volume_topic, host),
+ {"method": "delete_volume",
+ "args": {"volume_id": volume_id}})
+
+ def update(self, context, volume_id, fields):
+ self.db.volume_update(context, volume_id, fields)
+
+ def get(self, context, volume_id):
+ return self.db.volume_get(context, volume_id)
+
+ def get_all(self, context):
+ if context.user.is_admin():
+ return self.db.volume_get_all(context)
+ return self.db.volume_get_all_by_project(context, context.project_id)
+
+ def check_attach(self, context, volume_id):
+ volume = self.get(context, volume_id)
+ # TODO(vish): abstract status checking?
+ if volume['status'] != "available":
+ raise exception.ApiError(_("Volume status must be available"))
+ if volume['attach_status'] == "attached":
+ raise exception.ApiError(_("Volume is already attached"))
+
+ def check_detach(self, context, volume_id):
+ volume = self.get(context, volume_id)
+ # TODO(vish): abstract status checking?
+ if volume['status'] == "available":
+ raise exception.ApiError(_("Volume is already detached"))