merged trunk

This commit is contained in:
Chuck Short 2011-03-25 16:06:38 -04:00
commit 12581e79bd
30 changed files with 1787 additions and 179 deletions

View File

@ -1,4 +1,5 @@
Andy Smith <code@term.ie>
Andy Southgate <andy.southgate@citrix.com>
Anne Gentle <anne@openstack.org>
Anthony Young <sleepsonthefloor@gmail.com>
Antony Messerli <ant@openstack.org>
@ -22,6 +23,7 @@ Eldar Nugaev <enugaev@griddynamics.com>
Eric Day <eday@oddments.org>
Eric Windisch <eric@cloudscaling.com>
Ewan Mellor <ewan.mellor@citrix.com>
Gabe Westmaas <gabe.westmaas@rackspace.com>
Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>
Hisaki Ohara <hisaki.ohara@intel.com>
Ilya Alekseyev <ialekseev@griddynamics.com>

View File

@ -98,4 +98,4 @@ paste.app_factory = nova.api.openstack:APIRouterV11.factory
pipeline = faultwrap osversionapp
[app:osversionapp]
paste.app_factory = nova.api.openstack:Versions.factory
paste.app_factory = nova.api.openstack.versions:Versions.factory

View File

@ -33,8 +33,10 @@ from nova.api.openstack import backup_schedules
from nova.api.openstack import consoles
from nova.api.openstack import flavors
from nova.api.openstack import images
from nova.api.openstack import image_metadata
from nova.api.openstack import limits
from nova.api.openstack import servers
from nova.api.openstack import server_metadata
from nova.api.openstack import shared_ip_groups
from nova.api.openstack import users
from nova.api.openstack import zones
@ -117,9 +119,6 @@ class APIRouter(wsgi.Router):
mapper.resource("image", "images", controller=images.Controller(),
collection={'detail': 'GET'})
mapper.resource("flavor", "flavors", controller=flavors.Controller(),
collection={'detail': 'GET'})
mapper.resource("shared_ip_group", "shared_ip_groups",
collection={'detail': 'GET'},
controller=shared_ip_groups.Controller())
@ -138,6 +137,10 @@ class APIRouterV10(APIRouter):
collection={'detail': 'GET'},
member=self.server_members)
mapper.resource("flavor", "flavors",
controller=flavors.ControllerV10(),
collection={'detail': 'GET'})
class APIRouterV11(APIRouter):
"""Define routes specific to OpenStack API V1.1."""
@ -149,20 +152,16 @@ class APIRouterV11(APIRouter):
collection={'detail': 'GET'},
member=self.server_members)
mapper.resource("image_meta", "meta",
controller=image_metadata.Controller(),
parent_resource=dict(member_name='image',
collection_name='images'))
class Versions(wsgi.Application):
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
"""Respond to a request for all OpenStack API versions."""
response = {
"versions": [
dict(status="DEPRECATED", id="v1.0"),
dict(status="CURRENT", id="v1.1"),
],
}
metadata = {
"application/xml": {
"attributes": dict(version=["status", "id"])}}
mapper.resource("server_meta", "meta",
controller=server_metadata.Controller(),
parent_resource=dict(member_name='server',
collection_name='servers'))
content_type = req.best_match_content_type()
return wsgi.Serializer(metadata).serialize(response, content_type)
mapper.resource("flavor", "flavors",
controller=flavors.ControllerV11(),
collection={'detail': 'GET'})

View File

@ -15,16 +15,12 @@
# License for the specific language governing permissions and limitations
# under the License.
from webob import exc
import webob
from nova import db
from nova import context
from nova.api.openstack import faults
from nova.api.openstack import common
from nova.compute import instance_types
from nova.api.openstack.views import flavors as flavors_views
from nova import exception
from nova import wsgi
import nova.api.openstack
from nova.api.openstack import views
class Controller(wsgi.Controller):
@ -33,33 +29,50 @@ class Controller(wsgi.Controller):
_serialization_metadata = {
'application/xml': {
"attributes": {
"flavor": ["id", "name", "ram", "disk"]}}}
"flavor": ["id", "name", "ram", "disk"],
"link": ["rel", "type", "href"],
}
}
}
def index(self, req):
"""Return all flavors in brief."""
return dict(flavors=[dict(id=flavor['id'], name=flavor['name'])
for flavor in self.detail(req)['flavors']])
items = self._get_flavors(req, is_detail=False)
return dict(flavors=items)
def detail(self, req):
"""Return all flavors in detail."""
items = [self.show(req, id)['flavor'] for id in self._all_ids(req)]
items = self._get_flavors(req, is_detail=True)
return dict(flavors=items)
def _get_flavors(self, req, is_detail=True):
"""Helper function that returns a list of flavor dicts."""
ctxt = req.environ['nova.context']
flavors = db.api.instance_type_get_all(ctxt)
builder = self._get_view_builder(req)
items = [builder.build(flavor, is_detail=is_detail)
for flavor in flavors.values()]
return items
def show(self, req, id):
"""Return data about the given flavor id."""
ctxt = req.environ['nova.context']
flavor = db.api.instance_type_get_by_flavor_id(ctxt, id)
values = {
"id": flavor["flavorid"],
"name": flavor["name"],
"ram": flavor["memory_mb"],
"disk": flavor["local_gb"],
}
try:
ctxt = req.environ['nova.context']
flavor = db.api.instance_type_get_by_flavor_id(ctxt, id)
except exception.NotFound:
return webob.exc.HTTPNotFound()
builder = self._get_view_builder(req)
values = builder.build(flavor, is_detail=True)
return dict(flavor=values)
def _all_ids(self, req):
"""Return the list of all flavorids."""
ctxt = req.environ['nova.context']
inst_types = db.api.instance_type_get_all(ctxt)
flavor_ids = [inst_types[i]['flavorid'] for i in inst_types.keys()]
return sorted(flavor_ids)
class ControllerV10(Controller):
def _get_view_builder(self, req):
return views.flavors.ViewBuilder()
class ControllerV11(Controller):
def _get_view_builder(self, req):
base_url = req.application_url
return views.flavors.ViewBuilderV11(base_url)

View File

@ -0,0 +1,93 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# 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.
from webob import exc
from nova import flags
from nova import utils
from nova import wsgi
from nova.api.openstack import faults
FLAGS = flags.FLAGS
class Controller(wsgi.Controller):
"""The image metadata API controller for the Openstack API"""
def __init__(self):
self.image_service = utils.import_object(FLAGS.image_service)
super(Controller, self).__init__()
def _get_metadata(self, context, image_id, image=None):
if not image:
image = self.image_service.show(context, image_id)
metadata = image.get('properties', {})
return metadata
def index(self, req, image_id):
"""Returns the list of metadata for a given instance"""
context = req.environ['nova.context']
metadata = self._get_metadata(context, image_id)
return dict(metadata=metadata)
def show(self, req, image_id, id):
context = req.environ['nova.context']
metadata = self._get_metadata(context, image_id)
if id in metadata:
return {id: metadata[id]}
else:
return faults.Fault(exc.HTTPNotFound())
def create(self, req, image_id):
context = req.environ['nova.context']
body = self._deserialize(req.body, req.get_content_type())
img = self.image_service.show(context, image_id)
metadata = self._get_metadata(context, image_id, img)
if 'metadata' in body:
for key, value in body['metadata'].iteritems():
metadata[key] = value
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
return dict(metadata=metadata)
def update(self, req, image_id, id):
context = req.environ['nova.context']
body = self._deserialize(req.body, req.get_content_type())
if not id in body:
expl = _('Request body and URI mismatch')
raise exc.HTTPBadRequest(explanation=expl)
if len(body) > 1:
expl = _('Request body contains too many items')
raise exc.HTTPBadRequest(explanation=expl)
img = self.image_service.show(context, image_id)
metadata = self._get_metadata(context, image_id, img)
metadata[id] = body[id]
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
return req.body
def delete(self, req, image_id, id):
context = req.environ['nova.context']
img = self.image_service.show(context, image_id)
metadata = self._get_metadata(context, image_id)
if not id in metadata:
return faults.Fault(exc.HTTPNotFound())
metadata.pop(id)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)

View File

@ -0,0 +1,78 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# 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.
from webob import exc
from nova import compute
from nova import wsgi
from nova.api.openstack import faults
class Controller(wsgi.Controller):
""" The server metadata API controller for the Openstack API """
def __init__(self):
self.compute_api = compute.API()
super(Controller, self).__init__()
def _get_metadata(self, context, server_id):
metadata = self.compute_api.get_instance_metadata(context, server_id)
meta_dict = {}
for key, value in metadata.iteritems():
meta_dict[key] = value
return dict(metadata=meta_dict)
def index(self, req, server_id):
""" Returns the list of metadata for a given instance """
context = req.environ['nova.context']
return self._get_metadata(context, server_id)
def create(self, req, server_id):
context = req.environ['nova.context']
body = self._deserialize(req.body, req.get_content_type())
self.compute_api.update_or_create_instance_metadata(context,
server_id,
body['metadata'])
return req.body
def update(self, req, server_id, id):
context = req.environ['nova.context']
body = self._deserialize(req.body, req.get_content_type())
if not id in body:
expl = _('Request body and URI mismatch')
raise exc.HTTPBadRequest(explanation=expl)
if len(body) > 1:
expl = _('Request body contains too many items')
raise exc.HTTPBadRequest(explanation=expl)
self.compute_api.update_or_create_instance_metadata(context,
server_id,
body)
return req.body
def show(self, req, server_id, id):
""" Return a single metadata item """
context = req.environ['nova.context']
data = self._get_metadata(context, server_id)
if id in data['metadata']:
return {id: data['metadata'][id]}
else:
return faults.Fault(exc.HTTPNotFound())
def delete(self, req, server_id, id):
""" Deletes an existing metadata """
context = req.environ['nova.context']
self.compute_api.delete_instance_metadata(context, server_id, id)

View File

@ -48,11 +48,15 @@ class Controller(wsgi.Controller):
""" The Server API controller for the OpenStack API """
_serialization_metadata = {
'application/xml': {
"application/xml": {
"attributes": {
"server": ["id", "imageId", "name", "flavorId", "hostId",
"status", "progress", "adminPass", "flavorRef",
"imageRef"]}}}
"imageRef"],
"link": ["rel", "type", "href"],
},
},
}
def __init__(self):
self.compute_api = compute.API()
@ -502,33 +506,41 @@ class Controller(wsgi.Controller):
return dict(actions=actions)
def _get_kernel_ramdisk_from_image(self, req, image_id):
"""Retrevies kernel and ramdisk IDs from Glance
Only 'machine' (ami) type use kernel and ramdisk outside of the
image.
"""Fetch an image from the ImageService, then if present, return the
associated kernel and ramdisk image IDs.
"""
# FIXME(sirp): Since we're retrieving the kernel_id from an
# image_property, this means only Glance is supported.
# The BaseImageService needs to expose a consistent way of accessing
# kernel_id and ramdisk_id
image = self._image_service.show(req.environ['nova.context'], image_id)
context = req.environ['nova.context']
image_meta = self._image_service.show(context, image_id)
# NOTE(sirp): extracted to a separate method to aid unit-testing, the
# new method doesn't need a request obj or an ImageService stub
kernel_id, ramdisk_id = self._do_get_kernel_ramdisk_from_image(
image_meta)
return kernel_id, ramdisk_id
if image['status'] != 'active':
@staticmethod
def _do_get_kernel_ramdisk_from_image(image_meta):
"""Given an ImageService image_meta, return kernel and ramdisk image
ids if present.
This is only valid for `ami` style images.
"""
image_id = image_meta['id']
if image_meta['status'] != 'active':
raise exception.Invalid(
_("Cannot build from image %(image_id)s, status not active") %
locals())
if image['disk_format'] != 'ami':
if image_meta['properties']['disk_format'] != 'ami':
return None, None
try:
kernel_id = image['properties']['kernel_id']
kernel_id = image_meta['properties']['kernel_id']
except KeyError:
raise exception.NotFound(
_("Kernel not found for image %(image_id)s") % locals())
try:
ramdisk_id = image['properties']['ramdisk_id']
ramdisk_id = image_meta['properties']['ramdisk_id']
except KeyError:
raise exception.NotFound(
_("Ramdisk not found for image %(image_id)s") % locals())
@ -572,7 +584,7 @@ class ControllerV11(Controller):
base_url)
addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11()
return nova.api.openstack.views.servers.ViewBuilderV11(
addresses_builder, flavor_builder, image_builder)
addresses_builder, flavor_builder, image_builder, base_url)
def _get_addresses_view_builder(self, req):
return nova.api.openstack.views.addresses.ViewBuilderV11(req)

View File

@ -0,0 +1,54 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# 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.
import webob.dec
import webob.exc
from nova import wsgi
import nova.api.openstack.views.versions
class Versions(wsgi.Application):
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
"""Respond to a request for all OpenStack API versions."""
version_objs = [
{
"id": "v1.1",
"status": "CURRENT",
},
{
"id": "v1.0",
"status": "DEPRECATED",
},
]
builder = nova.api.openstack.views.versions.get_view_builder(req)
versions = [builder.build(version) for version in version_objs]
response = dict(versions=versions)
metadata = {
"application/xml": {
"attributes": {
"version": ["status", "id"],
"link": ["rel", "href"],
}
}
}
content_type = req.best_match_content_type()
return wsgi.Serializer(metadata).serialize(response, content_type)

View File

@ -19,16 +19,78 @@ from nova.api.openstack import common
class ViewBuilder(object):
def __init__(self):
pass
def build(self, flavor_obj):
raise NotImplementedError()
def build(self, flavor_obj, is_detail=False):
"""Generic method used to generate a flavor entity."""
if is_detail:
flavor = self._build_detail(flavor_obj)
else:
flavor = self._build_simple(flavor_obj)
self._build_extra(flavor)
return flavor
def _build_simple(self, flavor_obj):
"""Build a minimal representation of a flavor."""
return {
"id": flavor_obj["flavorid"],
"name": flavor_obj["name"],
}
def _build_detail(self, flavor_obj):
"""Build a more complete representation of a flavor."""
simple = self._build_simple(flavor_obj)
detail = {
"ram": flavor_obj["memory_mb"],
"disk": flavor_obj["local_gb"],
}
detail.update(simple)
return detail
def _build_extra(self, flavor_obj):
"""Hook for version-specific changes to newly created flavor object."""
pass
class ViewBuilderV11(ViewBuilder):
"""Openstack API v1.1 flavors view builder."""
def __init__(self, base_url):
"""
:param base_url: url of the root wsgi application
"""
self.base_url = base_url
def _build_extra(self, flavor_obj):
flavor_obj["links"] = self._build_links(flavor_obj)
def _build_links(self, flavor_obj):
"""Generate a container of links that refer to the provided flavor."""
href = self.generate_href(flavor_obj["id"])
links = [
{
"rel": "self",
"href": href,
},
{
"rel": "bookmark",
"type": "application/json",
"href": href,
},
{
"rel": "bookmark",
"type": "application/xml",
"href": href,
},
]
return links
def generate_href(self, flavor_id):
"""Create an url that refers to a specific flavor id."""
return "%s/flavors/%s" % (self.base_url, flavor_id)

View File

@ -16,6 +16,7 @@
# under the License.
import hashlib
import os
from nova.compute import power_state
import nova.compute
@ -41,9 +42,13 @@ class ViewBuilder(object):
def build(self, inst, is_detail):
"""Return a dict that represenst a server."""
if is_detail:
return self._build_detail(inst)
server = self._build_detail(inst)
else:
return self._build_simple(inst)
server = self._build_simple(inst)
self._build_extra(server, inst)
return server
def _build_simple(self, inst):
"""Return a simple model of a server."""
@ -97,29 +102,67 @@ class ViewBuilder(object):
"""Return the flavor sub-resource of a server."""
raise NotImplementedError()
def _build_extra(self, response, inst):
pass
class ViewBuilderV10(ViewBuilder):
"""Model an Openstack API V1.0 server response."""
def _build_image(self, response, inst):
response['imageId'] = inst['image_id']
if 'image_id' in dict(inst):
response['imageId'] = inst['image_id']
def _build_flavor(self, response, inst):
response['flavorId'] = inst['instance_type']
if 'instance_type' in dict(inst):
response['flavorId'] = inst['instance_type']
class ViewBuilderV11(ViewBuilder):
"""Model an Openstack API V1.0 server response."""
def __init__(self, addresses_builder, flavor_builder, image_builder):
def __init__(self, addresses_builder, flavor_builder, image_builder,
base_url):
ViewBuilder.__init__(self, addresses_builder)
self.flavor_builder = flavor_builder
self.image_builder = image_builder
self.base_url = base_url
def _build_image(self, response, inst):
image_id = inst["image_id"]
response["imageRef"] = self.image_builder.generate_href(image_id)
if "image_id" in dict(inst):
image_id = inst.get("image_id")
response["imageRef"] = self.image_builder.generate_href(image_id)
def _build_flavor(self, response, inst):
flavor_id = inst["instance_type"]
response["flavorRef"] = self.flavor_builder.generate_href(flavor_id)
if "instance_type" in dict(inst):
flavor_id = inst["instance_type"]
flavor_ref = self.flavor_builder.generate_href(flavor_id)
response["flavorRef"] = flavor_ref
def _build_extra(self, response, inst):
self._build_links(response, inst)
def _build_links(self, response, inst):
href = self.generate_href(inst["id"])
links = [
{
"rel": "self",
"href": href,
},
{
"rel": "bookmark",
"type": "application/json",
"href": href,
},
{
"rel": "bookmark",
"type": "application/xml",
"href": href,
},
]
response["server"]["links"] = links
def generate_href(self, server_id):
"""Create an url that refers to a specific server id."""
return os.path.join(self.base_url, "servers", str(server_id))

View File

@ -0,0 +1,59 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack LLC.
# 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.
import os
def get_view_builder(req):
base_url = req.application_url
return ViewBuilder(base_url)
class ViewBuilder(object):
def __init__(self, base_url):
"""
:param base_url: url of the root wsgi application
"""
self.base_url = base_url
def build(self, version_data):
"""Generic method used to generate a version entity."""
version = {
"id": version_data["id"],
"status": version_data["status"],
"links": self._build_links(version_data),
}
return version
def _build_links(self, version_data):
"""Generate a container of links that refer to the provided version."""
href = self.generate_href(version_data["id"])
links = [
{
"rel": "self",
"href": href,
},
]
return links
def generate_href(self, version_number):
"""Create an url that refers to a specific version_number."""
return os.path.join(self.base_url, version_number)

View File

@ -673,3 +673,18 @@ class API(base.Base):
self.network_api.associate_floating_ip(context,
floating_ip=address,
fixed_ip=instance['fixed_ip'])
def get_instance_metadata(self, context, instance_id):
"""Get all metadata associated with an instance."""
rv = self.db.instance_metadata_get(context, instance_id)
return dict(rv.iteritems())
def delete_instance_metadata(self, context, instance_id, key):
"""Delete the given metadata item"""
self.db.instance_metadata_delete(context, instance_id, key)
def update_or_create_instance_metadata(self, context, instance_id,
metadata):
"""Updates or creates instance metadata"""
self.db.instance_metadata_update_or_create(context, instance_id,
metadata)

View File

@ -1172,3 +1172,21 @@ def zone_get(context, zone_id):
def zone_get_all(context):
"""Get all child Zones."""
return IMPL.zone_get_all(context)
####################
def instance_metadata_get(context, instance_id):
"""Get all metadata for an instance"""
return IMPL.instance_metadata_get(context, instance_id)
def instance_metadata_delete(context, instance_id, key):
"""Delete the given metadata item"""
IMPL.instance_metadata_delete(context, instance_id, key)
def instance_metadata_update_or_create(context, instance_id, metadata):
"""Create or update instance metadata"""
IMPL.instance_metadata_update_or_create(context, instance_id, metadata)

View File

@ -2389,7 +2389,7 @@ def instance_type_get_by_flavor_id(context, id):
filter_by(flavorid=int(id)).\
first()
if not inst_type:
raise exception.NotFound(_("No flavor with name %s") % id)
raise exception.NotFound(_("No flavor with flavorid %s") % id)
else:
return dict(inst_type)
@ -2466,3 +2466,65 @@ def zone_get(context, zone_id):
def zone_get_all(context):
session = get_session()
return session.query(models.Zone).all()
####################
@require_context
def instance_metadata_get(context, instance_id):
session = get_session()
meta_results = session.query(models.InstanceMetadata).\
filter_by(instance_id=instance_id).\
filter_by(deleted=False).\
all()
meta_dict = {}
for i in meta_results:
meta_dict[i['key']] = i['value']
return meta_dict
@require_context
def instance_metadata_delete(context, instance_id, key):
session = get_session()
session.query(models.InstanceMetadata).\
filter_by(instance_id=instance_id).\
filter_by(key=key).\
filter_by(deleted=False).\
update({'deleted': 1,
'deleted_at': datetime.datetime.utcnow(),
'updated_at': literal_column('updated_at')})
@require_context
def instance_metadata_get_item(context, instance_id, key):
session = get_session()
meta_result = session.query(models.InstanceMetadata).\
filter_by(instance_id=instance_id).\
filter_by(key=key).\
filter_by(deleted=False).\
first()
if not meta_result:
raise exception.NotFound(_('Invalid metadata key for instance %s') %
instance_id)
return meta_result
@require_context
def instance_metadata_update_or_create(context, instance_id, metadata):
session = get_session()
meta_ref = None
for key, value in metadata.iteritems():
try:
meta_ref = instance_metadata_get_item(context, instance_id, key,
session)
except:
meta_ref = models.InstanceMetadata()
meta_ref.update({"key": key, "value": value,
"instance_id": instance_id,
"deleted": 0})
meta_ref.save(session=session)
return metadata

View File

@ -35,6 +35,7 @@ from nova import utils
import nova.api.openstack.auth
from nova.api import openstack
from nova.api.openstack import auth
from nova.api.openstack import versions
from nova.api.openstack import limits
from nova.auth.manager import User, Project
from nova.image import glance
@ -85,7 +86,7 @@ def wsgi_app(inner_app10=None, inner_app11=None):
limits.RateLimitingMiddleware(inner_app11)))
mapper['/v1.0'] = api10
mapper['/v1.1'] = api11
mapper['/'] = openstack.FaultWrapper(openstack.Versions())
mapper['/'] = openstack.FaultWrapper(versions.Versions())
return mapper
@ -184,15 +185,19 @@ def stub_out_glance(stubs, initial_fixtures=None):
for _ in range(20))
image_meta['id'] = image_id
self.fixtures.append(image_meta)
return image_meta
return copy.deepcopy(image_meta)
def fake_update_image(self, image_id, image_meta, data=None):
for attr in ('created_at', 'updated_at', 'deleted_at', 'deleted'):
if attr in image_meta:
del image_meta[attr]
f = self._find_image(image_id)
if not f:
raise glance_exc.NotFound
f.update(image_meta)
return f
return copy.deepcopy(f)
def fake_delete_image(self, image_id):
f = self._find_image(image_id)

View File

@ -19,11 +19,10 @@ import json
import stubout
import webob
from nova import test
import nova.api
import nova.db.api
from nova import context
from nova.api.openstack import flavors
from nova import db
from nova import exception
from nova import test
from nova.tests.api.openstack import fakes
@ -48,6 +47,10 @@ def return_instance_types(context, num=2):
return instance_types
def return_instance_type_not_found(context, flavorid):
raise exception.NotFound()
class FlavorsTest(test.TestCase):
def setUp(self):
super(FlavorsTest, self).setUp()
@ -67,7 +70,7 @@ class FlavorsTest(test.TestCase):
self.stubs.UnsetAll()
super(FlavorsTest, self).tearDown()
def test_get_flavor_list(self):
def test_get_flavor_list_v1_0(self):
req = webob.Request.blank('/v1.0/flavors')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
@ -84,7 +87,7 @@ class FlavorsTest(test.TestCase):
]
self.assertEqual(flavors, expected)
def test_get_flavor_list_detail(self):
def test_get_flavor_list_detail_v1_0(self):
req = webob.Request.blank('/v1.0/flavors/detail')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
@ -105,7 +108,7 @@ class FlavorsTest(test.TestCase):
]
self.assertEqual(flavors, expected)
def test_get_flavor_by_id(self):
def test_get_flavor_by_id_v1_0(self):
req = webob.Request.blank('/v1.0/flavors/12')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
@ -117,3 +120,144 @@ class FlavorsTest(test.TestCase):
"disk": "10",
}
self.assertEqual(flavor, expected)
def test_get_flavor_by_invalid_id(self):
self.stubs.Set(nova.db.api, "instance_type_get_by_flavor_id",
return_instance_type_not_found)
req = webob.Request.blank('/v1.0/flavors/asdf')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 404)
def test_get_flavor_by_id_v1_1(self):
req = webob.Request.blank('/v1.1/flavors/12')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
flavor = json.loads(res.body)["flavor"]
expected = {
"id": "12",
"name": "flavor 12",
"ram": "256",
"disk": "10",
"links": [
{
"rel": "self",
"href": "http://localhost/v1.1/flavors/12",
},
{
"rel": "bookmark",
"type": "application/json",
"href": "http://localhost/v1.1/flavors/12",
},
{
"rel": "bookmark",
"type": "application/xml",
"href": "http://localhost/v1.1/flavors/12",
},
],
}
self.assertEqual(flavor, expected)
def test_get_flavor_list_v1_1(self):
req = webob.Request.blank('/v1.1/flavors')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
flavor = json.loads(res.body)["flavors"]
expected = [
{
"id": "1",
"name": "flavor 1",
"links": [
{
"rel": "self",
"href": "http://localhost/v1.1/flavors/1",
},
{
"rel": "bookmark",
"type": "application/json",
"href": "http://localhost/v1.1/flavors/1",
},
{
"rel": "bookmark",
"type": "application/xml",
"href": "http://localhost/v1.1/flavors/1",
},
],
},
{
"id": "2",
"name": "flavor 2",
"links": [
{
"rel": "self",
"href": "http://localhost/v1.1/flavors/2",
},
{
"rel": "bookmark",
"type": "application/json",
"href": "http://localhost/v1.1/flavors/2",
},
{
"rel": "bookmark",
"type": "application/xml",
"href": "http://localhost/v1.1/flavors/2",
},
],
},
]
self.assertEqual(flavor, expected)
def test_get_flavor_list_detail_v1_1(self):
req = webob.Request.blank('/v1.1/flavors/detail')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
flavor = json.loads(res.body)["flavors"]
expected = [
{
"id": "1",
"name": "flavor 1",
"ram": "256",
"disk": "10",
"links": [
{
"rel": "self",
"href": "http://localhost/v1.1/flavors/1",
},
{
"rel": "bookmark",
"type": "application/json",
"href": "http://localhost/v1.1/flavors/1",
},
{
"rel": "bookmark",
"type": "application/xml",
"href": "http://localhost/v1.1/flavors/1",
},
],
},
{
"id": "2",
"name": "flavor 2",
"ram": "256",
"disk": "10",
"links": [
{
"rel": "self",
"href": "http://localhost/v1.1/flavors/2",
},
{
"rel": "bookmark",
"type": "application/json",
"href": "http://localhost/v1.1/flavors/2",
},
{
"rel": "bookmark",
"type": "application/xml",
"href": "http://localhost/v1.1/flavors/2",
},
],
},
]
self.assertEqual(flavor, expected)

View File

@ -0,0 +1,166 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# 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.
import json
import stubout
import unittest
import webob
from nova import flags
from nova.api import openstack
from nova.tests.api.openstack import fakes
import nova.wsgi
FLAGS = flags.FLAGS
class ImageMetaDataTest(unittest.TestCase):
IMAGE_FIXTURES = [
{'status': 'active',
'name': 'image1',
'deleted': False,
'container_format': None,
'created_at': '2011-03-22T17:40:15',
'disk_format': None,
'updated_at': '2011-03-22T17:40:15',
'id': '1',
'location': 'file:///var/lib/glance/images/1',
'is_public': True,
'deleted_at': None,
'properties': {
'type': 'ramdisk',
'key1': 'value1',
'key2': 'value2'
},
'size': 5882349},
{'status': 'active',
'name': 'image2',
'deleted': False,
'container_format': None,
'created_at': '2011-03-22T17:40:15',
'disk_format': None,
'updated_at': '2011-03-22T17:40:15',
'id': '2',
'location': 'file:///var/lib/glance/images/2',
'is_public': True,
'deleted_at': None,
'properties': {
'type': 'ramdisk',
'key1': 'value1',
'key2': 'value2'
},
'size': 5882349},
]
def setUp(self):
super(ImageMetaDataTest, self).setUp()
self.stubs = stubout.StubOutForTesting()
self.orig_image_service = FLAGS.image_service
FLAGS.image_service = 'nova.image.glance.GlanceImageService'
fakes.FakeAuthManager.auth_data = {}
fakes.FakeAuthDatabase.data = {}
fakes.stub_out_auth(self.stubs)
fakes.stub_out_glance(self.stubs, self.IMAGE_FIXTURES)
def tearDown(self):
self.stubs.UnsetAll()
FLAGS.image_service = self.orig_image_service
super(ImageMetaDataTest, self).tearDown()
def test_index(self):
req = webob.Request.blank('/v1.1/images/1/meta')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
self.assertEqual('value1', res_dict['metadata']['key1'])
def test_show(self):
req = webob.Request.blank('/v1.1/images/1/meta/key1')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
self.assertEqual('value1', res_dict['key1'])
def test_show_not_found(self):
req = webob.Request.blank('/v1.1/images/1/meta/key9')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(404, res.status_int)
def test_create(self):
req = webob.Request.blank('/v1.1/images/2/meta')
req.environ['api.version'] = '1.1'
req.method = 'POST'
req.body = '{"metadata": {"key9": "value9"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
self.assertEqual('value9', res_dict['metadata']['key9'])
# other items should not be modified
self.assertEqual('value1', res_dict['metadata']['key1'])
self.assertEqual('value2', res_dict['metadata']['key2'])
self.assertEqual(1, len(res_dict))
def test_update_item(self):
req = webob.Request.blank('/v1.1/images/1/meta/key1')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
req.body = '{"key1": "zz"}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(200, res.status_int)
res_dict = json.loads(res.body)
self.assertEqual('zz', res_dict['key1'])
def test_update_item_too_many_keys(self):
req = webob.Request.blank('/v1.1/images/1/meta/key1')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
req.body = '{"key1": "value1", "key2": "value2"}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
def test_update_item_body_uri_mismatch(self):
req = webob.Request.blank('/v1.1/images/1/meta/bad')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
req.body = '{"key1": "value1"}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
def test_delete(self):
req = webob.Request.blank('/v1.1/images/2/meta/key1')
req.environ['api.version'] = '1.1'
req.method = 'DELETE'
res = req.get_response(fakes.wsgi_app())
self.assertEqual(200, res.status_int)
def test_delete_not_found(self):
req = webob.Request.blank('/v1.1/images/2/meta/blah')
req.environ['api.version'] = '1.1'
req.method = 'DELETE'
res = req.get_response(fakes.wsgi_app())
self.assertEqual(404, res.status_int)

View File

@ -0,0 +1,164 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# 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.
import json
import stubout
import unittest
import webob
from nova.api import openstack
from nova.tests.api.openstack import fakes
import nova.wsgi
def return_create_instance_metadata(context, server_id, metadata):
return stub_server_metadata()
def return_server_metadata(context, server_id):
return stub_server_metadata()
def return_empty_server_metadata(context, server_id):
return {}
def delete_server_metadata(context, server_id, key):
pass
def stub_server_metadata():
metadata = {
"key1": "value1",
"key2": "value2",
"key3": "value3",
"key4": "value4",
"key5": "value5"
}
return metadata
class ServerMetaDataTest(unittest.TestCase):
def setUp(self):
super(ServerMetaDataTest, self).setUp()
self.stubs = stubout.StubOutForTesting()
fakes.FakeAuthManager.auth_data = {}
fakes.FakeAuthDatabase.data = {}
fakes.stub_out_auth(self.stubs)
fakes.stub_out_key_pair_funcs(self.stubs)
def tearDown(self):
self.stubs.UnsetAll()
super(ServerMetaDataTest, self).tearDown()
def test_index(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_server_metadata)
req = webob.Request.blank('/v1.1/servers/1/meta')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
self.assertEqual('value1', res_dict['metadata']['key1'])
def test_index_no_data(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_empty_server_metadata)
req = webob.Request.blank('/v1.1/servers/1/meta')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
self.assertEqual(0, len(res_dict['metadata']))
def test_show(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_server_metadata)
req = webob.Request.blank('/v1.1/servers/1/meta/key5')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
self.assertEqual('value5', res_dict['key5'])
def test_show_meta_not_found(self):
self.stubs.Set(nova.db.api, 'instance_metadata_get',
return_empty_server_metadata)
req = webob.Request.blank('/v1.1/servers/1/meta/key6')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(404, res.status_int)
def test_delete(self):
self.stubs.Set(nova.db.api, 'instance_metadata_delete',
delete_server_metadata)
req = webob.Request.blank('/v1.1/servers/1/meta/key5')
req.environ['api.version'] = '1.1'
req.method = 'DELETE'
res = req.get_response(fakes.wsgi_app())
self.assertEqual(200, res.status_int)
def test_create(self):
self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
return_create_instance_metadata)
req = webob.Request.blank('/v1.1/servers/1/meta')
req.environ['api.version'] = '1.1'
req.method = 'POST'
req.body = '{"metadata": {"key1": "value1"}}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(200, res.status_int)
self.assertEqual('value1', res_dict['metadata']['key1'])
def test_update_item(self):
self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
return_create_instance_metadata)
req = webob.Request.blank('/v1.1/servers/1/meta/key1')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
req.body = '{"key1": "value1"}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(200, res.status_int)
res_dict = json.loads(res.body)
self.assertEqual('value1', res_dict['key1'])
def test_update_item_too_many_keys(self):
self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
return_create_instance_metadata)
req = webob.Request.blank('/v1.1/servers/1/meta/key1')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
req.body = '{"key1": "value1", "key2": "value2"}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)
def test_update_item_body_uri_mismatch(self):
self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create',
return_create_instance_metadata)
req = webob.Request.blank('/v1.1/servers/1/meta/bad')
req.environ['api.version'] = '1.1'
req.method = 'PUT'
req.body = '{"key1": "value1"}'
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(400, res.status_int)

View File

@ -26,6 +26,7 @@ import webob
from nova import context
from nova import db
from nova import exception
from nova import flags
from nova import test
import nova.api.openstack
@ -164,6 +165,33 @@ class ServersTest(test.TestCase):
self.assertEqual(res_dict['server']['id'], 1)
self.assertEqual(res_dict['server']['name'], 'server1')
def test_get_server_by_id_v11(self):
req = webob.Request.blank('/v1.1/servers/1')
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(res_dict['server']['id'], 1)
self.assertEqual(res_dict['server']['name'], 'server1')
expected_links = [
{
"rel": "self",
"href": "http://localhost/v1.1/servers/1",
},
{
"rel": "bookmark",
"type": "application/json",
"href": "http://localhost/v1.1/servers/1",
},
{
"rel": "bookmark",
"type": "application/xml",
"href": "http://localhost/v1.1/servers/1",
},
]
print res_dict['server']
self.assertEqual(res_dict['server']['links'], expected_links)
def test_get_server_by_id_with_addresses(self):
private = "192.168.0.3"
public = ["1.2.3.4"]
@ -186,7 +214,6 @@ class ServersTest(test.TestCase):
new_return_server = return_server_with_addresses(private, public)
self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
req = webob.Request.blank('/v1.1/servers/1')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
self.assertEqual(res_dict['server']['id'], 1)
@ -211,6 +238,35 @@ class ServersTest(test.TestCase):
self.assertEqual(s.get('imageId', None), None)
i += 1
def test_get_server_list_v11(self):
req = webob.Request.blank('/v1.1/servers')
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
for i, s in enumerate(res_dict['servers']):
self.assertEqual(s['id'], i)
self.assertEqual(s['name'], 'server%d' % i)
self.assertEqual(s.get('imageId', None), None)
expected_links = [
{
"rel": "self",
"href": "http://localhost/v1.1/servers/%d" % (i,),
},
{
"rel": "bookmark",
"type": "application/json",
"href": "http://localhost/v1.1/servers/%d" % (i,),
},
{
"rel": "bookmark",
"type": "application/xml",
"href": "http://localhost/v1.1/servers/%d" % (i,),
},
]
self.assertEqual(s['links'], expected_links)
def test_get_servers_with_limit(self):
req = webob.Request.blank('/v1.0/servers?limit=3')
res = req.get_response(fakes.wsgi_app())
@ -458,7 +514,6 @@ class ServersTest(test.TestCase):
def test_get_all_server_details_v1_1(self):
req = webob.Request.blank('/v1.1/servers/detail')
req.environ['api.version'] = '1.1'
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
@ -1260,3 +1315,57 @@ class TestServerInstanceCreation(test.TestCase):
server = dom.childNodes[0]
self.assertEquals(server.nodeName, 'server')
self.assertTrue(server.getAttribute('adminPass').startswith('fake'))
class TestGetKernelRamdiskFromImage(test.TestCase):
"""
If we're building from an AMI-style image, we need to be able to fetch the
kernel and ramdisk associated with the machine image. This information is
stored with the image metadata and return via the ImageService.
These tests ensure that we parse the metadata return the ImageService
correctly and that we handle failure modes appropriately.
"""
def test_status_not_active(self):
"""We should only allow fetching of kernel and ramdisk information if
we have a 'fully-formed' image, aka 'active'
"""
image_meta = {'id': 1, 'status': 'queued'}
self.assertRaises(exception.Invalid, self._get_k_r, image_meta)
def test_not_ami(self):
"""Anything other than ami should return no kernel and no ramdisk"""
image_meta = {'id': 1, 'status': 'active',
'properties': {'disk_format': 'vhd'}}
kernel_id, ramdisk_id = self._get_k_r(image_meta)
self.assertEqual(kernel_id, None)
self.assertEqual(ramdisk_id, None)
def test_ami_no_kernel(self):
"""If an ami is missing a kernel it should raise NotFound"""
image_meta = {'id': 1, 'status': 'active',
'properties': {'disk_format': 'ami', 'ramdisk_id': 1}}
self.assertRaises(exception.NotFound, self._get_k_r, image_meta)
def test_ami_no_ramdisk(self):
"""If an ami is missing a ramdisk it should raise NotFound"""
image_meta = {'id': 1, 'status': 'active',
'properties': {'disk_format': 'ami', 'kernel_id': 1}}
self.assertRaises(exception.NotFound, self._get_k_r, image_meta)
def test_ami_kernel_ramdisk_present(self):
"""Return IDs if both kernel and ramdisk are present"""
image_meta = {'id': 1, 'status': 'active',
'properties': {'disk_format': 'ami', 'kernel_id': 1,
'ramdisk_id': 2}}
kernel_id, ramdisk_id = self._get_k_r(image_meta)
self.assertEqual(kernel_id, 1)
self.assertEqual(ramdisk_id, 2)
@staticmethod
def _get_k_r(image_meta):
"""Rebinding function to a shorter name for convenience"""
kernel_id, ramdisk_id = \
servers.Controller._do_get_kernel_ramdisk_from_image(image_meta)
return kernel_id, ramdisk_id

View File

@ -0,0 +1,97 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack LLC.
# 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.
import json
import webob
from nova import context
from nova import test
from nova.tests.api.openstack import fakes
from nova.api.openstack import views
class VersionsTest(test.TestCase):
def setUp(self):
super(VersionsTest, self).setUp()
self.context = context.get_admin_context()
def tearDown(self):
super(VersionsTest, self).tearDown()
def test_get_version_list(self):
req = webob.Request.blank('/')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
versions = json.loads(res.body)["versions"]
expected = [
{
"id": "v1.1",
"status": "CURRENT",
"links": [
{
"rel": "self",
"href": "http://localhost/v1.1",
}
],
},
{
"id": "v1.0",
"status": "DEPRECATED",
"links": [
{
"rel": "self",
"href": "http://localhost/v1.0",
}
],
},
]
self.assertEqual(versions, expected)
def test_view_builder(self):
base_url = "http://example.org/"
version_data = {
"id": "3.2.1",
"status": "CURRENT",
}
expected = {
"id": "3.2.1",
"status": "CURRENT",
"links": [
{
"rel": "self",
"href": "http://example.org/3.2.1",
},
],
}
builder = views.versions.ViewBuilder(base_url)
output = builder.build(version_data)
self.assertEqual(output, expected)
def test_generate_href(self):
base_url = "http://example.org/app/"
version_number = "v1.4.6"
expected = "http://example.org/app/v1.4.6"
builder = views.versions.ViewBuilder(base_url)
actual = builder.generate_href(version_number)
self.assertEqual(actual, expected)

View File

@ -24,7 +24,7 @@ from nova import test
from nova import utils
def stub_out_db_instance_api(stubs):
def stub_out_db_instance_api(stubs, injected=True):
""" Stubs out the db API for creating Instances """
INSTANCE_TYPES = {
@ -56,6 +56,25 @@ def stub_out_db_instance_api(stubs):
flavorid=5,
rxtx_cap=5)}
network_fields = {
'id': 'test',
'bridge': 'xenbr0',
'label': 'test_network',
'netmask': '255.255.255.0',
'cidr_v6': 'fe80::a00:0/120',
'netmask_v6': '120',
'gateway': '10.0.0.1',
'gateway_v6': 'fe80::a00:1',
'broadcast': '10.0.0.255',
'dns': '10.0.0.2',
'ra_server': None,
'injected': injected}
fixed_ip_fields = {
'address': '10.0.0.3',
'address_v6': 'fe80::a00:3',
'network_id': 'test'}
class FakeModel(object):
""" Stubs out for model """
def __init__(self, values):
@ -76,38 +95,29 @@ def stub_out_db_instance_api(stubs):
def fake_instance_type_get_by_name(context, name):
return INSTANCE_TYPES[name]
def fake_instance_create(values):
""" Stubs out the db.instance_create method """
type_data = INSTANCE_TYPES[values['instance_type']]
base_options = {
'name': values['name'],
'id': values['id'],
'reservation_id': utils.generate_uid('r'),
'image_id': values['image_id'],
'kernel_id': values['kernel_id'],
'ramdisk_id': values['ramdisk_id'],
'state_description': 'scheduling',
'user_id': values['user_id'],
'project_id': values['project_id'],
'launch_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'instance_type': values['instance_type'],
'memory_mb': type_data['memory_mb'],
'mac_address': values['mac_address'],
'vcpus': type_data['vcpus'],
'local_gb': type_data['local_gb'],
'os_type': values['os_type']}
return FakeModel(base_options)
def fake_network_get_by_instance(context, instance_id):
fields = {
'bridge': 'xenbr0',
}
return FakeModel(fields)
return FakeModel(network_fields)
def fake_network_get_all_by_instance(context, instance_id):
return [FakeModel(network_fields)]
def fake_instance_get_fixed_address(context, instance_id):
return FakeModel(fixed_ip_fields).address
def fake_instance_get_fixed_address_v6(context, instance_id):
return FakeModel(fixed_ip_fields).address
def fake_fixed_ip_get_all_by_instance(context, instance_id):
return [FakeModel(fixed_ip_fields)]
stubs.Set(db, 'instance_create', fake_instance_create)
stubs.Set(db, 'network_get_by_instance', fake_network_get_by_instance)
stubs.Set(db, 'instance_type_get_all', fake_instance_type_get_all)
stubs.Set(db, 'instance_type_get_by_name', fake_instance_type_get_by_name)
stubs.Set(db, 'instance_get_fixed_address',
fake_instance_get_fixed_address)
stubs.Set(db, 'instance_get_fixed_address_v6',
fake_instance_get_fixed_address_v6)
stubs.Set(db, 'network_get_all_by_instance',
fake_network_get_all_by_instance)
stubs.Set(db, 'fixed_ip_get_all_by_instance',
fake_fixed_ip_get_all_by_instance)

106
nova/tests/fake_utils.py Normal file
View File

@ -0,0 +1,106 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2011 Citrix Systems, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""This modules stubs out functions in nova.utils
"""
import re
import types
from eventlet import greenthread
from nova import exception
from nova import log as logging
from nova import utils
LOG = logging.getLogger('nova.tests.fake_utils')
_fake_execute_repliers = []
_fake_execute_log = []
def fake_execute_get_log():
return _fake_execute_log
def fake_execute_clear_log():
global _fake_execute_log
_fake_execute_log = []
def fake_execute_set_repliers(repliers):
"""Allows the client to configure replies to commands"""
global _fake_execute_repliers
_fake_execute_repliers = repliers
def fake_execute_default_reply_handler(*ignore_args, **ignore_kwargs):
"""A reply handler for commands that haven't been added to the reply
list. Returns empty strings for stdout and stderr
"""
return '', ''
def fake_execute(*cmd_parts, **kwargs):
"""This function stubs out execute, optionally executing
a preconfigued function to return expected data
"""
global _fake_execute_repliers
process_input = kwargs.get('process_input', None)
addl_env = kwargs.get('addl_env', None)
check_exit_code = kwargs.get('check_exit_code', 0)
cmd_str = ' '.join(str(part) for part in cmd_parts)
LOG.debug(_("Faking execution of cmd (subprocess): %s"), cmd_str)
_fake_execute_log.append(cmd_str)
reply_handler = fake_execute_default_reply_handler
for fake_replier in _fake_execute_repliers:
if re.match(fake_replier[0], cmd_str):
reply_handler = fake_replier[1]
LOG.debug(_('Faked command matched %s') % fake_replier[0])
break
if isinstance(reply_handler, basestring):
# If the reply handler is a string, return it as stdout
reply = reply_handler, ''
else:
try:
# Alternative is a function, so call it
reply = reply_handler(cmd_parts,
process_input=process_input,
addl_env=addl_env,
check_exit_code=check_exit_code)
except exception.ProcessExecutionError as e:
LOG.debug(_('Faked command raised an exception %s' % str(e)))
raise
stdout = reply[0]
stderr = reply[1]
LOG.debug(_("Reply to faked command is stdout='%(stdout)s' "
"stderr='%(stderr)s'") % locals())
# Replicate the sleep call in the real function
greenthread.sleep(0)
return reply
def stub_out_utils_execute(stubs):
fake_execute_set_repliers([])
fake_execute_clear_log()
stubs.Set(utils, 'execute', fake_execute)

View File

@ -19,11 +19,15 @@ Test suite for XenAPI
"""
import functools
import os
import re
import stubout
import ast
from nova import db
from nova import context
from nova import flags
from nova import log as logging
from nova import test
from nova import utils
from nova.auth import manager
@ -38,6 +42,9 @@ from nova.virt.xenapi.vmops import VMOps
from nova.tests.db import fakes as db_fakes
from nova.tests.xenapi import stubs
from nova.tests.glance import stubs as glance_stubs
from nova.tests import fake_utils
LOG = logging.getLogger('nova.tests.test_xenapi')
FLAGS = flags.FLAGS
@ -64,13 +71,14 @@ class XenAPIVolumeTestCase(test.TestCase):
def setUp(self):
super(XenAPIVolumeTestCase, self).setUp()
self.stubs = stubout.StubOutForTesting()
self.context = context.RequestContext('fake', 'fake', False)
FLAGS.target_host = '127.0.0.1'
FLAGS.xenapi_connection_url = 'test_url'
FLAGS.xenapi_connection_password = 'test_pass'
db_fakes.stub_out_db_instance_api(self.stubs)
stubs.stub_out_get_target(self.stubs)
xenapi_fake.reset()
self.values = {'name': 1, 'id': 1,
self.values = {'id': 1,
'project_id': 'fake',
'user_id': 'fake',
'image_id': 1,
@ -90,7 +98,7 @@ class XenAPIVolumeTestCase(test.TestCase):
vol['availability_zone'] = FLAGS.storage_availability_zone
vol['status'] = "creating"
vol['attach_status'] = "detached"
return db.volume_create(context.get_admin_context(), vol)
return db.volume_create(self.context, vol)
def test_create_iscsi_storage(self):
""" This shows how to test helper classes' methods """
@ -126,7 +134,7 @@ class XenAPIVolumeTestCase(test.TestCase):
stubs.stubout_session(self.stubs, stubs.FakeSessionForVolumeTests)
conn = xenapi_conn.get_connection(False)
volume = self._create_volume()
instance = db.instance_create(self.values)
instance = db.instance_create(self.context, self.values)
vm = xenapi_fake.create_vm(instance.name, 'Running')
result = conn.attach_volume(instance.name, volume['id'], '/dev/sdc')
@ -146,7 +154,7 @@ class XenAPIVolumeTestCase(test.TestCase):
stubs.FakeSessionForVolumeFailedTests)
conn = xenapi_conn.get_connection(False)
volume = self._create_volume()
instance = db.instance_create(self.values)
instance = db.instance_create(self.context, self.values)
xenapi_fake.create_vm(instance.name, 'Running')
self.assertRaises(Exception,
conn.attach_volume,
@ -175,8 +183,9 @@ class XenAPIVMTestCase(test.TestCase):
self.project = self.manager.create_project('fake', 'fake', 'fake')
self.network = utils.import_object(FLAGS.network_manager)
self.stubs = stubout.StubOutForTesting()
FLAGS.xenapi_connection_url = 'test_url'
FLAGS.xenapi_connection_password = 'test_pass'
self.flags(xenapi_connection_url='test_url',
xenapi_connection_password='test_pass',
instance_name_template='%d')
xenapi_fake.reset()
xenapi_fake.create_local_srs()
db_fakes.stub_out_db_instance_api(self.stubs)
@ -189,6 +198,8 @@ class XenAPIVMTestCase(test.TestCase):
stubs.stub_out_vm_methods(self.stubs)
glance_stubs.stubout_glance_client(self.stubs,
glance_stubs.FakeGlance)
fake_utils.stub_out_utils_execute(self.stubs)
self.context = context.RequestContext('fake', 'fake', False)
self.conn = xenapi_conn.get_connection(False)
def test_list_instances_0(self):
@ -213,7 +224,7 @@ class XenAPIVMTestCase(test.TestCase):
if not vm_rec["is_control_domain"]:
vm_labels.append(vm_rec["name_label"])
self.assertEquals(vm_labels, [1])
self.assertEquals(vm_labels, ['1'])
def ensure_vbd_was_torn_down():
vbd_labels = []
@ -221,7 +232,7 @@ class XenAPIVMTestCase(test.TestCase):
vbd_rec = xenapi_fake.get_record('VBD', vbd_ref)
vbd_labels.append(vbd_rec["vm_name_label"])
self.assertEquals(vbd_labels, [1])
self.assertEquals(vbd_labels, ['1'])
def ensure_vdi_was_torn_down():
for vdi_ref in xenapi_fake.get_all('VDI'):
@ -238,11 +249,10 @@ class XenAPIVMTestCase(test.TestCase):
def create_vm_record(self, conn, os_type):
instances = conn.list_instances()
self.assertEquals(instances, [1])
self.assertEquals(instances, ['1'])
# Get Nova record for VM
vm_info = conn.get_info(1)
# Get XenAPI record for VM
vms = [rec for ref, rec
in xenapi_fake.get_all_records('VM').iteritems()
@ -251,7 +261,7 @@ class XenAPIVMTestCase(test.TestCase):
self.vm_info = vm_info
self.vm = vm
def check_vm_record(self, conn):
def check_vm_record(self, conn, check_injection=False):
# Check that m1.large above turned into the right thing.
instance_type = db.instance_type_get_by_name(conn, 'm1.large')
mem_kib = long(instance_type['memory_mb']) << 10
@ -271,6 +281,25 @@ class XenAPIVMTestCase(test.TestCase):
# Check that the VM is running according to XenAPI.
self.assertEquals(self.vm['power_state'], 'Running')
if check_injection:
xenstore_data = self.vm['xenstore_data']
key = 'vm-data/networking/aabbccddeeff'
xenstore_value = xenstore_data[key]
tcpip_data = ast.literal_eval(xenstore_value)
self.assertEquals(tcpip_data, {
'label': 'test_network',
'broadcast': '10.0.0.255',
'ips': [{'ip': '10.0.0.3',
'netmask':'255.255.255.0',
'enabled':'1'}],
'ip6s': [{'ip': 'fe80::a8bb:ccff:fedd:eeff',
'netmask': '120',
'enabled': '1',
'gateway': 'fe80::a00:1'}],
'mac': 'aa:bb:cc:dd:ee:ff',
'dns': ['10.0.0.2'],
'gateway': '10.0.0.1'})
def check_vm_params_for_windows(self):
self.assertEquals(self.vm['platform']['nx'], 'true')
self.assertEquals(self.vm['HVM_boot_params'], {'order': 'dc'})
@ -304,10 +333,10 @@ class XenAPIVMTestCase(test.TestCase):
self.assertEquals(self.vm['HVM_boot_policy'], '')
def _test_spawn(self, image_id, kernel_id, ramdisk_id,
instance_type="m1.large", os_type="linux"):
stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
values = {'name': 1,
'id': 1,
instance_type="m1.large", os_type="linux",
check_injection=False):
stubs.stubout_loopingcall_start(self.stubs)
values = {'id': 1,
'project_id': self.project.id,
'user_id': self.user.id,
'image_id': image_id,
@ -316,12 +345,10 @@ class XenAPIVMTestCase(test.TestCase):
'instance_type': instance_type,
'mac_address': 'aa:bb:cc:dd:ee:ff',
'os_type': os_type}
conn = xenapi_conn.get_connection(False)
instance = db.instance_create(values)
conn.spawn(instance)
self.create_vm_record(conn, os_type)
self.check_vm_record(conn)
instance = db.instance_create(self.context, values)
self.conn.spawn(instance)
self.create_vm_record(self.conn, os_type)
self.check_vm_record(self.conn, check_injection)
def test_spawn_not_enough_memory(self):
FLAGS.xenapi_image_service = 'glance'
@ -362,6 +389,85 @@ class XenAPIVMTestCase(test.TestCase):
glance_stubs.FakeGlance.IMAGE_RAMDISK)
self.check_vm_params_for_linux_with_external_kernel()
def test_spawn_netinject_file(self):
FLAGS.xenapi_image_service = 'glance'
db_fakes.stub_out_db_instance_api(self.stubs, injected=True)
self._tee_executed = False
def _tee_handler(cmd, **kwargs):
input = kwargs.get('process_input', None)
self.assertNotEqual(input, None)
config = [line.strip() for line in input.split("\n")]
# Find the start of eth0 configuration and check it
index = config.index('auto eth0')
self.assertEquals(config[index + 1:index + 8], [
'iface eth0 inet static',
'address 10.0.0.3',
'netmask 255.255.255.0',
'broadcast 10.0.0.255',
'gateway 10.0.0.1',
'dns-nameservers 10.0.0.2',
''])
self._tee_executed = True
return '', ''
fake_utils.fake_execute_set_repliers([
# Capture the sudo tee .../etc/network/interfaces command
(r'(sudo\s+)?tee.*interfaces', _tee_handler),
])
FLAGS.xenapi_image_service = 'glance'
self._test_spawn(glance_stubs.FakeGlance.IMAGE_MACHINE,
glance_stubs.FakeGlance.IMAGE_KERNEL,
glance_stubs.FakeGlance.IMAGE_RAMDISK,
check_injection=True)
self.assertTrue(self._tee_executed)
def test_spawn_netinject_xenstore(self):
FLAGS.xenapi_image_service = 'glance'
db_fakes.stub_out_db_instance_api(self.stubs, injected=True)
self._tee_executed = False
def _mount_handler(cmd, *ignore_args, **ignore_kwargs):
# When mounting, create real files under the mountpoint to simulate
# files in the mounted filesystem
# mount point will be the last item of the command list
self._tmpdir = cmd[len(cmd) - 1]
LOG.debug(_('Creating files in %s to simulate guest agent' %
self._tmpdir))
os.makedirs(os.path.join(self._tmpdir, 'usr', 'sbin'))
# Touch the file using open
open(os.path.join(self._tmpdir, 'usr', 'sbin',
'xe-update-networking'), 'w').close()
return '', ''
def _umount_handler(cmd, *ignore_args, **ignore_kwargs):
# Umount would normall make files in the m,ounted filesystem
# disappear, so do that here
LOG.debug(_('Removing simulated guest agent files in %s' %
self._tmpdir))
os.remove(os.path.join(self._tmpdir, 'usr', 'sbin',
'xe-update-networking'))
os.rmdir(os.path.join(self._tmpdir, 'usr', 'sbin'))
os.rmdir(os.path.join(self._tmpdir, 'usr'))
return '', ''
def _tee_handler(cmd, *ignore_args, **ignore_kwargs):
self._tee_executed = True
return '', ''
fake_utils.fake_execute_set_repliers([
(r'(sudo\s+)?mount', _mount_handler),
(r'(sudo\s+)?umount', _umount_handler),
(r'(sudo\s+)?tee.*interfaces', _tee_handler)])
self._test_spawn(1, 2, 3, check_injection=True)
# tee must not run in this case, where an injection-capable
# guest agent is detected
self.assertFalse(self._tee_executed)
def test_spawn_with_network_qos(self):
self._create_instance()
for vif_ref in xenapi_fake.get_all('VIF'):
@ -371,6 +477,7 @@ class XenAPIVMTestCase(test.TestCase):
str(4 * 1024))
def test_rescue(self):
self.flags(xenapi_inject_image=False)
instance = self._create_instance()
conn = xenapi_conn.get_connection(False)
conn.rescue(instance, None)
@ -391,8 +498,8 @@ class XenAPIVMTestCase(test.TestCase):
def _create_instance(self):
"""Creates and spawns a test instance"""
stubs.stubout_loopingcall_start(self.stubs)
values = {
'name': 1,
'id': 1,
'project_id': self.project.id,
'user_id': self.user.id,
@ -402,7 +509,7 @@ class XenAPIVMTestCase(test.TestCase):
'instance_type': 'm1.large',
'mac_address': 'aa:bb:cc:dd:ee:ff',
'os_type': 'linux'}
instance = db.instance_create(values)
instance = db.instance_create(self.context, values)
self.conn.spawn(instance)
return instance
@ -447,21 +554,26 @@ class XenAPIMigrateInstance(test.TestCase):
db_fakes.stub_out_db_instance_api(self.stubs)
stubs.stub_out_get_target(self.stubs)
xenapi_fake.reset()
xenapi_fake.create_network('fake', FLAGS.flat_network_bridge)
self.manager = manager.AuthManager()
self.user = self.manager.create_user('fake', 'fake', 'fake',
admin=True)
self.project = self.manager.create_project('fake', 'fake', 'fake')
self.values = {'name': 1, 'id': 1,
self.context = context.RequestContext('fake', 'fake', False)
self.values = {'id': 1,
'project_id': self.project.id,
'user_id': self.user.id,
'image_id': 1,
'kernel_id': None,
'ramdisk_id': None,
'local_gb': 5,
'instance_type': 'm1.large',
'mac_address': 'aa:bb:cc:dd:ee:ff',
'os_type': 'linux'}
fake_utils.stub_out_utils_execute(self.stubs)
stubs.stub_out_migration_methods(self.stubs)
stubs.stubout_get_this_vm_uuid(self.stubs)
glance_stubs.stubout_glance_client(self.stubs,
glance_stubs.FakeGlance)
@ -472,14 +584,15 @@ class XenAPIMigrateInstance(test.TestCase):
self.stubs.UnsetAll()
def test_migrate_disk_and_power_off(self):
instance = db.instance_create(self.values)
instance = db.instance_create(self.context, self.values)
stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests)
conn = xenapi_conn.get_connection(False)
conn.migrate_disk_and_power_off(instance, '127.0.0.1')
def test_finish_resize(self):
instance = db.instance_create(self.values)
instance = db.instance_create(self.context, self.values)
stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests)
stubs.stubout_loopingcall_start(self.stubs)
conn = xenapi_conn.get_connection(False)
conn.finish_resize(instance, dict(base_copy='hurr', cow='durr'))

View File

@ -21,6 +21,7 @@ from nova.virt.xenapi import fake
from nova.virt.xenapi import volume_utils
from nova.virt.xenapi import vm_utils
from nova.virt.xenapi import vmops
from nova import utils
def stubout_instance_snapshot(stubs):
@ -137,14 +138,17 @@ def stubout_is_vdi_pv(stubs):
stubs.Set(vm_utils, '_is_vdi_pv', f)
def stubout_loopingcall_start(stubs):
def fake_start(self, interval, now=True):
self.f(*self.args, **self.kw)
stubs.Set(utils.LoopingCall, 'start', fake_start)
class FakeSessionForVMTests(fake.SessionBase):
""" Stubs out a XenAPISession for VM tests """
def __init__(self, uri):
super(FakeSessionForVMTests, self).__init__(uri)
def network_get_all_records_where(self, _1, _2):
return self.xenapi.network.get_all_records()
def host_call_plugin(self, _1, _2, _3, _4, _5):
sr_ref = fake.get_all('SR')[0]
vdi_ref = fake.create_vdi('', False, sr_ref, False)
@ -196,7 +200,7 @@ def stub_out_vm_methods(stubs):
pass
def fake_spawn_rescue(self, inst):
pass
inst._rescue = False
stubs.Set(vmops.VMOps, "_shutdown", fake_shutdown)
stubs.Set(vmops.VMOps, "_acquire_bootlock", fake_acquire_bootlock)

View File

@ -26,6 +26,8 @@ import os
import tempfile
import time
from nova import context
from nova import db
from nova import exception
from nova import flags
from nova import log as logging
@ -38,6 +40,9 @@ flags.DEFINE_integer('minimum_root_size', 1024 * 1024 * 1024 * 10,
'minimum size in bytes of root partition')
flags.DEFINE_integer('block_size', 1024 * 1024 * 256,
'block_size to use for dd')
flags.DEFINE_string('injected_network_template',
utils.abspath('virt/interfaces.template'),
'Template file for injected network')
flags.DEFINE_integer('timeout_nbd', 10,
'time to wait for a NBD device coming up')
flags.DEFINE_integer('max_nbd_devices', 16,
@ -97,11 +102,7 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False):
% err)
try:
if key:
# inject key file
_inject_key_into_fs(key, tmpdir)
if net:
_inject_net_into_fs(net, tmpdir)
inject_data_into_fs(tmpdir, key, net, utils.execute)
finally:
# unmount device
utils.execute('sudo', 'umount', mapped_device)
@ -196,7 +197,18 @@ def _free_device(device):
_DEVICES.append(device)
def _inject_key_into_fs(key, fs):
def inject_data_into_fs(fs, key, net, execute):
"""Injects data into a filesystem already mounted by the caller.
Virt connections can call this directly if they mount their fs
in a different way to inject_data
"""
if key:
_inject_key_into_fs(key, fs, execute=execute)
if net:
_inject_net_into_fs(net, fs, execute=execute)
def _inject_key_into_fs(key, fs, execute=None):
"""Add the given public ssh key to root's authorized_keys.
key is an ssh key string.
@ -211,7 +223,7 @@ def _inject_key_into_fs(key, fs):
process_input='\n' + key.strip() + '\n')
def _inject_net_into_fs(net, fs):
def _inject_net_into_fs(net, fs, execute=None):
"""Inject /etc/network/interfaces into the filesystem rooted at fs.
net is the contents of /etc/network/interfaces.

View File

@ -76,9 +76,7 @@ flags.DECLARE('live_migration_retry_count', 'nova.compute.manager')
flags.DEFINE_string('rescue_image_id', 'ami-rescue', 'Rescue ami image')
flags.DEFINE_string('rescue_kernel_id', 'aki-rescue', 'Rescue aki image')
flags.DEFINE_string('rescue_ramdisk_id', 'ari-rescue', 'Rescue ari image')
flags.DEFINE_string('injected_network_template',
utils.abspath('virt/interfaces.template'),
'Template file for injected network')
flags.DEFINE_string('libvirt_xml_template',
utils.abspath('virt/libvirt.xml.template'),
'Libvirt XML Template')

View File

@ -162,6 +162,12 @@ def after_VBD_create(vbd_ref, vbd_rec):
vbd_rec['vm_name_label'] = vm_name_label
def after_VM_create(vm_ref, vm_rec):
"""Create read-only fields in the VM record."""
if 'is_control_domain' not in vm_rec:
vm_rec['is_control_domain'] = False
def create_pbd(config, host_ref, sr_ref, attached):
return _create_object('PBD', {
'device-config': config,
@ -286,6 +292,25 @@ class SessionBase(object):
rec['currently_attached'] = False
rec['device'] = ''
def VM_get_xenstore_data(self, _1, vm_ref):
return _db_content['VM'][vm_ref].get('xenstore_data', '')
def VM_remove_from_xenstore_data(self, _1, vm_ref, key):
db_ref = _db_content['VM'][vm_ref]
if not 'xenstore_data' in db_ref:
return
db_ref['xenstore_data'][key] = None
def network_get_all_records_where(self, _1, _2):
# TODO (salvatore-orlando):filter table on _2
return _db_content['network']
def VM_add_to_xenstore_data(self, _1, vm_ref, key, value):
db_ref = _db_content['VM'][vm_ref]
if not 'xenstore_data' in db_ref:
db_ref['xenstore_data'] = {}
db_ref['xenstore_data'][key] = value
def host_compute_free_memory(self, _1, ref):
#Always return 12GB available
return 12 * 1024 * 1024 * 1024
@ -376,7 +401,6 @@ class SessionBase(object):
def _getter(self, name, params):
self._check_session(params)
(cls, func) = name.split('.')
if func == 'get_all':
self._check_arg_count(params, 1)
return get_all(cls)
@ -399,10 +423,11 @@ class SessionBase(object):
if len(params) == 2:
field = func[len('get_'):]
ref = params[1]
if (ref in _db_content[cls] and
field in _db_content[cls][ref]):
return _db_content[cls][ref][field]
if (ref in _db_content[cls]):
if (field in _db_content[cls][ref]):
return _db_content[cls][ref][field]
else:
raise Failure(['HANDLE_INVALID', cls, ref])
LOG.debug(_('Raising NotImplemented'))
raise NotImplementedError(
@ -476,7 +501,7 @@ class SessionBase(object):
def _check_session(self, params):
if (self._session is None or
self._session not in _db_content['session']):
raise Failure(['HANDLE_INVALID', 'session', self._session])
raise Failure(['HANDLE_INVALID', 'session', self._session])
if len(params) == 0 or params[0] != self._session:
LOG.debug(_('Raising NotImplemented'))
raise NotImplementedError('Call to XenAPI without using .xenapi')

View File

@ -22,6 +22,7 @@ their attributes like VDIs, VIFs, as well as their lookup functions.
import os
import pickle
import re
import tempfile
import time
import urllib
import uuid
@ -29,6 +30,8 @@ from xml.dom import minidom
from eventlet import event
import glance.client
from nova import context
from nova import db
from nova import exception
from nova import flags
from nova import log as logging
@ -36,6 +39,7 @@ from nova import utils
from nova.auth.manager import AuthManager
from nova.compute import instance_types
from nova.compute import power_state
from nova.virt import disk
from nova.virt import images
from nova.virt.xenapi import HelperBase
from nova.virt.xenapi.volume_utils import StorageError
@ -669,6 +673,23 @@ class VMHelper(HelperBase):
else:
return None
@classmethod
def preconfigure_instance(cls, session, instance, vdi_ref, network_info):
"""Makes alterations to the image before launching as part of spawn.
"""
# As mounting the image VDI is expensive, we only want do do it once,
# if at all, so determine whether it's required first, and then do
# everything
mount_required = False
key, net = _prepare_injectables(instance, network_info)
mount_required = key or net
if not mount_required:
return
with_vdi_attached_here(session, vdi_ref, False,
lambda dev: _mounted_processing(dev, key, net))
@classmethod
def lookup_kernel_ramdisk(cls, session, vm):
vm_rec = session.get_xenapi().VM.get_record(vm)
@ -927,6 +948,7 @@ def vbd_unplug_with_retry(session, vbd_ref):
e.details[0] == 'DEVICE_DETACH_REJECTED'):
LOG.debug(_('VBD.unplug rejected: retrying...'))
time.sleep(1)
LOG.debug(_('Not sleeping anymore!'))
elif (len(e.details) > 0 and
e.details[0] == 'DEVICE_ALREADY_DETACHED'):
LOG.debug(_('VBD.unplug successful eventually.'))
@ -1002,3 +1024,114 @@ def _write_partition(virtual_size, dev):
def get_name_label_for_image(image):
# TODO(sirp): This should eventually be the URI for the Glance image
return _('Glance image %s') % image
def _mount_filesystem(dev_path, dir):
"""mounts the device specified by dev_path in dir"""
try:
out, err = utils.execute('sudo', 'mount',
'-t', 'ext2,ext3',
dev_path, dir)
except exception.ProcessExecutionError as e:
err = str(e)
return err
def _find_guest_agent(base_dir, agent_rel_path):
"""
tries to locate a guest agent at the path
specificed by agent_rel_path
"""
agent_path = os.path.join(base_dir, agent_rel_path)
if os.path.isfile(agent_path):
# The presence of the guest agent
# file indicates that this instance can
# reconfigure the network from xenstore data,
# so manipulation of files in /etc is not
# required
LOG.info(_('XenServer tools installed in this '
'image are capable of network injection. '
'Networking files will not be'
'manipulated'))
return True
xe_daemon_filename = os.path.join(base_dir,
'usr', 'sbin', 'xe-daemon')
if os.path.isfile(xe_daemon_filename):
LOG.info(_('XenServer tools are present '
'in this image but are not capable '
'of network injection'))
else:
LOG.info(_('XenServer tools are not '
'installed in this image'))
return False
def _mounted_processing(device, key, net):
"""Callback which runs with the image VDI attached"""
dev_path = '/dev/' + device + '1' # NB: Partition 1 hardcoded
tmpdir = tempfile.mkdtemp()
try:
# Mount only Linux filesystems, to avoid disturbing NTFS images
err = _mount_filesystem(dev_path, tmpdir)
if not err:
try:
# This try block ensures that the umount occurs
if not _find_guest_agent(tmpdir, FLAGS.xenapi_agent_path):
LOG.info(_('Manipulating interface files '
'directly'))
disk.inject_data_into_fs(tmpdir, key, net,
utils.execute)
finally:
utils.execute('sudo', 'umount', dev_path)
else:
LOG.info(_('Failed to mount filesystem (expected for '
'non-linux instances): %s') % err)
finally:
# remove temporary directory
os.rmdir(tmpdir)
def _prepare_injectables(inst, networks_info):
"""
prepares the ssh key and the network configuration file to be
injected into the disk image
"""
#do the import here - Cheetah.Template will be loaded
#only if injection is performed
from Cheetah import Template as t
template = t.Template
template_data = open(FLAGS.injected_network_template).read()
key = str(inst['key_data'])
net = None
if networks_info:
ifc_num = -1
interfaces_info = []
for (network_ref, info) in networks_info:
ifc_num += 1
if not network_ref['injected']:
continue
ip_v4 = ip_v6 = None
if 'ips' in info and len(info['ips']) > 0:
ip_v4 = info['ips'][0]
if 'ip6s' in info and len(info['ip6s']) > 0:
ip_v6 = info['ip6s'][0]
if len(info['dns']) > 0:
dns = info['dns'][0]
interface_info = {'name': 'eth%d' % ifc_num,
'address': ip_v4 and ip_v4['ip'] or '',
'netmask': ip_v4 and ip_v4['netmask'] or '',
'gateway': info['gateway'],
'broadcast': info['broadcast'],
'dns': dns,
'address_v6': ip_v6 and ip_v6['ip'] or '',
'netmask_v6': ip_v6 and ip_v6['netmask'] or '',
'gateway_v6': ip_v6 and ip_v6['gateway'] or '',
'use_ipv6': FLAGS.use_ipv6}
interfaces_info.append(interface_info)
net = str(template(template_data,
searchList=[{'interfaces': interfaces_info,
'use_ipv6': FLAGS.use_ipv6}]))
return key, net

View File

@ -33,6 +33,7 @@ from nova import context
from nova import log as logging
from nova import exception
from nova import utils
from nova import flags
from nova.auth.manager import AuthManager
from nova.compute import power_state
@ -43,6 +44,7 @@ from nova.virt.xenapi.vm_utils import ImageType
XenAPI = None
LOG = logging.getLogger("nova.virt.xenapi.vmops")
FLAGS = flags.FLAGS
class VMOps(object):
@ -53,7 +55,6 @@ class VMOps(object):
self.XenAPI = session.get_imported_xenapi()
self._session = session
self.poll_rescue_last_ran = None
VMHelper.XenAPI = self.XenAPI
def list_instances(self):
@ -168,6 +169,12 @@ class VMOps(object):
# create it now. This goes away once nova-multi-nic hits.
if network_info is None:
network_info = self._get_network_info(instance)
# Alter the image before VM start for, e.g. network injection
if FLAGS.xenapi_inject_image:
VMHelper.preconfigure_instance(self._session, instance,
vdi_ref, network_info)
self.create_vifs(vm_ref, network_info)
self.inject_network_info(instance, vm_ref, network_info)
return vm_ref
@ -237,26 +244,17 @@ class VMOps(object):
obj = None
try:
# check for opaque ref
obj = self._session.get_xenapi().VM.get_record(instance_or_vm)
obj = self._session.get_xenapi().VM.get_uuid(instance_or_vm)
return instance_or_vm
except self.XenAPI.Failure:
# wasn't an opaque ref, must be an instance name
# wasn't an opaque ref, can be an instance name
instance_name = instance_or_vm
# if instance_or_vm is an int/long it must be instance id
elif isinstance(instance_or_vm, (int, long)):
ctx = context.get_admin_context()
try:
instance_obj = db.instance_get(ctx, instance_or_vm)
instance_name = instance_obj.name
except exception.NotFound:
# The unit tests screw this up, as they use an integer for
# the vm name. I'd fix that up, but that's a matter for
# another bug report. So for now, just try with the passed
# value
instance_name = instance_or_vm
# otherwise instance_or_vm is an instance object
instance_obj = db.instance_get(ctx, instance_or_vm)
instance_name = instance_obj.name
else:
instance_name = instance_or_vm.name
vm_ref = VMHelper.lookup(self._session, instance_name)
@ -692,7 +690,6 @@ class VMOps(object):
vm_ref = VMHelper.lookup(self._session, instance.name)
self._shutdown(instance, vm_ref)
self._acquire_bootlock(vm_ref)
instance._rescue = True
self.spawn_rescue(instance)
rescue_vm_ref = VMHelper.lookup(self._session, instance.name)
@ -816,6 +813,7 @@ class VMOps(object):
info = {
'label': network['label'],
'gateway': network['gateway'],
'broadcast': network['broadcast'],
'mac': instance.mac_address,
'rxtx_cap': flavor['rxtx_cap'],
'dns': [network['dns']],

View File

@ -107,8 +107,22 @@ flags.DEFINE_integer('xenapi_vhd_coalesce_max_attempts',
5,
'Max number of times to poll for VHD to coalesce.'
' Used only if connection_type=xenapi.')
flags.DEFINE_bool('xenapi_inject_image',
True,
'Specifies whether an attempt to inject network/key'
' data into the disk image should be made.'
' Used only if connection_type=xenapi.')
flags.DEFINE_string('xenapi_agent_path',
'usr/sbin/xe-update-networking',
'Specifies the path in which the xenapi guest agent'
' should be located. If the agent is present,'
' network configuration is not injected into the image'
' Used only if connection_type=xenapi.'
' and xenapi_inject_image=True')
flags.DEFINE_string('xenapi_sr_base_path', '/var/run/sr-mount',
'Base path to the storage repository')
flags.DEFINE_string('target_host',
None,
'iSCSI Target Host')