summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVishvananda Ishaya <vishvananda@yahoo.com>2010-09-30 18:13:45 -0700
committerVishvananda Ishaya <vishvananda@yahoo.com>2010-09-30 18:13:45 -0700
commit1dda065c53cbe11a34e7ae60e11e30dfaf6bf7ac (patch)
treed04a4ac2195d45e9c5f357039cab7d1c0cf40c3d
parent5d30881d721bdc9e26f71fd2130bd44e7edb1d4d (diff)
parent30747bba76ddc2b51b5b0bf564557e86a5d634c3 (diff)
downloadnova-1dda065c53cbe11a34e7ae60e11e30dfaf6bf7ac.tar.gz
nova-1dda065c53cbe11a34e7ae60e11e30dfaf6bf7ac.tar.xz
nova-1dda065c53cbe11a34e7ae60e11e30dfaf6bf7ac.zip
merged trunk
-rwxr-xr-xbin/nova-manage14
-rw-r--r--nova/api/cloud.py42
-rw-r--r--nova/api/ec2/__init__.py2
-rw-r--r--nova/api/ec2/cloud.py43
-rw-r--r--nova/api/ec2/images.py8
-rw-r--r--nova/api/rackspace/__init__.py29
-rw-r--r--nova/api/rackspace/auth.py7
-rw-r--r--nova/api/rackspace/backup_schedules.py7
-rw-r--r--nova/api/rackspace/context.py33
-rw-r--r--nova/api/rackspace/faults.py62
-rw-r--r--nova/api/rackspace/flavors.py8
-rw-r--r--nova/api/rackspace/images.py9
-rw-r--r--nova/api/rackspace/servers.py204
-rw-r--r--nova/auth/ldapdriver.py16
-rw-r--r--nova/auth/manager.py6
-rw-r--r--nova/db/api.py9
-rw-r--r--nova/db/sqlalchemy/api.py9
-rw-r--r--nova/db/sqlalchemy/models.py13
-rw-r--r--nova/flags.py2
-rw-r--r--nova/manager.py7
-rw-r--r--nova/network/linux_net.py68
-rw-r--r--nova/network/manager.py7
-rw-r--r--nova/objectstore/__init__.py2
-rw-r--r--nova/objectstore/handler.py21
-rw-r--r--nova/objectstore/image.py10
-rw-r--r--nova/service.py1
-rw-r--r--nova/tests/api/rackspace/__init__.py29
-rw-r--r--nova/tests/api/rackspace/auth.py8
-rw-r--r--nova/tests/api/rackspace/flavors.py1
-rw-r--r--nova/tests/api/rackspace/images.py1
-rw-r--r--nova/tests/api/rackspace/servers.py101
-rw-r--r--nova/tests/api/rackspace/sharedipgroups.py1
-rw-r--r--nova/tests/api/rackspace/test_helper.py15
-rw-r--r--nova/tests/api/rackspace/testfaults.py40
-rw-r--r--nova/tests/auth_unittest.py410
-rw-r--r--nova/tests/cloud_unittest.py76
-rw-r--r--nova/tests/objectstore_unittest.py6
-rw-r--r--nova/virt/xenapi.py4
-rw-r--r--nova/wsgi.py20
-rwxr-xr-xtools/setup_iptables.sh158
40 files changed, 1223 insertions, 286 deletions
diff --git a/bin/nova-manage b/bin/nova-manage
index fa6a49f36..bf3c67612 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -266,6 +266,18 @@ class UserCommands(object):
for user in self.manager.get_users():
print user.name
+ def modify(self, name, access_key, secret_key, is_admin):
+ """update a users keys & admin flag
+ arguments: accesskey secretkey admin
+ leave any field blank to ignore it, admin should be 'T', 'F', or blank
+ """
+ if not is_admin:
+ is_admin = None
+ elif is_admin.upper()[0] == 'T':
+ is_admin = True
+ else:
+ is_admin = False
+ self.manager.modify_user(name, access_key, secret_key, is_admin)
class ProjectCommands(object):
"""Class for managing projects."""
@@ -291,7 +303,7 @@ class ProjectCommands(object):
def environment(self, project_id, user_id, filename='novarc'):
"""Exports environment variables to an sourcable file
arguments: project_id user_id [filename='novarc]"""
- rc = self.manager.get_environment_rc(project_id, user_id)
+ rc = self.manager.get_environment_rc(user_id, project_id)
with open(filename, 'w') as f:
f.write(rc)
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 f3bd4f9d9..79c95788b 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
@@ -285,6 +286,9 @@ class CloudController(object):
'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,
@@ -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)
@@ -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']
@@ -577,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']
@@ -665,12 +685,19 @@ 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):
+ 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):
@@ -728,3 +755,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 98802663f..89a4693ad 100644
--- a/nova/api/rackspace/__init__.py
+++ b/nova/api/rackspace/__init__.py
@@ -31,6 +31,7 @@ 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
@@ -67,7 +68,7 @@ class AuthMiddleware(wsgi.Middleware):
user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"])
if not user:
- return webob.exc.HTTPUnauthorized()
+ return faults.Fault(webob.exc.HTTPUnauthorized())
if not req.environ.has_key('nova.context'):
req.environ['nova.context'] = {}
@@ -112,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):
@@ -165,3 +168,23 @@ class APIRouter(wsgi.Router):
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/auth.py b/nova/api/rackspace/auth.py
index 8bfb0753e..c45156ebd 100644
--- a/nova/api/rackspace/auth.py
+++ b/nova/api/rackspace/auth.py
@@ -11,6 +11,7 @@ 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
@@ -36,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)
@@ -57,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
index 46da778ee..cb83023bc 100644
--- a/nova/api/rackspace/backup_schedules.py
+++ b/nova/api/rackspace/backup_schedules.py
@@ -20,6 +20,7 @@ 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):
@@ -27,12 +28,12 @@ class Controller(wsgi.Controller):
pass
def index(self, req, server_id):
- return exc.HTTPNotFound()
+ 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 exc.HTTPNotFound()
+ return faults.Fault(exc.HTTPNotFound())
def delete(self, req, server_id):
- return exc.HTTPNotFound()
+ return faults.Fault(exc.HTTPNotFound())
diff --git a/nova/api/rackspace/context.py b/nova/api/rackspace/context.py
new file mode 100644
index 000000000..77394615b
--- /dev/null
+++ b/nova/api/rackspace/context.py
@@ -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)
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 3bcf170e5..916449854 100644
--- a/nova/api/rackspace/flavors.py
+++ b/nova/api/rackspace/flavors.py
@@ -15,9 +15,12 @@
# License for the specific language governing permissions and limitations
# under the License.
+from webob import exc
+
+from nova.api.rackspace import faults
from nova.compute import instance_types
from nova import wsgi
-from webob import exc
+import nova.api.rackspace
class Controller(wsgi.Controller):
"""Flavor controller for the Rackspace API."""
@@ -38,6 +41,7 @@ class Controller(wsgi.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(wsgi.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 11b058dec..4a7dd489c 100644
--- a/nova/api/rackspace/images.py
+++ b/nova/api/rackspace/images.py
@@ -19,7 +19,9 @@ from webob import exc
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):
@@ -45,6 +47,7 @@ class Controller(wsgi.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)
@@ -58,14 +61,14 @@ class Controller(wsgi.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 4ab04bde7..11efd8aef 100644
--- a/nova/api/rackspace/servers.py
+++ b/nova/api/rackspace/servers.py
@@ -17,33 +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):
@@ -82,7 +94,6 @@ def _entity_inst(inst):
class Controller(wsgi.Controller):
""" The Server API controller for the Openstack API """
-
_serialization_metadata = {
'application/xml': {
@@ -101,42 +112,58 @@ class Controller(wsgi.Controller):
def index(self, req):
""" Returns a list of server names and ids for a given user """
- user_id = req.environ['nova.context']['user']['id']
- instance_list = self.db_driver.instance_get_all_by_user(None, user_id)
- res = [_entity_inst(inst)['server'] for inst in instance_list]
- return _entity_list(res)
+ 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']
- res = [_entity_detail(inst)['server'] for inst in
- self.db_driver.instance_get_all_by_user(None, 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(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)
- raise exc.HTTPNotFound()
+ 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(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 exc.HTTPAccepted()
- return exc.HTTPNotFound()
+ return faults.Fault(exc.HTTPAccepted())
+ return faults.Fault(exc.HTTPNotFound())
def create(self, req):
""" Creates a new server for a given user """
- if not req.environ.has_key('inst_dict'):
- return exc.HTTPUnprocessableEntity()
- inst = self._build_server_instance(req)
+ 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())
rpc.cast(
FLAGS.compute_topic, {
@@ -146,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'):
- return exc.HTTPUnprocessableEntity()
+ 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)
- if not instance:
- return exc.HTTPNotFound()
+ inst_dict = self._deserialize(req.body, req)
+
+ if not inst_dict:
+ return faults.Fault(exc.HTTPUnprocessableEntity())
- attrs = req.environ['nova.context'].get('model_attributes', None)
- if attrs:
- self.db_driver.instance_update(None, id, _filter_params(attrs))
- return exc.HTTPNoContent()
+ 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 """
- if not req.environ.has_key('inst_dict'):
- return exc.HTTPUnprocessableEntity()
-
- def _build_server_instance(self, req):
+ 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."""
ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
inst = {}
- env = req.environ['inst_dict']
+ 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']
- opaque_id = translator_instance().from_rs_id(image_id)
+
+ img_service, image_id_trans = _image_service()
- inst['name'] = env['server']['server_name']
- inst['image_id'] = opaque_id
- inst['instance_type'] = env['server']['flavorId']
+ opaque_image_id = image_id_trans.to_rs_id(image_id)
+ image = img_service.show(opaque_image_id)
- user_id = req.environ['nova.context']['user']['id']
- inst['user_id'] = user_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)
diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py
index 021851ebf..640ea169e 100644
--- a/nova/auth/ldapdriver.py
+++ b/nova/auth/ldapdriver.py
@@ -256,8 +256,7 @@ class LdapDriver(object):
if not self.__user_exists(uid):
raise exception.NotFound("User %s doesn't exist" % uid)
self.__remove_from_all(uid)
- self.conn.delete_s('uid=%s,%s' % (uid,
- FLAGS.ldap_user_subtree))
+ self.conn.delete_s(self.__uid_to_dn(uid))
def delete_project(self, project_id):
"""Delete a project"""
@@ -265,6 +264,19 @@ class LdapDriver(object):
self.__delete_roles(project_dn)
self.__delete_group(project_dn)
+ def modify_user(self, uid, access_key=None, secret_key=None, admin=None):
+ """Modify an existing project"""
+ if not access_key and not secret_key and admin is None:
+ return
+ attr = []
+ if access_key:
+ attr.append((self.ldap.MOD_REPLACE, 'accessKey', access_key))
+ if secret_key:
+ attr.append((self.ldap.MOD_REPLACE, 'secretKey', secret_key))
+ if admin is not None:
+ attr.append((self.ldap.MOD_REPLACE, 'isAdmin', str(admin).upper()))
+ self.conn.modify_s(self.__uid_to_dn(uid), attr)
+
def __user_exists(self, uid):
"""Check if user exists"""
return self.get_user(uid) != None
diff --git a/nova/auth/manager.py b/nova/auth/manager.py
index 55fbf42aa..0bc12c80f 100644
--- a/nova/auth/manager.py
+++ b/nova/auth/manager.py
@@ -630,6 +630,12 @@ class AuthManager(object):
with self.driver() as drv:
drv.delete_user(uid)
+ def modify_user(self, user, access_key=None, secret_key=None, admin=None):
+ """Modify credentials for a user"""
+ uid = User.safe_id(user)
+ with self.driver() as drv:
+ drv.modify_user(uid, access_key, secret_key, admin)
+
def get_credentials(self, user, project=None):
"""Get credential zip for user in project"""
if not isinstance(user, User):
diff --git a/nova/db/api.py b/nova/db/api.py
index 669a4ecff..4aea0e6a4 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -406,9 +406,12 @@ def network_index_count(context):
return IMPL.network_index_count(context)
-def network_index_create(context, values):
- """Create a network index from the values dict"""
- return IMPL.network_index_create(context, values)
+def network_index_create_safe(context, values):
+ """Create a network index from the values dict
+
+ The index is not returned. If the create violates the unique
+ constraints because the index already exists, no exception is raised."""
+ return IMPL.network_index_create_safe(context, values)
def network_set_cidr(context, network_id, cidr):
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 25da37082..4aa3c693d 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -26,7 +26,9 @@ from nova import utils
from nova.db.sqlalchemy import models
from nova.db.sqlalchemy.session import get_session
from sqlalchemy import or_
+from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload_all
+from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.sql import exists, func
FLAGS = flags.FLAGS
@@ -671,11 +673,14 @@ def network_index_count(_context):
return models.NetworkIndex.count()
-def network_index_create(_context, values):
+def network_index_create_safe(_context, values):
network_index_ref = models.NetworkIndex()
for (key, value) in values.iteritems():
network_index_ref[key] = value
- network_index_ref.save()
+ try:
+ network_index_ref.save()
+ except IntegrityError:
+ pass
def network_set_host(_context, network_id, host_id):
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 3d2460c39..6cb377476 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -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))
@@ -239,7 +241,6 @@ class Instance(BASE, NovaBase):
vcpus = Column(Integer)
local_gb = Column(Integer)
-
hostname = Column(String(255))
host = Column(String(255)) # , ForeignKey('hosts.id'))
@@ -253,6 +254,10 @@ class Instance(BASE, NovaBase):
scheduled_at = Column(DateTime)
launched_at = Column(DateTime)
terminated_at = Column(DateTime)
+
+ display_name = Column(String(255))
+ display_description = Column(String(255))
+
# TODO(vish): see Ewan's email about state improvements, probably
# should be in a driver base class or some such
# vmstate_state = running, halted, suspended, paused
@@ -289,6 +294,10 @@ class Volume(BASE, NovaBase):
launched_at = Column(DateTime)
terminated_at = Column(DateTime)
+ display_name = Column(String(255))
+ display_description = Column(String(255))
+
+
class Quota(BASE, NovaBase):
"""Represents quota overrides for a project"""
__tablename__ = 'quotas'
@@ -398,7 +407,7 @@ class NetworkIndex(BASE, NovaBase):
"""
__tablename__ = 'network_indexes'
id = Column(Integer, primary_key=True)
- index = Column(Integer)
+ index = Column(Integer, unique=True)
network_id = Column(Integer, ForeignKey('networks.id'), nullable=True)
network = relationship(Network, backref=backref('network_index',
uselist=False))
diff --git a/nova/flags.py b/nova/flags.py
index 6a1c14490..c32cdd7a4 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -188,6 +188,8 @@ DEFINE_string('rabbit_userid', 'guest', 'rabbit userid')
DEFINE_string('rabbit_password', 'guest', 'rabbit password')
DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host')
DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to')
+DEFINE_string('cc_host', '127.0.0.1', 'ip of api server')
+DEFINE_integer('cc_port', 8773, 'cloud controller port')
DEFINE_string('ec2_url', 'http://127.0.0.1:8773/services/Cloud',
'Url to ec2 api server')
diff --git a/nova/manager.py b/nova/manager.py
index 0694907bd..56ba7d3f6 100644
--- a/nova/manager.py
+++ b/nova/manager.py
@@ -43,3 +43,10 @@ class Manager(object):
def periodic_tasks(self, context=None):
"""Tasks to be run at a periodic interval"""
yield
+
+ def init_host(self):
+ """Do any initialization that needs to be run if this is a standalone service.
+
+ Child classes should override this method.
+ """
+ pass
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index b5346218a..709195ba4 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -36,13 +36,34 @@ flags.DEFINE_string('dhcpbridge_flagfile',
flags.DEFINE_string('networks_path', utils.abspath('../networks'),
'Location to keep network config files')
flags.DEFINE_string('public_interface', 'vlan1',
- 'Interface for public IP addresses')
+ 'Interface for public IP addresses')
flags.DEFINE_string('bridge_dev', 'eth0',
- 'network device for bridges')
-
+ 'network device for bridges')
+flags.DEFINE_string('routing_source_ip', '127.0.0.1',
+ 'Public IP of network host')
+flags.DEFINE_bool('use_nova_chains', False,
+ 'use the nova_ routing chains instead of default')
DEFAULT_PORTS = [("tcp", 80), ("tcp", 22), ("udp", 1194), ("tcp", 443)]
+def init_host():
+ """Basic networking setup goes here"""
+ # NOTE(devcamcar): Cloud public DNAT entries, CloudPipe port
+ # forwarding entries and a default DNAT entry.
+ _confirm_rule("PREROUTING", "-t nat -s 0.0.0.0/0 "
+ "-d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT "
+ "--to-destination %s:%s" % (FLAGS.cc_host, FLAGS.cc_port))
+
+ # NOTE(devcamcar): Cloud public SNAT entries and the default
+ # SNAT rule for outbound traffic.
+ _confirm_rule("POSTROUTING", "-t nat -s %s "
+ "-j SNAT --to-source %s"
+ % (FLAGS.private_range, FLAGS.routing_source_ip))
+
+ _confirm_rule("POSTROUTING", "-t nat -s %s -j MASQUERADE" %
+ FLAGS.private_range)
+ _confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s -j ACCEPT" %
+ {'range': FLAGS.private_range})
def bind_floating_ip(floating_ip):
"""Bind ip to public interface"""
@@ -58,37 +79,37 @@ def unbind_floating_ip(floating_ip):
def ensure_vlan_forward(public_ip, port, private_ip):
"""Sets up forwarding rules for vlan"""
- _confirm_rule("FORWARD -d %s -p udp --dport 1194 -j ACCEPT" % private_ip)
- _confirm_rule(
- "PREROUTING -t nat -d %s -p udp --dport %s -j DNAT --to %s:1194"
+ _confirm_rule("FORWARD", "-d %s -p udp --dport 1194 -j ACCEPT" %
+ private_ip)
+ _confirm_rule("PREROUTING",
+ "-t nat -d %s -p udp --dport %s -j DNAT --to %s:1194"
% (public_ip, port, private_ip))
def ensure_floating_forward(floating_ip, fixed_ip):
"""Ensure floating ip forwarding rule"""
- _confirm_rule("PREROUTING -t nat -d %s -j DNAT --to %s"
+ _confirm_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
% (floating_ip, fixed_ip))
- _confirm_rule("POSTROUTING -t nat -s %s -j SNAT --to %s"
+ _confirm_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s"
% (fixed_ip, floating_ip))
# TODO(joshua): Get these from the secgroup datastore entries
- _confirm_rule("FORWARD -d %s -p icmp -j ACCEPT"
+ _confirm_rule("FORWARD", "-d %s -p icmp -j ACCEPT"
% (fixed_ip))
for (protocol, port) in DEFAULT_PORTS:
- _confirm_rule(
- "FORWARD -d %s -p %s --dport %s -j ACCEPT"
+ _confirm_rule("FORWARD","-d %s -p %s --dport %s -j ACCEPT"
% (fixed_ip, protocol, port))
def remove_floating_forward(floating_ip, fixed_ip):
"""Remove forwarding for floating ip"""
- _remove_rule("PREROUTING -t nat -d %s -j DNAT --to %s"
+ _remove_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
% (floating_ip, fixed_ip))
- _remove_rule("POSTROUTING -t nat -s %s -j SNAT --to %s"
+ _remove_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s"
% (fixed_ip, floating_ip))
- _remove_rule("FORWARD -d %s -p icmp -j ACCEPT"
+ _remove_rule("FORWARD", "-d %s -p icmp -j ACCEPT"
% (fixed_ip))
for (protocol, port) in DEFAULT_PORTS:
- _remove_rule("FORWARD -d %s -p %s --dport %s -j ACCEPT"
+ _remove_rule("FORWARD", "-d %s -p %s --dport %s -j ACCEPT"
% (fixed_ip, protocol, port))
@@ -124,9 +145,10 @@ def ensure_bridge(bridge, interface, net_attrs=None):
net_attrs['gateway'],
net_attrs['broadcast'],
net_attrs['netmask']))
- _confirm_rule("FORWARD --in-interface %s -j ACCEPT" % bridge)
else:
_execute("sudo ifconfig %s up" % bridge)
+ _confirm_rule("FORWARD", "--in-interface %s -j ACCEPT" % bridge)
+ _confirm_rule("FORWARD", "--out-interface %s -j ACCEPT" % bridge)
def get_dhcp_hosts(context, network_id):
@@ -195,15 +217,19 @@ def _device_exists(device):
return not err
-def _confirm_rule(cmd):
+def _confirm_rule(chain, cmd):
"""Delete and re-add iptables rule"""
- _execute("sudo iptables --delete %s" % (cmd), check_exit_code=False)
- _execute("sudo iptables -I %s" % (cmd))
+ if FLAGS.use_nova_chains:
+ chain = "nova_%s" % chain.lower()
+ _execute("sudo iptables --delete %s %s" % (chain, cmd), check_exit_code=False)
+ _execute("sudo iptables -I %s %s" % (chain, cmd))
-def _remove_rule(cmd):
+def _remove_rule(chain, cmd):
"""Remove iptables rule"""
- _execute("sudo iptables --delete %s" % (cmd))
+ if FLAGS.use_nova_chains:
+ chain = "%S" % chain.lower()
+ _execute("sudo iptables --delete %s %s" % (chain, cmd))
def _dnsmasq_cmd(net):
diff --git a/nova/network/manager.py b/nova/network/manager.py
index 4031f80f3..1325c300b 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -236,6 +236,11 @@ class VlanManager(NetworkManager):
if num:
logging.debug("Dissassociated %s stale fixed ip(s)", num)
+ def init_host(self):
+ """Do any initialization that needs to be run if this is a
+ standalone service.
+ """
+ self.driver.init_host()
def allocate_fixed_ip(self, context, instance_id, *args, **kwargs):
"""Gets a fixed ip from the pool"""
@@ -354,7 +359,7 @@ class VlanManager(NetworkManager):
This could use a manage command instead of keying off of a flag"""
if not self.db.network_index_count(context):
for index in range(FLAGS.num_networks):
- self.db.network_index_create(context, {'index': index})
+ self.db.network_index_create_safe(context, {'index': index})
def _on_set_network_host(self, context, network_id):
"""Called when this host becomes the host for a project"""
diff --git a/nova/objectstore/__init__.py b/nova/objectstore/__init__.py
index b8890ac03..ecad9be7c 100644
--- a/nova/objectstore/__init__.py
+++ b/nova/objectstore/__init__.py
@@ -22,7 +22,7 @@
.. automodule:: nova.objectstore
:platform: Unix
- :synopsis: Currently a trivial file-based system, getting extended w/ mongo.
+ :synopsis: Currently a trivial file-based system, getting extended w/ swift.
.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com>
.. moduleauthor:: Devin Carlen <devin.carlen@gmail.com>
.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com>
diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py
index aabf6831f..dfee64aca 100644
--- a/nova/objectstore/handler.py
+++ b/nova/objectstore/handler.py
@@ -352,6 +352,8 @@ class ImagesResource(resource.Resource):
m[u'imageType'] = m['type']
elif 'imageType' in m:
m[u'type'] = m['imageType']
+ if 'displayName' not in m:
+ m[u'displayName'] = u''
return m
request.write(json.dumps([decorate(i.metadata) for i in images]))
@@ -382,16 +384,25 @@ class ImagesResource(resource.Resource):
def render_POST(self, request): # pylint: disable-msg=R0201
"""Update image attributes: public/private"""
+ # image_id required for all requests
image_id = get_argument(request, 'image_id', u'')
- operation = get_argument(request, 'operation', u'')
-
image_object = image.Image(image_id)
-
if not image_object.is_authorized(request.context):
+ logging.debug("not authorized for render_POST in images")
raise exception.NotAuthorized
- image_object.set_public(operation=='add')
-
+ operation = get_argument(request, 'operation', u'')
+ if operation:
+ # operation implies publicity toggle
+ logging.debug("handling publicity toggle")
+ image_object.set_public(operation=='add')
+ else:
+ # other attributes imply update
+ logging.debug("update user fields")
+ clean_args = {}
+ for arg in request.args.keys():
+ clean_args[arg] = request.args[arg][0]
+ image_object.update_user_editable_fields(clean_args)
return ''
def render_DELETE(self, request): # pylint: disable-msg=R0201
diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py
index f3c02a425..def1b8167 100644
--- a/nova/objectstore/image.py
+++ b/nova/objectstore/image.py
@@ -82,6 +82,16 @@ class Image(object):
with open(os.path.join(self.path, 'info.json'), 'w') as f:
json.dump(md, f)
+ def update_user_editable_fields(self, args):
+ """args is from the request parameters, so requires extra cleaning"""
+ fields = {'display_name': 'displayName', 'description': 'description'}
+ info = self.metadata
+ for field in fields.keys():
+ if field in args:
+ info[fields[field]] = args[field]
+ with open(os.path.join(self.path, 'info.json'), 'w') as f:
+ json.dump(info, f)
+
@staticmethod
def all():
images = []
diff --git a/nova/service.py b/nova/service.py
index df450025b..a6c186896 100644
--- a/nova/service.py
+++ b/nova/service.py
@@ -54,6 +54,7 @@ class Service(object, service.Service):
self.topic = topic
manager_class = utils.import_class(manager)
self.manager = manager_class(host=host, *args, **kwargs)
+ self.manager.init_host()
self.model_disconnected = False
super(Service, self).__init__(*args, **kwargs)
try:
diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py
index 622cb4335..bfd0f87a7 100644
--- a/nova/tests/api/rackspace/__init__.py
+++ b/nova/tests/api/rackspace/__init__.py
@@ -17,6 +17,7 @@
import unittest
+from nova.api.rackspace import limited
from nova.api.rackspace import RateLimitingMiddleware
from nova.tests.api.test_helper import *
from webob import Request
@@ -77,3 +78,31 @@ class RateLimitingMiddlewareTest(unittest.TestCase):
self.assertEqual(middleware.limiter.__class__.__name__, "Limiter")
middleware = RateLimitingMiddleware(APIStub(), service_host='foobar')
self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy")
+
+
+class LimiterTest(unittest.TestCase):
+
+ def testLimiter(self):
+ items = range(2000)
+ req = Request.blank('/')
+ self.assertEqual(limited(items, req), items[ :1000])
+ req = Request.blank('/?offset=0')
+ self.assertEqual(limited(items, req), items[ :1000])
+ req = Request.blank('/?offset=3')
+ self.assertEqual(limited(items, req), items[3:1003])
+ req = Request.blank('/?offset=2005')
+ self.assertEqual(limited(items, req), [])
+ req = Request.blank('/?limit=10')
+ self.assertEqual(limited(items, req), items[ :10])
+ req = Request.blank('/?limit=0')
+ self.assertEqual(limited(items, req), items[ :1000])
+ req = Request.blank('/?limit=3000')
+ self.assertEqual(limited(items, req), items[ :1000])
+ req = Request.blank('/?offset=1&limit=3')
+ self.assertEqual(limited(items, req), items[1:4])
+ req = Request.blank('/?offset=3&limit=0')
+ self.assertEqual(limited(items, req), items[3:1003])
+ req = Request.blank('/?offset=3&limit=1500')
+ self.assertEqual(limited(items, req), items[3:1003])
+ req = Request.blank('/?offset=3000&limit=10')
+ self.assertEqual(limited(items, req), [])
diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py
index a6e10970f..56677c2f4 100644
--- a/nova/tests/api/rackspace/auth.py
+++ b/nova/tests/api/rackspace/auth.py
@@ -1,12 +1,14 @@
-import webob
-import webob.dec
+import datetime
import unittest
+
import stubout
+import webob
+import webob.dec
+
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):
diff --git a/nova/tests/api/rackspace/flavors.py b/nova/tests/api/rackspace/flavors.py
index 7bd1ea1c4..d25a2e2be 100644
--- a/nova/tests/api/rackspace/flavors.py
+++ b/nova/tests/api/rackspace/flavors.py
@@ -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
diff --git a/nova/tests/api/rackspace/images.py b/nova/tests/api/rackspace/images.py
index 560d8c898..4c9987e8b 100644
--- a/nova/tests/api/rackspace/images.py
+++ b/nova/tests/api/rackspace/images.py
@@ -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
diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py
index 9fd8e5e88..69ad2c1d3 100644
--- a/nova/tests/api/rackspace/servers.py
+++ b/nova/tests/api/rackspace/servers.py
@@ -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
+
+ def instance_create(context, inst):
+ class Foo(object):
+ ec2_id = 1
+ return Foo()
+
+ def fake_method(*args, **kwargs):
+ pass
+
+ def project_get_network(context, user_id):
+ return dict(id='1', host='localhost')
+
+ 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))
- # res = req.get_response(nova.api.API())
+ def server_update(context, id, params):
+ self.update_called = True
+ filtered_dict = dict(name='server_test', admin_pass='bacon')
+ self.assertEqual(params, filtered_dict)
- # print res
- def test_update_server_password(self):
- pass
+ self.stubs.Set(nova.db.api, 'instance_update',
+ server_update)
- def test_update_server_name(self):
- pass
+ 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')
diff --git a/nova/tests/api/rackspace/sharedipgroups.py b/nova/tests/api/rackspace/sharedipgroups.py
index b4b281db7..1906b54f5 100644
--- a/nova/tests/api/rackspace/sharedipgroups.py
+++ b/nova/tests/api/rackspace/sharedipgroups.py
@@ -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
diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py
index aa7fb382c..2cf154f63 100644
--- a/nova/tests/api/rackspace/test_helper.py
+++ b/nova/tests/api/rackspace/test_helper.py
@@ -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
diff --git a/nova/tests/api/rackspace/testfaults.py b/nova/tests/api/rackspace/testfaults.py
new file mode 100644
index 000000000..b2931bc98
--- /dev/null
+++ b/nova/tests/api/rackspace/testfaults.py
@@ -0,0 +1,40 @@
+import unittest
+import webob
+import webob.dec
+import webob.exc
+
+from nova.api.rackspace import faults
+
+class TestFaults(unittest.TestCase):
+
+ def test_fault_parts(self):
+ req = webob.Request.blank('/.xml')
+ f = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram'))
+ resp = req.get_response(f)
+
+ first_two_words = resp.body.strip().split()[:2]
+ self.assertEqual(first_two_words, ['<badRequest', 'code="400">'])
+ body_without_spaces = ''.join(resp.body.split())
+ self.assertTrue('<message>scram</message>' in body_without_spaces)
+
+ def test_retry_header(self):
+ req = webob.Request.blank('/.xml')
+ exc = webob.exc.HTTPRequestEntityTooLarge(explanation='sorry',
+ headers={'Retry-After': 4})
+ f = faults.Fault(exc)
+ resp = req.get_response(f)
+ first_two_words = resp.body.strip().split()[:2]
+ self.assertEqual(first_two_words, ['<overLimit', 'code="413">'])
+ body_sans_spaces = ''.join(resp.body.split())
+ self.assertTrue('<message>sorry</message>' in body_sans_spaces)
+ self.assertTrue('<retryAfter>4</retryAfter>' in body_sans_spaces)
+ self.assertEqual(resp.headers['Retry-After'], 4)
+
+ def test_raise(self):
+ @webob.dec.wsgify
+ def raiser(req):
+ raise faults.Fault(webob.exc.HTTPNotFound(explanation='whut?'))
+ req = webob.Request.blank('/.xml')
+ resp = req.get_response(raiser)
+ self.assertEqual(resp.status_int, 404)
+ self.assertTrue('whut?' in resp.body)
diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py
index f8be00613..1955bb417 100644
--- a/nova/tests/auth_unittest.py
+++ b/nova/tests/auth_unittest.py
@@ -28,26 +28,71 @@ from nova.api.ec2 import cloud
FLAGS = flags.FLAGS
-
-class AuthTestCase(test.TrialTestCase):
- flush_db = False
+class user_generator(object):
+ def __init__(self, manager, **user_state):
+ if 'name' not in user_state:
+ user_state['name'] = 'test1'
+ self.manager = manager
+ self.user = manager.create_user(**user_state)
+
+ def __enter__(self):
+ return self.user
+
+ def __exit__(self, value, type, trace):
+ self.manager.delete_user(self.user)
+
+class project_generator(object):
+ def __init__(self, manager, **project_state):
+ if 'name' not in project_state:
+ project_state['name'] = 'testproj'
+ if 'manager_user' not in project_state:
+ project_state['manager_user'] = 'test1'
+ self.manager = manager
+ self.project = manager.create_project(**project_state)
+
+ def __enter__(self):
+ return self.project
+
+ def __exit__(self, value, type, trace):
+ self.manager.delete_project(self.project)
+
+class user_and_project_generator(object):
+ def __init__(self, manager, user_state={}, project_state={}):
+ self.manager = manager
+ if 'name' not in user_state:
+ user_state['name'] = 'test1'
+ if 'name' not in project_state:
+ project_state['name'] = 'testproj'
+ if 'manager_user' not in project_state:
+ project_state['manager_user'] = 'test1'
+ self.user = manager.create_user(**user_state)
+ self.project = manager.create_project(**project_state)
+
+ def __enter__(self):
+ return (self.user, self.project)
+
+ def __exit__(self, value, type, trace):
+ self.manager.delete_user(self.user)
+ self.manager.delete_project(self.project)
+
+class AuthManagerTestCase(test.TrialTestCase):
def setUp(self):
- super(AuthTestCase, self).setUp()
+ super(AuthManagerTestCase, self).setUp()
self.flags(connection_type='fake')
self.manager = manager.AuthManager()
- def test_001_can_create_users(self):
- self.manager.create_user('test1', 'access', 'secret')
- self.manager.create_user('test2')
-
- def test_002_can_get_user(self):
- user = self.manager.get_user('test1')
+ def test_create_and_find_user(self):
+ with user_generator(self.manager):
+ self.assert_(self.manager.get_user('test1'))
- def test_003_can_retreive_properties(self):
- user = self.manager.get_user('test1')
- self.assertEqual('test1', user.id)
- self.assertEqual('access', user.access)
- self.assertEqual('secret', user.secret)
+ def test_create_and_find_with_properties(self):
+ with user_generator(self.manager, name="herbert", secret="classified",
+ access="private-party"):
+ u = self.manager.get_user('herbert')
+ self.assertEqual('herbert', u.id)
+ self.assertEqual('herbert', u.name)
+ self.assertEqual('classified', u.secret)
+ self.assertEqual('private-party', u.access)
def test_004_signature_is_valid(self):
#self.assertTrue(self.manager.authenticate( **boto.generate_url ... ? ? ? ))
@@ -64,133 +109,216 @@ class AuthTestCase(test.TrialTestCase):
'export S3_URL="http://127.0.0.1:3333/"\n' +
'export EC2_USER_ID="test1"\n')
- def test_010_can_list_users(self):
- users = self.manager.get_users()
- logging.warn(users)
- self.assertTrue(filter(lambda u: u.id == 'test1', users))
-
- def test_101_can_add_user_role(self):
- self.assertFalse(self.manager.has_role('test1', 'itsec'))
- self.manager.add_role('test1', 'itsec')
- self.assertTrue(self.manager.has_role('test1', 'itsec'))
-
- def test_199_can_remove_user_role(self):
- self.assertTrue(self.manager.has_role('test1', 'itsec'))
- self.manager.remove_role('test1', 'itsec')
- self.assertFalse(self.manager.has_role('test1', 'itsec'))
-
- def test_201_can_create_project(self):
- project = self.manager.create_project('testproj', 'test1', 'A test project', ['test1'])
- self.assertTrue(filter(lambda p: p.name == 'testproj', self.manager.get_projects()))
- self.assertEqual(project.name, 'testproj')
- self.assertEqual(project.description, 'A test project')
- self.assertEqual(project.project_manager_id, 'test1')
- self.assertTrue(project.has_member('test1'))
-
- def test_202_user1_is_project_member(self):
- self.assertTrue(self.manager.get_user('test1').is_project_member('testproj'))
-
- def test_203_user2_is_not_project_member(self):
- self.assertFalse(self.manager.get_user('test2').is_project_member('testproj'))
-
- def test_204_user1_is_project_manager(self):
- self.assertTrue(self.manager.get_user('test1').is_project_manager('testproj'))
-
- def test_205_user2_is_not_project_manager(self):
- self.assertFalse(self.manager.get_user('test2').is_project_manager('testproj'))
-
- def test_206_can_add_user_to_project(self):
- self.manager.add_to_project('test2', 'testproj')
- self.assertTrue(self.manager.get_project('testproj').has_member('test2'))
-
- def test_207_can_remove_user_from_project(self):
- self.manager.remove_from_project('test2', 'testproj')
- self.assertFalse(self.manager.get_project('testproj').has_member('test2'))
-
- def test_208_can_remove_add_user_with_role(self):
- self.manager.add_to_project('test2', 'testproj')
- self.manager.add_role('test2', 'developer', 'testproj')
- self.manager.remove_from_project('test2', 'testproj')
- self.assertFalse(self.manager.has_role('test2', 'developer', 'testproj'))
- self.manager.add_to_project('test2', 'testproj')
- self.manager.remove_from_project('test2', 'testproj')
-
- def test_209_can_generate_x509(self):
- # MUST HAVE RUN CLOUD SETUP BY NOW
- self.cloud = cloud.CloudController()
- self.cloud.setup()
- _key, cert_str = self.manager._generate_x509_cert('test1', 'testproj')
- logging.debug(cert_str)
-
- # Need to verify that it's signed by the right intermediate CA
- full_chain = crypto.fetch_ca(project_id='testproj', chain=True)
- int_cert = crypto.fetch_ca(project_id='testproj', chain=False)
- cloud_cert = crypto.fetch_ca()
- logging.debug("CA chain:\n\n =====\n%s\n\n=====" % full_chain)
- signed_cert = X509.load_cert_string(cert_str)
- chain_cert = X509.load_cert_string(full_chain)
- int_cert = X509.load_cert_string(int_cert)
- cloud_cert = X509.load_cert_string(cloud_cert)
- self.assertTrue(signed_cert.verify(chain_cert.get_pubkey()))
- self.assertTrue(signed_cert.verify(int_cert.get_pubkey()))
-
- if not FLAGS.use_intermediate_ca:
- self.assertTrue(signed_cert.verify(cloud_cert.get_pubkey()))
- else:
- self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey()))
-
- def test_210_can_add_project_role(self):
- project = self.manager.get_project('testproj')
- self.assertFalse(project.has_role('test1', 'sysadmin'))
- self.manager.add_role('test1', 'sysadmin')
- self.assertFalse(project.has_role('test1', 'sysadmin'))
- project.add_role('test1', 'sysadmin')
- self.assertTrue(project.has_role('test1', 'sysadmin'))
-
- def test_211_can_list_project_roles(self):
- project = self.manager.get_project('testproj')
- user = self.manager.get_user('test1')
- self.manager.add_role(user, 'netadmin', project)
- roles = self.manager.get_user_roles(user)
- self.assertTrue('sysadmin' in roles)
- self.assertFalse('netadmin' in roles)
- project_roles = self.manager.get_user_roles(user, project)
- self.assertTrue('sysadmin' in project_roles)
- self.assertTrue('netadmin' in project_roles)
- # has role should be false because global role is missing
- self.assertFalse(self.manager.has_role(user, 'netadmin', project))
-
-
- def test_212_can_remove_project_role(self):
- project = self.manager.get_project('testproj')
- self.assertTrue(project.has_role('test1', 'sysadmin'))
- project.remove_role('test1', 'sysadmin')
- self.assertFalse(project.has_role('test1', 'sysadmin'))
- self.manager.remove_role('test1', 'sysadmin')
- self.assertFalse(project.has_role('test1', 'sysadmin'))
-
- def test_214_can_retrieve_project_by_user(self):
- project = self.manager.create_project('testproj2', 'test2', 'Another test project', ['test2'])
- self.assert_(len(self.manager.get_projects()) > 1)
- self.assertEqual(len(self.manager.get_projects('test2')), 1)
-
- def test_220_can_modify_project(self):
- self.manager.modify_project('testproj', 'test2', 'new description')
- project = self.manager.get_project('testproj')
- self.assertEqual(project.project_manager_id, 'test2')
- self.assertEqual(project.description, 'new description')
-
- def test_299_can_delete_project(self):
- self.manager.delete_project('testproj')
- self.assertFalse(filter(lambda p: p.name == 'testproj', self.manager.get_projects()))
- self.manager.delete_project('testproj2')
-
- def test_999_can_delete_users(self):
+ def test_can_list_users(self):
+ with user_generator(self.manager):
+ with user_generator(self.manager, name="test2"):
+ users = self.manager.get_users()
+ self.assert_(filter(lambda u: u.id == 'test1', users))
+ self.assert_(filter(lambda u: u.id == 'test2', users))
+ self.assert_(not filter(lambda u: u.id == 'test3', users))
+
+ def test_can_add_and_remove_user_role(self):
+ with user_generator(self.manager):
+ self.assertFalse(self.manager.has_role('test1', 'itsec'))
+ self.manager.add_role('test1', 'itsec')
+ self.assertTrue(self.manager.has_role('test1', 'itsec'))
+ self.manager.remove_role('test1', 'itsec')
+ self.assertFalse(self.manager.has_role('test1', 'itsec'))
+
+ def test_can_create_and_get_project(self):
+ with user_and_project_generator(self.manager) as (u,p):
+ self.assert_(self.manager.get_user('test1'))
+ self.assert_(self.manager.get_user('test1'))
+ self.assert_(self.manager.get_project('testproj'))
+
+ def test_can_list_projects(self):
+ with user_and_project_generator(self.manager):
+ with project_generator(self.manager, name="testproj2"):
+ projects = self.manager.get_projects()
+ self.assert_(filter(lambda p: p.name == 'testproj', projects))
+ self.assert_(filter(lambda p: p.name == 'testproj2', projects))
+ self.assert_(not filter(lambda p: p.name == 'testproj3',
+ projects))
+
+ def test_can_create_and_get_project_with_attributes(self):
+ with user_generator(self.manager):
+ with project_generator(self.manager, description='A test project'):
+ project = self.manager.get_project('testproj')
+ self.assertEqual('A test project', project.description)
+
+ def test_can_create_project_with_manager(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.assertEqual('test1', project.project_manager_id)
+ self.assertTrue(self.manager.is_project_manager(user, project))
+
+ def test_create_project_assigns_manager_to_members(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.assertTrue(self.manager.is_project_member(user, project))
+
+ def test_no_extra_project_members(self):
+ with user_generator(self.manager, name='test2') as baduser:
+ with user_and_project_generator(self.manager) as (user, project):
+ self.assertFalse(self.manager.is_project_member(baduser,
+ project))
+
+ def test_no_extra_project_managers(self):
+ with user_generator(self.manager, name='test2') as baduser:
+ with user_and_project_generator(self.manager) as (user, project):
+ self.assertFalse(self.manager.is_project_manager(baduser,
+ project))
+
+ def test_can_add_user_to_project(self):
+ with user_generator(self.manager, name='test2') as user:
+ with user_and_project_generator(self.manager) as (_user, project):
+ self.manager.add_to_project(user, project)
+ project = self.manager.get_project('testproj')
+ self.assertTrue(self.manager.is_project_member(user, project))
+
+ def test_can_remove_user_from_project(self):
+ with user_generator(self.manager, name='test2') as user:
+ with user_and_project_generator(self.manager) as (_user, project):
+ self.manager.add_to_project(user, project)
+ project = self.manager.get_project('testproj')
+ self.assertTrue(self.manager.is_project_member(user, project))
+ self.manager.remove_from_project(user, project)
+ project = self.manager.get_project('testproj')
+ self.assertFalse(self.manager.is_project_member(user, project))
+
+ def test_can_add_remove_user_with_role(self):
+ with user_generator(self.manager, name='test2') as user:
+ with user_and_project_generator(self.manager) as (_user, project):
+ # NOTE(todd): after modifying users you must reload project
+ self.manager.add_to_project(user, project)
+ project = self.manager.get_project('testproj')
+ self.manager.add_role(user, 'developer', project)
+ self.assertTrue(self.manager.is_project_member(user, project))
+ self.manager.remove_from_project(user, project)
+ project = self.manager.get_project('testproj')
+ self.assertFalse(self.manager.has_role(user, 'developer',
+ project))
+ self.assertFalse(self.manager.is_project_member(user, project))
+
+ def test_can_generate_x509(self):
+ # NOTE(todd): this doesn't assert against the auth manager
+ # so it probably belongs in crypto_unittest
+ # but I'm leaving it where I found it.
+ with user_and_project_generator(self.manager) as (user, project):
+ # NOTE(todd): Should mention why we must setup controller first
+ # (somebody please clue me in)
+ cloud_controller = cloud.CloudController()
+ cloud_controller.setup()
+ _key, cert_str = self.manager._generate_x509_cert('test1',
+ 'testproj')
+ logging.debug(cert_str)
+
+ # Need to verify that it's signed by the right intermediate CA
+ full_chain = crypto.fetch_ca(project_id='testproj', chain=True)
+ int_cert = crypto.fetch_ca(project_id='testproj', chain=False)
+ cloud_cert = crypto.fetch_ca()
+ logging.debug("CA chain:\n\n =====\n%s\n\n=====" % full_chain)
+ signed_cert = X509.load_cert_string(cert_str)
+ chain_cert = X509.load_cert_string(full_chain)
+ int_cert = X509.load_cert_string(int_cert)
+ cloud_cert = X509.load_cert_string(cloud_cert)
+ self.assertTrue(signed_cert.verify(chain_cert.get_pubkey()))
+ self.assertTrue(signed_cert.verify(int_cert.get_pubkey()))
+ if not FLAGS.use_intermediate_ca:
+ self.assertTrue(signed_cert.verify(cloud_cert.get_pubkey()))
+ else:
+ self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey()))
+
+ def test_adding_role_to_project_is_ignored_unless_added_to_user(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.assertFalse(self.manager.has_role(user, 'sysadmin', project))
+ self.manager.add_role(user, 'sysadmin', project)
+ # NOTE(todd): it will still show up in get_user_roles(u, project)
+ self.assertFalse(self.manager.has_role(user, 'sysadmin', project))
+ self.manager.add_role(user, 'sysadmin')
+ self.assertTrue(self.manager.has_role(user, 'sysadmin', project))
+
+ def test_add_user_role_doesnt_infect_project_roles(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.assertFalse(self.manager.has_role(user, 'sysadmin', project))
+ self.manager.add_role(user, 'sysadmin')
+ self.assertFalse(self.manager.has_role(user, 'sysadmin', project))
+
+ def test_can_list_user_roles(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.manager.add_role(user, 'sysadmin')
+ roles = self.manager.get_user_roles(user)
+ self.assertTrue('sysadmin' in roles)
+ self.assertFalse('netadmin' in roles)
+
+ def test_can_list_project_roles(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.manager.add_role(user, 'sysadmin')
+ self.manager.add_role(user, 'sysadmin', project)
+ self.manager.add_role(user, 'netadmin', project)
+ project_roles = self.manager.get_user_roles(user, project)
+ self.assertTrue('sysadmin' in project_roles)
+ self.assertTrue('netadmin' in project_roles)
+ # has role should be false user-level role is missing
+ self.assertFalse(self.manager.has_role(user, 'netadmin', project))
+
+ def test_can_remove_user_roles(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.manager.add_role(user, 'sysadmin')
+ self.assertTrue(self.manager.has_role(user, 'sysadmin'))
+ self.manager.remove_role(user, 'sysadmin')
+ self.assertFalse(self.manager.has_role(user, 'sysadmin'))
+
+ def test_removing_user_role_hides_it_from_project(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.manager.add_role(user, 'sysadmin')
+ self.manager.add_role(user, 'sysadmin', project)
+ self.assertTrue(self.manager.has_role(user, 'sysadmin', project))
+ self.manager.remove_role(user, 'sysadmin')
+ self.assertFalse(self.manager.has_role(user, 'sysadmin', project))
+
+ def test_can_remove_project_role_but_keep_user_role(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.manager.add_role(user, 'sysadmin')
+ self.manager.add_role(user, 'sysadmin', project)
+ self.assertTrue(self.manager.has_role(user, 'sysadmin'))
+ self.manager.remove_role(user, 'sysadmin', project)
+ self.assertFalse(self.manager.has_role(user, 'sysadmin', project))
+ self.assertTrue(self.manager.has_role(user, 'sysadmin'))
+
+ def test_can_retrieve_project_by_user(self):
+ with user_and_project_generator(self.manager) as (user, project):
+ self.assertEqual(1, len(self.manager.get_projects('test1')))
+
+ def test_can_modify_project(self):
+ with user_and_project_generator(self.manager):
+ with user_generator(self.manager, name='test2'):
+ self.manager.modify_project('testproj', 'test2', 'new desc')
+ project = self.manager.get_project('testproj')
+ self.assertEqual('test2', project.project_manager_id)
+ self.assertEqual('new desc', project.description)
+
+ def test_can_delete_project(self):
+ with user_generator(self.manager):
+ self.manager.create_project('testproj', 'test1')
+ self.assert_(self.manager.get_project('testproj'))
+ self.manager.delete_project('testproj')
+ projectlist = self.manager.get_projects()
+ self.assert_(not filter(lambda p: p.name == 'testproj',
+ projectlist))
+
+ def test_can_delete_user(self):
+ self.manager.create_user('test1')
+ self.assert_(self.manager.get_user('test1'))
self.manager.delete_user('test1')
- users = self.manager.get_users()
- self.assertFalse(filter(lambda u: u.id == 'test1', users))
- self.manager.delete_user('test2')
- self.assertEqual(self.manager.get_user('test2'), None)
+ userlist = self.manager.get_users()
+ self.assert_(not filter(lambda u: u.id == 'test1', userlist))
+
+ def test_can_modify_users(self):
+ with user_generator(self.manager):
+ self.manager.modify_user('test1', 'access', 'secret', True)
+ user = self.manager.get_user('test1')
+ self.assertEqual('access', user.access)
+ self.assertEqual('secret', user.secret)
+ self.assertTrue(user.is_admin())
if __name__ == "__main__":
diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py
index e8ff42fc5..ae7dea1db 100644
--- a/nova/tests/cloud_unittest.py
+++ b/nova/tests/cloud_unittest.py
@@ -16,10 +16,13 @@
# License for the specific language governing permissions and limitations
# under the License.
+import json
import logging
from M2Crypto import BIO
from M2Crypto import RSA
+import os
import StringIO
+import tempfile
import time
from twisted.internet import defer
@@ -36,15 +39,22 @@ from nova.auth import manager
from nova.compute import power_state
from nova.api.ec2 import context
from nova.api.ec2 import cloud
+from nova.objectstore import image
FLAGS = flags.FLAGS
+# Temp dirs for working with image attributes through the cloud controller
+# (stole this from objectstore_unittest.py)
+OSS_TEMPDIR = tempfile.mkdtemp(prefix='test_oss-')
+IMAGES_PATH = os.path.join(OSS_TEMPDIR, 'images')
+os.makedirs(IMAGES_PATH)
+
class CloudTestCase(test.TrialTestCase):
def setUp(self):
super(CloudTestCase, self).setUp()
- self.flags(connection_type='fake')
+ self.flags(connection_type='fake', images_path=IMAGES_PATH)
self.conn = rpc.Connection.instance()
logging.getLogger().setLevel(logging.DEBUG)
@@ -191,3 +201,67 @@ class CloudTestCase(test.TrialTestCase):
#for i in xrange(4):
# data = self.cloud.get_metadata(instance(i)['private_dns_name'])
# self.assert_(data['meta-data']['ami-id'] == 'ami-%s' % i)
+
+ @staticmethod
+ def _fake_set_image_description(ctxt, image_id, description):
+ from nova.objectstore import handler
+ class req:
+ pass
+ request = req()
+ request.context = ctxt
+ request.args = {'image_id': [image_id],
+ 'description': [description]}
+
+ resource = handler.ImagesResource()
+ resource.render_POST(request)
+
+ def test_user_editable_image_endpoint(self):
+ pathdir = os.path.join(FLAGS.images_path, 'ami-testing')
+ os.mkdir(pathdir)
+ info = {'isPublic': False}
+ with open(os.path.join(pathdir, 'info.json'), 'w') as f:
+ json.dump(info, f)
+ img = image.Image('ami-testing')
+ # self.cloud.set_image_description(self.context, 'ami-testing',
+ # 'Foo Img')
+ # NOTE(vish): Above won't work unless we start objectstore or create
+ # a fake version of api/ec2/images.py conn that can
+ # call methods directly instead of going through boto.
+ # for now, just cheat and call the method directly
+ self._fake_set_image_description(self.context, 'ami-testing',
+ 'Foo Img')
+ self.assertEqual('Foo Img', img.metadata['description'])
+ self._fake_set_image_description(self.context, 'ami-testing', '')
+ self.assertEqual('', img.metadata['description'])
+
+ def test_update_of_instance_display_fields(self):
+ inst = db.instance_create({}, {})
+ self.cloud.update_instance(self.context, inst['ec2_id'],
+ display_name='c00l 1m4g3')
+ inst = db.instance_get({}, inst['id'])
+ self.assertEqual('c00l 1m4g3', inst['display_name'])
+ db.instance_destroy({}, inst['id'])
+
+ def test_update_of_instance_wont_update_private_fields(self):
+ inst = db.instance_create({}, {})
+ self.cloud.update_instance(self.context, inst['id'],
+ mac_address='DE:AD:BE:EF')
+ inst = db.instance_get({}, inst['id'])
+ self.assertEqual(None, inst['mac_address'])
+ db.instance_destroy({}, inst['id'])
+
+ def test_update_of_volume_display_fields(self):
+ vol = db.volume_create({}, {})
+ self.cloud.update_volume(self.context, vol['id'],
+ display_name='c00l v0lum3')
+ vol = db.volume_get({}, vol['id'])
+ self.assertEqual('c00l v0lum3', vol['display_name'])
+ db.volume_destroy({}, vol['id'])
+
+ def test_update_of_volume_wont_update_private_fields(self):
+ vol = db.volume_create({}, {})
+ self.cloud.update_volume(self.context, vol['id'],
+ mountpoint='/not/here')
+ vol = db.volume_get({}, vol['id'])
+ self.assertEqual(None, vol['mountpoint'])
+ db.volume_destroy({}, vol['id'])
diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py
index b5970d405..5a599ff3a 100644
--- a/nova/tests/objectstore_unittest.py
+++ b/nova/tests/objectstore_unittest.py
@@ -164,6 +164,12 @@ class ObjectStoreTestCase(test.TrialTestCase):
self.context.project = self.auth_manager.get_project('proj2')
self.assertFalse(my_img.is_authorized(self.context))
+ # change user-editable fields
+ my_img.update_user_editable_fields({'display_name': 'my cool image'})
+ self.assertEqual('my cool image', my_img.metadata['displayName'])
+ my_img.update_user_editable_fields({'display_name': ''})
+ self.assert_(not my_img.metadata['displayName'])
+
class TestHTTPChannel(http.HTTPChannel):
"""Dummy site required for twisted.web"""
diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py
index 1c6de4403..0d06b1fce 100644
--- a/nova/virt/xenapi.py
+++ b/nova/virt/xenapi.py
@@ -103,8 +103,8 @@ class XenAPIConnection(object):
self._conn.login_with_password(user, pw)
def list_instances(self):
- result = [self._conn.xenapi.VM.get_name_label(vm) \
- for vm in self._conn.xenapi.VM.get_all()]
+ return [self._conn.xenapi.VM.get_name_label(vm) \
+ for vm in self._conn.xenapi.VM.get_all()]
@defer.inlineCallbacks
def spawn(self, instance):
diff --git a/nova/wsgi.py b/nova/wsgi.py
index da9374542..b91d91121 100644
--- a/nova/wsgi.py
+++ b/nova/wsgi.py
@@ -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', {})
diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh
new file mode 100755
index 000000000..673353eb4
--- /dev/null
+++ b/tools/setup_iptables.sh
@@ -0,0 +1,158 @@
+#!/usr/bin/env bash
+# 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.
+
+# NOTE(vish): This script sets up some reasonable defaults for iptables and
+# creates nova-specific chains. If you use this script you should
+# run nova-network and nova-compute with --use_nova_chains=True
+
+# NOTE(vish): If you run nova-api on a different port, make sure to change
+# the port here
+API_PORT=${API_PORT:-"8773"}
+if [ -n "$1" ]; then
+ CMD=$1
+else
+ CMD="all"
+fi
+
+if [ -n "$2" ]; then
+ IP=$2
+else
+ # NOTE(vish): This will just get the first ip in the list, so if you
+ # have more than one eth device set up, this will fail, and
+ # you should explicitly pass in the ip of the instance
+ IP=`ifconfig | grep -m 1 'inet addr:'| cut -d: -f2 | awk '{print $1}'`
+fi
+
+if [ -n "$3" ]; then
+ PRIVATE_RANGE=$3
+else
+ PRIVATE_RANGE="10.0.0.0/12"
+fi
+
+
+if [ -n "$4" ]; then
+ # NOTE(vish): Management IP is the ip over which to allow ssh traffic. It
+ # will also allow traffic to nova-api
+ MGMT_IP=$4
+else
+ MGMT_IP="$IP"
+fi
+if [ "$CMD" == "clear" ]; then
+ iptables -P INPUT ACCEPT
+ iptables -P FORWARD ACCEPT
+ iptables -P OUTPUT ACCEPT
+ iptables -F
+ iptables -t nat -F
+ iptables -F nova_input
+ iptables -F nova_output
+ iptables -F nova_forward
+ iptables -t nat -F nova_input
+ iptables -t nat -F nova_output
+ iptables -t nat -F nova_forward
+ iptables -t nat -X
+ iptables -X
+fi
+
+if [ "$CMD" == "base" ] || [ "$CMD" == "all" ]; then
+ iptables -P INPUT DROP
+ iptables -A INPUT -m state --state INVALID -j DROP
+ iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
+ iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT
+ iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT
+ iptables -N nova_input
+ iptables -A INPUT -j nova_input
+ iptables -A INPUT -p icmp -j ACCEPT
+ iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset
+ iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable
+
+ iptables -P FORWARD DROP
+ iptables -A FORWARD -m state --state INVALID -j DROP
+ iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
+ iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
+ iptables -N nova_forward
+ iptables -A FORWARD -j nova_forward
+
+ # NOTE(vish): DROP on output is too restrictive for now. We need to add
+ # in a bunch of more specific output rules to use it.
+ # iptables -P OUTPUT DROP
+ iptables -A OUTPUT -m state --state INVALID -j DROP
+ iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
+ iptables -N nova_output
+ iptables -A OUTPUT -j nova_output
+
+ iptables -t nat -N nova_prerouting
+ iptables -t nat -A PREROUTING -j nova_prerouting
+
+ iptables -t nat -N nova_postrouting
+ iptables -t nat -A POSTROUTING -j nova_postrouting
+
+ iptables -t nat -N nova_output
+ iptables -t nat -A OUTPUT -j nova_output
+fi
+
+if [ "$CMD" == "ganglia" ] || [ "$CMD" == "all" ]; then
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 8649 -j ACCEPT
+ iptables -A nova_input -m udp -p udp -d $IP --dport 8649 -j ACCEPT
+fi
+
+if [ "$CMD" == "web" ] || [ "$CMD" == "all" ]; then
+ # NOTE(vish): This opens up ports for web access, allowing web-based
+ # dashboards to work.
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 80 -j ACCEPT
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 443 -j ACCEPT
+fi
+
+if [ "$CMD" == "objectstore" ] || [ "$CMD" == "all" ]; then
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 3333 -j ACCEPT
+fi
+
+if [ "$CMD" == "api" ] || [ "$CMD" == "all" ]; then
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport $API_PORT -j ACCEPT
+ if [ "$IP" != "$MGMT_IP" ]; then
+ iptables -A nova_input -m tcp -p tcp -d $MGMT_IP --dport $API_PORT -j ACCEPT
+ fi
+fi
+
+if [ "$CMD" == "redis" ] || [ "$CMD" == "all" ]; then
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 6379 -j ACCEPT
+fi
+
+if [ "$CMD" == "mysql" ] || [ "$CMD" == "all" ]; then
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 3306 -j ACCEPT
+fi
+
+if [ "$CMD" == "rabbitmq" ] || [ "$CMD" == "all" ]; then
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 4369 -j ACCEPT
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 5672 -j ACCEPT
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 53284 -j ACCEPT
+fi
+
+if [ "$CMD" == "dnsmasq" ] || [ "$CMD" == "all" ]; then
+ # NOTE(vish): this could theoretically be setup per network
+ # for each host, but it seems like overkill
+ iptables -A nova_input -m tcp -p tcp -s $PRIVATE_RANGE --dport 53 -j ACCEPT
+ iptables -A nova_input -m udp -p udp -s $PRIVATE_RANGE --dport 53 -j ACCEPT
+ iptables -A nova_input -m udp -p udp --dport 67 -j ACCEPT
+fi
+
+if [ "$CMD" == "ldap" ] || [ "$CMD" == "all" ]; then
+ iptables -A nova_input -m tcp -p tcp -d $IP --dport 389 -j ACCEPT
+fi
+
+