Includes changes for creating instances via the Rackspace API. Utilizes much of the existing EC2 functionality to power the Rackspace side of things, at least for now.

This commit is contained in:
Cerberus
2010-10-01 00:58:17 +00:00
committed by Tarmac
12 changed files with 327 additions and 78 deletions

42
nova/api/cloud.py Normal file
View File

@@ -0,0 +1,42 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Methods for API calls to control instances via AMQP.
"""
from nova import db
from nova import flags
from nova import rpc
FLAGS = flags.FLAGS
def reboot(instance_id, context=None):
"""Reboot the given instance.
#TODO(gundlach) not actually sure what context is used for by ec2 here
-- I think we can just remove it and use None all the time.
"""
instance_ref = db.instance_get_by_ec2_id(None, instance_id)
host = instance_ref['host']
rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "reboot_instance",
"args": {"context": None,
"instance_id": instance_ref['id']}})

View File

@@ -36,6 +36,7 @@ from nova import quota
from nova import rpc
from nova import utils
from nova.compute.instance_types import INSTANCE_TYPES
from nova.api import cloud
from nova.api.ec2 import images
@@ -684,12 +685,7 @@ class CloudController(object):
def reboot_instances(self, context, instance_id, **kwargs):
"""instance_id is a list of instance ids"""
for id_str in instance_id:
instance_ref = db.instance_get_by_ec2_id(context, id_str)
host = instance_ref['host']
rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "reboot_instance",
"args": {"context": None,
"instance_id": instance_ref['id']}})
cloud.reboot(id_str, context=context)
return True
def update_instance(self, context, instance_id, **kwargs):

View File

@@ -0,0 +1,33 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 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.
"""
APIRequestContext
"""
import random
class Project(object):
def __init__(self, user_id):
self.id = user_id
class APIRequestContext(object):
""" This is an adapter class to get around all of the assumptions made in
the FlatNetworking """
def __init__(self, user_id):
self.user_id = user_id
self.project = Project(user_id)

View File

@@ -17,35 +17,45 @@
import time
import webob
from webob import exc
from nova import flags
from nova import rpc
from nova import utils
from nova import wsgi
from nova.api import cloud
from nova.api.rackspace import _id_translator
from nova.api.rackspace import context
from nova.api.rackspace import faults
from nova.compute import instance_types
from nova.compute import power_state
import nova.api.rackspace
import nova.image.service
FLAGS = flags.FLAGS
flags.DEFINE_string('rs_network_manager', 'nova.network.manager.FlatManager',
'Networking for rackspace')
def _instance_id_translator():
""" Helper method for initializing an id translator for Rackspace instance
ids """
return _id_translator.RackspaceAPIIdTranslator( "instance", 'nova')
def translator_instance():
def _image_service():
""" Helper method for initializing the image id translator """
service = nova.image.service.ImageService.load()
return _id_translator.RackspaceAPIIdTranslator(
"image", service.__class__.__name__)
return (service, _id_translator.RackspaceAPIIdTranslator(
"image", service.__class__.__name__))
def _filter_params(inst_dict):
""" Extracts all updatable parameters for a server update request """
keys = ['name', 'adminPass']
keys = dict(name='name', admin_pass='adminPass')
new_attrs = {}
for k in keys:
if inst_dict.has_key(k):
new_attrs[k] = inst_dict[k]
for k, v in keys.items():
if inst_dict.has_key(v):
new_attrs[k] = inst_dict[v]
return new_attrs
def _entity_list(entities):
@@ -84,7 +94,6 @@ def _entity_inst(inst):
class Controller(wsgi.Controller):
""" The Server API controller for the Openstack API """
_serialization_metadata = {
'application/xml': {
@@ -122,8 +131,11 @@ class Controller(wsgi.Controller):
def show(self, req, id):
""" Returns server details by server id """
inst_id_trans = _instance_id_translator()
inst_id = inst_id_trans.from_rs_id(id)
user_id = req.environ['nova.context']['user']['id']
inst = self.db_driver.instance_get(None, id)
inst = self.db_driver.instance_get_by_ec2_id(None, inst_id)
if inst:
if inst.user_id == user_id:
return _entity_detail(inst)
@@ -131,8 +143,11 @@ class Controller(wsgi.Controller):
def delete(self, req, id):
""" Destroys a server """
inst_id_trans = _instance_id_translator()
inst_id = inst_id_trans.from_rs_id(id)
user_id = req.environ['nova.context']['user']['id']
instance = self.db_driver.instance_get(None, id)
instance = self.db_driver.instance_get_by_ec2_id(None, inst_id)
if instance and instance['user_id'] == user_id:
self.db_driver.instance_destroy(None, id)
return faults.Fault(exc.HTTPAccepted())
@@ -140,10 +155,15 @@ class Controller(wsgi.Controller):
def create(self, req):
""" Creates a new server for a given user """
if not req.environ.has_key('inst_dict'):
env = self._deserialize(req.body, req)
if not env:
return faults.Fault(exc.HTTPUnprocessableEntity())
inst = self._build_server_instance(req)
try:
inst = self._build_server_instance(req, env)
except Exception, e:
return faults.Fault(exc.HTTPUnprocessableEntity())
rpc.cast(
FLAGS.compute_topic, {
@@ -153,62 +173,127 @@ class Controller(wsgi.Controller):
def update(self, req, id):
""" Updates the server name or password """
if not req.environ.has_key('inst_dict'):
inst_id_trans = _instance_id_translator()
inst_id = inst_id_trans.from_rs_id(id)
user_id = req.environ['nova.context']['user']['id']
inst_dict = self._deserialize(req.body, req)
if not inst_dict:
return faults.Fault(exc.HTTPUnprocessableEntity())
instance = self.db_driver.instance_get(None, id)
if not instance:
instance = self.db_driver.instance_get_by_ec2_id(None, inst_id)
if not instance or instance.user_id != user_id:
return faults.Fault(exc.HTTPNotFound())
attrs = req.environ['nova.context'].get('model_attributes', None)
if attrs:
self.db_driver.instance_update(None, id, _filter_params(attrs))
self.db_driver.instance_update(None, id,
_filter_params(inst_dict['server']))
return faults.Fault(exc.HTTPNoContent())
def action(self, req, id):
""" multi-purpose method used to reboot, rebuild, and
resize a server """
if not req.environ.has_key('inst_dict'):
return faults.Fault(exc.HTTPUnprocessableEntity())
input_dict = self._deserialize(req.body, req)
try:
reboot_type = input_dict['reboot']['type']
except Exception:
raise faults.Fault(webob.exc.HTTPNotImplemented())
opaque_id = _instance_id_translator().from_rs_id(id)
cloud.reboot(opaque_id)
def _build_server_instance(self, req):
def _build_server_instance(self, req, env):
"""Build instance data structure and save it to the data store."""
ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
inst = {}
env = req.environ['inst_dict']
image_id = env['server']['imageId']
opaque_id = translator_instance().from_rs_id(image_id)
inst['name'] = env['server']['server_name']
inst['image_id'] = opaque_id
inst['instance_type'] = env['server']['flavorId']
inst_id_trans = _instance_id_translator()
user_id = req.environ['nova.context']['user']['id']
inst['user_id'] = user_id
flavor_id = env['server']['flavorId']
instance_type, flavor = [(k, v) for k, v in
instance_types.INSTANCE_TYPES.iteritems()
if v['flavorid'] == flavor_id][0]
image_id = env['server']['imageId']
img_service, image_id_trans = _image_service()
opaque_image_id = image_id_trans.to_rs_id(image_id)
image = img_service.show(opaque_image_id)
if not image:
raise Exception, "Image not found"
inst['server_name'] = env['server']['name']
inst['image_id'] = opaque_image_id
inst['user_id'] = user_id
inst['launch_time'] = ltime
inst['mac_address'] = utils.generate_mac()
inst['project_id'] = user_id
inst['project_id'] = env['project']['id']
inst['reservation_id'] = reservation
reservation = utils.generate_uid('r')
inst['state_description'] = 'scheduling'
inst['kernel_id'] = image.get('kernelId', FLAGS.default_kernel)
inst['ramdisk_id'] = image.get('ramdiskId', FLAGS.default_ramdisk)
inst['reservation_id'] = utils.generate_uid('r')
address = self.network.allocate_ip(
inst['user_id'],
inst['project_id'],
mac=inst['mac_address'])
inst['display_name'] = env['server']['name']
inst['display_description'] = env['server']['name']
inst['private_dns_name'] = str(address)
inst['bridge_name'] = network.BridgedNetwork.get_network_for_project(
inst['user_id'],
inst['project_id'],
'default')['bridge_name']
#TODO(dietz) this may be ill advised
key_pair_ref = self.db_driver.key_pair_get_all_by_user(
None, user_id)[0]
inst['key_data'] = key_pair_ref['public_key']
inst['key_name'] = key_pair_ref['name']
#TODO(dietz) stolen from ec2 api, see TODO there
inst['security_group'] = 'default'
# Flavor related attributes
inst['instance_type'] = instance_type
inst['memory_mb'] = flavor['memory_mb']
inst['vcpus'] = flavor['vcpus']
inst['local_gb'] = flavor['local_gb']
ref = self.db_driver.instance_create(None, inst)
inst['id'] = ref.id
inst['id'] = inst_id_trans.to_rs_id(ref.ec2_id)
# TODO(dietz): this isn't explicitly necessary, but the networking
# calls depend on an object with a project_id property, and therefore
# should be cleaned up later
api_context = context.APIRequestContext(user_id)
inst['mac_address'] = utils.generate_mac()
#TODO(dietz) is this necessary?
inst['launch_index'] = 0
inst['hostname'] = ref.ec2_id
self.db_driver.instance_update(None, inst['id'], inst)
network_manager = utils.import_object(FLAGS.rs_network_manager)
address = network_manager.allocate_fixed_ip(api_context,
inst['id'])
# TODO(vish): This probably should be done in the scheduler
# network is setup when host is assigned
network_topic = self._get_network_topic(user_id)
rpc.call(network_topic,
{"method": "setup_fixed_ip",
"args": {"context": None,
"address": address}})
return inst
def _get_network_topic(self, user_id):
"""Retrieves the network host for a project"""
network_ref = self.db_driver.project_get_network(None,
user_id)
host = network_ref['host']
if not host:
host = rpc.call(FLAGS.network_topic,
{"method": "set_network_host",
"args": {"context": None,
"project_id": user_id}})
return self.db_driver.queue_get_for(None, FLAGS.network_topic, host)

View File

@@ -199,6 +199,8 @@ class Instance(BASE, NovaBase):
id = Column(Integer, primary_key=True)
ec2_id = Column(String(10), unique=True)
admin_pass = Column(String(255))
user_id = Column(String(255))
project_id = Column(String(255))

View File

@@ -1,12 +1,14 @@
import datetime
import unittest
import stubout
import webob
import webob.dec
import unittest
import stubout
import nova.api
import nova.api.rackspace.auth
from nova import auth
from nova.tests.api.rackspace import test_helper
import datetime
class Test(unittest.TestCase):
def setUp(self):

View File

@@ -38,7 +38,6 @@ class FlavorsTest(unittest.TestCase):
def test_get_flavor_list(self):
req = webob.Request.blank('/v1.0/flavors')
res = req.get_response(nova.api.API())
print res
def test_get_flavor_by_id(self):
pass

View File

@@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import stubout
import unittest
from nova.api.rackspace import images

View File

@@ -26,6 +26,7 @@ import nova.api.rackspace
from nova.api.rackspace import servers
import nova.db.api
from nova.db.sqlalchemy.models import Instance
import nova.rpc
from nova.tests.api.test_helper import *
from nova.tests.api.rackspace import test_helper
@@ -52,8 +53,11 @@ class ServersTest(unittest.TestCase):
test_helper.stub_for_testing(self.stubs)
test_helper.stub_out_rate_limiting(self.stubs)
test_helper.stub_out_auth(self.stubs)
test_helper.stub_out_id_translator(self.stubs)
test_helper.stub_out_key_pair_funcs(self.stubs)
test_helper.stub_out_image_service(self.stubs)
self.stubs.Set(nova.db.api, 'instance_get_all', return_servers)
self.stubs.Set(nova.db.api, 'instance_get', return_server)
self.stubs.Set(nova.db.api, 'instance_get_by_ec2_id', return_server)
self.stubs.Set(nova.db.api, 'instance_get_all_by_user',
return_servers)
@@ -67,9 +71,6 @@ class ServersTest(unittest.TestCase):
self.assertEqual(res_dict['server']['id'], '1')
self.assertEqual(res_dict['server']['name'], 'server1')
def test_get_backup_schedule(self):
pass
def test_get_server_list(self):
req = webob.Request.blank('/v1.0/servers')
res = req.get_response(nova.api.API())
@@ -82,24 +83,86 @@ class ServersTest(unittest.TestCase):
self.assertEqual(s.get('imageId', None), None)
i += 1
#def test_create_instance(self):
# test_helper.stub_out_image_translator(self.stubs)
# body = dict(server=dict(
# name='server_test', imageId=2, flavorId=2, metadata={},
# personality = {}
# ))
# req = webob.Request.blank('/v1.0/servers')
# req.method = 'POST'
# req.body = json.dumps(body)
def test_create_instance(self):
def server_update(context, id, params):
pass
# res = req.get_response(nova.api.API())
def instance_create(context, inst):
class Foo(object):
ec2_id = 1
return Foo()
# print res
def test_update_server_password(self):
pass
def fake_method(*args, **kwargs):
pass
def project_get_network(context, user_id):
return dict(id='1', host='localhost')
def test_update_server_name(self):
pass
def queue_get_for(context, *args):
return 'network_topic'
self.stubs.Set(nova.db.api, 'project_get_network', project_get_network)
self.stubs.Set(nova.db.api, 'instance_create', instance_create)
self.stubs.Set(nova.rpc, 'cast', fake_method)
self.stubs.Set(nova.rpc, 'call', fake_method)
self.stubs.Set(nova.db.api, 'instance_update',
server_update)
self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for)
self.stubs.Set(nova.network.manager.FlatManager, 'allocate_fixed_ip',
fake_method)
test_helper.stub_out_id_translator(self.stubs)
body = dict(server=dict(
name='server_test', imageId=2, flavorId=2, metadata={},
personality = {}
))
req = webob.Request.blank('/v1.0/servers')
req.method = 'POST'
req.body = json.dumps(body)
res = req.get_response(nova.api.API())
self.assertEqual(res.status_int, 200)
def test_update_no_body(self):
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'PUT'
res = req.get_response(nova.api.API())
self.assertEqual(res.status_int, 422)
def test_update_bad_params(self):
""" Confirm that update is filtering params """
inst_dict = dict(cat='leopard', name='server_test', adminPass='bacon')
self.body = json.dumps(dict(server=inst_dict))
def server_update(context, id, params):
self.update_called = True
filtered_dict = dict(name='server_test', admin_pass='bacon')
self.assertEqual(params, filtered_dict)
self.stubs.Set(nova.db.api, 'instance_update',
server_update)
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'PUT'
req.body = self.body
req.get_response(nova.api.API())
def test_update_server(self):
inst_dict = dict(name='server_test', adminPass='bacon')
self.body = json.dumps(dict(server=inst_dict))
def server_update(context, id, params):
filtered_dict = dict(name='server_test', admin_pass='bacon')
self.assertEqual(params, filtered_dict)
self.stubs.Set(nova.db.api, 'instance_update',
server_update)
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'PUT'
req.body = self.body
req.get_response(nova.api.API())
def test_create_backup_schedules(self):
req = webob.Request.blank('/v1.0/servers/1/backup_schedules')

View File

@@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import stubout
import unittest
from nova.api.rackspace import sharedipgroups

View File

@@ -9,6 +9,7 @@ from nova import utils
from nova import flags
import nova.api.rackspace.auth
import nova.api.rackspace._id_translator
from nova.image import service
from nova.wsgi import Router
FLAGS = flags.FLAGS
@@ -40,7 +41,19 @@ def fake_wsgi(self, req):
req.environ['inst_dict'] = json.loads(req.body)
return self.application
def stub_out_image_translator(stubs):
def stub_out_key_pair_funcs(stubs):
def key_pair(context, user_id):
return [dict(name='key', public_key='public_key')]
stubs.Set(nova.db.api, 'key_pair_get_all_by_user',
key_pair)
def stub_out_image_service(stubs):
def fake_image_show(meh, id):
return dict(kernelId=1, ramdiskId=1)
stubs.Set(nova.image.service.LocalImageService, 'show', fake_image_show)
def stub_out_id_translator(stubs):
class FakeTranslator(object):
def __init__(self, id_type, service_name):
pass

View File

@@ -230,6 +230,15 @@ class Controller(object):
serializer = Serializer(request.environ, _metadata)
return serializer.to_content_type(data)
def _deserialize(self, data, request):
"""
Deserialize the request body to the response type requested in request.
Uses self._serialization_metadata if it exists, which is a dict mapping
MIME types to information needed to serialize to that type.
"""
_metadata = getattr(type(self), "_serialization_metadata", {})
serializer = Serializer(request.environ, _metadata)
return serializer.deserialize(data)
class Serializer(object):
"""
@@ -272,10 +281,13 @@ class Serializer(object):
The string must be in the format of a supported MIME type.
"""
datastring = datastring.strip()
is_xml = (datastring[0] == '<')
if not is_xml:
return json.loads(datastring)
return self._from_xml(datastring)
try:
is_xml = (datastring[0] == '<')
if not is_xml:
return json.loads(datastring)
return self._from_xml(datastring)
except:
return None
def _from_xml(self, datastring):
xmldata = self.metadata.get('application/xml', {})