merged trunk
This commit is contained in:
commit
12581e79bd
2
Authors
2
Authors
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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'})
|
||||
|
@ -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)
|
||||
|
93
nova/api/openstack/image_metadata.py
Normal file
93
nova/api/openstack/image_metadata.py
Normal 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)
|
78
nova/api/openstack/server_metadata.py
Normal file
78
nova/api/openstack/server_metadata.py
Normal 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)
|
@ -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)
|
||||
|
54
nova/api/openstack/versions.py
Normal file
54
nova/api/openstack/versions.py
Normal 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)
|
@ -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)
|
||||
|
@ -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))
|
||||
|
59
nova/api/openstack/views/versions.py
Normal file
59
nova/api/openstack/views/versions.py
Normal 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)
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
166
nova/tests/api/openstack/test_image_metadata.py
Normal file
166
nova/tests/api/openstack/test_image_metadata.py
Normal 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)
|
164
nova/tests/api/openstack/test_server_metadata.py
Normal file
164
nova/tests/api/openstack/test_server_metadata.py
Normal 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)
|
@ -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
|
||||
|
97
nova/tests/api/openstack/test_versions.py
Normal file
97
nova/tests/api/openstack/test_versions.py
Normal 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)
|
@ -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
106
nova/tests/fake_utils.py
Normal 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)
|
@ -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'))
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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']],
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user