summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/cloud.py42
-rw-r--r--nova/api/ec2/__init__.py2
-rw-r--r--nova/api/ec2/cloud.py108
-rw-r--r--nova/api/ec2/images.py8
-rw-r--r--nova/api/rackspace/__init__.py47
-rw-r--r--nova/api/rackspace/_id_translator.py2
-rw-r--r--nova/api/rackspace/auth.py15
-rw-r--r--nova/api/rackspace/backup_schedules.py39
-rw-r--r--nova/api/rackspace/context.py (renamed from nova/api/rackspace/base.py)25
-rw-r--r--nova/api/rackspace/faults.py62
-rw-r--r--nova/api/rackspace/flavors.py12
-rw-r--r--nova/api/rackspace/images.py18
-rw-r--r--nova/api/rackspace/servers.py299
-rw-r--r--nova/api/rackspace/sharedipgroups.py4
14 files changed, 569 insertions, 114 deletions
diff --git a/nova/api/cloud.py b/nova/api/cloud.py
new file mode 100644
index 000000000..345677d4f
--- /dev/null
+++ b/nova/api/cloud.py
@@ -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']}})
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index f0aa57ee4..7a958f841 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -158,12 +158,14 @@ class Authorizer(wsgi.Middleware):
'RunInstances': ['projectmanager', 'sysadmin'],
'TerminateInstances': ['projectmanager', 'sysadmin'],
'RebootInstances': ['projectmanager', 'sysadmin'],
+ 'UpdateInstance': ['projectmanager', 'sysadmin'],
'DeleteVolume': ['projectmanager', 'sysadmin'],
'DescribeImages': ['all'],
'DeregisterImage': ['projectmanager', 'sysadmin'],
'RegisterImage': ['projectmanager', 'sysadmin'],
'DescribeImageAttribute': ['all'],
'ModifyImageAttribute': ['projectmanager', 'sysadmin'],
+ 'UpdateImage': ['projectmanager', 'sysadmin'],
},
'AdminController': {
# All actions have the same permission: ['none'] (the default)
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 6a921c1fe..36646ad08 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -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
@@ -101,9 +102,9 @@ class CloudController(object):
def _get_mpi_data(self, project_id):
result = {}
- for instance in db.instance_get_by_project(None, project_id):
+ for instance in db.instance_get_all_by_project(None, project_id):
if instance['fixed_ip']:
- line = '%s slots=%d' % (instance['fixed_ip']['str_id'],
+ line = '%s slots=%d' % (instance['fixed_ip']['address'],
INSTANCE_TYPES[instance['instance_type']]['vcpus'])
key = str(instance['key_name'])
if key in result:
@@ -143,7 +144,7 @@ class CloudController(object):
},
'hostname': hostname,
'instance-action': 'none',
- 'instance-id': instance_ref['str_id'],
+ 'instance-id': instance_ref['ec2_id'],
'instance-type': instance_ref['instance_type'],
'local-hostname': hostname,
'local-ipv4': address,
@@ -245,7 +246,7 @@ class CloudController(object):
def get_console_output(self, context, instance_id, **kwargs):
# instance_id is passed in as a list of instances
- instance_ref = db.instance_get_by_str(context, instance_id[0])
+ instance_ref = db.instance_get_by_ec2_id(context, instance_id[0])
return rpc.call('%s.%s' % (FLAGS.compute_topic,
instance_ref['host']),
{"method": "get_console_output",
@@ -256,7 +257,7 @@ class CloudController(object):
if context.user.is_admin():
volumes = db.volume_get_all(context)
else:
- volumes = db.volume_get_by_project(context, context.project.id)
+ volumes = db.volume_get_all_by_project(context, context.project.id)
volumes = [self._format_volume(context, v) for v in volumes]
@@ -264,7 +265,7 @@ class CloudController(object):
def _format_volume(self, context, volume):
v = {}
- v['volumeId'] = volume['str_id']
+ v['volumeId'] = volume['ec2_id']
v['status'] = volume['status']
v['size'] = volume['size']
v['availabilityZone'] = volume['availability_zone']
@@ -282,9 +283,12 @@ class CloudController(object):
'device': volume['mountpoint'],
'instanceId': volume['instance_id'],
'status': 'attached',
- 'volume_id': volume['str_id']}]
+ 'volume_id': volume['ec2_id']}]
else:
v['attachmentSet'] = [{}]
+
+ v['display_name'] = volume['display_name']
+ v['display_description'] = volume['display_description']
return v
def create_volume(self, context, size, **kwargs):
@@ -302,6 +306,8 @@ class CloudController(object):
vol['availability_zone'] = FLAGS.storage_availability_zone
vol['status'] = "creating"
vol['attach_status'] = "detached"
+ vol['display_name'] = kwargs.get('display_name')
+ vol['display_description'] = kwargs.get('display_description')
volume_ref = db.volume_create(context, vol)
rpc.cast(FLAGS.scheduler_topic,
@@ -314,13 +320,13 @@ class CloudController(object):
def attach_volume(self, context, volume_id, instance_id, device, **kwargs):
- volume_ref = db.volume_get_by_str(context, volume_id)
+ volume_ref = db.volume_get_by_ec2_id(context, volume_id)
# TODO(vish): abstract status checking?
if volume_ref['status'] != "available":
raise exception.ApiError("Volume status must be available")
if volume_ref['attach_status'] == "attached":
raise exception.ApiError("Volume is already attached")
- instance_ref = db.instance_get_by_str(context, instance_id)
+ instance_ref = db.instance_get_by_ec2_id(context, instance_id)
host = instance_ref['host']
rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "attach_volume",
@@ -336,7 +342,7 @@ class CloudController(object):
'volumeId': volume_ref['id']}
def detach_volume(self, context, volume_id, **kwargs):
- volume_ref = db.volume_get_by_str(context, volume_id)
+ volume_ref = db.volume_get_by_ec2_id(context, volume_id)
instance_ref = db.volume_get_instance(context, volume_ref['id'])
if not instance_ref:
raise exception.ApiError("Volume isn't attached to anything!")
@@ -356,7 +362,7 @@ class CloudController(object):
db.volume_detached(context)
return {'attachTime': volume_ref['attach_time'],
'device': volume_ref['mountpoint'],
- 'instanceId': instance_ref['str_id'],
+ 'instanceId': instance_ref['ec2_id'],
'requestId': context.request_id,
'status': volume_ref['attach_status'],
'volumeId': volume_ref['id']}
@@ -368,6 +374,16 @@ class CloudController(object):
lst = [lst]
return [{label: x} for x in lst]
+ def update_volume(self, context, volume_id, **kwargs):
+ updatable_fields = ['display_name', 'display_description']
+ changes = {}
+ for field in updatable_fields:
+ if field in kwargs:
+ changes[field] = kwargs[field]
+ if changes:
+ db.volume_update(context, volume_id, kwargs)
+ return True
+
def describe_instances(self, context, **kwargs):
return self._format_describe_instances(context)
@@ -382,20 +398,20 @@ class CloudController(object):
def _format_instances(self, context, reservation_id=None):
reservations = {}
if reservation_id:
- instances = db.instance_get_by_reservation(context,
- reservation_id)
+ instances = db.instance_get_all_by_reservation(context,
+ reservation_id)
else:
if context.user.is_admin():
instances = db.instance_get_all(context)
else:
- instances = db.instance_get_by_project(context,
- context.project.id)
+ instances = db.instance_get_all_by_project(context,
+ context.project.id)
for instance in instances:
if not context.user.is_admin():
if instance['image_id'] == FLAGS.vpn_image_id:
continue
i = {}
- i['instanceId'] = instance['str_id']
+ i['instanceId'] = instance['ec2_id']
i['imageId'] = instance['image_id']
i['instanceState'] = {
'code': instance['state'],
@@ -404,10 +420,10 @@ class CloudController(object):
fixed_addr = None
floating_addr = None
if instance['fixed_ip']:
- fixed_addr = instance['fixed_ip']['str_id']
+ fixed_addr = instance['fixed_ip']['address']
if instance['fixed_ip']['floating_ips']:
fixed = instance['fixed_ip']
- floating_addr = fixed['floating_ips'][0]['str_id']
+ floating_addr = fixed['floating_ips'][0]['address']
i['privateDnsName'] = fixed_addr
i['publicDnsName'] = floating_addr
i['dnsName'] = i['publicDnsName'] or i['privateDnsName']
@@ -420,6 +436,8 @@ class CloudController(object):
i['instanceType'] = instance['instance_type']
i['launchTime'] = instance['created_at']
i['amiLaunchIndex'] = instance['launch_index']
+ i['displayName'] = instance['display_name']
+ i['displayDescription'] = instance['display_description']
if not reservations.has_key(instance['reservation_id']):
r = {}
r['reservationId'] = instance['reservation_id']
@@ -439,14 +457,14 @@ class CloudController(object):
if context.user.is_admin():
iterator = db.floating_ip_get_all(context)
else:
- iterator = db.floating_ip_get_by_project(context,
- context.project.id)
+ iterator = db.floating_ip_get_all_by_project(context,
+ context.project.id)
for floating_ip_ref in iterator:
- address = floating_ip_ref['str_id']
+ address = floating_ip_ref['address']
instance_id = None
if (floating_ip_ref['fixed_ip']
and floating_ip_ref['fixed_ip']['instance']):
- instance_id = floating_ip_ref['fixed_ip']['instance']['str_id']
+ instance_id = floating_ip_ref['fixed_ip']['instance']['ec2_id']
address_rv = {'public_ip': address,
'instance_id': instance_id}
if context.user.is_admin():
@@ -477,19 +495,20 @@ class CloudController(object):
rpc.cast(network_topic,
{"method": "deallocate_floating_ip",
"args": {"context": None,
- "floating_address": floating_ip_ref['str_id']}})
+ "floating_address": floating_ip_ref['address']}})
return {'releaseResponse': ["Address released."]}
def associate_address(self, context, instance_id, public_ip, **kwargs):
- instance_ref = db.instance_get_by_str(context, instance_id)
- fixed_ip_ref = db.fixed_ip_get_by_instance(context, instance_ref['id'])
+ instance_ref = db.instance_get_by_ec2_id(context, instance_id)
+ fixed_address = db.instance_get_fixed_address(context,
+ instance_ref['id'])
floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
network_topic = self._get_network_topic(context)
rpc.cast(network_topic,
{"method": "associate_floating_ip",
"args": {"context": None,
- "floating_address": floating_ip_ref['str_id'],
- "fixed_address": fixed_ip_ref['str_id']}})
+ "floating_address": floating_ip_ref['address'],
+ "fixed_address": fixed_address}})
return {'associateResponse': ["Address associated."]}
def disassociate_address(self, context, public_ip, **kwargs):
@@ -498,7 +517,7 @@ class CloudController(object):
rpc.cast(network_topic,
{"method": "disassociate_floating_ip",
"args": {"context": None,
- "floating_address": floating_ip_ref['str_id']}})
+ "floating_address": floating_ip_ref['address']}})
return {'disassociateResponse': ["Address disassociated."]}
def _get_network_topic(self, context):
@@ -576,6 +595,8 @@ class CloudController(object):
base_options['user_data'] = kwargs.get('user_data', '')
base_options['security_group'] = security_group
base_options['instance_type'] = instance_type
+ base_options['display_name'] = kwargs.get('display_name')
+ base_options['display_description'] = kwargs.get('display_description')
type_data = INSTANCE_TYPES[instance_type]
base_options['memory_mb'] = type_data['memory_mb']
@@ -589,7 +610,7 @@ class CloudController(object):
inst = {}
inst['mac_address'] = utils.generate_mac()
inst['launch_index'] = num
- inst['hostname'] = instance_ref['str_id']
+ inst['hostname'] = instance_ref['ec2_id']
db.instance_update(context, inst_id, inst)
address = self.network_manager.allocate_fixed_ip(context,
inst_id,
@@ -618,7 +639,7 @@ class CloudController(object):
for id_str in instance_id:
logging.debug("Going to try and terminate %s" % id_str)
try:
- instance_ref = db.instance_get_by_str(context, id_str)
+ instance_ref = db.instance_get_by_ec2_id(context, id_str)
except exception.NotFound:
logging.warning("Instance %s was not found during terminate",
id_str)
@@ -646,7 +667,7 @@ class CloudController(object):
rpc.cast(network_topic,
{"method": "disassociate_floating_ip",
"args": {"context": None,
- "address": address}})
+ "floating_address": address}})
address = db.instance_get_fixed_address(context,
instance_ref['id'])
@@ -670,17 +691,24 @@ 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_str(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):
+ updatable_fields = ['display_name', 'display_description']
+ changes = {}
+ for field in updatable_fields:
+ if field in kwargs:
+ changes[field] = kwargs[field]
+ if changes:
+ db_context = {}
+ inst = db.instance_get_by_ec2_id(db_context, instance_id)
+ db.instance_update(db_context, inst['id'], kwargs)
return True
def delete_volume(self, context, volume_id, **kwargs):
# TODO: return error if not authorized
- volume_ref = db.volume_get_by_str(context, volume_id)
+ volume_ref = db.volume_get_by_ec2_id(context, volume_id)
if volume_ref['status'] != "available":
raise exception.ApiError("Volume status must be available")
now = datetime.datetime.utcnow()
@@ -734,3 +762,7 @@ class CloudController(object):
if not operation_type in ['add', 'remove']:
raise exception.ApiError('operation_type must be add or remove')
return images.modify(context, image_id, operation_type)
+
+ def update_image(self, context, image_id, **kwargs):
+ result = images.update(context, image_id, dict(kwargs))
+ return result
diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py
index 4579cd81a..cb54cdda2 100644
--- a/nova/api/ec2/images.py
+++ b/nova/api/ec2/images.py
@@ -43,6 +43,14 @@ def modify(context, image_id, operation):
return True
+def update(context, image_id, attributes):
+ """update an image's attributes / info.json"""
+ attributes.update({"image_id": image_id})
+ conn(context).make_request(
+ method='POST',
+ bucket='_images',
+ query_args=qs(attributes))
+ return True
def register(context, image_location):
""" rpc call to register a new image based from a manifest """
diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py
index c24d08585..89a4693ad 100644
--- a/nova/api/rackspace/__init__.py
+++ b/nova/api/rackspace/__init__.py
@@ -31,6 +31,8 @@ import webob
from nova import flags
from nova import utils
from nova import wsgi
+from nova.api.rackspace import faults
+from nova.api.rackspace import backup_schedules
from nova.api.rackspace import flavors
from nova.api.rackspace import images
from nova.api.rackspace import ratelimiting
@@ -66,9 +68,11 @@ class AuthMiddleware(wsgi.Middleware):
user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"])
if not user:
- return webob.exc.HTTPUnauthorized()
- context = {'user': user}
- req.environ['nova.context'] = context
+ return faults.Fault(webob.exc.HTTPUnauthorized())
+
+ if not req.environ.has_key('nova.context'):
+ req.environ['nova.context'] = {}
+ req.environ['nova.context']['user'] = user
return self.application
class RateLimitingMiddleware(wsgi.Middleware):
@@ -109,8 +113,10 @@ class RateLimitingMiddleware(wsgi.Middleware):
delay = self.get_delay(action_name, username)
if delay:
# TODO(gundlach): Get the retry-after format correct.
- raise webob.exc.HTTPRequestEntityTooLarge(headers={
- 'Retry-After': time.time() + delay})
+ exc = webob.exc.HTTPRequestEntityTooLarge(
+ explanation='Too many requests.',
+ headers={'Retry-After': time.time() + delay})
+ raise faults.Fault(exc)
return self.application
def get_delay(self, action_name, username):
@@ -145,11 +151,40 @@ class APIRouter(wsgi.Router):
def __init__(self):
mapper = routes.Mapper()
- mapper.resource("server", "servers", controller=servers.Controller())
+ mapper.resource("server", "servers", controller=servers.Controller(),
+ collection={ 'detail': 'GET'},
+ member={'action':'POST'})
+
+ mapper.resource("backup_schedule", "backup_schedules",
+ controller=backup_schedules.Controller(),
+ parent_resource=dict(member_name='server',
+ collection_name = 'servers'))
+
mapper.resource("image", "images", controller=images.Controller(),
collection={'detail': 'GET'})
mapper.resource("flavor", "flavors", controller=flavors.Controller(),
collection={'detail': 'GET'})
mapper.resource("sharedipgroup", "sharedipgroups",
controller=sharedipgroups.Controller())
+
super(APIRouter, self).__init__(mapper)
+
+
+def limited(items, req):
+ """Return a slice of items according to requested offset and limit.
+
+ items - a sliceable
+ req - wobob.Request possibly containing offset and limit GET variables.
+ offset is where to start in the list, and limit is the maximum number
+ of items to return.
+
+ If limit is not specified, 0, or > 1000, defaults to 1000.
+ """
+ offset = int(req.GET.get('offset', 0))
+ limit = int(req.GET.get('limit', 0))
+ if not limit:
+ limit = 1000
+ limit = min(1000, limit)
+ range_end = offset + limit
+ return items[offset:range_end]
+
diff --git a/nova/api/rackspace/_id_translator.py b/nova/api/rackspace/_id_translator.py
index aec5fb6a5..333aa8434 100644
--- a/nova/api/rackspace/_id_translator.py
+++ b/nova/api/rackspace/_id_translator.py
@@ -37,6 +37,6 @@ class RackspaceAPIIdTranslator(object):
# every int id be used.)
return int(self._store.hget(self._fwd_key, str(opaque_id)))
- def from_rs_id(self, strategy_name, rs_id):
+ def from_rs_id(self, rs_id):
"""Convert a Rackspace id to a strategy-specific one."""
return self._store.hget(self._rev_key, rs_id)
diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py
index ce5a967eb..c45156ebd 100644
--- a/nova/api/rackspace/auth.py
+++ b/nova/api/rackspace/auth.py
@@ -1,14 +1,17 @@
import datetime
+import hashlib
import json
import time
+
import webob.exc
import webob.dec
-import hashlib
-from nova import flags
+
from nova import auth
-from nova import manager
from nova import db
+from nova import flags
+from nova import manager
from nova import utils
+from nova.api.rackspace import faults
FLAGS = flags.FLAGS
@@ -34,13 +37,13 @@ class BasicApiAuthManager(object):
# honor it
path_info = req.path_info
if len(path_info) > 1:
- return webob.exc.HTTPUnauthorized()
+ return faults.Fault(webob.exc.HTTPUnauthorized())
try:
username, key = req.headers['X-Auth-User'], \
req.headers['X-Auth-Key']
except KeyError:
- return webob.exc.HTTPUnauthorized()
+ return faults.Fault(webob.exc.HTTPUnauthorized())
username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key']
token, user = self._authorize_user(username, key)
@@ -55,7 +58,7 @@ class BasicApiAuthManager(object):
res.status = '204'
return res
else:
- return webob.exc.HTTPUnauthorized()
+ return faults.Fault(webob.exc.HTTPUnauthorized())
def authorize_token(self, token_hash):
""" retrieves user information from the datastore given a token
diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/rackspace/backup_schedules.py
new file mode 100644
index 000000000..cb83023bc
--- /dev/null
+++ b/nova/api/rackspace/backup_schedules.py
@@ -0,0 +1,39 @@
+# 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.
+
+import time
+from webob import exc
+
+from nova import wsgi
+from nova.api.rackspace import _id_translator
+from nova.api.rackspace import faults
+import nova.image.service
+
+class Controller(wsgi.Controller):
+ def __init__(self):
+ pass
+
+ def index(self, req, server_id):
+ return faults.Fault(exc.HTTPNotFound())
+
+ def create(self, req, server_id):
+ """ No actual update method required, since the existing API allows
+ both create and update through a POST """
+ return faults.Fault(exc.HTTPNotFound())
+
+ def delete(self, req, server_id):
+ return faults.Fault(exc.HTTPNotFound())
diff --git a/nova/api/rackspace/base.py b/nova/api/rackspace/context.py
index dd2c6543c..77394615b 100644
--- a/nova/api/rackspace/base.py
+++ b/nova/api/rackspace/context.py
@@ -15,16 +15,19 @@
# License for the specific language governing permissions and limitations
# under the License.
-from nova import wsgi
+"""
+APIRequestContext
+"""
+import random
-class Controller(wsgi.Controller):
- """TODO(eday): Base controller for all rackspace controllers. What is this
- for? Is this just Rackspace specific? """
-
- @classmethod
- def render(cls, instance):
- if isinstance(instance, list):
- return {cls.entity_name: cls.render(instance)}
- else:
- return {"TODO": "TODO"}
+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)
diff --git a/nova/api/rackspace/faults.py b/nova/api/rackspace/faults.py
new file mode 100644
index 000000000..32e5c866f
--- /dev/null
+++ b/nova/api/rackspace/faults.py
@@ -0,0 +1,62 @@
+# 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.
+
+
+import webob.dec
+import webob.exc
+
+from nova import wsgi
+
+
+class Fault(webob.exc.HTTPException):
+
+ """An RS API fault response."""
+
+ _fault_names = {
+ 400: "badRequest",
+ 401: "unauthorized",
+ 403: "resizeNotAllowed",
+ 404: "itemNotFound",
+ 405: "badMethod",
+ 409: "inProgress",
+ 413: "overLimit",
+ 415: "badMediaType",
+ 501: "notImplemented",
+ 503: "serviceUnavailable"}
+
+ def __init__(self, exception):
+ """Create a Fault for the given webob.exc.exception."""
+ self.wrapped_exc = exception
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ """Generate a WSGI response based on the exception passed to ctor."""
+ # Replace the body with fault details.
+ code = self.wrapped_exc.status_int
+ fault_name = self._fault_names.get(code, "cloudServersFault")
+ fault_data = {
+ fault_name: {
+ 'code': code,
+ 'message': self.wrapped_exc.explanation}}
+ if code == 413:
+ retry = self.wrapped_exc.headers['Retry-After']
+ fault_data[fault_name]['retryAfter'] = retry
+ # 'code' is an attribute on the fault tag itself
+ metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
+ serializer = wsgi.Serializer(req.environ, metadata)
+ self.wrapped_exc.body = serializer.to_content_type(fault_data)
+ return self.wrapped_exc
diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py
index 60b35c939..916449854 100644
--- a/nova/api/rackspace/flavors.py
+++ b/nova/api/rackspace/flavors.py
@@ -15,11 +15,14 @@
# License for the specific language governing permissions and limitations
# under the License.
-from nova.api.rackspace import base
-from nova.compute import instance_types
from webob import exc
-class Controller(base.Controller):
+from nova.api.rackspace import faults
+from nova.compute import instance_types
+from nova import wsgi
+import nova.api.rackspace
+
+class Controller(wsgi.Controller):
"""Flavor controller for the Rackspace API."""
_serialization_metadata = {
@@ -38,6 +41,7 @@ class Controller(base.Controller):
def detail(self, req):
"""Return all flavors in detail."""
items = [self.show(req, id)['flavor'] for id in self._all_ids()]
+ items = nova.api.rackspace.limited(items, req)
return dict(flavors=items)
def show(self, req, id):
@@ -47,7 +51,7 @@ class Controller(base.Controller):
item = dict(ram=val['memory_mb'], disk=val['local_gb'],
id=val['flavorid'], name=name)
return dict(flavor=item)
- raise exc.HTTPNotFound()
+ raise faults.Fault(exc.HTTPNotFound())
def _all_ids(self):
"""Return the list of all flavorids."""
diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py
index 2f3e928b9..4a7dd489c 100644
--- a/nova/api/rackspace/images.py
+++ b/nova/api/rackspace/images.py
@@ -15,12 +15,15 @@
# License for the specific language governing permissions and limitations
# under the License.
-import nova.image.service
-from nova.api.rackspace import base
-from nova.api.rackspace import _id_translator
from webob import exc
-class Controller(base.Controller):
+from nova import wsgi
+from nova.api.rackspace import _id_translator
+import nova.api.rackspace
+import nova.image.service
+from nova.api.rackspace import faults
+
+class Controller(wsgi.Controller):
_serialization_metadata = {
'application/xml': {
@@ -44,6 +47,7 @@ class Controller(base.Controller):
def detail(self, req):
"""Return all public images in detail."""
data = self._service.index()
+ data = nova.api.rackspace.limited(data, req)
for img in data:
img['id'] = self._id_translator.to_rs_id(img['id'])
return dict(images=data)
@@ -57,14 +61,14 @@ class Controller(base.Controller):
def delete(self, req, id):
# Only public images are supported for now.
- raise exc.HTTPNotFound()
+ raise faults.Fault(exc.HTTPNotFound())
def create(self, req):
# Only public images are supported for now, so a request to
# make a backup of a server cannot be supproted.
- raise exc.HTTPNotFound()
+ raise faults.Fault(exc.HTTPNotFound())
def update(self, req, id):
# Users may not modify public images, and that's all that
# we support for now.
- raise exc.HTTPNotFound()
+ raise faults.Fault(exc.HTTPNotFound())
diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py
index 1815f7523..11efd8aef 100644
--- a/nova/api/rackspace/servers.py
+++ b/nova/api/rackspace/servers.py
@@ -14,67 +14,286 @@
# 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 time
-from nova import db
+import webob
+from webob import exc
+
from nova import flags
from nova import rpc
from nova import utils
-from nova.api.rackspace import base
+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
-class Controller(base.Controller):
- entity_name = 'servers'
+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 _image_service():
+ """ Helper method for initializing the image id translator """
+ service = nova.image.service.ImageService.load()
+ return (service, _id_translator.RackspaceAPIIdTranslator(
+ "image", service.__class__.__name__))
+
+def _filter_params(inst_dict):
+ """ Extracts all updatable parameters for a server update request """
+ keys = dict(name='name', admin_pass='adminPass')
+ new_attrs = {}
+ 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):
+ """ Coerces a list of servers into proper dictionary format """
+ return dict(servers=entities)
+
+def _entity_detail(inst):
+ """ Maps everything to Rackspace-like attributes for return"""
+ power_mapping = {
+ power_state.NOSTATE: 'build',
+ power_state.RUNNING: 'active',
+ power_state.BLOCKED: 'active',
+ power_state.PAUSED: 'suspended',
+ power_state.SHUTDOWN: 'active',
+ power_state.SHUTOFF: 'active',
+ power_state.CRASHED: 'error'
+ }
+ inst_dict = {}
+
+ mapped_keys = dict(status='state', imageId='image_id',
+ flavorId='instance_type', name='server_name', id='id')
+
+ for k, v in mapped_keys.iteritems():
+ inst_dict[k] = inst[v]
+
+ inst_dict['status'] = power_mapping[inst_dict['status']]
+ inst_dict['addresses'] = dict(public=[], private=[])
+ inst_dict['metadata'] = {}
+ inst_dict['hostId'] = ''
+
+ return dict(server=inst_dict)
+
+def _entity_inst(inst):
+ """ Filters all model attributes save for id and name """
+ return dict(server=dict(id=inst['id'], name=inst['server_name']))
- def index(self, **kwargs):
- instances = []
- for inst in db.instance_get_all(None):
- instances.append(instance_details(inst))
+class Controller(wsgi.Controller):
+ """ The Server API controller for the Openstack API """
- def show(self, **kwargs):
- instance_id = kwargs['id']
- return db.instance_get(None, instance_id)
+ _serialization_metadata = {
+ 'application/xml': {
+ "attributes": {
+ "server": [ "id", "imageId", "name", "flavorId", "hostId",
+ "status", "progress", "progress" ]
+ }
+ }
+ }
- def delete(self, **kwargs):
- instance_id = kwargs['id']
- instance = db.instance_get(None, instance_id)
- if not instance:
- raise ServerNotFound("The requested server was not found")
- instance.destroy()
- return True
+ def __init__(self, db_driver=None):
+ if not db_driver:
+ db_driver = FLAGS.db_driver
+ self.db_driver = utils.import_object(db_driver)
+ super(Controller, self).__init__()
+
+ def index(self, req):
+ """ Returns a list of server names and ids for a given user """
+ return self._items(req, entity_maker=_entity_inst)
+
+ def detail(self, req):
+ """ Returns a list of server details for a given user """
+ return self._items(req, entity_maker=_entity_detail)
+
+ def _items(self, req, entity_maker):
+ """Returns a list of servers for a given user.
+
+ entity_maker - either _entity_detail or _entity_inst
+ """
+ user_id = req.environ['nova.context']['user']['id']
+ instance_list = self.db_driver.instance_get_all_by_user(None, user_id)
+ limited_list = nova.api.rackspace.limited(instance_list, req)
+ res = [entity_maker(inst)['server'] for inst in limited_list]
+ return _entity_list(res)
+
+ 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_by_ec2_id(None, inst_id)
+ if inst:
+ if inst.user_id == user_id:
+ return _entity_detail(inst)
+ raise faults.Fault(exc.HTTPNotFound())
+
+ 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_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())
+ return faults.Fault(exc.HTTPNotFound())
+
+ def create(self, req):
+ """ Creates a new server for a given user """
+
+ env = self._deserialize(req.body, req)
+ if not env:
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+
+ try:
+ inst = self._build_server_instance(req, env)
+ except Exception, e:
+ return faults.Fault(exc.HTTPUnprocessableEntity())
- def create(self, **kwargs):
- inst = self.build_server_instance(kwargs['server'])
rpc.cast(
FLAGS.compute_topic, {
"method": "run_instance",
"args": {"instance_id": inst['id']}})
+ return _entity_inst(inst)
+
+ def update(self, req, id):
+ """ Updates the server name or password """
+ inst_id_trans = _instance_id_translator()
+ inst_id = inst_id_trans.from_rs_id(id)
+ user_id = req.environ['nova.context']['user']['id']
- def update(self, **kwargs):
- instance_id = kwargs['id']
- instance = db.instance_get(None, instance_id)
- if not instance:
- raise ServerNotFound("The requested server was not found")
- instance.update(kwargs['server'])
- instance.save()
+ inst_dict = self._deserialize(req.body, req)
+
+ if not inst_dict:
+ return faults.Fault(exc.HTTPUnprocessableEntity())
- def build_server_instance(self, env):
+ 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())
+
+ 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 """
+ 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, env):
"""Build instance data structure and save it to the data store."""
- reservation = utils.generate_uid('r')
ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
inst = {}
- inst['name'] = env['server']['name']
- inst['image_id'] = env['server']['imageId']
- inst['instance_type'] = env['server']['flavorId']
- inst['user_id'] = env['user']['id']
- inst['project_id'] = env['project']['id']
- inst['reservation_id'] = reservation
+
+ inst_id_trans = _instance_id_translator()
+
+ user_id = req.environ['nova.context']['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_id = db.instance_create(None, inst)['id']
- address = self.network_manager.allocate_fixed_ip(None, inst_id)
- # key_data, key_name, ami_launch_index
- # TODO(todd): key data or root password
- inst.save()
+ inst['project_id'] = user_id
+
+ 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')
+
+ inst['display_name'] = env['server']['name']
+ inst['display_description'] = env['server']['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'] = 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)
diff --git a/nova/api/rackspace/sharedipgroups.py b/nova/api/rackspace/sharedipgroups.py
index 986f11434..4d2d0ede1 100644
--- a/nova/api/rackspace/sharedipgroups.py
+++ b/nova/api/rackspace/sharedipgroups.py
@@ -15,4 +15,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-class Controller(object): pass
+from nova import wsgi
+
+class Controller(wsgi.Controller): pass