summaryrefslogtreecommitdiff
path: root/nova/tests/unit/api/openstack
diff options
context:
space:
mode:
Diffstat (limited to 'nova/tests/unit/api/openstack')
-rw-r--r--nova/tests/unit/api/openstack/__init__.py0
-rw-r--r--nova/tests/unit/api/openstack/common.py55
-rw-r--r--nova/tests/unit/api/openstack/compute/__init__.py0
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/__init__.py0
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_admin_actions.py734
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_admin_password.py111
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_agents.py352
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_aggregates.py670
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_attach_interfaces.py455
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_availability_zone.py512
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_baremetal_nodes.py159
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping.py359
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping_v1.py421
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_cells.py698
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_certificates.py140
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe.py210
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe_update.py99
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_config_drive.py260
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_console_auth_tokens.py103
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_console_output.py171
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_consoles.py587
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_createserverext.py387
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_deferred_delete.py147
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_disk_config.py449
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_evacuate.py268
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_availability_zone.py184
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_evacuate_find_host.py114
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_hypervisors.py101
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_ips.py189
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_ips_mac.py196
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_rescue_with_image.py62
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_server_attributes.py148
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_status.py148
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_virtual_interfaces_net.py123
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_extended_volumes.py124
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_fixed_ips.py256
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_flavor_access.py402
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_flavor_disabled.py127
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_flavor_manage.py465
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_flavor_rxtx.py127
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_flavor_swap.py126
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_flavorextradata.py127
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_flavors_extra_specs.py403
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_dns.py412
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_pools.py83
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_floating_ips.py853
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_floating_ips_bulk.py139
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_fping.py106
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_hide_server_addresses.py172
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_hosts.py471
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_hypervisor_status.py92
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_hypervisors.py596
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_image_size.py138
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_instance_actions.py327
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_instance_usage_audit_log.py210
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py497
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_migrate_server.py231
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_migrations.py139
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_multinic.py204
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_networks.py610
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_neutron_security_groups.py918
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py222
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_quotas.py648
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_rescue.py270
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_scheduler_hints.py220
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_security_group_default_rules.py515
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_security_groups.py1767
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_server_diagnostics.py132
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_server_external_events.py158
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_server_group_quotas.py188
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_server_groups.py521
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_server_password.py94
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_server_start_stop.py183
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_server_usage.py159
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_services.py576
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_shelve.py148
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_simple_tenant_usage.py539
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_snapshots.py209
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_tenant_networks.py76
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_used_limits.py306
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py127
-rw-r--r--nova/tests/unit/api/openstack/compute/contrib/test_volumes.py1083
-rw-r--r--nova/tests/unit/api/openstack/compute/extensions/__init__.py0
-rw-r--r--nova/tests/unit/api/openstack/compute/extensions/foxinsocks.py92
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/__init__.py0
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/__init__.py0
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/admin_only_action_common.py263
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_access_ips.py383
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_console_auth_tokens.py95
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_consoles.py270
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_create_backup.py261
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_extended_volumes.py387
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_extension_info.py98
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_lock_server.py57
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_migrations.py115
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_multiple_create.py547
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_pause_server.py60
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_pci.py236
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_server_actions.py1131
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_server_external_events.py140
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_server_password.py80
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py3353
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_services.py453
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_suspend_server.py48
-rw-r--r--nova/tests/unit/api/openstack/compute/plugins/v3/test_user_data.py195
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/__init__.py0
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/test_schemas.py106
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/mixed.xml8
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial.xml5
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial2.xml6
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/empty.xml2
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/full.xml4
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/refs.xml5
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/mixed.xml9
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/no-metadata.xml6
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial.xml5
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial2.xml7
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/empty.xml2
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/full.xml14
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/refs.xml5
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/mixed.xml18
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial.xml5
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial2.xml6
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial3.xml7
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/detailed.xml27
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/empty.xml2
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/full.xml16
-rw-r--r--nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/refs.xml5
-rw-r--r--nova/tests/unit/api/openstack/compute/test_api.py186
-rw-r--r--nova/tests/unit/api/openstack/compute/test_auth.py61
-rw-r--r--nova/tests/unit/api/openstack/compute/test_consoles.py293
-rw-r--r--nova/tests/unit/api/openstack/compute/test_extensions.py747
-rw-r--r--nova/tests/unit/api/openstack/compute/test_flavors.py943
-rw-r--r--nova/tests/unit/api/openstack/compute/test_image_metadata.py366
-rw-r--r--nova/tests/unit/api/openstack/compute/test_images.py1046
-rw-r--r--nova/tests/unit/api/openstack/compute/test_limits.py1016
-rw-r--r--nova/tests/unit/api/openstack/compute/test_server_actions.py1556
-rw-r--r--nova/tests/unit/api/openstack/compute/test_server_metadata.py771
-rw-r--r--nova/tests/unit/api/openstack/compute/test_servers.py4625
-rw-r--r--nova/tests/unit/api/openstack/compute/test_urlmap.py171
-rw-r--r--nova/tests/unit/api/openstack/compute/test_v21_extensions.py196
-rw-r--r--nova/tests/unit/api/openstack/compute/test_v3_auth.py62
-rw-r--r--nova/tests/unit/api/openstack/compute/test_v3_extensions.py194
-rw-r--r--nova/tests/unit/api/openstack/compute/test_versions.py797
-rw-r--r--nova/tests/unit/api/openstack/fakes.py662
-rw-r--r--nova/tests/unit/api/openstack/test_common.py764
-rw-r--r--nova/tests/unit/api/openstack/test_faults.py315
-rw-r--r--nova/tests/unit/api/openstack/test_mapper.py46
-rw-r--r--nova/tests/unit/api/openstack/test_wsgi.py1244
-rw-r--r--nova/tests/unit/api/openstack/test_xmlutil.py948
150 files changed, 50651 insertions, 0 deletions
diff --git a/nova/tests/unit/api/openstack/__init__.py b/nova/tests/unit/api/openstack/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/nova/tests/unit/api/openstack/__init__.py
diff --git a/nova/tests/unit/api/openstack/common.py b/nova/tests/unit/api/openstack/common.py
new file mode 100644
index 0000000000..972958a329
--- /dev/null
+++ b/nova/tests/unit/api/openstack/common.py
@@ -0,0 +1,55 @@
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.serialization import jsonutils
+import webob
+
+
+def webob_factory(url):
+ """Factory for removing duplicate webob code from tests."""
+
+ base_url = url
+
+ def web_request(url, method=None, body=None):
+ req = webob.Request.blank("%s%s" % (base_url, url))
+ if method:
+ req.content_type = "application/json"
+ req.method = method
+ if body:
+ req.body = jsonutils.dumps(body)
+ return req
+ return web_request
+
+
+def compare_links(actual, expected):
+ """Compare xml atom links."""
+
+ return compare_tree_to_dict(actual, expected, ('rel', 'href', 'type'))
+
+
+def compare_media_types(actual, expected):
+ """Compare xml media types."""
+
+ return compare_tree_to_dict(actual, expected, ('base', 'type'))
+
+
+def compare_tree_to_dict(actual, expected, keys):
+ """Compare parts of lxml.etree objects to dicts."""
+
+ for elem, data in zip(actual, expected):
+ for key in keys:
+ if elem.get(key) != data.get(key):
+ return False
+ return True
diff --git a/nova/tests/unit/api/openstack/compute/__init__.py b/nova/tests/unit/api/openstack/compute/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/__init__.py
diff --git a/nova/tests/unit/api/openstack/compute/contrib/__init__.py b/nova/tests/unit/api/openstack/compute/contrib/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/__init__.py
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_admin_actions.py b/nova/tests/unit/api/openstack/compute/contrib/test_admin_actions.py
new file mode 100644
index 0000000000..44bf495b29
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_admin_actions.py
@@ -0,0 +1,734 @@
+# Copyright 2011 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+import webob
+
+from nova.api.openstack import common
+from nova.api.openstack.compute.contrib import admin_actions as \
+ admin_actions_v2
+from nova.api.openstack.compute.plugins.v3 import admin_actions as \
+ admin_actions_v21
+from nova.compute import vm_states
+import nova.context
+from nova import exception
+from nova import objects
+from nova.openstack.common import uuidutils
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+
+class CommonMixin(object):
+ admin_actions = None
+ fake_url = None
+
+ def _make_request(self, url, body):
+ req = webob.Request.blank(self.fake_url + url)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.content_type = 'application/json'
+ return req.get_response(self.app)
+
+ def _stub_instance_get(self, uuid=None):
+ if uuid is None:
+ uuid = uuidutils.generate_uuid()
+ instance = fake_instance.fake_db_instance(
+ id=1, uuid=uuid, vm_state=vm_states.ACTIVE,
+ task_state=None, launched_at=timeutils.utcnow())
+ instance = objects.Instance._from_db_object(
+ self.context, objects.Instance(), instance)
+ self.compute_api.get(self.context, uuid, expected_attrs=None,
+ want_objects=True).AndReturn(instance)
+ return instance
+
+ def _stub_instance_get_failure(self, exc_info, uuid=None):
+ if uuid is None:
+ uuid = uuidutils.generate_uuid()
+ self.compute_api.get(self.context, uuid, expected_attrs=None,
+ want_objects=True).AndRaise(exc_info)
+ return uuid
+
+ def _test_non_existing_instance(self, action, body_map=None):
+ uuid = uuidutils.generate_uuid()
+ self._stub_instance_get_failure(
+ exception.InstanceNotFound(instance_id=uuid), uuid=uuid)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % uuid,
+ {action: body_map.get(action)})
+ self.assertEqual(404, res.status_int)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def _test_action(self, action, body=None, method=None):
+ if method is None:
+ method = action
+
+ instance = self._stub_instance_get()
+ getattr(self.compute_api, method)(self.context, instance)
+
+ self.mox.ReplayAll()
+ res = self._make_request('/servers/%s/action' % instance['uuid'],
+ {action: None})
+ self.assertEqual(202, res.status_int)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def _test_invalid_state(self, action, method=None, body_map=None,
+ compute_api_args_map=None):
+ if method is None:
+ method = action
+ if body_map is None:
+ body_map = {}
+ if compute_api_args_map is None:
+ compute_api_args_map = {}
+
+ instance = self._stub_instance_get()
+
+ args, kwargs = compute_api_args_map.get(action, ((), {}))
+
+ getattr(self.compute_api, method)(self.context, instance,
+ *args, **kwargs).AndRaise(
+ exception.InstanceInvalidState(
+ attr='vm_state', instance_uuid=instance['uuid'],
+ state='foo', method=method))
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance['uuid'],
+ {action: body_map.get(action)})
+ self.assertEqual(409, res.status_int)
+ self.assertIn("Cannot \'%(action)s\' instance %(id)s"
+ % {'id': instance['uuid'], 'action': action}, res.body)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def _test_locked_instance(self, action, method=None, body_map=None,
+ compute_api_args_map=None):
+ if method is None:
+ method = action
+
+ instance = self._stub_instance_get()
+
+ args, kwargs = (), {}
+ act = None
+
+ if compute_api_args_map:
+ args, kwargs = compute_api_args_map.get(action, ((), {}))
+ act = body_map.get(action)
+
+ getattr(self.compute_api, method)(self.context, instance,
+ *args, **kwargs).AndRaise(
+ exception.InstanceIsLocked(instance_uuid=instance['uuid']))
+ self.mox.ReplayAll()
+ res = self._make_request('/servers/%s/action' % instance['uuid'],
+ {action: act})
+ self.assertEqual(409, res.status_int)
+ self.assertIn('Instance %s is locked' % instance['uuid'], res.body)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+
+class AdminActionsTestV21(CommonMixin, test.NoDBTestCase):
+ admin_actions = admin_actions_v21
+ fake_url = '/v2/fake'
+
+ def setUp(self):
+ super(AdminActionsTestV21, self).setUp()
+ self.controller = self.admin_actions.AdminActionsController()
+ self.compute_api = self.controller.compute_api
+ self.context = nova.context.RequestContext('fake', 'fake')
+
+ def _fake_controller(*args, **kwargs):
+ return self.controller
+
+ self.stubs.Set(self.admin_actions, 'AdminActionsController',
+ _fake_controller)
+
+ self.app = self._get_app()
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21(init_only=('servers',
+ 'os-admin-actions'),
+ fake_auth_context=self.context)
+
+ def test_actions(self):
+ actions = ['resetNetwork', 'injectNetworkInfo']
+ method_translations = {'resetNetwork': 'reset_network',
+ 'injectNetworkInfo': 'inject_network_info'}
+
+ for action in actions:
+ method = method_translations.get(action)
+ self.mox.StubOutWithMock(self.compute_api, method or action)
+ self._test_action(action, method=method)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_actions_with_non_existed_instance(self):
+ actions = ['resetNetwork', 'injectNetworkInfo', 'os-resetState']
+ body_map = {'os-resetState': {'state': 'active'}}
+
+ for action in actions:
+ self._test_non_existing_instance(action,
+ body_map=body_map)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_actions_with_locked_instance(self):
+ actions = ['resetNetwork', 'injectNetworkInfo']
+ method_translations = {'resetNetwork': 'reset_network',
+ 'injectNetworkInfo': 'inject_network_info'}
+
+ for action in actions:
+ method = method_translations.get(action)
+ self.mox.StubOutWithMock(self.compute_api, method or action)
+ self._test_locked_instance(action, method=method)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+
+class AdminActionsTestV2(AdminActionsTestV21):
+ admin_actions = admin_actions_v2
+
+ def setUp(self):
+ super(AdminActionsTestV2, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Admin_actions'])
+
+ def _get_app(self):
+ return fakes.wsgi_app(init_only=('servers',),
+ fake_auth_context=self.context)
+
+ def test_actions(self):
+ actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate',
+ 'resetNetwork', 'injectNetworkInfo', 'lock',
+ 'unlock']
+ method_translations = {'migrate': 'resize',
+ 'resetNetwork': 'reset_network',
+ 'injectNetworkInfo': 'inject_network_info'}
+
+ for action in actions:
+ method = method_translations.get(action)
+ self.mox.StubOutWithMock(self.compute_api, method or action)
+ self._test_action(action, method=method)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_actions_raise_conflict_on_invalid_state(self):
+ actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate',
+ 'os-migrateLive']
+ method_translations = {'migrate': 'resize',
+ 'os-migrateLive': 'live_migrate'}
+ body_map = {'os-migrateLive':
+ {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}}
+ args_map = {'os-migrateLive': ((False, False, 'hostname'), {})}
+
+ for action in actions:
+ method = method_translations.get(action)
+ self.mox.StubOutWithMock(self.compute_api, method or action)
+ self._test_invalid_state(action, method=method, body_map=body_map,
+ compute_api_args_map=args_map)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_actions_with_non_existed_instance(self):
+ actions = ['pause', 'unpause', 'suspend', 'resume',
+ 'resetNetwork', 'injectNetworkInfo', 'lock',
+ 'unlock', 'os-resetState', 'migrate', 'os-migrateLive']
+ body_map = {'os-resetState': {'state': 'active'},
+ 'os-migrateLive':
+ {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}}
+ for action in actions:
+ self._test_non_existing_instance(action,
+ body_map=body_map)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_actions_with_locked_instance(self):
+ actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate',
+ 'resetNetwork', 'injectNetworkInfo', 'os-migrateLive']
+ method_translations = {'migrate': 'resize',
+ 'resetNetwork': 'reset_network',
+ 'injectNetworkInfo': 'inject_network_info',
+ 'os-migrateLive': 'live_migrate'}
+ args_map = {'os-migrateLive': ((False, False, 'hostname'), {})}
+ body_map = {'os-migrateLive': {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}}
+
+ for action in actions:
+ method = method_translations.get(action)
+ self.mox.StubOutWithMock(self.compute_api, method or action)
+ self._test_locked_instance(action, method=method,
+ body_map=body_map,
+ compute_api_args_map=args_map)
+
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def _test_migrate_exception(self, exc_info, expected_result):
+ self.mox.StubOutWithMock(self.compute_api, 'resize')
+ instance = self._stub_instance_get()
+ self.compute_api.resize(self.context, instance).AndRaise(exc_info)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance['uuid'],
+ {'migrate': None})
+ self.assertEqual(expected_result, res.status_int)
+
+ def _test_migrate_live_succeeded(self, param):
+ self.mox.StubOutWithMock(self.compute_api, 'live_migrate')
+ instance = self._stub_instance_get()
+ self.compute_api.live_migrate(self.context, instance, False,
+ False, 'hostname')
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance['uuid'],
+ {'os-migrateLive': param})
+ self.assertEqual(202, res.status_int)
+
+ def test_migrate_live_enabled(self):
+ param = {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}
+ self._test_migrate_live_succeeded(param)
+
+ def test_migrate_live_enabled_with_string_param(self):
+ param = {'host': 'hostname',
+ 'block_migration': "False",
+ 'disk_over_commit': "False"}
+ self._test_migrate_live_succeeded(param)
+
+ def test_migrate_live_missing_dict_param(self):
+ body = {'os-migrateLive': {'dummy': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}}
+ res = self._make_request('/servers/FAKE/action', body)
+ self.assertEqual(400, res.status_int)
+
+ def test_migrate_live_with_invalid_block_migration(self):
+ body = {'os-migrateLive': {'host': 'hostname',
+ 'block_migration': "foo",
+ 'disk_over_commit': False}}
+ res = self._make_request('/servers/FAKE/action', body)
+ self.assertEqual(400, res.status_int)
+
+ def test_migrate_live_with_invalid_disk_over_commit(self):
+ body = {'os-migrateLive': {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': "foo"}}
+ res = self._make_request('/servers/FAKE/action', body)
+ self.assertEqual(400, res.status_int)
+
+ def _test_migrate_live_failed_with_exception(self, fake_exc,
+ uuid=None):
+ self.mox.StubOutWithMock(self.compute_api, 'live_migrate')
+
+ instance = self._stub_instance_get(uuid=uuid)
+ self.compute_api.live_migrate(self.context, instance, False,
+ False, 'hostname').AndRaise(fake_exc)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance.uuid,
+ {'os-migrateLive':
+ {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}})
+ self.assertEqual(400, res.status_int)
+ self.assertIn(unicode(fake_exc), res.body)
+
+ def test_migrate_live_compute_service_unavailable(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.ComputeServiceUnavailable(host='host'))
+
+ def test_migrate_live_invalid_hypervisor_type(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InvalidHypervisorType())
+
+ def test_migrate_live_invalid_cpu_info(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InvalidCPUInfo(reason=""))
+
+ def test_migrate_live_unable_to_migrate_to_self(self):
+ uuid = uuidutils.generate_uuid()
+ self._test_migrate_live_failed_with_exception(
+ exception.UnableToMigrateToSelf(instance_id=uuid,
+ host='host'),
+ uuid=uuid)
+
+ def test_migrate_live_destination_hypervisor_too_old(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.DestinationHypervisorTooOld())
+
+ def test_migrate_live_no_valid_host(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.NoValidHost(reason=''))
+
+ def test_migrate_live_invalid_local_storage(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InvalidLocalStorage(path='', reason=''))
+
+ def test_migrate_live_invalid_shared_storage(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InvalidSharedStorage(path='', reason=''))
+
+ def test_migrate_live_hypervisor_unavailable(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.HypervisorUnavailable(host=""))
+
+ def test_migrate_live_instance_not_running(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InstanceNotRunning(instance_id=""))
+
+ def test_migrate_live_migration_pre_check_error(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.MigrationPreCheckError(reason=''))
+
+ def test_unlock_not_authorized(self):
+ self.mox.StubOutWithMock(self.compute_api, 'unlock')
+
+ instance = self._stub_instance_get()
+
+ self.compute_api.unlock(self.context, instance).AndRaise(
+ exception.PolicyNotAuthorized(action='unlock'))
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance['uuid'],
+ {'unlock': None})
+ self.assertEqual(403, res.status_int)
+
+
+class CreateBackupTestsV2(CommonMixin, test.NoDBTestCase):
+ fake_url = '/v2/fake'
+
+ def setUp(self):
+ super(CreateBackupTestsV2, self).setUp()
+ self.controller = admin_actions_v2.AdminActionsController()
+ self.compute_api = self.controller.compute_api
+ self.context = nova.context.RequestContext('fake', 'fake')
+
+ def _fake_controller(*args, **kwargs):
+ return self.controller
+
+ self.stubs.Set(admin_actions_v2, 'AdminActionsController',
+ _fake_controller)
+
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Admin_actions'])
+
+ self.app = fakes.wsgi_app(init_only=('servers',),
+ fake_auth_context=self.context)
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+ self.mox.StubOutWithMock(common,
+ 'check_img_metadata_properties_quota')
+ self.mox.StubOutWithMock(self.compute_api,
+ 'backup')
+
+ def _make_url(self, uuid):
+ return '/servers/%s/action' % uuid
+
+ def test_create_backup_with_metadata(self):
+ metadata = {'123': 'asdf'}
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ 'metadata': metadata,
+ },
+ }
+
+ image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
+ properties=metadata)
+
+ common.check_img_metadata_properties_quota(self.context, metadata)
+ instance = self._stub_instance_get()
+ self.compute_api.backup(self.context, instance, 'Backup 1',
+ 'daily', 1,
+ extra_properties=metadata).AndReturn(image)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request(self._make_url(instance['uuid']), body=body)
+ self.assertEqual(202, res.status_int)
+ self.assertIn('fake-image-id', res.headers['Location'])
+
+ def test_create_backup_no_name(self):
+ # Name is required for backups.
+ body = {
+ 'createBackup': {
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+ res = self._make_request(self._make_url('fake'), body=body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_no_rotation(self):
+ # Rotation is required for backup requests.
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ },
+ }
+ res = self._make_request(self._make_url('fake'), body=body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_negative_rotation(self):
+ """Rotation must be greater than or equal to zero
+ for backup requests
+ """
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': -1,
+ },
+ }
+ res = self._make_request(self._make_url('fake'), body=body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_no_backup_type(self):
+ # Backup Type (daily or weekly) is required for backup requests.
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'rotation': 1,
+ },
+ }
+ res = self._make_request(self._make_url('fake'), body=body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_bad_entity(self):
+ body = {'createBackup': 'go'}
+ res = self._make_request(self._make_url('fake'), body=body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_rotation_is_zero(self):
+ # The happy path for creating backups if rotation is zero.
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 0,
+ },
+ }
+
+ image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
+ properties={})
+ common.check_img_metadata_properties_quota(self.context, {})
+ instance = self._stub_instance_get()
+ self.compute_api.backup(self.context, instance, 'Backup 1',
+ 'daily', 0,
+ extra_properties={}).AndReturn(image)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request(self._make_url(instance['uuid']), body=body)
+ self.assertEqual(202, res.status_int)
+ self.assertNotIn('Location', res.headers)
+
+ def test_create_backup_rotation_is_positive(self):
+ # The happy path for creating backups if rotation is positive.
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
+ properties={})
+ common.check_img_metadata_properties_quota(self.context, {})
+ instance = self._stub_instance_get()
+ self.compute_api.backup(self.context, instance, 'Backup 1',
+ 'daily', 1,
+ extra_properties={}).AndReturn(image)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request(self._make_url(instance['uuid']), body=body)
+ self.assertEqual(202, res.status_int)
+ self.assertIn('fake-image-id', res.headers['Location'])
+
+ def test_create_backup_raises_conflict_on_invalid_state(self):
+ body_map = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+ args_map = {
+ 'createBackup': (
+ ('Backup 1', 'daily', 1), {'extra_properties': {}}
+ ),
+ }
+ common.check_img_metadata_properties_quota(self.context, {})
+ self._test_invalid_state('createBackup', method='backup',
+ body_map=body_map,
+ compute_api_args_map=args_map)
+
+ def test_create_backup_with_non_existed_instance(self):
+ body_map = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+ common.check_img_metadata_properties_quota(self.context, {})
+ self._test_non_existing_instance('createBackup',
+ body_map=body_map)
+
+ def test_create_backup_with_invalid_createBackup(self):
+ body = {
+ 'createBackupup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+ res = self._make_request(self._make_url('fake'), body=body)
+ self.assertEqual(400, res.status_int)
+
+
+class ResetStateTestsV21(test.NoDBTestCase):
+ admin_act = admin_actions_v21
+ bad_request = exception.ValidationError
+ fake_url = '/servers'
+
+ def setUp(self):
+ super(ResetStateTestsV21, self).setUp()
+ self.uuid = uuidutils.generate_uuid()
+ self.admin_api = self.admin_act.AdminActionsController()
+ self.compute_api = self.admin_api.compute_api
+
+ url = '%s/%s/action' % (self.fake_url, self.uuid)
+ self.request = self._get_request(url)
+ self.context = self.request.environ['nova.context']
+
+ def _get_request(self, url):
+ return fakes.HTTPRequest.blank(url)
+
+ def test_no_state(self):
+ self.assertRaises(self.bad_request,
+ self.admin_api._reset_state,
+ self.request, self.uuid,
+ body={"os-resetState": None})
+
+ def test_bad_state(self):
+ self.assertRaises(self.bad_request,
+ self.admin_api._reset_state,
+ self.request, self.uuid,
+ body={"os-resetState": {"state": "spam"}})
+
+ def test_no_instance(self):
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+ exc = exception.InstanceNotFound(instance_id='inst_ud')
+ self.compute_api.get(self.context, self.uuid, expected_attrs=None,
+ want_objects=True).AndRaise(exc)
+ self.mox.ReplayAll()
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.admin_api._reset_state,
+ self.request, self.uuid,
+ body={"os-resetState": {"state": "active"}})
+
+ def _setup_mock(self, expected):
+ instance = objects.Instance()
+ instance.uuid = self.uuid
+ instance.vm_state = 'fake'
+ instance.task_state = 'fake'
+ instance.obj_reset_changes()
+
+ self.mox.StubOutWithMock(instance, 'save')
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def check_state(admin_state_reset=True):
+ self.assertEqual(set(expected.keys()),
+ instance.obj_what_changed())
+ for k, v in expected.items():
+ self.assertEqual(v, getattr(instance, k),
+ "Instance.%s doesn't match" % k)
+ instance.obj_reset_changes()
+
+ self.compute_api.get(self.context, instance.uuid, expected_attrs=None,
+ want_objects=True).AndReturn(instance)
+ instance.save(admin_state_reset=True).WithSideEffects(check_state)
+
+ def test_reset_active(self):
+ self._setup_mock(dict(vm_state=vm_states.ACTIVE,
+ task_state=None))
+ self.mox.ReplayAll()
+
+ body = {"os-resetState": {"state": "active"}}
+ result = self.admin_api._reset_state(self.request, self.uuid,
+ body=body)
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.admin_api,
+ admin_actions_v21.AdminActionsController):
+ status_int = self.admin_api._reset_state.wsgi_code
+ else:
+ status_int = result.status_int
+ self.assertEqual(202, status_int)
+
+ def test_reset_error(self):
+ self._setup_mock(dict(vm_state=vm_states.ERROR,
+ task_state=None))
+ self.mox.ReplayAll()
+ body = {"os-resetState": {"state": "error"}}
+ result = self.admin_api._reset_state(self.request, self.uuid,
+ body=body)
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.admin_api,
+ admin_actions_v21.AdminActionsController):
+ status_int = self.admin_api._reset_state.wsgi_code
+ else:
+ status_int = result.status_int
+ self.assertEqual(202, status_int)
+
+
+class ResetStateTestsV2(ResetStateTestsV21):
+ admin_act = admin_actions_v2
+ bad_request = webob.exc.HTTPBadRequest
+ fake_url = '/fake/servers'
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_admin_password.py b/nova/tests/unit/api/openstack/compute/contrib/test_admin_password.py
new file mode 100644
index 0000000000..4ddfc08dcc
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_admin_password.py
@@ -0,0 +1,111 @@
+# Copyright 2011 OpenStack Foundation
+# Copyright 2013 IBM Corp.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.plugins.v3 import admin_password \
+ as admin_password_v21
+from nova.compute import api as compute_api
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+def fake_get(self, context, id, expected_attrs=None, want_objects=False):
+ return {'uuid': id}
+
+
+def fake_get_non_existent(self, context, id, expected_attrs=None,
+ want_objects=False):
+ raise exception.InstanceNotFound(instance_id=id)
+
+
+def fake_set_admin_password(self, context, instance, password=None):
+ pass
+
+
+def fake_set_admin_password_failed(self, context, instance, password=None):
+ raise exception.InstancePasswordSetFailed(instance=instance, reason='')
+
+
+def fake_set_admin_password_not_implemented(self, context, instance,
+ password=None):
+ raise NotImplementedError()
+
+
+class AdminPasswordTestV21(test.NoDBTestCase):
+ plugin = admin_password_v21
+
+ def setUp(self):
+ super(AdminPasswordTestV21, self).setUp()
+ self.stubs.Set(compute_api.API, 'set_admin_password',
+ fake_set_admin_password)
+ self.stubs.Set(compute_api.API, 'get', fake_get)
+ self.app = fakes.wsgi_app_v21(init_only=('servers',
+ self.plugin.ALIAS))
+
+ def _make_request(self, body):
+ req = webob.Request.blank('/v2/fake/servers/1/action')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.content_type = 'application/json'
+ res = req.get_response(self.app)
+ return res
+
+ def test_change_password(self):
+ body = {'changePassword': {'adminPass': 'test'}}
+ res = self._make_request(body)
+ self.assertEqual(res.status_int, 202)
+
+ def test_change_password_empty_string(self):
+ body = {'changePassword': {'adminPass': ''}}
+ res = self._make_request(body)
+ self.assertEqual(res.status_int, 202)
+
+ def test_change_password_with_non_implement(self):
+ body = {'changePassword': {'adminPass': 'test'}}
+ self.stubs.Set(compute_api.API, 'set_admin_password',
+ fake_set_admin_password_not_implemented)
+ res = self._make_request(body)
+ self.assertEqual(res.status_int, 501)
+
+ def test_change_password_with_non_existed_instance(self):
+ body = {'changePassword': {'adminPass': 'test'}}
+ self.stubs.Set(compute_api.API, 'get', fake_get_non_existent)
+ res = self._make_request(body)
+ self.assertEqual(res.status_int, 404)
+
+ def test_change_password_with_non_string_password(self):
+ body = {'changePassword': {'adminPass': 1234}}
+ res = self._make_request(body)
+ self.assertEqual(res.status_int, 400)
+
+ def test_change_password_failed(self):
+ body = {'changePassword': {'adminPass': 'test'}}
+ self.stubs.Set(compute_api.API, 'set_admin_password',
+ fake_set_admin_password_failed)
+ res = self._make_request(body)
+ self.assertEqual(res.status_int, 409)
+
+ def test_change_password_without_admin_password(self):
+ body = {'changPassword': {}}
+ res = self._make_request(body)
+ self.assertEqual(res.status_int, 400)
+
+ def test_change_password_none(self):
+ body = {'changePassword': None}
+ res = self._make_request(body)
+ self.assertEqual(res.status_int, 400)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_agents.py b/nova/tests/unit/api/openstack/compute/contrib/test_agents.py
new file mode 100644
index 0000000000..b8c6f857b6
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_agents.py
@@ -0,0 +1,352 @@
+# Copyright 2012 IBM Corp.
+#
+# 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 mock
+import webob.exc
+
+from nova.api.openstack.compute.contrib import agents as agents_v2
+from nova.api.openstack.compute.plugins.v3 import agents as agents_v21
+from nova import context
+from nova import db
+from nova.db.sqlalchemy import models
+from nova import exception
+from nova import test
+
+fake_agents_list = [{'hypervisor': 'kvm', 'os': 'win',
+ 'architecture': 'x86',
+ 'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545',
+ 'id': 1},
+ {'hypervisor': 'kvm', 'os': 'linux',
+ 'architecture': 'x86',
+ 'version': '16.0',
+ 'url': 'http://example.com/path/to/resource1',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f546',
+ 'id': 2},
+ {'hypervisor': 'xen', 'os': 'linux',
+ 'architecture': 'x86',
+ 'version': '16.0',
+ 'url': 'http://example.com/path/to/resource2',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f547',
+ 'id': 3},
+ {'hypervisor': 'xen', 'os': 'win',
+ 'architecture': 'power',
+ 'version': '7.0',
+ 'url': 'http://example.com/path/to/resource3',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f548',
+ 'id': 4},
+ ]
+
+
+def fake_agent_build_get_all(context, hypervisor):
+ agent_build_all = []
+ for agent in fake_agents_list:
+ if hypervisor and hypervisor != agent['hypervisor']:
+ continue
+ agent_build_ref = models.AgentBuild()
+ agent_build_ref.update(agent)
+ agent_build_all.append(agent_build_ref)
+ return agent_build_all
+
+
+def fake_agent_build_update(context, agent_build_id, values):
+ pass
+
+
+def fake_agent_build_destroy(context, agent_update_id):
+ pass
+
+
+def fake_agent_build_create(context, values):
+ values['id'] = 1
+ agent_build_ref = models.AgentBuild()
+ agent_build_ref.update(values)
+ return agent_build_ref
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {}
+
+
+class FakeRequestWithHypervisor(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {'hypervisor': 'kvm'}
+
+
+class AgentsTestV21(test.NoDBTestCase):
+ controller = agents_v21.AgentController()
+ validation_error = exception.ValidationError
+
+ def setUp(self):
+ super(AgentsTestV21, self).setUp()
+
+ self.stubs.Set(db, "agent_build_get_all",
+ fake_agent_build_get_all)
+ self.stubs.Set(db, "agent_build_update",
+ fake_agent_build_update)
+ self.stubs.Set(db, "agent_build_destroy",
+ fake_agent_build_destroy)
+ self.stubs.Set(db, "agent_build_create",
+ fake_agent_build_create)
+ self.context = context.get_admin_context()
+
+ def test_agents_create(self):
+ req = FakeRequest()
+ body = {'agent': {'hypervisor': 'kvm',
+ 'os': 'win',
+ 'architecture': 'x86',
+ 'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ response = {'agent': {'hypervisor': 'kvm',
+ 'os': 'win',
+ 'architecture': 'x86',
+ 'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545',
+ 'agent_id': 1}}
+ res_dict = self.controller.create(req, body=body)
+ self.assertEqual(res_dict, response)
+
+ def _test_agents_create_key_error(self, key):
+ req = FakeRequest()
+ body = {'agent': {'hypervisor': 'kvm',
+ 'os': 'win',
+ 'architecture': 'x86',
+ 'version': '7.0',
+ 'url': 'xxx://xxxx/xxx/xxx',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ body['agent'].pop(key)
+ self.assertRaises(self.validation_error,
+ self.controller.create, req, body=body)
+
+ def test_agents_create_without_hypervisor(self):
+ self._test_agents_create_key_error('hypervisor')
+
+ def test_agents_create_without_os(self):
+ self._test_agents_create_key_error('os')
+
+ def test_agents_create_without_architecture(self):
+ self._test_agents_create_key_error('architecture')
+
+ def test_agents_create_without_version(self):
+ self._test_agents_create_key_error('version')
+
+ def test_agents_create_without_url(self):
+ self._test_agents_create_key_error('url')
+
+ def test_agents_create_without_md5hash(self):
+ self._test_agents_create_key_error('md5hash')
+
+ def test_agents_create_with_wrong_type(self):
+ req = FakeRequest()
+ body = {'agent': None}
+ self.assertRaises(self.validation_error,
+ self.controller.create, req, body=body)
+
+ def test_agents_create_with_empty_type(self):
+ req = FakeRequest()
+ body = {}
+ self.assertRaises(self.validation_error,
+ self.controller.create, req, body=body)
+
+ def test_agents_create_with_existed_agent(self):
+ def fake_agent_build_create_with_exited_agent(context, values):
+ raise exception.AgentBuildExists(**values)
+
+ self.stubs.Set(db, 'agent_build_create',
+ fake_agent_build_create_with_exited_agent)
+ req = FakeRequest()
+ body = {'agent': {'hypervisor': 'kvm',
+ 'os': 'win',
+ 'architecture': 'x86',
+ 'version': '7.0',
+ 'url': 'xxx://xxxx/xxx/xxx',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ self.assertRaises(webob.exc.HTTPConflict, self.controller.create, req,
+ body=body)
+
+ def _test_agents_create_with_invalid_length(self, key):
+ req = FakeRequest()
+ body = {'agent': {'hypervisor': 'kvm',
+ 'os': 'win',
+ 'architecture': 'x86',
+ 'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ body['agent'][key] = 'x' * 256
+ self.assertRaises(self.validation_error,
+ self.controller.create, req, body=body)
+
+ def test_agents_create_with_invalid_length_hypervisor(self):
+ self._test_agents_create_with_invalid_length('hypervisor')
+
+ def test_agents_create_with_invalid_length_os(self):
+ self._test_agents_create_with_invalid_length('os')
+
+ def test_agents_create_with_invalid_length_architecture(self):
+ self._test_agents_create_with_invalid_length('architecture')
+
+ def test_agents_create_with_invalid_length_version(self):
+ self._test_agents_create_with_invalid_length('version')
+
+ def test_agents_create_with_invalid_length_url(self):
+ self._test_agents_create_with_invalid_length('url')
+
+ def test_agents_create_with_invalid_length_md5hash(self):
+ self._test_agents_create_with_invalid_length('md5hash')
+
+ def test_agents_delete(self):
+ req = FakeRequest()
+ self.controller.delete(req, 1)
+
+ def test_agents_delete_with_id_not_found(self):
+ with mock.patch.object(db, 'agent_build_destroy',
+ side_effect=exception.AgentBuildNotFound(id=1)):
+ req = FakeRequest()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, req, 1)
+
+ def test_agents_list(self):
+ req = FakeRequest()
+ res_dict = self.controller.index(req)
+ agents_list = [{'hypervisor': 'kvm', 'os': 'win',
+ 'architecture': 'x86',
+ 'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545',
+ 'agent_id': 1},
+ {'hypervisor': 'kvm', 'os': 'linux',
+ 'architecture': 'x86',
+ 'version': '16.0',
+ 'url': 'http://example.com/path/to/resource1',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f546',
+ 'agent_id': 2},
+ {'hypervisor': 'xen', 'os': 'linux',
+ 'architecture': 'x86',
+ 'version': '16.0',
+ 'url': 'http://example.com/path/to/resource2',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f547',
+ 'agent_id': 3},
+ {'hypervisor': 'xen', 'os': 'win',
+ 'architecture': 'power',
+ 'version': '7.0',
+ 'url': 'http://example.com/path/to/resource3',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f548',
+ 'agent_id': 4},
+ ]
+ self.assertEqual(res_dict, {'agents': agents_list})
+
+ def test_agents_list_with_hypervisor(self):
+ req = FakeRequestWithHypervisor()
+ res_dict = self.controller.index(req)
+ response = [{'hypervisor': 'kvm', 'os': 'win',
+ 'architecture': 'x86',
+ 'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545',
+ 'agent_id': 1},
+ {'hypervisor': 'kvm', 'os': 'linux',
+ 'architecture': 'x86',
+ 'version': '16.0',
+ 'url': 'http://example.com/path/to/resource1',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f546',
+ 'agent_id': 2},
+ ]
+ self.assertEqual(res_dict, {'agents': response})
+
+ def test_agents_update(self):
+ req = FakeRequest()
+ body = {'para': {'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ response = {'agent': {'agent_id': 1,
+ 'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ res_dict = self.controller.update(req, 1, body=body)
+ self.assertEqual(res_dict, response)
+
+ def _test_agents_update_key_error(self, key):
+ req = FakeRequest()
+ body = {'para': {'version': '7.0',
+ 'url': 'xxx://xxxx/xxx/xxx',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ body['para'].pop(key)
+ self.assertRaises(self.validation_error,
+ self.controller.update, req, 1, body=body)
+
+ def test_agents_update_without_version(self):
+ self._test_agents_update_key_error('version')
+
+ def test_agents_update_without_url(self):
+ self._test_agents_update_key_error('url')
+
+ def test_agents_update_without_md5hash(self):
+ self._test_agents_update_key_error('md5hash')
+
+ def test_agents_update_with_wrong_type(self):
+ req = FakeRequest()
+ body = {'agent': None}
+ self.assertRaises(self.validation_error,
+ self.controller.update, req, 1, body=body)
+
+ def test_agents_update_with_empty(self):
+ req = FakeRequest()
+ body = {}
+ self.assertRaises(self.validation_error,
+ self.controller.update, req, 1, body=body)
+
+ def test_agents_update_value_error(self):
+ req = FakeRequest()
+ body = {'para': {'version': '7.0',
+ 'url': 1111,
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ self.assertRaises(self.validation_error,
+ self.controller.update, req, 1, body=body)
+
+ def _test_agents_update_with_invalid_length(self, key):
+ req = FakeRequest()
+ body = {'para': {'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ body['para'][key] = 'x' * 256
+ self.assertRaises(self.validation_error,
+ self.controller.update, req, 1, body=body)
+
+ def test_agents_update_with_invalid_length_version(self):
+ self._test_agents_update_with_invalid_length('version')
+
+ def test_agents_update_with_invalid_length_url(self):
+ self._test_agents_update_with_invalid_length('url')
+
+ def test_agents_update_with_invalid_length_md5hash(self):
+ self._test_agents_update_with_invalid_length('md5hash')
+
+ def test_agents_update_with_id_not_found(self):
+ with mock.patch.object(db, 'agent_build_update',
+ side_effect=exception.AgentBuildNotFound(id=1)):
+ req = FakeRequest()
+ body = {'para': {'version': '7.0',
+ 'url': 'http://example.com/path/to/resource',
+ 'md5hash': 'add6bb58e139be103324d04d82d8f545'}}
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update, req, 1, body=body)
+
+
+class AgentsTestV2(AgentsTestV21):
+ controller = agents_v2.AgentController()
+ validation_error = webob.exc.HTTPBadRequest
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_aggregates.py b/nova/tests/unit/api/openstack/compute/contrib/test_aggregates.py
new file mode 100644
index 0000000000..9b52146fa1
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_aggregates.py
@@ -0,0 +1,670 @@
+# Copyright (c) 2012 Citrix Systems, Inc.
+# 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.
+
+"""Tests for the aggregates admin api."""
+
+import mock
+from webob import exc
+
+from nova.api.openstack.compute.contrib import aggregates as aggregates_v2
+from nova.api.openstack.compute.plugins.v3 import aggregates as aggregates_v21
+from nova import context
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import matchers
+
+AGGREGATE_LIST = [
+ {"name": "aggregate1", "id": "1", "availability_zone": "nova1"},
+ {"name": "aggregate2", "id": "2", "availability_zone": "nova1"},
+ {"name": "aggregate3", "id": "3", "availability_zone": "nova2"},
+ {"name": "aggregate1", "id": "4", "availability_zone": "nova1"}]
+AGGREGATE = {"name": "aggregate1",
+ "id": "1",
+ "availability_zone": "nova1",
+ "metadata": {"foo": "bar"},
+ "hosts": ["host1, host2"]}
+
+FORMATTED_AGGREGATE = {"name": "aggregate1",
+ "id": "1",
+ "availability_zone": "nova1"}
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context.get_admin_context()}
+
+
+class AggregateTestCaseV21(test.NoDBTestCase):
+ """Test Case for aggregates admin api."""
+
+ add_host = 'self.controller._add_host'
+ remove_host = 'self.controller._remove_host'
+ set_metadata = 'self.controller._set_metadata'
+ bad_request = exception.ValidationError
+
+ def _set_up(self):
+ self.controller = aggregates_v21.AggregateController()
+ self.req = fakes.HTTPRequest.blank('/v3/os-aggregates',
+ use_admin_context=True)
+ self.user_req = fakes.HTTPRequest.blank('/v3/os-aggregates')
+ self.context = self.req.environ['nova.context']
+
+ def setUp(self):
+ super(AggregateTestCaseV21, self).setUp()
+ self._set_up()
+
+ def test_index(self):
+ def stub_list_aggregates(context):
+ if context is None:
+ raise Exception()
+ return AGGREGATE_LIST
+ self.stubs.Set(self.controller.api, 'get_aggregate_list',
+ stub_list_aggregates)
+
+ result = self.controller.index(self.req)
+
+ self.assertEqual(AGGREGATE_LIST, result["aggregates"])
+
+ def test_index_no_admin(self):
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.index,
+ self.user_req)
+
+ def test_create(self):
+ def stub_create_aggregate(context, name, availability_zone):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("test", name, "name")
+ self.assertEqual("nova1", availability_zone, "availability_zone")
+ return AGGREGATE
+ self.stubs.Set(self.controller.api, "create_aggregate",
+ stub_create_aggregate)
+
+ result = self.controller.create(self.req, body={"aggregate":
+ {"name": "test",
+ "availability_zone": "nova1"}})
+ self.assertEqual(FORMATTED_AGGREGATE, result["aggregate"])
+
+ def test_create_no_admin(self):
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.create, self.user_req,
+ body={"aggregate":
+ {"name": "test",
+ "availability_zone": "nova1"}})
+
+ def test_create_with_duplicate_aggregate_name(self):
+ def stub_create_aggregate(context, name, availability_zone):
+ raise exception.AggregateNameExists(aggregate_name=name)
+ self.stubs.Set(self.controller.api, "create_aggregate",
+ stub_create_aggregate)
+
+ self.assertRaises(exc.HTTPConflict, self.controller.create,
+ self.req, body={"aggregate":
+ {"name": "test",
+ "availability_zone": "nova1"}})
+
+ def test_create_with_incorrect_availability_zone(self):
+ def stub_create_aggregate(context, name, availability_zone):
+ raise exception.InvalidAggregateAction(action='create_aggregate',
+ aggregate_id="'N/A'",
+ reason='invalid zone')
+
+ self.stubs.Set(self.controller.api, "create_aggregate",
+ stub_create_aggregate)
+
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create,
+ self.req, body={"aggregate":
+ {"name": "test",
+ "availability_zone": "nova_bad"}})
+
+ def test_create_with_no_aggregate(self):
+ self.assertRaises(self.bad_request, self.controller.create,
+ self.req, body={"foo":
+ {"name": "test",
+ "availability_zone": "nova1"}})
+
+ def test_create_with_no_name(self):
+ self.assertRaises(self.bad_request, self.controller.create,
+ self.req, body={"aggregate":
+ {"foo": "test",
+ "availability_zone": "nova1"}})
+
+ def test_create_with_no_availability_zone(self):
+ def stub_create_aggregate(context, name, availability_zone):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("test", name, "name")
+ self.assertIsNone(availability_zone, "availability_zone")
+ return AGGREGATE
+ self.stubs.Set(self.controller.api, "create_aggregate",
+ stub_create_aggregate)
+
+ result = self.controller.create(self.req,
+ body={"aggregate": {"name": "test"}})
+ self.assertEqual(FORMATTED_AGGREGATE, result["aggregate"])
+
+ def test_create_with_null_name(self):
+ self.assertRaises(self.bad_request, self.controller.create,
+ self.req, body={"aggregate":
+ {"name": "",
+ "availability_zone": "nova1"}})
+
+ def test_create_with_name_too_long(self):
+ self.assertRaises(self.bad_request, self.controller.create,
+ self.req, body={"aggregate":
+ {"name": "x" * 256,
+ "availability_zone": "nova1"}})
+
+ def test_create_with_availability_zone_too_long(self):
+ self.assertRaises(self.bad_request, self.controller.create,
+ self.req, body={"aggregate":
+ {"name": "test",
+ "availability_zone": "x" * 256}})
+
+ def test_create_with_null_availability_zone(self):
+ aggregate = {"name": "aggregate1",
+ "id": "1",
+ "availability_zone": None,
+ "metadata": {},
+ "hosts": []}
+
+ formatted_aggregate = {"name": "aggregate1",
+ "id": "1",
+ "availability_zone": None}
+
+ def stub_create_aggregate(context, name, az_name):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("aggregate1", name, "name")
+ self.assertIsNone(az_name, "availability_zone")
+ return aggregate
+ self.stubs.Set(self.controller.api, 'create_aggregate',
+ stub_create_aggregate)
+
+ result = self.controller.create(self.req,
+ body={"aggregate":
+ {"name": "aggregate1",
+ "availability_zone": None}})
+ self.assertEqual(formatted_aggregate, result["aggregate"])
+
+ def test_create_with_empty_availability_zone(self):
+ self.assertRaises(self.bad_request, self.controller.create,
+ self.req, body={"aggregate":
+ {"name": "test",
+ "availability_zone": ""}})
+
+ def test_create_with_extra_invalid_arg(self):
+ self.assertRaises(self.bad_request, self.controller.create,
+ self.req, body={"name": "test",
+ "availability_zone": "nova1",
+ "foo": 'bar'})
+
+ def test_show(self):
+ def stub_get_aggregate(context, id):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("1", id, "id")
+ return AGGREGATE
+ self.stubs.Set(self.controller.api, 'get_aggregate',
+ stub_get_aggregate)
+
+ aggregate = self.controller.show(self.req, "1")
+
+ self.assertEqual(AGGREGATE, aggregate["aggregate"])
+
+ def test_show_no_admin(self):
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.show,
+ self.user_req, "1")
+
+ def test_show_with_invalid_id(self):
+ def stub_get_aggregate(context, id):
+ raise exception.AggregateNotFound(aggregate_id=2)
+
+ self.stubs.Set(self.controller.api, 'get_aggregate',
+ stub_get_aggregate)
+
+ self.assertRaises(exc.HTTPNotFound,
+ self.controller.show, self.req, "2")
+
+ def test_update(self):
+ body = {"aggregate": {"name": "new_name",
+ "availability_zone": "nova1"}}
+
+ def stub_update_aggregate(context, aggregate, values):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("1", aggregate, "aggregate")
+ self.assertEqual(body["aggregate"], values, "values")
+ return AGGREGATE
+ self.stubs.Set(self.controller.api, "update_aggregate",
+ stub_update_aggregate)
+
+ result = self.controller.update(self.req, "1", body=body)
+
+ self.assertEqual(AGGREGATE, result["aggregate"])
+
+ def test_update_no_admin(self):
+ body = {"aggregate": {"availability_zone": "nova"}}
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.update,
+ self.user_req, "1", body=body)
+
+ def test_update_with_only_name(self):
+ body = {"aggregate": {"name": "new_name"}}
+
+ def stub_update_aggregate(context, aggregate, values):
+ return AGGREGATE
+ self.stubs.Set(self.controller.api, "update_aggregate",
+ stub_update_aggregate)
+
+ result = self.controller.update(self.req, "1", body=body)
+
+ self.assertEqual(AGGREGATE, result["aggregate"])
+
+ def test_update_with_only_availability_zone(self):
+ body = {"aggregate": {"availability_zone": "nova1"}}
+
+ def stub_update_aggregate(context, aggregate, values):
+ return AGGREGATE
+ self.stubs.Set(self.controller.api, "update_aggregate",
+ stub_update_aggregate)
+ result = self.controller.update(self.req, "1", body=body)
+ self.assertEqual(AGGREGATE, result["aggregate"])
+
+ def test_update_with_no_updates(self):
+ test_metadata = {"aggregate": {}}
+ self.assertRaises(self.bad_request, self.controller.update,
+ self.req, "2", body=test_metadata)
+
+ def test_update_with_no_update_key(self):
+ test_metadata = {"asdf": {}}
+ self.assertRaises(self.bad_request, self.controller.update,
+ self.req, "2", body=test_metadata)
+
+ def test_update_with_wrong_updates(self):
+ test_metadata = {"aggregate": {"status": "disable",
+ "foo": "bar"}}
+ self.assertRaises(self.bad_request, self.controller.update,
+ self.req, "2", body=test_metadata)
+
+ def test_update_with_null_name(self):
+ test_metadata = {"aggregate": {"name": ""}}
+ self.assertRaises(self.bad_request, self.controller.update,
+ self.req, "2", body=test_metadata)
+
+ def test_update_with_name_too_long(self):
+ test_metadata = {"aggregate": {"name": "x" * 256}}
+ self.assertRaises(self.bad_request, self.controller.update,
+ self.req, "2", body=test_metadata)
+
+ def test_update_with_availability_zone_too_long(self):
+ test_metadata = {"aggregate": {"availability_zone": "x" * 256}}
+ self.assertRaises(self.bad_request, self.controller.update,
+ self.req, "2", body=test_metadata)
+
+ def test_update_with_empty_availability_zone(self):
+ test_metadata = {"aggregate": {"availability_zone": ""}}
+ self.assertRaises(self.bad_request, self.controller.update,
+ self.req, "2", body=test_metadata)
+
+ def test_update_with_null_availability_zone(self):
+ body = {"aggregate": {"availability_zone": None}}
+ aggre = {"name": "aggregate1",
+ "id": "1",
+ "availability_zone": None}
+
+ def stub_update_aggregate(context, aggregate, values):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("1", aggregate, "aggregate")
+ self.assertIsNone(values["availability_zone"], "availability_zone")
+ return aggre
+ self.stubs.Set(self.controller.api, "update_aggregate",
+ stub_update_aggregate)
+
+ result = self.controller.update(self.req, "1", body=body)
+
+ self.assertEqual(aggre, result["aggregate"])
+
+ def test_update_with_bad_aggregate(self):
+ test_metadata = {"aggregate": {"name": "test_name"}}
+
+ def stub_update_aggregate(context, aggregate, metadata):
+ raise exception.AggregateNotFound(aggregate_id=2)
+ self.stubs.Set(self.controller.api, "update_aggregate",
+ stub_update_aggregate)
+
+ self.assertRaises(exc.HTTPNotFound, self.controller.update,
+ self.req, "2", body=test_metadata)
+
+ def test_update_with_duplicated_name(self):
+ test_metadata = {"aggregate": {"name": "test_name"}}
+
+ def stub_update_aggregate(context, aggregate, metadata):
+ raise exception.AggregateNameExists(aggregate_name="test_name")
+
+ self.stubs.Set(self.controller.api, "update_aggregate",
+ stub_update_aggregate)
+ self.assertRaises(exc.HTTPConflict, self.controller.update,
+ self.req, "2", body=test_metadata)
+
+ def test_invalid_action(self):
+ body = {"append_host": {"host": "host1"}}
+ self.assertRaises(self.bad_request,
+ eval(self.add_host), self.req, "1", body=body)
+
+ def test_update_with_invalid_action(self):
+ with mock.patch.object(self.controller.api, "update_aggregate",
+ side_effect=exception.InvalidAggregateAction(
+ action='invalid', aggregate_id='agg1', reason= "not empty")):
+ body = {"aggregate": {"availability_zone": "nova"}}
+ self.assertRaises(exc.HTTPBadRequest, self.controller.update,
+ self.req, "1", body=body)
+
+ def test_add_host(self):
+ def stub_add_host_to_aggregate(context, aggregate, host):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("1", aggregate, "aggregate")
+ self.assertEqual("host1", host, "host")
+ return AGGREGATE
+ self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+ stub_add_host_to_aggregate)
+
+ aggregate = eval(self.add_host)(self.req, "1",
+ body={"add_host": {"host":
+ "host1"}})
+
+ self.assertEqual(aggregate["aggregate"], AGGREGATE)
+
+ def test_add_host_no_admin(self):
+ self.assertRaises(exception.PolicyNotAuthorized,
+ eval(self.add_host),
+ self.user_req, "1",
+ body={"add_host": {"host": "host1"}})
+
+ def test_add_host_with_already_added_host(self):
+ def stub_add_host_to_aggregate(context, aggregate, host):
+ raise exception.AggregateHostExists(aggregate_id=aggregate,
+ host=host)
+ self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+ stub_add_host_to_aggregate)
+
+ self.assertRaises(exc.HTTPConflict, eval(self.add_host),
+ self.req, "1",
+ body={"add_host": {"host": "host1"}})
+
+ def test_add_host_with_bad_aggregate(self):
+ def stub_add_host_to_aggregate(context, aggregate, host):
+ raise exception.AggregateNotFound(aggregate_id=aggregate)
+ self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+ stub_add_host_to_aggregate)
+
+ self.assertRaises(exc.HTTPNotFound, eval(self.add_host),
+ self.req, "bogus_aggregate",
+ body={"add_host": {"host": "host1"}})
+
+ def test_add_host_with_bad_host(self):
+ def stub_add_host_to_aggregate(context, aggregate, host):
+ raise exception.ComputeHostNotFound(host=host)
+ self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+ stub_add_host_to_aggregate)
+
+ self.assertRaises(exc.HTTPNotFound, eval(self.add_host),
+ self.req, "1",
+ body={"add_host": {"host": "bogus_host"}})
+
+ def test_add_host_with_missing_host(self):
+ self.assertRaises(self.bad_request, eval(self.add_host),
+ self.req, "1", body={"add_host": {"asdf": "asdf"}})
+
+ def test_add_host_with_invalid_format_host(self):
+ self.assertRaises(self.bad_request, eval(self.add_host),
+ self.req, "1", body={"add_host": {"host": "a" * 300}})
+
+ def test_add_host_with_multiple_hosts(self):
+ self.assertRaises(self.bad_request, eval(self.add_host),
+ self.req, "1", body={"add_host": {"host": ["host1", "host2"]}})
+
+ def test_add_host_raises_key_error(self):
+ def stub_add_host_to_aggregate(context, aggregate, host):
+ raise KeyError
+ self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+ stub_add_host_to_aggregate)
+ self.assertRaises(exc.HTTPInternalServerError,
+ eval(self.add_host), self.req, "1",
+ body={"add_host": {"host": "host1"}})
+
+ def test_add_host_with_invalid_request(self):
+ self.assertRaises(self.bad_request, eval(self.add_host),
+ self.req, "1", body={"add_host": "1"})
+
+ def test_add_host_with_non_string(self):
+ self.assertRaises(self.bad_request, eval(self.add_host),
+ self.req, "1", body={"add_host": {"host": 1}})
+
+ def test_remove_host(self):
+ def stub_remove_host_from_aggregate(context, aggregate, host):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("1", aggregate, "aggregate")
+ self.assertEqual("host1", host, "host")
+ stub_remove_host_from_aggregate.called = True
+ return {}
+ self.stubs.Set(self.controller.api,
+ "remove_host_from_aggregate",
+ stub_remove_host_from_aggregate)
+ eval(self.remove_host)(self.req, "1",
+ body={"remove_host": {"host": "host1"}})
+
+ self.assertTrue(stub_remove_host_from_aggregate.called)
+
+ def test_remove_host_no_admin(self):
+ self.assertRaises(exception.PolicyNotAuthorized,
+ eval(self.remove_host),
+ self.user_req, "1",
+ body={"remove_host": {"host": "host1"}})
+
+ def test_remove_host_with_bad_aggregate(self):
+ def stub_remove_host_from_aggregate(context, aggregate, host):
+ raise exception.AggregateNotFound(aggregate_id=aggregate)
+ self.stubs.Set(self.controller.api,
+ "remove_host_from_aggregate",
+ stub_remove_host_from_aggregate)
+
+ self.assertRaises(exc.HTTPNotFound, eval(self.remove_host),
+ self.req, "bogus_aggregate",
+ body={"remove_host": {"host": "host1"}})
+
+ def test_remove_host_with_host_not_in_aggregate(self):
+ def stub_remove_host_from_aggregate(context, aggregate, host):
+ raise exception.AggregateHostNotFound(aggregate_id=aggregate,
+ host=host)
+ self.stubs.Set(self.controller.api,
+ "remove_host_from_aggregate",
+ stub_remove_host_from_aggregate)
+
+ self.assertRaises(exc.HTTPNotFound, eval(self.remove_host),
+ self.req, "1",
+ body={"remove_host": {"host": "host1"}})
+
+ def test_remove_host_with_bad_host(self):
+ def stub_remove_host_from_aggregate(context, aggregate, host):
+ raise exception.ComputeHostNotFound(host=host)
+ self.stubs.Set(self.controller.api,
+ "remove_host_from_aggregate",
+ stub_remove_host_from_aggregate)
+
+ self.assertRaises(exc.HTTPNotFound, eval(self.remove_host),
+ self.req, "1", body={"remove_host": {"host": "bogushost"}})
+
+ def test_remove_host_with_missing_host(self):
+ self.assertRaises(self.bad_request, eval(self.remove_host),
+ self.req, "1", body={"asdf": "asdf"})
+
+ def test_remove_host_with_multiple_hosts(self):
+ self.assertRaises(self.bad_request, eval(self.remove_host),
+ self.req, "1", body={"remove_host": {"host":
+ ["host1", "host2"]}})
+
+ def test_remove_host_with_extra_param(self):
+ self.assertRaises(self.bad_request, eval(self.remove_host),
+ self.req, "1", body={"remove_host": {"asdf": "asdf",
+ "host": "asdf"}})
+
+ def test_remove_host_with_invalid_request(self):
+ self.assertRaises(self.bad_request,
+ eval(self.remove_host),
+ self.req, "1", body={"remove_host": "1"})
+
+ def test_remove_host_with_missing_host_empty(self):
+ self.assertRaises(self.bad_request,
+ eval(self.remove_host),
+ self.req, "1", body={"remove_host": {}})
+
+ def test_set_metadata(self):
+ body = {"set_metadata": {"metadata": {"foo": "bar"}}}
+
+ def stub_update_aggregate(context, aggregate, values):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("1", aggregate, "aggregate")
+ self.assertThat(body["set_metadata"]['metadata'],
+ matchers.DictMatches(values))
+ return AGGREGATE
+ self.stubs.Set(self.controller.api,
+ "update_aggregate_metadata",
+ stub_update_aggregate)
+
+ result = eval(self.set_metadata)(self.req, "1", body=body)
+
+ self.assertEqual(AGGREGATE, result["aggregate"])
+
+ def test_set_metadata_delete(self):
+ body = {"set_metadata": {"metadata": {"foo": None}}}
+
+ with mock.patch.object(self.controller.api,
+ 'update_aggregate_metadata') as mocked:
+ mocked.return_value = AGGREGATE
+ result = eval(self.set_metadata)(self.req, "1", body=body)
+
+ self.assertEqual(AGGREGATE, result["aggregate"])
+ mocked.assert_called_once_with(self.context, "1",
+ body["set_metadata"]["metadata"])
+
+ def test_set_metadata_no_admin(self):
+ self.assertRaises(exception.PolicyNotAuthorized,
+ eval(self.set_metadata),
+ self.user_req, "1",
+ body={"set_metadata": {"metadata":
+ {"foo": "bar"}}})
+
+ def test_set_metadata_with_bad_aggregate(self):
+ body = {"set_metadata": {"metadata": {"foo": "bar"}}}
+
+ def stub_update_aggregate(context, aggregate, metadata):
+ raise exception.AggregateNotFound(aggregate_id=aggregate)
+ self.stubs.Set(self.controller.api,
+ "update_aggregate_metadata",
+ stub_update_aggregate)
+ self.assertRaises(exc.HTTPNotFound, eval(self.set_metadata),
+ self.req, "bad_aggregate", body=body)
+
+ def test_set_metadata_with_missing_metadata(self):
+ body = {"asdf": {"foo": "bar"}}
+ self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata),
+ self.req, "1", body=body)
+
+ def test_set_metadata_with_extra_params(self):
+ body = {"metadata": {"foo": "bar"}, "asdf": {"foo": "bar"}}
+ self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata),
+ self.req, "1", body=body)
+
+ def test_set_metadata_without_dict(self):
+ body = {"set_metadata": {'metadata': 1}}
+ self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata),
+ self.req, "1", body=body)
+
+ def test_set_metadata_with_empty_key(self):
+ body = {"set_metadata": {"metadata": {"": "value"}}}
+ self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata),
+ self.req, "1", body=body)
+
+ def test_set_metadata_with_key_too_long(self):
+ body = {"set_metadata": {"metadata": {"x" * 256: "value"}}}
+ self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata),
+ self.req, "1", body=body)
+
+ def test_set_metadata_with_value_too_long(self):
+ body = {"set_metadata": {"metadata": {"key": "x" * 256}}}
+ self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata),
+ self.req, "1", body=body)
+
+ def test_set_metadata_with_string(self):
+ body = {"set_metadata": {"metadata": "test"}}
+ self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata),
+ self.req, "1", body=body)
+
+ def test_delete_aggregate(self):
+ def stub_delete_aggregate(context, aggregate):
+ self.assertEqual(context, self.context, "context")
+ self.assertEqual("1", aggregate, "aggregate")
+ stub_delete_aggregate.called = True
+ self.stubs.Set(self.controller.api, "delete_aggregate",
+ stub_delete_aggregate)
+
+ self.controller.delete(self.req, "1")
+ self.assertTrue(stub_delete_aggregate.called)
+
+ def test_delete_aggregate_no_admin(self):
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.delete,
+ self.user_req, "1")
+
+ def test_delete_aggregate_with_bad_aggregate(self):
+ def stub_delete_aggregate(context, aggregate):
+ raise exception.AggregateNotFound(aggregate_id=aggregate)
+ self.stubs.Set(self.controller.api, "delete_aggregate",
+ stub_delete_aggregate)
+
+ self.assertRaises(exc.HTTPNotFound, self.controller.delete,
+ self.req, "bogus_aggregate")
+
+ def test_delete_aggregate_with_host(self):
+ with mock.patch.object(self.controller.api, "delete_aggregate",
+ side_effect=exception.InvalidAggregateAction(
+ action="delete", aggregate_id="agg1",
+ reason="not empty")):
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.delete,
+ self.req, "agg1")
+
+
+class AggregateTestCaseV2(AggregateTestCaseV21):
+ add_host = 'self.controller.action'
+ remove_host = 'self.controller.action'
+ set_metadata = 'self.controller.action'
+ bad_request = exc.HTTPBadRequest
+
+ def _set_up(self):
+ self.controller = aggregates_v2.AggregateController()
+ self.req = FakeRequest()
+ self.user_req = fakes.HTTPRequest.blank('/v2/os-aggregates')
+ self.context = self.req.environ['nova.context']
+
+ def test_add_host_raises_key_error(self):
+ def stub_add_host_to_aggregate(context, aggregate, host):
+ raise KeyError
+ self.stubs.Set(self.controller.api, "add_host_to_aggregate",
+ stub_add_host_to_aggregate)
+ # NOTE(mtreinish) The check for a KeyError here is to ensure that
+ # if add_host_to_aggregate() raises a KeyError it propagates. At
+ # one point the api code would mask the error as a HTTPBadRequest.
+ # This test is to ensure that this doesn't occur again.
+ self.assertRaises(KeyError, eval(self.add_host), self.req, "1",
+ body={"add_host": {"host": "host1"}})
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_attach_interfaces.py b/nova/tests/unit/api/openstack/compute/contrib/test_attach_interfaces.py
new file mode 100644
index 0000000000..3b7e0b058a
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_attach_interfaces.py
@@ -0,0 +1,455 @@
+# Copyright 2012 SINA Inc.
+# 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 mock
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+
+from nova.api.openstack.compute.contrib import attach_interfaces \
+ as attach_interfaces_v2
+from nova.api.openstack.compute.plugins.v3 import attach_interfaces \
+ as attach_interfaces_v3
+from nova.compute import api as compute_api
+from nova import context
+from nova import exception
+from nova.network import api as network_api
+from nova import objects
+from nova import test
+from nova.tests.unit import fake_network_cache_model
+
+import webob
+from webob import exc
+
+
+CONF = cfg.CONF
+
+FAKE_UUID1 = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+FAKE_UUID2 = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
+
+FAKE_PORT_ID1 = '11111111-1111-1111-1111-111111111111'
+FAKE_PORT_ID2 = '22222222-2222-2222-2222-222222222222'
+FAKE_PORT_ID3 = '33333333-3333-3333-3333-333333333333'
+
+FAKE_NET_ID1 = '44444444-4444-4444-4444-444444444444'
+FAKE_NET_ID2 = '55555555-5555-5555-5555-555555555555'
+FAKE_NET_ID3 = '66666666-6666-6666-6666-666666666666'
+FAKE_BAD_NET_ID = '00000000-0000-0000-0000-000000000000'
+
+port_data1 = {
+ "id": FAKE_PORT_ID1,
+ "network_id": FAKE_NET_ID1,
+ "admin_state_up": True,
+ "status": "ACTIVE",
+ "mac_address": "aa:aa:aa:aa:aa:aa",
+ "fixed_ips": ["10.0.1.2"],
+ "device_id": FAKE_UUID1,
+}
+
+port_data2 = {
+ "id": FAKE_PORT_ID2,
+ "network_id": FAKE_NET_ID2,
+ "admin_state_up": True,
+ "status": "ACTIVE",
+ "mac_address": "bb:bb:bb:bb:bb:bb",
+ "fixed_ips": ["10.0.2.2"],
+ "device_id": FAKE_UUID1,
+}
+
+port_data3 = {
+ "id": FAKE_PORT_ID3,
+ "network_id": FAKE_NET_ID3,
+ "admin_state_up": True,
+ "status": "ACTIVE",
+ "mac_address": "bb:bb:bb:bb:bb:bb",
+ "fixed_ips": ["10.0.2.2"],
+ "device_id": '',
+}
+
+fake_networks = [FAKE_NET_ID1, FAKE_NET_ID2]
+ports = [port_data1, port_data2, port_data3]
+
+
+def fake_list_ports(self, *args, **kwargs):
+ result = []
+ for port in ports:
+ if port['device_id'] == kwargs['device_id']:
+ result.append(port)
+ return {'ports': result}
+
+
+def fake_show_port(self, context, port_id, **kwargs):
+ for port in ports:
+ if port['id'] == port_id:
+ return {'port': port}
+ else:
+ raise exception.PortNotFound(port_id=port_id)
+
+
+def fake_attach_interface(self, context, instance, network_id, port_id,
+ requested_ip='192.168.1.3'):
+ if not network_id:
+ # if no network_id is given when add a port to an instance, use the
+ # first default network.
+ network_id = fake_networks[0]
+ if network_id == FAKE_BAD_NET_ID:
+ raise exception.NetworkNotFound(network_id=network_id)
+ if not port_id:
+ port_id = ports[fake_networks.index(network_id)]['id']
+ vif = fake_network_cache_model.new_vif()
+ vif['id'] = port_id
+ vif['network']['id'] = network_id
+ vif['network']['subnets'][0]['ips'][0]['address'] = requested_ip
+ return vif
+
+
+def fake_detach_interface(self, context, instance, port_id):
+ for port in ports:
+ if port['id'] == port_id:
+ return
+ raise exception.PortNotFound(port_id=port_id)
+
+
+def fake_get_instance(self, *args, **kwargs):
+ return objects.Instance(uuid=FAKE_UUID1)
+
+
+class InterfaceAttachTestsV21(test.NoDBTestCase):
+ url = '/v3/os-interfaces'
+ controller_cls = attach_interfaces_v3.InterfaceAttachmentController
+ validate_exc = exception.ValidationError
+
+ def setUp(self):
+ super(InterfaceAttachTestsV21, self).setUp()
+ self.flags(auth_strategy=None, group='neutron')
+ self.flags(url='http://anyhost/', group='neutron')
+ self.flags(url_timeout=30, group='neutron')
+ self.stubs.Set(network_api.API, 'show_port', fake_show_port)
+ self.stubs.Set(network_api.API, 'list_ports', fake_list_ports)
+ self.stubs.Set(compute_api.API, 'get', fake_get_instance)
+ self.context = context.get_admin_context()
+ self.expected_show = {'interfaceAttachment':
+ {'net_id': FAKE_NET_ID1,
+ 'port_id': FAKE_PORT_ID1,
+ 'mac_addr': port_data1['mac_address'],
+ 'port_state': port_data1['status'],
+ 'fixed_ips': port_data1['fixed_ips'],
+ }}
+ self.attachments = self.controller_cls()
+
+ @mock.patch.object(compute_api.API, 'get',
+ side_effect=exception.InstanceNotFound(instance_id=''))
+ def _test_instance_not_found(self, url, func, args, mock_get, kwargs=None,
+ method='GET'):
+ req = webob.Request.blank(url)
+ req.method = method
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ if not kwargs:
+ kwargs = {}
+ self.assertRaises(exc.HTTPNotFound, func, req, *args, **kwargs)
+
+ def test_show_instance_not_found(self):
+ self._test_instance_not_found(self.url + 'fake',
+ self.attachments.show, ('fake', 'fake'))
+
+ def test_index_instance_not_found(self):
+ self._test_instance_not_found(self.url,
+ self.attachments.index, ('fake', ))
+
+ def test_detach_interface_instance_not_found(self):
+ self._test_instance_not_found(self.url + '/fake',
+ self.attachments.delete,
+ ('fake', 'fake'), method='DELETE')
+
+ def test_attach_interface_instance_not_found(self):
+ self._test_instance_not_found(
+ '/v2/fake/os-interfaces', self.attachments.create, ('fake', ),
+ kwargs={'body': {'interfaceAttachment': {}}}, method='POST')
+
+ def test_show(self):
+ req = webob.Request.blank(self.url + '/show')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ result = self.attachments.show(req, FAKE_UUID1, FAKE_PORT_ID1)
+ self.assertEqual(self.expected_show, result)
+
+ def test_show_invalid(self):
+ req = webob.Request.blank(self.url + '/show')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(exc.HTTPNotFound,
+ self.attachments.show, req, FAKE_UUID2,
+ FAKE_PORT_ID1)
+
+ @mock.patch.object(network_api.API, 'show_port',
+ side_effect=exception.Forbidden)
+ def test_show_forbidden(self, show_port_mock):
+ req = webob.Request.blank(self.url + '/show')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(exc.HTTPForbidden,
+ self.attachments.show, req, FAKE_UUID1,
+ FAKE_PORT_ID1)
+
+ def test_delete(self):
+ self.stubs.Set(compute_api.API, 'detach_interface',
+ fake_detach_interface)
+ req = webob.Request.blank(self.url + '/delete')
+ req.method = 'DELETE'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ result = self.attachments.delete(req, FAKE_UUID1, FAKE_PORT_ID1)
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.attachments,
+ attach_interfaces_v3.InterfaceAttachmentController):
+ status_int = self.attachments.delete.wsgi_code
+ else:
+ status_int = result.status_int
+ self.assertEqual(202, status_int)
+
+ def test_detach_interface_instance_locked(self):
+ def fake_detach_interface_from_locked_server(self, context,
+ instance, port_id):
+ raise exception.InstanceIsLocked(instance_uuid=FAKE_UUID1)
+
+ self.stubs.Set(compute_api.API,
+ 'detach_interface',
+ fake_detach_interface_from_locked_server)
+ req = webob.Request.blank(self.url + '/delete')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(exc.HTTPConflict,
+ self.attachments.delete,
+ req,
+ FAKE_UUID1,
+ FAKE_PORT_ID1)
+
+ def test_delete_interface_not_found(self):
+ self.stubs.Set(compute_api.API, 'detach_interface',
+ fake_detach_interface)
+ req = webob.Request.blank(self.url + '/delete')
+ req.method = 'DELETE'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(exc.HTTPNotFound,
+ self.attachments.delete,
+ req,
+ FAKE_UUID1,
+ 'invaid-port-id')
+
+ def test_attach_interface_instance_locked(self):
+ def fake_attach_interface_to_locked_server(self, context,
+ instance, network_id, port_id, requested_ip):
+ raise exception.InstanceIsLocked(instance_uuid=FAKE_UUID1)
+
+ self.stubs.Set(compute_api.API,
+ 'attach_interface',
+ fake_attach_interface_to_locked_server)
+ req = webob.Request.blank(self.url + '/attach')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ self.assertRaises(exc.HTTPConflict,
+ self.attachments.create, req, FAKE_UUID1,
+ body=jsonutils.loads(req.body))
+
+ def test_attach_interface_without_network_id(self):
+ self.stubs.Set(compute_api.API, 'attach_interface',
+ fake_attach_interface)
+ req = webob.Request.blank(self.url + '/attach')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ result = self.attachments.create(req, FAKE_UUID1,
+ body=jsonutils.loads(req.body))
+ self.assertEqual(result['interfaceAttachment']['net_id'],
+ FAKE_NET_ID1)
+
+ def test_attach_interface_with_network_id(self):
+ self.stubs.Set(compute_api.API, 'attach_interface',
+ fake_attach_interface)
+ req = webob.Request.blank(self.url + '/attach')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({'interfaceAttachment':
+ {'net_id': FAKE_NET_ID2}})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ result = self.attachments.create(req, FAKE_UUID1,
+ body=jsonutils.loads(req.body))
+ self.assertEqual(result['interfaceAttachment']['net_id'],
+ FAKE_NET_ID2)
+
+ def _attach_interface_bad_request_case(self, body):
+ self.stubs.Set(compute_api.API, 'attach_interface',
+ fake_attach_interface)
+ req = webob.Request.blank(self.url + '/attach')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ self.assertRaises(exc.HTTPBadRequest,
+ self.attachments.create, req, FAKE_UUID1,
+ body=jsonutils.loads(req.body))
+
+ def test_attach_interface_with_port_and_network_id(self):
+ body = {
+ 'interfaceAttachment': {
+ 'port_id': FAKE_PORT_ID1,
+ 'net_id': FAKE_NET_ID2
+ }
+ }
+ self._attach_interface_bad_request_case(body)
+
+ def test_attach_interface_with_invalid_data(self):
+ body = {
+ 'interfaceAttachment': {
+ 'net_id': FAKE_BAD_NET_ID
+ }
+ }
+ self._attach_interface_bad_request_case(body)
+
+ def test_attach_interface_with_invalid_state(self):
+ def fake_attach_interface_invalid_state(*args, **kwargs):
+ raise exception.InstanceInvalidState(
+ instance_uuid='', attr='', state='',
+ method='attach_interface')
+
+ self.stubs.Set(compute_api.API, 'attach_interface',
+ fake_attach_interface_invalid_state)
+ req = webob.Request.blank(self.url + '/attach')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({'interfaceAttachment':
+ {'net_id': FAKE_NET_ID1}})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ self.assertRaises(exc.HTTPConflict,
+ self.attachments.create, req, FAKE_UUID1,
+ body=jsonutils.loads(req.body))
+
+ def test_detach_interface_with_invalid_state(self):
+ def fake_detach_interface_invalid_state(*args, **kwargs):
+ raise exception.InstanceInvalidState(
+ instance_uuid='', attr='', state='',
+ method='detach_interface')
+
+ self.stubs.Set(compute_api.API, 'detach_interface',
+ fake_detach_interface_invalid_state)
+ req = webob.Request.blank(self.url + '/attach')
+ req.method = 'DELETE'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ self.assertRaises(exc.HTTPConflict,
+ self.attachments.delete,
+ req,
+ FAKE_UUID1,
+ FAKE_NET_ID1)
+
+ def test_attach_interface_invalid_fixed_ip(self):
+ req = webob.Request.blank(self.url + '/attach')
+ req.method = 'POST'
+ body = {
+ 'interfaceAttachment': {
+ 'net_id': FAKE_NET_ID1,
+ 'fixed_ips': [{'ip_address': 'invalid_ip'}]
+ }
+ }
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ self.assertRaises(self.validate_exc,
+ self.attachments.create, req, FAKE_UUID1,
+ body=jsonutils.loads(req.body))
+
+ @mock.patch.object(compute_api.API, 'get')
+ @mock.patch.object(compute_api.API, 'attach_interface')
+ def test_attach_interface_fixed_ip_already_in_use(self,
+ attach_mock,
+ get_mock):
+ fake_instance = objects.Instance(uuid=FAKE_UUID1)
+ get_mock.return_value = fake_instance
+ attach_mock.side_effect = exception.FixedIpAlreadyInUse(
+ address='10.0.2.2', instance_uuid=FAKE_UUID1)
+ req = webob.Request.blank(self.url + '/attach')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ self.assertRaises(exc.HTTPBadRequest,
+ self.attachments.create, req, FAKE_UUID1,
+ body=jsonutils.loads(req.body))
+ attach_mock.assert_called_once_with(self.context, fake_instance, None,
+ None, None)
+ get_mock.assert_called_once_with(self.context, FAKE_UUID1,
+ want_objects=True,
+ expected_attrs=None)
+
+ def _test_attach_interface_with_invalid_parameter(self, param):
+ self.stubs.Set(compute_api.API, 'attach_interface',
+ fake_attach_interface)
+ req = webob.Request.blank(self.url + '/attach')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({'interface_attachment': param})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ self.assertRaises(exception.ValidationError,
+ self.attachments.create, req, FAKE_UUID1,
+ body=jsonutils.loads(req.body))
+
+ def test_attach_interface_instance_with_non_uuid_net_id(self):
+ param = {'net_id': 'non_uuid'}
+ self._test_attach_interface_with_invalid_parameter(param)
+
+ def test_attach_interface_instance_with_non_uuid_port_id(self):
+ param = {'port_id': 'non_uuid'}
+ self._test_attach_interface_with_invalid_parameter(param)
+
+ def test_attach_interface_instance_with_non_array_fixed_ips(self):
+ param = {'fixed_ips': 'non_array'}
+ self._test_attach_interface_with_invalid_parameter(param)
+
+
+class InterfaceAttachTestsV2(InterfaceAttachTestsV21):
+ url = '/v2/fake/os-interfaces'
+ controller_cls = attach_interfaces_v2.InterfaceAttachmentController
+ validate_exc = exc.HTTPBadRequest
+
+ def test_attach_interface_instance_with_non_uuid_net_id(self):
+ pass
+
+ def test_attach_interface_instance_with_non_uuid_port_id(self):
+ pass
+
+ def test_attach_interface_instance_with_non_array_fixed_ips(self):
+ pass
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_availability_zone.py b/nova/tests/unit/api/openstack/compute/contrib/test_availability_zone.py
new file mode 100644
index 0000000000..31b20d6861
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_availability_zone.py
@@ -0,0 +1,512 @@
+# Copyright 2012 IBM Corp.
+#
+# 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 datetime
+
+from lxml import etree
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import availability_zone as az_v2
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import availability_zone as az_v21
+from nova.api.openstack.compute.plugins.v3 import servers as servers_v21
+from nova.api.openstack.compute import servers as servers_v2
+from nova.api.openstack import extensions
+from nova import availability_zones
+from nova.compute import api as compute_api
+from nova.compute import flavors
+from nova import context
+from nova import db
+from nova import exception
+from nova import servicegroup
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+from nova.tests.unit.image import fake
+from nova.tests.unit import matchers
+from nova.tests.unit.objects import test_service
+
+FAKE_UUID = fakes.FAKE_UUID
+
+
+def fake_service_get_all(context, disabled=None):
+ def __fake_service(binary, availability_zone,
+ created_at, updated_at, host, disabled):
+ return dict(test_service.fake_service,
+ binary=binary,
+ availability_zone=availability_zone,
+ available_zones=availability_zone,
+ created_at=created_at,
+ updated_at=updated_at,
+ host=host,
+ disabled=disabled)
+
+ if disabled:
+ return [__fake_service("nova-compute", "zone-2",
+ datetime.datetime(2012, 11, 14, 9, 53, 25, 0),
+ datetime.datetime(2012, 12, 26, 14, 45, 25, 0),
+ "fake_host-1", True),
+ __fake_service("nova-scheduler", "internal",
+ datetime.datetime(2012, 11, 14, 9, 57, 3, 0),
+ datetime.datetime(2012, 12, 26, 14, 45, 25, 0),
+ "fake_host-1", True),
+ __fake_service("nova-network", "internal",
+ datetime.datetime(2012, 11, 16, 7, 25, 46, 0),
+ datetime.datetime(2012, 12, 26, 14, 45, 24, 0),
+ "fake_host-2", True)]
+ else:
+ return [__fake_service("nova-compute", "zone-1",
+ datetime.datetime(2012, 11, 14, 9, 53, 25, 0),
+ datetime.datetime(2012, 12, 26, 14, 45, 25, 0),
+ "fake_host-1", False),
+ __fake_service("nova-sched", "internal",
+ datetime.datetime(2012, 11, 14, 9, 57, 3, 0),
+ datetime.datetime(2012, 12, 26, 14, 45, 25, 0),
+ "fake_host-1", False),
+ __fake_service("nova-network", "internal",
+ datetime.datetime(2012, 11, 16, 7, 25, 46, 0),
+ datetime.datetime(2012, 12, 26, 14, 45, 24, 0),
+ "fake_host-2", False)]
+
+
+def fake_service_is_up(self, service):
+ return service['binary'] != u"nova-network"
+
+
+def fake_set_availability_zones(context, services):
+ return services
+
+
+def fake_get_availability_zones(context):
+ return ['nova'], []
+
+
+CONF = cfg.CONF
+
+
+class AvailabilityZoneApiTestV21(test.NoDBTestCase):
+ availability_zone = az_v21
+ url = '/v2/fake/os-availability-zone'
+
+ def setUp(self):
+ super(AvailabilityZoneApiTestV21, self).setUp()
+ availability_zones.reset_cache()
+ self.stubs.Set(db, 'service_get_all', fake_service_get_all)
+ self.stubs.Set(availability_zones, 'set_availability_zones',
+ fake_set_availability_zones)
+ self.stubs.Set(servicegroup.API, 'service_is_up', fake_service_is_up)
+
+ def _get_wsgi_instance(self):
+ return fakes.wsgi_app_v21(init_only=('os-availability-zone',
+ 'servers'))
+
+ def test_filtered_availability_zones(self):
+ az = self.availability_zone.AvailabilityZoneController()
+ zones = ['zone1', 'internal']
+ expected = [{'zoneName': 'zone1',
+ 'zoneState': {'available': True},
+ "hosts": None}]
+ result = az._get_filtered_availability_zones(zones, True)
+ self.assertEqual(result, expected)
+
+ expected = [{'zoneName': 'zone1',
+ 'zoneState': {'available': False},
+ "hosts": None}]
+ result = az._get_filtered_availability_zones(zones, False)
+ self.assertEqual(result, expected)
+
+ def test_availability_zone_index(self):
+ req = webob.Request.blank(self.url)
+ resp = req.get_response(self._get_wsgi_instance())
+ self.assertEqual(resp.status_int, 200)
+ resp_dict = jsonutils.loads(resp.body)
+
+ self.assertIn('availabilityZoneInfo', resp_dict)
+ zones = resp_dict['availabilityZoneInfo']
+ self.assertEqual(len(zones), 2)
+ self.assertEqual(zones[0]['zoneName'], u'zone-1')
+ self.assertTrue(zones[0]['zoneState']['available'])
+ self.assertIsNone(zones[0]['hosts'])
+ self.assertEqual(zones[1]['zoneName'], u'zone-2')
+ self.assertFalse(zones[1]['zoneState']['available'])
+ self.assertIsNone(zones[1]['hosts'])
+
+ def test_availability_zone_detail(self):
+ def _formatZone(zone_dict):
+ result = []
+
+ # Zone tree view item
+ result.append({'zoneName': zone_dict['zoneName'],
+ 'zoneState': u'available'
+ if zone_dict['zoneState']['available'] else
+ u'not available'})
+
+ if zone_dict['hosts'] is not None:
+ for (host, services) in zone_dict['hosts'].items():
+ # Host tree view item
+ result.append({'zoneName': u'|- %s' % host,
+ 'zoneState': u''})
+ for (svc, state) in services.items():
+ # Service tree view item
+ result.append({'zoneName': u'| |- %s' % svc,
+ 'zoneState': u'%s %s %s' % (
+ 'enabled' if state['active'] else
+ 'disabled',
+ ':-)' if state['available'] else
+ 'XXX',
+ jsonutils.to_primitive(
+ state['updated_at']))})
+ return result
+
+ def _assertZone(zone, name, status):
+ self.assertEqual(zone['zoneName'], name)
+ self.assertEqual(zone['zoneState'], status)
+
+ availabilityZone = self.availability_zone.AvailabilityZoneController()
+
+ req_url = self.url + '/detail'
+ req = webob.Request.blank(req_url)
+ req.method = 'GET'
+ req.environ['nova.context'] = context.get_admin_context()
+ resp_dict = availabilityZone.detail(req)
+
+ self.assertIn('availabilityZoneInfo', resp_dict)
+ zones = resp_dict['availabilityZoneInfo']
+ self.assertEqual(len(zones), 3)
+
+ ''' availabilityZoneInfo field content in response body:
+ [{'zoneName': 'zone-1',
+ 'zoneState': {'available': True},
+ 'hosts': {'fake_host-1': {
+ 'nova-compute': {'active': True, 'available': True,
+ 'updated_at': datetime(2012, 12, 26, 14, 45, 25)}}}},
+ {'zoneName': 'internal',
+ 'zoneState': {'available': True},
+ 'hosts': {'fake_host-1': {
+ 'nova-sched': {'active': True, 'available': True,
+ 'updated_at': datetime(2012, 12, 26, 14, 45, 25)}},
+ 'fake_host-2': {
+ 'nova-network': {'active': True, 'available': False,
+ 'updated_at': datetime(2012, 12, 26, 14, 45, 24)}}}},
+ {'zoneName': 'zone-2',
+ 'zoneState': {'available': False},
+ 'hosts': None}]
+ '''
+
+ l0 = [u'zone-1', u'available']
+ l1 = [u'|- fake_host-1', u'']
+ l2 = [u'| |- nova-compute', u'enabled :-) 2012-12-26T14:45:25.000000']
+ l3 = [u'internal', u'available']
+ l4 = [u'|- fake_host-1', u'']
+ l5 = [u'| |- nova-sched', u'enabled :-) 2012-12-26T14:45:25.000000']
+ l6 = [u'|- fake_host-2', u'']
+ l7 = [u'| |- nova-network', u'enabled XXX 2012-12-26T14:45:24.000000']
+ l8 = [u'zone-2', u'not available']
+
+ z0 = _formatZone(zones[0])
+ z1 = _formatZone(zones[1])
+ z2 = _formatZone(zones[2])
+
+ self.assertEqual(len(z0), 3)
+ self.assertEqual(len(z1), 5)
+ self.assertEqual(len(z2), 1)
+
+ _assertZone(z0[0], l0[0], l0[1])
+ _assertZone(z0[1], l1[0], l1[1])
+ _assertZone(z0[2], l2[0], l2[1])
+ _assertZone(z1[0], l3[0], l3[1])
+ _assertZone(z1[1], l4[0], l4[1])
+ _assertZone(z1[2], l5[0], l5[1])
+ _assertZone(z1[3], l6[0], l6[1])
+ _assertZone(z1[4], l7[0], l7[1])
+ _assertZone(z2[0], l8[0], l8[1])
+
+ def test_availability_zone_detail_no_services(self):
+ expected_response = {'availabilityZoneInfo':
+ [{'zoneState': {'available': True},
+ 'hosts': {},
+ 'zoneName': 'nova'}]}
+ self.stubs.Set(availability_zones, 'get_availability_zones',
+ fake_get_availability_zones)
+ availabilityZone = self.availability_zone.AvailabilityZoneController()
+
+ req_url = self.url + '/detail'
+ req = webob.Request.blank(req_url)
+ req.method = 'GET'
+ req.environ['nova.context'] = context.get_admin_context()
+ resp_dict = availabilityZone.detail(req)
+
+ self.assertThat(resp_dict,
+ matchers.DictMatches(expected_response))
+
+
+class AvailabilityZoneApiTestV2(AvailabilityZoneApiTestV21):
+ availability_zone = az_v2
+
+ def _get_wsgi_instance(self):
+ return fakes.wsgi_app()
+
+
+class ServersControllerCreateTestV21(test.TestCase):
+ base_url = '/v2/fake/'
+
+ def setUp(self):
+ """Shared implementation for tests below that create instance."""
+ super(ServersControllerCreateTestV21, self).setUp()
+
+ self.instance_cache_num = 0
+
+ self._set_up_controller()
+
+ def instance_create(context, inst):
+ inst_type = flavors.get_flavor_by_flavor_id(3)
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ def_image_ref = 'http://localhost/images/%s' % image_uuid
+ self.instance_cache_num += 1
+ instance = fake_instance.fake_db_instance(**{
+ 'id': self.instance_cache_num,
+ 'display_name': inst['display_name'] or 'test',
+ 'uuid': FAKE_UUID,
+ 'instance_type': inst_type,
+ 'access_ip_v4': '1.2.3.4',
+ 'access_ip_v6': 'fead::1234',
+ 'image_ref': inst.get('image_ref', def_image_ref),
+ 'user_id': 'fake',
+ 'project_id': 'fake',
+ 'availability_zone': 'nova',
+ 'reservation_id': inst['reservation_id'],
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "progress": 0,
+ "fixed_ips": [],
+ "task_state": "",
+ "vm_state": "",
+ "root_device_name": inst.get('root_device_name', 'vda'),
+ })
+
+ return instance
+
+ fake.stub_out_image_service(self.stubs)
+ self.stubs.Set(db, 'instance_create', instance_create)
+
+ def _set_up_controller(self):
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers_v21.ServersController(
+ extension_info=ext_info)
+ CONF.set_override('extensions_blacklist',
+ 'os-availability-zone',
+ 'osapi_v3')
+ self.no_availability_zone_controller = servers_v21.ServersController(
+ extension_info=ext_info)
+
+ def _verify_no_availability_zone(self, **kwargs):
+ self.assertNotIn('availability_zone', kwargs)
+
+ def _test_create_extra(self, params, controller):
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ server = dict(name='server_test', imageRef=image_uuid, flavorRef=2)
+ server.update(params)
+ body = dict(server=server)
+ req = fakes.HTTPRequest.blank(self.base_url + 'servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ server = controller.create(req, body=body).obj['server']
+
+ def test_create_instance_with_availability_zone_disabled(self):
+ params = {'availability_zone': 'foo'}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self._verify_no_availability_zone(**kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params, self.no_availability_zone_controller)
+
+ def _create_instance_with_availability_zone(self, zone_name):
+ def create(*args, **kwargs):
+ self.assertIn('availability_zone', kwargs)
+ self.assertEqual('nova', kwargs['availability_zone'])
+ return old_create(*args, **kwargs)
+
+ old_create = compute_api.API.create
+ self.stubs.Set(compute_api.API, 'create', create)
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = ('http://localhost' + self.base_url + 'flavors/3')
+ body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ 'availability_zone': zone_name,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.base_url + 'servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ admin_context = context.get_admin_context()
+ db.service_create(admin_context, {'host': 'host1_zones',
+ 'binary': "nova-compute",
+ 'topic': 'compute',
+ 'report_count': 0})
+ agg = db.aggregate_create(admin_context,
+ {'name': 'agg1'}, {'availability_zone': 'nova'})
+ db.aggregate_host_add(admin_context, agg['id'], 'host1_zones')
+ return req, body
+
+ def test_create_instance_with_availability_zone(self):
+ zone_name = 'nova'
+ req, body = self._create_instance_with_availability_zone(zone_name)
+ res = self.controller.create(req, body=body).obj
+ server = res['server']
+ self.assertEqual(fakes.FAKE_UUID, server['id'])
+
+ def test_create_instance_with_invalid_availability_zone_too_long(self):
+ zone_name = 'a' * 256
+ req, body = self._create_instance_with_availability_zone(zone_name)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, req, body=body)
+
+ def test_create_instance_with_invalid_availability_zone_too_short(self):
+ zone_name = ''
+ req, body = self._create_instance_with_availability_zone(zone_name)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, req, body=body)
+
+ def test_create_instance_with_invalid_availability_zone_not_str(self):
+ zone_name = 111
+ req, body = self._create_instance_with_availability_zone(zone_name)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, req, body=body)
+
+ def test_create_instance_without_availability_zone(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = ('http://localhost' + self.base_url + 'flavors/3')
+ body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.base_url + 'servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = self.controller.create(req, body=body).obj
+ server = res['server']
+ self.assertEqual(fakes.FAKE_UUID, server['id'])
+
+
+class ServersControllerCreateTestV2(ServersControllerCreateTestV21):
+
+ def _set_up_controller(self):
+ ext_mgr = extensions.ExtensionManager()
+ ext_mgr.extensions = {'os-availability-zone': 'fake'}
+ self.controller = servers_v2.Controller(ext_mgr)
+ ext_mgr_no_az = extensions.ExtensionManager()
+ ext_mgr_no_az.extensions = {}
+ self.no_availability_zone_controller = servers_v2.Controller(
+ ext_mgr_no_az)
+
+ def _verify_no_availability_zone(self, **kwargs):
+ self.assertIsNone(kwargs['availability_zone'])
+
+ def test_create_instance_with_invalid_availability_zone_too_long(self):
+ # NOTE: v2.0 API does not check this bad request case.
+ # So we skip this test for v2.0 API.
+ pass
+
+ def test_create_instance_with_invalid_availability_zone_too_short(self):
+ # NOTE: v2.0 API does not check this bad request case.
+ # So we skip this test for v2.0 API.
+ pass
+
+ def test_create_instance_with_invalid_availability_zone_not_str(self):
+ # NOTE: v2.0 API does not check this bad request case.
+ # So we skip this test for v2.0 API.
+ pass
+
+
+class AvailabilityZoneSerializerTest(test.NoDBTestCase):
+ def test_availability_zone_index_detail_serializer(self):
+ def _verify_zone(zone_dict, tree):
+ self.assertEqual(tree.tag, 'availabilityZone')
+ self.assertEqual(zone_dict['zoneName'], tree.get('name'))
+ self.assertEqual(str(zone_dict['zoneState']['available']),
+ tree[0].get('available'))
+
+ for _idx, host_child in enumerate(tree[1]):
+ self.assertIn(host_child.get('name'), zone_dict['hosts'])
+ svcs = zone_dict['hosts'][host_child.get('name')]
+ for _idx, svc_child in enumerate(host_child[0]):
+ self.assertIn(svc_child.get('name'), svcs)
+ svc = svcs[svc_child.get('name')]
+ self.assertEqual(len(svc_child), 1)
+
+ self.assertEqual(str(svc['available']),
+ svc_child[0].get('available'))
+ self.assertEqual(str(svc['active']),
+ svc_child[0].get('active'))
+ self.assertEqual(str(svc['updated_at']),
+ svc_child[0].get('updated_at'))
+
+ serializer = az_v2.AvailabilityZonesTemplate()
+ raw_availability_zones = \
+ [{'zoneName': 'zone-1',
+ 'zoneState': {'available': True},
+ 'hosts': {'fake_host-1': {
+ 'nova-compute': {'active': True, 'available': True,
+ 'updated_at':
+ datetime.datetime(
+ 2012, 12, 26, 14, 45, 25)}}}},
+ {'zoneName': 'internal',
+ 'zoneState': {'available': True},
+ 'hosts': {'fake_host-1': {
+ 'nova-sched': {'active': True, 'available': True,
+ 'updated_at':
+ datetime.datetime(
+ 2012, 12, 26, 14, 45, 25)}},
+ 'fake_host-2': {
+ 'nova-network': {'active': True,
+ 'available': False,
+ 'updated_at':
+ datetime.datetime(
+ 2012, 12, 26, 14, 45, 24)}}}},
+ {'zoneName': 'zone-2',
+ 'zoneState': {'available': False},
+ 'hosts': None}]
+
+ text = serializer.serialize(
+ dict(availabilityZoneInfo=raw_availability_zones))
+ tree = etree.fromstring(text)
+
+ self.assertEqual('availabilityZones', tree.tag)
+ self.assertEqual(len(raw_availability_zones), len(tree))
+ for idx, child in enumerate(tree):
+ _verify_zone(raw_availability_zones[idx], child)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_baremetal_nodes.py b/nova/tests/unit/api/openstack/compute/contrib/test_baremetal_nodes.py
new file mode 100644
index 0000000000..451c92a40b
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_baremetal_nodes.py
@@ -0,0 +1,159 @@
+# Copyright (c) 2013 NTT DOCOMO, INC.
+# 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 mock
+from webob import exc
+
+from nova.api.openstack.compute.contrib import baremetal_nodes as b_nodes_v2
+from nova.api.openstack.compute.plugins.v3 import baremetal_nodes \
+ as b_nodes_v21
+from nova.api.openstack import extensions
+from nova import context
+from nova import test
+from nova.tests.unit.virt.ironic import utils as ironic_utils
+
+
+class FakeRequest(object):
+
+ def __init__(self, context):
+ self.environ = {"nova.context": context}
+
+
+def fake_node(**updates):
+ node = {
+ 'id': 1,
+ 'service_host': "host",
+ 'cpus': 8,
+ 'memory_mb': 8192,
+ 'local_gb': 128,
+ 'pm_address': "10.1.2.3",
+ 'pm_user': "pm_user",
+ 'pm_password': "pm_pass",
+ 'terminal_port': 8000,
+ 'interfaces': [],
+ 'instance_uuid': 'fake-instance-uuid',
+ }
+ if updates:
+ node.update(updates)
+ return node
+
+
+def fake_node_ext_status(**updates):
+ node = fake_node(uuid='fake-uuid',
+ task_state='fake-task-state',
+ updated_at='fake-updated-at',
+ pxe_config_path='fake-pxe-config-path')
+ if updates:
+ node.update(updates)
+ return node
+
+
+FAKE_IRONIC_CLIENT = ironic_utils.FakeClient()
+
+
+@mock.patch.object(b_nodes_v21, '_get_ironic_client',
+ lambda *_: FAKE_IRONIC_CLIENT)
+class BareMetalNodesTestV21(test.NoDBTestCase):
+ def setUp(self):
+ super(BareMetalNodesTestV21, self).setUp()
+
+ self._setup()
+ self.context = context.get_admin_context()
+ self.request = FakeRequest(self.context)
+
+ def _setup(self):
+ self.controller = b_nodes_v21.BareMetalNodeController()
+
+ @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list')
+ def test_index_ironic(self, mock_list):
+ properties = {'cpus': 2, 'memory_mb': 1024, 'local_gb': 20}
+ node = ironic_utils.get_test_node(properties=properties)
+ mock_list.return_value = [node]
+
+ res_dict = self.controller.index(self.request)
+ expected_output = {'nodes':
+ [{'memory_mb': properties['memory_mb'],
+ 'host': 'IRONIC MANAGED',
+ 'disk_gb': properties['local_gb'],
+ 'interfaces': [],
+ 'task_state': None,
+ 'id': node.uuid,
+ 'cpus': properties['cpus']}]}
+ self.assertEqual(expected_output, res_dict)
+ mock_list.assert_called_once_with(detail=True)
+
+ @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list_ports')
+ @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'get')
+ def test_show_ironic(self, mock_get, mock_list_ports):
+ properties = {'cpus': 1, 'memory_mb': 512, 'local_gb': 10}
+ node = ironic_utils.get_test_node(properties=properties)
+ port = ironic_utils.get_test_port()
+ mock_get.return_value = node
+ mock_list_ports.return_value = [port]
+
+ res_dict = self.controller.show(self.request, node.uuid)
+ expected_output = {'node':
+ {'memory_mb': properties['memory_mb'],
+ 'instance_uuid': None,
+ 'host': 'IRONIC MANAGED',
+ 'disk_gb': properties['local_gb'],
+ 'interfaces': [{'address': port.address}],
+ 'task_state': None,
+ 'id': node.uuid,
+ 'cpus': properties['cpus']}}
+ self.assertEqual(expected_output, res_dict)
+ mock_get.assert_called_once_with(node.uuid)
+ mock_list_ports.assert_called_once_with(node.uuid)
+
+ @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list_ports')
+ @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'get')
+ def test_show_ironic_no_interfaces(self, mock_get, mock_list_ports):
+ properties = {'cpus': 1, 'memory_mb': 512, 'local_gb': 10}
+ node = ironic_utils.get_test_node(properties=properties)
+ mock_get.return_value = node
+ mock_list_ports.return_value = []
+
+ res_dict = self.controller.show(self.request, node.uuid)
+ self.assertEqual([], res_dict['node']['interfaces'])
+ mock_get.assert_called_once_with(node.uuid)
+ mock_list_ports.assert_called_once_with(node.uuid)
+
+ def test_create_ironic_not_supported(self):
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create,
+ self.request, {'node': object()})
+
+ def test_delete_ironic_not_supported(self):
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.delete,
+ self.request, 'fake-id')
+
+ def test_add_interface_ironic_not_supported(self):
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller._add_interface,
+ self.request, 'fake-id', 'fake-body')
+
+ def test_remove_interface_ironic_not_supported(self):
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller._remove_interface,
+ self.request, 'fake-id', 'fake-body')
+
+
+@mock.patch.object(b_nodes_v2, '_get_ironic_client',
+ lambda *_: FAKE_IRONIC_CLIENT)
+class BareMetalNodesTestV2(BareMetalNodesTestV21):
+ def _setup(self):
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.controller = b_nodes_v2.BareMetalNodeController(self.ext_mgr)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping.py b/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping.py
new file mode 100644
index 0000000000..ab20ad85c3
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping.py
@@ -0,0 +1,359 @@
+# Copyright 2013 OpenStack Foundation
+# 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 mock
+import mox
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+from webob import exc
+
+from nova.api.openstack.compute import extensions
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import block_device_mapping
+from nova.api.openstack.compute.plugins.v3 import servers as servers_v3
+from nova.api.openstack.compute import servers as servers_v2
+from nova import block_device
+from nova.compute import api as compute_api
+from nova import exception
+from nova import objects
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.image import fake
+from nova.tests.unit import matchers
+
+CONF = cfg.CONF
+
+
+class BlockDeviceMappingTestV21(test.TestCase):
+
+ def _setup_controller(self):
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers_v3.ServersController(extension_info=ext_info)
+ CONF.set_override('extensions_blacklist', 'os-block-device-mapping',
+ 'osapi_v3')
+ self.no_bdm_v2_controller = servers_v3.ServersController(
+ extension_info=ext_info)
+ CONF.set_override('extensions_blacklist', '', 'osapi_v3')
+
+ def setUp(self):
+ super(BlockDeviceMappingTestV21, self).setUp()
+ self._setup_controller()
+ fake.stub_out_image_service(self.stubs)
+
+ self.bdm = [{
+ 'no_device': None,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'uuid': 'fake',
+ 'device_name': 'vda',
+ 'delete_on_termination': False,
+ }]
+
+ def _get_servers_body(self, no_image=False):
+ body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ 'flavorRef': 'http://localhost/123/flavors/3',
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ },
+ }
+ if no_image:
+ del body['server']['imageRef']
+ return body
+
+ def _test_create(self, params, no_image=False, override_controller=None):
+ body = self._get_servers_body(no_image)
+ body['server'].update(params)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers')
+ req.method = 'POST'
+ req.headers['content-type'] = 'application/json'
+
+ req.body = jsonutils.dumps(body)
+
+ if override_controller:
+ override_controller.create(req, body=body).obj['server']
+ else:
+ self.controller.create(req, body=body).obj['server']
+
+ def test_create_instance_with_block_device_mapping_disabled(self):
+ bdm = [{'device_name': 'foo'}]
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertNotIn('block_device_mapping', kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: bdm}
+ self._test_create(params,
+ override_controller=self.no_bdm_v2_controller)
+
+ def test_create_instance_with_volumes_enabled_no_image(self):
+ """Test that the create will fail if there is no image
+ and no bdms supplied in the request
+ """
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertNotIn('imageRef', kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, {}, no_image=True)
+
+ def test_create_instance_with_bdms_and_no_image(self):
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertThat(
+ block_device.BlockDeviceDict(self.bdm[0]),
+ matchers.DictMatches(kwargs['block_device_mapping'][0])
+ )
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ self.mox.StubOutWithMock(compute_api.API, '_validate_bdm')
+ self.mox.StubOutWithMock(compute_api.API, '_get_bdm_image_metadata')
+
+ compute_api.API._validate_bdm(
+ mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg(),
+ mox.IgnoreArg()).AndReturn(True)
+ compute_api.API._get_bdm_image_metadata(
+ mox.IgnoreArg(), mox.IgnoreArg(), False).AndReturn({})
+ self.mox.ReplayAll()
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
+ self._test_create(params, no_image=True)
+
+ def test_create_instance_with_device_name_not_string(self):
+ self.bdm[0]['device_name'] = 123
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params, no_image=True)
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_instance_with_bdm_param_not_list(self, mock_create):
+ self.params = {'block_device_mapping': '/dev/vdb'}
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, self.params)
+
+ def test_create_instance_with_device_name_empty(self):
+ self.bdm[0]['device_name'] = ''
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params, no_image=True)
+
+ def test_create_instance_with_device_name_too_long(self):
+ self.bdm[0]['device_name'] = 'a' * 256
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params, no_image=True)
+
+ def test_create_instance_with_space_in_device_name(self):
+ self.bdm[0]['device_name'] = 'v da'
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertTrue(kwargs['legacy_bdm'])
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params, no_image=True)
+
+ def test_create_instance_with_invalid_size(self):
+ self.bdm[0]['volume_size'] = 'hello world'
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params, no_image=True)
+
+ def test_create_instance_bdm(self):
+ bdm = [{
+ 'source_type': 'volume',
+ 'device_name': 'fake_dev',
+ 'uuid': 'fake_vol'
+ }]
+ bdm_expected = [{
+ 'source_type': 'volume',
+ 'device_name': 'fake_dev',
+ 'volume_id': 'fake_vol'
+ }]
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertFalse(kwargs['legacy_bdm'])
+ for expected, received in zip(bdm_expected,
+ kwargs['block_device_mapping']):
+ self.assertThat(block_device.BlockDeviceDict(expected),
+ matchers.DictMatches(received))
+ return old_create(*args, **kwargs)
+
+ def _validate_bdm(*args, **kwargs):
+ pass
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: bdm}
+ self._test_create(params, no_image=True)
+
+ def test_create_instance_bdm_missing_device_name(self):
+ del self.bdm[0]['device_name']
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertFalse(kwargs['legacy_bdm'])
+ self.assertNotIn(None,
+ kwargs['block_device_mapping'][0]['device_name'])
+ return old_create(*args, **kwargs)
+
+ def _validate_bdm(*args, **kwargs):
+ pass
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
+ self._test_create(params, no_image=True)
+
+ def test_create_instance_bdm_validation_error(self):
+ def _validate(*args, **kwargs):
+ raise exception.InvalidBDMFormat(details='Wrong BDM')
+
+ self.stubs.Set(block_device.BlockDeviceDict,
+ '_validate', _validate)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params, no_image=True)
+
+ @mock.patch('nova.compute.api.API._get_bdm_image_metadata')
+ def test_create_instance_non_bootable_volume_fails(self, fake_bdm_meta):
+ params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
+ fake_bdm_meta.side_effect = exception.InvalidBDMVolumeNotBootable(id=1)
+ self.assertRaises(exc.HTTPBadRequest, self._test_create, params,
+ no_image=True)
+
+ def test_create_instance_bdm_api_validation_fails(self):
+ self.validation_fail_test_validate_called = False
+ self.validation_fail_instance_destroy_called = False
+
+ bdm_exceptions = ((exception.InvalidBDMSnapshot, {'id': 'fake'}),
+ (exception.InvalidBDMVolume, {'id': 'fake'}),
+ (exception.InvalidBDMImage, {'id': 'fake'}),
+ (exception.InvalidBDMBootSequence, {}),
+ (exception.InvalidBDMLocalsLimit, {}))
+
+ ex_iter = iter(bdm_exceptions)
+
+ def _validate_bdm(*args, **kwargs):
+ self.validation_fail_test_validate_called = True
+ ex, kargs = ex_iter.next()
+ raise ex(**kargs)
+
+ def _instance_destroy(*args, **kwargs):
+ self.validation_fail_instance_destroy_called = True
+
+ self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm)
+ self.stubs.Set(objects.Instance, 'destroy', _instance_destroy)
+
+ for _unused in xrange(len(bdm_exceptions)):
+ params = {block_device_mapping.ATTRIBUTE_NAME:
+ [self.bdm[0].copy()]}
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params)
+ self.assertTrue(self.validation_fail_test_validate_called)
+ self.assertTrue(self.validation_fail_instance_destroy_called)
+ self.validation_fail_test_validate_called = False
+ self.validation_fail_instance_destroy_called = False
+
+
+class BlockDeviceMappingTestV2(BlockDeviceMappingTestV21):
+
+ def _setup_controller(self):
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {'os-volumes': 'fake',
+ 'os-block-device-mapping-v2-boot': 'fake'}
+ self.controller = servers_v2.Controller(self.ext_mgr)
+ self.ext_mgr_bdm_v2 = extensions.ExtensionManager()
+ self.ext_mgr_bdm_v2.extensions = {'os-volumes': 'fake'}
+ self.no_bdm_v2_controller = servers_v2.Controller(
+ self.ext_mgr_bdm_v2)
+
+ def test_create_instance_with_block_device_mapping_disabled(self):
+ bdm = [{'device_name': 'foo'}]
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIsNone(kwargs['block_device_mapping'], None)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ params = {block_device_mapping.ATTRIBUTE_NAME: bdm}
+ self._test_create(params,
+ override_controller=self.no_bdm_v2_controller)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping_v1.py b/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping_v1.py
new file mode 100644
index 0000000000..2f73f00952
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping_v1.py
@@ -0,0 +1,421 @@
+# Copyright (c) 2014 IBM Corp.
+# 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 mock
+import mox
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+from webob import exc
+
+from nova.api.openstack.compute import extensions
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import block_device_mapping_v1 as \
+ block_device_mapping
+from nova.api.openstack.compute.plugins.v3 import servers as servers_v3
+from nova.api.openstack.compute import servers as servers_v2
+from nova.compute import api as compute_api
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.image import fake
+
+CONF = cfg.CONF
+
+
+class BlockDeviceMappingTestV21(test.TestCase):
+
+ def _setup_controller(self):
+ ext_info = plugins.LoadedExtensionInfo()
+ CONF.set_override('extensions_blacklist', 'os-block-device-mapping',
+ 'osapi_v3')
+ self.controller = servers_v3.ServersController(extension_info=ext_info)
+ CONF.set_override('extensions_blacklist',
+ ['os-block-device-mapping-v1',
+ 'os-block-device-mapping'],
+ 'osapi_v3')
+ self.no_volumes_controller = servers_v3.ServersController(
+ extension_info=ext_info)
+ CONF.set_override('extensions_blacklist', '', 'osapi_v3')
+
+ def setUp(self):
+ super(BlockDeviceMappingTestV21, self).setUp()
+ self._setup_controller()
+ fake.stub_out_image_service(self.stubs)
+ self.volume_id = fakes.FAKE_UUID
+ self.bdm = [{
+ 'id': 1,
+ 'no_device': None,
+ 'virtual_name': None,
+ 'snapshot_id': None,
+ 'volume_id': self.volume_id,
+ 'status': 'active',
+ 'device_name': 'vda',
+ 'delete_on_termination': False,
+ 'volume_image_metadata':
+ {'test_key': 'test_value'}
+ }]
+
+ def _get_servers_body(self, no_image=False):
+ body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ 'flavorRef': 'http://localhost/123/flavors/3',
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ },
+ }
+ if no_image:
+ del body['server']['imageRef']
+ return body
+
+ def _test_create(self, params, no_image=False, override_controller=None):
+ body = self._get_servers_body(no_image)
+ body['server'].update(params)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers')
+ req.method = 'POST'
+ req.headers['content-type'] = 'application/json'
+
+ req.body = jsonutils.dumps(body)
+
+ if override_controller:
+ override_controller.create(req, body=body).obj['server']
+ else:
+ self.controller.create(req, body=body).obj['server']
+
+ def test_create_instance_with_volumes_enabled(self):
+ params = {'block_device_mapping': self.bdm}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ def _validate_bdm(*args, **kwargs):
+ pass
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm)
+ self._test_create(params)
+
+ def test_create_instance_with_volumes_enabled_and_bdms_no_image(self):
+ """Test that the create works if there is no image supplied but
+ os-volumes extension is enabled and bdms are supplied
+ """
+ self.mox.StubOutWithMock(compute_api.API, '_validate_bdm')
+ self.mox.StubOutWithMock(compute_api.API, '_get_bdm_image_metadata')
+ volume = self.bdm[0]
+ compute_api.API._validate_bdm(mox.IgnoreArg(),
+ mox.IgnoreArg(), mox.IgnoreArg(),
+ mox.IgnoreArg()).AndReturn(True)
+ compute_api.API._get_bdm_image_metadata(mox.IgnoreArg(),
+ self.bdm,
+ True).AndReturn(volume)
+ params = {'block_device_mapping': self.bdm}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ self.assertNotIn('imageRef', kwargs)
+ return old_create(*args, **kwargs)
+
+ def _validate_bdm(*args, **kwargs):
+ pass
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.mox.ReplayAll()
+ self._test_create(params, no_image=True)
+
+ def test_create_instance_with_volumes_disabled(self):
+ bdm = [{'device_name': 'foo'}]
+ params = {'block_device_mapping': bdm}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertNotIn(block_device_mapping, kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create(params,
+ override_controller=self.no_volumes_controller)
+
+ @mock.patch('nova.compute.api.API._get_bdm_image_metadata')
+ def test_create_instance_non_bootable_volume_fails(self, fake_bdm_meta):
+ bdm = [{
+ 'id': 1,
+ 'bootable': False,
+ 'volume_id': self.volume_id,
+ 'status': 'active',
+ 'device_name': 'vda',
+ }]
+ params = {'block_device_mapping': bdm}
+ fake_bdm_meta.side_effect = exception.InvalidBDMVolumeNotBootable(id=1)
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params, no_image=True)
+
+ def test_create_instance_with_device_name_not_string(self):
+ old_create = compute_api.API.create
+ self.params = {'block_device_mapping': self.bdm}
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, self.params)
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_instance_with_bdm_param_not_list(self, mock_create):
+ self.params = {'block_device_mapping': '/dev/vdb'}
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, self.params)
+
+ def test_create_instance_with_device_name_empty(self):
+ self.bdm[0]['device_name'] = ''
+ params = {'block_device_mapping': self.bdm}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params)
+
+ def test_create_instance_with_device_name_too_long(self):
+ self.bdm[0]['device_name'] = 'a' * 256,
+ params = {'block_device_mapping': self.bdm}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params)
+
+ def test_create_instance_with_space_in_device_name(self):
+ self.bdm[0]['device_name'] = 'vd a',
+ params = {'block_device_mapping': self.bdm}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertTrue(kwargs['legacy_bdm'])
+ self.assertEqual(kwargs['block_device_mapping'], self.bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params)
+
+ def test_create_instance_with_invalid_size(self):
+ bdm = [{'delete_on_termination': 1,
+ 'device_name': 'vda',
+ 'volume_size': "hello world",
+ 'volume_id': '11111111-1111-1111-1111-111111111111'}]
+ params = {'block_device_mapping': bdm}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['block_device_mapping'], bdm)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.assertRaises(exc.HTTPBadRequest,
+ self._test_create, params)
+
+ def test_create_instance_with_bdm_delete_on_termination(self):
+ bdm = [{'device_name': 'foo1', 'volume_id': 'fake_vol',
+ 'delete_on_termination': 1},
+ {'device_name': 'foo2', 'volume_id': 'fake_vol',
+ 'delete_on_termination': True},
+ {'device_name': 'foo3', 'volume_id': 'fake_vol',
+ 'delete_on_termination': 'invalid'},
+ {'device_name': 'foo4', 'volume_id': 'fake_vol',
+ 'delete_on_termination': 0},
+ {'device_name': 'foo5', 'volume_id': 'fake_vol',
+ 'delete_on_termination': False}]
+ expected_bdm = [
+ {'device_name': 'foo1', 'volume_id': 'fake_vol',
+ 'delete_on_termination': True},
+ {'device_name': 'foo2', 'volume_id': 'fake_vol',
+ 'delete_on_termination': True},
+ {'device_name': 'foo3', 'volume_id': 'fake_vol',
+ 'delete_on_termination': False},
+ {'device_name': 'foo4', 'volume_id': 'fake_vol',
+ 'delete_on_termination': False},
+ {'device_name': 'foo5', 'volume_id': 'fake_vol',
+ 'delete_on_termination': False}]
+ params = {'block_device_mapping': bdm}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(expected_bdm, kwargs['block_device_mapping'])
+ return old_create(*args, **kwargs)
+
+ def _validate_bdm(*args, **kwargs):
+ pass
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm)
+ self._test_create(params)
+
+ def test_create_instance_decide_format_legacy(self):
+ ext_info = plugins.LoadedExtensionInfo()
+ CONF.set_override('extensions_blacklist',
+ ['os-block-device-mapping',
+ 'os-block-device-mapping-v1'],
+ 'osapi_v3')
+ controller = servers_v3.ServersController(extension_info=ext_info)
+ bdm = [{'device_name': 'foo1',
+ 'volume_id': 'fake_vol',
+ 'delete_on_termination': 1}]
+
+ expected_legacy_flag = True
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ legacy_bdm = kwargs.get('legacy_bdm', True)
+ self.assertEqual(legacy_bdm, expected_legacy_flag)
+ return old_create(*args, **kwargs)
+
+ def _validate_bdm(*args, **kwargs):
+ pass
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.stubs.Set(compute_api.API, '_validate_bdm',
+ _validate_bdm)
+
+ self._test_create({}, override_controller=controller)
+
+ params = {'block_device_mapping': bdm}
+ self._test_create(params, override_controller=controller)
+
+ def test_create_instance_both_bdm_formats(self):
+ bdm = [{'device_name': 'foo'}]
+ bdm_v2 = [{'source_type': 'volume',
+ 'uuid': 'fake_vol'}]
+ params = {'block_device_mapping': bdm,
+ 'block_device_mapping_v2': bdm_v2}
+ self.assertRaises(exc.HTTPBadRequest, self._test_create, params)
+
+
+class BlockDeviceMappingTestV2(BlockDeviceMappingTestV21):
+
+ def _setup_controller(self):
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {'os-volumes': 'fake'}
+ self.controller = servers_v2.Controller(self.ext_mgr)
+ self.ext_mgr_no_vols = extensions.ExtensionManager()
+ self.ext_mgr_no_vols.extensions = {}
+ self.no_volumes_controller = servers_v2.Controller(
+ self.ext_mgr_no_vols)
+
+ def test_create_instance_with_volumes_disabled(self):
+ bdm = [{'device_name': 'foo'}]
+ params = {'block_device_mapping': bdm}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIsNone(kwargs['block_device_mapping'])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create(params,
+ override_controller=self.no_volumes_controller)
+
+ def test_create_instance_decide_format_legacy(self):
+ ext_mgr = extensions.ExtensionManager()
+ ext_mgr.extensions = {'os-volumes': 'fake',
+ 'os-block-device-mapping-v2-boot': 'fake'}
+ controller = servers_v2.Controller(self.ext_mgr)
+ bdm = [{'device_name': 'foo1',
+ 'volume_id': 'fake_vol',
+ 'delete_on_termination': 1}]
+
+ expected_legacy_flag = True
+
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ legacy_bdm = kwargs.get('legacy_bdm', True)
+ self.assertEqual(legacy_bdm, expected_legacy_flag)
+ return old_create(*args, **kwargs)
+
+ def _validate_bdm(*args, **kwargs):
+ pass
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.stubs.Set(compute_api.API, '_validate_bdm',
+ _validate_bdm)
+
+ self._test_create({}, override_controller=controller)
+
+ params = {'block_device_mapping': bdm}
+ self._test_create(params, override_controller=controller)
+
+
+class TestServerCreateRequestXMLDeserializer(test.TestCase):
+
+ def setUp(self):
+ super(TestServerCreateRequestXMLDeserializer, self).setUp()
+ self.deserializer = servers_v2.CreateDeserializer()
+
+ def test_request_with_block_device_mapping(self):
+ serial_request = """
+ <server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <block_device_mapping>
+ <mapping volume_id="7329b667-50c7-46a6-b913-cb2a09dfeee0"
+ device_name="/dev/vda" virtual_name="root"
+ delete_on_termination="False" />
+ <mapping snapshot_id="f31efb24-34d2-43e1-8b44-316052956a39"
+ device_name="/dev/vdb" virtual_name="ephemeral0"
+ delete_on_termination="False" />
+ <mapping device_name="/dev/vdc" no_device="True" />
+ </block_device_mapping>
+ </server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "block_device_mapping": [
+ {
+ "volume_id": "7329b667-50c7-46a6-b913-cb2a09dfeee0",
+ "device_name": "/dev/vda",
+ "virtual_name": "root",
+ "delete_on_termination": False,
+ },
+ {
+ "snapshot_id": "f31efb24-34d2-43e1-8b44-316052956a39",
+ "device_name": "/dev/vdb",
+ "virtual_name": "ephemeral0",
+ "delete_on_termination": False,
+ },
+ {
+ "device_name": "/dev/vdc",
+ "no_device": True,
+ },
+ ]
+ }}
+ self.assertEqual(request['body'], expected)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_cells.py b/nova/tests/unit/api/openstack/compute/contrib/test_cells.py
new file mode 100644
index 0000000000..1460d33e3a
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_cells.py
@@ -0,0 +1,698 @@
+# Copyright 2011-2012 OpenStack Foundation
+# 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 copy
+
+from lxml import etree
+from oslo.utils import timeutils
+from webob import exc
+
+from nova.api.openstack.compute.contrib import cells as cells_ext_v2
+from nova.api.openstack.compute.plugins.v3 import cells as cells_ext_v21
+from nova.api.openstack import extensions
+from nova.api.openstack import xmlutil
+from nova.cells import rpcapi as cells_rpcapi
+from nova import context
+from nova import exception
+from nova import rpc
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import utils
+
+
+class BaseCellsTest(test.NoDBTestCase):
+ def setUp(self):
+ super(BaseCellsTest, self).setUp()
+
+ self.fake_cells = [
+ dict(id=1, name='cell1', is_parent=True,
+ weight_scale=1.0, weight_offset=0.0,
+ transport_url='rabbit://bob:xxxx@r1.example.org/'),
+ dict(id=2, name='cell2', is_parent=False,
+ weight_scale=1.0, weight_offset=0.0,
+ transport_url='rabbit://alice:qwerty@r2.example.org/')]
+
+ self.fake_capabilities = [
+ {'cap1': '0,1', 'cap2': '2,3'},
+ {'cap3': '4,5', 'cap4': '5,6'}]
+
+ def fake_cell_get(_self, context, cell_name):
+ for cell in self.fake_cells:
+ if cell_name == cell['name']:
+ return cell
+ else:
+ raise exception.CellNotFound(cell_name=cell_name)
+
+ def fake_cell_create(_self, context, values):
+ cell = dict(id=1)
+ cell.update(values)
+ return cell
+
+ def fake_cell_update(_self, context, cell_id, values):
+ cell = fake_cell_get(_self, context, cell_id)
+ cell.update(values)
+ return cell
+
+ def fake_cells_api_get_all_cell_info(*args):
+ return self._get_all_cell_info(*args)
+
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_get', fake_cell_get)
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_update', fake_cell_update)
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_create', fake_cell_create)
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'get_cell_info_for_neighbors',
+ fake_cells_api_get_all_cell_info)
+
+ def _get_all_cell_info(self, *args):
+ def insecure_transport_url(url):
+ transport_url = rpc.get_transport_url(url)
+ transport_url.hosts[0].password = None
+ return str(transport_url)
+
+ cells = copy.deepcopy(self.fake_cells)
+ cells[0]['transport_url'] = insecure_transport_url(
+ cells[0]['transport_url'])
+ cells[1]['transport_url'] = insecure_transport_url(
+ cells[1]['transport_url'])
+ for i, cell in enumerate(cells):
+ cell['capabilities'] = self.fake_capabilities[i]
+ return cells
+
+
+class CellsTestV21(BaseCellsTest):
+ cell_extension = 'compute_extension:v3:os-cells'
+ bad_request = exception.ValidationError
+
+ def _get_cell_controller(self, ext_mgr):
+ return cells_ext_v21.CellsController()
+
+ def _get_request(self, resource):
+ return fakes.HTTPRequest.blank('/v2/fake/' + resource)
+
+ def setUp(self):
+ super(CellsTestV21, self).setUp()
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.controller = self._get_cell_controller(self.ext_mgr)
+ self.context = context.get_admin_context()
+ self.flags(enable=True, group='cells')
+
+ def test_index(self):
+ req = self._get_request("cells")
+ res_dict = self.controller.index(req)
+
+ self.assertEqual(len(res_dict['cells']), 2)
+ for i, cell in enumerate(res_dict['cells']):
+ self.assertEqual(cell['name'], self.fake_cells[i]['name'])
+ self.assertNotIn('capabilitiles', cell)
+ self.assertNotIn('password', cell)
+
+ def test_detail(self):
+ req = self._get_request("cells/detail")
+ res_dict = self.controller.detail(req)
+
+ self.assertEqual(len(res_dict['cells']), 2)
+ for i, cell in enumerate(res_dict['cells']):
+ self.assertEqual(cell['name'], self.fake_cells[i]['name'])
+ self.assertEqual(cell['capabilities'], self.fake_capabilities[i])
+ self.assertNotIn('password', cell)
+
+ def test_show_bogus_cell_raises(self):
+ req = self._get_request("cells/bogus")
+ self.assertRaises(exc.HTTPNotFound, self.controller.show, req, 'bogus')
+
+ def test_get_cell_by_name(self):
+ req = self._get_request("cells/cell1")
+ res_dict = self.controller.show(req, 'cell1')
+ cell = res_dict['cell']
+
+ self.assertEqual(cell['name'], 'cell1')
+ self.assertEqual(cell['rpc_host'], 'r1.example.org')
+ self.assertNotIn('password', cell)
+
+ def _cell_delete(self):
+ call_info = {'delete_called': 0}
+
+ def fake_cell_delete(inst, context, cell_name):
+ self.assertEqual(cell_name, 'cell999')
+ call_info['delete_called'] += 1
+
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_delete', fake_cell_delete)
+
+ req = self._get_request("cells/cell999")
+ req.environ['nova.context'] = self.context
+ self.controller.delete(req, 'cell999')
+ self.assertEqual(call_info['delete_called'], 1)
+
+ def test_cell_delete(self):
+ # Test cell delete with just cell policy
+ rules = {"default": "is_admin:true",
+ self.cell_extension: "is_admin:true"}
+ self.policy.set_rules(rules)
+ self._cell_delete()
+
+ def test_cell_delete_with_delete_policy(self):
+ self._cell_delete()
+
+ def test_delete_bogus_cell_raises(self):
+ def fake_cell_delete(inst, context, cell_name):
+ return 0
+
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_delete', fake_cell_delete)
+
+ req = self._get_request("cells/cell999")
+ req.environ['nova.context'] = self.context
+ self.assertRaises(exc.HTTPNotFound, self.controller.delete, req,
+ 'cell999')
+
+ def test_cell_delete_fails_for_invalid_policy(self):
+ def fake_cell_delete(inst, context, cell_name):
+ pass
+
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_delete', fake_cell_delete)
+
+ req = self._get_request("cells/cell999")
+ req.environ['nova.context'] = self.context
+ req.environ["nova.context"].is_admin = False
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.delete, req, 'cell999')
+
+ def _cell_create_parent(self):
+ body = {'cell': {'name': 'meow',
+ 'username': 'fred',
+ 'password': 'fubar',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ req.environ['nova.context'] = self.context
+ res_dict = self.controller.create(req, body=body)
+ cell = res_dict['cell']
+
+ self.assertEqual(cell['name'], 'meow')
+ self.assertEqual(cell['username'], 'fred')
+ self.assertEqual(cell['rpc_host'], 'r3.example.org')
+ self.assertEqual(cell['type'], 'parent')
+ self.assertNotIn('password', cell)
+ self.assertNotIn('is_parent', cell)
+
+ def test_cell_create_parent(self):
+ # Test create with just cells policy
+ rules = {"default": "is_admin:true",
+ self.cell_extension: "is_admin:true"}
+ self.policy.set_rules(rules)
+ self._cell_create_parent()
+
+ def test_cell_create_parent_with_create_policy(self):
+ self._cell_create_parent()
+
+ def _cell_create_child(self):
+ body = {'cell': {'name': 'meow',
+ 'username': 'fred',
+ 'password': 'fubar',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'child'}}
+
+ req = self._get_request("cells")
+ req.environ['nova.context'] = self.context
+ res_dict = self.controller.create(req, body=body)
+ cell = res_dict['cell']
+
+ self.assertEqual(cell['name'], 'meow')
+ self.assertEqual(cell['username'], 'fred')
+ self.assertEqual(cell['rpc_host'], 'r3.example.org')
+ self.assertEqual(cell['type'], 'child')
+ self.assertNotIn('password', cell)
+ self.assertNotIn('is_parent', cell)
+
+ def test_cell_create_child(self):
+ # Test create with just cells policy
+ rules = {"default": "is_admin:true",
+ self.cell_extension: "is_admin:true"}
+ self.policy.set_rules(rules)
+ self._cell_create_child()
+
+ def test_cell_create_child_with_create_policy(self):
+ self._cell_create_child()
+
+ def test_cell_create_no_name_raises(self):
+ body = {'cell': {'username': 'moocow',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ req.environ['nova.context'] = self.context
+ self.assertRaises(self.bad_request,
+ self.controller.create, req, body=body)
+
+ def test_cell_create_name_empty_string_raises(self):
+ body = {'cell': {'name': '',
+ 'username': 'fred',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ req.environ['nova.context'] = self.context
+ self.assertRaises(self.bad_request,
+ self.controller.create, req, body=body)
+
+ def test_cell_create_name_with_bang_raises(self):
+ body = {'cell': {'name': 'moo!cow',
+ 'username': 'fred',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ req.environ['nova.context'] = self.context
+ self.assertRaises(self.bad_request,
+ self.controller.create, req, body=body)
+
+ def test_cell_create_name_with_dot_raises(self):
+ body = {'cell': {'name': 'moo.cow',
+ 'username': 'fred',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ req.environ['nova.context'] = self.context
+ res_dict = self.controller.create(req, body=body)
+ cell = res_dict['cell']
+ self.assertEqual(cell['name'], 'moo.cow')
+
+ def test_cell_create_name_with_invalid_type_raises(self):
+ body = {'cell': {'name': 'moocow',
+ 'username': 'fred',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'invalid'}}
+
+ req = self._get_request("cells")
+ req.environ['nova.context'] = self.context
+ self.assertRaises(self.bad_request,
+ self.controller.create, req, body=body)
+
+ def test_cell_create_fails_for_invalid_policy(self):
+ body = {'cell': {'name': 'fake'}}
+ req = self._get_request("cells")
+ req.environ['nova.context'] = self.context
+ req.environ['nova.context'].is_admin = False
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.create, req, body=body)
+
+ def _cell_update(self):
+ body = {'cell': {'username': 'zeb',
+ 'password': 'sneaky'}}
+
+ req = self._get_request("cells/cell1")
+ req.environ['nova.context'] = self.context
+ res_dict = self.controller.update(req, 'cell1', body=body)
+ cell = res_dict['cell']
+
+ self.assertEqual(cell['name'], 'cell1')
+ self.assertEqual(cell['rpc_host'], 'r1.example.org')
+ self.assertEqual(cell['username'], 'zeb')
+ self.assertNotIn('password', cell)
+
+ def test_cell_update(self):
+ # Test cell update with just cell policy
+ rules = {"default": "is_admin:true",
+ self.cell_extension: "is_admin:true"}
+ self.policy.set_rules(rules)
+ self._cell_update()
+
+ def test_cell_update_with_update_policy(self):
+ self._cell_update()
+
+ def test_cell_update_fails_for_invalid_policy(self):
+ body = {'cell': {'name': 'got_changed'}}
+ req = self._get_request("cells/cell1")
+ req.environ['nova.context'] = self.context
+ req.environ['nova.context'].is_admin = False
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.create, req, body=body)
+
+ def test_cell_update_empty_name_raises(self):
+ body = {'cell': {'name': '',
+ 'username': 'zeb',
+ 'password': 'sneaky'}}
+
+ req = self._get_request("cells/cell1")
+ req.environ['nova.context'] = self.context
+ self.assertRaises(self.bad_request,
+ self.controller.update, req, 'cell1', body=body)
+
+ def test_cell_update_invalid_type_raises(self):
+ body = {'cell': {'username': 'zeb',
+ 'type': 'invalid',
+ 'password': 'sneaky'}}
+
+ req = self._get_request("cells/cell1")
+ req.environ['nova.context'] = self.context
+ self.assertRaises(self.bad_request,
+ self.controller.update, req, 'cell1', body=body)
+
+ def test_cell_update_without_type_specified(self):
+ body = {'cell': {'username': 'wingwj'}}
+
+ req = self._get_request("cells/cell1")
+ req.environ['nova.context'] = self.context
+ res_dict = self.controller.update(req, 'cell1', body=body)
+ cell = res_dict['cell']
+
+ self.assertEqual(cell['name'], 'cell1')
+ self.assertEqual(cell['rpc_host'], 'r1.example.org')
+ self.assertEqual(cell['username'], 'wingwj')
+ self.assertEqual(cell['type'], 'parent')
+
+ def test_cell_update_with_type_specified(self):
+ body1 = {'cell': {'username': 'wingwj', 'type': 'child'}}
+ body2 = {'cell': {'username': 'wingwj', 'type': 'parent'}}
+
+ req1 = self._get_request("cells/cell1")
+ req1.environ['nova.context'] = self.context
+ res_dict1 = self.controller.update(req1, 'cell1', body=body1)
+ cell1 = res_dict1['cell']
+
+ req2 = self._get_request("cells/cell2")
+ req2.environ['nova.context'] = self.context
+ res_dict2 = self.controller.update(req2, 'cell2', body=body2)
+ cell2 = res_dict2['cell']
+
+ self.assertEqual(cell1['name'], 'cell1')
+ self.assertEqual(cell1['rpc_host'], 'r1.example.org')
+ self.assertEqual(cell1['username'], 'wingwj')
+ self.assertEqual(cell1['type'], 'child')
+
+ self.assertEqual(cell2['name'], 'cell2')
+ self.assertEqual(cell2['rpc_host'], 'r2.example.org')
+ self.assertEqual(cell2['username'], 'wingwj')
+ self.assertEqual(cell2['type'], 'parent')
+
+ def test_cell_info(self):
+ caps = ['cap1=a;b', 'cap2=c;d']
+ self.flags(name='darksecret', capabilities=caps, group='cells')
+
+ req = self._get_request("cells/info")
+ res_dict = self.controller.info(req)
+ cell = res_dict['cell']
+ cell_caps = cell['capabilities']
+
+ self.assertEqual(cell['name'], 'darksecret')
+ self.assertEqual(cell_caps['cap1'], 'a;b')
+ self.assertEqual(cell_caps['cap2'], 'c;d')
+
+ def test_show_capacities(self):
+ if (self.cell_extension == 'compute_extension:cells'):
+ self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True)
+ self.mox.StubOutWithMock(self.controller.cells_rpcapi,
+ 'get_capacities')
+ response = {"ram_free":
+ {"units_by_mb": {"8192": 0, "512": 13,
+ "4096": 1, "2048": 3, "16384": 0},
+ "total_mb": 7680},
+ "disk_free":
+ {"units_by_mb": {"81920": 11, "20480": 46,
+ "40960": 23, "163840": 5, "0": 0},
+ "total_mb": 1052672}
+ }
+ self.controller.cells_rpcapi.\
+ get_capacities(self.context, cell_name=None).AndReturn(response)
+ self.mox.ReplayAll()
+ req = self._get_request("cells/capacities")
+ req.environ["nova.context"] = self.context
+ res_dict = self.controller.capacities(req)
+ self.assertEqual(response, res_dict['cell']['capacities'])
+
+ def test_show_capacity_fails_with_non_admin_context(self):
+ if (self.cell_extension == 'compute_extension:cells'):
+ self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True)
+ rules = {self.cell_extension: "is_admin:true"}
+ self.policy.set_rules(rules)
+
+ self.mox.ReplayAll()
+ req = self._get_request("cells/capacities")
+ req.environ["nova.context"] = self.context
+ req.environ["nova.context"].is_admin = False
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.capacities, req)
+
+ def test_show_capacities_for_invalid_cell(self):
+ if (self.cell_extension == 'compute_extension:cells'):
+ self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True)
+ self.mox.StubOutWithMock(self.controller.cells_rpcapi,
+ 'get_capacities')
+ self.controller.cells_rpcapi. \
+ get_capacities(self.context, cell_name="invalid_cell").AndRaise(
+ exception.CellNotFound(cell_name="invalid_cell"))
+ self.mox.ReplayAll()
+ req = self._get_request("cells/invalid_cell/capacities")
+ req.environ["nova.context"] = self.context
+ self.assertRaises(exc.HTTPNotFound,
+ self.controller.capacities, req, "invalid_cell")
+
+ def test_show_capacities_for_cell(self):
+ if (self.cell_extension == 'compute_extension:cells'):
+ self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True)
+ self.mox.StubOutWithMock(self.controller.cells_rpcapi,
+ 'get_capacities')
+ response = {"ram_free":
+ {"units_by_mb": {"8192": 0, "512": 13,
+ "4096": 1, "2048": 3, "16384": 0},
+ "total_mb": 7680},
+ "disk_free":
+ {"units_by_mb": {"81920": 11, "20480": 46,
+ "40960": 23, "163840": 5, "0": 0},
+ "total_mb": 1052672}
+ }
+ self.controller.cells_rpcapi.\
+ get_capacities(self.context, cell_name='cell_name').\
+ AndReturn(response)
+ self.mox.ReplayAll()
+ req = self._get_request("cells/capacities")
+ req.environ["nova.context"] = self.context
+ res_dict = self.controller.capacities(req, 'cell_name')
+ self.assertEqual(response, res_dict['cell']['capacities'])
+
+ def test_sync_instances(self):
+ call_info = {}
+
+ def sync_instances(self, context, **kwargs):
+ call_info['project_id'] = kwargs.get('project_id')
+ call_info['updated_since'] = kwargs.get('updated_since')
+ call_info['deleted'] = kwargs.get('deleted')
+
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'sync_instances', sync_instances)
+
+ req = self._get_request("cells/sync_instances")
+ req.environ['nova.context'] = self.context
+ body = {}
+ self.controller.sync_instances(req, body=body)
+ self.assertIsNone(call_info['project_id'])
+ self.assertIsNone(call_info['updated_since'])
+
+ body = {'project_id': 'test-project'}
+ self.controller.sync_instances(req, body=body)
+ self.assertEqual(call_info['project_id'], 'test-project')
+ self.assertIsNone(call_info['updated_since'])
+
+ expected = timeutils.utcnow().isoformat()
+ if not expected.endswith("+00:00"):
+ expected += "+00:00"
+
+ body = {'updated_since': expected}
+ self.controller.sync_instances(req, body=body)
+ self.assertIsNone(call_info['project_id'])
+ self.assertEqual(call_info['updated_since'], expected)
+
+ body = {'updated_since': 'skjdfkjsdkf'}
+ self.assertRaises(self.bad_request,
+ self.controller.sync_instances, req, body=body)
+
+ body = {'deleted': False}
+ self.controller.sync_instances(req, body=body)
+ self.assertIsNone(call_info['project_id'])
+ self.assertIsNone(call_info['updated_since'])
+ self.assertEqual(call_info['deleted'], False)
+
+ body = {'deleted': 'False'}
+ self.controller.sync_instances(req, body=body)
+ self.assertIsNone(call_info['project_id'])
+ self.assertIsNone(call_info['updated_since'])
+ self.assertEqual(call_info['deleted'], False)
+
+ body = {'deleted': 'True'}
+ self.controller.sync_instances(req, body=body)
+ self.assertIsNone(call_info['project_id'])
+ self.assertIsNone(call_info['updated_since'])
+ self.assertEqual(call_info['deleted'], True)
+
+ body = {'deleted': 'foo'}
+ self.assertRaises(self.bad_request,
+ self.controller.sync_instances, req, body=body)
+
+ body = {'foo': 'meow'}
+ self.assertRaises(self.bad_request,
+ self.controller.sync_instances, req, body=body)
+
+ def test_sync_instances_fails_for_invalid_policy(self):
+ def sync_instances(self, context, **kwargs):
+ pass
+
+ self.stubs.Set(cells_rpcapi.CellsAPI, 'sync_instances', sync_instances)
+
+ req = self._get_request("cells/sync_instances")
+ req.environ['nova.context'] = self.context
+ req.environ['nova.context'].is_admin = False
+
+ body = {}
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.sync_instances, req, body=body)
+
+ def test_cells_disabled(self):
+ self.flags(enable=False, group='cells')
+
+ req = self._get_request("cells")
+ self.assertRaises(exc.HTTPNotImplemented,
+ self.controller.index, req)
+
+ req = self._get_request("cells/detail")
+ self.assertRaises(exc.HTTPNotImplemented,
+ self.controller.detail, req)
+
+ req = self._get_request("cells/cell1")
+ self.assertRaises(exc.HTTPNotImplemented,
+ self.controller.show, req)
+
+ self.assertRaises(exc.HTTPNotImplemented,
+ self.controller.delete, req, 'cell999')
+
+ req = self._get_request("cells/cells")
+ self.assertRaises(exc.HTTPNotImplemented,
+ self.controller.create, req, {})
+
+ req = self._get_request("cells/capacities")
+ self.assertRaises(exc.HTTPNotImplemented,
+ self.controller.capacities, req)
+
+ req = self._get_request("cells/sync_instances")
+ self.assertRaises(exc.HTTPNotImplemented,
+ self.controller.sync_instances, req, {})
+
+
+class CellsTestV2(CellsTestV21):
+ cell_extension = 'compute_extension:cells'
+ bad_request = exc.HTTPBadRequest
+
+ def _get_cell_controller(self, ext_mgr):
+ return cells_ext_v2.Controller(ext_mgr)
+
+ def test_cell_create_name_with_dot_raises(self):
+ body = {'cell': {'name': 'moo.cow',
+ 'username': 'fred',
+ 'password': 'secret',
+ 'rpc_host': 'r3.example.org',
+ 'type': 'parent'}}
+
+ req = self._get_request("cells")
+ req.environ['nova.context'] = self.context
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create, req, body=body)
+
+
+class TestCellsXMLSerializer(BaseCellsTest):
+ def test_multiple_cells(self):
+ fixture = {'cells': self._get_all_cell_info()}
+
+ serializer = cells_ext_v2.CellsTemplate()
+ output = serializer.serialize(fixture)
+ res_tree = etree.XML(output)
+
+ self.assertEqual(res_tree.tag, '{%s}cells' % xmlutil.XMLNS_V10)
+ self.assertEqual(len(res_tree), 2)
+ self.assertEqual(res_tree[0].tag, '{%s}cell' % xmlutil.XMLNS_V10)
+ self.assertEqual(res_tree[1].tag, '{%s}cell' % xmlutil.XMLNS_V10)
+
+ def test_single_cell_with_caps(self):
+ cell = {'id': 1,
+ 'name': 'darksecret',
+ 'username': 'meow',
+ 'capabilities': {'cap1': 'a;b',
+ 'cap2': 'c;d'}}
+ fixture = {'cell': cell}
+
+ serializer = cells_ext_v2.CellTemplate()
+ output = serializer.serialize(fixture)
+ res_tree = etree.XML(output)
+
+ self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10)
+ self.assertEqual(res_tree.get('name'), 'darksecret')
+ self.assertEqual(res_tree.get('username'), 'meow')
+ self.assertIsNone(res_tree.get('password'))
+ self.assertEqual(len(res_tree), 1)
+
+ child = res_tree[0]
+ self.assertEqual(child.tag,
+ '{%s}capabilities' % xmlutil.XMLNS_V10)
+ for elem in child:
+ self.assertIn(elem.tag, ('{%s}cap1' % xmlutil.XMLNS_V10,
+ '{%s}cap2' % xmlutil.XMLNS_V10))
+ if elem.tag == '{%s}cap1' % xmlutil.XMLNS_V10:
+ self.assertEqual(elem.text, 'a;b')
+ elif elem.tag == '{%s}cap2' % xmlutil.XMLNS_V10:
+ self.assertEqual(elem.text, 'c;d')
+
+ def test_single_cell_without_caps(self):
+ cell = {'id': 1,
+ 'username': 'woof',
+ 'name': 'darksecret'}
+ fixture = {'cell': cell}
+
+ serializer = cells_ext_v2.CellTemplate()
+ output = serializer.serialize(fixture)
+ res_tree = etree.XML(output)
+
+ self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10)
+ self.assertEqual(res_tree.get('name'), 'darksecret')
+ self.assertEqual(res_tree.get('username'), 'woof')
+ self.assertIsNone(res_tree.get('password'))
+ self.assertEqual(len(res_tree), 0)
+
+
+class TestCellsXMLDeserializer(test.NoDBTestCase):
+ def test_cell_deserializer(self):
+ caps_dict = {'cap1': 'a;b',
+ 'cap2': 'c;d'}
+ caps_xml = ("<capabilities><cap1>a;b</cap1>"
+ "<cap2>c;d</cap2></capabilities>")
+ expected = {'cell': {'name': 'testcell1',
+ 'type': 'child',
+ 'rpc_host': 'localhost',
+ 'capabilities': caps_dict}}
+ intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<cell><name>testcell1</name><type>child</type>"
+ "<rpc_host>localhost</rpc_host>"
+ "%s</cell>") % caps_xml
+ deserializer = cells_ext_v2.CellDeserializer()
+ result = deserializer.deserialize(intext)
+ self.assertEqual(dict(body=expected), result)
+
+ def test_with_corrupt_xml(self):
+ deserializer = cells_ext_v2.CellDeserializer()
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ deserializer.deserialize,
+ utils.killer_xml_body())
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_certificates.py b/nova/tests/unit/api/openstack/compute/contrib/test_certificates.py
new file mode 100644
index 0000000000..c7066516d8
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_certificates.py
@@ -0,0 +1,140 @@
+# Copyright (c) 2012 OpenStack Foundation
+# All Rights Reserved.
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+import mock
+import mox
+from webob import exc
+
+from nova.api.openstack.compute.contrib import certificates as certificates_v2
+from nova.api.openstack.compute.plugins.v3 import certificates \
+ as certificates_v21
+from nova.cert import rpcapi
+from nova import context
+from nova import exception
+from nova.openstack.common import policy as common_policy
+from nova import policy
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+class CertificatesTestV21(test.NoDBTestCase):
+ certificates = certificates_v21
+ url = '/v3/os-certificates'
+ certificate_show_extension = 'compute_extension:v3:os-certificates:show'
+ certificate_create_extension = \
+ 'compute_extension:v3:os-certificates:create'
+
+ def setUp(self):
+ super(CertificatesTestV21, self).setUp()
+ self.context = context.RequestContext('fake', 'fake')
+ self.controller = self.certificates.CertificatesController()
+
+ def test_translate_certificate_view(self):
+ pk, cert = 'fakepk', 'fakecert'
+ view = self.certificates._translate_certificate_view(cert, pk)
+ self.assertEqual(view['data'], cert)
+ self.assertEqual(view['private_key'], pk)
+
+ def test_certificates_show_root(self):
+ self.mox.StubOutWithMock(self.controller.cert_rpcapi, 'fetch_ca')
+
+ self.controller.cert_rpcapi.fetch_ca(
+ mox.IgnoreArg(), project_id='fake').AndReturn('fakeroot')
+
+ self.mox.ReplayAll()
+
+ req = fakes.HTTPRequest.blank(self.url + '/root')
+ res_dict = self.controller.show(req, 'root')
+
+ response = {'certificate': {'data': 'fakeroot', 'private_key': None}}
+ self.assertEqual(res_dict, response)
+
+ def test_certificates_show_policy_failed(self):
+ rules = {
+ self.certificate_show_extension:
+ common_policy.parse_rule("!")
+ }
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.url + '/root')
+ exc = self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.show, req, 'root')
+ self.assertIn(self.certificate_show_extension,
+ exc.format_message())
+
+ def test_certificates_create_certificate(self):
+ self.mox.StubOutWithMock(self.controller.cert_rpcapi,
+ 'generate_x509_cert')
+
+ self.controller.cert_rpcapi.generate_x509_cert(
+ mox.IgnoreArg(),
+ user_id='fake_user',
+ project_id='fake').AndReturn(('fakepk', 'fakecert'))
+
+ self.mox.ReplayAll()
+
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.create(req)
+
+ response = {
+ 'certificate': {'data': 'fakecert',
+ 'private_key': 'fakepk'}
+ }
+ self.assertEqual(res_dict, response)
+
+ def test_certificates_create_policy_failed(self):
+ rules = {
+ self.certificate_create_extension:
+ common_policy.parse_rule("!")
+ }
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.url)
+ exc = self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.create, req)
+ self.assertIn(self.certificate_create_extension,
+ exc.format_message())
+
+ @mock.patch.object(rpcapi.CertAPI, 'fetch_ca',
+ side_effect=exception.CryptoCAFileNotFound(project='fake'))
+ def test_non_exist_certificates_show(self, mock_fetch_ca):
+ req = fakes.HTTPRequest.blank(self.url + '/root')
+ self.assertRaises(
+ exc.HTTPNotFound,
+ self.controller.show,
+ req, 'root')
+
+
+class CertificatesTestV2(CertificatesTestV21):
+ certificates = certificates_v2
+ url = '/v2/fake/os-certificates'
+ certificate_show_extension = 'compute_extension:certificates'
+ certificate_create_extension = 'compute_extension:certificates'
+
+
+class CertificatesSerializerTest(test.NoDBTestCase):
+ def test_index_serializer(self):
+ serializer = certificates_v2.CertificateTemplate()
+ text = serializer.serialize(dict(
+ certificate=dict(
+ data='fakecert',
+ private_key='fakepk'),
+ ))
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('certificate', tree.tag)
+ self.assertEqual('fakepk', tree.get('private_key'))
+ self.assertEqual('fakecert', tree.get('data'))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe.py b/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe.py
new file mode 100644
index 0000000000..ab3b1a58cc
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe.py
@@ -0,0 +1,210 @@
+# Copyright 2011 OpenStack Foundation
+# 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 uuid as uuid_lib
+
+from lxml import etree
+from oslo.config import cfg
+from oslo.utils import timeutils
+from webob import exc
+
+from nova.api.openstack.compute.contrib import cloudpipe as cloudpipe_v2
+from nova.api.openstack.compute.plugins.v3 import cloudpipe as cloudpipe_v21
+from nova.api.openstack import wsgi
+from nova.compute import utils as compute_utils
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_network
+from nova.tests.unit import matchers
+from nova import utils
+
+CONF = cfg.CONF
+CONF.import_opt('vpn_image_id', 'nova.cloudpipe.pipelib')
+
+
+project_id = str(uuid_lib.uuid4().hex)
+uuid = str(uuid_lib.uuid4())
+
+
+def fake_vpn_instance():
+ return {
+ 'id': 7, 'image_ref': CONF.vpn_image_id, 'vm_state': 'active',
+ 'created_at': timeutils.parse_strtime('1981-10-20T00:00:00.000000'),
+ 'uuid': uuid, 'project_id': project_id,
+ }
+
+
+def compute_api_get_all_empty(context, search_opts=None):
+ return []
+
+
+def compute_api_get_all(context, search_opts=None):
+ return [fake_vpn_instance()]
+
+
+def utils_vpn_ping(addr, port, timoeout=0.05, session_id=None):
+ return True
+
+
+class CloudpipeTestV21(test.NoDBTestCase):
+ cloudpipe = cloudpipe_v21
+ url = '/v2/fake/os-cloudpipe'
+
+ def setUp(self):
+ super(CloudpipeTestV21, self).setUp()
+ self.controller = self.cloudpipe.CloudpipeController()
+ self.stubs.Set(self.controller.compute_api, "get_all",
+ compute_api_get_all_empty)
+ self.stubs.Set(utils, 'vpn_ping', utils_vpn_ping)
+
+ def test_cloudpipe_list_no_network(self):
+
+ def fake_get_nw_info_for_instance(instance):
+ return {}
+
+ self.stubs.Set(compute_utils, "get_nw_info_for_instance",
+ fake_get_nw_info_for_instance)
+ self.stubs.Set(self.controller.compute_api, "get_all",
+ compute_api_get_all)
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.index(req)
+ response = {'cloudpipes': [{'project_id': project_id,
+ 'instance_id': uuid,
+ 'created_at': '1981-10-20T00:00:00Z'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_cloudpipe_list(self):
+
+ def network_api_get(context, network_id):
+ self.assertEqual(context.project_id, project_id)
+ return {'vpn_public_address': '127.0.0.1',
+ 'vpn_public_port': 22}
+
+ def fake_get_nw_info_for_instance(instance):
+ return fake_network.fake_get_instance_nw_info(self.stubs)
+
+ self.stubs.Set(compute_utils, "get_nw_info_for_instance",
+ fake_get_nw_info_for_instance)
+ self.stubs.Set(self.controller.network_api, "get",
+ network_api_get)
+ self.stubs.Set(self.controller.compute_api, "get_all",
+ compute_api_get_all)
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.index(req)
+ response = {'cloudpipes': [{'project_id': project_id,
+ 'internal_ip': '192.168.1.100',
+ 'public_ip': '127.0.0.1',
+ 'public_port': 22,
+ 'state': 'running',
+ 'instance_id': uuid,
+ 'created_at': '1981-10-20T00:00:00Z'}]}
+ self.assertThat(res_dict, matchers.DictMatches(response))
+
+ def test_cloudpipe_create(self):
+ def launch_vpn_instance(context):
+ return ([fake_vpn_instance()], 'fake-reservation')
+
+ self.stubs.Set(self.controller.cloudpipe, 'launch_vpn_instance',
+ launch_vpn_instance)
+ body = {'cloudpipe': {'project_id': project_id}}
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.create(req, body=body)
+
+ response = {'instance_id': uuid}
+ self.assertEqual(res_dict, response)
+
+ def test_cloudpipe_create_no_networks(self):
+ def launch_vpn_instance(context):
+ raise exception.NoMoreNetworks
+
+ self.stubs.Set(self.controller.cloudpipe, 'launch_vpn_instance',
+ launch_vpn_instance)
+ body = {'cloudpipe': {'project_id': project_id}}
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create, req, body=body)
+
+ def test_cloudpipe_create_already_running(self):
+ def launch_vpn_instance(*args, **kwargs):
+ self.fail("Method should not have been called")
+
+ self.stubs.Set(self.controller.cloudpipe, 'launch_vpn_instance',
+ launch_vpn_instance)
+ self.stubs.Set(self.controller.compute_api, "get_all",
+ compute_api_get_all)
+ body = {'cloudpipe': {'project_id': project_id}}
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.create(req, body=body)
+ response = {'instance_id': uuid}
+ self.assertEqual(res_dict, response)
+
+ def test_cloudpipe_create_with_bad_project_id_failed(self):
+ body = {'cloudpipe': {'project_id': 'bad.project.id'}}
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, req, body=body)
+
+
+class CloudpipeTestV2(CloudpipeTestV21):
+ cloudpipe = cloudpipe_v2
+
+ def test_cloudpipe_create_with_bad_project_id_failed(self):
+ pass
+
+
+class CloudpipesXMLSerializerTestV2(test.NoDBTestCase):
+ def test_default_serializer(self):
+ serializer = cloudpipe_v2.CloudpipeTemplate()
+ exemplar = dict(cloudpipe=dict(instance_id='1234-1234-1234-1234'))
+ text = serializer.serialize(exemplar)
+ tree = etree.fromstring(text)
+ self.assertEqual('cloudpipe', tree.tag)
+ for child in tree:
+ self.assertIn(child.tag, exemplar['cloudpipe'])
+ self.assertEqual(child.text, exemplar['cloudpipe'][child.tag])
+
+ def test_index_serializer(self):
+ serializer = cloudpipe_v2.CloudpipesTemplate()
+ exemplar = dict(cloudpipes=[
+ dict(
+ project_id='1234',
+ public_ip='1.2.3.4',
+ public_port='321',
+ instance_id='1234-1234-1234-1234',
+ created_at=timeutils.isotime(),
+ state='running'),
+ dict(
+ project_id='4321',
+ public_ip='4.3.2.1',
+ public_port='123',
+ state='pending')])
+ text = serializer.serialize(exemplar)
+ tree = etree.fromstring(text)
+ self.assertEqual('cloudpipes', tree.tag)
+ self.assertEqual(len(exemplar['cloudpipes']), len(tree))
+ for idx, cl_pipe in enumerate(tree):
+ kp_data = exemplar['cloudpipes'][idx]
+ for child in cl_pipe:
+ self.assertIn(child.tag, kp_data)
+ self.assertEqual(child.text, kp_data[child.tag])
+
+ def test_deserializer(self):
+ deserializer = wsgi.XMLDeserializer()
+ exemplar = dict(cloudpipe=dict(project_id='4321'))
+ intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<cloudpipe><project_id>4321</project_id></cloudpipe>')
+ result = deserializer.deserialize(intext)['body']
+ self.assertEqual(result, exemplar)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe_update.py b/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe_update.py
new file mode 100644
index 0000000000..23faf6275a
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe_update.py
@@ -0,0 +1,99 @@
+# Copyright 2012 IBM Corp.
+#
+# 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
+
+from nova.api.openstack.compute.contrib import cloudpipe_update as clup_v2
+from nova.api.openstack.compute.plugins.v3 import cloudpipe as clup_v21
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_network
+
+
+fake_networks = [fake_network.fake_network(1),
+ fake_network.fake_network(2)]
+
+
+def fake_project_get_networks(context, project_id, associate=True):
+ return fake_networks
+
+
+def fake_network_update(context, network_id, values):
+ for network in fake_networks:
+ if network['id'] == network_id:
+ for key in values:
+ network[key] = values[key]
+
+
+class CloudpipeUpdateTestV21(test.NoDBTestCase):
+ bad_request = exception.ValidationError
+
+ def setUp(self):
+ super(CloudpipeUpdateTestV21, self).setUp()
+ self.stubs.Set(db, "project_get_networks", fake_project_get_networks)
+ self.stubs.Set(db, "network_update", fake_network_update)
+ self._setup()
+
+ def _setup(self):
+ self.controller = clup_v21.CloudpipeController()
+
+ def _check_status(self, expected_status, res, controller_methord):
+ self.assertEqual(expected_status, controller_methord.wsgi_code)
+
+ def test_cloudpipe_configure_project(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-cloudpipe/configure-project')
+ body = {"configure_project": {"vpn_ip": "1.2.3.4", "vpn_port": 222}}
+ result = self.controller.update(req, 'configure-project',
+ body=body)
+ self._check_status(202, result, self.controller.update)
+ self.assertEqual(fake_networks[0]['vpn_public_address'], "1.2.3.4")
+ self.assertEqual(fake_networks[0]['vpn_public_port'], 222)
+
+ def test_cloudpipe_configure_project_bad_url(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-cloudpipe/configure-projectx')
+ body = {"configure_project": {"vpn_ip": "1.2.3.4", "vpn_port": 222}}
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req,
+ 'configure-projectx', body=body)
+
+ def test_cloudpipe_configure_project_bad_data(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-cloudpipe/configure-project')
+ body = {"configure_project": {"vpn_ipxx": "1.2.3.4", "vpn_port": 222}}
+ self.assertRaises(self.bad_request,
+ self.controller.update, req,
+ 'configure-project', body=body)
+
+ def test_cloudpipe_configure_project_bad_vpn_port(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-cloudpipe/configure-project')
+ body = {"configure_project": {"vpn_ipxx": "1.2.3.4",
+ "vpn_port": "foo"}}
+ self.assertRaises(self.bad_request,
+ self.controller.update, req,
+ 'configure-project', body=body)
+
+
+class CloudpipeUpdateTestV2(CloudpipeUpdateTestV21):
+ bad_request = webob.exc.HTTPBadRequest
+
+ def _setup(self):
+ self.controller = clup_v2.CloudpipeUpdateController()
+
+ def _check_status(self, expected_status, res, controller_methord):
+ self.assertEqual(expected_status, res.status_int)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_config_drive.py b/nova/tests/unit/api/openstack/compute/contrib/test_config_drive.py
new file mode 100644
index 0000000000..ef94db0d23
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_config_drive.py
@@ -0,0 +1,260 @@
+# Copyright 2012 OpenStack Foundation
+# 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 datetime
+
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import config_drive as config_drive_v2
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import config_drive \
+ as config_drive_v21
+from nova.api.openstack.compute.plugins.v3 import servers as servers_v21
+from nova.api.openstack.compute import servers as servers_v2
+from nova.api.openstack import extensions
+from nova.compute import api as compute_api
+from nova.compute import flavors
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+from nova.tests.unit.image import fake
+
+
+CONF = cfg.CONF
+
+
+class ConfigDriveTestV21(test.TestCase):
+ base_url = '/v2/fake/servers/'
+
+ def _setup_wsgi(self):
+ self.app = fakes.wsgi_app_v21(init_only=('servers', 'os-config-drive'))
+
+ def _get_config_drive_controller(self):
+ return config_drive_v21.ConfigDriveController()
+
+ def setUp(self):
+ super(ConfigDriveTestV21, self).setUp()
+ self.Controller = self._get_config_drive_controller()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+ fake.stub_out_image_service(self.stubs)
+ self._setup_wsgi()
+
+ def test_show(self):
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get())
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get())
+ req = webob.Request.blank(self.base_url + '1')
+ req.headers['Content-Type'] = 'application/json'
+ response = req.get_response(self.app)
+ self.assertEqual(response.status_int, 200)
+ res_dict = jsonutils.loads(response.body)
+ self.assertIn('config_drive', res_dict['server'])
+
+ def test_detail_servers(self):
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fakes.fake_instance_get_all_by_filters())
+ req = fakes.HTTPRequest.blank(self.base_url + 'detail')
+ res = req.get_response(self.app)
+ server_dicts = jsonutils.loads(res.body)['servers']
+ self.assertNotEqual(len(server_dicts), 0)
+ for server_dict in server_dicts:
+ self.assertIn('config_drive', server_dict)
+
+
+class ConfigDriveTestV2(ConfigDriveTestV21):
+
+ def _get_config_drive_controller(self):
+ return config_drive_v2.Controller()
+
+ def _setup_wsgi(self):
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Config_drive'])
+ self.app = fakes.wsgi_app(init_only=('servers',))
+
+
+class ServersControllerCreateTestV21(test.TestCase):
+ base_url = '/v2/fake/'
+ bad_request = exception.ValidationError
+
+ def _set_up_controller(self):
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers_v21.ServersController(
+ extension_info=ext_info)
+ CONF.set_override('extensions_blacklist',
+ 'os-config-drive',
+ 'osapi_v3')
+ self.no_config_drive_controller = servers_v21.ServersController(
+ extension_info=ext_info)
+
+ def _verfiy_config_drive(self, **kwargs):
+ self.assertNotIn('config_drive', kwargs)
+
+ def _initialize_extension(self):
+ pass
+
+ def setUp(self):
+ """Shared implementation for tests below that create instance."""
+ super(ServersControllerCreateTestV21, self).setUp()
+
+ self.instance_cache_num = 0
+ self._set_up_controller()
+
+ def instance_create(context, inst):
+ inst_type = flavors.get_flavor_by_flavor_id(3)
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ def_image_ref = 'http://localhost/images/%s' % image_uuid
+ self.instance_cache_num += 1
+ instance = fake_instance.fake_db_instance(**{
+ 'id': self.instance_cache_num,
+ 'display_name': inst['display_name'] or 'test',
+ 'uuid': fakes.FAKE_UUID,
+ 'instance_type': inst_type,
+ 'access_ip_v4': '1.2.3.4',
+ 'access_ip_v6': 'fead::1234',
+ 'image_ref': inst.get('image_ref', def_image_ref),
+ 'user_id': 'fake',
+ 'project_id': 'fake',
+ 'reservation_id': inst['reservation_id'],
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "progress": 0,
+ "fixed_ips": [],
+ "task_state": "",
+ "vm_state": "",
+ "root_device_name": inst.get('root_device_name', 'vda'),
+ })
+
+ return instance
+
+ fake.stub_out_image_service(self.stubs)
+ self.stubs.Set(db, 'instance_create', instance_create)
+
+ def _test_create_extra(self, params, override_controller):
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ server = dict(name='server_test', imageRef=image_uuid, flavorRef=2)
+ server.update(params)
+ body = dict(server=server)
+ req = fakes.HTTPRequest.blank(self.base_url + 'servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ if override_controller is not None:
+ server = override_controller.create(req, body=body).obj['server']
+ else:
+ server = self.controller.create(req, body=body).obj['server']
+
+ def test_create_instance_with_config_drive_disabled(self):
+ params = {'config_drive': "False"}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self._verfiy_config_drive(**kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params,
+ override_controller=self.no_config_drive_controller)
+
+ def _create_instance_body_of_config_drive(self, param):
+ self._initialize_extension()
+
+ def create(*args, **kwargs):
+ self.assertIn('config_drive', kwargs)
+ return old_create(*args, **kwargs)
+
+ old_create = compute_api.API.create
+ self.stubs.Set(compute_api.API, 'create', create)
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = ('http://localhost' + self.base_url + 'flavors/3')
+ body = {
+ 'server': {
+ 'name': 'config_drive_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'config_drive': param,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.base_url + 'servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ return req, body
+
+ def test_create_instance_with_config_drive(self):
+ param = True
+ req, body = self._create_instance_body_of_config_drive(param)
+ res = self.controller.create(req, body=body).obj
+ server = res['server']
+ self.assertEqual(fakes.FAKE_UUID, server['id'])
+
+ def test_create_instance_with_config_drive_as_boolean_string(self):
+ param = 'false'
+ req, body = self._create_instance_body_of_config_drive(param)
+ res = self.controller.create(req, body=body).obj
+ server = res['server']
+ self.assertEqual(fakes.FAKE_UUID, server['id'])
+
+ def test_create_instance_with_bad_config_drive(self):
+ param = 12345
+ req, body = self._create_instance_body_of_config_drive(param)
+ self.assertRaises(self.bad_request,
+ self.controller.create, req, body=body)
+
+ def test_create_instance_without_config_drive(self):
+ param = True
+ req, body = self._create_instance_body_of_config_drive(param)
+ del body['server']['config_drive']
+ res = self.controller.create(req, body=body).obj
+ server = res['server']
+ self.assertEqual(fakes.FAKE_UUID, server['id'])
+
+ def test_create_instance_with_empty_config_drive(self):
+ param = ''
+ req, body = self._create_instance_body_of_config_drive(param)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, req, body=body)
+
+
+class ServersControllerCreateTestV2(ServersControllerCreateTestV21):
+ bad_request = webob.exc.HTTPBadRequest
+
+ def _set_up_controller(self):
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = servers_v2.Controller(self.ext_mgr)
+ self.no_config_drive_controller = None
+
+ def _verfiy_config_drive(self, **kwargs):
+ self.assertIsNone(kwargs['config_drive'])
+
+ def _initialize_extension(self):
+ self.ext_mgr.extensions = {'os-config-drive': 'fake'}
+
+ def test_create_instance_with_empty_config_drive(self):
+ param = ''
+ req, body = self._create_instance_body_of_config_drive(param)
+ res = self.controller.create(req, body=body).obj
+ server = res['server']
+ self.assertEqual(fakes.FAKE_UUID, server['id'])
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_console_auth_tokens.py b/nova/tests/unit/api/openstack/compute/contrib/test_console_auth_tokens.py
new file mode 100644
index 0000000000..eef4cd62ea
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_console_auth_tokens.py
@@ -0,0 +1,103 @@
+# Copyright 2013 Cloudbase Solutions Srl
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.consoleauth import rpcapi as consoleauth_rpcapi
+from nova import context
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+CONF = cfg.CONF
+CONF.import_opt('osapi_compute_ext_list', 'nova.api.openstack.compute.contrib')
+
+_FAKE_CONNECT_INFO = {'instance_uuid': 'fake_instance_uuid',
+ 'host': 'fake_host',
+ 'port': 'fake_port',
+ 'internal_access_path': 'fake_access_path',
+ 'console_type': 'rdp-html5'}
+
+
+def _fake_check_token(self, context, token):
+ return _FAKE_CONNECT_INFO
+
+
+def _fake_check_token_not_found(self, context, token):
+ return None
+
+
+def _fake_check_token_unauthorized(self, context, token):
+ connect_info = _FAKE_CONNECT_INFO
+ connect_info['console_type'] = 'unauthorized_console_type'
+ return connect_info
+
+
+class ConsoleAuthTokensExtensionTest(test.TestCase):
+
+ _FAKE_URL = '/v2/fake/os-console-auth-tokens/1'
+
+ _EXPECTED_OUTPUT = {'console': {'instance_uuid': 'fake_instance_uuid',
+ 'host': 'fake_host',
+ 'port': 'fake_port',
+ 'internal_access_path':
+ 'fake_access_path'}}
+
+ def setUp(self):
+ super(ConsoleAuthTokensExtensionTest, self).setUp()
+ self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token',
+ _fake_check_token)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Console_auth_tokens'])
+
+ ctxt = self._get_admin_context()
+ self.app = fakes.wsgi_app(init_only=('os-console-auth-tokens',),
+ fake_auth_context=ctxt)
+
+ def _get_admin_context(self):
+ ctxt = context.get_admin_context()
+ ctxt.user_id = 'fake'
+ ctxt.project_id = 'fake'
+ return ctxt
+
+ def _create_request(self):
+ req = webob.Request.blank(self._FAKE_URL)
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+ return req
+
+ def test_get_console_connect_info(self):
+ req = self._create_request()
+ res = req.get_response(self.app)
+ self.assertEqual(200, res.status_int)
+ output = jsonutils.loads(res.body)
+ self.assertEqual(self._EXPECTED_OUTPUT, output)
+
+ def test_get_console_connect_info_token_not_found(self):
+ self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token',
+ _fake_check_token_not_found)
+ req = self._create_request()
+ res = req.get_response(self.app)
+ self.assertEqual(404, res.status_int)
+
+ def test_get_console_connect_info_unauthorized_console_type(self):
+ self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token',
+ _fake_check_token_unauthorized)
+ req = self._create_request()
+ res = req.get_response(self.app)
+ self.assertEqual(401, res.status_int)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_console_output.py b/nova/tests/unit/api/openstack/compute/contrib/test_console_output.py
new file mode 100644
index 0000000000..441899a19b
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_console_output.py
@@ -0,0 +1,171 @@
+# Copyright 2011 Eldar Nugaev
+# 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 string
+
+from oslo.serialization import jsonutils
+
+from nova.compute import api as compute_api
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+
+def fake_get_console_output(self, _context, _instance, tail_length):
+ fixture = [str(i) for i in range(5)]
+
+ if tail_length is None:
+ pass
+ elif tail_length == 0:
+ fixture = []
+ else:
+ fixture = fixture[-int(tail_length):]
+
+ return '\n'.join(fixture)
+
+
+def fake_get_console_output_not_ready(self, _context, _instance, tail_length):
+ raise exception.InstanceNotReady(instance_id=_instance["uuid"])
+
+
+def fake_get_console_output_all_characters(self, _ctx, _instance, _tail_len):
+ return string.printable
+
+
+def fake_get(self, context, instance_uuid, want_objects=False,
+ expected_attrs=None):
+ return fake_instance.fake_instance_obj(context, **{'uuid': instance_uuid})
+
+
+def fake_get_not_found(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+
+class ConsoleOutputExtensionTestV21(test.NoDBTestCase):
+ application_type = "application/json"
+ action_url = '/v2/fake/servers/1/action'
+
+ def setUp(self):
+ super(ConsoleOutputExtensionTestV21, self).setUp()
+ self.stubs.Set(compute_api.API, 'get_console_output',
+ fake_get_console_output)
+ self.stubs.Set(compute_api.API, 'get', fake_get)
+ self.app = self._get_app()
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21(init_only=('servers',
+ 'os-console-output'))
+
+ def _get_response(self, length_dict=None):
+ length_dict = length_dict or {}
+ body = {'os-getConsoleOutput': length_dict}
+ req = fakes.HTTPRequest.blank(self.action_url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = self.application_type
+ res = req.get_response(self.app)
+ return res
+
+ def test_get_text_console_instance_action(self):
+ res = self._get_response()
+ output = jsonutils.loads(res.body)
+ self.assertEqual(200, res.status_int)
+ self.assertEqual({'output': '0\n1\n2\n3\n4'}, output)
+
+ def test_get_console_output_with_tail(self):
+ res = self._get_response(length_dict={'length': 3})
+ output = jsonutils.loads(res.body)
+ self.assertEqual(200, res.status_int)
+ self.assertEqual({'output': '2\n3\n4'}, output)
+
+ def test_get_console_output_with_none_length(self):
+ res = self._get_response(length_dict={'length': None})
+ output = jsonutils.loads(res.body)
+ self.assertEqual(200, res.status_int)
+ self.assertEqual({'output': '0\n1\n2\n3\n4'}, output)
+
+ def test_get_console_output_with_length_as_str(self):
+ res = self._get_response(length_dict={'length': '3'})
+ output = jsonutils.loads(res.body)
+ self.assertEqual(200, res.status_int)
+ self.assertEqual({'output': '2\n3\n4'}, output)
+
+ def test_get_console_output_filtered_characters(self):
+ self.stubs.Set(compute_api.API, 'get_console_output',
+ fake_get_console_output_all_characters)
+ res = self._get_response()
+ output = jsonutils.loads(res.body)
+ self.assertEqual(200, res.status_int)
+ expect = string.digits + string.letters + string.punctuation + ' \t\n'
+ self.assertEqual({'output': expect}, output)
+
+ def test_get_text_console_no_instance(self):
+ self.stubs.Set(compute_api.API, 'get', fake_get_not_found)
+ res = self._get_response()
+ self.assertEqual(404, res.status_int)
+
+ def test_get_text_console_no_instance_on_get_output(self):
+ self.stubs.Set(compute_api.API,
+ 'get_console_output',
+ fake_get_not_found)
+ res = self._get_response()
+ self.assertEqual(404, res.status_int)
+
+ def _get_console_output_bad_request_case(self, body):
+ req = fakes.HTTPRequest.blank(self.action_url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+ def test_get_console_output_with_non_integer_length(self):
+ body = {'os-getConsoleOutput': {'length': 'NaN'}}
+ self._get_console_output_bad_request_case(body)
+
+ def test_get_text_console_bad_body(self):
+ body = {}
+ self._get_console_output_bad_request_case(body)
+
+ def test_get_console_output_with_length_as_float(self):
+ body = {'os-getConsoleOutput': {'length': 2.5}}
+ self._get_console_output_bad_request_case(body)
+
+ def test_get_console_output_not_ready(self):
+ self.stubs.Set(compute_api.API, 'get_console_output',
+ fake_get_console_output_not_ready)
+ res = self._get_response(length_dict={'length': 3})
+ self.assertEqual(409, res.status_int)
+
+ def test_not_implemented(self):
+ self.stubs.Set(compute_api.API, 'get_console_output',
+ fakes.fake_not_implemented)
+ res = self._get_response()
+ self.assertEqual(501, res.status_int)
+
+ def test_get_console_output_with_boolean_length(self):
+ res = self._get_response(length_dict={'length': True})
+ self.assertEqual(400, res.status_int)
+
+
+class ConsoleOutputExtensionTestV2(ConsoleOutputExtensionTestV21):
+ need_osapi_compute_extension = True
+
+ def _get_app(self):
+ self.flags(osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Console_output'])
+ return fakes.wsgi_app(init_only=('servers',))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py b/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py
new file mode 100644
index 0000000000..debd1e7f5f
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py
@@ -0,0 +1,587 @@
+# Copyright 2012 OpenStack Foundation
+# 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 mock
+from oslo.serialization import jsonutils
+import webob
+
+from nova.compute import api as compute_api
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+def fake_get_vnc_console(self, _context, _instance, _console_type):
+ return {'url': 'http://fake'}
+
+
+def fake_get_spice_console(self, _context, _instance, _console_type):
+ return {'url': 'http://fake'}
+
+
+def fake_get_rdp_console(self, _context, _instance, _console_type):
+ return {'url': 'http://fake'}
+
+
+def fake_get_serial_console(self, _context, _instance, _console_type):
+ return {'url': 'http://fake'}
+
+
+def fake_get_vnc_console_invalid_type(self, _context,
+ _instance, _console_type):
+ raise exception.ConsoleTypeInvalid(console_type=_console_type)
+
+
+def fake_get_spice_console_invalid_type(self, _context,
+ _instance, _console_type):
+ raise exception.ConsoleTypeInvalid(console_type=_console_type)
+
+
+def fake_get_rdp_console_invalid_type(self, _context,
+ _instance, _console_type):
+ raise exception.ConsoleTypeInvalid(console_type=_console_type)
+
+
+def fake_get_vnc_console_type_unavailable(self, _context,
+ _instance, _console_type):
+ raise exception.ConsoleTypeUnavailable(console_type=_console_type)
+
+
+def fake_get_spice_console_type_unavailable(self, _context,
+ _instance, _console_type):
+ raise exception.ConsoleTypeUnavailable(console_type=_console_type)
+
+
+def fake_get_rdp_console_type_unavailable(self, _context,
+ _instance, _console_type):
+ raise exception.ConsoleTypeUnavailable(console_type=_console_type)
+
+
+def fake_get_vnc_console_not_ready(self, _context, instance, _console_type):
+ raise exception.InstanceNotReady(instance_id=instance["uuid"])
+
+
+def fake_get_spice_console_not_ready(self, _context, instance, _console_type):
+ raise exception.InstanceNotReady(instance_id=instance["uuid"])
+
+
+def fake_get_rdp_console_not_ready(self, _context, instance, _console_type):
+ raise exception.InstanceNotReady(instance_id=instance["uuid"])
+
+
+def fake_get_vnc_console_not_found(self, _context, instance, _console_type):
+ raise exception.InstanceNotFound(instance_id=instance["uuid"])
+
+
+def fake_get_spice_console_not_found(self, _context, instance, _console_type):
+ raise exception.InstanceNotFound(instance_id=instance["uuid"])
+
+
+def fake_get_rdp_console_not_found(self, _context, instance, _console_type):
+ raise exception.InstanceNotFound(instance_id=instance["uuid"])
+
+
+def fake_get(self, context, instance_uuid, want_objects=False,
+ expected_attrs=None):
+ return {'uuid': instance_uuid}
+
+
+def fake_get_not_found(self, context, instance_uuid, want_objects=False,
+ expected_attrs=None):
+ raise exception.InstanceNotFound(instance_id=instance_uuid)
+
+
+class ConsolesExtensionTestV21(test.NoDBTestCase):
+ url = '/v2/fake/servers/1/action'
+
+ def _setup_wsgi(self):
+ self.app = fakes.wsgi_app_v21(init_only=('servers',
+ 'os-remote-consoles'))
+
+ def setUp(self):
+ super(ConsolesExtensionTestV21, self).setUp()
+ self.stubs.Set(compute_api.API, 'get_vnc_console',
+ fake_get_vnc_console)
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console)
+ self.stubs.Set(compute_api.API, 'get_rdp_console',
+ fake_get_rdp_console)
+ self.stubs.Set(compute_api.API, 'get_serial_console',
+ fake_get_serial_console)
+ self.stubs.Set(compute_api.API, 'get', fake_get)
+ self._setup_wsgi()
+
+ def test_get_vnc_console(self):
+ body = {'os-getVNCConsole': {'type': 'novnc'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ output = jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(output,
+ {u'console': {u'url': u'http://fake', u'type': u'novnc'}})
+
+ def test_get_vnc_console_not_ready(self):
+ self.stubs.Set(compute_api.API, 'get_vnc_console',
+ fake_get_vnc_console_not_ready)
+ body = {'os-getVNCConsole': {'type': 'novnc'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 409)
+
+ def test_get_vnc_console_no_type(self):
+ self.stubs.Set(compute_api.API, 'get_vnc_console',
+ fake_get_vnc_console_invalid_type)
+ body = {'os-getVNCConsole': {}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_get_vnc_console_no_instance(self):
+ self.stubs.Set(compute_api.API, 'get', fake_get_not_found)
+ body = {'os-getVNCConsole': {'type': 'novnc'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_get_vnc_console_no_instance_on_console_get(self):
+ self.stubs.Set(compute_api.API, 'get_vnc_console',
+ fake_get_vnc_console_not_found)
+ body = {'os-getVNCConsole': {'type': 'novnc'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_get_vnc_console_invalid_type(self):
+ body = {'os-getVNCConsole': {'type': 'invalid'}}
+ self.stubs.Set(compute_api.API, 'get_vnc_console',
+ fake_get_vnc_console_invalid_type)
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_get_vnc_console_type_unavailable(self):
+ body = {'os-getVNCConsole': {'type': 'unavailable'}}
+ self.stubs.Set(compute_api.API, 'get_vnc_console',
+ fake_get_vnc_console_type_unavailable)
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+ def test_get_vnc_console_not_implemented(self):
+ self.stubs.Set(compute_api.API, 'get_vnc_console',
+ fakes.fake_not_implemented)
+
+ body = {'os-getVNCConsole': {'type': 'novnc'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 501)
+
+ def test_get_spice_console(self):
+ body = {'os-getSPICEConsole': {'type': 'spice-html5'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ output = jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(output,
+ {u'console': {u'url': u'http://fake', u'type': u'spice-html5'}})
+
+ def test_get_spice_console_not_ready(self):
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console_not_ready)
+ body = {'os-getSPICEConsole': {'type': 'spice-html5'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 409)
+
+ def test_get_spice_console_no_type(self):
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console_invalid_type)
+ body = {'os-getSPICEConsole': {}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_get_spice_console_no_instance(self):
+ self.stubs.Set(compute_api.API, 'get', fake_get_not_found)
+ body = {'os-getSPICEConsole': {'type': 'spice-html5'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_get_spice_console_no_instance_on_console_get(self):
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console_not_found)
+ body = {'os-getSPICEConsole': {'type': 'spice-html5'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_get_spice_console_invalid_type(self):
+ body = {'os-getSPICEConsole': {'type': 'invalid'}}
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console_invalid_type)
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_get_spice_console_not_implemented(self):
+ body = {'os-getSPICEConsole': {'type': 'spice-html5'}}
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fakes.fake_not_implemented)
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 501)
+
+ def test_get_spice_console_type_unavailable(self):
+ body = {'os-getSPICEConsole': {'type': 'unavailable'}}
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console_type_unavailable)
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+ def test_get_rdp_console(self):
+ body = {'os-getRDPConsole': {'type': 'rdp-html5'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ output = jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(output,
+ {u'console': {u'url': u'http://fake', u'type': u'rdp-html5'}})
+
+ def test_get_rdp_console_not_ready(self):
+ self.stubs.Set(compute_api.API, 'get_rdp_console',
+ fake_get_rdp_console_not_ready)
+ body = {'os-getRDPConsole': {'type': 'rdp-html5'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 409)
+
+ def test_get_rdp_console_no_type(self):
+ self.stubs.Set(compute_api.API, 'get_rdp_console',
+ fake_get_rdp_console_invalid_type)
+ body = {'os-getRDPConsole': {}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_get_rdp_console_no_instance(self):
+ self.stubs.Set(compute_api.API, 'get', fake_get_not_found)
+ body = {'os-getRDPConsole': {'type': 'rdp-html5'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_get_rdp_console_no_instance_on_console_get(self):
+ self.stubs.Set(compute_api.API, 'get_rdp_console',
+ fake_get_rdp_console_not_found)
+ body = {'os-getRDPConsole': {'type': 'rdp-html5'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_get_rdp_console_invalid_type(self):
+ body = {'os-getRDPConsole': {'type': 'invalid'}}
+ self.stubs.Set(compute_api.API, 'get_rdp_console',
+ fake_get_rdp_console_invalid_type)
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_get_rdp_console_type_unavailable(self):
+ body = {'os-getRDPConsole': {'type': 'unavailable'}}
+ self.stubs.Set(compute_api.API, 'get_rdp_console',
+ fake_get_rdp_console_type_unavailable)
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+ def test_get_vnc_console_with_undefined_param(self):
+ body = {'os-getVNCConsole': {'type': 'novnc', 'undefined': 'foo'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+ def test_get_spice_console_with_undefined_param(self):
+ body = {'os-getSPICEConsole': {'type': 'spice-html5',
+ 'undefined': 'foo'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+ def test_get_rdp_console_with_undefined_param(self):
+ body = {'os-getRDPConsole': {'type': 'rdp-html5', 'undefined': 'foo'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+
+class ConsolesExtensionTestV2(ConsolesExtensionTestV21):
+
+ def _setup_wsgi(self):
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Consoles'])
+ self.app = fakes.wsgi_app(init_only=('servers',))
+
+ def test_get_vnc_console_with_undefined_param(self):
+ pass
+
+ def test_get_spice_console_with_undefined_param(self):
+ pass
+
+ def test_get_rdp_console_with_undefined_param(self):
+ pass
+
+ def test_get_serial_console(self):
+ body = {'os-getSerialConsole': {'type': 'serial'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ output = jsonutils.loads(res.body)
+ self.assertEqual(200, res.status_int)
+ self.assertEqual({u'console': {u'url': u'http://fake',
+ u'type': u'serial'}},
+ output)
+
+ @mock.patch.object(compute_api.API, 'get_serial_console')
+ def test_get_serial_console_not_enable(self, get_serial_console):
+ get_serial_console.side_effect = exception.ConsoleTypeUnavailable(
+ console_type="serial")
+
+ body = {'os-getSerialConsole': {'type': 'serial'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+ self.assertTrue(get_serial_console.called)
+
+ @mock.patch.object(compute_api.API, 'get_serial_console')
+ def test_get_serial_console_invalid_type(self, get_serial_console):
+ get_serial_console.side_effect = (
+ exception.ConsoleTypeInvalid(console_type='invalid'))
+
+ body = {'os-getSerialConsole': {'type': 'invalid'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+ self.assertTrue(get_serial_console.called)
+
+ @mock.patch.object(compute_api.API, 'get_serial_console')
+ def test_get_serial_console_no_type(self, get_serial_console):
+ get_serial_console.side_effect = (
+ exception.ConsoleTypeInvalid(console_type=''))
+
+ body = {'os-getSerialConsole': {}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+ self.assertTrue(get_serial_console.called)
+
+ @mock.patch.object(compute_api.API, 'get_serial_console')
+ def test_get_serial_console_no_instance(self, get_serial_console):
+ get_serial_console.side_effect = (
+ exception.InstanceNotFound(instance_id='xxx'))
+
+ body = {'os-getSerialConsole': {'type': 'serial'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+ self.assertTrue(get_serial_console.called)
+
+ @mock.patch.object(compute_api.API, 'get_serial_console')
+ def test_get_serial_console_instance_not_ready(self, get_serial_console):
+ get_serial_console.side_effect = (
+ exception.InstanceNotReady(instance_id='xxx'))
+
+ body = {'os-getSerialConsole': {'type': 'serial'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 409)
+ self.assertTrue(get_serial_console.called)
+
+ @mock.patch.object(compute_api.API, 'get_serial_console')
+ def test_get_serial_console_socket_exhausted(self, get_serial_console):
+ get_serial_console.side_effect = (
+ exception.SocketPortRangeExhaustedException(
+ host='127.0.0.1'))
+
+ body = {'os-getSerialConsole': {'type': 'serial'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 500)
+ self.assertTrue(get_serial_console.called)
+
+ @mock.patch.object(compute_api.API, 'get_serial_console')
+ def test_get_serial_console_image_nport_invalid(self, get_serial_console):
+ get_serial_console.side_effect = (
+ exception.ImageSerialPortNumberInvalid(
+ num_ports='x', property="hw_serial_port_count"))
+
+ body = {'os-getSerialConsole': {'type': 'serial'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+ self.assertTrue(get_serial_console.called)
+
+ @mock.patch.object(compute_api.API, 'get_serial_console')
+ def test_get_serial_console_image_nport_exceed(self, get_serial_console):
+ get_serial_console.side_effect = (
+ exception.ImageSerialPortNumberExceedFlavorValue())
+
+ body = {'os-getSerialConsole': {'type': 'serial'}}
+ req = webob.Request.blank(self.url)
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+ self.assertTrue(get_serial_console.called)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_createserverext.py b/nova/tests/unit/api/openstack/compute/contrib/test_createserverext.py
new file mode 100644
index 0000000000..eca3aa3953
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_createserverext.py
@@ -0,0 +1,387 @@
+# Copyright 2010-2011 OpenStack Foundation
+# 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 base64
+from xml.dom import minidom
+
+from oslo.serialization import jsonutils
+import webob
+
+from nova.compute import api as compute_api
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+FAKE_UUID = fakes.FAKE_UUID
+
+FAKE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'),
+ ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '10.0.2.12')]
+
+DUPLICATE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'),
+ ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12')]
+
+INVALID_NETWORKS = [('invalid', 'invalid-ip-address')]
+
+
+def return_security_group_non_existing(context, project_id, group_name):
+ raise exception.SecurityGroupNotFoundForProject(project_id=project_id,
+ security_group_id=group_name)
+
+
+def return_security_group_get_by_name(context, project_id, group_name):
+ return {'id': 1, 'name': group_name}
+
+
+def return_security_group_get(context, security_group_id, session):
+ return {'id': security_group_id}
+
+
+def return_instance_add_security_group(context, instance_id,
+ security_group_id):
+ pass
+
+
+class CreateserverextTest(test.TestCase):
+ def setUp(self):
+ super(CreateserverextTest, self).setUp()
+
+ self.security_group = None
+ self.injected_files = None
+ self.networks = None
+ self.user_data = None
+
+ def create(*args, **kwargs):
+ if 'security_group' in kwargs:
+ self.security_group = kwargs['security_group']
+ else:
+ self.security_group = None
+ if 'injected_files' in kwargs:
+ self.injected_files = kwargs['injected_files']
+ else:
+ self.injected_files = None
+
+ if 'requested_networks' in kwargs:
+ self.networks = kwargs['requested_networks']
+ else:
+ self.networks = None
+
+ if 'user_data' in kwargs:
+ self.user_data = kwargs['user_data']
+
+ resv_id = None
+
+ return ([{'id': '1234', 'display_name': 'fakeinstance',
+ 'uuid': FAKE_UUID,
+ 'user_id': 'fake',
+ 'project_id': 'fake',
+ 'created_at': "",
+ 'updated_at': "",
+ 'fixed_ips': [],
+ 'progress': 0}], resv_id)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Createserverext', 'User_data',
+ 'Security_groups', 'Os_networks'])
+
+ def _make_stub_method(self, canned_return):
+ def stub_method(*args, **kwargs):
+ return canned_return
+ return stub_method
+
+ def _create_security_group_request_dict(self, security_groups):
+ server = {}
+ server['name'] = 'new-server-test'
+ server['imageRef'] = 'cedef40a-ed67-4d10-800e-17455edce175'
+ server['flavorRef'] = 1
+ if security_groups is not None:
+ sg_list = []
+ for name in security_groups:
+ sg_list.append({'name': name})
+ server['security_groups'] = sg_list
+ return {'server': server}
+
+ def _create_networks_request_dict(self, networks):
+ server = {}
+ server['name'] = 'new-server-test'
+ server['imageRef'] = 'cedef40a-ed67-4d10-800e-17455edce175'
+ server['flavorRef'] = 1
+ if networks is not None:
+ network_list = []
+ for uuid, fixed_ip in networks:
+ network_list.append({'uuid': uuid, 'fixed_ip': fixed_ip})
+ server['networks'] = network_list
+ return {'server': server}
+
+ def _create_user_data_request_dict(self, user_data):
+ server = {}
+ server['name'] = 'new-server-test'
+ server['imageRef'] = 'cedef40a-ed67-4d10-800e-17455edce175'
+ server['flavorRef'] = 1
+ server['user_data'] = user_data
+ return {'server': server}
+
+ def _get_create_request_json(self, body_dict):
+ req = webob.Request.blank('/v2/fake/os-create-server-ext')
+ req.headers['Content-Type'] = 'application/json'
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body_dict)
+ return req
+
+ def _format_xml_request_body(self, body_dict):
+ server = body_dict['server']
+ body_parts = []
+ body_parts.extend([
+ '<?xml version="1.0" encoding="UTF-8"?>',
+ '<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.1"',
+ ' name="%s" imageRef="%s" flavorRef="%s">' % (
+ server['name'], server['imageRef'], server['flavorRef'])])
+ if 'metadata' in server:
+ metadata = server['metadata']
+ body_parts.append('<metadata>')
+ for item in metadata.iteritems():
+ body_parts.append('<meta key="%s">%s</meta>' % item)
+ body_parts.append('</metadata>')
+ if 'personality' in server:
+ personalities = server['personality']
+ body_parts.append('<personality>')
+ for file in personalities:
+ item = (file['path'], file['contents'])
+ body_parts.append('<file path="%s">%s</file>' % item)
+ body_parts.append('</personality>')
+ if 'networks' in server:
+ networks = server['networks']
+ body_parts.append('<networks>')
+ for network in networks:
+ item = (network['uuid'], network['fixed_ip'])
+ body_parts.append('<network uuid="%s" fixed_ip="%s"></network>'
+ % item)
+ body_parts.append('</networks>')
+ body_parts.append('</server>')
+ return ''.join(body_parts)
+
+ def _get_create_request_xml(self, body_dict):
+ req = webob.Request.blank('/v2/fake/os-create-server-ext')
+ req.content_type = 'application/xml'
+ req.accept = 'application/xml'
+ req.method = 'POST'
+ req.body = self._format_xml_request_body(body_dict)
+ return req
+
+ def _create_instance_with_networks_json(self, networks):
+ body_dict = self._create_networks_request_dict(networks)
+ request = self._get_create_request_json(body_dict)
+ response = request.get_response(fakes.wsgi_app(
+ init_only=('servers', 'os-create-server-ext')))
+ return request, response, self.networks
+
+ def _create_instance_with_user_data_json(self, networks):
+ body_dict = self._create_user_data_request_dict(networks)
+ request = self._get_create_request_json(body_dict)
+ response = request.get_response(fakes.wsgi_app(
+ init_only=('servers', 'os-create-server-ext')))
+ return request, response, self.user_data
+
+ def _create_instance_with_networks_xml(self, networks):
+ body_dict = self._create_networks_request_dict(networks)
+ request = self._get_create_request_xml(body_dict)
+ response = request.get_response(fakes.wsgi_app(
+ init_only=('servers', 'os-create-server-ext')))
+ return request, response, self.networks
+
+ def test_create_instance_with_no_networks(self):
+ _create_inst = self._create_instance_with_networks_json
+ request, response, networks = _create_inst(networks=None)
+ self.assertEqual(response.status_int, 202)
+ self.assertIsNone(networks)
+
+ def test_create_instance_with_no_networks_xml(self):
+ _create_inst = self._create_instance_with_networks_xml
+ request, response, networks = _create_inst(networks=None)
+ self.assertEqual(response.status_int, 202)
+ self.assertIsNone(networks)
+
+ def test_create_instance_with_one_network(self):
+ _create_inst = self._create_instance_with_networks_json
+ request, response, networks = _create_inst([FAKE_NETWORKS[0]])
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual([FAKE_NETWORKS[0]], networks.as_tuples())
+
+ def test_create_instance_with_one_network_xml(self):
+ _create_inst = self._create_instance_with_networks_xml
+ request, response, networks = _create_inst([FAKE_NETWORKS[0]])
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual([FAKE_NETWORKS[0]], networks.as_tuples())
+
+ def test_create_instance_with_two_networks(self):
+ _create_inst = self._create_instance_with_networks_json
+ request, response, networks = _create_inst(FAKE_NETWORKS)
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual(FAKE_NETWORKS, networks.as_tuples())
+
+ def test_create_instance_with_two_networks_xml(self):
+ _create_inst = self._create_instance_with_networks_xml
+ request, response, networks = _create_inst(FAKE_NETWORKS)
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual(FAKE_NETWORKS, networks.as_tuples())
+
+ def test_create_instance_with_duplicate_networks(self):
+ _create_inst = self._create_instance_with_networks_json
+ request, response, networks = _create_inst(DUPLICATE_NETWORKS)
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(networks)
+
+ def test_create_instance_with_duplicate_networks_xml(self):
+ _create_inst = self._create_instance_with_networks_xml
+ request, response, networks = _create_inst(DUPLICATE_NETWORKS)
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(networks)
+
+ def test_create_instance_with_network_no_id(self):
+ body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
+ del body_dict['server']['networks'][0]['uuid']
+ request = self._get_create_request_json(body_dict)
+ response = request.get_response(fakes.wsgi_app(
+ init_only=('servers', 'os-create-server-ext')))
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(self.networks)
+
+ def test_create_instance_with_network_no_id_xml(self):
+ body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
+ request = self._get_create_request_xml(body_dict)
+ uuid = ' uuid="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"'
+ request.body = request.body.replace(uuid, '')
+ response = request.get_response(fakes.wsgi_app(
+ init_only=('servers', 'os-create-server-ext')))
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(self.networks)
+
+ def test_create_instance_with_network_invalid_id(self):
+ _create_inst = self._create_instance_with_networks_json
+ request, response, networks = _create_inst(INVALID_NETWORKS)
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(networks)
+
+ def test_create_instance_with_network_invalid_id_xml(self):
+ _create_inst = self._create_instance_with_networks_xml
+ request, response, networks = _create_inst(INVALID_NETWORKS)
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(networks)
+
+ def test_create_instance_with_network_empty_fixed_ip(self):
+ networks = [('1', '')]
+ _create_inst = self._create_instance_with_networks_json
+ request, response, networks = _create_inst(networks)
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(networks)
+
+ def test_create_instance_with_network_non_string_fixed_ip(self):
+ networks = [('1', 12345)]
+ _create_inst = self._create_instance_with_networks_json
+ request, response, networks = _create_inst(networks)
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(networks)
+
+ def test_create_instance_with_network_empty_fixed_ip_xml(self):
+ networks = [('1', '')]
+ _create_inst = self._create_instance_with_networks_xml
+ request, response, networks = _create_inst(networks)
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(networks)
+
+ def test_create_instance_with_network_no_fixed_ip(self):
+ body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
+ del body_dict['server']['networks'][0]['fixed_ip']
+ request = self._get_create_request_json(body_dict)
+ response = request.get_response(fakes.wsgi_app(
+ init_only=('servers', 'os-create-server-ext')))
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual([('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)],
+ self.networks.as_tuples())
+
+ def test_create_instance_with_network_no_fixed_ip_xml(self):
+ body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
+ request = self._get_create_request_xml(body_dict)
+ request.body = request.body.replace(' fixed_ip="10.0.1.12"', '')
+ response = request.get_response(fakes.wsgi_app(
+ init_only=('servers', 'os-create-server-ext')))
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual([('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)],
+ self.networks.as_tuples())
+
+ def test_create_instance_with_userdata(self):
+ user_data_contents = '#!/bin/bash\necho "Oh no!"\n'
+ user_data_contents = base64.b64encode(user_data_contents)
+ _create_inst = self._create_instance_with_user_data_json
+ request, response, user_data = _create_inst(user_data_contents)
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual(user_data, user_data_contents)
+
+ def test_create_instance_with_userdata_none(self):
+ user_data_contents = None
+ _create_inst = self._create_instance_with_user_data_json
+ request, response, user_data = _create_inst(user_data_contents)
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual(user_data, user_data_contents)
+
+ def test_create_instance_with_userdata_with_non_b64_content(self):
+ user_data_contents = '#!/bin/bash\necho "Oh no!"\n'
+ _create_inst = self._create_instance_with_user_data_json
+ request, response, user_data = _create_inst(user_data_contents)
+ self.assertEqual(response.status_int, 400)
+ self.assertIsNone(user_data)
+
+ def test_create_instance_with_security_group_json(self):
+ security_groups = ['test', 'test1']
+ self.stubs.Set(db, 'security_group_get_by_name',
+ return_security_group_get_by_name)
+ self.stubs.Set(db, 'instance_add_security_group',
+ return_instance_add_security_group)
+ body_dict = self._create_security_group_request_dict(security_groups)
+ request = self._get_create_request_json(body_dict)
+ response = request.get_response(fakes.wsgi_app(
+ init_only=('servers', 'os-create-server-ext')))
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual(self.security_group, security_groups)
+
+ def test_get_server_by_id_verify_security_groups_json(self):
+ self.stubs.Set(db, 'instance_get', fakes.fake_instance_get())
+ self.stubs.Set(db, 'instance_get_by_uuid', fakes.fake_instance_get())
+ req = webob.Request.blank('/v2/fake/os-create-server-ext/1')
+ req.headers['Content-Type'] = 'application/json'
+ response = req.get_response(fakes.wsgi_app(
+ init_only=('os-create-server-ext', 'servers')))
+ self.assertEqual(response.status_int, 200)
+ res_dict = jsonutils.loads(response.body)
+ expected_security_group = [{"name": "test"}]
+ self.assertEqual(res_dict['server'].get('security_groups'),
+ expected_security_group)
+
+ def test_get_server_by_id_verify_security_groups_xml(self):
+ self.stubs.Set(db, 'instance_get', fakes.fake_instance_get())
+ self.stubs.Set(db, 'instance_get_by_uuid', fakes.fake_instance_get())
+ req = webob.Request.blank('/v2/fake/os-create-server-ext/1')
+ req.headers['Accept'] = 'application/xml'
+ response = req.get_response(fakes.wsgi_app(
+ init_only=('os-create-server-ext', 'servers')))
+ self.assertEqual(response.status_int, 200)
+ dom = minidom.parseString(response.body)
+ server = dom.childNodes[0]
+ sec_groups = server.getElementsByTagName('security_groups')[0]
+ sec_group = sec_groups.getElementsByTagName('security_group')[0]
+ self.assertEqual('test', sec_group.getAttribute("name"))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_deferred_delete.py b/nova/tests/unit/api/openstack/compute/contrib/test_deferred_delete.py
new file mode 100644
index 0000000000..0dfd0e5339
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_deferred_delete.py
@@ -0,0 +1,147 @@
+# Copyright 2011 OpenStack Foundation
+# 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 mock
+import webob
+
+from nova.api.openstack.compute.contrib import deferred_delete
+from nova.api.openstack.compute.plugins.v3 import deferred_delete as dd_v21
+from nova.compute import api as compute_api
+from nova import context
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+class FakeRequest(object):
+ def __init__(self, context):
+ self.environ = {'nova.context': context}
+
+
+class DeferredDeleteExtensionTestV21(test.NoDBTestCase):
+ ext_ver = dd_v21.DeferredDeleteController
+
+ def setUp(self):
+ super(DeferredDeleteExtensionTestV21, self).setUp()
+ self.fake_input_dict = {}
+ self.fake_uuid = 'fake_uuid'
+ self.fake_context = context.RequestContext('fake', 'fake')
+ self.fake_req = FakeRequest(self.fake_context)
+ self.extension = self.ext_ver()
+
+ def test_force_delete(self):
+ self.mox.StubOutWithMock(compute_api.API, 'get')
+ self.mox.StubOutWithMock(compute_api.API, 'force_delete')
+
+ fake_instance = 'fake_instance'
+
+ compute_api.API.get(self.fake_context, self.fake_uuid,
+ expected_attrs=None,
+ want_objects=True).AndReturn(fake_instance)
+ compute_api.API.force_delete(self.fake_context, fake_instance)
+
+ self.mox.ReplayAll()
+ res = self.extension._force_delete(self.fake_req, self.fake_uuid,
+ self.fake_input_dict)
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.extension, dd_v21.DeferredDeleteController):
+ status_int = self.extension._force_delete.wsgi_code
+ else:
+ status_int = res.status_int
+ self.assertEqual(202, status_int)
+
+ def test_force_delete_instance_not_found(self):
+ self.mox.StubOutWithMock(compute_api.API, 'get')
+
+ compute_api.API.get(self.fake_context, self.fake_uuid,
+ expected_attrs=None,
+ want_objects=True).AndRaise(
+ exception.InstanceNotFound(instance_id='instance-0000'))
+
+ self.mox.ReplayAll()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.extension._force_delete,
+ self.fake_req,
+ self.fake_uuid,
+ self.fake_input_dict)
+
+ @mock.patch.object(compute_api.API, 'get')
+ @mock.patch.object(compute_api.API, 'force_delete',
+ side_effect=exception.InstanceIsLocked(
+ instance_uuid='fake_uuid'))
+ def test_force_delete_instance_locked(self, mock_force_delete, mock_get):
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/fake_uuid/action')
+ ex = self.assertRaises(webob.exc.HTTPConflict,
+ self.extension._force_delete,
+ req, 'fake_uuid', '')
+ self.assertIn('Instance fake_uuid is locked', ex.explanation)
+
+ def test_restore(self):
+ self.mox.StubOutWithMock(compute_api.API, 'get')
+ self.mox.StubOutWithMock(compute_api.API, 'restore')
+
+ fake_instance = 'fake_instance'
+
+ compute_api.API.get(self.fake_context, self.fake_uuid,
+ expected_attrs=None,
+ want_objects=True).AndReturn(fake_instance)
+ compute_api.API.restore(self.fake_context, fake_instance)
+
+ self.mox.ReplayAll()
+ res = self.extension._restore(self.fake_req, self.fake_uuid,
+ self.fake_input_dict)
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.extension, dd_v21.DeferredDeleteController):
+ status_int = self.extension._restore.wsgi_code
+ else:
+ status_int = res.status_int
+ self.assertEqual(202, status_int)
+
+ def test_restore_instance_not_found(self):
+ self.mox.StubOutWithMock(compute_api.API, 'get')
+
+ compute_api.API.get(self.fake_context, self.fake_uuid,
+ expected_attrs=None, want_objects=True).AndRaise(
+ exception.InstanceNotFound(instance_id='instance-0000'))
+
+ self.mox.ReplayAll()
+ self.assertRaises(webob.exc.HTTPNotFound, self.extension._restore,
+ self.fake_req, self.fake_uuid,
+ self.fake_input_dict)
+
+ def test_restore_raises_conflict_on_invalid_state(self):
+ self.mox.StubOutWithMock(compute_api.API, 'get')
+ self.mox.StubOutWithMock(compute_api.API, 'restore')
+
+ fake_instance = 'fake_instance'
+ exc = exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ compute_api.API.get(self.fake_context, self.fake_uuid,
+ expected_attrs=None,
+ want_objects=True).AndReturn(fake_instance)
+ compute_api.API.restore(self.fake_context, fake_instance).AndRaise(
+ exc)
+
+ self.mox.ReplayAll()
+ self.assertRaises(webob.exc.HTTPConflict, self.extension._restore,
+ self.fake_req, self.fake_uuid, self.fake_input_dict)
+
+
+class DeferredDeleteExtensionTestV2(DeferredDeleteExtensionTestV21):
+ ext_ver = deferred_delete.DeferredDeleteController
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_disk_config.py b/nova/tests/unit/api/openstack/compute/contrib/test_disk_config.py
new file mode 100644
index 0000000000..b9a514a451
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_disk_config.py
@@ -0,0 +1,449 @@
+# Copyright 2011 OpenStack Foundation
+# 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 datetime
+
+from oslo.serialization import jsonutils
+
+from nova.api.openstack import compute
+from nova.compute import api as compute_api
+from nova import db
+from nova import objects
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+import nova.tests.unit.image.fake
+
+
+MANUAL_INSTANCE_UUID = fakes.FAKE_UUID
+AUTO_INSTANCE_UUID = fakes.FAKE_UUID.replace('a', 'b')
+
+stub_instance = fakes.stub_instance
+
+API_DISK_CONFIG = 'OS-DCF:diskConfig'
+
+
+def instance_addresses(context, instance_id):
+ return None
+
+
+class DiskConfigTestCaseV21(test.TestCase):
+
+ def setUp(self):
+ super(DiskConfigTestCaseV21, self).setUp()
+ self._set_up_app()
+ self._setup_fake_image_service()
+
+ fakes.stub_out_nw_api(self.stubs)
+
+ FAKE_INSTANCES = [
+ fakes.stub_instance(1,
+ uuid=MANUAL_INSTANCE_UUID,
+ auto_disk_config=False),
+ fakes.stub_instance(2,
+ uuid=AUTO_INSTANCE_UUID,
+ auto_disk_config=True)
+ ]
+
+ def fake_instance_get(context, id_):
+ for instance in FAKE_INSTANCES:
+ if id_ == instance['id']:
+ return instance
+
+ self.stubs.Set(db, 'instance_get', fake_instance_get)
+
+ def fake_instance_get_by_uuid(context, uuid,
+ columns_to_join=None, use_slave=False):
+ for instance in FAKE_INSTANCES:
+ if uuid == instance['uuid']:
+ return instance
+
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fake_instance_get_by_uuid)
+
+ def fake_instance_get_all(context, *args, **kwargs):
+ return FAKE_INSTANCES
+
+ self.stubs.Set(db, 'instance_get_all', fake_instance_get_all)
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_instance_get_all)
+
+ self.stubs.Set(objects.Instance, 'save',
+ lambda *args, **kwargs: None)
+
+ def fake_rebuild(*args, **kwargs):
+ pass
+
+ self.stubs.Set(compute_api.API, 'rebuild', fake_rebuild)
+
+ def fake_instance_create(context, inst_, session=None):
+ inst = fake_instance.fake_db_instance(**{
+ 'id': 1,
+ 'uuid': AUTO_INSTANCE_UUID,
+ 'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0),
+ 'updated_at': datetime.datetime(2010, 10, 10, 12, 0, 0),
+ 'progress': 0,
+ 'name': 'instance-1', # this is a property
+ 'task_state': '',
+ 'vm_state': '',
+ 'auto_disk_config': inst_['auto_disk_config'],
+ 'security_groups': inst_['security_groups'],
+ })
+
+ def fake_instance_get_for_create(context, id_, *args, **kwargs):
+ return (inst, inst)
+
+ self.stubs.Set(db, 'instance_update_and_get_original',
+ fake_instance_get_for_create)
+
+ def fake_instance_get_all_for_create(context, *args, **kwargs):
+ return [inst]
+ self.stubs.Set(db, 'instance_get_all',
+ fake_instance_get_all_for_create)
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_instance_get_all_for_create)
+
+ def fake_instance_add_security_group(context, instance_id,
+ security_group_id):
+ pass
+
+ self.stubs.Set(db,
+ 'instance_add_security_group',
+ fake_instance_add_security_group)
+
+ return inst
+
+ self.stubs.Set(db, 'instance_create', fake_instance_create)
+
+ def _set_up_app(self):
+ self.app = compute.APIRouterV21(init_only=('servers', 'images',
+ 'os-disk-config'))
+
+ def _get_expected_msg_for_invalid_disk_config(self):
+ return ('{{"badRequest": {{"message": "Invalid input for'
+ ' field/attribute {0}. Value: {1}. u\'{1}\' is'
+ ' not one of [\'AUTO\', \'MANUAL\']", "code": 400}}}}')
+
+ def _setup_fake_image_service(self):
+ self.image_service = nova.tests.unit.image.fake.stub_out_image_service(
+ self.stubs)
+ timestamp = datetime.datetime(2011, 1, 1, 1, 2, 3)
+ image = {'id': '88580842-f50a-11e2-8d3a-f23c91aec05e',
+ 'name': 'fakeimage7',
+ 'created_at': timestamp,
+ 'updated_at': timestamp,
+ 'deleted_at': None,
+ 'deleted': False,
+ 'status': 'active',
+ 'is_public': False,
+ 'container_format': 'ova',
+ 'disk_format': 'vhd',
+ 'size': '74185822',
+ 'properties': {'auto_disk_config': 'Disabled'}}
+ self.image_service.create(None, image)
+
+ def tearDown(self):
+ super(DiskConfigTestCaseV21, self).tearDown()
+ nova.tests.unit.image.fake.FakeImageService_reset()
+
+ def assertDiskConfig(self, dict_, value):
+ self.assertIn(API_DISK_CONFIG, dict_)
+ self.assertEqual(dict_[API_DISK_CONFIG], value)
+
+ def test_show_server(self):
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers/%s' % MANUAL_INSTANCE_UUID)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'MANUAL')
+
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers/%s' % AUTO_INSTANCE_UUID)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'AUTO')
+
+ def test_detail_servers(self):
+ req = fakes.HTTPRequest.blank('/fake/servers/detail')
+ res = req.get_response(self.app)
+ server_dicts = jsonutils.loads(res.body)['servers']
+
+ expectations = ['MANUAL', 'AUTO']
+ for server_dict, expected in zip(server_dicts, expectations):
+ self.assertDiskConfig(server_dict, expected)
+
+ def test_show_image(self):
+ req = fakes.HTTPRequest.blank(
+ '/fake/images/a440c04b-79fa-479c-bed1-0b816eaec379')
+ res = req.get_response(self.app)
+ image_dict = jsonutils.loads(res.body)['image']
+ self.assertDiskConfig(image_dict, 'MANUAL')
+
+ req = fakes.HTTPRequest.blank(
+ '/fake/images/70a599e0-31e7-49b7-b260-868f441e862b')
+ res = req.get_response(self.app)
+ image_dict = jsonutils.loads(res.body)['image']
+ self.assertDiskConfig(image_dict, 'AUTO')
+
+ def test_detail_image(self):
+ req = fakes.HTTPRequest.blank('/fake/images/detail')
+ res = req.get_response(self.app)
+ image_dicts = jsonutils.loads(res.body)['images']
+
+ expectations = ['MANUAL', 'AUTO']
+ for image_dict, expected in zip(image_dicts, expectations):
+ # NOTE(sirp): image fixtures 6 and 7 are setup for
+ # auto_disk_config testing
+ if image_dict['id'] in (6, 7):
+ self.assertDiskConfig(image_dict, expected)
+
+ def test_create_server_override_auto(self):
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'server': {
+ 'name': 'server_test',
+ 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175',
+ 'flavorRef': '1',
+ API_DISK_CONFIG: 'AUTO'
+ }}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'AUTO')
+
+ def test_create_server_override_manual(self):
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'server': {
+ 'name': 'server_test',
+ 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175',
+ 'flavorRef': '1',
+ API_DISK_CONFIG: 'MANUAL'
+ }}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'MANUAL')
+
+ def test_create_server_detect_from_image(self):
+ """If user doesn't pass in diskConfig for server, use image metadata
+ to specify AUTO or MANUAL.
+ """
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'server': {
+ 'name': 'server_test',
+ 'imageRef': 'a440c04b-79fa-479c-bed1-0b816eaec379',
+ 'flavorRef': '1',
+ }}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'MANUAL')
+
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'server': {
+ 'name': 'server_test',
+ 'imageRef': '70a599e0-31e7-49b7-b260-868f441e862b',
+ 'flavorRef': '1',
+ }}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'AUTO')
+
+ def test_create_server_detect_from_image_disabled_goes_to_manual(self):
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'server': {
+ 'name': 'server_test',
+ 'imageRef': '88580842-f50a-11e2-8d3a-f23c91aec05e',
+ 'flavorRef': '1',
+ }}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'MANUAL')
+
+ def test_create_server_errors_when_disabled_and_auto(self):
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'server': {
+ 'name': 'server_test',
+ 'imageRef': '88580842-f50a-11e2-8d3a-f23c91aec05e',
+ 'flavorRef': '1',
+ API_DISK_CONFIG: 'AUTO'
+ }}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_create_server_when_disabled_and_manual(self):
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'server': {
+ 'name': 'server_test',
+ 'imageRef': '88580842-f50a-11e2-8d3a-f23c91aec05e',
+ 'flavorRef': '1',
+ API_DISK_CONFIG: 'MANUAL'
+ }}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'MANUAL')
+
+ def _test_update_server_disk_config(self, uuid, disk_config):
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers/%s' % uuid)
+ req.method = 'PUT'
+ req.content_type = 'application/json'
+ body = {'server': {API_DISK_CONFIG: disk_config}}
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, disk_config)
+
+ def test_update_server_override_auto(self):
+ self._test_update_server_disk_config(AUTO_INSTANCE_UUID, 'AUTO')
+
+ def test_update_server_override_manual(self):
+ self._test_update_server_disk_config(MANUAL_INSTANCE_UUID, 'MANUAL')
+
+ def test_update_server_invalid_disk_config(self):
+ # Return BadRequest if user passes an invalid diskConfig value.
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers/%s' % MANUAL_INSTANCE_UUID)
+ req.method = 'PUT'
+ req.content_type = 'application/json'
+ body = {'server': {API_DISK_CONFIG: 'server_test'}}
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+ expected_msg = self._get_expected_msg_for_invalid_disk_config()
+ self.assertEqual(expected_msg.format(API_DISK_CONFIG, 'server_test'),
+ res.body)
+
+ def _test_rebuild_server_disk_config(self, uuid, disk_config):
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers/%s/action' % uuid)
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {"rebuild": {
+ 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175',
+ API_DISK_CONFIG: disk_config
+ }}
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, disk_config)
+
+ def test_rebuild_server_override_auto(self):
+ self._test_rebuild_server_disk_config(AUTO_INSTANCE_UUID, 'AUTO')
+
+ def test_rebuild_server_override_manual(self):
+ self._test_rebuild_server_disk_config(MANUAL_INSTANCE_UUID, 'MANUAL')
+
+ def test_create_server_with_auto_disk_config(self):
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'server': {
+ 'name': 'server_test',
+ 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175',
+ 'flavorRef': '1',
+ API_DISK_CONFIG: 'AUTO'
+ }}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIn('auto_disk_config', kwargs)
+ self.assertEqual(True, kwargs['auto_disk_config'])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'AUTO')
+
+ def test_rebuild_server_with_auto_disk_config(self):
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers/%s/action' % AUTO_INSTANCE_UUID)
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {"rebuild": {
+ 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175',
+ API_DISK_CONFIG: 'AUTO'
+ }}
+
+ def rebuild(*args, **kwargs):
+ self.assertIn('auto_disk_config', kwargs)
+ self.assertEqual(True, kwargs['auto_disk_config'])
+
+ self.stubs.Set(compute_api.API, 'rebuild', rebuild)
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ server_dict = jsonutils.loads(res.body)['server']
+ self.assertDiskConfig(server_dict, 'AUTO')
+
+ def test_resize_server_with_auto_disk_config(self):
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers/%s/action' % AUTO_INSTANCE_UUID)
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {"resize": {
+ "flavorRef": "3",
+ API_DISK_CONFIG: 'AUTO'
+ }}
+
+ def resize(*args, **kwargs):
+ self.assertIn('auto_disk_config', kwargs)
+ self.assertEqual(True, kwargs['auto_disk_config'])
+
+ self.stubs.Set(compute_api.API, 'resize', resize)
+
+ req.body = jsonutils.dumps(body)
+ req.get_response(self.app)
+
+
+class DiskConfigTestCaseV2(DiskConfigTestCaseV21):
+ def _set_up_app(self):
+ self.flags(verbose=True,
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Disk_config'])
+
+ self.app = compute.APIRouter(init_only=('servers', 'images'))
+
+ def _get_expected_msg_for_invalid_disk_config(self):
+ return ('{{"badRequest": {{"message": "{0} must be either'
+ ' \'MANUAL\' or \'AUTO\'.", "code": 400}}}}')
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_evacuate.py b/nova/tests/unit/api/openstack/compute/contrib/test_evacuate.py
new file mode 100644
index 0000000000..3f5b662db5
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_evacuate.py
@@ -0,0 +1,268 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 uuid
+
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.compute import api as compute_api
+from nova.compute import vm_states
+from nova import context
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+
+CONF = cfg.CONF
+CONF.import_opt('password_length', 'nova.utils')
+
+
+def fake_compute_api(*args, **kwargs):
+ return True
+
+
+def fake_compute_api_get(self, context, instance_id, want_objects=False,
+ **kwargs):
+ # BAD_UUID is something that does not exist
+ if instance_id == 'BAD_UUID':
+ raise exception.InstanceNotFound(instance_id=instance_id)
+ else:
+ return fake_instance.fake_instance_obj(context, id=1, uuid=instance_id,
+ task_state=None, host='host1',
+ vm_state=vm_states.ACTIVE)
+
+
+def fake_service_get_by_compute_host(self, context, host):
+ if host == 'bad-host':
+ raise exception.ComputeHostNotFound(host=host)
+ else:
+ return {
+ 'host_name': host,
+ 'service': 'compute',
+ 'zone': 'nova'
+ }
+
+
+class EvacuateTestV21(test.NoDBTestCase):
+
+ _methods = ('resize', 'evacuate')
+ fake_url = '/v2/fake'
+
+ def setUp(self):
+ super(EvacuateTestV21, self).setUp()
+ self.stubs.Set(compute_api.API, 'get', fake_compute_api_get)
+ self.stubs.Set(compute_api.HostAPI, 'service_get_by_compute_host',
+ fake_service_get_by_compute_host)
+ self.UUID = uuid.uuid4()
+ for _method in self._methods:
+ self.stubs.Set(compute_api.API, _method, fake_compute_api)
+
+ def _fake_wsgi_app(self, ctxt):
+ return fakes.wsgi_app_v21(fake_auth_context=ctxt)
+
+ def _gen_resource_with_app(self, json_load, is_admin=True, uuid=None):
+ ctxt = context.get_admin_context()
+ ctxt.user_id = 'fake'
+ ctxt.project_id = 'fake'
+ ctxt.is_admin = is_admin
+ app = self._fake_wsgi_app(ctxt)
+ req = webob.Request.blank('%s/servers/%s/action' % (self.fake_url,
+ uuid or self.UUID))
+ req.method = 'POST'
+ base_json_load = {'evacuate': json_load}
+ req.body = jsonutils.dumps(base_json_load)
+ req.content_type = 'application/json'
+
+ return req.get_response(app)
+
+ def _fake_update(self, inst, context, instance, task_state,
+ expected_task_state):
+ return None
+
+ def test_evacuate_with_valid_instance(self):
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+
+ self.assertEqual(res.status_int, 200)
+
+ def test_evacuate_with_invalid_instance(self):
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'},
+ uuid='BAD_UUID')
+
+ self.assertEqual(res.status_int, 404)
+
+ def test_evacuate_with_active_service(self):
+ def fake_evacuate(*args, **kwargs):
+ raise exception.ComputeServiceInUse("Service still in use")
+
+ self.stubs.Set(compute_api.API, 'evacuate', fake_evacuate)
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(res.status_int, 400)
+
+ def test_evacuate_instance_with_no_target(self):
+ res = self._gen_resource_with_app({'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(200, res.status_int)
+
+ def test_evacuate_instance_without_on_shared_storage(self):
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(res.status_int, 400)
+
+ def test_evacuate_instance_with_invalid_characters_host(self):
+ host = 'abc!#'
+ res = self._gen_resource_with_app({'host': host,
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(400, res.status_int)
+
+ def test_evacuate_instance_with_too_long_host(self):
+ host = 'a' * 256
+ res = self._gen_resource_with_app({'host': host,
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(400, res.status_int)
+
+ def test_evacuate_instance_with_invalid_on_shared_storage(self):
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'onSharedStorage': 'foo',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(400, res.status_int)
+
+ def test_evacuate_instance_with_bad_target(self):
+ res = self._gen_resource_with_app({'host': 'bad-host',
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(res.status_int, 404)
+
+ def test_evacuate_instance_with_target(self):
+ self.stubs.Set(compute_api.API, 'update', self._fake_update)
+
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(res.status_int, 200)
+ resp_json = jsonutils.loads(res.body)
+ self.assertEqual("MyNewPass", resp_json['adminPass'])
+
+ def test_evacuate_shared_and_pass(self):
+ self.stubs.Set(compute_api.API, 'update', self._fake_update)
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'onSharedStorage': 'True',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(res.status_int, 400)
+
+ def test_evacuate_not_shared_pass_generated(self):
+ self.stubs.Set(compute_api.API, 'update', self._fake_update)
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'onSharedStorage': 'False'})
+ self.assertEqual(res.status_int, 200)
+ resp_json = jsonutils.loads(res.body)
+ self.assertEqual(CONF.password_length, len(resp_json['adminPass']))
+
+ def test_evacuate_shared(self):
+ self.stubs.Set(compute_api.API, 'update', self._fake_update)
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'onSharedStorage': 'True'})
+ self.assertEqual(res.status_int, 200)
+
+ def test_not_admin(self):
+ res = self._gen_resource_with_app({'host': 'my-host',
+ 'onSharedStorage': 'True'},
+ is_admin=False)
+ self.assertEqual(res.status_int, 403)
+
+ def test_evacuate_to_same_host(self):
+ res = self._gen_resource_with_app({'host': 'host1',
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(res.status_int, 400)
+
+ def test_evacuate_instance_with_empty_host(self):
+ res = self._gen_resource_with_app({'host': '',
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(400, res.status_int)
+
+ def test_evacuate_instance_with_underscore_in_hostname(self):
+ # NOTE: The hostname grammar in RFC952 does not allow for
+ # underscores in hostnames. However, we should test that it
+ # is supported because it sometimes occurs in real systems.
+ self.stubs.Set(compute_api.API, 'update', self._fake_update)
+ res = self._gen_resource_with_app({'host': 'underscore_hostname',
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(200, res.status_int)
+ resp_json = jsonutils.loads(res.body)
+ self.assertEqual("MyNewPass", resp_json['adminPass'])
+
+ def test_evacuate_disable_password_return(self):
+ self._test_evacuate_enable_instance_password_conf(False)
+
+ def test_evacuate_enable_password_return(self):
+ self._test_evacuate_enable_instance_password_conf(True)
+
+ def _test_evacuate_enable_instance_password_conf(self, enable_pass):
+ self.flags(enable_instance_password=enable_pass)
+ self.stubs.Set(compute_api.API, 'update', self._fake_update)
+
+ res = self._gen_resource_with_app({'host': 'my_host',
+ 'onSharedStorage': 'False'})
+ self.assertEqual(res.status_int, 200)
+ resp_json = jsonutils.loads(res.body)
+ if enable_pass:
+ self.assertIn('adminPass', resp_json)
+ else:
+ self.assertIsNone(resp_json.get('adminPass'))
+
+
+class EvacuateTestV2(EvacuateTestV21):
+
+ def setUp(self):
+ super(EvacuateTestV2, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Evacuate'])
+
+ def _fake_wsgi_app(self, ctxt):
+ return fakes.wsgi_app(fake_auth_context=ctxt)
+
+ def test_evacuate_instance_with_no_target(self):
+ res = self._gen_resource_with_app({'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'})
+ self.assertEqual(400, res.status_int)
+
+ def test_evacuate_instance_with_too_long_host(self):
+ pass
+
+ def test_evacuate_instance_with_invalid_characters_host(self):
+ pass
+
+ def test_evacuate_instance_with_invalid_on_shared_storage(self):
+ pass
+
+ def test_evacuate_disable_password_return(self):
+ pass
+
+ def test_evacuate_enable_password_return(self):
+ pass
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_availability_zone.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_availability_zone.py
new file mode 100644
index 0000000000..a3e6dd4a78
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_availability_zone.py
@@ -0,0 +1,184 @@
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import extended_availability_zone
+from nova import availability_zones
+from nova import compute
+from nova.compute import vm_states
+from nova import db
+from nova import exception
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+UUID3 = '00000000-0000-0000-0000-000000000003'
+
+
+def fake_compute_get_az(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3, host="get-host",
+ vm_state=vm_states.ACTIVE,
+ availability_zone='fakeaz')
+ return fake_instance.fake_instance_obj(args[1], **inst)
+
+
+def fake_compute_get_empty(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3, host="",
+ vm_state=vm_states.ACTIVE,
+ availability_zone='fakeaz')
+ return fake_instance.fake_instance_obj(args[1], **inst)
+
+
+def fake_compute_get(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3, host="get-host",
+ vm_state=vm_states.ACTIVE)
+ return fake_instance.fake_instance_obj(args[1], **inst)
+
+
+def fake_compute_get_all(*args, **kwargs):
+ inst1 = fakes.stub_instance(1, uuid=UUID1, host="all-host",
+ vm_state=vm_states.ACTIVE)
+ inst2 = fakes.stub_instance(2, uuid=UUID2, host="all-host",
+ vm_state=vm_states.ACTIVE)
+ db_list = [inst1, inst2]
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return instance_obj._make_instance_list(args[1],
+ objects.InstanceList(),
+ db_list, fields)
+
+
+def fake_get_host_availability_zone(context, host):
+ return host
+
+
+def fake_get_no_host_availability_zone(context, host):
+ return None
+
+
+class ExtendedAvailabilityZoneTestV21(test.TestCase):
+ content_type = 'application/json'
+ prefix = 'OS-EXT-AZ:'
+ base_url = '/v2/fake/servers/'
+
+ def setUp(self):
+ super(ExtendedAvailabilityZoneTestV21, self).setUp()
+ availability_zones.reset_cache()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
+ self.stubs.Set(availability_zones, 'get_host_availability_zone',
+ fake_get_host_availability_zone)
+ return_server = fakes.fake_instance_get()
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app_v21(init_only=None))
+ return res
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def assertAvailabilityZone(self, server, az):
+ self.assertEqual(server.get('%savailability_zone' % self.prefix),
+ az)
+
+ def test_show_no_host_az(self):
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get_az)
+ self.stubs.Set(availability_zones, 'get_host_availability_zone',
+ fake_get_no_host_availability_zone)
+
+ url = self.base_url + UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertAvailabilityZone(self._get_server(res.body), 'fakeaz')
+
+ def test_show_empty_host_az(self):
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get_empty)
+ self.stubs.Set(availability_zones, 'get_host_availability_zone',
+ fake_get_no_host_availability_zone)
+
+ url = self.base_url + UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertAvailabilityZone(self._get_server(res.body), 'fakeaz')
+
+ def test_show(self):
+ url = self.base_url + UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertAvailabilityZone(self._get_server(res.body), 'get-host')
+
+ def test_detail(self):
+ url = self.base_url + 'detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ for i, server in enumerate(self._get_servers(res.body)):
+ self.assertAvailabilityZone(server, 'all-host')
+
+ def test_no_instance_passthrough_404(self):
+
+ def fake_compute_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ url = self.base_url + '70f6db34-de8d-4fbd-aafb-4065bdfa6115'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 404)
+
+
+class ExtendedAvailabilityZoneTestV2(ExtendedAvailabilityZoneTestV21):
+
+ def setUp(self):
+ super(ExtendedAvailabilityZoneTestV2, self).setUp()
+
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Extended_availability_zone'])
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app(init_only=('servers',)))
+ return res
+
+
+class ExtendedAvailabilityZoneXmlTestV2(ExtendedAvailabilityZoneTestV2):
+ content_type = 'application/xml'
+ prefix = '{%s}' % extended_availability_zone.\
+ Extended_availability_zone.namespace
+
+ def _get_server(self, body):
+ return etree.XML(body)
+
+ def _get_servers(self, body):
+ return etree.XML(body).getchildren()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_evacuate_find_host.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_evacuate_find_host.py
new file mode 100644
index 0000000000..1aaee6837a
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_evacuate_find_host.py
@@ -0,0 +1,114 @@
+# Copyright 2013 OpenStack Foundation
+#
+# 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 uuid
+
+import mock
+from oslo.serialization import jsonutils
+import webob
+
+from nova.compute import vm_states
+from nova import context
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+
+class ExtendedEvacuateFindHostTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(ExtendedEvacuateFindHostTest, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Extended_evacuate_find_host',
+ 'Evacuate'])
+ self.UUID = uuid.uuid4()
+
+ def _get_admin_context(self, user_id='fake', project_id='fake'):
+ ctxt = context.get_admin_context()
+ ctxt.user_id = user_id
+ ctxt.project_id = project_id
+ return ctxt
+
+ def _fake_compute_api(*args, **kwargs):
+ return True
+
+ def _fake_compute_api_get(self, context, instance_id, **kwargs):
+ instance = fake_instance.fake_db_instance(id=1, uuid=uuid,
+ task_state=None,
+ host='host1',
+ vm_state=vm_states.ACTIVE)
+ instance = instance_obj.Instance._from_db_object(context,
+ instance_obj.Instance(),
+ instance)
+ return instance
+
+ def _fake_service_get_by_compute_host(self, context, host):
+ return {'host_name': host,
+ 'service': 'compute',
+ 'zone': 'nova'
+ }
+
+ @mock.patch('nova.compute.api.HostAPI.service_get_by_compute_host')
+ @mock.patch('nova.compute.api.API.get')
+ @mock.patch('nova.compute.api.API.evacuate')
+ def test_evacuate_instance_with_no_target(self, evacuate_mock,
+ api_get_mock,
+ service_get_mock):
+ service_get_mock.side_effects = self._fake_service_get_by_compute_host
+ api_get_mock.side_effects = self._fake_compute_api_get
+ evacuate_mock.side_effects = self._fake_compute_api
+
+ ctxt = self._get_admin_context()
+ app = fakes.wsgi_app(fake_auth_context=ctxt)
+ req = webob.Request.blank('/v2/fake/servers/%s/action' % self.UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps({
+ 'evacuate': {
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'
+ }
+ })
+ req.content_type = 'application/json'
+ res = req.get_response(app)
+ self.assertEqual(200, res.status_int)
+ evacuate_mock.assert_called_once_with(mock.ANY, mock.ANY, None,
+ mock.ANY, mock.ANY)
+
+ @mock.patch('nova.compute.api.HostAPI.service_get_by_compute_host')
+ @mock.patch('nova.compute.api.API.get')
+ def test_no_target_fails_if_extension_not_loaded(self, api_get_mock,
+ service_get_mock):
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Evacuate'])
+ service_get_mock.side_effects = self._fake_service_get_by_compute_host
+ api_get_mock.side_effects = self._fake_compute_api_get
+
+ ctxt = self._get_admin_context()
+ app = fakes.wsgi_app(fake_auth_context=ctxt)
+ req = webob.Request.blank('/v2/fake/servers/%s/action' % self.UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps({
+ 'evacuate': {
+ 'onSharedStorage': 'False',
+ 'adminPass': 'MyNewPass'
+ }
+ })
+ req.content_type = 'application/json'
+ res = req.get_response(app)
+ self.assertEqual(400, res.status_int)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_hypervisors.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_hypervisors.py
new file mode 100644
index 0000000000..df5e0d787a
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_hypervisors.py
@@ -0,0 +1,101 @@
+# Copyright 2014 IBM Corp.
+#
+# 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 copy
+
+import mock
+
+from nova.api.openstack.compute.contrib import hypervisors as hypervisors_v2
+from nova.api.openstack.compute.plugins.v3 import hypervisors \
+ as hypervisors_v21
+from nova.api.openstack import extensions
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack.compute.contrib import test_hypervisors
+from nova.tests.unit.api.openstack import fakes
+
+
+def fake_compute_node_get(context, compute_id):
+ for hyper in test_hypervisors.TEST_HYPERS:
+ if hyper['id'] == compute_id:
+ return hyper
+ raise exception.ComputeHostNotFound(host=compute_id)
+
+
+def fake_compute_node_get_all(context):
+ return test_hypervisors.TEST_HYPERS
+
+
+class ExtendedHypervisorsTestV21(test.NoDBTestCase):
+ DETAIL_HYPERS_DICTS = copy.deepcopy(test_hypervisors.TEST_HYPERS)
+ del DETAIL_HYPERS_DICTS[0]['service_id']
+ del DETAIL_HYPERS_DICTS[1]['service_id']
+ DETAIL_HYPERS_DICTS[0].update({'state': 'up',
+ 'status': 'enabled',
+ 'service': dict(id=1, host='compute1',
+ disabled_reason=None)})
+ DETAIL_HYPERS_DICTS[1].update({'state': 'up',
+ 'status': 'enabled',
+ 'service': dict(id=2, host='compute2',
+ disabled_reason=None)})
+
+ def _set_up_controller(self):
+ self.controller = hypervisors_v21.HypervisorsController()
+ self.controller.servicegroup_api.service_is_up = mock.MagicMock(
+ return_value=True)
+
+ def _get_request(self):
+ return fakes.HTTPRequest.blank('/v2/fake/os-hypervisors/detail',
+ use_admin_context=True)
+
+ def setUp(self):
+ super(ExtendedHypervisorsTestV21, self).setUp()
+ self._set_up_controller()
+
+ self.stubs.Set(db, 'compute_node_get_all', fake_compute_node_get_all)
+ self.stubs.Set(db, 'compute_node_get',
+ fake_compute_node_get)
+
+ def test_view_hypervisor_detail_noservers(self):
+ result = self.controller._view_hypervisor(
+ test_hypervisors.TEST_HYPERS[0], True)
+
+ self.assertEqual(result, self.DETAIL_HYPERS_DICTS[0])
+
+ def test_detail(self):
+ req = self._get_request()
+ result = self.controller.detail(req)
+
+ self.assertEqual(result, dict(hypervisors=self.DETAIL_HYPERS_DICTS))
+
+ def test_show_withid(self):
+ req = self._get_request()
+ result = self.controller.show(req, '1')
+
+ self.assertEqual(result, dict(hypervisor=self.DETAIL_HYPERS_DICTS[0]))
+
+
+class ExtendedHypervisorsTestV2(ExtendedHypervisorsTestV21):
+ DETAIL_HYPERS_DICTS = copy.deepcopy(test_hypervisors.TEST_HYPERS)
+ del DETAIL_HYPERS_DICTS[0]['service_id']
+ del DETAIL_HYPERS_DICTS[1]['service_id']
+ DETAIL_HYPERS_DICTS[0].update({'service': dict(id=1, host='compute1')})
+ DETAIL_HYPERS_DICTS[1].update({'service': dict(id=2, host='compute2')})
+
+ def _set_up_controller(self):
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.ext_mgr.extensions['os-extended-hypervisors'] = True
+ self.controller = hypervisors_v2.HypervisorsController(self.ext_mgr)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips.py
new file mode 100644
index 0000000000..770814116c
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips.py
@@ -0,0 +1,189 @@
+# Copyright 2013 Nebula, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import extended_ips
+from nova.api.openstack import xmlutil
+from nova import compute
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+UUID3 = '00000000-0000-0000-0000-000000000003'
+NW_CACHE = [
+ {
+ 'address': 'aa:aa:aa:aa:aa:aa',
+ 'id': 1,
+ 'network': {
+ 'bridge': 'br0',
+ 'id': 1,
+ 'label': 'private',
+ 'subnets': [
+ {
+ 'cidr': '192.168.1.0/24',
+ 'ips': [
+ {
+ 'address': '192.168.1.100',
+ 'type': 'fixed',
+ 'floating_ips': [
+ {'address': '5.0.0.1', 'type': 'floating'},
+ ],
+ },
+ ],
+ },
+ ]
+ }
+ },
+ {
+ 'address': 'bb:bb:bb:bb:bb:bb',
+ 'id': 2,
+ 'network': {
+ 'bridge': 'br1',
+ 'id': 2,
+ 'label': 'public',
+ 'subnets': [
+ {
+ 'cidr': '10.0.0.0/24',
+ 'ips': [
+ {
+ 'address': '10.0.0.100',
+ 'type': 'fixed',
+ 'floating_ips': [
+ {'address': '5.0.0.2', 'type': 'floating'},
+ ],
+ }
+ ],
+ },
+ ]
+ }
+ }
+]
+ALL_IPS = []
+for cache in NW_CACHE:
+ for subnet in cache['network']['subnets']:
+ for fixed in subnet['ips']:
+ sanitized = dict(fixed)
+ sanitized.pop('floating_ips')
+ ALL_IPS.append(sanitized)
+ for floating in fixed['floating_ips']:
+ ALL_IPS.append(floating)
+ALL_IPS.sort()
+
+
+def fake_compute_get(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3, nw_cache=NW_CACHE)
+ return fake_instance.fake_instance_obj(args[1],
+ expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS, **inst)
+
+
+def fake_compute_get_all(*args, **kwargs):
+ db_list = [
+ fakes.stub_instance(1, uuid=UUID1, nw_cache=NW_CACHE),
+ fakes.stub_instance(2, uuid=UUID2, nw_cache=NW_CACHE),
+ ]
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return instance_obj._make_instance_list(args[1],
+ objects.InstanceList(),
+ db_list, fields)
+
+
+class ExtendedIpsTestV21(test.TestCase):
+ content_type = 'application/json'
+ prefix = 'OS-EXT-IPS:'
+
+ def setUp(self):
+ super(ExtendedIpsTestV21, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app_v21(init_only=('servers',)))
+ return res
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def _get_ips(self, server):
+ for network in server['addresses'].itervalues():
+ for ip in network:
+ yield ip
+
+ def assertServerStates(self, server):
+ results = []
+ for ip in self._get_ips(server):
+ results.append({'address': ip.get('addr'),
+ 'type': ip.get('%stype' % self.prefix)})
+
+ self.assertEqual(ALL_IPS, sorted(results))
+
+ def test_show(self):
+ url = '/v2/fake/servers/%s' % UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertServerStates(self._get_server(res.body))
+
+ def test_detail(self):
+ url = '/v2/fake/servers/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ for i, server in enumerate(self._get_servers(res.body)):
+ self.assertServerStates(server)
+
+
+class ExtendedIpsTestV2(ExtendedIpsTestV21):
+
+ def setUp(self):
+ super(ExtendedIpsTestV2, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Extended_ips'])
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app(init_only=('servers',)))
+ return res
+
+
+class ExtendedIpsXmlTest(ExtendedIpsTestV2):
+ content_type = 'application/xml'
+ prefix = '{%s}' % extended_ips.Extended_ips.namespace
+
+ def _get_server(self, body):
+ return etree.XML(body)
+
+ def _get_servers(self, body):
+ return etree.XML(body).getchildren()
+
+ def _get_ips(self, server):
+ for network in server.find('{%s}addresses' % xmlutil.XMLNS_V11):
+ for ip in network:
+ yield ip
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips_mac.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips_mac.py
new file mode 100644
index 0000000000..c3e94600aa
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips_mac.py
@@ -0,0 +1,196 @@
+# Copyright 2013 IBM Corp.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import extended_ips_mac
+from nova.api.openstack import xmlutil
+from nova import compute
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+UUID3 = '00000000-0000-0000-0000-000000000003'
+NW_CACHE = [
+ {
+ 'address': 'aa:aa:aa:aa:aa:aa',
+ 'id': 1,
+ 'network': {
+ 'bridge': 'br0',
+ 'id': 1,
+ 'label': 'private',
+ 'subnets': [
+ {
+ 'cidr': '192.168.1.0/24',
+ 'ips': [
+ {
+ 'address': '192.168.1.100',
+ 'type': 'fixed',
+ 'floating_ips': [
+ {'address': '5.0.0.1', 'type': 'floating'},
+ ],
+ },
+ ],
+ },
+ ]
+ }
+ },
+ {
+ 'address': 'bb:bb:bb:bb:bb:bb',
+ 'id': 2,
+ 'network': {
+ 'bridge': 'br1',
+ 'id': 2,
+ 'label': 'public',
+ 'subnets': [
+ {
+ 'cidr': '10.0.0.0/24',
+ 'ips': [
+ {
+ 'address': '10.0.0.100',
+ 'type': 'fixed',
+ 'floating_ips': [
+ {'address': '5.0.0.2', 'type': 'floating'},
+ ],
+ }
+ ],
+ },
+ ]
+ }
+ }
+]
+ALL_IPS = []
+for cache in NW_CACHE:
+ for subnet in cache['network']['subnets']:
+ for fixed in subnet['ips']:
+ sanitized = dict(fixed)
+ sanitized['mac_address'] = cache['address']
+ sanitized.pop('floating_ips')
+ sanitized.pop('type')
+ ALL_IPS.append(sanitized)
+ for floating in fixed['floating_ips']:
+ sanitized = dict(floating)
+ sanitized['mac_address'] = cache['address']
+ sanitized.pop('type')
+ ALL_IPS.append(sanitized)
+ALL_IPS.sort()
+
+
+def fake_compute_get(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3, nw_cache=NW_CACHE)
+ return fake_instance.fake_instance_obj(args[1],
+ expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS, **inst)
+
+
+def fake_compute_get_all(*args, **kwargs):
+ db_list = [
+ fakes.stub_instance(1, uuid=UUID1, nw_cache=NW_CACHE),
+ fakes.stub_instance(2, uuid=UUID2, nw_cache=NW_CACHE),
+ ]
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return instance_obj._make_instance_list(args[1],
+ objects.InstanceList(),
+ db_list, fields)
+
+
+class ExtendedIpsMacTestV21(test.TestCase):
+ content_type = 'application/json'
+ prefix = '%s:' % extended_ips_mac.Extended_ips_mac.alias
+
+ def setUp(self):
+ super(ExtendedIpsMacTestV21, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app_v21(init_only=('servers',)))
+ return res
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def _get_ips(self, server):
+ for network in server['addresses'].itervalues():
+ for ip in network:
+ yield ip
+
+ def assertServerStates(self, server):
+ results = []
+ for ip in self._get_ips(server):
+ results.append({'address': ip.get('addr'),
+ 'mac_address': ip.get('%smac_addr' % self.prefix)})
+
+ self.assertEqual(ALL_IPS, sorted(results))
+
+ def test_show(self):
+ url = '/v2/fake/servers/%s' % UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertServerStates(self._get_server(res.body))
+
+ def test_detail(self):
+ url = '/v2/fake/servers/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ for _i, server in enumerate(self._get_servers(res.body)):
+ self.assertServerStates(server)
+
+
+class ExtendedIpsMacTestV2(ExtendedIpsMacTestV21):
+ content_type = 'application/json'
+ prefix = '%s:' % extended_ips_mac.Extended_ips_mac.alias
+
+ def setUp(self):
+ super(ExtendedIpsMacTestV2, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Extended_ips_mac'])
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app(init_only=('servers',)))
+ return res
+
+
+class ExtendedIpsMacXmlTest(ExtendedIpsMacTestV2):
+ content_type = 'application/xml'
+ prefix = '{%s}' % extended_ips_mac.Extended_ips_mac.namespace
+
+ def _get_server(self, body):
+ return etree.XML(body)
+
+ def _get_servers(self, body):
+ return etree.XML(body).getchildren()
+
+ def _get_ips(self, server):
+ for network in server.find('{%s}addresses' % xmlutil.XMLNS_V11):
+ for ip in network:
+ yield ip
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_rescue_with_image.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_rescue_with_image.py
new file mode 100644
index 0000000000..42a8382595
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_rescue_with_image.py
@@ -0,0 +1,62 @@
+# Copyright 2014 OpenStack Foundation
+#
+# 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 mock
+from oslo.config import cfg
+
+from nova.api.openstack import common
+from nova.api.openstack.compute.contrib import rescue
+from nova.api.openstack import extensions
+from nova import compute
+import nova.context as context
+from nova import test
+
+CONF = cfg.CONF
+CONF.import_opt('password_length', 'nova.utils')
+
+
+class FakeRequest(object):
+ def __init__(self, context):
+ self.environ = {"nova.context": context}
+
+
+class ExtendedRescueWithImageTest(test.NoDBTestCase):
+ def setUp(self):
+ super(ExtendedRescueWithImageTest, self).setUp()
+ ext_mgr = extensions.ExtensionManager()
+ ext_mgr.extensions = {'os-extended-rescue-with-image': 'fake'}
+ self.controller = rescue.RescueController(ext_mgr)
+
+ @mock.patch.object(common, 'get_instance',
+ return_value="instance")
+ @mock.patch.object(compute.api.API, "rescue")
+ def _make_rescue_request_with_image_ref(self, body, mock_rescue,
+ mock_get_instance):
+ instance = "instance"
+ self.controller._get_instance = mock.Mock(return_value=instance)
+ fake_context = context.RequestContext('fake', 'fake')
+ req = FakeRequest(fake_context)
+
+ self.controller._rescue(req, "id", body)
+ rescue_image_ref = body["rescue"].get("rescue_image_ref")
+ mock_rescue.assert_called_with(mock.ANY, mock.ANY,
+ rescue_password=mock.ANY, rescue_image_ref=rescue_image_ref)
+
+ def test_rescue_with_image_specified(self):
+ body = dict(rescue={"rescue_image_ref": "image-ref"})
+ self._make_rescue_request_with_image_ref(body)
+
+ def test_rescue_without_image_specified(self):
+ body = dict(rescue={})
+ self._make_rescue_request_with_image_ref(body)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_server_attributes.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_server_attributes.py
new file mode 100644
index 0000000000..f944289efe
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_server_attributes.py
@@ -0,0 +1,148 @@
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import extended_server_attributes
+from nova import compute
+from nova import db
+from nova import exception
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+from oslo.config import cfg
+
+
+NAME_FMT = cfg.CONF.instance_name_template
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+UUID3 = '00000000-0000-0000-0000-000000000003'
+
+
+def fake_compute_get(*args, **kwargs):
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return objects.Instance._from_db_object(
+ args[1], objects.Instance(),
+ fakes.stub_instance(1, uuid=UUID3, host="host-fake",
+ node="node-fake"), fields)
+
+
+def fake_compute_get_all(*args, **kwargs):
+ db_list = [
+ fakes.stub_instance(1, uuid=UUID1, host="host-1", node="node-1"),
+ fakes.stub_instance(2, uuid=UUID2, host="host-2", node="node-2")
+ ]
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return instance_obj._make_instance_list(args[1],
+ objects.InstanceList(),
+ db_list, fields)
+
+
+class ExtendedServerAttributesTestV21(test.TestCase):
+ content_type = 'application/json'
+ prefix = 'OS-EXT-SRV-ATTR:'
+ fake_url = '/v2/fake'
+
+ def setUp(self):
+ super(ExtendedServerAttributesTestV21, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_compute_get)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(
+ fakes.wsgi_app_v21(init_only=('servers',
+ 'os-extended-server-attributes')))
+ return res
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def assertServerAttributes(self, server, host, node, instance_name):
+ self.assertEqual(server.get('%shost' % self.prefix), host)
+ self.assertEqual(server.get('%sinstance_name' % self.prefix),
+ instance_name)
+ self.assertEqual(server.get('%shypervisor_hostname' % self.prefix),
+ node)
+
+ def test_show(self):
+ url = self.fake_url + '/servers/%s' % UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertServerAttributes(self._get_server(res.body),
+ host='host-fake',
+ node='node-fake',
+ instance_name=NAME_FMT % 1)
+
+ def test_detail(self):
+ url = self.fake_url + '/servers/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ for i, server in enumerate(self._get_servers(res.body)):
+ self.assertServerAttributes(server,
+ host='host-%s' % (i + 1),
+ node='node-%s' % (i + 1),
+ instance_name=NAME_FMT % (i + 1))
+
+ def test_no_instance_passthrough_404(self):
+
+ def fake_compute_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ url = self.fake_url + '/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 404)
+
+
+class ExtendedServerAttributesTestV2(ExtendedServerAttributesTestV21):
+
+ def setUp(self):
+ super(ExtendedServerAttributesTestV2, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Extended_server_attributes'])
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app(init_only=('servers',)))
+ return res
+
+
+class ExtendedServerAttributesXmlTest(ExtendedServerAttributesTestV2):
+ content_type = 'application/xml'
+ ext = extended_server_attributes
+ prefix = '{%s}' % ext.Extended_server_attributes.namespace
+
+ def _get_server(self, body):
+ return etree.XML(body)
+
+ def _get_servers(self, body):
+ return etree.XML(body).getchildren()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_status.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_status.py
new file mode 100644
index 0000000000..b47562f7a7
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_status.py
@@ -0,0 +1,148 @@
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import extended_status
+from nova import compute
+from nova import db
+from nova import exception
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+UUID3 = '00000000-0000-0000-0000-000000000003'
+
+
+def fake_compute_get(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3, task_state="kayaking",
+ vm_state="slightly crunchy", power_state=1)
+ return fake_instance.fake_instance_obj(args[1], **inst)
+
+
+def fake_compute_get_all(*args, **kwargs):
+ db_list = [
+ fakes.stub_instance(1, uuid=UUID1, task_state="task-1",
+ vm_state="vm-1", power_state=1),
+ fakes.stub_instance(2, uuid=UUID2, task_state="task-2",
+ vm_state="vm-2", power_state=2),
+ ]
+
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return instance_obj._make_instance_list(args[1],
+ objects.InstanceList(),
+ db_list, fields)
+
+
+class ExtendedStatusTestV21(test.TestCase):
+ content_type = 'application/json'
+ prefix = 'OS-EXT-STS:'
+ fake_url = '/v2/fake'
+
+ def _set_flags(self):
+ pass
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app_v21(
+ init_only=('servers',
+ 'os-extended-status')))
+ return res
+
+ def setUp(self):
+ super(ExtendedStatusTestV21, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
+ self._set_flags()
+ return_server = fakes.fake_instance_get()
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def assertServerStates(self, server, vm_state, power_state, task_state):
+ self.assertEqual(server.get('%svm_state' % self.prefix), vm_state)
+ self.assertEqual(int(server.get('%spower_state' % self.prefix)),
+ power_state)
+ self.assertEqual(server.get('%stask_state' % self.prefix), task_state)
+
+ def test_show(self):
+ url = self.fake_url + '/servers/%s' % UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertServerStates(self._get_server(res.body),
+ vm_state='slightly crunchy',
+ power_state=1,
+ task_state='kayaking')
+
+ def test_detail(self):
+ url = self.fake_url + '/servers/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ for i, server in enumerate(self._get_servers(res.body)):
+ self.assertServerStates(server,
+ vm_state='vm-%s' % (i + 1),
+ power_state=(i + 1),
+ task_state='task-%s' % (i + 1))
+
+ def test_no_instance_passthrough_404(self):
+
+ def fake_compute_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ url = self.fake_url + '/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 404)
+
+
+class ExtendedStatusTestV2(ExtendedStatusTestV21):
+
+ def _set_flags(self):
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Extended_status'])
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app(init_only=('servers',)))
+ return res
+
+
+class ExtendedStatusXmlTest(ExtendedStatusTestV2):
+ content_type = 'application/xml'
+ prefix = '{%s}' % extended_status.Extended_status.namespace
+
+ def _get_server(self, body):
+ return etree.XML(body)
+
+ def _get_servers(self, body):
+ return etree.XML(body).getchildren()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_virtual_interfaces_net.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_virtual_interfaces_net.py
new file mode 100644
index 0000000000..851848d7a5
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_virtual_interfaces_net.py
@@ -0,0 +1,123 @@
+# Copyright 2013 IBM Corp.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import extended_virtual_interfaces_net
+from nova.api.openstack import wsgi
+from nova import compute
+from nova import network
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+
+
+FAKE_VIFS = [{'uuid': '00000000-0000-0000-0000-00000000000000000',
+ 'address': '00-00-00-00-00-00',
+ 'net_uuid': '00000000-0000-0000-0000-00000000000000001'},
+ {'uuid': '11111111-1111-1111-1111-11111111111111111',
+ 'address': '11-11-11-11-11-11',
+ 'net_uuid': '11111111-1111-1111-1111-11111111111111112'}]
+
+EXPECTED_NET_UUIDS = ['00000000-0000-0000-0000-00000000000000001',
+ '11111111-1111-1111-1111-11111111111111112']
+
+
+def compute_api_get(self, context, instance_id, expected_attrs=None,
+ want_objects=False):
+ return dict(uuid=FAKE_UUID, id=instance_id, instance_type_id=1, host='bob')
+
+
+def get_vifs_by_instance(self, context, instance_id):
+ return FAKE_VIFS
+
+
+def get_vif_by_mac_address(self, context, mac_address):
+ if mac_address == "00-00-00-00-00-00":
+ return {'net_uuid': '00000000-0000-0000-0000-00000000000000001'}
+ else:
+ return {'net_uuid': '11111111-1111-1111-1111-11111111111111112'}
+
+
+class ExtendedServerVIFNetTest(test.NoDBTestCase):
+ content_type = 'application/json'
+ prefix = "%s:" % extended_virtual_interfaces_net. \
+ Extended_virtual_interfaces_net.alias
+
+ def setUp(self):
+ super(ExtendedServerVIFNetTest, self).setUp()
+ self.stubs.Set(compute.api.API, "get",
+ compute_api_get)
+ self.stubs.Set(network.api.API, "get_vifs_by_instance",
+ get_vifs_by_instance)
+ self.stubs.Set(network.api.API, "get_vif_by_mac_address",
+ get_vif_by_mac_address)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Virtual_interfaces',
+ 'Extended_virtual_interfaces_net'])
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app(init_only=(
+ 'os-virtual-interfaces', 'OS-EXT-VIF-NET')))
+ return res
+
+ def _get_vifs(self, body):
+ return jsonutils.loads(body).get('virtual_interfaces')
+
+ def _get_net_id(self, vifs):
+ for vif in vifs:
+ yield vif['%snet_id' % self.prefix]
+
+ def assertVIFs(self, vifs):
+ result = []
+ for net_id in self._get_net_id(vifs):
+ result.append(net_id)
+ sorted(result)
+
+ for i, net_uuid in enumerate(result):
+ self.assertEqual(net_uuid, EXPECTED_NET_UUIDS[i])
+
+ def test_get_extend_virtual_interfaces_list(self):
+ res = self._make_request('/v2/fake/servers/abcd/os-virtual-interfaces')
+
+ self.assertEqual(res.status_int, 200)
+ self.assertVIFs(self._get_vifs(res.body))
+
+
+class ExtendedServerVIFNetSerializerTest(ExtendedServerVIFNetTest):
+ content_type = 'application/xml'
+ prefix = "{%s}" % extended_virtual_interfaces_net. \
+ Extended_virtual_interfaces_net.namespace
+
+ def setUp(self):
+ super(ExtendedServerVIFNetSerializerTest, self).setUp()
+ self.namespace = wsgi.XMLNS_V11
+ self.serializer = extended_virtual_interfaces_net. \
+ ExtendedVirtualInterfaceNetTemplate()
+
+ def _get_vifs(self, body):
+ return etree.XML(body).getchildren()
+
+ def _get_net_id(self, vifs):
+ for vif in vifs:
+ yield vif.attrib['%snet_id' % self.prefix]
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_volumes.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_volumes.py
new file mode 100644
index 0000000000..d441013e8d
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_volumes.py
@@ -0,0 +1,124 @@
+# Copyright 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import extended_volumes
+from nova import compute
+from nova import db
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_block_device
+from nova.tests.unit import fake_instance
+
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+UUID3 = '00000000-0000-0000-0000-000000000003'
+
+
+def fake_compute_get(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID1)
+ return fake_instance.fake_instance_obj(args[1], **inst)
+
+
+def fake_compute_get_all(*args, **kwargs):
+ db_list = [fakes.stub_instance(1), fakes.stub_instance(2)]
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return instance_obj._make_instance_list(args[1],
+ objects.InstanceList(),
+ db_list, fields)
+
+
+def fake_bdms_get_all_by_instance(*args, **kwargs):
+ return [fake_block_device.FakeDbBlockDeviceDict(
+ {'volume_id': UUID1, 'source_type': 'volume',
+ 'destination_type': 'volume', 'id': 1}),
+ fake_block_device.FakeDbBlockDeviceDict(
+ {'volume_id': UUID2, 'source_type': 'volume',
+ 'destination_type': 'volume', 'id': 2})]
+
+
+class ExtendedVolumesTest(test.TestCase):
+ content_type = 'application/json'
+ prefix = 'os-extended-volumes:'
+
+ def setUp(self):
+ super(ExtendedVolumesTest, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ fake_bdms_get_all_by_instance)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Extended_volumes'])
+ return_server = fakes.fake_instance_get()
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app(init_only=('servers',)))
+ return res
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def test_show(self):
+ url = '/v2/fake/servers/%s' % UUID1
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ server = self._get_server(res.body)
+ exp_volumes = [{'id': UUID1}, {'id': UUID2}]
+ if self.content_type == 'application/json':
+ actual = server.get('%svolumes_attached' % self.prefix)
+ elif self.content_type == 'application/xml':
+ actual = [dict(elem.items()) for elem in
+ server.findall('%svolume_attached' % self.prefix)]
+ self.assertEqual(exp_volumes, actual)
+
+ def test_detail(self):
+ url = '/v2/fake/servers/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ exp_volumes = [{'id': UUID1}, {'id': UUID2}]
+ for i, server in enumerate(self._get_servers(res.body)):
+ if self.content_type == 'application/json':
+ actual = server.get('%svolumes_attached' % self.prefix)
+ elif self.content_type == 'application/xml':
+ actual = [dict(elem.items()) for elem in
+ server.findall('%svolume_attached' % self.prefix)]
+ self.assertEqual(exp_volumes, actual)
+
+
+class ExtendedVolumesXmlTest(ExtendedVolumesTest):
+ content_type = 'application/xml'
+ prefix = '{%s}' % extended_volumes.Extended_volumes.namespace
+
+ def _get_server(self, body):
+ return etree.XML(body)
+
+ def _get_servers(self, body):
+ return etree.XML(body).getchildren()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_fixed_ips.py b/nova/tests/unit/api/openstack/compute/contrib/test_fixed_ips.py
new file mode 100644
index 0000000000..f331da80fe
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_fixed_ips.py
@@ -0,0 +1,256 @@
+# Copyright 2012 IBM Corp.
+#
+# 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
+
+from nova.api.openstack.compute.contrib import fixed_ips as fixed_ips_v2
+from nova.api.openstack.compute.plugins.v3 import fixed_ips as fixed_ips_v21
+from nova import context
+from nova import db
+from nova import exception
+from nova.i18n import _
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.objects import test_network
+
+
+fake_fixed_ips = [{'id': 1,
+ 'address': '192.168.1.1',
+ 'network_id': 1,
+ 'virtual_interface_id': 1,
+ 'instance_uuid': '1',
+ 'allocated': False,
+ 'leased': False,
+ 'reserved': False,
+ 'host': None,
+ 'instance': None,
+ 'network': test_network.fake_network,
+ 'created_at': None,
+ 'updated_at': None,
+ 'deleted_at': None,
+ 'deleted': False},
+ {'id': 2,
+ 'address': '192.168.1.2',
+ 'network_id': 1,
+ 'virtual_interface_id': 2,
+ 'instance_uuid': '2',
+ 'allocated': False,
+ 'leased': False,
+ 'reserved': False,
+ 'host': None,
+ 'instance': None,
+ 'network': test_network.fake_network,
+ 'created_at': None,
+ 'updated_at': None,
+ 'deleted_at': None,
+ 'deleted': False},
+ {'id': 3,
+ 'address': '10.0.0.2',
+ 'network_id': 1,
+ 'virtual_interface_id': 3,
+ 'instance_uuid': '3',
+ 'allocated': False,
+ 'leased': False,
+ 'reserved': False,
+ 'host': None,
+ 'instance': None,
+ 'network': test_network.fake_network,
+ 'created_at': None,
+ 'updated_at': None,
+ 'deleted_at': None,
+ 'deleted': True},
+ ]
+
+
+def fake_fixed_ip_get_by_address(context, address, columns_to_join=None):
+ if address == 'inv.ali.d.ip':
+ msg = _("Invalid fixed IP Address %s in request") % address
+ raise exception.FixedIpInvalid(msg)
+ for fixed_ip in fake_fixed_ips:
+ if fixed_ip['address'] == address and not fixed_ip['deleted']:
+ return fixed_ip
+ raise exception.FixedIpNotFoundForAddress(address=address)
+
+
+def fake_fixed_ip_get_by_address_detailed(context, address):
+ network = {'id': 1,
+ 'cidr': "192.168.1.0/24"}
+ for fixed_ip in fake_fixed_ips:
+ if fixed_ip['address'] == address and not fixed_ip['deleted']:
+ return (fixed_ip, FakeModel(network), None)
+ raise exception.FixedIpNotFoundForAddress(address=address)
+
+
+def fake_fixed_ip_update(context, address, values):
+ fixed_ip = fake_fixed_ip_get_by_address(context, address)
+ if fixed_ip is None:
+ raise exception.FixedIpNotFoundForAddress(address=address)
+ else:
+ for key in values:
+ fixed_ip[key] = values[key]
+
+
+class FakeModel(object):
+ """Stubs out for model."""
+ def __init__(self, values):
+ self.values = values
+
+ def __getattr__(self, name):
+ return self.values[name]
+
+ def __getitem__(self, key):
+ if key in self.values:
+ return self.values[key]
+ else:
+ raise NotImplementedError()
+
+ def __repr__(self):
+ return '<FakeModel: %s>' % self.values
+
+
+def fake_network_get_all(context):
+ network = {'id': 1,
+ 'cidr': "192.168.1.0/24"}
+ return [FakeModel(network)]
+
+
+class FixedIpTestV21(test.NoDBTestCase):
+
+ fixed_ips = fixed_ips_v21
+ url = '/v2/fake/os-fixed-ips'
+
+ def setUp(self):
+ super(FixedIpTestV21, self).setUp()
+
+ self.stubs.Set(db, "fixed_ip_get_by_address",
+ fake_fixed_ip_get_by_address)
+ self.stubs.Set(db, "fixed_ip_get_by_address_detailed",
+ fake_fixed_ip_get_by_address_detailed)
+ self.stubs.Set(db, "fixed_ip_update", fake_fixed_ip_update)
+
+ self.context = context.get_admin_context()
+ self.controller = self.fixed_ips.FixedIPController()
+
+ def _assert_equal(self, ret, exp):
+ self.assertEqual(ret.wsgi_code, exp)
+
+ def _get_reserve_action(self):
+ return self.controller.reserve
+
+ def _get_unreserve_action(self):
+ return self.controller.unreserve
+
+ def test_fixed_ips_get(self):
+ req = fakes.HTTPRequest.blank('%s/192.168.1.1' % self.url)
+ res_dict = self.controller.show(req, '192.168.1.1')
+ response = {'fixed_ip': {'cidr': '192.168.1.0/24',
+ 'hostname': None,
+ 'host': None,
+ 'address': '192.168.1.1'}}
+ self.assertEqual(response, res_dict)
+
+ def test_fixed_ips_get_bad_ip_fail(self):
+ req = fakes.HTTPRequest.blank('%s/10.0.0.1' % self.url)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, req,
+ '10.0.0.1')
+
+ def test_fixed_ips_get_invalid_ip_address(self):
+ req = fakes.HTTPRequest.blank('%s/inv.ali.d.ip' % self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.show, req,
+ 'inv.ali.d.ip')
+
+ def test_fixed_ips_get_deleted_ip_fail(self):
+ req = fakes.HTTPRequest.blank('%s/10.0.0.2' % self.url)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, req,
+ '10.0.0.2')
+
+ def test_fixed_ip_reserve(self):
+ fake_fixed_ips[0]['reserved'] = False
+ body = {'reserve': None}
+ req = fakes.HTTPRequest.blank('%s/192.168.1.1/action' % self.url)
+ action = self._get_reserve_action()
+ result = action(req, "192.168.1.1", body)
+
+ self._assert_equal(result or action, 202)
+ self.assertEqual(fake_fixed_ips[0]['reserved'], True)
+
+ def test_fixed_ip_reserve_bad_ip(self):
+ body = {'reserve': None}
+ req = fakes.HTTPRequest.blank('%s/10.0.0.1/action' % self.url)
+ action = self._get_reserve_action()
+
+ self.assertRaises(webob.exc.HTTPNotFound, action, req,
+ '10.0.0.1', body)
+
+ def test_fixed_ip_reserve_invalid_ip_address(self):
+ body = {'reserve': None}
+ req = fakes.HTTPRequest.blank('%s/inv.ali.d.ip/action' % self.url)
+ action = self._get_reserve_action()
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ action, req, 'inv.ali.d.ip', body)
+
+ def test_fixed_ip_reserve_deleted_ip(self):
+ body = {'reserve': None}
+ action = self._get_reserve_action()
+
+ req = fakes.HTTPRequest.blank('%s/10.0.0.2/action' % self.url)
+ self.assertRaises(webob.exc.HTTPNotFound, action, req,
+ '10.0.0.2', body)
+
+ def test_fixed_ip_unreserve(self):
+ fake_fixed_ips[0]['reserved'] = True
+ body = {'unreserve': None}
+ req = fakes.HTTPRequest.blank('%s/192.168.1.1/action' % self.url)
+ action = self._get_unreserve_action()
+ result = action(req, "192.168.1.1", body)
+
+ self._assert_equal(result or action, 202)
+ self.assertEqual(fake_fixed_ips[0]['reserved'], False)
+
+ def test_fixed_ip_unreserve_bad_ip(self):
+ body = {'unreserve': None}
+ req = fakes.HTTPRequest.blank('%s/10.0.0.1/action' % self.url)
+ action = self._get_unreserve_action()
+
+ self.assertRaises(webob.exc.HTTPNotFound, action, req,
+ '10.0.0.1', body)
+
+ def test_fixed_ip_unreserve_invalid_ip_address(self):
+ body = {'unreserve': None}
+ req = fakes.HTTPRequest.blank('%s/inv.ali.d.ip/action' % self.url)
+ action = self._get_unreserve_action()
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ action, req, 'inv.ali.d.ip', body)
+
+ def test_fixed_ip_unreserve_deleted_ip(self):
+ body = {'unreserve': None}
+ req = fakes.HTTPRequest.blank('%s/10.0.0.2/action' % self.url)
+ action = self._get_unreserve_action()
+ self.assertRaises(webob.exc.HTTPNotFound, action, req,
+ '10.0.0.2', body)
+
+
+class FixedIpTestV2(FixedIpTestV21):
+
+ fixed_ips = fixed_ips_v2
+
+ def _assert_equal(self, ret, exp):
+ self.assertEqual(ret.status, '202 Accepted')
+
+ def _get_reserve_action(self):
+ return self.controller.action
+
+ def _get_unreserve_action(self):
+ return self.controller.action
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_access.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_access.py
new file mode 100644
index 0000000000..5718a826e4
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_access.py
@@ -0,0 +1,402 @@
+# Copyright 2012 OpenStack Foundation
+# 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 datetime
+
+from lxml import etree
+from webob import exc
+
+from nova.api.openstack.compute.contrib import flavor_access \
+ as flavor_access_v2
+from nova.api.openstack.compute import flavors as flavors_api
+from nova.api.openstack.compute.plugins.v3 import flavor_access \
+ as flavor_access_v3
+from nova import context
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+def generate_flavor(flavorid, ispublic):
+ return {
+ 'id': flavorid,
+ 'flavorid': str(flavorid),
+ 'root_gb': 1,
+ 'ephemeral_gb': 1,
+ 'name': u'test',
+ 'deleted': False,
+ 'created_at': datetime.datetime(2012, 1, 1, 1, 1, 1, 1),
+ 'updated_at': None,
+ 'memory_mb': 512,
+ 'vcpus': 1,
+ 'swap': 512,
+ 'rxtx_factor': 1.0,
+ 'disabled': False,
+ 'extra_specs': {},
+ 'deleted_at': None,
+ 'vcpu_weight': None,
+ 'is_public': bool(ispublic)
+ }
+
+
+INSTANCE_TYPES = {
+ '0': generate_flavor(0, True),
+ '1': generate_flavor(1, True),
+ '2': generate_flavor(2, False),
+ '3': generate_flavor(3, False)}
+
+
+ACCESS_LIST = [{'flavor_id': '2', 'project_id': 'proj2'},
+ {'flavor_id': '2', 'project_id': 'proj3'},
+ {'flavor_id': '3', 'project_id': 'proj3'}]
+
+
+def fake_get_flavor_access_by_flavor_id(context, flavorid):
+ res = []
+ for access in ACCESS_LIST:
+ if access['flavor_id'] == flavorid:
+ res.append(access)
+ return res
+
+
+def fake_get_flavor_by_flavor_id(context, flavorid, read_deleted=None):
+ return INSTANCE_TYPES[flavorid]
+
+
+def _has_flavor_access(flavorid, projectid):
+ for access in ACCESS_LIST:
+ if access['flavor_id'] == flavorid and \
+ access['project_id'] == projectid:
+ return True
+ return False
+
+
+def fake_get_all_flavors_sorted_list(context, inactive=False,
+ filters=None, sort_key='flavorid',
+ sort_dir='asc', limit=None, marker=None):
+ if filters is None or filters['is_public'] is None:
+ return sorted(INSTANCE_TYPES.values(), key=lambda item: item[sort_key])
+
+ res = {}
+ for k, v in INSTANCE_TYPES.iteritems():
+ if filters['is_public'] and _has_flavor_access(k, context.project_id):
+ res.update({k: v})
+ continue
+ if v['is_public'] == filters['is_public']:
+ res.update({k: v})
+
+ res = sorted(res.values(), key=lambda item: item[sort_key])
+ return res
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context.get_admin_context()}
+
+ def get_db_flavor(self, flavor_id):
+ return INSTANCE_TYPES[flavor_id]
+
+
+class FakeResponse(object):
+ obj = {'flavor': {'id': '0'},
+ 'flavors': [
+ {'id': '0'},
+ {'id': '2'}]
+ }
+
+ def attach(self, **kwargs):
+ pass
+
+
+class FlavorAccessTestV21(test.NoDBTestCase):
+ api_version = "2.1"
+ FlavorAccessController = flavor_access_v3.FlavorAccessController
+ FlavorActionController = flavor_access_v3.FlavorActionController
+ _prefix = "/v3"
+ validation_ex = exception.ValidationError
+
+ def setUp(self):
+ super(FlavorAccessTestV21, self).setUp()
+ self.flavor_controller = flavors_api.Controller()
+ self.req = FakeRequest()
+ self.context = self.req.environ['nova.context']
+ self.stubs.Set(db, 'flavor_get_by_flavor_id',
+ fake_get_flavor_by_flavor_id)
+ self.stubs.Set(db, 'flavor_get_all',
+ fake_get_all_flavors_sorted_list)
+ self.stubs.Set(db, 'flavor_access_get_by_flavor_id',
+ fake_get_flavor_access_by_flavor_id)
+
+ self.flavor_access_controller = self.FlavorAccessController()
+ self.flavor_action_controller = self.FlavorActionController()
+
+ def _verify_flavor_list(self, result, expected):
+ # result already sorted by flavor_id
+ self.assertEqual(len(result), len(expected))
+
+ for d1, d2 in zip(result, expected):
+ self.assertEqual(d1['id'], d2['id'])
+
+ def test_list_flavor_access_public(self):
+ # query os-flavor-access on public flavor should return 404
+ self.assertRaises(exc.HTTPNotFound,
+ self.flavor_access_controller.index,
+ self.req, '1')
+
+ def test_list_flavor_access_private(self):
+ expected = {'flavor_access': [
+ {'flavor_id': '2', 'tenant_id': 'proj2'},
+ {'flavor_id': '2', 'tenant_id': 'proj3'}]}
+ result = self.flavor_access_controller.index(self.req, '2')
+ self.assertEqual(result, expected)
+
+ def test_list_with_no_context(self):
+ req = fakes.HTTPRequest.blank(self._prefix + '/flavors/fake/flavors')
+
+ def fake_authorize(context, target=None, action=None):
+ raise exception.PolicyNotAuthorized(action='index')
+
+ if self.api_version == "2.1":
+ self.stubs.Set(flavor_access_v3,
+ 'authorize',
+ fake_authorize)
+ else:
+ self.stubs.Set(flavor_access_v2,
+ 'authorize',
+ fake_authorize)
+
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.flavor_access_controller.index,
+ req, 'fake')
+
+ def test_list_flavor_with_admin_default_proj1(self):
+ expected = {'flavors': [{'id': '0'}, {'id': '1'}]}
+ req = fakes.HTTPRequest.blank(self._prefix + '/fake/flavors',
+ use_admin_context=True)
+ req.environ['nova.context'].project_id = 'proj1'
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_list_flavor_with_admin_default_proj2(self):
+ expected = {'flavors': [{'id': '0'}, {'id': '1'}, {'id': '2'}]}
+ req = fakes.HTTPRequest.blank(self._prefix + '/flavors',
+ use_admin_context=True)
+ req.environ['nova.context'].project_id = 'proj2'
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_list_flavor_with_admin_ispublic_true(self):
+ expected = {'flavors': [{'id': '0'}, {'id': '1'}]}
+ url = self._prefix + '/flavors?is_public=true'
+ req = fakes.HTTPRequest.blank(url,
+ use_admin_context=True)
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_list_flavor_with_admin_ispublic_false(self):
+ expected = {'flavors': [{'id': '2'}, {'id': '3'}]}
+ url = self._prefix + '/flavors?is_public=false'
+ req = fakes.HTTPRequest.blank(url,
+ use_admin_context=True)
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_list_flavor_with_admin_ispublic_false_proj2(self):
+ expected = {'flavors': [{'id': '2'}, {'id': '3'}]}
+ url = self._prefix + '/flavors?is_public=false'
+ req = fakes.HTTPRequest.blank(url,
+ use_admin_context=True)
+ req.environ['nova.context'].project_id = 'proj2'
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_list_flavor_with_admin_ispublic_none(self):
+ expected = {'flavors': [{'id': '0'}, {'id': '1'}, {'id': '2'},
+ {'id': '3'}]}
+ url = self._prefix + '/flavors?is_public=none'
+ req = fakes.HTTPRequest.blank(url,
+ use_admin_context=True)
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_list_flavor_with_no_admin_default(self):
+ expected = {'flavors': [{'id': '0'}, {'id': '1'}]}
+ req = fakes.HTTPRequest.blank(self._prefix + '/flavors',
+ use_admin_context=False)
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_list_flavor_with_no_admin_ispublic_true(self):
+ expected = {'flavors': [{'id': '0'}, {'id': '1'}]}
+ url = self._prefix + '/flavors?is_public=true'
+ req = fakes.HTTPRequest.blank(url,
+ use_admin_context=False)
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_list_flavor_with_no_admin_ispublic_false(self):
+ expected = {'flavors': [{'id': '0'}, {'id': '1'}]}
+ url = self._prefix + '/flavors?is_public=false'
+ req = fakes.HTTPRequest.blank(url,
+ use_admin_context=False)
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_list_flavor_with_no_admin_ispublic_none(self):
+ expected = {'flavors': [{'id': '0'}, {'id': '1'}]}
+ url = self._prefix + '/flavors?is_public=none'
+ req = fakes.HTTPRequest.blank(url,
+ use_admin_context=False)
+ result = self.flavor_controller.index(req)
+ self._verify_flavor_list(result['flavors'], expected['flavors'])
+
+ def test_show(self):
+ resp = FakeResponse()
+ self.flavor_action_controller.show(self.req, resp, '0')
+ self.assertEqual({'id': '0', 'os-flavor-access:is_public': True},
+ resp.obj['flavor'])
+ self.flavor_action_controller.show(self.req, resp, '2')
+ self.assertEqual({'id': '0', 'os-flavor-access:is_public': False},
+ resp.obj['flavor'])
+
+ def test_detail(self):
+ resp = FakeResponse()
+ self.flavor_action_controller.detail(self.req, resp)
+ self.assertEqual([{'id': '0', 'os-flavor-access:is_public': True},
+ {'id': '2', 'os-flavor-access:is_public': False}],
+ resp.obj['flavors'])
+
+ def test_create(self):
+ resp = FakeResponse()
+ self.flavor_action_controller.create(self.req, {}, resp)
+ self.assertEqual({'id': '0', 'os-flavor-access:is_public': True},
+ resp.obj['flavor'])
+
+ def _get_add_access(self):
+ if self.api_version == "2.1":
+ return self.flavor_action_controller._add_tenant_access
+ else:
+ return self.flavor_action_controller._addTenantAccess
+
+ def _get_remove_access(self):
+ if self.api_version == "2.1":
+ return self.flavor_action_controller._remove_tenant_access
+ else:
+ return self.flavor_action_controller._removeTenantAccess
+
+ def test_add_tenant_access(self):
+ def stub_add_flavor_access(context, flavorid, projectid):
+ self.assertEqual('3', flavorid, "flavorid")
+ self.assertEqual("proj2", projectid, "projectid")
+ self.stubs.Set(db, 'flavor_access_add',
+ stub_add_flavor_access)
+ expected = {'flavor_access':
+ [{'flavor_id': '3', 'tenant_id': 'proj3'}]}
+ body = {'addTenantAccess': {'tenant': 'proj2'}}
+ req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action',
+ use_admin_context=True)
+
+ add_access = self._get_add_access()
+ result = add_access(req, '3', body=body)
+ self.assertEqual(result, expected)
+
+ def test_add_tenant_access_with_no_admin_user(self):
+ req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action',
+ use_admin_context=False)
+ body = {'addTenantAccess': {'tenant': 'proj2'}}
+ add_access = self._get_add_access()
+ self.assertRaises(exception.PolicyNotAuthorized,
+ add_access, req, '2', body=body)
+
+ def test_add_tenant_access_with_no_tenant(self):
+ req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action',
+ use_admin_context=True)
+ body = {'addTenantAccess': {'foo': 'proj2'}}
+ add_access = self._get_add_access()
+ self.assertRaises(self.validation_ex,
+ add_access, req, '2', body=body)
+ body = {'addTenantAccess': {'tenant': ''}}
+ self.assertRaises(self.validation_ex,
+ add_access, req, '2', body=body)
+
+ def test_add_tenant_access_with_already_added_access(self):
+ def stub_add_flavor_access(context, flavorid, projectid):
+ raise exception.FlavorAccessExists(flavor_id=flavorid,
+ project_id=projectid)
+ self.stubs.Set(db, 'flavor_access_add',
+ stub_add_flavor_access)
+ body = {'addTenantAccess': {'tenant': 'proj2'}}
+ add_access = self._get_add_access()
+ self.assertRaises(exc.HTTPConflict,
+ add_access, self.req, '3', body=body)
+
+ def test_remove_tenant_access_with_bad_access(self):
+ def stub_remove_flavor_access(context, flavorid, projectid):
+ raise exception.FlavorAccessNotFound(flavor_id=flavorid,
+ project_id=projectid)
+ self.stubs.Set(db, 'flavor_access_remove',
+ stub_remove_flavor_access)
+ body = {'removeTenantAccess': {'tenant': 'proj2'}}
+ remove_access = self._get_remove_access()
+ self.assertRaises(exc.HTTPNotFound,
+ remove_access, self.req, '3', body=body)
+
+ def test_delete_tenant_access_with_no_tenant(self):
+ req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action',
+ use_admin_context=True)
+ remove_access = self._get_remove_access()
+ body = {'removeTenantAccess': {'foo': 'proj2'}}
+ self.assertRaises(self.validation_ex,
+ remove_access, req, '2', body=body)
+ body = {'removeTenantAccess': {'tenant': ''}}
+ self.assertRaises(self.validation_ex,
+ remove_access, req, '2', body=body)
+
+ def test_remove_tenant_access_with_no_admin_user(self):
+ req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action',
+ use_admin_context=False)
+ body = {'removeTenantAccess': {'tenant': 'proj2'}}
+ remove_access = self._get_remove_access()
+ self.assertRaises(exception.PolicyNotAuthorized,
+ remove_access, req, '2', body=body)
+
+
+class FlavorAccessTestV20(FlavorAccessTestV21):
+ api_version = "2.0"
+ FlavorAccessController = flavor_access_v2.FlavorAccessController
+ FlavorActionController = flavor_access_v2.FlavorActionController
+ _prefix = "/v2/fake"
+ validation_ex = exc.HTTPBadRequest
+
+
+class FlavorAccessSerializerTest(test.NoDBTestCase):
+ def test_serializer_empty(self):
+ serializer = flavor_access_v2.FlavorAccessTemplate()
+ text = serializer.serialize(dict(flavor_access=[]))
+ tree = etree.fromstring(text)
+ self.assertEqual(len(tree), 0)
+
+ def test_serializer(self):
+ expected = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<flavor_access>'
+ '<access tenant_id="proj2" flavor_id="2"/>'
+ '<access tenant_id="proj3" flavor_id="2"/>'
+ '</flavor_access>')
+ access_list = [{'flavor_id': '2', 'tenant_id': 'proj2'},
+ {'flavor_id': '2', 'tenant_id': 'proj3'}]
+
+ serializer = flavor_access_v2.FlavorAccessTemplate()
+ text = serializer.serialize(dict(flavor_access=access_list))
+ self.assertEqual(text, expected)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_disabled.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_disabled.py
new file mode 100644
index 0000000000..a646f43fd1
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_disabled.py
@@ -0,0 +1,127 @@
+# Copyright 2012 Nebula, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import flavor_disabled
+from nova.compute import flavors
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+FAKE_FLAVORS = {
+ 'flavor 1': {
+ "flavorid": '1',
+ "name": 'flavor 1',
+ "memory_mb": '256',
+ "root_gb": '10',
+ "swap": 512,
+ "vcpus": 1,
+ "ephemeral_gb": 1,
+ "disabled": False,
+ },
+ 'flavor 2': {
+ "flavorid": '2',
+ "name": 'flavor 2',
+ "memory_mb": '512',
+ "root_gb": '20',
+ "swap": None,
+ "vcpus": 1,
+ "ephemeral_gb": 1,
+ "disabled": True,
+ },
+}
+
+
+def fake_flavor_get_by_flavor_id(flavorid, ctxt=None):
+ return FAKE_FLAVORS['flavor %s' % flavorid]
+
+
+def fake_get_all_flavors_sorted_list(context=None, inactive=False,
+ filters=None, sort_key='flavorid',
+ sort_dir='asc', limit=None, marker=None):
+ return [
+ fake_flavor_get_by_flavor_id(1),
+ fake_flavor_get_by_flavor_id(2)
+ ]
+
+
+class FlavorDisabledTestV21(test.NoDBTestCase):
+ base_url = '/v2/fake/flavors'
+ content_type = 'application/json'
+ prefix = "OS-FLV-DISABLED:"
+
+ def setUp(self):
+ super(FlavorDisabledTestV21, self).setUp()
+ ext = ('nova.api.openstack.compute.contrib'
+ '.flavor_disabled.Flavor_disabled')
+ self.flags(osapi_compute_extension=[ext])
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(flavors, "get_all_flavors_sorted_list",
+ fake_get_all_flavors_sorted_list)
+ self.stubs.Set(flavors,
+ "get_flavor_by_flavor_id",
+ fake_flavor_get_by_flavor_id)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app_v21(init_only=('flavors')))
+ return res
+
+ def _get_flavor(self, body):
+ return jsonutils.loads(body).get('flavor')
+
+ def _get_flavors(self, body):
+ return jsonutils.loads(body).get('flavors')
+
+ def assertFlavorDisabled(self, flavor, disabled):
+ self.assertEqual(str(flavor.get('%sdisabled' % self.prefix)), disabled)
+
+ def test_show(self):
+ url = self.base_url + '/1'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertFlavorDisabled(self._get_flavor(res.body), 'False')
+
+ def test_detail(self):
+ url = self.base_url + '/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ flavors = self._get_flavors(res.body)
+ self.assertFlavorDisabled(flavors[0], 'False')
+ self.assertFlavorDisabled(flavors[1], 'True')
+
+
+class FlavorDisabledTestV2(FlavorDisabledTestV21):
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app())
+ return res
+
+
+class FlavorDisabledXmlTest(FlavorDisabledTestV2):
+ content_type = 'application/xml'
+ prefix = '{%s}' % flavor_disabled.Flavor_disabled.namespace
+
+ def _get_flavor(self, body):
+ return etree.XML(body)
+
+ def _get_flavors(self, body):
+ return etree.XML(body).getchildren()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_manage.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_manage.py
new file mode 100644
index 0000000000..3d44e4970b
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_manage.py
@@ -0,0 +1,465 @@
+# Copyright 2011 Andrew Bogott for the Wikimedia Foundation
+# 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 datetime
+
+import mock
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import flavor_access
+from nova.api.openstack.compute.contrib import flavormanage as flavormanage_v2
+from nova.api.openstack.compute.plugins.v3 import flavor_manage as \
+ flavormanage_v21
+from nova.compute import flavors
+from nova import context
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+def fake_db_flavor(**updates):
+ db_flavor = {
+ 'root_gb': 1,
+ 'ephemeral_gb': 1,
+ 'name': u'frob',
+ 'deleted': False,
+ 'created_at': datetime.datetime(2012, 1, 19, 18, 49, 30, 877329),
+ 'updated_at': None,
+ 'memory_mb': 256,
+ 'vcpus': 1,
+ 'flavorid': 1,
+ 'swap': 0,
+ 'rxtx_factor': 1.0,
+ 'extra_specs': {},
+ 'deleted_at': None,
+ 'vcpu_weight': None,
+ 'id': 7,
+ 'is_public': True,
+ 'disabled': False,
+ }
+ if updates:
+ db_flavor.update(updates)
+ return db_flavor
+
+
+def fake_get_flavor_by_flavor_id(flavorid, ctxt=None, read_deleted='yes'):
+ if flavorid == 'failtest':
+ raise exception.FlavorNotFound(flavor_id=flavorid)
+ elif not str(flavorid) == '1234':
+ raise Exception("This test expects flavorid 1234, not %s" % flavorid)
+ if read_deleted != 'no':
+ raise test.TestingException("Should not be reading deleted")
+ return fake_db_flavor(flavorid=flavorid)
+
+
+def fake_destroy(flavorname):
+ pass
+
+
+def fake_create(context, kwargs, projects=None):
+ newflavor = fake_db_flavor()
+
+ flavorid = kwargs.get('flavorid')
+ if flavorid is None:
+ flavorid = 1234
+
+ newflavor['flavorid'] = flavorid
+ newflavor["name"] = kwargs.get('name')
+ newflavor["memory_mb"] = int(kwargs.get('memory_mb'))
+ newflavor["vcpus"] = int(kwargs.get('vcpus'))
+ newflavor["root_gb"] = int(kwargs.get('root_gb'))
+ newflavor["ephemeral_gb"] = int(kwargs.get('ephemeral_gb'))
+ newflavor["swap"] = kwargs.get('swap')
+ newflavor["rxtx_factor"] = float(kwargs.get('rxtx_factor'))
+ newflavor["is_public"] = bool(kwargs.get('is_public'))
+ newflavor["disabled"] = bool(kwargs.get('disabled'))
+
+ return newflavor
+
+
+class FlavorManageTestV21(test.NoDBTestCase):
+ controller = flavormanage_v21.FlavorManageController()
+ validation_error = exception.ValidationError
+ base_url = '/v2/fake/flavors'
+
+ def setUp(self):
+ super(FlavorManageTestV21, self).setUp()
+ self.stubs.Set(flavors,
+ "get_flavor_by_flavor_id",
+ fake_get_flavor_by_flavor_id)
+ self.stubs.Set(flavors, "destroy", fake_destroy)
+ self.stubs.Set(db, "flavor_create", fake_create)
+ self.ctxt = context.RequestContext('fake', 'fake',
+ is_admin=True, auth_token=True)
+ self.app = self._setup_app()
+
+ self.request_body = {
+ "flavor": {
+ "name": "test",
+ "ram": 512,
+ "vcpus": 2,
+ "disk": 1,
+ "OS-FLV-EXT-DATA:ephemeral": 1,
+ "id": unicode('1234'),
+ "swap": 512,
+ "rxtx_factor": 1,
+ "os-flavor-access:is_public": True,
+ }
+ }
+ self.expected_flavor = self.request_body
+
+ def _setup_app(self):
+ return fakes.wsgi_app_v21(init_only=('flavor-manage', 'os-flavor-rxtx',
+ 'os-flavor-access', 'flavors',
+ 'os-flavor-extra-data'))
+
+ def test_delete(self):
+ req = fakes.HTTPRequest.blank(self.base_url + '/1234')
+ res = self.controller._delete(req, 1234)
+
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.controller,
+ flavormanage_v21.FlavorManageController):
+ status_int = self.controller._delete.wsgi_code
+ else:
+ status_int = res.status_int
+ self.assertEqual(202, status_int)
+
+ # subsequent delete should fail
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._delete, req, "failtest")
+
+ def _test_create_missing_parameter(self, parameter):
+ body = {
+ "flavor": {
+ "name": "azAZ09. -_",
+ "ram": 512,
+ "vcpus": 2,
+ "disk": 1,
+ "OS-FLV-EXT-DATA:ephemeral": 1,
+ "id": unicode('1234'),
+ "swap": 512,
+ "rxtx_factor": 1,
+ "os-flavor-access:is_public": True,
+ }
+ }
+
+ del body['flavor'][parameter]
+
+ req = fakes.HTTPRequest.blank(self.base_url)
+ self.assertRaises(self.validation_error, self.controller._create,
+ req, body=body)
+
+ def test_create_missing_name(self):
+ self._test_create_missing_parameter('name')
+
+ def test_create_missing_ram(self):
+ self._test_create_missing_parameter('ram')
+
+ def test_create_missing_vcpus(self):
+ self._test_create_missing_parameter('vcpus')
+
+ def test_create_missing_disk(self):
+ self._test_create_missing_parameter('disk')
+
+ def _create_flavor_success_case(self, body):
+ req = webob.Request.blank(self.base_url)
+ req.headers['Content-Type'] = 'application/json'
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(200, res.status_code)
+ return jsonutils.loads(res.body)
+
+ def test_create(self):
+ body = self._create_flavor_success_case(self.request_body)
+ for key in self.expected_flavor["flavor"]:
+ self.assertEqual(body["flavor"][key],
+ self.expected_flavor["flavor"][key])
+
+ def test_create_public_default(self):
+ del self.request_body['flavor']['os-flavor-access:is_public']
+ body = self._create_flavor_success_case(self.request_body)
+ for key in self.expected_flavor["flavor"]:
+ self.assertEqual(body["flavor"][key],
+ self.expected_flavor["flavor"][key])
+
+ def test_create_without_flavorid(self):
+ del self.request_body['flavor']['id']
+ body = self._create_flavor_success_case(self.request_body)
+ for key in self.expected_flavor["flavor"]:
+ self.assertEqual(body["flavor"][key],
+ self.expected_flavor["flavor"][key])
+
+ def _create_flavor_bad_request_case(self, body):
+ self.stubs.UnsetAll()
+
+ req = webob.Request.blank(self.base_url)
+ req.headers['Content-Type'] = 'application/json'
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_code, 400)
+
+ def test_create_invalid_name(self):
+ self.request_body['flavor']['name'] = 'bad !@#!$% name'
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_flavor_name_is_whitespace(self):
+ self.request_body['flavor']['name'] = ' '
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_with_name_too_long(self):
+ self.request_body['flavor']['name'] = 'a' * 256
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_without_flavorname(self):
+ del self.request_body['flavor']['name']
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_empty_body(self):
+ body = {
+ "flavor": {}
+ }
+ self._create_flavor_bad_request_case(body)
+
+ def test_create_no_body(self):
+ body = {}
+ self._create_flavor_bad_request_case(body)
+
+ def test_create_invalid_format_body(self):
+ body = {
+ "flavor": []
+ }
+ self._create_flavor_bad_request_case(body)
+
+ def test_create_invalid_flavorid(self):
+ self.request_body['flavor']['id'] = "!@#!$#!$^#&^$&"
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_check_flavor_id_length(self):
+ MAX_LENGTH = 255
+ self.request_body['flavor']['id'] = "a" * (MAX_LENGTH + 1)
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_with_leading_trailing_whitespaces_in_flavor_id(self):
+ self.request_body['flavor']['id'] = " bad_id "
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_without_ram(self):
+ del self.request_body['flavor']['ram']
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_with_0_ram(self):
+ self.request_body['flavor']['ram'] = 0
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_without_vcpus(self):
+ del self.request_body['flavor']['vcpus']
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_with_0_vcpus(self):
+ self.request_body['flavor']['vcpus'] = 0
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_without_disk(self):
+ del self.request_body['flavor']['disk']
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_with_minus_disk(self):
+ self.request_body['flavor']['disk'] = -1
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_with_minus_ephemeral(self):
+ self.request_body['flavor']['OS-FLV-EXT-DATA:ephemeral'] = -1
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_with_minus_swap(self):
+ self.request_body['flavor']['swap'] = -1
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_with_minus_rxtx_factor(self):
+ self.request_body['flavor']['rxtx_factor'] = -1
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_create_with_non_boolean_is_public(self):
+ self.request_body['flavor']['os-flavor-access:is_public'] = 123
+ self._create_flavor_bad_request_case(self.request_body)
+
+ def test_flavor_exists_exception_returns_409(self):
+ expected = {
+ "flavor": {
+ "name": "test",
+ "ram": 512,
+ "vcpus": 2,
+ "disk": 1,
+ "OS-FLV-EXT-DATA:ephemeral": 1,
+ "id": 1235,
+ "swap": 512,
+ "rxtx_factor": 1,
+ "os-flavor-access:is_public": True,
+ }
+ }
+
+ def fake_create(name, memory_mb, vcpus, root_gb, ephemeral_gb,
+ flavorid, swap, rxtx_factor, is_public):
+ raise exception.FlavorExists(name=name)
+
+ self.stubs.Set(flavors, "create", fake_create)
+ req = webob.Request.blank(self.base_url)
+ req.headers['Content-Type'] = 'application/json'
+ req.method = 'POST'
+ req.body = jsonutils.dumps(expected)
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 409)
+
+ @mock.patch('nova.compute.flavors.create',
+ side_effect=exception.FlavorCreateFailed)
+ def test_flavor_create_db_failed(self, mock_create):
+ request_dict = {
+ "flavor": {
+ "name": "test",
+ 'id': "12345",
+ "ram": 512,
+ "vcpus": 2,
+ "disk": 1,
+ "OS-FLV-EXT-DATA:ephemeral": 1,
+ "swap": 512,
+ "rxtx_factor": 1,
+ "os-flavor-access:is_public": True,
+ }
+ }
+ req = webob.Request.blank(self.base_url)
+ req.headers['Content-Type'] = 'application/json'
+ req.method = 'POST'
+ req.body = jsonutils.dumps(request_dict)
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 500)
+ self.assertIn('Unable to create flavor', res.body)
+
+ def test_invalid_memory_mb(self):
+ """Check negative and decimal number can't be accepted."""
+
+ self.stubs.UnsetAll()
+ self.assertRaises(exception.InvalidInput, flavors.create, "abc",
+ -512, 2, 1, 1, 1234, 512, 1, True)
+ self.assertRaises(exception.InvalidInput, flavors.create, "abcd",
+ 512.2, 2, 1, 1, 1234, 512, 1, True)
+ self.assertRaises(exception.InvalidInput, flavors.create, "abcde",
+ None, 2, 1, 1, 1234, 512, 1, True)
+ self.assertRaises(exception.InvalidInput, flavors.create, "abcdef",
+ 512, 2, None, 1, 1234, 512, 1, True)
+ self.assertRaises(exception.InvalidInput, flavors.create, "abcdef",
+ "test_memory_mb", 2, None, 1, 1234, 512, 1, True)
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context.get_admin_context()}
+
+
+class PrivateFlavorManageTestV21(test.TestCase):
+ controller = flavormanage_v21.FlavorManageController()
+ base_url = '/v2/fake/flavors'
+
+ def setUp(self):
+ super(PrivateFlavorManageTestV21, self).setUp()
+ self.flavor_access_controller = flavor_access.FlavorAccessController()
+ self.ctxt = context.RequestContext('fake', 'fake',
+ is_admin=True, auth_token=True)
+ self.app = self._setup_app()
+ self.expected = {
+ "flavor": {
+ "name": "test",
+ "ram": 512,
+ "vcpus": 2,
+ "disk": 1,
+ "OS-FLV-EXT-DATA:ephemeral": 1,
+ "swap": 512,
+ "rxtx_factor": 1
+ }
+ }
+
+ def _setup_app(self):
+ return fakes.wsgi_app_v21(init_only=('flavor-manage',
+ 'os-flavor-access',
+ 'os-flavor-rxtx', 'flavors',
+ 'os-flavor-extra-data'),
+ fake_auth_context=self.ctxt)
+
+ def _get_response(self):
+ req = webob.Request.blank(self.base_url)
+ req.headers['Content-Type'] = 'application/json'
+ req.method = 'POST'
+ req.body = jsonutils.dumps(self.expected)
+ res = req.get_response(self.app)
+ return jsonutils.loads(res.body)
+
+ def test_create_private_flavor_should_not_grant_flavor_access(self):
+ self.expected["flavor"]["os-flavor-access:is_public"] = False
+ body = self._get_response()
+ for key in self.expected["flavor"]:
+ self.assertEqual(body["flavor"][key], self.expected["flavor"][key])
+ flavor_access_body = self.flavor_access_controller.index(
+ FakeRequest(), body["flavor"]["id"])
+ expected_flavor_access_body = {
+ "tenant_id": "%s" % self.ctxt.project_id,
+ "flavor_id": "%s" % body["flavor"]["id"]
+ }
+ self.assertNotIn(expected_flavor_access_body,
+ flavor_access_body["flavor_access"])
+
+ def test_create_public_flavor_should_not_create_flavor_access(self):
+ self.expected["flavor"]["os-flavor-access:is_public"] = True
+ self.mox.StubOutWithMock(flavors, "add_flavor_access")
+ self.mox.ReplayAll()
+ body = self._get_response()
+ for key in self.expected["flavor"]:
+ self.assertEqual(body["flavor"][key], self.expected["flavor"][key])
+
+
+class FlavorManageTestV2(FlavorManageTestV21):
+ controller = flavormanage_v2.FlavorManageController()
+ validation_error = webob.exc.HTTPBadRequest
+
+ def setUp(self):
+ super(FlavorManageTestV2, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Flavormanage', 'Flavorextradata',
+ 'Flavor_access', 'Flavor_rxtx', 'Flavor_swap'])
+
+ def _setup_app(self):
+ return fakes.wsgi_app(init_only=('flavors',),
+ fake_auth_context=self.ctxt)
+
+
+class PrivateFlavorManageTestV2(PrivateFlavorManageTestV21):
+ controller = flavormanage_v2.FlavorManageController()
+
+ def setUp(self):
+ super(PrivateFlavorManageTestV2, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Flavormanage', 'Flavorextradata',
+ 'Flavor_access', 'Flavor_rxtx', 'Flavor_swap'])
+
+ def _setup_app(self):
+ return fakes.wsgi_app(init_only=('flavors',),
+ fake_auth_context=self.ctxt)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_rxtx.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_rxtx.py
new file mode 100644
index 0000000000..a8f31653c1
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_rxtx.py
@@ -0,0 +1,127 @@
+# Copyright 2012 Nebula, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.compute import flavors
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+FAKE_FLAVORS = {
+ 'flavor 1': {
+ "flavorid": '1',
+ "name": 'flavor 1',
+ "memory_mb": '256',
+ "root_gb": '10',
+ "swap": '5',
+ "disabled": False,
+ "ephemeral_gb": '20',
+ "rxtx_factor": '1.0',
+ "vcpus": 1,
+ },
+ 'flavor 2': {
+ "flavorid": '2',
+ "name": 'flavor 2',
+ "memory_mb": '512',
+ "root_gb": '10',
+ "swap": '10',
+ "ephemeral_gb": '25',
+ "rxtx_factor": None,
+ "disabled": False,
+ "vcpus": 1,
+ },
+}
+
+
+def fake_flavor_get_by_flavor_id(flavorid, ctxt=None):
+ return FAKE_FLAVORS['flavor %s' % flavorid]
+
+
+def fake_get_all_flavors_sorted_list(context=None, inactive=False,
+ filters=None, sort_key='flavorid',
+ sort_dir='asc', limit=None, marker=None):
+ return [
+ fake_flavor_get_by_flavor_id(1),
+ fake_flavor_get_by_flavor_id(2)
+ ]
+
+
+class FlavorRxtxTestV21(test.NoDBTestCase):
+ content_type = 'application/json'
+ _prefix = "/v2/fake"
+
+ def setUp(self):
+ super(FlavorRxtxTestV21, self).setUp()
+ ext = ('nova.api.openstack.compute.contrib'
+ '.flavor_rxtx.Flavor_rxtx')
+ self.flags(osapi_compute_extension=[ext])
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(flavors, "get_all_flavors_sorted_list",
+ fake_get_all_flavors_sorted_list)
+ self.stubs.Set(flavors,
+ "get_flavor_by_flavor_id",
+ fake_flavor_get_by_flavor_id)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(self._get_app())
+ return res
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21(init_only=('servers',
+ 'flavors', 'os-flavor-rxtx'))
+
+ def _get_flavor(self, body):
+ return jsonutils.loads(body).get('flavor')
+
+ def _get_flavors(self, body):
+ return jsonutils.loads(body).get('flavors')
+
+ def assertFlavorRxtx(self, flavor, rxtx):
+ self.assertEqual(str(flavor.get('rxtx_factor')), rxtx)
+
+ def test_show(self):
+ url = self._prefix + '/flavors/1'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertFlavorRxtx(self._get_flavor(res.body), '1.0')
+
+ def test_detail(self):
+ url = self._prefix + '/flavors/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ flavors = self._get_flavors(res.body)
+ self.assertFlavorRxtx(flavors[0], '1.0')
+ self.assertFlavorRxtx(flavors[1], '')
+
+
+class FlavorRxtxTestV20(FlavorRxtxTestV21):
+
+ def _get_app(self):
+ return fakes.wsgi_app()
+
+
+class FlavorRxtxXmlTest(FlavorRxtxTestV20):
+ content_type = 'application/xml'
+
+ def _get_flavor(self, body):
+ return etree.XML(body)
+
+ def _get_flavors(self, body):
+ return etree.XML(body).getchildren()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_swap.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_swap.py
new file mode 100644
index 0000000000..f168db060a
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_swap.py
@@ -0,0 +1,126 @@
+# Copyright 2012 Nebula, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.compute import flavors
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+FAKE_FLAVORS = {
+ 'flavor 1': {
+ "flavorid": '1',
+ "name": 'flavor 1',
+ "memory_mb": '256',
+ "root_gb": '10',
+ "swap": 512,
+ "vcpus": 1,
+ "ephemeral_gb": 1,
+ "disabled": False,
+ },
+ 'flavor 2': {
+ "flavorid": '2',
+ "name": 'flavor 2',
+ "memory_mb": '512',
+ "root_gb": '10',
+ "swap": None,
+ "vcpus": 1,
+ "ephemeral_gb": 1,
+ "disabled": False,
+ },
+}
+
+
+# TODO(jogo) dedup these across nova.api.openstack.contrib.test_flavor*
+def fake_flavor_get_by_flavor_id(flavorid, ctxt=None):
+ return FAKE_FLAVORS['flavor %s' % flavorid]
+
+
+def fake_get_all_flavors_sorted_list(context=None, inactive=False,
+ filters=None, sort_key='flavorid',
+ sort_dir='asc', limit=None, marker=None):
+ return [
+ fake_flavor_get_by_flavor_id(1),
+ fake_flavor_get_by_flavor_id(2)
+ ]
+
+
+class FlavorSwapTestV21(test.NoDBTestCase):
+ base_url = '/v2/fake/flavors'
+ content_type = 'application/json'
+ prefix = ''
+
+ def setUp(self):
+ super(FlavorSwapTestV21, self).setUp()
+ ext = ('nova.api.openstack.compute.contrib'
+ '.flavor_swap.Flavor_swap')
+ self.flags(osapi_compute_extension=[ext])
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(flavors, "get_all_flavors_sorted_list",
+ fake_get_all_flavors_sorted_list)
+ self.stubs.Set(flavors,
+ "get_flavor_by_flavor_id",
+ fake_flavor_get_by_flavor_id)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app_v21(init_only=('flavors')))
+ return res
+
+ def _get_flavor(self, body):
+ return jsonutils.loads(body).get('flavor')
+
+ def _get_flavors(self, body):
+ return jsonutils.loads(body).get('flavors')
+
+ def assertFlavorSwap(self, flavor, swap):
+ self.assertEqual(str(flavor.get('%sswap' % self.prefix)), swap)
+
+ def test_show(self):
+ url = self.base_url + '/1'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertFlavorSwap(self._get_flavor(res.body), '512')
+
+ def test_detail(self):
+ url = self.base_url + '/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ flavors = self._get_flavors(res.body)
+ self.assertFlavorSwap(flavors[0], '512')
+ self.assertFlavorSwap(flavors[1], '')
+
+
+class FlavorSwapTestV2(FlavorSwapTestV21):
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app())
+ return res
+
+
+class FlavorSwapXmlTest(FlavorSwapTestV2):
+ content_type = 'application/xml'
+
+ def _get_flavor(self, body):
+ return etree.XML(body)
+
+ def _get_flavors(self, body):
+ return etree.XML(body).getchildren()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavorextradata.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavorextradata.py
new file mode 100644
index 0000000000..1299b6c88d
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavorextradata.py
@@ -0,0 +1,127 @@
+# Copyright 2012 OpenStack Foundation
+# 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 datetime
+
+from oslo.serialization import jsonutils
+import webob
+
+from nova.compute import flavors
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+def fake_get_flavor_by_flavor_id(flavorid, ctxt=None):
+ return {
+ 'id': flavorid,
+ 'flavorid': str(flavorid),
+ 'root_gb': 1,
+ 'ephemeral_gb': 1,
+ 'name': u'test',
+ 'deleted': False,
+ 'created_at': datetime.datetime(2012, 1, 1, 1, 1, 1, 1),
+ 'updated_at': None,
+ 'memory_mb': 512,
+ 'vcpus': 1,
+ 'extra_specs': {},
+ 'deleted_at': None,
+ 'vcpu_weight': None,
+ 'swap': 0,
+ 'disabled': False,
+ }
+
+
+def fake_get_all_flavors_sorted_list(context=None, inactive=False,
+ filters=None, sort_key='flavorid',
+ sort_dir='asc', limit=None, marker=None):
+ return [
+ fake_get_flavor_by_flavor_id(1),
+ fake_get_flavor_by_flavor_id(2)
+ ]
+
+
+class FlavorExtraDataTestV21(test.NoDBTestCase):
+ base_url = '/v2/fake/flavors'
+
+ def setUp(self):
+ super(FlavorExtraDataTestV21, self).setUp()
+ ext = ('nova.api.openstack.compute.contrib'
+ '.flavorextradata.Flavorextradata')
+ self.flags(osapi_compute_extension=[ext])
+ self.stubs.Set(flavors, 'get_flavor_by_flavor_id',
+ fake_get_flavor_by_flavor_id)
+ self.stubs.Set(flavors, 'get_all_flavors_sorted_list',
+ fake_get_all_flavors_sorted_list)
+ self._setup_app()
+
+ def _setup_app(self):
+ self.app = fakes.wsgi_app_v21(init_only=('flavors'))
+
+ def _verify_flavor_response(self, flavor, expected):
+ for key in expected:
+ self.assertEqual(flavor[key], expected[key])
+
+ def test_show(self):
+ expected = {
+ 'flavor': {
+ 'id': '1',
+ 'name': 'test',
+ 'ram': 512,
+ 'vcpus': 1,
+ 'disk': 1,
+ 'OS-FLV-EXT-DATA:ephemeral': 1,
+ }
+ }
+
+ url = self.base_url + '/1'
+ req = webob.Request.blank(url)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ body = jsonutils.loads(res.body)
+ self._verify_flavor_response(body['flavor'], expected['flavor'])
+
+ def test_detail(self):
+ expected = [
+ {
+ 'id': '1',
+ 'name': 'test',
+ 'ram': 512,
+ 'vcpus': 1,
+ 'disk': 1,
+ 'OS-FLV-EXT-DATA:ephemeral': 1,
+ },
+ {
+ 'id': '2',
+ 'name': 'test',
+ 'ram': 512,
+ 'vcpus': 1,
+ 'disk': 1,
+ 'OS-FLV-EXT-DATA:ephemeral': 1,
+ },
+ ]
+
+ url = self.base_url + '/detail'
+ req = webob.Request.blank(url)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ body = jsonutils.loads(res.body)
+ for i, flavor in enumerate(body['flavors']):
+ self._verify_flavor_response(flavor, expected[i])
+
+
+class FlavorExtraDataTestV2(FlavorExtraDataTestV21):
+
+ def _setup_app(self):
+ self.app = fakes.wsgi_app(init_only=('flavors',))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavors_extra_specs.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavors_extra_specs.py
new file mode 100644
index 0000000000..8a6f4814a8
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavors_extra_specs.py
@@ -0,0 +1,403 @@
+# Copyright 2011 University of Southern California
+# 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 mock
+import webob
+
+from nova.api.openstack.compute.contrib import flavorextraspecs \
+ as flavorextraspecs_v2
+from nova.api.openstack.compute.plugins.v3 import flavors_extraspecs \
+ as flavorextraspecs_v21
+import nova.db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.objects import test_flavor
+
+
+def return_create_flavor_extra_specs(context, flavor_id, extra_specs):
+ return stub_flavor_extra_specs()
+
+
+def return_flavor_extra_specs(context, flavor_id):
+ return stub_flavor_extra_specs()
+
+
+def return_flavor_extra_specs_item(context, flavor_id, key):
+ return {key: stub_flavor_extra_specs()[key]}
+
+
+def return_empty_flavor_extra_specs(context, flavor_id):
+ return {}
+
+
+def delete_flavor_extra_specs(context, flavor_id, key):
+ pass
+
+
+def stub_flavor_extra_specs():
+ specs = {
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ "key4": "value4",
+ "key5": "value5"}
+ return specs
+
+
+class FlavorsExtraSpecsTestV21(test.TestCase):
+ bad_request = exception.ValidationError
+ flavorextraspecs = flavorextraspecs_v21
+
+ def _get_request(self, url, use_admin_context=False):
+ req_url = '/v2/fake/flavors/' + url
+ return fakes.HTTPRequest.blank(req_url,
+ use_admin_context=use_admin_context)
+
+ def setUp(self):
+ super(FlavorsExtraSpecsTestV21, self).setUp()
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ self.controller = self.flavorextraspecs.FlavorExtraSpecsController()
+
+ def test_index(self):
+ flavor = dict(test_flavor.fake_flavor,
+ extra_specs={'key1': 'value1'})
+
+ req = self._get_request('1/os-extra_specs')
+ with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get:
+ mock_get.return_value = flavor
+ res_dict = self.controller.index(req, 1)
+
+ self.assertEqual('value1', res_dict['extra_specs']['key1'])
+
+ def test_index_no_data(self):
+ self.stubs.Set(nova.db, 'flavor_extra_specs_get',
+ return_empty_flavor_extra_specs)
+
+ req = self._get_request('1/os-extra_specs')
+ res_dict = self.controller.index(req, 1)
+
+ self.assertEqual(0, len(res_dict['extra_specs']))
+
+ def test_show(self):
+ flavor = dict(test_flavor.fake_flavor,
+ extra_specs={'key5': 'value5'})
+ req = self._get_request('1/os-extra_specs/key5')
+ with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get:
+ mock_get.return_value = flavor
+ res_dict = self.controller.show(req, 1, 'key5')
+
+ self.assertEqual('value5', res_dict['key5'])
+
+ def test_show_spec_not_found(self):
+ self.stubs.Set(nova.db, 'flavor_extra_specs_get',
+ return_empty_flavor_extra_specs)
+
+ req = self._get_request('1/os-extra_specs/key6')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
+ req, 1, 'key6')
+
+ def test_not_found_because_flavor(self):
+ req = self._get_request('1/os-extra_specs/key5',
+ use_admin_context=True)
+ with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get:
+ mock_get.side_effect = exception.FlavorNotFound(flavor_id='1')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
+ req, 1, 'key5')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
+ req, 1, 'key5', body={'key5': 'value5'})
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, 1, 'key5')
+
+ req = self._get_request('1/os-extra_specs', use_admin_context=True)
+ with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get:
+ mock_get.side_effect = exception.FlavorNotFound(flavor_id='1')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.create,
+ req, 1, body={'extra_specs': {'key5': 'value5'}})
+
+ def test_delete(self):
+ flavor = dict(test_flavor.fake_flavor,
+ extra_specs={'key5': 'value5'})
+ self.stubs.Set(nova.db, 'flavor_extra_specs_delete',
+ delete_flavor_extra_specs)
+
+ req = self._get_request('1/os-extra_specs/key5',
+ use_admin_context=True)
+ with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get:
+ mock_get.return_value = flavor
+ self.controller.delete(req, 1, 'key5')
+
+ def test_delete_no_admin(self):
+ self.stubs.Set(nova.db, 'flavor_extra_specs_delete',
+ delete_flavor_extra_specs)
+
+ req = self._get_request('1/os-extra_specs/key5')
+ self.assertRaises(exception.Forbidden, self.controller.delete,
+ req, 1, 'key 5')
+
+ def test_delete_spec_not_found(self):
+ req = self._get_request('1/os-extra_specs/key6',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, 1, 'key6')
+
+ def test_create(self):
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ return_create_flavor_extra_specs)
+ body = {"extra_specs": {"key1": "value1", "key2": 0.5, "key3": 5}}
+
+ req = self._get_request('1/os-extra_specs', use_admin_context=True)
+ res_dict = self.controller.create(req, 1, body=body)
+
+ self.assertEqual('value1', res_dict['extra_specs']['key1'])
+ self.assertEqual(0.5, res_dict['extra_specs']['key2'])
+ self.assertEqual(5, res_dict['extra_specs']['key3'])
+
+ def test_create_no_admin(self):
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ return_create_flavor_extra_specs)
+ body = {"extra_specs": {"key1": "value1"}}
+
+ req = self._get_request('1/os-extra_specs')
+ self.assertRaises(exception.Forbidden, self.controller.create,
+ req, 1, body=body)
+
+ def test_create_flavor_not_found(self):
+ def fake_instance_type_extra_specs_update_or_create(*args, **kwargs):
+ raise exception.FlavorNotFound(flavor_id='')
+
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ fake_instance_type_extra_specs_update_or_create)
+ body = {"extra_specs": {"key1": "value1"}}
+ req = self._get_request('1/os-extra_specs', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.create,
+ req, 1, body=body)
+
+ def test_create_flavor_db_duplicate(self):
+ def fake_instance_type_extra_specs_update_or_create(*args, **kwargs):
+ raise exception.FlavorExtraSpecUpdateCreateFailed(id=1, retries=5)
+
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ fake_instance_type_extra_specs_update_or_create)
+ body = {"extra_specs": {"key1": "value1"}}
+ req = self._get_request('1/os-extra_specs', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPConflict, self.controller.create,
+ req, 1, body=body)
+
+ def _test_create_bad_request(self, body):
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ return_create_flavor_extra_specs)
+
+ req = self._get_request('1/os-extra_specs', use_admin_context=True)
+ self.assertRaises(self.bad_request, self.controller.create,
+ req, 1, body=body)
+
+ def test_create_empty_body(self):
+ self._test_create_bad_request('')
+
+ def test_create_non_dict_extra_specs(self):
+ self._test_create_bad_request({"extra_specs": "non_dict"})
+
+ def test_create_non_string_key(self):
+ self._test_create_bad_request({"extra_specs": {None: "value1"}})
+
+ def test_create_non_string_value(self):
+ self._test_create_bad_request({"extra_specs": {"key1": None}})
+
+ def test_create_zero_length_key(self):
+ self._test_create_bad_request({"extra_specs": {"": "value1"}})
+
+ def test_create_long_key(self):
+ key = "a" * 256
+ self._test_create_bad_request({"extra_specs": {key: "value1"}})
+
+ def test_create_long_value(self):
+ value = "a" * 256
+ self._test_create_bad_request({"extra_specs": {"key1": value}})
+
+ @mock.patch('nova.db.flavor_extra_specs_update_or_create')
+ def test_create_really_long_integer_value(self, mock_flavor_extra_specs):
+ value = 10 ** 1000
+ mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs
+
+ req = self._get_request('1/os-extra_specs', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, 1, body={"extra_specs": {"key1": value}})
+
+ @mock.patch('nova.db.flavor_extra_specs_update_or_create')
+ def test_create_invalid_specs_key(self, mock_flavor_extra_specs):
+ invalid_keys = ("key1/", "<key>", "$$akey$", "!akey", "")
+ mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs
+
+ for key in invalid_keys:
+ body = {"extra_specs": {key: "value1"}}
+ req = self._get_request('1/os-extra_specs', use_admin_context=True)
+ self.assertRaises(self.bad_request, self.controller.create,
+ req, 1, body=body)
+
+ @mock.patch('nova.db.flavor_extra_specs_update_or_create')
+ def test_create_valid_specs_key(self, mock_flavor_extra_specs):
+ valid_keys = ("key1", "month.price", "I_am-a Key", "finance:g2")
+ mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs
+
+ for key in valid_keys:
+ body = {"extra_specs": {key: "value1"}}
+ req = self._get_request('1/os-extra_specs', use_admin_context=True)
+ res_dict = self.controller.create(req, 1, body=body)
+ self.assertEqual('value1', res_dict['extra_specs'][key])
+
+ def test_update_item(self):
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ return_create_flavor_extra_specs)
+ body = {"key1": "value1"}
+
+ req = self._get_request('1/os-extra_specs/key1',
+ use_admin_context=True)
+ res_dict = self.controller.update(req, 1, 'key1', body=body)
+
+ self.assertEqual('value1', res_dict['key1'])
+
+ def test_update_item_no_admin(self):
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ return_create_flavor_extra_specs)
+ body = {"key1": "value1"}
+
+ req = self._get_request('1/os-extra_specs/key1')
+ self.assertRaises(exception.Forbidden, self.controller.update,
+ req, 1, 'key1', body=body)
+
+ def _test_update_item_bad_request(self, body):
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ return_create_flavor_extra_specs)
+
+ req = self._get_request('1/os-extra_specs/key1',
+ use_admin_context=True)
+ self.assertRaises(self.bad_request, self.controller.update,
+ req, 1, 'key1', body=body)
+
+ def test_update_item_empty_body(self):
+ self._test_update_item_bad_request('')
+
+ def test_update_item_too_many_keys(self):
+ body = {"key1": "value1", "key2": "value2"}
+ self._test_update_item_bad_request(body)
+
+ def test_update_item_non_dict_extra_specs(self):
+ self._test_update_item_bad_request("non_dict")
+
+ def test_update_item_non_string_key(self):
+ self._test_update_item_bad_request({None: "value1"})
+
+ def test_update_item_non_string_value(self):
+ self._test_update_item_bad_request({"key1": None})
+
+ def test_update_item_zero_length_key(self):
+ self._test_update_item_bad_request({"": "value1"})
+
+ def test_update_item_long_key(self):
+ key = "a" * 256
+ self._test_update_item_bad_request({key: "value1"})
+
+ def test_update_item_long_value(self):
+ value = "a" * 256
+ self._test_update_item_bad_request({"key1": value})
+
+ def test_update_item_body_uri_mismatch(self):
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ return_create_flavor_extra_specs)
+ body = {"key1": "value1"}
+
+ req = self._get_request('1/os-extra_specs/bad', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 1, 'bad', body=body)
+
+ def test_update_flavor_not_found(self):
+ def fake_instance_type_extra_specs_update_or_create(*args, **kwargs):
+ raise exception.FlavorNotFound(flavor_id='')
+
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ fake_instance_type_extra_specs_update_or_create)
+ body = {"key1": "value1"}
+
+ req = self._get_request('1/os-extra_specs/key1',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
+ req, 1, 'key1', body=body)
+
+ def test_update_flavor_db_duplicate(self):
+ def fake_instance_type_extra_specs_update_or_create(*args, **kwargs):
+ raise exception.FlavorExtraSpecUpdateCreateFailed(id=1, retries=5)
+
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ fake_instance_type_extra_specs_update_or_create)
+ body = {"key1": "value1"}
+
+ req = self._get_request('1/os-extra_specs/key1',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPConflict, self.controller.update,
+ req, 1, 'key1', body=body)
+
+ def test_update_really_long_integer_value(self):
+ value = 10 ** 1000
+ self.stubs.Set(nova.db,
+ 'flavor_extra_specs_update_or_create',
+ return_create_flavor_extra_specs)
+
+ req = self._get_request('1/os-extra_specs/key1',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 1, 'key1', body={"key1": value})
+
+
+class FlavorsExtraSpecsTestV2(FlavorsExtraSpecsTestV21):
+ bad_request = webob.exc.HTTPBadRequest
+ flavorextraspecs = flavorextraspecs_v2
+
+
+class FlavorsExtraSpecsXMLSerializerTest(test.TestCase):
+ def test_serializer(self):
+ serializer = flavorextraspecs_v2.ExtraSpecsTemplate()
+ expected = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<extra_specs><key1>value1</key1></extra_specs>')
+ text = serializer.serialize(dict(extra_specs={"key1": "value1"}))
+ self.assertEqual(text, expected)
+
+ def test_show_update_serializer(self):
+ serializer = flavorextraspecs_v2.ExtraSpecTemplate()
+ expected = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<extra_spec key="key1">value1</extra_spec>')
+ text = serializer.serialize(dict({"key1": "value1"}))
+ self.assertEqual(text, expected)
+
+ def test_serializer_with_colon_tagname(self):
+ # Our test object to serialize
+ obj = {'extra_specs': {'foo:bar': '999'}}
+ serializer = flavorextraspecs_v2.ExtraSpecsTemplate()
+ expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<extra_specs><foo:bar xmlns:foo="foo">999</foo:bar>'
+ '</extra_specs>'))
+ result = serializer.serialize(obj)
+ self.assertEqual(expected_xml, result)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_dns.py b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_dns.py
new file mode 100644
index 0000000000..9a68e0de60
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_dns.py
@@ -0,0 +1,412 @@
+# Copyright 2011 Andrew Bogott for the Wikimedia Foundation
+# All Rights Reserved.
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import urllib
+
+from lxml import etree
+import webob
+
+from nova.api.openstack.compute.contrib import floating_ip_dns as fipdns_v2
+from nova.api.openstack.compute.plugins.v3 import floating_ip_dns as \
+ fipdns_v21
+from nova import context
+from nova import db
+from nova import exception
+from nova import network
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+name = "arbitraryname"
+name2 = "anotherarbitraryname"
+
+test_ipv4_address = '10.0.0.66'
+test_ipv4_address2 = '10.0.0.67'
+
+test_ipv6_address = 'fe80:0:0:0:0:0:a00:42'
+
+domain = "example.org"
+domain2 = "example.net"
+floating_ip_id = '1'
+
+
+def _quote_domain(domain):
+ """Domain names tend to have .'s in them. Urllib doesn't quote dots,
+ but Routes tends to choke on them, so we need an extra level of
+ by-hand quoting here. This function needs to duplicate the one in
+ python-novaclient/novaclient/v1_1/floating_ip_dns.py
+ """
+ return urllib.quote(domain.replace('.', '%2E'))
+
+
+def network_api_get_floating_ip(self, context, id):
+ return {'id': floating_ip_id, 'address': test_ipv4_address,
+ 'fixed_ip': None}
+
+
+def network_get_dns_domains(self, context):
+ return [{'domain': 'example.org', 'scope': 'public'},
+ {'domain': 'example.com', 'scope': 'public',
+ 'project': 'project1'},
+ {'domain': 'private.example.com', 'scope': 'private',
+ 'availability_zone': 'avzone'}]
+
+
+def network_get_dns_entries_by_address(self, context, address, domain):
+ return [name, name2]
+
+
+def network_get_dns_entries_by_name(self, context, address, domain):
+ return [test_ipv4_address]
+
+
+def network_add_dns_entry(self, context, address, name, dns_type, domain):
+ return {'dns_entry': {'ip': test_ipv4_address,
+ 'name': name,
+ 'type': dns_type,
+ 'domain': domain}}
+
+
+def network_modify_dns_entry(self, context, address, name, domain):
+ return {'dns_entry': {'name': name,
+ 'ip': address,
+ 'domain': domain}}
+
+
+def network_create_private_dns_domain(self, context, domain, avail_zone):
+ pass
+
+
+def network_create_public_dns_domain(self, context, domain, project):
+ pass
+
+
+class FloatingIpDNSTestV21(test.TestCase):
+ floating_ip_dns = fipdns_v21
+
+ def _create_floating_ip(self):
+ """Create a floating ip object."""
+ host = "fake_host"
+ db.floating_ip_create(self.context,
+ {'address': test_ipv4_address,
+ 'host': host})
+ db.floating_ip_create(self.context,
+ {'address': test_ipv6_address,
+ 'host': host})
+
+ def _delete_floating_ip(self):
+ db.floating_ip_destroy(self.context, test_ipv4_address)
+ db.floating_ip_destroy(self.context, test_ipv6_address)
+
+ def _check_status(self, expected_status, res, controller_methord):
+ self.assertEqual(expected_status, controller_methord.wsgi_code)
+
+ def _bad_request(self):
+ return webob.exc.HTTPBadRequest
+
+ def setUp(self):
+ super(FloatingIpDNSTestV21, self).setUp()
+ self.stubs.Set(network.api.API, "get_dns_domains",
+ network_get_dns_domains)
+ self.stubs.Set(network.api.API, "get_dns_entries_by_address",
+ network_get_dns_entries_by_address)
+ self.stubs.Set(network.api.API, "get_dns_entries_by_name",
+ network_get_dns_entries_by_name)
+ self.stubs.Set(network.api.API, "get_floating_ip",
+ network_api_get_floating_ip)
+ self.stubs.Set(network.api.API, "add_dns_entry",
+ network_add_dns_entry)
+ self.stubs.Set(network.api.API, "modify_dns_entry",
+ network_modify_dns_entry)
+ self.stubs.Set(network.api.API, "create_public_dns_domain",
+ network_create_public_dns_domain)
+ self.stubs.Set(network.api.API, "create_private_dns_domain",
+ network_create_private_dns_domain)
+
+ self.context = context.get_admin_context()
+
+ self._create_floating_ip()
+ temp = self.floating_ip_dns.FloatingIPDNSDomainController()
+ self.domain_controller = temp
+ self.entry_controller = self.floating_ip_dns.\
+ FloatingIPDNSEntryController()
+
+ def tearDown(self):
+ self._delete_floating_ip()
+ super(FloatingIpDNSTestV21, self).tearDown()
+
+ def test_dns_domains_list(self):
+ req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns')
+ res_dict = self.domain_controller.index(req)
+ entries = res_dict['domain_entries']
+ self.assertTrue(entries)
+ self.assertEqual(entries[0]['domain'], "example.org")
+ self.assertFalse(entries[0]['project'])
+ self.assertFalse(entries[0]['availability_zone'])
+ self.assertEqual(entries[1]['domain'], "example.com")
+ self.assertEqual(entries[1]['project'], "project1")
+ self.assertFalse(entries[1]['availability_zone'])
+ self.assertEqual(entries[2]['domain'], "private.example.com")
+ self.assertFalse(entries[2]['project'])
+ self.assertEqual(entries[2]['availability_zone'], "avzone")
+
+ def _test_get_dns_entries_by_address(self, address):
+
+ qparams = {'ip': address}
+ params = "?%s" % urllib.urlencode(qparams) if qparams else ""
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/123/os-floating-ip-dns/%s/entries/%s'
+ % (_quote_domain(domain), params))
+ entries = self.entry_controller.show(req, _quote_domain(domain),
+ address)
+ entries = entries.obj
+ self.assertEqual(len(entries['dns_entries']), 2)
+ self.assertEqual(entries['dns_entries'][0]['name'],
+ name)
+ self.assertEqual(entries['dns_entries'][1]['name'],
+ name2)
+ self.assertEqual(entries['dns_entries'][0]['domain'],
+ domain)
+
+ def test_get_dns_entries_by_ipv4_address(self):
+ self._test_get_dns_entries_by_address(test_ipv4_address)
+
+ def test_get_dns_entries_by_ipv6_address(self):
+ self._test_get_dns_entries_by_address(test_ipv6_address)
+
+ def test_get_dns_entries_by_name(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/123/os-floating-ip-dns/%s/entries/%s' %
+ (_quote_domain(domain), name))
+ entry = self.entry_controller.show(req, _quote_domain(domain), name)
+
+ self.assertEqual(entry['dns_entry']['ip'],
+ test_ipv4_address)
+ self.assertEqual(entry['dns_entry']['domain'],
+ domain)
+
+ def test_dns_entries_not_found(self):
+ def fake_get_dns_entries_by_name(self, context, address, domain):
+ raise webob.exc.HTTPNotFound()
+
+ self.stubs.Set(network.api.API, "get_dns_entries_by_name",
+ fake_get_dns_entries_by_name)
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/123/os-floating-ip-dns/%s/entries/%s' %
+ (_quote_domain(domain), 'nonexistent'))
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.entry_controller.show,
+ req, _quote_domain(domain), 'nonexistent')
+
+ def test_create_entry(self):
+ body = {'dns_entry':
+ {'ip': test_ipv4_address,
+ 'dns_type': 'A'}}
+ req = fakes.HTTPRequest.blank(
+ '/v2/123/os-floating-ip-dns/%s/entries/%s' %
+ (_quote_domain(domain), name))
+ entry = self.entry_controller.update(req, _quote_domain(domain),
+ name, body=body)
+ self.assertEqual(entry['dns_entry']['ip'], test_ipv4_address)
+
+ def test_create_domain(self):
+ req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' %
+ _quote_domain(domain))
+ body = {'domain_entry':
+ {'scope': 'private',
+ 'project': 'testproject'}}
+ self.assertRaises(self._bad_request(),
+ self.domain_controller.update,
+ req, _quote_domain(domain), body=body)
+
+ body = {'domain_entry':
+ {'scope': 'public',
+ 'availability_zone': 'zone1'}}
+ self.assertRaises(self._bad_request(),
+ self.domain_controller.update,
+ req, _quote_domain(domain), body=body)
+
+ body = {'domain_entry':
+ {'scope': 'public',
+ 'project': 'testproject'}}
+ entry = self.domain_controller.update(req, _quote_domain(domain),
+ body=body)
+ self.assertEqual(entry['domain_entry']['domain'], domain)
+ self.assertEqual(entry['domain_entry']['scope'], 'public')
+ self.assertEqual(entry['domain_entry']['project'], 'testproject')
+
+ body = {'domain_entry':
+ {'scope': 'private',
+ 'availability_zone': 'zone1'}}
+ entry = self.domain_controller.update(req, _quote_domain(domain),
+ body=body)
+ self.assertEqual(entry['domain_entry']['domain'], domain)
+ self.assertEqual(entry['domain_entry']['scope'], 'private')
+ self.assertEqual(entry['domain_entry']['availability_zone'], 'zone1')
+
+ def test_delete_entry(self):
+ calls = []
+
+ def network_delete_dns_entry(fakeself, context, name, domain):
+ calls.append((name, domain))
+
+ self.stubs.Set(network.api.API, "delete_dns_entry",
+ network_delete_dns_entry)
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/123/os-floating-ip-dns/%s/entries/%s' %
+ (_quote_domain(domain), name))
+ res = self.entry_controller.delete(req, _quote_domain(domain), name)
+
+ self._check_status(202, res, self.entry_controller.delete)
+ self.assertEqual([(name, domain)], calls)
+
+ def test_delete_entry_notfound(self):
+ def delete_dns_entry_notfound(fakeself, context, name, domain):
+ raise exception.NotFound
+
+ self.stubs.Set(network.api.API, "delete_dns_entry",
+ delete_dns_entry_notfound)
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/123/os-floating-ip-dns/%s/entries/%s' %
+ (_quote_domain(domain), name))
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.entry_controller.delete, req, _quote_domain(domain), name)
+
+ def test_delete_domain(self):
+ calls = []
+
+ def network_delete_dns_domain(fakeself, context, fqdomain):
+ calls.append(fqdomain)
+
+ self.stubs.Set(network.api.API, "delete_dns_domain",
+ network_delete_dns_domain)
+
+ req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' %
+ _quote_domain(domain))
+ res = self.domain_controller.delete(req, _quote_domain(domain))
+
+ self._check_status(202, res, self.domain_controller.delete)
+ self.assertEqual([domain], calls)
+
+ def test_delete_domain_notfound(self):
+ def delete_dns_domain_notfound(fakeself, context, fqdomain):
+ raise exception.NotFound
+
+ self.stubs.Set(network.api.API, "delete_dns_domain",
+ delete_dns_domain_notfound)
+
+ req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' %
+ _quote_domain(domain))
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.domain_controller.delete, req, _quote_domain(domain))
+
+ def test_modify(self):
+ body = {'dns_entry':
+ {'ip': test_ipv4_address2,
+ 'dns_type': 'A'}}
+ req = fakes.HTTPRequest.blank(
+ '/v2/123/os-floating-ip-dns/%s/entries/%s' % (domain, name))
+ entry = self.entry_controller.update(req, domain, name, body=body)
+
+ self.assertEqual(entry['dns_entry']['ip'], test_ipv4_address2)
+
+
+class FloatingIpDNSTestV2(FloatingIpDNSTestV21):
+ floating_ip_dns = fipdns_v2
+
+ def _check_status(self, expected_status, res, controller_methord):
+ self.assertEqual(expected_status, res.status_int)
+
+ def _bad_request(self):
+ return webob.exc.HTTPUnprocessableEntity
+
+
+class FloatingIpDNSSerializerTestV2(test.TestCase):
+ floating_ip_dns = fipdns_v2
+
+ def test_domains(self):
+ serializer = self.floating_ip_dns.DomainsTemplate()
+ text = serializer.serialize(dict(
+ domain_entries=[
+ dict(domain=domain, scope='public', project='testproject'),
+ dict(domain=domain2, scope='private',
+ availability_zone='avzone')]))
+
+ tree = etree.fromstring(text)
+ self.assertEqual('domain_entries', tree.tag)
+ self.assertEqual(2, len(tree))
+ self.assertEqual(domain, tree[0].get('domain'))
+ self.assertEqual(domain2, tree[1].get('domain'))
+ self.assertEqual('avzone', tree[1].get('availability_zone'))
+
+ def test_domain_serializer(self):
+ serializer = self.floating_ip_dns.DomainTemplate()
+ text = serializer.serialize(dict(
+ domain_entry=dict(domain=domain,
+ scope='public',
+ project='testproject')))
+
+ tree = etree.fromstring(text)
+ self.assertEqual('domain_entry', tree.tag)
+ self.assertEqual(domain, tree.get('domain'))
+ self.assertEqual('testproject', tree.get('project'))
+
+ def test_entries_serializer(self):
+ serializer = self.floating_ip_dns.FloatingIPDNSsTemplate()
+ text = serializer.serialize(dict(
+ dns_entries=[
+ dict(ip=test_ipv4_address,
+ type='A',
+ domain=domain,
+ name=name),
+ dict(ip=test_ipv4_address2,
+ type='C',
+ domain=domain,
+ name=name2)]))
+
+ tree = etree.fromstring(text)
+ self.assertEqual('dns_entries', tree.tag)
+ self.assertEqual(2, len(tree))
+ self.assertEqual('dns_entry', tree[0].tag)
+ self.assertEqual('dns_entry', tree[1].tag)
+ self.assertEqual(test_ipv4_address, tree[0].get('ip'))
+ self.assertEqual('A', tree[0].get('type'))
+ self.assertEqual(domain, tree[0].get('domain'))
+ self.assertEqual(name, tree[0].get('name'))
+ self.assertEqual(test_ipv4_address2, tree[1].get('ip'))
+ self.assertEqual('C', tree[1].get('type'))
+ self.assertEqual(domain, tree[1].get('domain'))
+ self.assertEqual(name2, tree[1].get('name'))
+
+ def test_entry_serializer(self):
+ serializer = self.floating_ip_dns.FloatingIPDNSTemplate()
+ text = serializer.serialize(dict(
+ dns_entry=dict(
+ ip=test_ipv4_address,
+ type='A',
+ domain=domain,
+ name=name)))
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('dns_entry', tree.tag)
+ self.assertEqual(test_ipv4_address, tree.get('ip'))
+ self.assertEqual(domain, tree.get('domain'))
+ self.assertEqual(name, tree.get('name'))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_pools.py b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_pools.py
new file mode 100644
index 0000000000..926e88c6ae
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_pools.py
@@ -0,0 +1,83 @@
+# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+
+from nova.api.openstack.compute.contrib import floating_ip_pools as fipp_v2
+from nova.api.openstack.compute.plugins.v3 import floating_ip_pools as\
+ fipp_v21
+from nova import context
+from nova import network
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+def fake_get_floating_ip_pools(self, context):
+ return ['nova', 'other']
+
+
+class FloatingIpPoolTestV21(test.NoDBTestCase):
+ floating_ip_pools = fipp_v21
+ url = '/v2/fake/os-floating-ip-pools'
+
+ def setUp(self):
+ super(FloatingIpPoolTestV21, self).setUp()
+ self.stubs.Set(network.api.API, "get_floating_ip_pools",
+ fake_get_floating_ip_pools)
+
+ self.context = context.RequestContext('fake', 'fake')
+ self.controller = self.floating_ip_pools.FloatingIPPoolsController()
+
+ def test_translate_floating_ip_pools_view(self):
+ pools = fake_get_floating_ip_pools(None, self.context)
+ view = self.floating_ip_pools._translate_floating_ip_pools_view(pools)
+ self.assertIn('floating_ip_pools', view)
+ self.assertEqual(view['floating_ip_pools'][0]['name'],
+ pools[0])
+ self.assertEqual(view['floating_ip_pools'][1]['name'],
+ pools[1])
+
+ def test_floating_ips_pools_list(self):
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.index(req)
+
+ pools = fake_get_floating_ip_pools(None, self.context)
+ response = {'floating_ip_pools': [{'name': name} for name in pools]}
+ self.assertEqual(res_dict, response)
+
+
+class FloatingIpPoolTestV2(FloatingIpPoolTestV21):
+ floating_ip_pools = fipp_v2
+
+
+class FloatingIpPoolSerializerTestV2(test.NoDBTestCase):
+ floating_ip_pools = fipp_v2
+
+ def test_index_serializer(self):
+ serializer = self.floating_ip_pools.FloatingIPPoolsTemplate()
+ text = serializer.serialize(dict(
+ floating_ip_pools=[
+ dict(name='nova'),
+ dict(name='other')
+ ]))
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('floating_ip_pools', tree.tag)
+ self.assertEqual(2, len(tree))
+ self.assertEqual('floating_ip_pool', tree[0].tag)
+ self.assertEqual('floating_ip_pool', tree[1].tag)
+ self.assertEqual('nova', tree[0].get('name'))
+ self.assertEqual('other', tree[1].get('name'))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips.py b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips.py
new file mode 100644
index 0000000000..b383d1dbc1
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips.py
@@ -0,0 +1,853 @@
+# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
+# Copyright 2011 Eldar Nugaev
+# 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 contextlib
+import uuid
+
+from lxml import etree
+import mock
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import floating_ips
+from nova.api.openstack import extensions
+from nova import compute
+from nova.compute import utils as compute_utils
+from nova import context
+from nova import db
+from nova import exception
+from nova import network
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_network
+
+
+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+
+
+def network_api_get_floating_ip(self, context, id):
+ return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova',
+ 'fixed_ip_id': None}
+
+
+def network_api_get_floating_ip_by_address(self, context, address):
+ return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova',
+ 'fixed_ip_id': 10}
+
+
+def network_api_get_floating_ips_by_project(self, context):
+ return [{'id': 1,
+ 'address': '10.10.10.10',
+ 'pool': 'nova',
+ 'fixed_ip': {'address': '10.0.0.1',
+ 'instance_uuid': FAKE_UUID,
+ 'instance': {'uuid': FAKE_UUID}}},
+ {'id': 2,
+ 'pool': 'nova', 'interface': 'eth0',
+ 'address': '10.10.10.11',
+ 'fixed_ip': None}]
+
+
+def compute_api_get(self, context, instance_id, expected_attrs=None,
+ want_objects=False):
+ return dict(uuid=FAKE_UUID, id=instance_id, instance_type_id=1, host='bob')
+
+
+def network_api_allocate(self, context):
+ return '10.10.10.10'
+
+
+def network_api_release(self, context, address):
+ pass
+
+
+def compute_api_associate(self, context, instance_id, address):
+ pass
+
+
+def network_api_associate(self, context, floating_address, fixed_address):
+ pass
+
+
+def network_api_disassociate(self, context, instance, floating_address):
+ pass
+
+
+def fake_instance_get(context, instance_id):
+ return {
+ "id": 1,
+ "uuid": uuid.uuid4(),
+ "name": 'fake',
+ "user_id": 'fakeuser',
+ "project_id": '123'}
+
+
+def stub_nw_info(stubs):
+ def get_nw_info_for_instance(instance):
+ return fake_network.fake_get_instance_nw_info(stubs)
+ return get_nw_info_for_instance
+
+
+def get_instance_by_floating_ip_addr(self, context, address):
+ return None
+
+
+class FloatingIpTestNeutron(test.NoDBTestCase):
+
+ def setUp(self):
+ super(FloatingIpTestNeutron, self).setUp()
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ self.controller = floating_ips.FloatingIPController()
+
+ def _get_fake_request(self):
+ return fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/1')
+
+ def test_floatingip_delete(self):
+ req = self._get_fake_request()
+ fip_val = {'address': '1.1.1.1', 'fixed_ip_id': '192.168.1.2'}
+ with contextlib.nested(
+ mock.patch.object(self.controller.network_api,
+ 'disassociate_floating_ip'),
+ mock.patch.object(self.controller.network_api,
+ 'disassociate_and_release_floating_ip'),
+ mock.patch.object(self.controller.network_api,
+ 'release_floating_ip'),
+ mock.patch.object(self.controller.network_api,
+ 'get_instance_id_by_floating_address',
+ return_value=None),
+ mock.patch.object(self.controller.network_api,
+ 'get_floating_ip',
+ return_value=fip_val)) as (
+ disoc_fip, dis_and_del, rel_fip, _, _):
+ self.controller.delete(req, 1)
+ self.assertFalse(disoc_fip.called)
+ self.assertFalse(rel_fip.called)
+ # Only disassociate_and_release_floating_ip is
+ # called if using neutron
+ self.assertTrue(dis_and_del.called)
+
+
+class FloatingIpTest(test.TestCase):
+ floating_ip = "10.10.10.10"
+ floating_ip_2 = "10.10.10.11"
+
+ def _create_floating_ips(self, floating_ips=None):
+ """Create a floating ip object."""
+ if floating_ips is None:
+ floating_ips = [self.floating_ip]
+ elif not isinstance(floating_ips, (list, tuple)):
+ floating_ips = [floating_ips]
+
+ def make_ip_dict(ip):
+ """Shortcut for creating floating ip dict."""
+ return
+
+ dict_ = {'pool': 'nova', 'host': 'fake_host'}
+ return db.floating_ip_bulk_create(
+ self.context, [dict(address=ip, **dict_) for ip in floating_ips],
+ )
+
+ def _delete_floating_ip(self):
+ db.floating_ip_destroy(self.context, self.floating_ip)
+
+ def _get_fake_fip_request(self, act=''):
+ return fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/%s' % act)
+
+ def _get_fake_server_request(self):
+ return fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+
+ def _get_fake_response(self, req, init_only):
+ return req.get_response(fakes.wsgi_app(init_only=(init_only,)))
+
+ def setUp(self):
+ super(FloatingIpTest, self).setUp()
+ self.stubs.Set(compute.api.API, "get",
+ compute_api_get)
+ self.stubs.Set(network.api.API, "get_floating_ip",
+ network_api_get_floating_ip)
+ self.stubs.Set(network.api.API, "get_floating_ip_by_address",
+ network_api_get_floating_ip_by_address)
+ self.stubs.Set(network.api.API, "get_floating_ips_by_project",
+ network_api_get_floating_ips_by_project)
+ self.stubs.Set(network.api.API, "release_floating_ip",
+ network_api_release)
+ self.stubs.Set(network.api.API, "disassociate_floating_ip",
+ network_api_disassociate)
+ self.stubs.Set(network.api.API, "get_instance_id_by_floating_address",
+ get_instance_by_floating_ip_addr)
+ self.stubs.Set(compute_utils, "get_nw_info_for_instance",
+ stub_nw_info(self.stubs))
+
+ fake_network.stub_out_nw_api_get_instance_nw_info(self.stubs)
+ self.stubs.Set(db, 'instance_get',
+ fake_instance_get)
+
+ self.context = context.get_admin_context()
+ self._create_floating_ips()
+
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = floating_ips.FloatingIPController()
+ self.manager = floating_ips.FloatingIPActionController(self.ext_mgr)
+
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Floating_ips'])
+
+ def tearDown(self):
+ self._delete_floating_ip()
+ super(FloatingIpTest, self).tearDown()
+
+ def test_floatingip_delete(self):
+ req = self._get_fake_fip_request('1')
+ fip_val = {'address': '1.1.1.1', 'fixed_ip_id': '192.168.1.2'}
+ with contextlib.nested(
+ mock.patch.object(self.controller.network_api,
+ 'disassociate_floating_ip'),
+ mock.patch.object(self.controller.network_api,
+ 'release_floating_ip'),
+ mock.patch.object(self.controller.network_api,
+ 'get_instance_id_by_floating_address',
+ return_value=None),
+ mock.patch.object(self.controller.network_api,
+ 'get_floating_ip',
+ return_value=fip_val)) as (
+ disoc_fip, rel_fip, _, _):
+ self.controller.delete(req, 1)
+ self.assertTrue(disoc_fip.called)
+ self.assertTrue(rel_fip.called)
+
+ def test_translate_floating_ip_view(self):
+ floating_ip_address = self.floating_ip
+ floating_ip = db.floating_ip_get_by_address(self.context,
+ floating_ip_address)
+ # NOTE(vish): network_get uses the id not the address
+ floating_ip = db.floating_ip_get(self.context, floating_ip['id'])
+ view = floating_ips._translate_floating_ip_view(floating_ip)
+ self.assertIn('floating_ip', view)
+ self.assertTrue(view['floating_ip']['id'])
+ self.assertEqual(view['floating_ip']['ip'], self.floating_ip)
+ self.assertIsNone(view['floating_ip']['fixed_ip'])
+ self.assertIsNone(view['floating_ip']['instance_id'])
+
+ def test_translate_floating_ip_view_dict(self):
+ floating_ip = {'id': 0, 'address': '10.0.0.10', 'pool': 'nova',
+ 'fixed_ip': None}
+ view = floating_ips._translate_floating_ip_view(floating_ip)
+ self.assertIn('floating_ip', view)
+
+ def test_floating_ips_list(self):
+ req = self._get_fake_fip_request()
+ res_dict = self.controller.index(req)
+
+ response = {'floating_ips': [{'instance_id': FAKE_UUID,
+ 'ip': '10.10.10.10',
+ 'pool': 'nova',
+ 'fixed_ip': '10.0.0.1',
+ 'id': 1},
+ {'instance_id': None,
+ 'ip': '10.10.10.11',
+ 'pool': 'nova',
+ 'fixed_ip': None,
+ 'id': 2}]}
+ self.assertEqual(res_dict, response)
+
+ def test_floating_ip_release_nonexisting(self):
+ def fake_get_floating_ip(*args, **kwargs):
+ raise exception.FloatingIpNotFound(id=id)
+
+ self.stubs.Set(network.api.API, "get_floating_ip",
+ fake_get_floating_ip)
+
+ req = self._get_fake_fip_request('9876')
+ req.method = 'DELETE'
+ res = self._get_fake_response(req, 'os-floating-ips')
+ self.assertEqual(res.status_int, 404)
+ expected_msg = ('{"itemNotFound": {"message": "Floating ip not found '
+ 'for id 9876", "code": 404}}')
+ self.assertEqual(res.body, expected_msg)
+
+ def test_floating_ip_release_race_cond(self):
+ def fake_get_floating_ip(*args, **kwargs):
+ return {'fixed_ip_id': 1, 'address': self.floating_ip}
+
+ def fake_get_instance_by_floating_ip_addr(*args, **kwargs):
+ return 'test-inst'
+
+ def fake_disassociate_floating_ip(*args, **kwargs):
+ raise exception.FloatingIpNotAssociated(args[3])
+
+ self.stubs.Set(network.api.API, "get_floating_ip",
+ fake_get_floating_ip)
+ self.stubs.Set(floating_ips, "get_instance_by_floating_ip_addr",
+ fake_get_instance_by_floating_ip_addr)
+ self.stubs.Set(floating_ips, "disassociate_floating_ip",
+ fake_disassociate_floating_ip)
+
+ req = self._get_fake_fip_request('1')
+ req.method = 'DELETE'
+ res = self._get_fake_response(req, 'os-floating-ips')
+ self.assertEqual(res.status_int, 202)
+
+ def test_floating_ip_show(self):
+ req = self._get_fake_fip_request('1')
+ res_dict = self.controller.show(req, 1)
+
+ self.assertEqual(res_dict['floating_ip']['id'], 1)
+ self.assertEqual(res_dict['floating_ip']['ip'], '10.10.10.10')
+ self.assertIsNone(res_dict['floating_ip']['instance_id'])
+
+ def test_floating_ip_show_not_found(self):
+ def fake_get_floating_ip(*args, **kwargs):
+ raise exception.FloatingIpNotFound(id='fake')
+
+ self.stubs.Set(network.api.API, "get_floating_ip",
+ fake_get_floating_ip)
+
+ req = self._get_fake_fip_request('9876')
+ res = self._get_fake_response(req, 'os-floating-ips')
+ self.assertEqual(res.status_int, 404)
+ expected_msg = ('{"itemNotFound": {"message": "Floating ip not found '
+ 'for id 9876", "code": 404}}')
+ self.assertEqual(res.body, expected_msg)
+
+ def test_show_associated_floating_ip(self):
+ def get_floating_ip(self, context, id):
+ return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova',
+ 'fixed_ip': {'address': '10.0.0.1',
+ 'instance_uuid': FAKE_UUID,
+ 'instance': {'uuid': FAKE_UUID}}}
+
+ self.stubs.Set(network.api.API, "get_floating_ip", get_floating_ip)
+
+ req = self._get_fake_fip_request('1')
+ res_dict = self.controller.show(req, 1)
+
+ self.assertEqual(res_dict['floating_ip']['id'], 1)
+ self.assertEqual(res_dict['floating_ip']['ip'], '10.10.10.10')
+ self.assertEqual(res_dict['floating_ip']['fixed_ip'], '10.0.0.1')
+ self.assertEqual(res_dict['floating_ip']['instance_id'], FAKE_UUID)
+
+ def test_recreation_of_floating_ip(self):
+ self._delete_floating_ip()
+ self._create_floating_ips()
+
+ def test_floating_ip_in_bulk_creation(self):
+ self._delete_floating_ip()
+
+ self._create_floating_ips([self.floating_ip, self.floating_ip_2])
+ all_ips = db.floating_ip_get_all(self.context)
+ ip_list = [ip['address'] for ip in all_ips]
+ self.assertIn(self.floating_ip, ip_list)
+ self.assertIn(self.floating_ip_2, ip_list)
+
+ def test_fail_floating_ip_in_bulk_creation(self):
+ self.assertRaises(exception.FloatingIpExists,
+ self._create_floating_ips,
+ [self.floating_ip, self.floating_ip_2])
+ all_ips = db.floating_ip_get_all(self.context)
+ ip_list = [ip['address'] for ip in all_ips]
+ self.assertIn(self.floating_ip, ip_list)
+ self.assertNotIn(self.floating_ip_2, ip_list)
+
+ def test_floating_ip_allocate_no_free_ips(self):
+ def fake_allocate(*args, **kwargs):
+ raise exception.NoMoreFloatingIps()
+
+ self.stubs.Set(network.api.API, "allocate_floating_ip", fake_allocate)
+
+ req = self._get_fake_fip_request()
+ ex = self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.create, req)
+
+ self.assertIn('No more floating ips', ex.explanation)
+
+ def test_floating_ip_allocate_no_free_ips_pool(self):
+ def fake_allocate(*args, **kwargs):
+ raise exception.NoMoreFloatingIps()
+
+ self.stubs.Set(network.api.API, "allocate_floating_ip", fake_allocate)
+
+ req = self._get_fake_fip_request()
+ ex = self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.create, req, {'pool': 'non_existent_pool'})
+
+ self.assertIn('No more floating ips in pool non_existent_pool',
+ ex.explanation)
+
+ @mock.patch('nova.network.api.API.allocate_floating_ip',
+ side_effect=exception.FloatingIpLimitExceeded())
+ def test_floating_ip_allocate_over_quota(self, allocate_mock):
+ req = self._get_fake_fip_request()
+ ex = self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.create, req)
+
+ self.assertIn('IP allocation over quota', ex.explanation)
+
+ @mock.patch('nova.network.api.API.allocate_floating_ip',
+ side_effect=exception.FloatingIpLimitExceeded())
+ def test_floating_ip_allocate_quota_exceed_in_pool(self, allocate_mock):
+ req = self._get_fake_fip_request()
+ ex = self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.create, req, {'pool': 'non_existent_pool'})
+
+ self.assertIn('IP allocation over quota in pool non_existent_pool.',
+ ex.explanation)
+
+ @mock.patch('nova.network.api.API.allocate_floating_ip',
+ side_effect=exception.FloatingIpPoolNotFound())
+ def test_floating_ip_create_with_unknown_pool(self, allocate_mock):
+ req = self._get_fake_fip_request()
+ ex = self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.create, req, {'pool': 'non_existent_pool'})
+
+ self.assertIn('Floating ip pool not found.', ex.explanation)
+
+ def test_floating_ip_allocate(self):
+ def fake1(*args, **kwargs):
+ pass
+
+ def fake2(*args, **kwargs):
+ return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova'}
+
+ self.stubs.Set(network.api.API, "allocate_floating_ip",
+ fake1)
+ self.stubs.Set(network.api.API, "get_floating_ip_by_address",
+ fake2)
+
+ req = self._get_fake_fip_request()
+ res_dict = self.controller.create(req)
+
+ ip = res_dict['floating_ip']
+
+ expected = {
+ "id": 1,
+ "instance_id": None,
+ "ip": "10.10.10.10",
+ "fixed_ip": None,
+ "pool": 'nova'}
+ self.assertEqual(ip, expected)
+
+ def test_floating_ip_release(self):
+ req = self._get_fake_fip_request('1')
+ self.controller.delete(req, 1)
+
+ def test_floating_ip_associate(self):
+ fixed_address = '192.168.1.100'
+
+ def fake_associate_floating_ip(*args, **kwargs):
+ self.assertEqual(fixed_address, kwargs['fixed_address'])
+
+ self.stubs.Set(network.api.API, "associate_floating_ip",
+ fake_associate_floating_ip)
+ body = dict(addFloatingIp=dict(address=self.floating_ip))
+
+ req = self._get_fake_server_request()
+ rsp = self.manager._add_floating_ip(req, 'test_inst', body)
+ self.assertEqual(202, rsp.status_int)
+
+ def test_floating_ip_associate_invalid_instance(self):
+
+ def fake_get(self, context, id, expected_attrs=None,
+ want_objects=False):
+ raise exception.InstanceNotFound(instance_id=id)
+
+ self.stubs.Set(compute.api.API, "get", fake_get)
+
+ body = dict(addFloatingIp=dict(address=self.floating_ip))
+
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._add_floating_ip, req, 'test_inst',
+ body)
+
+ def test_not_extended_floating_ip_associate_fixed(self):
+ # Check that fixed_address is ignored if os-extended-floating-ips
+ # is not loaded
+ fixed_address_requested = '192.168.1.101'
+ fixed_address_allocated = '192.168.1.100'
+
+ def fake_associate_floating_ip(*args, **kwargs):
+ self.assertEqual(fixed_address_allocated,
+ kwargs['fixed_address'])
+
+ self.stubs.Set(network.api.API, "associate_floating_ip",
+ fake_associate_floating_ip)
+ body = dict(addFloatingIp=dict(address=self.floating_ip,
+ fixed_address=fixed_address_requested))
+
+ req = self._get_fake_server_request()
+ rsp = self.manager._add_floating_ip(req, 'test_inst', body)
+ self.assertEqual(202, rsp.status_int)
+
+ def test_associate_not_allocated_floating_ip_to_instance(self):
+ def fake_associate_floating_ip(self, context, instance,
+ floating_address, fixed_address,
+ affect_auto_assigned=False):
+ raise exception.FloatingIpNotFoundForAddress(
+ address=floating_address)
+ self.stubs.Set(network.api.API, "associate_floating_ip",
+ fake_associate_floating_ip)
+ floating_ip = '10.10.10.11'
+ body = dict(addFloatingIp=dict(address=floating_ip))
+ req = self._get_fake_server_request()
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ resp = self._get_fake_response(req, 'servers')
+ res_dict = jsonutils.loads(resp.body)
+ self.assertEqual(resp.status_int, 404)
+ self.assertEqual(res_dict['itemNotFound']['message'],
+ "floating ip not found")
+
+ @mock.patch.object(network.api.API, 'associate_floating_ip',
+ side_effect=exception.Forbidden)
+ def test_associate_floating_ip_forbidden(self, associate_mock):
+ body = dict(addFloatingIp=dict(address='10.10.10.11'))
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.manager._add_floating_ip, req, 'test_inst',
+ body)
+
+ def test_associate_floating_ip_bad_address_key(self):
+ body = dict(addFloatingIp=dict(bad_address='10.10.10.11'))
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._add_floating_ip, req, 'test_inst',
+ body)
+
+ def test_associate_floating_ip_bad_addfloatingip_key(self):
+ body = dict(bad_addFloatingIp=dict(address='10.10.10.11'))
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._add_floating_ip, req, 'test_inst',
+ body)
+
+ def test_floating_ip_disassociate(self):
+ def get_instance_by_floating_ip_addr(self, context, address):
+ if address == '10.10.10.10':
+ return 'test_inst'
+
+ self.stubs.Set(network.api.API, "get_instance_id_by_floating_address",
+ get_instance_by_floating_ip_addr)
+
+ body = dict(removeFloatingIp=dict(address='10.10.10.10'))
+
+ req = self._get_fake_server_request()
+ rsp = self.manager._remove_floating_ip(req, 'test_inst', body)
+ self.assertEqual(202, rsp.status_int)
+
+ def test_floating_ip_disassociate_missing(self):
+ body = dict(removeFloatingIp=dict(address='10.10.10.10'))
+
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPUnprocessableEntity,
+ self.manager._remove_floating_ip,
+ req, 'test_inst', body)
+
+ def test_floating_ip_associate_non_existent_ip(self):
+ def fake_network_api_associate(self, context, instance,
+ floating_address=None,
+ fixed_address=None):
+ floating_ips = ["10.10.10.10", "10.10.10.11"]
+ if floating_address not in floating_ips:
+ raise exception.FloatingIpNotFoundForAddress(
+ address=floating_address)
+
+ self.stubs.Set(network.api.API, "associate_floating_ip",
+ fake_network_api_associate)
+
+ body = dict(addFloatingIp=dict(address='1.1.1.1'))
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._add_floating_ip,
+ req, 'test_inst', body)
+
+ def test_floating_ip_disassociate_non_existent_ip(self):
+ def network_api_get_floating_ip_by_address(self, context,
+ floating_address):
+ floating_ips = ["10.10.10.10", "10.10.10.11"]
+ if floating_address not in floating_ips:
+ raise exception.FloatingIpNotFoundForAddress(
+ address=floating_address)
+
+ self.stubs.Set(network.api.API, "get_floating_ip_by_address",
+ network_api_get_floating_ip_by_address)
+
+ body = dict(removeFloatingIp=dict(address='1.1.1.1'))
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._remove_floating_ip,
+ req, 'test_inst', body)
+
+ def test_floating_ip_disassociate_wrong_instance_uuid(self):
+ def get_instance_by_floating_ip_addr(self, context, address):
+ if address == '10.10.10.10':
+ return 'test_inst'
+
+ self.stubs.Set(network.api.API, "get_instance_id_by_floating_address",
+ get_instance_by_floating_ip_addr)
+
+ wrong_uuid = 'aaaaaaaa-ffff-ffff-ffff-aaaaaaaaaaaa'
+ body = dict(removeFloatingIp=dict(address='10.10.10.10'))
+
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPUnprocessableEntity,
+ self.manager._remove_floating_ip,
+ req, wrong_uuid, body)
+
+ def test_floating_ip_disassociate_wrong_instance_id(self):
+ def get_instance_by_floating_ip_addr(self, context, address):
+ if address == '10.10.10.10':
+ return 'wrong_inst'
+
+ self.stubs.Set(network.api.API, "get_instance_id_by_floating_address",
+ get_instance_by_floating_ip_addr)
+
+ body = dict(removeFloatingIp=dict(address='10.10.10.10'))
+
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPUnprocessableEntity,
+ self.manager._remove_floating_ip,
+ req, 'test_inst', body)
+
+ def test_floating_ip_disassociate_auto_assigned(self):
+ def fake_get_floating_ip_addr_auto_assigned(self, context, address):
+ return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova',
+ 'fixed_ip_id': 10, 'auto_assigned': 1}
+
+ def get_instance_by_floating_ip_addr(self, context, address):
+ if address == '10.10.10.10':
+ return 'test_inst'
+
+ def network_api_disassociate(self, context, instance,
+ floating_address):
+ raise exception.CannotDisassociateAutoAssignedFloatingIP()
+
+ self.stubs.Set(network.api.API, "get_floating_ip_by_address",
+ fake_get_floating_ip_addr_auto_assigned)
+ self.stubs.Set(network.api.API, "get_instance_id_by_floating_address",
+ get_instance_by_floating_ip_addr)
+ self.stubs.Set(network.api.API, "disassociate_floating_ip",
+ network_api_disassociate)
+ body = dict(removeFloatingIp=dict(address='10.10.10.10'))
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.manager._remove_floating_ip,
+ req, 'test_inst', body)
+
+ def test_floating_ip_disassociate_map_authorization_exc(self):
+ def fake_get_floating_ip_addr_auto_assigned(self, context, address):
+ return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova',
+ 'fixed_ip_id': 10, 'auto_assigned': 1}
+
+ def get_instance_by_floating_ip_addr(self, context, address):
+ if address == '10.10.10.10':
+ return 'test_inst'
+
+ def network_api_disassociate(self, context, instance, address):
+ raise exception.Forbidden()
+
+ self.stubs.Set(network.api.API, "get_floating_ip_by_address",
+ fake_get_floating_ip_addr_auto_assigned)
+ self.stubs.Set(network.api.API, "get_instance_id_by_floating_address",
+ get_instance_by_floating_ip_addr)
+ self.stubs.Set(network.api.API, "disassociate_floating_ip",
+ network_api_disassociate)
+ body = dict(removeFloatingIp=dict(address='10.10.10.10'))
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.manager._remove_floating_ip,
+ req, 'test_inst', body)
+
+# these are a few bad param tests
+
+ def test_bad_address_param_in_remove_floating_ip(self):
+ body = dict(removeFloatingIp=dict(badparam='11.0.0.1'))
+
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._remove_floating_ip, req, 'test_inst',
+ body)
+
+ def test_missing_dict_param_in_remove_floating_ip(self):
+ body = dict(removeFloatingIp='11.0.0.1')
+
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._remove_floating_ip, req, 'test_inst',
+ body)
+
+ def test_missing_dict_param_in_add_floating_ip(self):
+ body = dict(addFloatingIp='11.0.0.1')
+
+ req = self._get_fake_server_request()
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._add_floating_ip, req, 'test_inst',
+ body)
+
+
+class ExtendedFloatingIpTest(test.TestCase):
+ floating_ip = "10.10.10.10"
+ floating_ip_2 = "10.10.10.11"
+
+ def _create_floating_ips(self, floating_ips=None):
+ """Create a floating ip object."""
+ if floating_ips is None:
+ floating_ips = [self.floating_ip]
+ elif not isinstance(floating_ips, (list, tuple)):
+ floating_ips = [floating_ips]
+
+ def make_ip_dict(ip):
+ """Shortcut for creating floating ip dict."""
+ return
+
+ dict_ = {'pool': 'nova', 'host': 'fake_host'}
+ return db.floating_ip_bulk_create(
+ self.context, [dict(address=ip, **dict_) for ip in floating_ips],
+ )
+
+ def _delete_floating_ip(self):
+ db.floating_ip_destroy(self.context, self.floating_ip)
+
+ def _get_fake_request(self):
+ return fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+
+ def _get_fake_response(self, req, init_only):
+ return req.get_response(fakes.wsgi_app(init_only=(init_only,)))
+
+ def setUp(self):
+ super(ExtendedFloatingIpTest, self).setUp()
+ self.stubs.Set(compute.api.API, "get",
+ compute_api_get)
+ self.stubs.Set(network.api.API, "get_floating_ip",
+ network_api_get_floating_ip)
+ self.stubs.Set(network.api.API, "get_floating_ip_by_address",
+ network_api_get_floating_ip_by_address)
+ self.stubs.Set(network.api.API, "get_floating_ips_by_project",
+ network_api_get_floating_ips_by_project)
+ self.stubs.Set(network.api.API, "release_floating_ip",
+ network_api_release)
+ self.stubs.Set(network.api.API, "disassociate_floating_ip",
+ network_api_disassociate)
+ self.stubs.Set(network.api.API, "get_instance_id_by_floating_address",
+ get_instance_by_floating_ip_addr)
+ self.stubs.Set(compute_utils, "get_nw_info_for_instance",
+ stub_nw_info(self.stubs))
+
+ fake_network.stub_out_nw_api_get_instance_nw_info(self.stubs)
+ self.stubs.Set(db, 'instance_get',
+ fake_instance_get)
+
+ self.context = context.get_admin_context()
+ self._create_floating_ips()
+
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.ext_mgr.extensions['os-floating-ips'] = True
+ self.ext_mgr.extensions['os-extended-floating-ips'] = True
+ self.controller = floating_ips.FloatingIPController()
+ self.manager = floating_ips.FloatingIPActionController(self.ext_mgr)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Floating_ips', 'Extended_floating_ips'])
+
+ def tearDown(self):
+ self._delete_floating_ip()
+ super(ExtendedFloatingIpTest, self).tearDown()
+
+ def test_extended_floating_ip_associate_fixed(self):
+ fixed_address = '192.168.1.101'
+
+ def fake_associate_floating_ip(*args, **kwargs):
+ self.assertEqual(fixed_address, kwargs['fixed_address'])
+
+ self.stubs.Set(network.api.API, "associate_floating_ip",
+ fake_associate_floating_ip)
+ body = dict(addFloatingIp=dict(address=self.floating_ip,
+ fixed_address=fixed_address))
+
+ req = self._get_fake_request()
+ rsp = self.manager._add_floating_ip(req, 'test_inst', body)
+ self.assertEqual(202, rsp.status_int)
+
+ def test_extended_floating_ip_associate_fixed_not_allocated(self):
+ def fake_associate_floating_ip(*args, **kwargs):
+ pass
+
+ self.stubs.Set(network.api.API, "associate_floating_ip",
+ fake_associate_floating_ip)
+ body = dict(addFloatingIp=dict(address=self.floating_ip,
+ fixed_address='11.11.11.11'))
+
+ req = self._get_fake_request()
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ resp = self._get_fake_response(req, 'servers')
+ res_dict = jsonutils.loads(resp.body)
+ self.assertEqual(resp.status_int, 400)
+ self.assertEqual(res_dict['badRequest']['message'],
+ "Specified fixed address not assigned to instance")
+
+
+class FloatingIpSerializerTest(test.TestCase):
+ def test_default_serializer(self):
+ serializer = floating_ips.FloatingIPTemplate()
+ text = serializer.serialize(dict(
+ floating_ip=dict(
+ instance_id=1,
+ ip='10.10.10.10',
+ fixed_ip='10.0.0.1',
+ id=1)))
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('floating_ip', tree.tag)
+ self.assertEqual('1', tree.get('instance_id'))
+ self.assertEqual('10.10.10.10', tree.get('ip'))
+ self.assertEqual('10.0.0.1', tree.get('fixed_ip'))
+ self.assertEqual('1', tree.get('id'))
+
+ def test_index_serializer(self):
+ serializer = floating_ips.FloatingIPsTemplate()
+ text = serializer.serialize(dict(
+ floating_ips=[
+ dict(instance_id=1,
+ ip='10.10.10.10',
+ fixed_ip='10.0.0.1',
+ id=1),
+ dict(instance_id=None,
+ ip='10.10.10.11',
+ fixed_ip=None,
+ id=2)]))
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('floating_ips', tree.tag)
+ self.assertEqual(2, len(tree))
+ self.assertEqual('floating_ip', tree[0].tag)
+ self.assertEqual('floating_ip', tree[1].tag)
+ self.assertEqual('1', tree[0].get('instance_id'))
+ self.assertEqual('None', tree[1].get('instance_id'))
+ self.assertEqual('10.10.10.10', tree[0].get('ip'))
+ self.assertEqual('10.10.10.11', tree[1].get('ip'))
+ self.assertEqual('10.0.0.1', tree[0].get('fixed_ip'))
+ self.assertEqual('None', tree[1].get('fixed_ip'))
+ self.assertEqual('1', tree[0].get('id'))
+ self.assertEqual('2', tree[1].get('id'))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips_bulk.py b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips_bulk.py
new file mode 100644
index 0000000000..8c81d99ab0
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips_bulk.py
@@ -0,0 +1,139 @@
+# Copyright 2012 IBM Corp.
+#
+# 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 netaddr
+from oslo.config import cfg
+import webob
+
+from nova.api.openstack.compute.contrib import floating_ips_bulk as fipbulk_v2
+from nova.api.openstack.compute.plugins.v3 import floating_ips_bulk as\
+ fipbulk_v21
+from nova import context
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+CONF = cfg.CONF
+
+
+class FloatingIPBulkV21(test.TestCase):
+
+ floating_ips_bulk = fipbulk_v21
+ url = '/v2/fake/os-floating-ips-bulk'
+ delete_url = '/v2/fake/os-fixed-ips/delete'
+ bad_request = exception.ValidationError
+
+ def setUp(self):
+ super(FloatingIPBulkV21, self).setUp()
+
+ self.context = context.get_admin_context()
+ self.controller = self.floating_ips_bulk.FloatingIPBulkController()
+
+ def _setup_floating_ips(self, ip_range):
+ body = {'floating_ips_bulk_create': {'ip_range': ip_range}}
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.create(req, body=body)
+ response = {"floating_ips_bulk_create": {
+ 'ip_range': ip_range,
+ 'pool': CONF.default_floating_pool,
+ 'interface': CONF.public_interface}}
+ self.assertEqual(res_dict, response)
+
+ def test_create_ips(self):
+ ip_range = '192.168.1.0/24'
+ self._setup_floating_ips(ip_range)
+
+ def test_create_ips_pool(self):
+ ip_range = '10.0.1.0/20'
+ pool = 'a new pool'
+ body = {'floating_ips_bulk_create':
+ {'ip_range': ip_range,
+ 'pool': pool}}
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.create(req, body=body)
+ response = {"floating_ips_bulk_create": {
+ 'ip_range': ip_range,
+ 'pool': pool,
+ 'interface': CONF.public_interface}}
+ self.assertEqual(res_dict, response)
+
+ def test_list_ips(self):
+ ip_range = '192.168.1.1/28'
+ self._setup_floating_ips(ip_range)
+ req = fakes.HTTPRequest.blank(self.url, use_admin_context=True)
+ res_dict = self.controller.index(req)
+
+ ip_info = [{'address': str(ip_addr),
+ 'pool': CONF.default_floating_pool,
+ 'interface': CONF.public_interface,
+ 'project_id': None,
+ 'instance_uuid': None}
+ for ip_addr in netaddr.IPNetwork(ip_range).iter_hosts()]
+ response = {'floating_ip_info': ip_info}
+
+ self.assertEqual(res_dict, response)
+
+ def test_list_ip_by_host(self):
+ ip_range = '192.168.1.1/28'
+ self._setup_floating_ips(ip_range)
+ req = fakes.HTTPRequest.blank(self.url, use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, 'host')
+
+ def test_delete_ips(self):
+ ip_range = '192.168.1.0/20'
+ self._setup_floating_ips(ip_range)
+
+ body = {'ip_range': ip_range}
+ req = fakes.HTTPRequest.blank(self.delete_url)
+ res_dict = self.controller.update(req, "delete", body=body)
+
+ response = {"floating_ips_bulk_delete": ip_range}
+ self.assertEqual(res_dict, response)
+
+ # Check that the IPs are actually deleted
+ req = fakes.HTTPRequest.blank(self.url, use_admin_context=True)
+ res_dict = self.controller.index(req)
+ response = {'floating_ip_info': []}
+ self.assertEqual(res_dict, response)
+
+ def test_create_duplicate_fail(self):
+ ip_range = '192.168.1.0/20'
+ self._setup_floating_ips(ip_range)
+
+ ip_range = '192.168.1.0/28'
+ body = {'floating_ips_bulk_create': {'ip_range': ip_range}}
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, body=body)
+
+ def test_create_bad_cidr_fail(self):
+ # netaddr can't handle /32 or 31 cidrs
+ ip_range = '192.168.1.1/32'
+ body = {'floating_ips_bulk_create': {'ip_range': ip_range}}
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, body=body)
+
+ def test_create_invalid_cidr_fail(self):
+ ip_range = 'not a cidr'
+ body = {'floating_ips_bulk_create': {'ip_range': ip_range}}
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(self.bad_request, self.controller.create,
+ req, body=body)
+
+
+class FloatingIPBulkV2(FloatingIPBulkV21):
+ floating_ips_bulk = fipbulk_v2
+ bad_request = webob.exc.HTTPBadRequest
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_fping.py b/nova/tests/unit/api/openstack/compute/contrib/test_fping.py
new file mode 100644
index 0000000000..a6364d6ee7
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_fping.py
@@ -0,0 +1,106 @@
+# Copyright 2011 Grid Dynamics
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova.api.openstack.compute.contrib import fping
+from nova.api.openstack.compute.plugins.v3 import fping as fping_v21
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+import nova.utils
+
+
+FAKE_UUID = fakes.FAKE_UUID
+
+
+def execute(*cmd, **args):
+ return "".join(["%s is alive" % ip for ip in cmd[1:]])
+
+
+class FpingTestV21(test.TestCase):
+ controller_cls = fping_v21.FpingController
+
+ def setUp(self):
+ super(FpingTestV21, self).setUp()
+ self.flags(verbose=True, use_ipv6=False)
+ return_server = fakes.fake_instance_get()
+ return_servers = fakes.fake_instance_get_all_by_filters()
+ self.stubs.Set(nova.db, "instance_get_all_by_filters",
+ return_servers)
+ self.stubs.Set(nova.db, "instance_get_by_uuid",
+ return_server)
+ self.stubs.Set(nova.utils, "execute",
+ execute)
+ self.stubs.Set(self.controller_cls, "check_fping",
+ lambda self: None)
+ self.controller = self.controller_cls()
+
+ def _get_url(self):
+ return "/v3"
+
+ def test_fping_index(self):
+ req = fakes.HTTPRequest.blank(self._get_url() + "/os-fping")
+ res_dict = self.controller.index(req)
+ self.assertIn("servers", res_dict)
+ for srv in res_dict["servers"]:
+ for key in "project_id", "id", "alive":
+ self.assertIn(key, srv)
+
+ def test_fping_index_policy(self):
+ req = fakes.HTTPRequest.blank(self._get_url() +
+ "os-fping?all_tenants=1")
+ self.assertRaises(exception.Forbidden, self.controller.index, req)
+ req = fakes.HTTPRequest.blank(self._get_url() +
+ "/os-fping?all_tenants=1")
+ req.environ["nova.context"].is_admin = True
+ res_dict = self.controller.index(req)
+ self.assertIn("servers", res_dict)
+
+ def test_fping_index_include(self):
+ req = fakes.HTTPRequest.blank(self._get_url() + "/os-fping")
+ res_dict = self.controller.index(req)
+ ids = [srv["id"] for srv in res_dict["servers"]]
+ req = fakes.HTTPRequest.blank(self._get_url() +
+ "/os-fping?include=%s" % ids[0])
+ res_dict = self.controller.index(req)
+ self.assertEqual(len(res_dict["servers"]), 1)
+ self.assertEqual(res_dict["servers"][0]["id"], ids[0])
+
+ def test_fping_index_exclude(self):
+ req = fakes.HTTPRequest.blank(self._get_url() + "/os-fping")
+ res_dict = self.controller.index(req)
+ ids = [srv["id"] for srv in res_dict["servers"]]
+ req = fakes.HTTPRequest.blank(self._get_url() +
+ "/os-fping?exclude=%s" %
+ ",".join(ids[1:]))
+ res_dict = self.controller.index(req)
+ self.assertEqual(len(res_dict["servers"]), 1)
+ self.assertEqual(res_dict["servers"][0]["id"], ids[0])
+
+ def test_fping_show(self):
+ req = fakes.HTTPRequest.blank(self._get_url() +
+ "os-fping/%s" % FAKE_UUID)
+ res_dict = self.controller.show(req, FAKE_UUID)
+ self.assertIn("server", res_dict)
+ srv = res_dict["server"]
+ for key in "project_id", "id", "alive":
+ self.assertIn(key, srv)
+
+
+class FpingTestV2(FpingTestV21):
+ controller_cls = fping.FpingController
+
+ def _get_url(self):
+ return "/v2/1234"
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_hide_server_addresses.py b/nova/tests/unit/api/openstack/compute/contrib/test_hide_server_addresses.py
new file mode 100644
index 0000000000..217fd480f9
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_hide_server_addresses.py
@@ -0,0 +1,172 @@
+# Copyright 2012 OpenStack Foundation
+# 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 itertools
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack import wsgi
+from nova import compute
+from nova.compute import vm_states
+from nova import db
+from nova import exception
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+
+SENTINEL = object()
+
+
+def fake_compute_get(*args, **kwargs):
+ def _return_server(*_args, **_kwargs):
+ inst = fakes.stub_instance(*args, **kwargs)
+ return fake_instance.fake_instance_obj(_args[1], **inst)
+ return _return_server
+
+
+class HideServerAddressesTestV21(test.TestCase):
+ content_type = 'application/json'
+ base_url = '/v2/fake/servers'
+
+ def _setup_wsgi(self):
+ self.wsgi_app = fakes.wsgi_app_v21(
+ init_only=('servers', 'os-hide-server-addresses'))
+
+ def setUp(self):
+ super(HideServerAddressesTestV21, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ return_server = fakes.fake_instance_get()
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+ self._setup_wsgi()
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(self.wsgi_app)
+ return res
+
+ @staticmethod
+ def _get_server(body):
+ return jsonutils.loads(body).get('server')
+
+ @staticmethod
+ def _get_servers(body):
+ return jsonutils.loads(body).get('servers')
+
+ @staticmethod
+ def _get_addresses(server):
+ return server.get('addresses', SENTINEL)
+
+ def _check_addresses(self, addresses, exists):
+ self.assertTrue(addresses is not SENTINEL)
+ if exists:
+ self.assertTrue(addresses)
+ else:
+ self.assertFalse(addresses)
+
+ def test_show_hides_in_building(self):
+ instance_id = 1
+ uuid = fakes.get_fake_uuid(instance_id)
+ self.stubs.Set(compute.api.API, 'get',
+ fake_compute_get(instance_id, uuid=uuid,
+ vm_state=vm_states.BUILDING))
+ res = self._make_request(self.base_url + '/%s' % uuid)
+ self.assertEqual(res.status_int, 200)
+
+ server = self._get_server(res.body)
+ addresses = self._get_addresses(server)
+ self._check_addresses(addresses, exists=False)
+
+ def test_show(self):
+ instance_id = 1
+ uuid = fakes.get_fake_uuid(instance_id)
+ self.stubs.Set(compute.api.API, 'get',
+ fake_compute_get(instance_id, uuid=uuid,
+ vm_state=vm_states.ACTIVE))
+ res = self._make_request(self.base_url + '/%s' % uuid)
+ self.assertEqual(res.status_int, 200)
+
+ server = self._get_server(res.body)
+ addresses = self._get_addresses(server)
+ self._check_addresses(addresses, exists=True)
+
+ def test_detail_hides_building_server_addresses(self):
+ instance_0 = fakes.stub_instance(0, uuid=fakes.get_fake_uuid(0),
+ vm_state=vm_states.ACTIVE)
+ instance_1 = fakes.stub_instance(1, uuid=fakes.get_fake_uuid(1),
+ vm_state=vm_states.BUILDING)
+ instances = [instance_0, instance_1]
+
+ def get_all(*args, **kwargs):
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return instance_obj._make_instance_list(
+ args[1], objects.InstanceList(), instances, fields)
+
+ self.stubs.Set(compute.api.API, 'get_all', get_all)
+ res = self._make_request(self.base_url + '/detail')
+
+ self.assertEqual(res.status_int, 200)
+ servers = self._get_servers(res.body)
+
+ self.assertEqual(len(servers), len(instances))
+
+ for instance, server in itertools.izip(instances, servers):
+ addresses = self._get_addresses(server)
+ exists = (instance['vm_state'] == vm_states.ACTIVE)
+ self._check_addresses(addresses, exists=exists)
+
+ def test_no_instance_passthrough_404(self):
+
+ def fake_compute_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ res = self._make_request(self.base_url + '/' + fakes.get_fake_uuid())
+
+ self.assertEqual(res.status_int, 404)
+
+
+class HideServerAddressesTestV2(HideServerAddressesTestV21):
+
+ def _setup_wsgi(self):
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Hide_server_addresses'])
+ self.wsgi_app = fakes.wsgi_app(init_only=('servers',))
+
+
+class HideAddressesXmlTest(HideServerAddressesTestV2):
+ content_type = 'application/xml'
+
+ @staticmethod
+ def _get_server(body):
+ return etree.XML(body)
+
+ @staticmethod
+ def _get_servers(body):
+ return etree.XML(body).getchildren()
+
+ @staticmethod
+ def _get_addresses(server):
+ addresses = server.find('{%s}addresses' % wsgi.XMLNS_V11)
+ if addresses is None:
+ return SENTINEL
+ return addresses
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_hosts.py b/nova/tests/unit/api/openstack/compute/contrib/test_hosts.py
new file mode 100644
index 0000000000..5478a7dd33
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_hosts.py
@@ -0,0 +1,471 @@
+# Copyright (c) 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+import testtools
+import webob.exc
+
+from nova.api.openstack.compute.contrib import hosts as os_hosts_v2
+from nova.api.openstack.compute.plugins.v3 import hosts as os_hosts_v3
+from nova.compute import power_state
+from nova.compute import vm_states
+from nova import context as context_maker
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit import fake_hosts
+from nova.tests.unit import utils
+
+
+def stub_service_get_all(context, disabled=None):
+ return fake_hosts.SERVICES_LIST
+
+
+def stub_service_get_by_host_and_topic(context, host_name, topic):
+ for service in stub_service_get_all(context):
+ if service['host'] == host_name and service['topic'] == topic:
+ return service
+
+
+def stub_set_host_enabled(context, host_name, enabled):
+ """Simulates three possible behaviours for VM drivers or compute
+ drivers when enabling or disabling a host.
+
+ 'enabled' means new instances can go to this host
+ 'disabled' means they can't
+ """
+ results = {True: "enabled", False: "disabled"}
+ if host_name == "notimplemented":
+ # The vm driver for this host doesn't support this feature
+ raise NotImplementedError()
+ elif host_name == "dummydest":
+ # The host does not exist
+ raise exception.ComputeHostNotFound(host=host_name)
+ elif host_name == "host_c2":
+ # Simulate a failure
+ return results[not enabled]
+ else:
+ # Do the right thing
+ return results[enabled]
+
+
+def stub_set_host_maintenance(context, host_name, mode):
+ # We'll simulate success and failure by assuming
+ # that 'host_c1' always succeeds, and 'host_c2'
+ # always fails
+ results = {True: "on_maintenance", False: "off_maintenance"}
+ if host_name == "notimplemented":
+ # The vm driver for this host doesn't support this feature
+ raise NotImplementedError()
+ elif host_name == "dummydest":
+ # The host does not exist
+ raise exception.ComputeHostNotFound(host=host_name)
+ elif host_name == "host_c2":
+ # Simulate a failure
+ return results[not mode]
+ else:
+ # Do the right thing
+ return results[mode]
+
+
+def stub_host_power_action(context, host_name, action):
+ if host_name == "notimplemented":
+ raise NotImplementedError()
+ elif host_name == "dummydest":
+ # The host does not exist
+ raise exception.ComputeHostNotFound(host=host_name)
+ return action
+
+
+def _create_instance(**kwargs):
+ """Create a test instance."""
+ ctxt = context_maker.get_admin_context()
+ return db.instance_create(ctxt, _create_instance_dict(**kwargs))
+
+
+def _create_instance_dict(**kwargs):
+ """Create a dictionary for a test instance."""
+ inst = {}
+ inst['image_ref'] = 'cedef40a-ed67-4d10-800e-17455edce175'
+ inst['reservation_id'] = 'r-fakeres'
+ inst['user_id'] = kwargs.get('user_id', 'admin')
+ inst['project_id'] = kwargs.get('project_id', 'fake')
+ inst['instance_type_id'] = '1'
+ if 'host' in kwargs:
+ inst['host'] = kwargs.get('host')
+ inst['vcpus'] = kwargs.get('vcpus', 1)
+ inst['memory_mb'] = kwargs.get('memory_mb', 20)
+ inst['root_gb'] = kwargs.get('root_gb', 30)
+ inst['ephemeral_gb'] = kwargs.get('ephemeral_gb', 30)
+ inst['vm_state'] = kwargs.get('vm_state', vm_states.ACTIVE)
+ inst['power_state'] = kwargs.get('power_state', power_state.RUNNING)
+ inst['task_state'] = kwargs.get('task_state', None)
+ inst['availability_zone'] = kwargs.get('availability_zone', None)
+ inst['ami_launch_index'] = 0
+ inst['launched_on'] = kwargs.get('launched_on', 'dummy')
+ return inst
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context_maker.get_admin_context()}
+ GET = {}
+
+
+class FakeRequestWithNovaZone(object):
+ environ = {"nova.context": context_maker.get_admin_context()}
+ GET = {"zone": "nova"}
+
+
+class FakeRequestWithNovaService(object):
+ environ = {"nova.context": context_maker.get_admin_context()}
+ GET = {"service": "compute"}
+
+
+class FakeRequestWithInvalidNovaService(object):
+ environ = {"nova.context": context_maker.get_admin_context()}
+ GET = {"service": "invalid"}
+
+
+class HostTestCaseV21(test.TestCase):
+ """Test Case for hosts."""
+ validation_ex = exception.ValidationError
+ Controller = os_hosts_v3.HostController
+ policy_ex = exception.PolicyNotAuthorized
+
+ def _setup_stubs(self):
+ # Pretend we have fake_hosts.HOST_LIST in the DB
+ self.stubs.Set(db, 'service_get_all',
+ stub_service_get_all)
+ # Only hosts in our fake DB exist
+ self.stubs.Set(db, 'service_get_by_host_and_topic',
+ stub_service_get_by_host_and_topic)
+ # 'host_c1' always succeeds, and 'host_c2'
+ self.stubs.Set(self.hosts_api, 'set_host_enabled',
+ stub_set_host_enabled)
+ # 'host_c1' always succeeds, and 'host_c2'
+ self.stubs.Set(self.hosts_api, 'set_host_maintenance',
+ stub_set_host_maintenance)
+ self.stubs.Set(self.hosts_api, 'host_power_action',
+ stub_host_power_action)
+
+ def setUp(self):
+ super(HostTestCaseV21, self).setUp()
+ self.controller = self.Controller()
+ self.hosts_api = self.controller.api
+ self.req = FakeRequest()
+
+ self._setup_stubs()
+
+ def _test_host_update(self, host, key, val, expected_value):
+ body = {key: val}
+ result = self.controller.update(self.req, host, body=body)
+ self.assertEqual(result[key], expected_value)
+
+ def test_list_hosts(self):
+ """Verify that the compute hosts are returned."""
+ result = self.controller.index(self.req)
+ self.assertIn('hosts', result)
+ hosts = result['hosts']
+ self.assertEqual(fake_hosts.HOST_LIST, hosts)
+
+ def test_disable_host(self):
+ self._test_host_update('host_c1', 'status', 'disable', 'disabled')
+ self._test_host_update('host_c2', 'status', 'disable', 'enabled')
+
+ def test_enable_host(self):
+ self._test_host_update('host_c1', 'status', 'enable', 'enabled')
+ self._test_host_update('host_c2', 'status', 'enable', 'disabled')
+
+ def test_enable_maintenance(self):
+ self._test_host_update('host_c1', 'maintenance_mode',
+ 'enable', 'on_maintenance')
+
+ def test_disable_maintenance(self):
+ self._test_host_update('host_c1', 'maintenance_mode',
+ 'disable', 'off_maintenance')
+
+ def _test_host_update_notimpl(self, key, val):
+ def stub_service_get_all_notimpl(self, req):
+ return [{'host': 'notimplemented', 'topic': None,
+ 'availability_zone': None}]
+ self.stubs.Set(db, 'service_get_all',
+ stub_service_get_all_notimpl)
+ body = {key: val}
+ self.assertRaises(webob.exc.HTTPNotImplemented,
+ self.controller.update,
+ self.req, 'notimplemented', body=body)
+
+ def test_disable_host_notimpl(self):
+ self._test_host_update_notimpl('status', 'disable')
+
+ def test_enable_maintenance_notimpl(self):
+ self._test_host_update_notimpl('maintenance_mode', 'enable')
+
+ def test_host_startup(self):
+ result = self.controller.startup(self.req, "host_c1")
+ self.assertEqual(result["power_action"], "startup")
+
+ def test_host_shutdown(self):
+ result = self.controller.shutdown(self.req, "host_c1")
+ self.assertEqual(result["power_action"], "shutdown")
+
+ def test_host_reboot(self):
+ result = self.controller.reboot(self.req, "host_c1")
+ self.assertEqual(result["power_action"], "reboot")
+
+ def _test_host_power_action_notimpl(self, method):
+ self.assertRaises(webob.exc.HTTPNotImplemented,
+ method, self.req, "notimplemented")
+
+ def test_host_startup_notimpl(self):
+ self._test_host_power_action_notimpl(self.controller.startup)
+
+ def test_host_shutdown_notimpl(self):
+ self._test_host_power_action_notimpl(self.controller.shutdown)
+
+ def test_host_reboot_notimpl(self):
+ self._test_host_power_action_notimpl(self.controller.reboot)
+
+ def test_host_status_bad_host(self):
+ # A host given as an argument does not exist.
+ self.req.environ["nova.context"].is_admin = True
+ dest = 'dummydest'
+ with testtools.ExpectedException(webob.exc.HTTPNotFound,
+ ".*%s.*" % dest):
+ self.controller.update(self.req, dest, body={'status': 'enable'})
+
+ def test_host_maintenance_bad_host(self):
+ # A host given as an argument does not exist.
+ self.req.environ["nova.context"].is_admin = True
+ dest = 'dummydest'
+ with testtools.ExpectedException(webob.exc.HTTPNotFound,
+ ".*%s.*" % dest):
+ self.controller.update(self.req, dest,
+ body={'maintenance_mode': 'enable'})
+
+ def test_host_power_action_bad_host(self):
+ # A host given as an argument does not exist.
+ self.req.environ["nova.context"].is_admin = True
+ dest = 'dummydest'
+ with testtools.ExpectedException(webob.exc.HTTPNotFound,
+ ".*%s.*" % dest):
+ self.controller.reboot(self.req, dest)
+
+ def test_bad_status_value(self):
+ bad_body = {"status": "bad"}
+ self.assertRaises(self.validation_ex, self.controller.update,
+ self.req, "host_c1", body=bad_body)
+ bad_body2 = {"status": "disablabc"}
+ self.assertRaises(self.validation_ex, self.controller.update,
+ self.req, "host_c1", body=bad_body2)
+
+ def test_bad_update_key(self):
+ bad_body = {"crazy": "bad"}
+ self.assertRaises(self.validation_ex, self.controller.update,
+ self.req, "host_c1", body=bad_body)
+
+ def test_bad_update_key_and_correct_update_key(self):
+ bad_body = {"status": "disable", "crazy": "bad"}
+ self.assertRaises(self.validation_ex, self.controller.update,
+ self.req, "host_c1", body=bad_body)
+
+ def test_good_update_keys(self):
+ body = {"status": "disable", "maintenance_mode": "enable"}
+ result = self.controller.update(self.req, 'host_c1', body=body)
+ self.assertEqual(result["host"], "host_c1")
+ self.assertEqual(result["status"], "disabled")
+ self.assertEqual(result["maintenance_mode"], "on_maintenance")
+
+ def test_show_forbidden(self):
+ self.req.environ["nova.context"].is_admin = False
+ dest = 'dummydest'
+ self.assertRaises(self.policy_ex,
+ self.controller.show,
+ self.req, dest)
+ self.req.environ["nova.context"].is_admin = True
+
+ def test_show_host_not_exist(self):
+ # A host given as an argument does not exist.
+ self.req.environ["nova.context"].is_admin = True
+ dest = 'dummydest'
+ with testtools.ExpectedException(webob.exc.HTTPNotFound,
+ ".*%s.*" % dest):
+ self.controller.show(self.req, dest)
+
+ def _create_compute_service(self):
+ """Create compute-manager(ComputeNode and Service record)."""
+ ctxt = self.req.environ["nova.context"]
+ dic = {'host': 'dummy', 'binary': 'nova-compute', 'topic': 'compute',
+ 'report_count': 0}
+ s_ref = db.service_create(ctxt, dic)
+
+ dic = {'service_id': s_ref['id'],
+ 'vcpus': 16, 'memory_mb': 32, 'local_gb': 100,
+ 'vcpus_used': 16, 'memory_mb_used': 32, 'local_gb_used': 10,
+ 'hypervisor_type': 'qemu', 'hypervisor_version': 12003,
+ 'cpu_info': '', 'stats': ''}
+ db.compute_node_create(ctxt, dic)
+
+ return db.service_get(ctxt, s_ref['id'])
+
+ def test_show_no_project(self):
+ """No instances are running on the given host."""
+ ctxt = context_maker.get_admin_context()
+ s_ref = self._create_compute_service()
+
+ result = self.controller.show(self.req, s_ref['host'])
+
+ proj = ['(total)', '(used_now)', '(used_max)']
+ column = ['host', 'project', 'cpu', 'memory_mb', 'disk_gb']
+ self.assertEqual(len(result['host']), 3)
+ for resource in result['host']:
+ self.assertIn(resource['resource']['project'], proj)
+ self.assertEqual(len(resource['resource']), 5)
+ self.assertEqual(set(column), set(resource['resource'].keys()))
+ db.service_destroy(ctxt, s_ref['id'])
+
+ def test_show_works_correctly(self):
+ """show() works correctly as expected."""
+ ctxt = context_maker.get_admin_context()
+ s_ref = self._create_compute_service()
+ i_ref1 = _create_instance(project_id='p-01', host=s_ref['host'])
+ i_ref2 = _create_instance(project_id='p-02', vcpus=3,
+ host=s_ref['host'])
+
+ result = self.controller.show(self.req, s_ref['host'])
+
+ proj = ['(total)', '(used_now)', '(used_max)', 'p-01', 'p-02']
+ column = ['host', 'project', 'cpu', 'memory_mb', 'disk_gb']
+ self.assertEqual(len(result['host']), 5)
+ for resource in result['host']:
+ self.assertIn(resource['resource']['project'], proj)
+ self.assertEqual(len(resource['resource']), 5)
+ self.assertEqual(set(column), set(resource['resource'].keys()))
+ db.service_destroy(ctxt, s_ref['id'])
+ db.instance_destroy(ctxt, i_ref1['uuid'])
+ db.instance_destroy(ctxt, i_ref2['uuid'])
+
+ def test_list_hosts_with_zone(self):
+ result = self.controller.index(FakeRequestWithNovaZone())
+ self.assertIn('hosts', result)
+ hosts = result['hosts']
+ self.assertEqual(fake_hosts.HOST_LIST_NOVA_ZONE, hosts)
+
+ def test_list_hosts_with_service(self):
+ result = self.controller.index(FakeRequestWithNovaService())
+ self.assertEqual(fake_hosts.HOST_LIST_NOVA_ZONE, result['hosts'])
+
+ def test_list_hosts_with_invalid_service(self):
+ result = self.controller.index(FakeRequestWithInvalidNovaService())
+ self.assertEqual([], result['hosts'])
+
+
+class HostTestCaseV20(HostTestCaseV21):
+ validation_ex = webob.exc.HTTPBadRequest
+ policy_ex = webob.exc.HTTPForbidden
+ Controller = os_hosts_v2.HostController
+
+ # Note: V2 api don't support list with services
+ def test_list_hosts_with_service(self):
+ pass
+
+ def test_list_hosts_with_invalid_service(self):
+ pass
+
+
+class HostSerializerTest(test.TestCase):
+ def setUp(self):
+ super(HostSerializerTest, self).setUp()
+ self.deserializer = os_hosts_v2.HostUpdateDeserializer()
+
+ def test_index_serializer(self):
+ serializer = os_hosts_v2.HostIndexTemplate()
+ text = serializer.serialize(fake_hosts.OS_API_HOST_LIST)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('hosts', tree.tag)
+ self.assertEqual(len(fake_hosts.HOST_LIST), len(tree))
+ for i in range(len(fake_hosts.HOST_LIST)):
+ self.assertEqual('host', tree[i].tag)
+ self.assertEqual(fake_hosts.HOST_LIST[i]['host_name'],
+ tree[i].get('host_name'))
+ self.assertEqual(fake_hosts.HOST_LIST[i]['service'],
+ tree[i].get('service'))
+ self.assertEqual(fake_hosts.HOST_LIST[i]['zone'],
+ tree[i].get('zone'))
+
+ def test_update_serializer_with_status(self):
+ exemplar = dict(host='host_c1', status='enabled')
+ serializer = os_hosts_v2.HostUpdateTemplate()
+ text = serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('host', tree.tag)
+ for key, value in exemplar.items():
+ self.assertEqual(value, tree.get(key))
+
+ def test_update_serializer_with_maintenance_mode(self):
+ exemplar = dict(host='host_c1', maintenance_mode='enabled')
+ serializer = os_hosts_v2.HostUpdateTemplate()
+ text = serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('host', tree.tag)
+ for key, value in exemplar.items():
+ self.assertEqual(value, tree.get(key))
+
+ def test_update_serializer_with_maintenance_mode_and_status(self):
+ exemplar = dict(host='host_c1',
+ maintenance_mode='enabled',
+ status='enabled')
+ serializer = os_hosts_v2.HostUpdateTemplate()
+ text = serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('host', tree.tag)
+ for key, value in exemplar.items():
+ self.assertEqual(value, tree.get(key))
+
+ def test_action_serializer(self):
+ exemplar = dict(host='host_c1', power_action='reboot')
+ serializer = os_hosts_v2.HostActionTemplate()
+ text = serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('host', tree.tag)
+ for key, value in exemplar.items():
+ self.assertEqual(value, tree.get(key))
+
+ def test_update_deserializer(self):
+ exemplar = dict(status='enabled', maintenance_mode='disable')
+ intext = """<?xml version='1.0' encoding='UTF-8'?>
+ <updates>
+ <status>enabled</status>
+ <maintenance_mode>disable</maintenance_mode>
+ </updates>"""
+ result = self.deserializer.deserialize(intext)
+
+ self.assertEqual(dict(body=exemplar), result)
+
+ def test_corrupt_xml(self):
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ self.deserializer.deserialize,
+ utils.killer_xml_body())
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_hypervisor_status.py b/nova/tests/unit/api/openstack/compute/contrib/test_hypervisor_status.py
new file mode 100644
index 0000000000..2d9187a7d1
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_hypervisor_status.py
@@ -0,0 +1,92 @@
+# Copyright 2014 Intel Corp.
+#
+# 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 copy
+
+import mock
+
+from nova.api.openstack.compute.contrib import hypervisors as hypervisors_v2
+from nova.api.openstack.compute.plugins.v3 import hypervisors \
+ as hypervisors_v21
+from nova.api.openstack import extensions
+from nova import test
+from nova.tests.unit.api.openstack.compute.contrib import test_hypervisors
+
+TEST_HYPER = dict(test_hypervisors.TEST_HYPERS[0],
+ service=dict(id=1,
+ host="compute1",
+ binary="nova-compute",
+ topic="compute_topic",
+ report_count=5,
+ disabled=False,
+ disabled_reason=None,
+ availability_zone="nova"),
+ )
+
+
+class HypervisorStatusTestV21(test.NoDBTestCase):
+ def _prepare_extension(self):
+ self.controller = hypervisors_v21.HypervisorsController()
+ self.controller.servicegroup_api.service_is_up = mock.MagicMock(
+ return_value=True)
+
+ def test_view_hypervisor_service_status(self):
+ self._prepare_extension()
+ result = self.controller._view_hypervisor(
+ TEST_HYPER, False)
+ self.assertEqual('enabled', result['status'])
+ self.assertEqual('up', result['state'])
+ self.assertEqual('enabled', result['status'])
+
+ self.controller.servicegroup_api.service_is_up.return_value = False
+ result = self.controller._view_hypervisor(
+ TEST_HYPER, False)
+ self.assertEqual('down', result['state'])
+
+ hyper = copy.deepcopy(TEST_HYPER)
+ hyper['service']['disabled'] = True
+ result = self.controller._view_hypervisor(hyper, False)
+ self.assertEqual('disabled', result['status'])
+
+ def test_view_hypervisor_detail_status(self):
+ self._prepare_extension()
+
+ result = self.controller._view_hypervisor(
+ TEST_HYPER, True)
+
+ self.assertEqual('enabled', result['status'])
+ self.assertEqual('up', result['state'])
+ self.assertIsNone(result['service']['disabled_reason'])
+
+ self.controller.servicegroup_api.service_is_up.return_value = False
+ result = self.controller._view_hypervisor(
+ TEST_HYPER, True)
+ self.assertEqual('down', result['state'])
+
+ hyper = copy.deepcopy(TEST_HYPER)
+ hyper['service']['disabled'] = True
+ hyper['service']['disabled_reason'] = "fake"
+ result = self.controller._view_hypervisor(hyper, True)
+ self.assertEqual('disabled', result['status'],)
+ self.assertEqual('fake', result['service']['disabled_reason'])
+
+
+class HypervisorStatusTestV2(HypervisorStatusTestV21):
+ def _prepare_extension(self):
+ ext_mgr = extensions.ExtensionManager()
+ ext_mgr.extensions = {}
+ ext_mgr.extensions['os-hypervisor-status'] = True
+ self.controller = hypervisors_v2.HypervisorsController(ext_mgr)
+ self.controller.servicegroup_api.service_is_up = mock.MagicMock(
+ return_value=True)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_hypervisors.py b/nova/tests/unit/api/openstack/compute/contrib/test_hypervisors.py
new file mode 100644
index 0000000000..9ae3c307c5
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_hypervisors.py
@@ -0,0 +1,596 @@
+# Copyright (c) 2012 OpenStack Foundation
+# 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 copy
+
+from lxml import etree
+import mock
+from webob import exc
+
+from nova.api.openstack.compute.contrib import hypervisors as hypervisors_v2
+from nova.api.openstack.compute.plugins.v3 import hypervisors \
+ as hypervisors_v21
+from nova.api.openstack import extensions
+from nova import context
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+TEST_HYPERS = [
+ dict(id=1,
+ service_id=1,
+ service=dict(id=1,
+ host="compute1",
+ binary="nova-compute",
+ topic="compute_topic",
+ report_count=5,
+ disabled=False,
+ disabled_reason=None,
+ availability_zone="nova"),
+ vcpus=4,
+ memory_mb=10 * 1024,
+ local_gb=250,
+ vcpus_used=2,
+ memory_mb_used=5 * 1024,
+ local_gb_used=125,
+ hypervisor_type="xen",
+ hypervisor_version=3,
+ hypervisor_hostname="hyper1",
+ free_ram_mb=5 * 1024,
+ free_disk_gb=125,
+ current_workload=2,
+ running_vms=2,
+ cpu_info='cpu_info',
+ disk_available_least=100,
+ host_ip='1.1.1.1'),
+ dict(id=2,
+ service_id=2,
+ service=dict(id=2,
+ host="compute2",
+ binary="nova-compute",
+ topic="compute_topic",
+ report_count=5,
+ disabled=False,
+ disabled_reason=None,
+ availability_zone="nova"),
+ vcpus=4,
+ memory_mb=10 * 1024,
+ local_gb=250,
+ vcpus_used=2,
+ memory_mb_used=5 * 1024,
+ local_gb_used=125,
+ hypervisor_type="xen",
+ hypervisor_version=3,
+ hypervisor_hostname="hyper2",
+ free_ram_mb=5 * 1024,
+ free_disk_gb=125,
+ current_workload=2,
+ running_vms=2,
+ cpu_info='cpu_info',
+ disk_available_least=100,
+ host_ip='2.2.2.2')]
+TEST_SERVERS = [dict(name="inst1", uuid="uuid1", host="compute1"),
+ dict(name="inst2", uuid="uuid2", host="compute2"),
+ dict(name="inst3", uuid="uuid3", host="compute1"),
+ dict(name="inst4", uuid="uuid4", host="compute2")]
+
+
+def fake_compute_node_get_all(context):
+ return TEST_HYPERS
+
+
+def fake_compute_node_search_by_hypervisor(context, hypervisor_re):
+ return TEST_HYPERS
+
+
+def fake_compute_node_get(context, compute_id):
+ for hyper in TEST_HYPERS:
+ if hyper['id'] == compute_id:
+ return hyper
+ raise exception.ComputeHostNotFound(host=compute_id)
+
+
+def fake_compute_node_statistics(context):
+ result = dict(
+ count=0,
+ vcpus=0,
+ memory_mb=0,
+ local_gb=0,
+ vcpus_used=0,
+ memory_mb_used=0,
+ local_gb_used=0,
+ free_ram_mb=0,
+ free_disk_gb=0,
+ current_workload=0,
+ running_vms=0,
+ disk_available_least=0,
+ )
+
+ for hyper in TEST_HYPERS:
+ for key in result:
+ if key == 'count':
+ result[key] += 1
+ else:
+ result[key] += hyper[key]
+
+ return result
+
+
+def fake_instance_get_all_by_host(context, host):
+ results = []
+ for inst in TEST_SERVERS:
+ if inst['host'] == host:
+ results.append(inst)
+ return results
+
+
+class HypervisorsTestV21(test.NoDBTestCase):
+ DETAIL_HYPERS_DICTS = copy.deepcopy(TEST_HYPERS)
+ del DETAIL_HYPERS_DICTS[0]['service_id']
+ del DETAIL_HYPERS_DICTS[1]['service_id']
+ DETAIL_HYPERS_DICTS[0].update({'state': 'up',
+ 'status': 'enabled',
+ 'service': dict(id=1, host='compute1',
+ disabled_reason=None)})
+ DETAIL_HYPERS_DICTS[1].update({'state': 'up',
+ 'status': 'enabled',
+ 'service': dict(id=2, host='compute2',
+ disabled_reason=None)})
+
+ INDEX_HYPER_DICTS = [
+ dict(id=1, hypervisor_hostname="hyper1",
+ state='up', status='enabled'),
+ dict(id=2, hypervisor_hostname="hyper2",
+ state='up', status='enabled')]
+
+ NO_SERVER_HYPER_DICTS = copy.deepcopy(INDEX_HYPER_DICTS)
+ NO_SERVER_HYPER_DICTS[0].update({'servers': []})
+ NO_SERVER_HYPER_DICTS[1].update({'servers': []})
+
+ def _get_request(self, use_admin_context):
+ return fakes.HTTPRequest.blank('/v2/fake/os-hypervisors/statistics',
+ use_admin_context=use_admin_context)
+
+ def _set_up_controller(self):
+ self.controller = hypervisors_v21.HypervisorsController()
+ self.controller.servicegroup_api.service_is_up = mock.MagicMock(
+ return_value=True)
+
+ def setUp(self):
+ super(HypervisorsTestV21, self).setUp()
+ self._set_up_controller()
+
+ self.stubs.Set(db, 'compute_node_get_all', fake_compute_node_get_all)
+ self.stubs.Set(db, 'compute_node_search_by_hypervisor',
+ fake_compute_node_search_by_hypervisor)
+ self.stubs.Set(db, 'compute_node_get',
+ fake_compute_node_get)
+ self.stubs.Set(db, 'compute_node_statistics',
+ fake_compute_node_statistics)
+ self.stubs.Set(db, 'instance_get_all_by_host',
+ fake_instance_get_all_by_host)
+
+ def test_view_hypervisor_nodetail_noservers(self):
+ result = self.controller._view_hypervisor(TEST_HYPERS[0], False)
+
+ self.assertEqual(result, self.INDEX_HYPER_DICTS[0])
+
+ def test_view_hypervisor_detail_noservers(self):
+ result = self.controller._view_hypervisor(TEST_HYPERS[0], True)
+
+ self.assertEqual(result, self.DETAIL_HYPERS_DICTS[0])
+
+ def test_view_hypervisor_servers(self):
+ result = self.controller._view_hypervisor(TEST_HYPERS[0], False,
+ TEST_SERVERS)
+ expected_dict = copy.deepcopy(self.INDEX_HYPER_DICTS[0])
+ expected_dict.update({'servers': [
+ dict(name="inst1", uuid="uuid1"),
+ dict(name="inst2", uuid="uuid2"),
+ dict(name="inst3", uuid="uuid3"),
+ dict(name="inst4", uuid="uuid4")]})
+
+ self.assertEqual(result, expected_dict)
+
+ def test_index(self):
+ req = self._get_request(True)
+ result = self.controller.index(req)
+
+ self.assertEqual(result, dict(hypervisors=self.INDEX_HYPER_DICTS))
+
+ def test_index_non_admin(self):
+ req = self._get_request(False)
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.index, req)
+
+ def test_detail(self):
+ req = self._get_request(True)
+ result = self.controller.detail(req)
+
+ self.assertEqual(result, dict(hypervisors=self.DETAIL_HYPERS_DICTS))
+
+ def test_detail_non_admin(self):
+ req = self._get_request(False)
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.detail, req)
+
+ def test_show_noid(self):
+ req = self._get_request(True)
+ self.assertRaises(exc.HTTPNotFound, self.controller.show, req, '3')
+
+ def test_show_non_integer_id(self):
+ req = self._get_request(True)
+ self.assertRaises(exc.HTTPNotFound, self.controller.show, req, 'abc')
+
+ def test_show_withid(self):
+ req = self._get_request(True)
+ result = self.controller.show(req, '1')
+
+ self.assertEqual(result, dict(hypervisor=self.DETAIL_HYPERS_DICTS[0]))
+
+ def test_show_non_admin(self):
+ req = self._get_request(False)
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.show, req, '1')
+
+ def test_uptime_noid(self):
+ req = self._get_request(True)
+ self.assertRaises(exc.HTTPNotFound, self.controller.uptime, req, '3')
+
+ def test_uptime_notimplemented(self):
+ def fake_get_host_uptime(context, hyp):
+ raise exc.HTTPNotImplemented()
+
+ self.stubs.Set(self.controller.host_api, 'get_host_uptime',
+ fake_get_host_uptime)
+
+ req = self._get_request(True)
+ self.assertRaises(exc.HTTPNotImplemented,
+ self.controller.uptime, req, '1')
+
+ def test_uptime_implemented(self):
+ def fake_get_host_uptime(context, hyp):
+ return "fake uptime"
+
+ self.stubs.Set(self.controller.host_api, 'get_host_uptime',
+ fake_get_host_uptime)
+
+ req = self._get_request(True)
+ result = self.controller.uptime(req, '1')
+
+ expected_dict = copy.deepcopy(self.INDEX_HYPER_DICTS[0])
+ expected_dict.update({'uptime': "fake uptime"})
+ self.assertEqual(result, dict(hypervisor=expected_dict))
+
+ def test_uptime_non_integer_id(self):
+ req = self._get_request(True)
+ self.assertRaises(exc.HTTPNotFound, self.controller.uptime, req, 'abc')
+
+ def test_uptime_non_admin(self):
+ req = self._get_request(False)
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.uptime, req, '1')
+
+ def test_search(self):
+ req = self._get_request(True)
+ result = self.controller.search(req, 'hyper')
+
+ self.assertEqual(result, dict(hypervisors=self.INDEX_HYPER_DICTS))
+
+ def test_search_non_admin(self):
+ req = self._get_request(False)
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.search, req, '1')
+
+ def test_search_non_exist(self):
+ def fake_compute_node_search_by_hypervisor_return_empty(context,
+ hypervisor_re):
+ return []
+ self.stubs.Set(db, 'compute_node_search_by_hypervisor',
+ fake_compute_node_search_by_hypervisor_return_empty)
+ req = self._get_request(True)
+ self.assertRaises(exc.HTTPNotFound, self.controller.search, req, 'a')
+
+ def test_servers(self):
+ req = self._get_request(True)
+ result = self.controller.servers(req, 'hyper')
+
+ expected_dict = copy.deepcopy(self.INDEX_HYPER_DICTS)
+ expected_dict[0].update({'servers': [
+ dict(name="inst1", uuid="uuid1"),
+ dict(name="inst3", uuid="uuid3")]})
+ expected_dict[1].update({'servers': [
+ dict(name="inst2", uuid="uuid2"),
+ dict(name="inst4", uuid="uuid4")]})
+
+ self.assertEqual(result, dict(hypervisors=expected_dict))
+
+ def test_servers_non_id(self):
+ def fake_compute_node_search_by_hypervisor_return_empty(context,
+ hypervisor_re):
+ return []
+ self.stubs.Set(db, 'compute_node_search_by_hypervisor',
+ fake_compute_node_search_by_hypervisor_return_empty)
+
+ req = self._get_request(True)
+ self.assertRaises(exc.HTTPNotFound,
+ self.controller.servers,
+ req, '115')
+
+ def test_servers_non_admin(self):
+ req = self._get_request(False)
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.servers, req, '1')
+
+ def test_servers_with_non_integer_hypervisor_id(self):
+ def fake_compute_node_search_by_hypervisor_return_empty(context,
+ hypervisor_re):
+ return []
+ self.stubs.Set(db, 'compute_node_search_by_hypervisor',
+ fake_compute_node_search_by_hypervisor_return_empty)
+
+ req = self._get_request(True)
+ self.assertRaises(exc.HTTPNotFound,
+ self.controller.servers, req, 'abc')
+
+ def test_servers_with_no_server(self):
+ def fake_instance_get_all_by_host_return_empty(context, hypervisor_re):
+ return []
+ self.stubs.Set(db, 'instance_get_all_by_host',
+ fake_instance_get_all_by_host_return_empty)
+ req = self._get_request(True)
+ result = self.controller.servers(req, '1')
+ self.assertEqual(result, dict(hypervisors=self.NO_SERVER_HYPER_DICTS))
+
+ def test_statistics(self):
+ req = self._get_request(True)
+ result = self.controller.statistics(req)
+
+ self.assertEqual(result, dict(hypervisor_statistics=dict(
+ count=2,
+ vcpus=8,
+ memory_mb=20 * 1024,
+ local_gb=500,
+ vcpus_used=4,
+ memory_mb_used=10 * 1024,
+ local_gb_used=250,
+ free_ram_mb=10 * 1024,
+ free_disk_gb=250,
+ current_workload=4,
+ running_vms=4,
+ disk_available_least=200)))
+
+ def test_statistics_non_admin(self):
+ req = self._get_request(False)
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.statistics, req)
+
+
+class HypervisorsTestV2(HypervisorsTestV21):
+ DETAIL_HYPERS_DICTS = copy.deepcopy(
+ HypervisorsTestV21.DETAIL_HYPERS_DICTS)
+ del DETAIL_HYPERS_DICTS[0]['state']
+ del DETAIL_HYPERS_DICTS[1]['state']
+ del DETAIL_HYPERS_DICTS[0]['status']
+ del DETAIL_HYPERS_DICTS[1]['status']
+ del DETAIL_HYPERS_DICTS[0]['service']['disabled_reason']
+ del DETAIL_HYPERS_DICTS[1]['service']['disabled_reason']
+ del DETAIL_HYPERS_DICTS[0]['host_ip']
+ del DETAIL_HYPERS_DICTS[1]['host_ip']
+
+ INDEX_HYPER_DICTS = copy.deepcopy(HypervisorsTestV21.INDEX_HYPER_DICTS)
+ del INDEX_HYPER_DICTS[0]['state']
+ del INDEX_HYPER_DICTS[1]['state']
+ del INDEX_HYPER_DICTS[0]['status']
+ del INDEX_HYPER_DICTS[1]['status']
+
+ NO_SERVER_HYPER_DICTS = copy.deepcopy(
+ HypervisorsTestV21.NO_SERVER_HYPER_DICTS)
+ del NO_SERVER_HYPER_DICTS[0]['state']
+ del NO_SERVER_HYPER_DICTS[1]['state']
+ del NO_SERVER_HYPER_DICTS[0]['status']
+ del NO_SERVER_HYPER_DICTS[1]['status']
+ del NO_SERVER_HYPER_DICTS[0]['servers']
+ del NO_SERVER_HYPER_DICTS[1]['servers']
+
+ def _set_up_controller(self):
+ self.context = context.get_admin_context()
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = hypervisors_v2.HypervisorsController(self.ext_mgr)
+
+
+class HypervisorsSerializersTest(test.NoDBTestCase):
+ def compare_to_exemplar(self, exemplar, hyper):
+ # Check attributes
+ for key, value in exemplar.items():
+ if key in ('service', 'servers'):
+ # These turn into child elements and get tested
+ # separately below...
+ continue
+
+ self.assertEqual(str(value), hyper.get(key))
+
+ # Check child elements
+ required_children = set([child for child in ('service', 'servers')
+ if child in exemplar])
+ for child in hyper:
+ self.assertIn(child.tag, required_children)
+ required_children.remove(child.tag)
+
+ # Check the node...
+ if child.tag == 'service':
+ for key, value in exemplar['service'].items():
+ self.assertEqual(str(value), child.get(key))
+ elif child.tag == 'servers':
+ for idx, grandchild in enumerate(child):
+ self.assertEqual('server', grandchild.tag)
+ for key, value in exemplar['servers'][idx].items():
+ self.assertEqual(str(value), grandchild.get(key))
+
+ # Are they all accounted for?
+ self.assertEqual(len(required_children), 0)
+
+ def test_index_serializer(self):
+ serializer = hypervisors_v2.HypervisorIndexTemplate()
+ exemplar = dict(hypervisors=[
+ dict(hypervisor_hostname="hyper1",
+ id=1),
+ dict(hypervisor_hostname="hyper2",
+ id=2)])
+ text = serializer.serialize(exemplar)
+ tree = etree.fromstring(text)
+
+ self.assertEqual('hypervisors', tree.tag)
+ self.assertEqual(len(exemplar['hypervisors']), len(tree))
+ for idx, hyper in enumerate(tree):
+ self.assertEqual('hypervisor', hyper.tag)
+ self.compare_to_exemplar(exemplar['hypervisors'][idx], hyper)
+
+ def test_detail_serializer(self):
+ serializer = hypervisors_v2.HypervisorDetailTemplate()
+ exemplar = dict(hypervisors=[
+ dict(hypervisor_hostname="hyper1",
+ id=1,
+ vcpus=4,
+ memory_mb=10 * 1024,
+ local_gb=500,
+ vcpus_used=2,
+ memory_mb_used=5 * 1024,
+ local_gb_used=250,
+ hypervisor_type='xen',
+ hypervisor_version=3,
+ free_ram_mb=5 * 1024,
+ free_disk_gb=250,
+ current_workload=2,
+ running_vms=2,
+ cpu_info="json data",
+ disk_available_least=100,
+ host_ip='1.1.1.1',
+ service=dict(id=1, host="compute1")),
+ dict(hypervisor_hostname="hyper2",
+ id=2,
+ vcpus=4,
+ memory_mb=10 * 1024,
+ local_gb=500,
+ vcpus_used=2,
+ memory_mb_used=5 * 1024,
+ local_gb_used=250,
+ hypervisor_type='xen',
+ hypervisor_version=3,
+ free_ram_mb=5 * 1024,
+ free_disk_gb=250,
+ current_workload=2,
+ running_vms=2,
+ cpu_info="json data",
+ disk_available_least=100,
+ host_ip='2.2.2.2',
+ service=dict(id=2, host="compute2"))])
+ text = serializer.serialize(exemplar)
+ tree = etree.fromstring(text)
+
+ self.assertEqual('hypervisors', tree.tag)
+ self.assertEqual(len(exemplar['hypervisors']), len(tree))
+ for idx, hyper in enumerate(tree):
+ self.assertEqual('hypervisor', hyper.tag)
+ self.compare_to_exemplar(exemplar['hypervisors'][idx], hyper)
+
+ def test_show_serializer(self):
+ serializer = hypervisors_v2.HypervisorTemplate()
+ exemplar = dict(hypervisor=dict(
+ hypervisor_hostname="hyper1",
+ id=1,
+ vcpus=4,
+ memory_mb=10 * 1024,
+ local_gb=500,
+ vcpus_used=2,
+ memory_mb_used=5 * 1024,
+ local_gb_used=250,
+ hypervisor_type='xen',
+ hypervisor_version=3,
+ free_ram_mb=5 * 1024,
+ free_disk_gb=250,
+ current_workload=2,
+ running_vms=2,
+ cpu_info="json data",
+ disk_available_least=100,
+ host_ip='1.1.1.1',
+ service=dict(id=1, host="compute1")))
+ text = serializer.serialize(exemplar)
+ tree = etree.fromstring(text)
+
+ self.assertEqual('hypervisor', tree.tag)
+ self.compare_to_exemplar(exemplar['hypervisor'], tree)
+
+ def test_uptime_serializer(self):
+ serializer = hypervisors_v2.HypervisorUptimeTemplate()
+ exemplar = dict(hypervisor=dict(
+ hypervisor_hostname="hyper1",
+ id=1,
+ uptime='fake uptime'))
+ text = serializer.serialize(exemplar)
+ tree = etree.fromstring(text)
+
+ self.assertEqual('hypervisor', tree.tag)
+ self.compare_to_exemplar(exemplar['hypervisor'], tree)
+
+ def test_servers_serializer(self):
+ serializer = hypervisors_v2.HypervisorServersTemplate()
+ exemplar = dict(hypervisors=[
+ dict(hypervisor_hostname="hyper1",
+ id=1,
+ servers=[
+ dict(name="inst1",
+ uuid="uuid1"),
+ dict(name="inst2",
+ uuid="uuid2")]),
+ dict(hypervisor_hostname="hyper2",
+ id=2,
+ servers=[
+ dict(name="inst3",
+ uuid="uuid3"),
+ dict(name="inst4",
+ uuid="uuid4")])])
+ text = serializer.serialize(exemplar)
+ tree = etree.fromstring(text)
+
+ self.assertEqual('hypervisors', tree.tag)
+ self.assertEqual(len(exemplar['hypervisors']), len(tree))
+ for idx, hyper in enumerate(tree):
+ self.assertEqual('hypervisor', hyper.tag)
+ self.compare_to_exemplar(exemplar['hypervisors'][idx], hyper)
+
+ def test_statistics_serializer(self):
+ serializer = hypervisors_v2.HypervisorStatisticsTemplate()
+ exemplar = dict(hypervisor_statistics=dict(
+ count=2,
+ vcpus=8,
+ memory_mb=20 * 1024,
+ local_gb=500,
+ vcpus_used=4,
+ memory_mb_used=10 * 1024,
+ local_gb_used=250,
+ free_ram_mb=10 * 1024,
+ free_disk_gb=250,
+ current_workload=4,
+ running_vms=4,
+ disk_available_least=200))
+ text = serializer.serialize(exemplar)
+ tree = etree.fromstring(text)
+
+ self.assertEqual('hypervisor_statistics', tree.tag)
+ self.compare_to_exemplar(exemplar['hypervisor_statistics'], tree)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_image_size.py b/nova/tests/unit/api/openstack/compute/contrib/test_image_size.py
new file mode 100644
index 0000000000..2a8d95cb86
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_image_size.py
@@ -0,0 +1,138 @@
+# Copyright 2013 Rackspace Hosting
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import image_size
+from nova.image import glance
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+NOW_API_FORMAT = "2010-10-11T10:30:22Z"
+IMAGES = [{
+ 'id': '123',
+ 'name': 'public image',
+ 'metadata': {'key1': 'value1'},
+ 'updated': NOW_API_FORMAT,
+ 'created': NOW_API_FORMAT,
+ 'status': 'ACTIVE',
+ 'progress': 100,
+ 'minDisk': 10,
+ 'minRam': 128,
+ 'size': 12345678,
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v2/fake/images/123",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/images/123",
+ }],
+ },
+ {
+ 'id': '124',
+ 'name': 'queued snapshot',
+ 'updated': NOW_API_FORMAT,
+ 'created': NOW_API_FORMAT,
+ 'status': 'SAVING',
+ 'progress': 25,
+ 'minDisk': 0,
+ 'minRam': 0,
+ 'size': 87654321,
+ "links": [{
+ "rel": "self",
+ "href": "http://localhost/v2/fake/images/124",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/images/124",
+ }],
+ }]
+
+
+def fake_show(*args, **kwargs):
+ return IMAGES[0]
+
+
+def fake_detail(*args, **kwargs):
+ return IMAGES
+
+
+class ImageSizeTestV21(test.NoDBTestCase):
+ content_type = 'application/json'
+ prefix = 'OS-EXT-IMG-SIZE'
+
+ def setUp(self):
+ super(ImageSizeTestV21, self).setUp()
+ self.stubs.Set(glance.GlanceImageService, 'show', fake_show)
+ self.stubs.Set(glance.GlanceImageService, 'detail', fake_detail)
+ self.flags(osapi_compute_extension=['nova.api.openstack.compute'
+ '.contrib.image_size.Image_size'])
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(self._get_app())
+ return res
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21()
+
+ def _get_image(self, body):
+ return jsonutils.loads(body).get('image')
+
+ def _get_images(self, body):
+ return jsonutils.loads(body).get('images')
+
+ def assertImageSize(self, image, size):
+ self.assertEqual(image.get('%s:size' % self.prefix), size)
+
+ def test_show(self):
+ url = '/v2/fake/images/1'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ image = self._get_image(res.body)
+ self.assertImageSize(image, 12345678)
+
+ def test_detail(self):
+ url = '/v2/fake/images/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ images = self._get_images(res.body)
+ self.assertImageSize(images[0], 12345678)
+ self.assertImageSize(images[1], 87654321)
+
+
+class ImageSizeTestV2(ImageSizeTestV21):
+ def _get_app(self):
+ return fakes.wsgi_app()
+
+
+class ImageSizeXmlTest(ImageSizeTestV2):
+ content_type = 'application/xml'
+ prefix = '{%s}' % image_size.Image_size.namespace
+
+ def _get_image(self, body):
+ return etree.XML(body)
+
+ def _get_images(self, body):
+ return etree.XML(body).getchildren()
+
+ def assertImageSize(self, image, size):
+ self.assertEqual(int(image.get('%ssize' % self.prefix)), size)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_instance_actions.py b/nova/tests/unit/api/openstack/compute/contrib/test_instance_actions.py
new file mode 100644
index 0000000000..a5ea3784e3
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_instance_actions.py
@@ -0,0 +1,327 @@
+# Copyright 2013 Rackspace Hosting
+# 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 copy
+import uuid
+
+from lxml import etree
+from webob import exc
+
+from nova.api.openstack.compute.contrib import instance_actions \
+ as instance_actions_v2
+from nova.api.openstack.compute.plugins.v3 import instance_actions \
+ as instance_actions_v21
+from nova.compute import api as compute_api
+from nova import db
+from nova.db.sqlalchemy import models
+from nova import exception
+from nova.openstack.common import policy as common_policy
+from nova import policy
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+from nova.tests.unit import fake_server_actions
+
+FAKE_UUID = fake_server_actions.FAKE_UUID
+FAKE_REQUEST_ID = fake_server_actions.FAKE_REQUEST_ID1
+
+
+def format_action(action):
+ '''Remove keys that aren't serialized.'''
+ to_delete = ('id', 'finish_time', 'created_at', 'updated_at', 'deleted_at',
+ 'deleted')
+ for key in to_delete:
+ if key in action:
+ del(action[key])
+ if 'start_time' in action:
+ # NOTE(danms): Without WSGI above us, these will be just stringified
+ action['start_time'] = str(action['start_time'].replace(tzinfo=None))
+ for event in action.get('events', []):
+ format_event(event)
+ return action
+
+
+def format_event(event):
+ '''Remove keys that aren't serialized.'''
+ to_delete = ('id', 'created_at', 'updated_at', 'deleted_at', 'deleted',
+ 'action_id')
+ for key in to_delete:
+ if key in event:
+ del(event[key])
+ if 'start_time' in event:
+ # NOTE(danms): Without WSGI above us, these will be just stringified
+ event['start_time'] = str(event['start_time'].replace(tzinfo=None))
+ if 'finish_time' in event:
+ # NOTE(danms): Without WSGI above us, these will be just stringified
+ event['finish_time'] = str(event['finish_time'].replace(tzinfo=None))
+ return event
+
+
+class InstanceActionsPolicyTestV21(test.NoDBTestCase):
+ instance_actions = instance_actions_v21
+
+ def setUp(self):
+ super(InstanceActionsPolicyTestV21, self).setUp()
+ self.controller = self.instance_actions.InstanceActionsController()
+
+ def _get_http_req(self, action):
+ fake_url = '/123/servers/12/%s' % action
+ return fakes.HTTPRequest.blank(fake_url)
+
+ def _set_policy_rules(self):
+ rules = {'compute:get': common_policy.parse_rule(''),
+ 'compute_extension:v3:os-instance-actions':
+ common_policy.parse_rule('project_id:%(project_id)s')}
+ policy.set_rules(rules)
+
+ def test_list_actions_restricted_by_project(self):
+ self._set_policy_rules()
+
+ def fake_instance_get_by_uuid(context, instance_id,
+ columns_to_join=None,
+ use_slave=False):
+ return fake_instance.fake_db_instance(
+ **{'name': 'fake', 'project_id': '%s_unequal' %
+ context.project_id})
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ req = self._get_http_req('os-instance-actions')
+ self.assertRaises(exception.Forbidden, self.controller.index, req,
+ str(uuid.uuid4()))
+
+ def test_get_action_restricted_by_project(self):
+ self._set_policy_rules()
+
+ def fake_instance_get_by_uuid(context, instance_id,
+ columns_to_join=None,
+ use_slave=False):
+ return fake_instance.fake_db_instance(
+ **{'name': 'fake', 'project_id': '%s_unequal' %
+ context.project_id})
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ req = self._get_http_req('os-instance-actions/1')
+ self.assertRaises(exception.Forbidden, self.controller.show, req,
+ str(uuid.uuid4()), '1')
+
+
+class InstanceActionsPolicyTestV2(InstanceActionsPolicyTestV21):
+ instance_actions = instance_actions_v2
+
+ def _set_policy_rules(self):
+ rules = {'compute:get': common_policy.parse_rule(''),
+ 'compute_extension:instance_actions':
+ common_policy.parse_rule('project_id:%(project_id)s')}
+ policy.set_rules(rules)
+
+
+class InstanceActionsTestV21(test.NoDBTestCase):
+ instance_actions = instance_actions_v21
+
+ def setUp(self):
+ super(InstanceActionsTestV21, self).setUp()
+ self.controller = self.instance_actions.InstanceActionsController()
+ self.fake_actions = copy.deepcopy(fake_server_actions.FAKE_ACTIONS)
+ self.fake_events = copy.deepcopy(fake_server_actions.FAKE_EVENTS)
+
+ def fake_get(self, context, instance_uuid, expected_attrs=None,
+ want_objects=False):
+ return {'uuid': instance_uuid}
+
+ def fake_instance_get_by_uuid(context, instance_id, use_slave=False):
+ return {'name': 'fake', 'project_id': context.project_id}
+
+ self.stubs.Set(compute_api.API, 'get', fake_get)
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+
+ def _get_http_req(self, action, use_admin_context=False):
+ fake_url = '/123/servers/12/%s' % action
+ return fakes.HTTPRequest.blank(fake_url,
+ use_admin_context=use_admin_context)
+
+ def _set_policy_rules(self):
+ rules = {'compute:get': common_policy.parse_rule(''),
+ 'compute_extension:v3:os-instance-actions':
+ common_policy.parse_rule(''),
+ 'compute_extension:v3:os-instance-actions:events':
+ common_policy.parse_rule('is_admin:True')}
+ policy.set_rules(rules)
+
+ def test_list_actions(self):
+ def fake_get_actions(context, uuid):
+ actions = []
+ for act in self.fake_actions[uuid].itervalues():
+ action = models.InstanceAction()
+ action.update(act)
+ actions.append(action)
+ return actions
+
+ self.stubs.Set(db, 'actions_get', fake_get_actions)
+ req = self._get_http_req('os-instance-actions')
+ res_dict = self.controller.index(req, FAKE_UUID)
+ for res in res_dict['instanceActions']:
+ fake_action = self.fake_actions[FAKE_UUID][res['request_id']]
+ self.assertEqual(format_action(fake_action), format_action(res))
+
+ def test_get_action_with_events_allowed(self):
+ def fake_get_action(context, uuid, request_id):
+ action = models.InstanceAction()
+ action.update(self.fake_actions[uuid][request_id])
+ return action
+
+ def fake_get_events(context, action_id):
+ events = []
+ for evt in self.fake_events[action_id]:
+ event = models.InstanceActionEvent()
+ event.update(evt)
+ events.append(event)
+ return events
+
+ self.stubs.Set(db, 'action_get_by_request_id', fake_get_action)
+ self.stubs.Set(db, 'action_events_get', fake_get_events)
+ req = self._get_http_req('os-instance-actions/1',
+ use_admin_context=True)
+ res_dict = self.controller.show(req, FAKE_UUID, FAKE_REQUEST_ID)
+ fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID]
+ fake_events = self.fake_events[fake_action['id']]
+ fake_action['events'] = fake_events
+ self.assertEqual(format_action(fake_action),
+ format_action(res_dict['instanceAction']))
+
+ def test_get_action_with_events_not_allowed(self):
+ def fake_get_action(context, uuid, request_id):
+ return self.fake_actions[uuid][request_id]
+
+ def fake_get_events(context, action_id):
+ return self.fake_events[action_id]
+
+ self.stubs.Set(db, 'action_get_by_request_id', fake_get_action)
+ self.stubs.Set(db, 'action_events_get', fake_get_events)
+
+ self._set_policy_rules()
+ req = self._get_http_req('os-instance-actions/1')
+ res_dict = self.controller.show(req, FAKE_UUID, FAKE_REQUEST_ID)
+ fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID]
+ self.assertEqual(format_action(fake_action),
+ format_action(res_dict['instanceAction']))
+
+ def test_action_not_found(self):
+ def fake_no_action(context, uuid, action_id):
+ return None
+
+ self.stubs.Set(db, 'action_get_by_request_id', fake_no_action)
+ req = self._get_http_req('os-instance-actions/1')
+ self.assertRaises(exc.HTTPNotFound, self.controller.show, req,
+ FAKE_UUID, FAKE_REQUEST_ID)
+
+ def test_index_instance_not_found(self):
+ def fake_get(self, context, instance_uuid, expected_attrs=None,
+ want_objects=False):
+ raise exception.InstanceNotFound(instance_id=instance_uuid)
+ self.stubs.Set(compute_api.API, 'get', fake_get)
+ req = self._get_http_req('os-instance-actions')
+ self.assertRaises(exc.HTTPNotFound, self.controller.index, req,
+ FAKE_UUID)
+
+ def test_show_instance_not_found(self):
+ def fake_get(self, context, instance_uuid, expected_attrs=None,
+ want_objects=False):
+ raise exception.InstanceNotFound(instance_id=instance_uuid)
+ self.stubs.Set(compute_api.API, 'get', fake_get)
+ req = self._get_http_req('os-instance-actions/fake')
+ self.assertRaises(exc.HTTPNotFound, self.controller.show, req,
+ FAKE_UUID, 'fake')
+
+
+class InstanceActionsTestV2(InstanceActionsTestV21):
+ instance_actions = instance_actions_v2
+
+ def _set_policy_rules(self):
+ rules = {'compute:get': common_policy.parse_rule(''),
+ 'compute_extension:instance_actions':
+ common_policy.parse_rule(''),
+ 'compute_extension:instance_actions:events':
+ common_policy.parse_rule('is_admin:True')}
+ policy.set_rules(rules)
+
+
+class InstanceActionsSerializerTestV2(test.NoDBTestCase):
+ def setUp(self):
+ super(InstanceActionsSerializerTestV2, self).setUp()
+ self.fake_actions = copy.deepcopy(fake_server_actions.FAKE_ACTIONS)
+ self.fake_events = copy.deepcopy(fake_server_actions.FAKE_EVENTS)
+
+ def _verify_instance_action_attachment(self, attach, tree):
+ for key in attach.keys():
+ if key != 'events':
+ self.assertEqual(attach[key], tree.get(key),
+ '%s did not match' % key)
+
+ def _verify_instance_action_event_attachment(self, attach, tree):
+ for key in attach.keys():
+ self.assertEqual(attach[key], tree.get(key),
+ '%s did not match' % key)
+
+ def test_instance_action_serializer(self):
+ serializer = instance_actions_v2.InstanceActionTemplate()
+ action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID]
+ text = serializer.serialize({'instanceAction': action})
+ tree = etree.fromstring(text)
+
+ action = format_action(action)
+ self.assertEqual('instanceAction', tree.tag)
+ self._verify_instance_action_attachment(action, tree)
+ found_events = False
+ for child in tree:
+ if child.tag == 'events':
+ found_events = True
+ self.assertFalse(found_events)
+
+ def test_instance_action_events_serializer(self):
+ serializer = instance_actions_v2.InstanceActionTemplate()
+ action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID]
+ event = self.fake_events[action['id']][0]
+ action['events'] = [dict(event), dict(event)]
+ text = serializer.serialize({'instanceAction': action})
+ tree = etree.fromstring(text)
+
+ action = format_action(action)
+ self.assertEqual('instanceAction', tree.tag)
+ self._verify_instance_action_attachment(action, tree)
+
+ event = format_event(event)
+ found_events = False
+ for child in tree:
+ if child.tag == 'events':
+ found_events = True
+ for key in event:
+ self.assertEqual(event[key], child.get(key))
+ self.assertTrue(found_events)
+
+ def test_instance_actions_serializer(self):
+ serializer = instance_actions_v2.InstanceActionsTemplate()
+ action_list = self.fake_actions[FAKE_UUID].values()
+ text = serializer.serialize({'instanceActions': action_list})
+ tree = etree.fromstring(text)
+
+ action_list = [format_action(action) for action in action_list]
+ self.assertEqual('instanceActions', tree.tag)
+ self.assertEqual(len(action_list), len(tree))
+ for idx, child in enumerate(tree):
+ self.assertEqual('instanceAction', child.tag)
+ request_id = child.get('request_id')
+ self._verify_instance_action_attachment(
+ self.fake_actions[FAKE_UUID][request_id],
+ child)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_instance_usage_audit_log.py b/nova/tests/unit/api/openstack/compute/contrib/test_instance_usage_audit_log.py
new file mode 100644
index 0000000000..1ae85c8625
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_instance_usage_audit_log.py
@@ -0,0 +1,210 @@
+# Copyright (c) 2012 OpenStack Foundation
+# 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 datetime
+
+from oslo.utils import timeutils
+
+from nova.api.openstack.compute.contrib import instance_usage_audit_log as ial
+from nova import context
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.objects import test_service
+from nova import utils
+
+
+service_base = test_service.fake_service
+TEST_COMPUTE_SERVICES = [dict(service_base, host='foo', topic='compute'),
+ dict(service_base, host='bar', topic='compute'),
+ dict(service_base, host='baz', topic='compute'),
+ dict(service_base, host='plonk', topic='compute'),
+ dict(service_base, host='wibble', topic='bogus'),
+ ]
+
+
+begin1 = datetime.datetime(2012, 7, 4, 6, 0, 0)
+begin2 = end1 = datetime.datetime(2012, 7, 5, 6, 0, 0)
+begin3 = end2 = datetime.datetime(2012, 7, 6, 6, 0, 0)
+end3 = datetime.datetime(2012, 7, 7, 6, 0, 0)
+
+
+# test data
+
+
+TEST_LOGS1 = [
+ # all services done, no errors.
+ dict(host="plonk", period_beginning=begin1, period_ending=end1,
+ state="DONE", errors=0, task_items=23, message="test1"),
+ dict(host="baz", period_beginning=begin1, period_ending=end1,
+ state="DONE", errors=0, task_items=17, message="test2"),
+ dict(host="bar", period_beginning=begin1, period_ending=end1,
+ state="DONE", errors=0, task_items=10, message="test3"),
+ dict(host="foo", period_beginning=begin1, period_ending=end1,
+ state="DONE", errors=0, task_items=7, message="test4"),
+ ]
+
+
+TEST_LOGS2 = [
+ # some still running...
+ dict(host="plonk", period_beginning=begin2, period_ending=end2,
+ state="DONE", errors=0, task_items=23, message="test5"),
+ dict(host="baz", period_beginning=begin2, period_ending=end2,
+ state="DONE", errors=0, task_items=17, message="test6"),
+ dict(host="bar", period_beginning=begin2, period_ending=end2,
+ state="RUNNING", errors=0, task_items=10, message="test7"),
+ dict(host="foo", period_beginning=begin2, period_ending=end2,
+ state="DONE", errors=0, task_items=7, message="test8"),
+ ]
+
+
+TEST_LOGS3 = [
+ # some errors..
+ dict(host="plonk", period_beginning=begin3, period_ending=end3,
+ state="DONE", errors=0, task_items=23, message="test9"),
+ dict(host="baz", period_beginning=begin3, period_ending=end3,
+ state="DONE", errors=2, task_items=17, message="test10"),
+ dict(host="bar", period_beginning=begin3, period_ending=end3,
+ state="DONE", errors=0, task_items=10, message="test11"),
+ dict(host="foo", period_beginning=begin3, period_ending=end3,
+ state="DONE", errors=1, task_items=7, message="test12"),
+ ]
+
+
+def fake_task_log_get_all(context, task_name, begin, end,
+ host=None, state=None):
+ assert task_name == "instance_usage_audit"
+
+ if begin == begin1 and end == end1:
+ return TEST_LOGS1
+ if begin == begin2 and end == end2:
+ return TEST_LOGS2
+ if begin == begin3 and end == end3:
+ return TEST_LOGS3
+ raise AssertionError("Invalid date %s to %s" % (begin, end))
+
+
+def fake_last_completed_audit_period(unit=None, before=None):
+ audit_periods = [(begin3, end3),
+ (begin2, end2),
+ (begin1, end1)]
+ if before is not None:
+ for begin, end in audit_periods:
+ if before > end:
+ return begin, end
+ raise AssertionError("Invalid before date %s" % (before))
+ return begin1, end1
+
+
+class InstanceUsageAuditLogTest(test.NoDBTestCase):
+ def setUp(self):
+ super(InstanceUsageAuditLogTest, self).setUp()
+ self.context = context.get_admin_context()
+ timeutils.set_time_override(datetime.datetime(2012, 7, 5, 10, 0, 0))
+ self.controller = ial.InstanceUsageAuditLogController()
+ self.host_api = self.controller.host_api
+
+ def fake_service_get_all(context, disabled):
+ self.assertIsNone(disabled)
+ return TEST_COMPUTE_SERVICES
+
+ self.stubs.Set(utils, 'last_completed_audit_period',
+ fake_last_completed_audit_period)
+ self.stubs.Set(db, 'service_get_all',
+ fake_service_get_all)
+ self.stubs.Set(db, 'task_log_get_all',
+ fake_task_log_get_all)
+
+ def tearDown(self):
+ super(InstanceUsageAuditLogTest, self).tearDown()
+ timeutils.clear_time_override()
+
+ def test_index(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-instance_usage_audit_log',
+ use_admin_context=True)
+ result = self.controller.index(req)
+ self.assertIn('instance_usage_audit_logs', result)
+ logs = result['instance_usage_audit_logs']
+ self.assertEqual(57, logs['total_instances'])
+ self.assertEqual(0, logs['total_errors'])
+ self.assertEqual(4, len(logs['log']))
+ self.assertEqual(4, logs['num_hosts'])
+ self.assertEqual(4, logs['num_hosts_done'])
+ self.assertEqual(0, logs['num_hosts_running'])
+ self.assertEqual(0, logs['num_hosts_not_run'])
+ self.assertEqual("ALL hosts done. 0 errors.", logs['overall_status'])
+
+ def test_index_non_admin(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-instance_usage_audit_log',
+ use_admin_context=False)
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.index, req)
+
+ def test_show(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-instance_usage_audit_log/show',
+ use_admin_context=True)
+ result = self.controller.show(req, '2012-07-05 10:00:00')
+ self.assertIn('instance_usage_audit_log', result)
+ logs = result['instance_usage_audit_log']
+ self.assertEqual(57, logs['total_instances'])
+ self.assertEqual(0, logs['total_errors'])
+ self.assertEqual(4, len(logs['log']))
+ self.assertEqual(4, logs['num_hosts'])
+ self.assertEqual(4, logs['num_hosts_done'])
+ self.assertEqual(0, logs['num_hosts_running'])
+ self.assertEqual(0, logs['num_hosts_not_run'])
+ self.assertEqual("ALL hosts done. 0 errors.", logs['overall_status'])
+
+ def test_show_non_admin(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-instance_usage_audit_log',
+ use_admin_context=False)
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.show, req, '2012-07-05 10:00:00')
+
+ def test_show_with_running(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-instance_usage_audit_log/show',
+ use_admin_context=True)
+ result = self.controller.show(req, '2012-07-06 10:00:00')
+ self.assertIn('instance_usage_audit_log', result)
+ logs = result['instance_usage_audit_log']
+ self.assertEqual(57, logs['total_instances'])
+ self.assertEqual(0, logs['total_errors'])
+ self.assertEqual(4, len(logs['log']))
+ self.assertEqual(4, logs['num_hosts'])
+ self.assertEqual(3, logs['num_hosts_done'])
+ self.assertEqual(1, logs['num_hosts_running'])
+ self.assertEqual(0, logs['num_hosts_not_run'])
+ self.assertEqual("3 of 4 hosts done. 0 errors.",
+ logs['overall_status'])
+
+ def test_show_with_errors(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-instance_usage_audit_log/show',
+ use_admin_context=True)
+ result = self.controller.show(req, '2012-07-07 10:00:00')
+ self.assertIn('instance_usage_audit_log', result)
+ logs = result['instance_usage_audit_log']
+ self.assertEqual(57, logs['total_instances'])
+ self.assertEqual(3, logs['total_errors'])
+ self.assertEqual(4, len(logs['log']))
+ self.assertEqual(4, logs['num_hosts'])
+ self.assertEqual(4, logs['num_hosts_done'])
+ self.assertEqual(0, logs['num_hosts_running'])
+ self.assertEqual(0, logs['num_hosts_not_run'])
+ self.assertEqual("ALL hosts done. 3 errors.",
+ logs['overall_status'])
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py b/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py
new file mode 100644
index 0000000000..6a6c6f0736
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py
@@ -0,0 +1,497 @@
+# Copyright 2011 Eldar Nugaev
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import keypairs as keypairs_v2
+from nova.api.openstack.compute.plugins.v3 import keypairs as keypairs_v21
+from nova.api.openstack import wsgi
+from nova import db
+from nova import exception
+from nova.openstack.common import policy as common_policy
+from nova import policy
+from nova import quota
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.objects import test_keypair
+
+
+QUOTAS = quota.QUOTAS
+
+
+keypair_data = {
+ 'public_key': 'FAKE_KEY',
+ 'fingerprint': 'FAKE_FINGERPRINT',
+}
+
+
+def fake_keypair(name):
+ return dict(test_keypair.fake_keypair,
+ name=name, **keypair_data)
+
+
+def db_key_pair_get_all_by_user(self, user_id):
+ return [fake_keypair('FAKE')]
+
+
+def db_key_pair_create(self, keypair):
+ return fake_keypair(name=keypair['name'])
+
+
+def db_key_pair_destroy(context, user_id, name):
+ if not (user_id and name):
+ raise Exception()
+
+
+def db_key_pair_create_duplicate(context, keypair):
+ raise exception.KeyPairExists(key_name=keypair.get('name', ''))
+
+
+class KeypairsTestV21(test.TestCase):
+ base_url = '/v2/fake'
+
+ def _setup_app(self):
+ self.app = fakes.wsgi_app_v21(init_only=('os-keypairs', 'servers'))
+ self.app_server = self.app
+
+ def setUp(self):
+ super(KeypairsTestV21, self).setUp()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+
+ self.stubs.Set(db, "key_pair_get_all_by_user",
+ db_key_pair_get_all_by_user)
+ self.stubs.Set(db, "key_pair_create",
+ db_key_pair_create)
+ self.stubs.Set(db, "key_pair_destroy",
+ db_key_pair_destroy)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Keypairs'])
+ self._setup_app()
+
+ def test_keypair_list(self):
+ req = webob.Request.blank(self.base_url + '/os-keypairs')
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 200)
+ res_dict = jsonutils.loads(res.body)
+ response = {'keypairs': [{'keypair': dict(keypair_data, name='FAKE')}]}
+ self.assertEqual(res_dict, response)
+
+ def test_keypair_create(self):
+ body = {'keypair': {'name': 'create_test'}}
+ req = webob.Request.blank(self.base_url + '/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 200)
+ res_dict = jsonutils.loads(res.body)
+ self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
+ self.assertTrue(len(res_dict['keypair']['private_key']) > 0)
+
+ def _test_keypair_create_bad_request_case(self, body):
+ req = webob.Request.blank(self.base_url + '/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 400)
+
+ def test_keypair_create_with_empty_name(self):
+ body = {'keypair': {'name': ''}}
+ self._test_keypair_create_bad_request_case(body)
+
+ def test_keypair_create_with_name_too_long(self):
+ body = {
+ 'keypair': {
+ 'name': 'a' * 256
+ }
+ }
+ self._test_keypair_create_bad_request_case(body)
+
+ def test_keypair_create_with_non_alphanumeric_name(self):
+ body = {
+ 'keypair': {
+ 'name': 'test/keypair'
+ }
+ }
+ self._test_keypair_create_bad_request_case(body)
+
+ def test_keypair_import_bad_key(self):
+ body = {
+ 'keypair': {
+ 'name': 'create_test',
+ 'public_key': 'ssh-what negative',
+ },
+ }
+ self._test_keypair_create_bad_request_case(body)
+
+ def test_keypair_create_with_invalid_keypair_body(self):
+ body = {'alpha': {'name': 'create_test'}}
+ self._test_keypair_create_bad_request_case(body)
+
+ def test_keypair_import(self):
+ body = {
+ 'keypair': {
+ 'name': 'create_test',
+ 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA'
+ 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4'
+ 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y'
+ 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi'
+ 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj'
+ 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1'
+ 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc'
+ 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB'
+ 'bHkXa6OciiJDvkRzJXzf',
+ },
+ }
+
+ req = webob.Request.blank(self.base_url + '/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 200)
+ # FIXME(ja): sholud we check that public_key was sent to create?
+ res_dict = jsonutils.loads(res.body)
+ self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
+ self.assertNotIn('private_key', res_dict['keypair'])
+
+ def test_keypair_import_quota_limit(self):
+
+ def fake_quotas_count(self, context, resource, *args, **kwargs):
+ return 100
+
+ self.stubs.Set(QUOTAS, "count", fake_quotas_count)
+
+ body = {
+ 'keypair': {
+ 'name': 'create_test',
+ 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA'
+ 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4'
+ 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y'
+ 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi'
+ 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj'
+ 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1'
+ 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc'
+ 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB'
+ 'bHkXa6OciiJDvkRzJXzf',
+ },
+ }
+
+ req = webob.Request.blank(self.base_url + '/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 403)
+ res_dict = jsonutils.loads(res.body)
+ self.assertEqual(
+ "Quota exceeded, too many key pairs.",
+ res_dict['forbidden']['message'])
+
+ def test_keypair_create_quota_limit(self):
+
+ def fake_quotas_count(self, context, resource, *args, **kwargs):
+ return 100
+
+ self.stubs.Set(QUOTAS, "count", fake_quotas_count)
+
+ body = {
+ 'keypair': {
+ 'name': 'create_test',
+ },
+ }
+
+ req = webob.Request.blank(self.base_url + '/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 403)
+ res_dict = jsonutils.loads(res.body)
+ self.assertEqual(
+ "Quota exceeded, too many key pairs.",
+ res_dict['forbidden']['message'])
+
+ def test_keypair_create_duplicate(self):
+ self.stubs.Set(db, "key_pair_create", db_key_pair_create_duplicate)
+ body = {'keypair': {'name': 'create_duplicate'}}
+ req = webob.Request.blank(self.base_url + '/os-keypairs')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 409)
+ res_dict = jsonutils.loads(res.body)
+ self.assertEqual(
+ "Key pair 'create_duplicate' already exists.",
+ res_dict['conflictingRequest']['message'])
+
+ def test_keypair_delete(self):
+ req = webob.Request.blank(self.base_url + '/os-keypairs/FAKE')
+ req.method = 'DELETE'
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 202)
+
+ def test_keypair_get_keypair_not_found(self):
+ req = webob.Request.blank(self.base_url + '/os-keypairs/DOESNOTEXIST')
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_keypair_delete_not_found(self):
+
+ def db_key_pair_get_not_found(context, user_id, name):
+ raise exception.KeypairNotFound(user_id=user_id, name=name)
+
+ self.stubs.Set(db, "key_pair_get",
+ db_key_pair_get_not_found)
+ req = webob.Request.blank(self.base_url + '/os-keypairs/WHAT')
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_keypair_show(self):
+
+ def _db_key_pair_get(context, user_id, name):
+ return dict(test_keypair.fake_keypair,
+ name='foo', public_key='XXX', fingerprint='YYY')
+
+ self.stubs.Set(db, "key_pair_get", _db_key_pair_get)
+
+ req = webob.Request.blank(self.base_url + '/os-keypairs/FAKE')
+ req.method = 'GET'
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ res_dict = jsonutils.loads(res.body)
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual('foo', res_dict['keypair']['name'])
+ self.assertEqual('XXX', res_dict['keypair']['public_key'])
+ self.assertEqual('YYY', res_dict['keypair']['fingerprint'])
+
+ def test_keypair_show_not_found(self):
+
+ def _db_key_pair_get(context, user_id, name):
+ raise exception.KeypairNotFound(user_id=user_id, name=name)
+
+ self.stubs.Set(db, "key_pair_get", _db_key_pair_get)
+
+ req = webob.Request.blank(self.base_url + '/os-keypairs/FAKE')
+ req.method = 'GET'
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(self.app)
+ self.assertEqual(res.status_int, 404)
+
+ def test_show_server(self):
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get())
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get())
+ req = webob.Request.blank(self.base_url + '/servers/1')
+ req.headers['Content-Type'] = 'application/json'
+ response = req.get_response(self.app_server)
+ self.assertEqual(response.status_int, 200)
+ res_dict = jsonutils.loads(response.body)
+ self.assertIn('key_name', res_dict['server'])
+ self.assertEqual(res_dict['server']['key_name'], '')
+
+ def test_detail_servers(self):
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fakes.fake_instance_get_all_by_filters())
+ req = fakes.HTTPRequest.blank(self.base_url + '/servers/detail')
+ res = req.get_response(self.app_server)
+ server_dicts = jsonutils.loads(res.body)['servers']
+ self.assertEqual(len(server_dicts), 5)
+
+ for server_dict in server_dicts:
+ self.assertIn('key_name', server_dict)
+ self.assertEqual(server_dict['key_name'], '')
+
+
+class KeypairPolicyTestV21(test.TestCase):
+ KeyPairController = keypairs_v21.KeypairController()
+ policy_path = 'compute_extension:v3:os-keypairs'
+ base_url = '/v2/fake'
+
+ def setUp(self):
+ super(KeypairPolicyTestV21, self).setUp()
+
+ def _db_key_pair_get(context, user_id, name):
+ return dict(test_keypair.fake_keypair,
+ name='foo', public_key='XXX', fingerprint='YYY')
+
+ self.stubs.Set(db, "key_pair_get",
+ _db_key_pair_get)
+ self.stubs.Set(db, "key_pair_get_all_by_user",
+ db_key_pair_get_all_by_user)
+ self.stubs.Set(db, "key_pair_create",
+ db_key_pair_create)
+ self.stubs.Set(db, "key_pair_destroy",
+ db_key_pair_destroy)
+
+ def test_keypair_list_fail_policy(self):
+ rules = {self.policy_path + ':index':
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs')
+ self.assertRaises(exception.Forbidden,
+ self.KeyPairController.index,
+ req)
+
+ def test_keypair_list_pass_policy(self):
+ rules = {self.policy_path + ':index':
+ common_policy.parse_rule('')}
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs')
+ res = self.KeyPairController.index(req)
+ self.assertIn('keypairs', res)
+
+ def test_keypair_show_fail_policy(self):
+ rules = {self.policy_path + ':show':
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs/FAKE')
+ self.assertRaises(exception.Forbidden,
+ self.KeyPairController.show,
+ req, 'FAKE')
+
+ def test_keypair_show_pass_policy(self):
+ rules = {self.policy_path + ':show':
+ common_policy.parse_rule('')}
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs/FAKE')
+ res = self.KeyPairController.show(req, 'FAKE')
+ self.assertIn('keypair', res)
+
+ def test_keypair_create_fail_policy(self):
+ body = {'keypair': {'name': 'create_test'}}
+ rules = {self.policy_path + ':create':
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs')
+ req.method = 'POST'
+ self.assertRaises(exception.Forbidden,
+ self.KeyPairController.create,
+ req, body=body)
+
+ def test_keypair_create_pass_policy(self):
+ body = {'keypair': {'name': 'create_test'}}
+ rules = {self.policy_path + ':create':
+ common_policy.parse_rule('')}
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs')
+ req.method = 'POST'
+ res = self.KeyPairController.create(req, body=body)
+ self.assertIn('keypair', res)
+
+ def test_keypair_delete_fail_policy(self):
+ rules = {self.policy_path + ':delete':
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs/FAKE')
+ req.method = 'DELETE'
+ self.assertRaises(exception.Forbidden,
+ self.KeyPairController.delete,
+ req, 'FAKE')
+
+ def test_keypair_delete_pass_policy(self):
+ rules = {self.policy_path + ':delete':
+ common_policy.parse_rule('')}
+ policy.set_rules(rules)
+ req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs/FAKE')
+ req.method = 'DELETE'
+ res = self.KeyPairController.delete(req, 'FAKE')
+
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.KeyPairController, keypairs_v21.KeypairController):
+ status_int = self.KeyPairController.delete.wsgi_code
+ else:
+ status_int = res.status_int
+ self.assertEqual(202, status_int)
+
+
+class KeypairsXMLSerializerTest(test.TestCase):
+ def setUp(self):
+ super(KeypairsXMLSerializerTest, self).setUp()
+ self.deserializer = wsgi.XMLDeserializer()
+
+ def test_default_serializer(self):
+ exemplar = dict(keypair=dict(
+ public_key='fake_public_key',
+ private_key='fake_private_key',
+ fingerprint='fake_fingerprint',
+ user_id='fake_user_id',
+ name='fake_key_name'))
+ serializer = keypairs_v2.KeypairTemplate()
+ text = serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('keypair', tree.tag)
+ for child in tree:
+ self.assertIn(child.tag, exemplar['keypair'])
+ self.assertEqual(child.text, exemplar['keypair'][child.tag])
+
+ def test_index_serializer(self):
+ exemplar = dict(keypairs=[
+ dict(keypair=dict(
+ name='key1_name',
+ public_key='key1_key',
+ fingerprint='key1_fingerprint')),
+ dict(keypair=dict(
+ name='key2_name',
+ public_key='key2_key',
+ fingerprint='key2_fingerprint'))])
+ serializer = keypairs_v2.KeypairsTemplate()
+ text = serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('keypairs', tree.tag)
+ self.assertEqual(len(exemplar['keypairs']), len(tree))
+ for idx, keypair in enumerate(tree):
+ self.assertEqual('keypair', keypair.tag)
+ kp_data = exemplar['keypairs'][idx]['keypair']
+ for child in keypair:
+ self.assertIn(child.tag, kp_data)
+ self.assertEqual(child.text, kp_data[child.tag])
+
+ def test_deserializer(self):
+ exemplar = dict(keypair=dict(
+ name='key_name',
+ public_key='public_key'))
+ intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<keypair><name>key_name</name>'
+ '<public_key>public_key</public_key></keypair>')
+
+ result = self.deserializer.deserialize(intext)['body']
+ self.assertEqual(result, exemplar)
+
+
+class KeypairsTestV2(KeypairsTestV21):
+
+ def _setup_app(self):
+ self.app = fakes.wsgi_app(init_only=('os-keypairs',))
+ self.app_server = fakes.wsgi_app(init_only=('servers',))
+
+
+class KeypairPolicyTestV2(KeypairPolicyTestV21):
+ KeyPairController = keypairs_v2.KeypairController()
+ policy_path = 'compute_extension:keypairs'
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_migrate_server.py b/nova/tests/unit/api/openstack/compute/contrib/test_migrate_server.py
new file mode 100644
index 0000000000..069b688837
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_migrate_server.py
@@ -0,0 +1,231 @@
+# Copyright 2011 OpenStack Foundation
+# Copyright 2013 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova.api.openstack.compute.plugins.v3 import migrate_server
+from nova import exception
+from nova.openstack.common import uuidutils
+from nova.tests.unit.api.openstack.compute.plugins.v3 import \
+ admin_only_action_common
+from nova.tests.unit.api.openstack import fakes
+
+
+class MigrateServerTests(admin_only_action_common.CommonTests):
+ def setUp(self):
+ super(MigrateServerTests, self).setUp()
+ self.controller = migrate_server.MigrateServerController()
+ self.compute_api = self.controller.compute_api
+
+ def _fake_controller(*args, **kwargs):
+ return self.controller
+
+ self.stubs.Set(migrate_server, 'MigrateServerController',
+ _fake_controller)
+ self.app = fakes.wsgi_app_v21(init_only=('servers',
+ 'os-migrate-server'),
+ fake_auth_context=self.context)
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_migrate(self):
+ method_translations = {'migrate': 'resize',
+ 'os-migrateLive': 'live_migrate'}
+ body_map = {'os-migrateLive': {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}}
+ args_map = {'os-migrateLive': ((False, False, 'hostname'), {})}
+ self._test_actions(['migrate', 'os-migrateLive'], body_map=body_map,
+ method_translations=method_translations,
+ args_map=args_map)
+
+ def test_migrate_none_hostname(self):
+ method_translations = {'migrate': 'resize',
+ 'os-migrateLive': 'live_migrate'}
+ body_map = {'os-migrateLive': {'host': None,
+ 'block_migration': False,
+ 'disk_over_commit': False}}
+ args_map = {'os-migrateLive': ((False, False, None), {})}
+ self._test_actions(['migrate', 'os-migrateLive'], body_map=body_map,
+ method_translations=method_translations,
+ args_map=args_map)
+
+ def test_migrate_with_non_existed_instance(self):
+ body_map = {'os-migrateLive': {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}}
+ self._test_actions_with_non_existed_instance(
+ ['migrate', 'os-migrateLive'], body_map=body_map)
+
+ def test_migrate_raise_conflict_on_invalid_state(self):
+ method_translations = {'migrate': 'resize',
+ 'os-migrateLive': 'live_migrate'}
+ body_map = {'os-migrateLive': {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}}
+ args_map = {'os-migrateLive': ((False, False, 'hostname'), {})}
+ self._test_actions_raise_conflict_on_invalid_state(
+ ['migrate', 'os-migrateLive'], body_map=body_map,
+ args_map=args_map, method_translations=method_translations)
+
+ def test_actions_with_locked_instance(self):
+ method_translations = {'migrate': 'resize',
+ 'os-migrateLive': 'live_migrate'}
+ body_map = {'os-migrateLive': {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}}
+ args_map = {'os-migrateLive': ((False, False, 'hostname'), {})}
+ self._test_actions_with_locked_instance(
+ ['migrate', 'os-migrateLive'], body_map=body_map,
+ args_map=args_map, method_translations=method_translations)
+
+ def _test_migrate_exception(self, exc_info, expected_result):
+ self.mox.StubOutWithMock(self.compute_api, 'resize')
+ instance = self._stub_instance_get()
+ self.compute_api.resize(self.context, instance).AndRaise(exc_info)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance['uuid'],
+ {'migrate': None})
+ self.assertEqual(expected_result, res.status_int)
+
+ def test_migrate_too_many_instances(self):
+ exc_info = exception.TooManyInstances(overs='', req='', used=0,
+ allowed=0, resource='')
+ self._test_migrate_exception(exc_info, 403)
+
+ def _test_migrate_live_succeeded(self, param):
+ self.mox.StubOutWithMock(self.compute_api, 'live_migrate')
+ instance = self._stub_instance_get()
+ self.compute_api.live_migrate(self.context, instance, False,
+ False, 'hostname')
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance.uuid,
+ {'os-migrateLive': param})
+ self.assertEqual(202, res.status_int)
+
+ def test_migrate_live_enabled(self):
+ param = {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}
+ self._test_migrate_live_succeeded(param)
+
+ def test_migrate_live_enabled_with_string_param(self):
+ param = {'host': 'hostname',
+ 'block_migration': "False",
+ 'disk_over_commit': "False"}
+ self._test_migrate_live_succeeded(param)
+
+ def test_migrate_live_without_host(self):
+ res = self._make_request('/servers/FAKE/action',
+ {'os-migrateLive':
+ {'block_migration': False,
+ 'disk_over_commit': False}})
+ self.assertEqual(400, res.status_int)
+
+ def test_migrate_live_without_block_migration(self):
+ res = self._make_request('/servers/FAKE/action',
+ {'os-migrateLive':
+ {'host': 'hostname',
+ 'disk_over_commit': False}})
+ self.assertEqual(400, res.status_int)
+
+ def test_migrate_live_without_disk_over_commit(self):
+ res = self._make_request('/servers/FAKE/action',
+ {'os-migrateLive':
+ {'host': 'hostname',
+ 'block_migration': False}})
+ self.assertEqual(400, res.status_int)
+
+ def test_migrate_live_with_invalid_block_migration(self):
+ res = self._make_request('/servers/FAKE/action',
+ {'os-migrateLive':
+ {'host': 'hostname',
+ 'block_migration': "foo",
+ 'disk_over_commit': False}})
+ self.assertEqual(400, res.status_int)
+
+ def test_migrate_live_with_invalid_disk_over_commit(self):
+ res = self._make_request('/servers/FAKE/action',
+ {'os-migrateLive':
+ {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': "foo"}})
+ self.assertEqual(400, res.status_int)
+
+ def _test_migrate_live_failed_with_exception(self, fake_exc,
+ uuid=None):
+ self.mox.StubOutWithMock(self.compute_api, 'live_migrate')
+
+ instance = self._stub_instance_get(uuid=uuid)
+ self.compute_api.live_migrate(self.context, instance, False,
+ False, 'hostname').AndRaise(fake_exc)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance.uuid,
+ {'os-migrateLive':
+ {'host': 'hostname',
+ 'block_migration': False,
+ 'disk_over_commit': False}})
+ self.assertEqual(400, res.status_int)
+ self.assertIn(unicode(fake_exc), res.body)
+
+ def test_migrate_live_compute_service_unavailable(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.ComputeServiceUnavailable(host='host'))
+
+ def test_migrate_live_invalid_hypervisor_type(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InvalidHypervisorType())
+
+ def test_migrate_live_invalid_cpu_info(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InvalidCPUInfo(reason=""))
+
+ def test_migrate_live_unable_to_migrate_to_self(self):
+ uuid = uuidutils.generate_uuid()
+ self._test_migrate_live_failed_with_exception(
+ exception.UnableToMigrateToSelf(instance_id=uuid,
+ host='host'),
+ uuid=uuid)
+
+ def test_migrate_live_destination_hypervisor_too_old(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.DestinationHypervisorTooOld())
+
+ def test_migrate_live_no_valid_host(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.NoValidHost(reason=''))
+
+ def test_migrate_live_invalid_local_storage(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InvalidLocalStorage(path='', reason=''))
+
+ def test_migrate_live_invalid_shared_storage(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InvalidSharedStorage(path='', reason=''))
+
+ def test_migrate_live_hypervisor_unavailable(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.HypervisorUnavailable(host=""))
+
+ def test_migrate_live_instance_not_running(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.InstanceNotRunning(instance_id=""))
+
+ def test_migrate_live_pre_check_error(self):
+ self._test_migrate_live_failed_with_exception(
+ exception.MigrationPreCheckError(reason=''))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_migrations.py b/nova/tests/unit/api/openstack/compute/contrib/test_migrations.py
new file mode 100644
index 0000000000..ac18576389
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_migrations.py
@@ -0,0 +1,139 @@
+# 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 datetime
+
+from lxml import etree
+
+from nova.api.openstack.compute.contrib import migrations
+from nova import context
+from nova import exception
+from nova import objects
+from nova.objects import base
+from nova.openstack.common.fixture import moxstubout
+from nova import test
+
+fake_migrations = [
+ {
+ 'id': 1234,
+ 'source_node': 'node1',
+ 'dest_node': 'node2',
+ 'source_compute': 'compute1',
+ 'dest_compute': 'compute2',
+ 'dest_host': '1.2.3.4',
+ 'status': 'Done',
+ 'instance_uuid': 'instance_id_123',
+ 'old_instance_type_id': 1,
+ 'new_instance_type_id': 2,
+ 'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
+ 'deleted_at': None,
+ 'deleted': False
+ },
+ {
+ 'id': 5678,
+ 'source_node': 'node10',
+ 'dest_node': 'node20',
+ 'source_compute': 'compute10',
+ 'dest_compute': 'compute20',
+ 'dest_host': '5.6.7.8',
+ 'status': 'Done',
+ 'instance_uuid': 'instance_id_456',
+ 'old_instance_type_id': 5,
+ 'new_instance_type_id': 6,
+ 'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
+ 'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
+ 'deleted_at': None,
+ 'deleted': False
+ }
+]
+
+migrations_obj = base.obj_make_list(
+ 'fake-context',
+ objects.MigrationList(),
+ objects.Migration,
+ fake_migrations)
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {}
+
+
+class MigrationsTestCase(test.NoDBTestCase):
+ def setUp(self):
+ """Run before each test."""
+ super(MigrationsTestCase, self).setUp()
+ self.controller = migrations.MigrationsController()
+ self.context = context.get_admin_context()
+ self.req = FakeRequest()
+ self.req.environ['nova.context'] = self.context
+ mox_fixture = self.useFixture(moxstubout.MoxStubout())
+ self.mox = mox_fixture.mox
+
+ def test_index(self):
+ migrations_in_progress = {
+ 'migrations': migrations.output(migrations_obj)}
+
+ for mig in migrations_in_progress['migrations']:
+ self.assertIn('id', mig)
+ self.assertNotIn('deleted', mig)
+ self.assertNotIn('deleted_at', mig)
+
+ filters = {'host': 'host1', 'status': 'migrating',
+ 'cell_name': 'ChildCell'}
+ self.req.GET = filters
+ self.mox.StubOutWithMock(self.controller.compute_api,
+ "get_migrations")
+
+ self.controller.compute_api.get_migrations(
+ self.context, filters).AndReturn(migrations_obj)
+ self.mox.ReplayAll()
+
+ response = self.controller.index(self.req)
+ self.assertEqual(migrations_in_progress, response)
+
+ def test_index_needs_authorization(self):
+ user_context = context.RequestContext(user_id=None,
+ project_id=None,
+ is_admin=False,
+ read_deleted="no",
+ overwrite=False)
+ self.req.environ['nova.context'] = user_context
+
+ self.assertRaises(exception.PolicyNotAuthorized, self.controller.index,
+ self.req)
+
+
+class MigrationsTemplateTest(test.NoDBTestCase):
+ def setUp(self):
+ super(MigrationsTemplateTest, self).setUp()
+ self.serializer = migrations.MigrationsTemplate()
+
+ def test_index_serialization(self):
+ migrations_out = migrations.output(migrations_obj)
+ res_xml = self.serializer.serialize(
+ {'migrations': migrations_out})
+
+ tree = etree.XML(res_xml)
+ children = tree.findall('migration')
+ self.assertEqual(tree.tag, 'migrations')
+ self.assertEqual(2, len(children))
+
+ for idx, child in enumerate(children):
+ self.assertEqual(child.tag, 'migration')
+ migration = migrations_out[idx]
+ for attr in migration.keys():
+ self.assertEqual(str(migration[attr]),
+ child.get(attr))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_multinic.py b/nova/tests/unit/api/openstack/compute/contrib/test_multinic.py
new file mode 100644
index 0000000000..dcf1dd299f
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_multinic.py
@@ -0,0 +1,204 @@
+# Copyright 2011 OpenStack Foundation
+# 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 mock
+from oslo.serialization import jsonutils
+import webob
+
+from nova import compute
+from nova import exception
+from nova import objects
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+UUID = '70f6db34-de8d-4fbd-aafb-4065bdfa6114'
+last_add_fixed_ip = (None, None)
+last_remove_fixed_ip = (None, None)
+
+
+def compute_api_add_fixed_ip(self, context, instance, network_id):
+ global last_add_fixed_ip
+
+ last_add_fixed_ip = (instance['uuid'], network_id)
+
+
+def compute_api_remove_fixed_ip(self, context, instance, address):
+ global last_remove_fixed_ip
+
+ last_remove_fixed_ip = (instance['uuid'], address)
+
+
+def compute_api_get(self, context, instance_id, want_objects=False,
+ expected_attrs=None):
+ instance = objects.Instance()
+ instance.uuid = instance_id
+ instance.id = 1
+ instance.vm_state = 'fake'
+ instance.task_state = 'fake'
+ instance.obj_reset_changes()
+ return instance
+
+
+class FixedIpTestV21(test.NoDBTestCase):
+ def setUp(self):
+ super(FixedIpTestV21, self).setUp()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+ self.stubs.Set(compute.api.API, "add_fixed_ip",
+ compute_api_add_fixed_ip)
+ self.stubs.Set(compute.api.API, "remove_fixed_ip",
+ compute_api_remove_fixed_ip)
+ self.stubs.Set(compute.api.API, 'get', compute_api_get)
+ self.app = self._get_app()
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21(init_only=('servers', 'os-multinic'))
+
+ def _get_url(self):
+ return '/v2/fake'
+
+ def test_add_fixed_ip(self):
+ global last_add_fixed_ip
+ last_add_fixed_ip = (None, None)
+
+ body = dict(addFixedIp=dict(networkId='test_net'))
+ req = webob.Request.blank(
+ self._get_url() + '/servers/%s/action' % UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 202)
+ self.assertEqual(last_add_fixed_ip, (UUID, 'test_net'))
+
+ def _test_add_fixed_ip_bad_request(self, body):
+ req = webob.Request.blank(
+ self._get_url() + '/servers/%s/action' % UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+ resp = req.get_response(self.app)
+ self.assertEqual(400, resp.status_int)
+
+ def test_add_fixed_ip_empty_network_id(self):
+ body = {'addFixedIp': {'network_id': ''}}
+ self._test_add_fixed_ip_bad_request(body)
+
+ def test_add_fixed_ip_network_id_bigger_than_36(self):
+ body = {'addFixedIp': {'network_id': 'a' * 37}}
+ self._test_add_fixed_ip_bad_request(body)
+
+ def test_add_fixed_ip_no_network(self):
+ global last_add_fixed_ip
+ last_add_fixed_ip = (None, None)
+
+ body = dict(addFixedIp=dict())
+ req = webob.Request.blank(
+ self._get_url() + '/servers/%s/action' % UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 400)
+ self.assertEqual(last_add_fixed_ip, (None, None))
+
+ @mock.patch.object(compute.api.API, 'add_fixed_ip')
+ def test_add_fixed_ip_no_more_ips_available(self, mock_add_fixed_ip):
+ mock_add_fixed_ip.side_effect = exception.NoMoreFixedIps(net='netid')
+
+ body = dict(addFixedIp=dict(networkId='test_net'))
+ req = webob.Request.blank(
+ self._get_url() + '/servers/%s/action' % UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 400)
+
+ def test_remove_fixed_ip(self):
+ global last_remove_fixed_ip
+ last_remove_fixed_ip = (None, None)
+
+ body = dict(removeFixedIp=dict(address='10.10.10.1'))
+ req = webob.Request.blank(
+ self._get_url() + '/servers/%s/action' % UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 202)
+ self.assertEqual(last_remove_fixed_ip, (UUID, '10.10.10.1'))
+
+ def test_remove_fixed_ip_no_address(self):
+ global last_remove_fixed_ip
+ last_remove_fixed_ip = (None, None)
+
+ body = dict(removeFixedIp=dict())
+ req = webob.Request.blank(
+ self._get_url() + '/servers/%s/action' % UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 400)
+ self.assertEqual(last_remove_fixed_ip, (None, None))
+
+ def test_remove_fixed_ip_invalid_address(self):
+ body = {'remove_fixed_ip': {'address': ''}}
+ req = webob.Request.blank(
+ self._get_url() + '/servers/%s/action' % UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+ resp = req.get_response(self.app)
+ self.assertEqual(400, resp.status_int)
+
+ @mock.patch.object(compute.api.API, 'remove_fixed_ip',
+ side_effect=exception.FixedIpNotFoundForSpecificInstance(
+ instance_uuid=UUID, ip='10.10.10.1'))
+ def test_remove_fixed_ip_not_found(self, _remove_fixed_ip):
+
+ body = {'remove_fixed_ip': {'address': '10.10.10.1'}}
+ req = webob.Request.blank(
+ self._get_url() + '/servers/%s/action' % UUID)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(400, resp.status_int)
+
+
+class FixedIpTestV2(FixedIpTestV21):
+ def setUp(self):
+ super(FixedIpTestV2, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Multinic'])
+
+ def _get_app(self):
+ return fakes.wsgi_app(init_only=('servers',))
+
+ def test_remove_fixed_ip_invalid_address(self):
+ # NOTE(cyeoh): This test is disabled for the V2 API because it is
+ # has poorer input validation.
+ pass
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_networks.py b/nova/tests/unit/api/openstack/compute/contrib/test_networks.py
new file mode 100644
index 0000000000..5636a06d0d
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_networks.py
@@ -0,0 +1,610 @@
+# Copyright 2011 Grid Dynamics
+# Copyright 2011 OpenStack Foundation
+# 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 copy
+import datetime
+import math
+import uuid
+
+import iso8601
+import mock
+import netaddr
+from oslo.config import cfg
+import webob
+
+from nova.api.openstack.compute.contrib import networks_associate
+from nova.api.openstack.compute.contrib import os_networks as networks
+from nova.api.openstack.compute.plugins.v3 import networks as networks_v21
+from nova.api.openstack.compute.plugins.v3 import networks_associate as \
+ networks_associate_v21
+from nova.api.openstack import extensions
+import nova.context
+from nova import exception
+from nova.network import manager
+from nova import objects
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+import nova.utils
+
+CONF = cfg.CONF
+
+UTC = iso8601.iso8601.Utc()
+FAKE_NETWORKS = [
+ {
+ 'bridge': 'br100', 'vpn_public_port': 1000,
+ 'dhcp_start': '10.0.0.3', 'bridge_interface': 'eth0',
+ 'updated_at': datetime.datetime(2011, 8, 16, 9, 26, 13, 48257,
+ tzinfo=UTC),
+ 'id': 1, 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf047',
+ 'cidr_v6': None, 'deleted_at': None,
+ 'gateway': '10.0.0.1', 'label': 'mynet_0',
+ 'project_id': '1234', 'rxtx_base': None,
+ 'vpn_private_address': '10.0.0.2', 'deleted': False,
+ 'vlan': 100, 'broadcast': '10.0.0.7',
+ 'netmask': '255.255.255.248', 'injected': False,
+ 'cidr': '10.0.0.0/29',
+ 'vpn_public_address': '127.0.0.1', 'multi_host': False,
+ 'dns1': None, 'dns2': None, 'host': 'nsokolov-desktop',
+ 'gateway_v6': None, 'netmask_v6': None, 'priority': None,
+ 'created_at': datetime.datetime(2011, 8, 15, 6, 19, 19, 387525,
+ tzinfo=UTC),
+ 'mtu': None, 'dhcp_server': '10.0.0.1', 'enable_dhcp': True,
+ 'share_address': False,
+ },
+ {
+ 'bridge': 'br101', 'vpn_public_port': 1001,
+ 'dhcp_start': '10.0.0.11', 'bridge_interface': 'eth0',
+ 'updated_at': None, 'id': 2, 'cidr_v6': None,
+ 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf000',
+ 'deleted_at': None, 'gateway': '10.0.0.9',
+ 'label': 'mynet_1', 'project_id': None,
+ 'vpn_private_address': '10.0.0.10', 'deleted': False,
+ 'vlan': 101, 'broadcast': '10.0.0.15', 'rxtx_base': None,
+ 'netmask': '255.255.255.248', 'injected': False,
+ 'cidr': '10.0.0.10/29', 'vpn_public_address': None,
+ 'multi_host': False, 'dns1': None, 'dns2': None, 'host': None,
+ 'gateway_v6': None, 'netmask_v6': None, 'priority': None,
+ 'created_at': datetime.datetime(2011, 8, 15, 6, 19, 19, 885495,
+ tzinfo=UTC),
+ 'mtu': None, 'dhcp_server': '10.0.0.9', 'enable_dhcp': True,
+ 'share_address': False,
+ },
+]
+
+
+FAKE_USER_NETWORKS = [
+ {
+ 'id': 1, 'cidr': '10.0.0.0/29', 'netmask': '255.255.255.248',
+ 'gateway': '10.0.0.1', 'broadcast': '10.0.0.7', 'dns1': None,
+ 'dns2': None, 'cidr_v6': None, 'gateway_v6': None, 'label': 'mynet_0',
+ 'netmask_v6': None, 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf047',
+ },
+ {
+ 'id': 2, 'cidr': '10.0.0.10/29', 'netmask': '255.255.255.248',
+ 'gateway': '10.0.0.9', 'broadcast': '10.0.0.15', 'dns1': None,
+ 'dns2': None, 'cidr_v6': None, 'gateway_v6': None, 'label': 'mynet_1',
+ 'netmask_v6': None, 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf000',
+ },
+]
+
+NEW_NETWORK = {
+ "network": {
+ "bridge_interface": "eth0",
+ "cidr": "10.20.105.0/24",
+ "label": "new net 111",
+ "vlan_start": 111,
+ "injected": False,
+ "multi_host": False,
+ 'mtu': None,
+ 'dhcp_server': '10.0.0.1',
+ 'enable_dhcp': True,
+ 'share_address': False,
+ }
+}
+
+
+class FakeNetworkAPI(object):
+
+ _sentinel = object()
+ _vlan_is_disabled = False
+
+ def __init__(self):
+ self.networks = copy.deepcopy(FAKE_NETWORKS)
+
+ def disable_vlan(self):
+ self._vlan_is_disabled = True
+
+ def delete(self, context, network_id):
+ if network_id == 'always_delete':
+ return True
+ if network_id == -1:
+ raise exception.NetworkInUse(network_id=network_id)
+ for i, network in enumerate(self.networks):
+ if network['id'] == network_id:
+ del self.networks[0]
+ return True
+ raise exception.NetworkNotFoundForUUID(uuid=network_id)
+
+ def disassociate(self, context, network_uuid):
+ for network in self.networks:
+ if network.get('uuid') == network_uuid:
+ network['project_id'] = None
+ return True
+ raise exception.NetworkNotFound(network_id=network_uuid)
+
+ def associate(self, context, network_uuid, host=_sentinel,
+ project=_sentinel):
+ for network in self.networks:
+ if network.get('uuid') == network_uuid:
+ if host is not FakeNetworkAPI._sentinel:
+ network['host'] = host
+ if project is not FakeNetworkAPI._sentinel:
+ network['project_id'] = project
+ return True
+ raise exception.NetworkNotFound(network_id=network_uuid)
+
+ def add_network_to_project(self, context,
+ project_id, network_uuid=None):
+ if self._vlan_is_disabled:
+ raise NotImplementedError()
+ if network_uuid:
+ for network in self.networks:
+ if network.get('project_id', None) is None:
+ network['project_id'] = project_id
+ return
+ return
+ for network in self.networks:
+ if network.get('uuid') == network_uuid:
+ network['project_id'] = project_id
+ return
+
+ def get_all(self, context):
+ return self._fake_db_network_get_all(context, project_only=True)
+
+ def _fake_db_network_get_all(self, context, project_only="allow_none"):
+ project_id = context.project_id
+ nets = self.networks
+ if nova.context.is_user_context(context) and project_only:
+ if project_only == 'allow_none':
+ nets = [n for n in self.networks
+ if (n['project_id'] == project_id or
+ n['project_id'] is None)]
+ else:
+ nets = [n for n in self.networks
+ if n['project_id'] == project_id]
+ objs = [objects.Network._from_db_object(context,
+ objects.Network(),
+ net)
+ for net in nets]
+ return objects.NetworkList(objects=objs)
+
+ def get(self, context, network_id):
+ for network in self.networks:
+ if network.get('uuid') == network_id:
+ return objects.Network._from_db_object(context,
+ objects.Network(),
+ network)
+ raise exception.NetworkNotFound(network_id=network_id)
+
+ def create(self, context, **kwargs):
+ subnet_bits = int(math.ceil(math.log(kwargs.get(
+ 'network_size', CONF.network_size), 2)))
+ fixed_net_v4 = netaddr.IPNetwork(kwargs['cidr'])
+ prefixlen_v4 = 32 - subnet_bits
+ subnets_v4 = list(fixed_net_v4.subnet(
+ prefixlen_v4,
+ count=kwargs.get('num_networks', CONF.num_networks)))
+ new_networks = []
+ new_id = max((net['id'] for net in self.networks))
+ for index, subnet_v4 in enumerate(subnets_v4):
+ new_id += 1
+ net = {'id': new_id, 'uuid': str(uuid.uuid4())}
+
+ net['cidr'] = str(subnet_v4)
+ net['netmask'] = str(subnet_v4.netmask)
+ net['gateway'] = kwargs.get('gateway') or str(subnet_v4[1])
+ net['broadcast'] = str(subnet_v4.broadcast)
+ net['dhcp_start'] = str(subnet_v4[2])
+
+ for key in FAKE_NETWORKS[0].iterkeys():
+ net.setdefault(key, kwargs.get(key))
+ new_networks.append(net)
+ self.networks += new_networks
+ return new_networks
+
+
+# NOTE(vish): tests that network create Exceptions actually return
+# the proper error responses
+class NetworkCreateExceptionsTestV21(test.TestCase):
+ url_prefix = '/v2/1234'
+
+ class PassthroughAPI(object):
+ def __init__(self):
+ self.network_manager = manager.FlatDHCPManager()
+
+ def create(self, *args, **kwargs):
+ if kwargs['label'] == 'fail_NetworkNotCreated':
+ raise exception.NetworkNotCreated(req='fake_fail')
+ return self.network_manager.create_networks(*args, **kwargs)
+
+ def setUp(self):
+ super(NetworkCreateExceptionsTestV21, self).setUp()
+ self._setup()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+
+ def _setup(self):
+ self.controller = networks_v21.NetworkController(self.PassthroughAPI())
+
+ def test_network_create_bad_vlan(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ net = copy.deepcopy(NEW_NETWORK)
+ net['network']['vlan_start'] = 'foo'
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, net)
+
+ def test_network_create_no_cidr(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ net = copy.deepcopy(NEW_NETWORK)
+ net['network']['cidr'] = ''
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, net)
+
+ def test_network_create_invalid_fixed_cidr(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ net = copy.deepcopy(NEW_NETWORK)
+ net['network']['fixed_cidr'] = 'foo'
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, net)
+
+ def test_network_create_invalid_start(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ net = copy.deepcopy(NEW_NETWORK)
+ net['network']['allowed_start'] = 'foo'
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, net)
+
+ def test_network_create_handle_network_not_created(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ net = copy.deepcopy(NEW_NETWORK)
+ net['network']['label'] = 'fail_NetworkNotCreated'
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, net)
+
+ def test_network_create_cidr_conflict(self):
+
+ @staticmethod
+ def get_all(context):
+ ret = objects.NetworkList(context=context, objects=[])
+ net = objects.Network(cidr='10.0.0.0/23')
+ ret.objects.append(net)
+ return ret
+
+ self.stubs.Set(objects.NetworkList, 'get_all', get_all)
+
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ net = copy.deepcopy(NEW_NETWORK)
+ net['network']['cidr'] = '10.0.0.0/24'
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller.create, req, net)
+
+
+class NetworkCreateExceptionsTestV2(NetworkCreateExceptionsTestV21):
+
+ def _setup(self):
+ ext_mgr = extensions.ExtensionManager()
+ ext_mgr.extensions = {'os-extended-networks': 'fake'}
+
+ self.controller = networks.NetworkController(
+ self.PassthroughAPI(), ext_mgr)
+
+
+class NetworksTestV21(test.NoDBTestCase):
+ url_prefix = '/v2/1234'
+
+ def setUp(self):
+ super(NetworksTestV21, self).setUp()
+ self.fake_network_api = FakeNetworkAPI()
+ self._setup()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+
+ def _setup(self):
+ self.controller = networks_v21.NetworkController(
+ self.fake_network_api)
+
+ def _check_status(self, res, method, code):
+ self.assertEqual(method.wsgi_code, 202)
+
+ @staticmethod
+ def network_uuid_to_id(network):
+ network['id'] = network['uuid']
+ del network['uuid']
+
+ def test_network_list_all_as_user(self):
+ self.maxDiff = None
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ res_dict = self.controller.index(req)
+ self.assertEqual(res_dict, {'networks': []})
+
+ project_id = req.environ["nova.context"].project_id
+ cxt = req.environ["nova.context"]
+ uuid = FAKE_NETWORKS[0]['uuid']
+ self.fake_network_api.associate(context=cxt,
+ network_uuid=uuid,
+ project=project_id)
+ res_dict = self.controller.index(req)
+ expected = [copy.deepcopy(FAKE_USER_NETWORKS[0])]
+ for network in expected:
+ self.network_uuid_to_id(network)
+ self.assertEqual({'networks': expected}, res_dict)
+
+ def test_network_list_all_as_admin(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ req.environ["nova.context"].is_admin = True
+ res_dict = self.controller.index(req)
+ expected = copy.deepcopy(FAKE_NETWORKS)
+ for network in expected:
+ self.network_uuid_to_id(network)
+ self.assertEqual({'networks': expected}, res_dict)
+
+ def test_network_disassociate(self):
+ uuid = FAKE_NETWORKS[0]['uuid']
+ req = fakes.HTTPRequest.blank(self.url_prefix +
+ '/os-networks/%s/action' % uuid)
+ res = self.controller._disassociate_host_and_project(
+ req, uuid, {'disassociate': None})
+ self._check_status(res, self.controller._disassociate_host_and_project,
+ 202)
+ self.assertIsNone(self.fake_network_api.networks[0]['project_id'])
+ self.assertIsNone(self.fake_network_api.networks[0]['host'])
+
+ def test_network_disassociate_not_found(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix +
+ '/os-networks/100/action')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._disassociate_host_and_project,
+ req, 100, {'disassociate': None})
+
+ def test_network_get_as_user(self):
+ uuid = FAKE_USER_NETWORKS[0]['uuid']
+ req = fakes.HTTPRequest.blank(self.url_prefix +
+ '/os-networks/%s' % uuid)
+ res_dict = self.controller.show(req, uuid)
+ expected = {'network': copy.deepcopy(FAKE_USER_NETWORKS[0])}
+ self.network_uuid_to_id(expected['network'])
+ self.assertEqual(expected, res_dict)
+
+ def test_network_get_as_admin(self):
+ uuid = FAKE_NETWORKS[0]['uuid']
+ req = fakes.HTTPRequest.blank(self.url_prefix +
+ '/os-networks/%s' % uuid)
+ req.environ["nova.context"].is_admin = True
+ res_dict = self.controller.show(req, uuid)
+ expected = {'network': copy.deepcopy(FAKE_NETWORKS[0])}
+ self.network_uuid_to_id(expected['network'])
+ self.assertEqual(expected, res_dict)
+
+ def test_network_get_not_found(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/100')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, 100)
+
+ def test_network_delete(self):
+ uuid = FAKE_NETWORKS[0]['uuid']
+ req = fakes.HTTPRequest.blank(self.url_prefix +
+ '/os-networks/%s' % uuid)
+ res = self.controller.delete(req, 1)
+ self._check_status(res, self.controller._disassociate_host_and_project,
+ 202)
+
+ def test_network_delete_not_found(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/100')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, req, 100)
+
+ def test_network_delete_in_use(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/-1')
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller.delete, req, -1)
+
+ def test_network_add(self):
+ uuid = FAKE_NETWORKS[1]['uuid']
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/add')
+ res = self.controller.add(req, {'id': uuid})
+ self._check_status(res, self.controller._disassociate_host_and_project,
+ 202)
+ req = fakes.HTTPRequest.blank(self.url_prefix +
+ '/os-networks/%s' % uuid)
+ req.environ["nova.context"].is_admin = True
+ res_dict = self.controller.show(req, uuid)
+ self.assertEqual(res_dict['network']['project_id'], 'fake')
+
+ @mock.patch('nova.tests.unit.api.openstack.compute.contrib.test_networks.'
+ 'FakeNetworkAPI.add_network_to_project',
+ side_effect=exception.NoMoreNetworks)
+ def test_network_add_no_more_networks_fail(self, mock_add):
+ uuid = FAKE_NETWORKS[1]['uuid']
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/add')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.add, req,
+ {'id': uuid})
+
+ @mock.patch('nova.tests.unit.api.openstack.compute.contrib.test_networks.'
+ 'FakeNetworkAPI.add_network_to_project',
+ side_effect=exception.NetworkNotFoundForUUID(uuid='fake_uuid'))
+ def test_network_add_network_not_found_networks_fail(self, mock_add):
+ uuid = FAKE_NETWORKS[1]['uuid']
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/add')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.add, req,
+ {'id': uuid})
+
+ def test_network_create(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ res_dict = self.controller.create(req, NEW_NETWORK)
+ self.assertIn('network', res_dict)
+ uuid = res_dict['network']['id']
+ req = fakes.HTTPRequest.blank(self.url_prefix +
+ '/os-networks/%s' % uuid)
+ res_dict = self.controller.show(req, uuid)
+ self.assertTrue(res_dict['network']['label'].
+ startswith(NEW_NETWORK['network']['label']))
+
+ def test_network_create_large(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ large_network = copy.deepcopy(NEW_NETWORK)
+ large_network['network']['cidr'] = '128.0.0.0/4'
+ res_dict = self.controller.create(req, large_network)
+ self.assertEqual(res_dict['network']['cidr'],
+ large_network['network']['cidr'])
+
+ def test_network_create_bad_cidr(self):
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ net = copy.deepcopy(NEW_NETWORK)
+ net['network']['cidr'] = '128.0.0.0/900'
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, net)
+
+ def test_network_neutron_disassociate_not_implemented(self):
+ uuid = FAKE_NETWORKS[1]['uuid']
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ controller = networks.NetworkController()
+ req = fakes.HTTPRequest.blank(self.url_prefix +
+ '/os-networks/%s/action' % uuid)
+ self.assertRaises(webob.exc.HTTPNotImplemented,
+ controller._disassociate_host_and_project,
+ req, uuid, {'disassociate': None})
+
+
+class NetworksTestV2(NetworksTestV21):
+
+ def _setup(self):
+ ext_mgr = extensions.ExtensionManager()
+ ext_mgr.extensions = {'os-extended-networks': 'fake'}
+ self.controller = networks.NetworkController(self.fake_network_api,
+ ext_mgr)
+
+ def _check_status(self, res, method, code):
+ self.assertEqual(res.status_int, 202)
+
+ def test_network_create_not_extended(self):
+ self.stubs.Set(self.controller, 'extended', False)
+ # NOTE(vish): Verify that new params are not passed through if
+ # extension is not enabled.
+
+ def no_mtu(*args, **kwargs):
+ if 'mtu' in kwargs:
+ raise test.TestingException("mtu should not pass through")
+ return [{}]
+
+ self.stubs.Set(self.controller.network_api, 'create', no_mtu)
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks')
+ net = copy.deepcopy(NEW_NETWORK)
+ net['network']['mtu'] = 9000
+ self.controller.create(req, net)
+
+
+class NetworksAssociateTestV21(test.NoDBTestCase):
+
+ def setUp(self):
+ super(NetworksAssociateTestV21, self).setUp()
+ self.fake_network_api = FakeNetworkAPI()
+ self._setup()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+
+ def _setup(self):
+ self.controller = networks.NetworkController(self.fake_network_api)
+ self.associate_controller = networks_associate_v21\
+ .NetworkAssociateActionController(self.fake_network_api)
+
+ def _check_status(self, res, method, code):
+ self.assertEqual(method.wsgi_code, code)
+
+ def test_network_disassociate_host_only(self):
+ uuid = FAKE_NETWORKS[0]['uuid']
+ req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid)
+ res = self.associate_controller._disassociate_host_only(
+ req, uuid, {'disassociate_host': None})
+ self._check_status(res,
+ self.associate_controller._disassociate_host_only,
+ 202)
+ self.assertIsNotNone(self.fake_network_api.networks[0]['project_id'])
+ self.assertIsNone(self.fake_network_api.networks[0]['host'])
+
+ def test_network_disassociate_project_only(self):
+ uuid = FAKE_NETWORKS[0]['uuid']
+ req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid)
+ res = self.associate_controller._disassociate_project_only(
+ req, uuid, {'disassociate_project': None})
+ self._check_status(
+ res, self.associate_controller._disassociate_project_only, 202)
+ self.assertIsNone(self.fake_network_api.networks[0]['project_id'])
+ self.assertIsNotNone(self.fake_network_api.networks[0]['host'])
+
+ def test_network_associate_with_host(self):
+ uuid = FAKE_NETWORKS[1]['uuid']
+ req = fakes.HTTPRequest.blank('/v2/1234//os-networks/%s/action' % uuid)
+ res = self.associate_controller._associate_host(
+ req, uuid, {'associate_host': "TestHost"})
+ self._check_status(res, self.associate_controller._associate_host, 202)
+ req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s' % uuid)
+ req.environ["nova.context"].is_admin = True
+ res_dict = self.controller.show(req, uuid)
+ self.assertEqual(res_dict['network']['host'], 'TestHost')
+
+ def test_network_neutron_associate_not_implemented(self):
+ uuid = FAKE_NETWORKS[1]['uuid']
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ assoc_ctrl = networks_associate.NetworkAssociateActionController()
+
+ req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid)
+ self.assertRaises(webob.exc.HTTPNotImplemented,
+ assoc_ctrl._associate_host,
+ req, uuid, {'associate_host': "TestHost"})
+
+ def test_network_neutron_disassociate_project_not_implemented(self):
+ uuid = FAKE_NETWORKS[1]['uuid']
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ assoc_ctrl = networks_associate.NetworkAssociateActionController()
+
+ req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid)
+ self.assertRaises(webob.exc.HTTPNotImplemented,
+ assoc_ctrl._disassociate_project_only,
+ req, uuid, {'disassociate_project': None})
+
+ def test_network_neutron_disassociate_host_not_implemented(self):
+ uuid = FAKE_NETWORKS[1]['uuid']
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ assoc_ctrl = networks_associate.NetworkAssociateActionController()
+ req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid)
+ self.assertRaises(webob.exc.HTTPNotImplemented,
+ assoc_ctrl._disassociate_host_only,
+ req, uuid, {'disassociate_host': None})
+
+
+class NetworksAssociateTestV2(NetworksAssociateTestV21):
+
+ def _setup(self):
+ ext_mgr = extensions.ExtensionManager()
+ ext_mgr.extensions = {'os-extended-networks': 'fake'}
+ self.controller = networks.NetworkController(
+ self.fake_network_api,
+ ext_mgr)
+ self.associate_controller = networks_associate\
+ .NetworkAssociateActionController(self.fake_network_api)
+
+ def _check_status(self, res, method, code):
+ self.assertEqual(res.status_int, 202)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_neutron_security_groups.py b/nova/tests/unit/api/openstack/compute/contrib/test_neutron_security_groups.py
new file mode 100644
index 0000000000..704de21005
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_neutron_security_groups.py
@@ -0,0 +1,918 @@
+# Copyright 2013 Nicira, Inc.
+# 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 uuid
+
+from lxml import etree
+import mock
+from neutronclient.common import exceptions as n_exc
+from neutronclient.neutron import v2_0 as neutronv20
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import security_groups
+from nova.api.openstack import xmlutil
+from nova import compute
+from nova import context
+import nova.db
+from nova import exception
+from nova.network import model
+from nova.network import neutronv2
+from nova.network.neutronv2 import api as neutron_api
+from nova.network.security_group import neutron_driver
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack.compute.contrib import test_security_groups
+from nova.tests.unit.api.openstack import fakes
+
+
+class TestNeutronSecurityGroupsTestCase(test.TestCase):
+ def setUp(self):
+ super(TestNeutronSecurityGroupsTestCase, self).setUp()
+ cfg.CONF.set_override('security_group_api', 'neutron')
+ self.original_client = neutronv2.get_client
+ neutronv2.get_client = get_client
+
+ def tearDown(self):
+ neutronv2.get_client = self.original_client
+ get_client()._reset()
+ super(TestNeutronSecurityGroupsTestCase, self).tearDown()
+
+
+class TestNeutronSecurityGroupsV21(
+ test_security_groups.TestSecurityGroupsV21,
+ TestNeutronSecurityGroupsTestCase):
+
+ def _create_sg_template(self, **kwargs):
+ sg = test_security_groups.security_group_template(**kwargs)
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ return self.controller.create(req, {'security_group': sg})
+
+ def _create_network(self):
+ body = {'network': {'name': 'net1'}}
+ neutron = get_client()
+ net = neutron.create_network(body)
+ body = {'subnet': {'network_id': net['network']['id'],
+ 'cidr': '10.0.0.0/24'}}
+ neutron.create_subnet(body)
+ return net
+
+ def _create_port(self, **kwargs):
+ body = {'port': {'binding:vnic_type': model.VNIC_TYPE_NORMAL}}
+ fields = ['security_groups', 'device_id', 'network_id',
+ 'port_security_enabled']
+ for field in fields:
+ if field in kwargs:
+ body['port'][field] = kwargs[field]
+ neutron = get_client()
+ return neutron.create_port(body)
+
+ def _create_security_group(self, **kwargs):
+ body = {'security_group': {}}
+ fields = ['name', 'description']
+ for field in fields:
+ if field in kwargs:
+ body['security_group'][field] = kwargs[field]
+ neutron = get_client()
+ return neutron.create_security_group(body)
+
+ def test_create_security_group_with_no_description(self):
+ # Neutron's security group description field is optional.
+ pass
+
+ def test_create_security_group_with_empty_description(self):
+ # Neutron's security group description field is optional.
+ pass
+
+ def test_create_security_group_with_blank_name(self):
+ # Neutron's security group name field is optional.
+ pass
+
+ def test_create_security_group_with_whitespace_name(self):
+ # Neutron allows security group name to be whitespace.
+ pass
+
+ def test_create_security_group_with_blank_description(self):
+ # Neutron's security group description field is optional.
+ pass
+
+ def test_create_security_group_with_whitespace_description(self):
+ # Neutron allows description to be whitespace.
+ pass
+
+ def test_create_security_group_with_duplicate_name(self):
+ # Neutron allows duplicate names for security groups.
+ pass
+
+ def test_create_security_group_non_string_name(self):
+ # Neutron allows security group name to be non string.
+ pass
+
+ def test_create_security_group_non_string_description(self):
+ # Neutron allows non string description.
+ pass
+
+ def test_create_security_group_quota_limit(self):
+ # Enforced by Neutron server.
+ pass
+
+ def test_update_security_group(self):
+ # Enforced by Neutron server.
+ pass
+
+ def test_get_security_group_list(self):
+ self._create_sg_template().get('security_group')
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ list_dict = self.controller.index(req)
+ self.assertEqual(len(list_dict['security_groups']), 2)
+
+ def test_get_security_group_list_all_tenants(self):
+ pass
+
+ def test_get_security_group_by_instance(self):
+ sg = self._create_sg_template().get('security_group')
+ net = self._create_network()
+ self._create_port(
+ network_id=net['network']['id'], security_groups=[sg['id']],
+ device_id=test_security_groups.FAKE_UUID1)
+ expected = [{'rules': [], 'tenant_id': 'fake', 'id': sg['id'],
+ 'name': 'test', 'description': 'test-description'}]
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ test_security_groups.return_server_by_uuid)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/%s/os-security-groups'
+ % test_security_groups.FAKE_UUID1)
+ res_dict = self.server_controller.index(
+ req, test_security_groups.FAKE_UUID1)['security_groups']
+ self.assertEqual(expected, res_dict)
+
+ def test_get_security_group_by_id(self):
+ sg = self._create_sg_template().get('security_group')
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s'
+ % sg['id'])
+ res_dict = self.controller.show(req, sg['id'])
+ expected = {'security_group': sg}
+ self.assertEqual(res_dict, expected)
+
+ def test_delete_security_group_by_id(self):
+ sg = self._create_sg_template().get('security_group')
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s' %
+ sg['id'])
+ self.controller.delete(req, sg['id'])
+
+ def test_delete_security_group_by_admin(self):
+ sg = self._create_sg_template().get('security_group')
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s' %
+ sg['id'], use_admin_context=True)
+ self.controller.delete(req, sg['id'])
+
+ def test_delete_security_group_in_use(self):
+ sg = self._create_sg_template().get('security_group')
+ self._create_network()
+ db_inst = fakes.stub_instance(id=1, nw_cache=[], security_groups=[])
+ _context = context.get_admin_context()
+ instance = instance_obj.Instance._from_db_object(
+ _context, instance_obj.Instance(), db_inst,
+ expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS)
+ neutron = neutron_api.API()
+ with mock.patch.object(nova.db, 'instance_get_by_uuid',
+ return_value=db_inst):
+ neutron.allocate_for_instance(_context, instance,
+ security_groups=[sg['id']])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s'
+ % sg['id'])
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
+ req, sg['id'])
+
+ def test_associate_non_running_instance(self):
+ # Neutron does not care if the instance is running or not. When the
+ # instances is detected by nuetron it will push down the security
+ # group policy to it.
+ pass
+
+ def test_associate_already_associated_security_group_to_instance(self):
+ # Neutron security groups does not raise an error if you update a
+ # port adding a security group to it that was already associated
+ # to the port. This is because PUT semantics are used.
+ pass
+
+ def test_associate(self):
+ sg = self._create_sg_template().get('security_group')
+ net = self._create_network()
+ self._create_port(
+ network_id=net['network']['id'], security_groups=[sg['id']],
+ device_id=test_security_groups.FAKE_UUID1)
+
+ self.stubs.Set(nova.db, 'instance_get',
+ test_security_groups.return_server)
+ body = dict(addSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.manager._addSecurityGroup(req, '1', body)
+
+ def test_associate_duplicate_names(self):
+ sg1 = self._create_security_group(name='sg1',
+ description='sg1')['security_group']
+ self._create_security_group(name='sg1',
+ description='sg1')['security_group']
+ net = self._create_network()
+ self._create_port(
+ network_id=net['network']['id'], security_groups=[sg1['id']],
+ device_id=test_security_groups.FAKE_UUID1)
+
+ self.stubs.Set(nova.db, 'instance_get',
+ test_security_groups.return_server)
+ body = dict(addSecurityGroup=dict(name="sg1"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.manager._addSecurityGroup, req, '1', body)
+
+ def test_associate_port_security_enabled_true(self):
+ sg = self._create_sg_template().get('security_group')
+ net = self._create_network()
+ self._create_port(
+ network_id=net['network']['id'], security_groups=[sg['id']],
+ port_security_enabled=True,
+ device_id=test_security_groups.FAKE_UUID1)
+
+ self.stubs.Set(nova.db, 'instance_get',
+ test_security_groups.return_server)
+ body = dict(addSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.manager._addSecurityGroup(req, '1', body)
+
+ def test_associate_port_security_enabled_false(self):
+ self._create_sg_template().get('security_group')
+ net = self._create_network()
+ self._create_port(
+ network_id=net['network']['id'], port_security_enabled=False,
+ device_id=test_security_groups.FAKE_UUID1)
+
+ self.stubs.Set(nova.db, 'instance_get',
+ test_security_groups.return_server)
+ body = dict(addSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._addSecurityGroup,
+ req, '1', body)
+
+ def test_disassociate_by_non_existing_security_group_name(self):
+ self.stubs.Set(nova.db, 'instance_get',
+ test_security_groups.return_server)
+ body = dict(removeSecurityGroup=dict(name='non-existing'))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._removeSecurityGroup, req, '1', body)
+
+ def test_disassociate_non_running_instance(self):
+ # Neutron does not care if the instance is running or not. When the
+ # instances is detected by neutron it will push down the security
+ # group policy to it.
+ pass
+
+ def test_disassociate_already_associated_security_group_to_instance(self):
+ # Neutron security groups does not raise an error if you update a
+ # port adding a security group to it that was already associated
+ # to the port. This is because PUT semantics are used.
+ pass
+
+ def test_disassociate(self):
+ sg = self._create_sg_template().get('security_group')
+ net = self._create_network()
+ self._create_port(
+ network_id=net['network']['id'], security_groups=[sg['id']],
+ device_id=test_security_groups.FAKE_UUID1)
+
+ self.stubs.Set(nova.db, 'instance_get',
+ test_security_groups.return_server)
+ body = dict(removeSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.manager._removeSecurityGroup(req, '1', body)
+
+ def test_get_raises_no_unique_match_error(self):
+
+ def fake_find_resourceid_by_name_or_id(client, param, name,
+ project_id=None):
+ raise n_exc.NeutronClientNoUniqueMatch()
+
+ self.stubs.Set(neutronv20, 'find_resourceid_by_name_or_id',
+ fake_find_resourceid_by_name_or_id)
+ security_group_api = self.controller.security_group_api
+ self.assertRaises(exception.NoUniqueMatch, security_group_api.get,
+ context.get_admin_context(), 'foobar')
+
+ def test_get_instances_security_groups_bindings(self):
+ servers = [{'id': test_security_groups.FAKE_UUID1},
+ {'id': test_security_groups.FAKE_UUID2}]
+ sg1 = self._create_sg_template(name='test1').get('security_group')
+ sg2 = self._create_sg_template(name='test2').get('security_group')
+ # test name='' is replaced with id
+ sg3 = self._create_sg_template(name='').get('security_group')
+ net = self._create_network()
+ self._create_port(
+ network_id=net['network']['id'], security_groups=[sg1['id'],
+ sg2['id']],
+ device_id=test_security_groups.FAKE_UUID1)
+ self._create_port(
+ network_id=net['network']['id'], security_groups=[sg2['id'],
+ sg3['id']],
+ device_id=test_security_groups.FAKE_UUID2)
+ expected = {test_security_groups.FAKE_UUID1: [{'name': sg1['name']},
+ {'name': sg2['name']}],
+ test_security_groups.FAKE_UUID2: [{'name': sg2['name']},
+ {'name': sg3['id']}]}
+ security_group_api = self.controller.security_group_api
+ bindings = (
+ security_group_api.get_instances_security_groups_bindings(
+ context.get_admin_context(), servers))
+ self.assertEqual(bindings, expected)
+
+ def test_get_instance_security_groups(self):
+ sg1 = self._create_sg_template(name='test1').get('security_group')
+ sg2 = self._create_sg_template(name='test2').get('security_group')
+ # test name='' is replaced with id
+ sg3 = self._create_sg_template(name='').get('security_group')
+ net = self._create_network()
+ self._create_port(
+ network_id=net['network']['id'], security_groups=[sg1['id'],
+ sg2['id'],
+ sg3['id']],
+ device_id=test_security_groups.FAKE_UUID1)
+
+ expected = [{'name': sg1['name']}, {'name': sg2['name']},
+ {'name': sg3['id']}]
+ security_group_api = self.controller.security_group_api
+ sgs = security_group_api.get_instance_security_groups(
+ context.get_admin_context(), test_security_groups.FAKE_UUID1)
+ self.assertEqual(sgs, expected)
+
+ @mock.patch('nova.network.security_group.neutron_driver.SecurityGroupAPI.'
+ 'get_instances_security_groups_bindings')
+ def test_get_security_group_empty_for_instance(self, neutron_sg_bind_mock):
+ servers = [{'id': test_security_groups.FAKE_UUID1}]
+ neutron_sg_bind_mock.return_value = {}
+
+ security_group_api = self.controller.security_group_api
+ ctx = context.get_admin_context()
+ sgs = security_group_api.get_instance_security_groups(ctx,
+ test_security_groups.FAKE_UUID1)
+
+ neutron_sg_bind_mock.assert_called_once_with(ctx, servers, False)
+ self.assertEqual([], sgs)
+
+ def test_create_port_with_sg_and_port_security_enabled_true(self):
+ sg1 = self._create_sg_template(name='test1').get('security_group')
+ net = self._create_network()
+ self._create_port(
+ network_id=net['network']['id'], security_groups=[sg1['id']],
+ port_security_enabled=True,
+ device_id=test_security_groups.FAKE_UUID1)
+ security_group_api = self.controller.security_group_api
+ sgs = security_group_api.get_instance_security_groups(
+ context.get_admin_context(), test_security_groups.FAKE_UUID1)
+ self.assertEqual(sgs, [{'name': 'test1'}])
+
+ def test_create_port_with_sg_and_port_security_enabled_false(self):
+ sg1 = self._create_sg_template(name='test1').get('security_group')
+ net = self._create_network()
+ self.assertRaises(exception.SecurityGroupCannotBeApplied,
+ self._create_port,
+ network_id=net['network']['id'],
+ security_groups=[sg1['id']],
+ port_security_enabled=False,
+ device_id=test_security_groups.FAKE_UUID1)
+
+
+class TestNeutronSecurityGroupsV2(TestNeutronSecurityGroupsV21):
+ controller_cls = security_groups.SecurityGroupController
+ server_secgrp_ctl_cls = security_groups.ServerSecurityGroupController
+ secgrp_act_ctl_cls = security_groups.SecurityGroupActionController
+
+
+class TestNeutronSecurityGroupRulesTestCase(TestNeutronSecurityGroupsTestCase):
+ def setUp(self):
+ super(TestNeutronSecurityGroupRulesTestCase, self).setUp()
+ id1 = '11111111-1111-1111-1111-111111111111'
+ sg_template1 = test_security_groups.security_group_template(
+ security_group_rules=[], id=id1)
+ id2 = '22222222-2222-2222-2222-222222222222'
+ sg_template2 = test_security_groups.security_group_template(
+ security_group_rules=[], id=id2)
+ self.controller_sg = security_groups.SecurityGroupController()
+ neutron = get_client()
+ neutron._fake_security_groups[id1] = sg_template1
+ neutron._fake_security_groups[id2] = sg_template2
+
+ def tearDown(self):
+ neutronv2.get_client = self.original_client
+ get_client()._reset()
+ super(TestNeutronSecurityGroupsTestCase, self).tearDown()
+
+
+class _TestNeutronSecurityGroupRulesBase(object):
+
+ def test_create_add_existing_rules_by_cidr(self):
+ sg = test_security_groups.security_group_template()
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.controller_sg.create(req, {'security_group': sg})
+ rule = test_security_groups.security_group_rule_template(
+ cidr='15.0.0.0/8', parent_group_id=self.sg2['id'])
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.controller.create(req, {'security_group_rule': rule})
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_add_existing_rules_by_group_id(self):
+ sg = test_security_groups.security_group_template()
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.controller_sg.create(req, {'security_group': sg})
+ rule = test_security_groups.security_group_rule_template(
+ group=self.sg1['id'], parent_group_id=self.sg2['id'])
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.controller.create(req, {'security_group_rule': rule})
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_delete(self):
+ rule = test_security_groups.security_group_rule_template(
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+ security_group_rule = res_dict['security_group_rule']
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules/%s'
+ % security_group_rule['id'])
+ self.controller.delete(req, security_group_rule['id'])
+
+ def test_create_rule_quota_limit(self):
+ # Enforced by neutron
+ pass
+
+
+class TestNeutronSecurityGroupRulesV2(
+ _TestNeutronSecurityGroupRulesBase,
+ test_security_groups.TestSecurityGroupRulesV2,
+ TestNeutronSecurityGroupRulesTestCase):
+ pass
+
+
+class TestNeutronSecurityGroupRulesV21(
+ _TestNeutronSecurityGroupRulesBase,
+ test_security_groups.TestSecurityGroupRulesV21,
+ TestNeutronSecurityGroupRulesTestCase):
+ pass
+
+
+class TestNeutronSecurityGroupsXMLDeserializer(
+ test_security_groups.TestSecurityGroupXMLDeserializer,
+ TestNeutronSecurityGroupsTestCase):
+ pass
+
+
+class TestNeutronSecurityGroupsXMLSerializer(
+ test_security_groups.TestSecurityGroupXMLSerializer,
+ TestNeutronSecurityGroupsTestCase):
+ pass
+
+
+class TestNeutronSecurityGroupsOutputTest(TestNeutronSecurityGroupsTestCase):
+ content_type = 'application/json'
+
+ def setUp(self):
+ super(TestNeutronSecurityGroupsOutputTest, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.controller = security_groups.SecurityGroupController()
+ self.stubs.Set(compute.api.API, 'get',
+ test_security_groups.fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all',
+ test_security_groups.fake_compute_get_all)
+ self.stubs.Set(compute.api.API, 'create',
+ test_security_groups.fake_compute_create)
+ self.stubs.Set(neutron_driver.SecurityGroupAPI,
+ 'get_instances_security_groups_bindings',
+ (test_security_groups.
+ fake_get_instances_security_groups_bindings))
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Security_groups'])
+
+ def _make_request(self, url, body=None):
+ req = webob.Request.blank(url)
+ if body:
+ req.method = 'POST'
+ req.body = self._encode_body(body)
+ req.content_type = self.content_type
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app(init_only=('servers',)))
+ return res
+
+ def _encode_body(self, body):
+ return jsonutils.dumps(body)
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def _get_groups(self, server):
+ return server.get('security_groups')
+
+ def test_create(self):
+ url = '/v2/fake/servers'
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ security_groups = [{'name': 'fake-2-0'}, {'name': 'fake-2-1'}]
+ for security_group in security_groups:
+ sg = test_security_groups.security_group_template(
+ name=security_group['name'])
+ self.controller.create(req, {'security_group': sg})
+
+ server = dict(name='server_test', imageRef=image_uuid, flavorRef=2,
+ security_groups=security_groups)
+ res = self._make_request(url, {'server': server})
+ self.assertEqual(res.status_int, 202)
+ server = self._get_server(res.body)
+ for i, group in enumerate(self._get_groups(server)):
+ name = 'fake-2-%s' % i
+ self.assertEqual(group.get('name'), name)
+
+ def test_create_server_get_default_security_group(self):
+ url = '/v2/fake/servers'
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ server = dict(name='server_test', imageRef=image_uuid, flavorRef=2)
+ res = self._make_request(url, {'server': server})
+ self.assertEqual(res.status_int, 202)
+ server = self._get_server(res.body)
+ group = self._get_groups(server)[0]
+ self.assertEqual(group.get('name'), 'default')
+
+ def test_show(self):
+ def fake_get_instance_security_groups(inst, context, id):
+ return [{'name': 'fake-2-0'}, {'name': 'fake-2-1'}]
+
+ self.stubs.Set(neutron_driver.SecurityGroupAPI,
+ 'get_instance_security_groups',
+ fake_get_instance_security_groups)
+
+ url = '/v2/fake/servers'
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ security_groups = [{'name': 'fake-2-0'}, {'name': 'fake-2-1'}]
+ for security_group in security_groups:
+ sg = test_security_groups.security_group_template(
+ name=security_group['name'])
+ self.controller.create(req, {'security_group': sg})
+ server = dict(name='server_test', imageRef=image_uuid, flavorRef=2,
+ security_groups=security_groups)
+
+ res = self._make_request(url, {'server': server})
+ self.assertEqual(res.status_int, 202)
+ server = self._get_server(res.body)
+ for i, group in enumerate(self._get_groups(server)):
+ name = 'fake-2-%s' % i
+ self.assertEqual(group.get('name'), name)
+
+ # Test that show (GET) returns the same information as create (POST)
+ url = '/v2/fake/servers/' + test_security_groups.UUID3
+ res = self._make_request(url)
+ self.assertEqual(res.status_int, 200)
+ server = self._get_server(res.body)
+
+ for i, group in enumerate(self._get_groups(server)):
+ name = 'fake-2-%s' % i
+ self.assertEqual(group.get('name'), name)
+
+ def test_detail(self):
+ url = '/v2/fake/servers/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ for i, server in enumerate(self._get_servers(res.body)):
+ for j, group in enumerate(self._get_groups(server)):
+ name = 'fake-%s-%s' % (i, j)
+ self.assertEqual(group.get('name'), name)
+
+ def test_no_instance_passthrough_404(self):
+
+ def fake_compute_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ url = '/v2/fake/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 404)
+
+
+class TestNeutronSecurityGroupsOutputXMLTest(
+ TestNeutronSecurityGroupsOutputTest):
+
+ content_type = 'application/xml'
+
+ class MinimalCreateServerTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('server', selector='server')
+ root.set('name')
+ root.set('id')
+ root.set('imageRef')
+ root.set('flavorRef')
+ elem = xmlutil.SubTemplateElement(root, 'security_groups')
+ sg = xmlutil.SubTemplateElement(elem, 'security_group',
+ selector='security_groups')
+ sg.set('name')
+ return xmlutil.MasterTemplate(root, 1,
+ nsmap={None: xmlutil.XMLNS_V11})
+
+ def _encode_body(self, body):
+ serializer = self.MinimalCreateServerTemplate()
+ return serializer.serialize(body)
+
+ def _get_server(self, body):
+ return etree.XML(body)
+
+ def _get_servers(self, body):
+ return etree.XML(body).getchildren()
+
+ def _get_groups(self, server):
+ # NOTE(vish): we are adding security groups without an extension
+ # namespace so we don't break people using the existing
+ # functionality, but that means we need to use find with
+ # the existing server namespace.
+ namespace = server.nsmap[None]
+ return server.find('{%s}security_groups' % namespace).getchildren()
+
+
+def get_client(context=None, admin=False):
+ return MockClient()
+
+
+class MockClient(object):
+
+ # Needs to be global to survive multiple calls to get_client.
+ _fake_security_groups = {}
+ _fake_ports = {}
+ _fake_networks = {}
+ _fake_subnets = {}
+ _fake_security_group_rules = {}
+
+ def __init__(self):
+ # add default security group
+ if not len(self._fake_security_groups):
+ ret = {'name': 'default', 'description': 'default',
+ 'tenant_id': 'fake_tenant', 'security_group_rules': [],
+ 'id': str(uuid.uuid4())}
+ self._fake_security_groups[ret['id']] = ret
+
+ def _reset(self):
+ self._fake_security_groups.clear()
+ self._fake_ports.clear()
+ self._fake_networks.clear()
+ self._fake_subnets.clear()
+ self._fake_security_group_rules.clear()
+
+ def create_security_group(self, body=None):
+ s = body.get('security_group')
+ if len(s.get('name')) > 255 or len(s.get('description')) > 255:
+ msg = 'Security Group name great than 255'
+ raise n_exc.NeutronClientException(message=msg, status_code=401)
+ ret = {'name': s.get('name'), 'description': s.get('description'),
+ 'tenant_id': 'fake', 'security_group_rules': [],
+ 'id': str(uuid.uuid4())}
+
+ self._fake_security_groups[ret['id']] = ret
+ return {'security_group': ret}
+
+ def create_network(self, body):
+ n = body.get('network')
+ ret = {'status': 'ACTIVE', 'subnets': [], 'name': n.get('name'),
+ 'admin_state_up': n.get('admin_state_up', True),
+ 'tenant_id': 'fake_tenant',
+ 'id': str(uuid.uuid4())}
+ if 'port_security_enabled' in n:
+ ret['port_security_enabled'] = n['port_security_enabled']
+ self._fake_networks[ret['id']] = ret
+ return {'network': ret}
+
+ def create_subnet(self, body):
+ s = body.get('subnet')
+ try:
+ net = self._fake_networks[s.get('network_id')]
+ except KeyError:
+ msg = 'Network %s not found' % s.get('network_id')
+ raise n_exc.NeutronClientException(message=msg, status_code=404)
+ ret = {'name': s.get('name'), 'network_id': s.get('network_id'),
+ 'tenant_id': 'fake_tenant', 'cidr': s.get('cidr'),
+ 'id': str(uuid.uuid4()), 'gateway_ip': '10.0.0.1'}
+ net['subnets'].append(ret['id'])
+ self._fake_networks[net['id']] = net
+ self._fake_subnets[ret['id']] = ret
+ return {'subnet': ret}
+
+ def create_port(self, body):
+ p = body.get('port')
+ ret = {'status': 'ACTIVE', 'id': str(uuid.uuid4()),
+ 'mac_address': p.get('mac_address', 'fa:16:3e:b8:f5:fb'),
+ 'device_id': p.get('device_id', str(uuid.uuid4())),
+ 'admin_state_up': p.get('admin_state_up', True),
+ 'security_groups': p.get('security_groups', []),
+ 'network_id': p.get('network_id'),
+ 'binding:vnic_type':
+ p.get('binding:vnic_type') or model.VNIC_TYPE_NORMAL}
+
+ network = self._fake_networks[p['network_id']]
+ if 'port_security_enabled' in p:
+ ret['port_security_enabled'] = p['port_security_enabled']
+ elif 'port_security_enabled' in network:
+ ret['port_security_enabled'] = network['port_security_enabled']
+
+ port_security = ret.get('port_security_enabled', True)
+ # port_security must be True if security groups are present
+ if not port_security and ret['security_groups']:
+ raise exception.SecurityGroupCannotBeApplied()
+
+ if network['subnets']:
+ ret['fixed_ips'] = [{'subnet_id': network['subnets'][0],
+ 'ip_address': '10.0.0.1'}]
+ if not ret['security_groups'] and (port_security is None or
+ port_security is True):
+ for security_group in self._fake_security_groups.values():
+ if security_group['name'] == 'default':
+ ret['security_groups'] = [security_group['id']]
+ break
+ self._fake_ports[ret['id']] = ret
+ return {'port': ret}
+
+ def create_security_group_rule(self, body):
+ # does not handle bulk case so just picks rule[0]
+ r = body.get('security_group_rules')[0]
+ fields = ['direction', 'protocol', 'port_range_min', 'port_range_max',
+ 'ethertype', 'remote_ip_prefix', 'tenant_id',
+ 'security_group_id', 'remote_group_id']
+ ret = {}
+ for field in fields:
+ ret[field] = r.get(field)
+ ret['id'] = str(uuid.uuid4())
+ self._fake_security_group_rules[ret['id']] = ret
+ return {'security_group_rules': [ret]}
+
+ def show_security_group(self, security_group, **_params):
+ try:
+ sg = self._fake_security_groups[security_group]
+ except KeyError:
+ msg = 'Security Group %s not found' % security_group
+ raise n_exc.NeutronClientException(message=msg, status_code=404)
+ for security_group_rule in self._fake_security_group_rules.values():
+ if security_group_rule['security_group_id'] == sg['id']:
+ sg['security_group_rules'].append(security_group_rule)
+
+ return {'security_group': sg}
+
+ def show_security_group_rule(self, security_group_rule, **_params):
+ try:
+ return {'security_group_rule':
+ self._fake_security_group_rules[security_group_rule]}
+ except KeyError:
+ msg = 'Security Group rule %s not found' % security_group_rule
+ raise n_exc.NeutronClientException(message=msg, status_code=404)
+
+ def show_network(self, network, **_params):
+ try:
+ return {'network':
+ self._fake_networks[network]}
+ except KeyError:
+ msg = 'Network %s not found' % network
+ raise n_exc.NeutronClientException(message=msg, status_code=404)
+
+ def show_port(self, port, **_params):
+ try:
+ return {'port':
+ self._fake_ports[port]}
+ except KeyError:
+ msg = 'Port %s not found' % port
+ raise n_exc.NeutronClientException(message=msg, status_code=404)
+
+ def show_subnet(self, subnet, **_params):
+ try:
+ return {'subnet':
+ self._fake_subnets[subnet]}
+ except KeyError:
+ msg = 'Port %s not found' % subnet
+ raise n_exc.NeutronClientException(message=msg, status_code=404)
+
+ def list_security_groups(self, **_params):
+ ret = []
+ for security_group in self._fake_security_groups.values():
+ names = _params.get('name')
+ if names:
+ if not isinstance(names, list):
+ names = [names]
+ for name in names:
+ if security_group.get('name') == name:
+ ret.append(security_group)
+ ids = _params.get('id')
+ if ids:
+ if not isinstance(ids, list):
+ ids = [ids]
+ for id in ids:
+ if security_group.get('id') == id:
+ ret.append(security_group)
+ elif not (names or ids):
+ ret.append(security_group)
+ return {'security_groups': ret}
+
+ def list_networks(self, **_params):
+ # neutronv2/api.py _get_available_networks calls this assuming
+ # search_opts filter "shared" is implemented and not ignored
+ shared = _params.get("shared", None)
+ if shared:
+ return {'networks': []}
+ else:
+ return {'networks':
+ [network for network in self._fake_networks.values()]}
+
+ def list_ports(self, **_params):
+ ret = []
+ device_id = _params.get('device_id')
+ for port in self._fake_ports.values():
+ if device_id:
+ if port['device_id'] in device_id:
+ ret.append(port)
+ else:
+ ret.append(port)
+ return {'ports': ret}
+
+ def list_subnets(self, **_params):
+ return {'subnets':
+ [subnet for subnet in self._fake_subnets.values()]}
+
+ def list_floatingips(self, **_params):
+ return {'floatingips': []}
+
+ def delete_security_group(self, security_group):
+ self.show_security_group(security_group)
+ ports = self.list_ports()
+ for port in ports.get('ports'):
+ for sg_port in port['security_groups']:
+ if sg_port == security_group:
+ msg = ('Unable to delete Security group %s in use'
+ % security_group)
+ raise n_exc.NeutronClientException(message=msg,
+ status_code=409)
+ del self._fake_security_groups[security_group]
+
+ def delete_security_group_rule(self, security_group_rule):
+ self.show_security_group_rule(security_group_rule)
+ del self._fake_security_group_rules[security_group_rule]
+
+ def delete_network(self, network):
+ self.show_network(network)
+ self._check_ports_on_network(network)
+ for subnet in self._fake_subnets.values():
+ if subnet['network_id'] == network:
+ del self._fake_subnets[subnet['id']]
+ del self._fake_networks[network]
+
+ def delete_subnet(self, subnet):
+ subnet = self.show_subnet(subnet).get('subnet')
+ self._check_ports_on_network(subnet['network_id'])
+ del self._fake_subnet[subnet]
+
+ def delete_port(self, port):
+ self.show_port(port)
+ del self._fake_ports[port]
+
+ def update_port(self, port, body=None):
+ self.show_port(port)
+ self._fake_ports[port].update(body['port'])
+ return {'port': self._fake_ports[port]}
+
+ def list_extensions(self, **_parms):
+ return {'extensions': []}
+
+ def _check_ports_on_network(self, network):
+ ports = self.list_ports()
+ for port in ports:
+ if port['network_id'] == network:
+ msg = ('Unable to complete operation on network %s. There is '
+ 'one or more ports still in use on the network'
+ % network)
+ raise n_exc.NeutronClientException(message=msg, status_code=409)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py b/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py
new file mode 100644
index 0000000000..228b44f369
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py
@@ -0,0 +1,222 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+import webob
+
+from nova.api.openstack.compute.contrib import quota_classes
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+def quota_set(class_name):
+ return {'quota_class_set': {'id': class_name, 'metadata_items': 128,
+ 'ram': 51200, 'floating_ips': 10,
+ 'fixed_ips': -1, 'instances': 10,
+ 'injected_files': 5, 'cores': 20,
+ 'injected_file_content_bytes': 10240,
+ 'security_groups': 10,
+ 'security_group_rules': 20, 'key_pairs': 100,
+ 'injected_file_path_bytes': 255}}
+
+
+class QuotaClassSetsTest(test.TestCase):
+
+ def setUp(self):
+ super(QuotaClassSetsTest, self).setUp()
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = quota_classes.QuotaClassSetsController(self.ext_mgr)
+
+ def test_format_quota_set(self):
+ raw_quota_set = {
+ 'instances': 10,
+ 'cores': 20,
+ 'ram': 51200,
+ 'floating_ips': 10,
+ 'fixed_ips': -1,
+ 'metadata_items': 128,
+ 'injected_files': 5,
+ 'injected_file_path_bytes': 255,
+ 'injected_file_content_bytes': 10240,
+ 'security_groups': 10,
+ 'security_group_rules': 20,
+ 'key_pairs': 100,
+ }
+
+ quota_set = self.controller._format_quota_set('test_class',
+ raw_quota_set)
+ qs = quota_set['quota_class_set']
+
+ self.assertEqual(qs['id'], 'test_class')
+ self.assertEqual(qs['instances'], 10)
+ self.assertEqual(qs['cores'], 20)
+ self.assertEqual(qs['ram'], 51200)
+ self.assertEqual(qs['floating_ips'], 10)
+ self.assertEqual(qs['fixed_ips'], -1)
+ self.assertEqual(qs['metadata_items'], 128)
+ self.assertEqual(qs['injected_files'], 5)
+ self.assertEqual(qs['injected_file_path_bytes'], 255)
+ self.assertEqual(qs['injected_file_content_bytes'], 10240)
+ self.assertEqual(qs['security_groups'], 10)
+ self.assertEqual(qs['security_group_rules'], 20)
+ self.assertEqual(qs['key_pairs'], 100)
+
+ def test_quotas_show_as_admin(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class',
+ use_admin_context=True)
+ res_dict = self.controller.show(req, 'test_class')
+
+ self.assertEqual(res_dict, quota_set('test_class'))
+
+ def test_quotas_show_as_unauthorized_user(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class')
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
+ req, 'test_class')
+
+ def test_quotas_update_as_admin(self):
+ body = {'quota_class_set': {'instances': 50, 'cores': 50,
+ 'ram': 51200, 'floating_ips': 10,
+ 'fixed_ips': -1, 'metadata_items': 128,
+ 'injected_files': 5,
+ 'injected_file_content_bytes': 10240,
+ 'injected_file_path_bytes': 255,
+ 'security_groups': 10,
+ 'security_group_rules': 20,
+ 'key_pairs': 100}}
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class',
+ use_admin_context=True)
+ res_dict = self.controller.update(req, 'test_class', body)
+
+ self.assertEqual(res_dict, body)
+
+ def test_quotas_update_as_user(self):
+ body = {'quota_class_set': {'instances': 50, 'cores': 50,
+ 'ram': 51200, 'floating_ips': 10,
+ 'fixed_ips': -1, 'metadata_items': 128,
+ 'injected_files': 5,
+ 'injected_file_content_bytes': 10240,
+ 'security_groups': 10,
+ 'security_group_rules': 20,
+ 'key_pairs': 100,
+ }}
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class')
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
+ req, 'test_class', body)
+
+ def test_quotas_update_with_empty_body(self):
+ body = {}
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 'test_class', body)
+
+ def test_quotas_update_with_non_integer(self):
+ body = {'quota_class_set': {'instances': "abc"}}
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 'test_class', body)
+
+ body = {'quota_class_set': {'instances': 50.5}}
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 'test_class', body)
+
+ body = {'quota_class_set': {
+ 'instances': u'\u30aa\u30fc\u30d7\u30f3'}}
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 'test_class', body)
+
+
+class QuotaTemplateXMLSerializerTest(test.TestCase):
+ def setUp(self):
+ super(QuotaTemplateXMLSerializerTest, self).setUp()
+ self.serializer = quota_classes.QuotaClassTemplate()
+ self.deserializer = wsgi.XMLDeserializer()
+
+ def test_serializer(self):
+ exemplar = dict(quota_class_set=dict(
+ id='test_class',
+ metadata_items=10,
+ injected_file_path_bytes=255,
+ injected_file_content_bytes=20,
+ ram=50,
+ floating_ips=60,
+ fixed_ips=-1,
+ instances=70,
+ injected_files=80,
+ security_groups=10,
+ security_group_rules=20,
+ key_pairs=100,
+ cores=90))
+ text = self.serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('quota_class_set', tree.tag)
+ self.assertEqual('test_class', tree.get('id'))
+ self.assertEqual(len(exemplar['quota_class_set']) - 1, len(tree))
+ for child in tree:
+ self.assertIn(child.tag, exemplar['quota_class_set'])
+ self.assertEqual(int(child.text),
+ exemplar['quota_class_set'][child.tag])
+
+ def test_deserializer(self):
+ exemplar = dict(quota_class_set=dict(
+ metadata_items='10',
+ injected_file_content_bytes='20',
+ ram='50',
+ floating_ips='60',
+ fixed_ips='-1',
+ instances='70',
+ injected_files='80',
+ security_groups='10',
+ security_group_rules='20',
+ key_pairs='100',
+ cores='90'))
+ intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<quota_class_set>'
+ '<metadata_items>10</metadata_items>'
+ '<injected_file_content_bytes>20'
+ '</injected_file_content_bytes>'
+ '<ram>50</ram>'
+ '<floating_ips>60</floating_ips>'
+ '<fixed_ips>-1</fixed_ips>'
+ '<instances>70</instances>'
+ '<injected_files>80</injected_files>'
+ '<cores>90</cores>'
+ '<security_groups>10</security_groups>'
+ '<security_group_rules>20</security_group_rules>'
+ '<key_pairs>100</key_pairs>'
+ '</quota_class_set>')
+
+ result = self.deserializer.deserialize(intext)['body']
+ self.assertEqual(result, exemplar)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_quotas.py b/nova/tests/unit/api/openstack/compute/contrib/test_quotas.py
new file mode 100644
index 0000000000..33511b0cc3
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_quotas.py
@@ -0,0 +1,648 @@
+# Copyright 2011 OpenStack Foundation
+# Copyright 2013 IBM Corp.
+# 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 copy
+
+from lxml import etree
+import mock
+import webob
+
+from nova.api.openstack.compute.contrib import quotas as quotas_v2
+from nova.api.openstack.compute.plugins.v3 import quota_sets as quotas_v21
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova import context as context_maker
+from nova import exception
+from nova import quota
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+def quota_set(id, include_server_group_quotas=True):
+ res = {'quota_set': {'id': id, 'metadata_items': 128,
+ 'ram': 51200, 'floating_ips': 10, 'fixed_ips': -1,
+ 'instances': 10, 'injected_files': 5, 'cores': 20,
+ 'injected_file_content_bytes': 10240,
+ 'security_groups': 10, 'security_group_rules': 20,
+ 'key_pairs': 100, 'injected_file_path_bytes': 255}}
+ if include_server_group_quotas:
+ res['quota_set']['server_groups'] = 10
+ res['quota_set']['server_group_members'] = 10
+ return res
+
+
+class BaseQuotaSetsTest(test.TestCase):
+
+ def _is_v20_api_test(self):
+ # NOTE(oomichi): If a test is for v2.0 API, this method returns
+ # True. Otherwise(v2.1 API test), returns False.
+ return (self.plugin == quotas_v2)
+
+ def get_update_expected_response(self, base_body):
+ # NOTE(oomichi): "id" parameter is added to a response of
+ # "update quota" API since v2.1 API, because it makes the
+ # API consistent and it is not backwards incompatible change.
+ # This method adds "id" for an expected body of a response.
+ if self._is_v20_api_test():
+ expected_body = base_body
+ else:
+ expected_body = copy.deepcopy(base_body)
+ expected_body['quota_set'].update({'id': 'update_me'})
+ return expected_body
+
+ def setup_mock_for_show(self):
+ if self._is_v20_api_test():
+ self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
+ self.mox.ReplayAll()
+
+ def setup_mock_for_update(self):
+ if self._is_v20_api_test():
+ self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
+ self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
+ self.mox.ReplayAll()
+
+ def get_delete_status_int(self, res):
+ if self._is_v20_api_test():
+ return res.status_int
+ else:
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ return self.controller.delete.wsgi_code
+
+
+class QuotaSetsTestV21(BaseQuotaSetsTest):
+ plugin = quotas_v21
+ validation_error = exception.ValidationError
+ include_server_group_quotas = True
+
+ def setUp(self):
+ super(QuotaSetsTestV21, self).setUp()
+ self._setup_controller()
+ self.default_quotas = {
+ 'instances': 10,
+ 'cores': 20,
+ 'ram': 51200,
+ 'floating_ips': 10,
+ 'fixed_ips': -1,
+ 'metadata_items': 128,
+ 'injected_files': 5,
+ 'injected_file_path_bytes': 255,
+ 'injected_file_content_bytes': 10240,
+ 'security_groups': 10,
+ 'security_group_rules': 20,
+ 'key_pairs': 100,
+ }
+ if self.include_server_group_quotas:
+ self.default_quotas['server_groups'] = 10
+ self.default_quotas['server_group_members'] = 10
+
+ def _setup_controller(self):
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
+
+ def test_format_quota_set(self):
+ quota_set = self.controller._format_quota_set('1234',
+ self.default_quotas)
+ qs = quota_set['quota_set']
+
+ self.assertEqual(qs['id'], '1234')
+ self.assertEqual(qs['instances'], 10)
+ self.assertEqual(qs['cores'], 20)
+ self.assertEqual(qs['ram'], 51200)
+ self.assertEqual(qs['floating_ips'], 10)
+ self.assertEqual(qs['fixed_ips'], -1)
+ self.assertEqual(qs['metadata_items'], 128)
+ self.assertEqual(qs['injected_files'], 5)
+ self.assertEqual(qs['injected_file_path_bytes'], 255)
+ self.assertEqual(qs['injected_file_content_bytes'], 10240)
+ self.assertEqual(qs['security_groups'], 10)
+ self.assertEqual(qs['security_group_rules'], 20)
+ self.assertEqual(qs['key_pairs'], 100)
+ if self.include_server_group_quotas:
+ self.assertEqual(qs['server_groups'], 10)
+ self.assertEqual(qs['server_group_members'], 10)
+
+ def test_quotas_defaults(self):
+ uri = '/v2/fake_tenant/os-quota-sets/fake_tenant/defaults'
+
+ req = fakes.HTTPRequest.blank(uri)
+ res_dict = self.controller.defaults(req, 'fake_tenant')
+ self.default_quotas.update({'id': 'fake_tenant'})
+ expected = {'quota_set': self.default_quotas}
+
+ self.assertEqual(res_dict, expected)
+
+ def test_quotas_show_as_admin(self):
+ self.setup_mock_for_show()
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234',
+ use_admin_context=True)
+ res_dict = self.controller.show(req, 1234)
+
+ ref_quota_set = quota_set('1234', self.include_server_group_quotas)
+ self.assertEqual(res_dict, ref_quota_set)
+
+ def test_quotas_show_as_unauthorized_user(self):
+ self.setup_mock_for_show()
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234')
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
+ req, 1234)
+
+ def test_quotas_update_as_admin(self):
+ self.setup_mock_for_update()
+ self.default_quotas.update({
+ 'instances': 50,
+ 'cores': 50
+ })
+ body = {'quota_set': self.default_quotas}
+ expected_body = self.get_update_expected_response(body)
+
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
+ use_admin_context=True)
+ res_dict = self.controller.update(req, 'update_me', body=body)
+ self.assertEqual(expected_body, res_dict)
+
+ def test_quotas_update_zero_value_as_admin(self):
+ self.setup_mock_for_update()
+ body = {'quota_set': {'instances': 0, 'cores': 0,
+ 'ram': 0, 'floating_ips': 0,
+ 'metadata_items': 0,
+ 'injected_files': 0,
+ 'injected_file_content_bytes': 0,
+ 'injected_file_path_bytes': 0,
+ 'security_groups': 0,
+ 'security_group_rules': 0,
+ 'key_pairs': 100, 'fixed_ips': -1}}
+ if self.include_server_group_quotas:
+ body['quota_set']['server_groups'] = 10
+ body['quota_set']['server_group_members'] = 10
+ expected_body = self.get_update_expected_response(body)
+
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
+ use_admin_context=True)
+ res_dict = self.controller.update(req, 'update_me', body=body)
+ self.assertEqual(expected_body, res_dict)
+
+ def test_quotas_update_as_user(self):
+ self.setup_mock_for_update()
+ self.default_quotas.update({
+ 'instances': 50,
+ 'cores': 50
+ })
+ body = {'quota_set': self.default_quotas}
+
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me')
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
+ req, 'update_me', body=body)
+
+ def _quotas_update_bad_request_case(self, body):
+ self.setup_mock_for_update()
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
+ use_admin_context=True)
+ self.assertRaises(self.validation_error, self.controller.update,
+ req, 'update_me', body=body)
+
+ def test_quotas_update_invalid_key(self):
+ body = {'quota_set': {'instances2': -2, 'cores': -2,
+ 'ram': -2, 'floating_ips': -2,
+ 'metadata_items': -2, 'injected_files': -2,
+ 'injected_file_content_bytes': -2}}
+ self._quotas_update_bad_request_case(body)
+
+ def test_quotas_update_invalid_limit(self):
+ body = {'quota_set': {'instances': -2, 'cores': -2,
+ 'ram': -2, 'floating_ips': -2, 'fixed_ips': -2,
+ 'metadata_items': -2, 'injected_files': -2,
+ 'injected_file_content_bytes': -2}}
+ self._quotas_update_bad_request_case(body)
+
+ def test_quotas_update_empty_body(self):
+ body = {}
+ self._quotas_update_bad_request_case(body)
+
+ def test_quotas_update_invalid_value_non_int(self):
+ # when PUT non integer value
+ self.default_quotas.update({
+ 'instances': 'test'
+ })
+ body = {'quota_set': self.default_quotas}
+ self._quotas_update_bad_request_case(body)
+
+ def test_quotas_update_invalid_value_with_float(self):
+ # when PUT non integer value
+ self.default_quotas.update({
+ 'instances': 50.5
+ })
+ body = {'quota_set': self.default_quotas}
+ self._quotas_update_bad_request_case(body)
+
+ def test_quotas_update_invalid_value_with_unicode(self):
+ # when PUT non integer value
+ self.default_quotas.update({
+ 'instances': u'\u30aa\u30fc\u30d7\u30f3'
+ })
+ body = {'quota_set': self.default_quotas}
+ self._quotas_update_bad_request_case(body)
+
+ def test_quotas_delete_as_unauthorized_user(self):
+ if self._is_v20_api_test():
+ self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
+ self.mox.ReplayAll()
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234')
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
+ req, 1234)
+
+ def test_quotas_delete_as_admin(self):
+ if self._is_v20_api_test():
+ self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
+ context = context_maker.get_admin_context()
+ self.req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234')
+ self.req.environ['nova.context'] = context
+ self.mox.StubOutWithMock(quota.QUOTAS,
+ "destroy_all_by_project")
+ quota.QUOTAS.destroy_all_by_project(context, 1234)
+ self.mox.ReplayAll()
+ res = self.controller.delete(self.req, 1234)
+ self.mox.VerifyAll()
+ self.assertEqual(202, self.get_delete_status_int(res))
+
+
+class QuotaXMLSerializerTest(test.TestCase):
+ def setUp(self):
+ super(QuotaXMLSerializerTest, self).setUp()
+ self.serializer = quotas_v2.QuotaTemplate()
+ self.deserializer = wsgi.XMLDeserializer()
+
+ def test_serializer(self):
+ exemplar = dict(quota_set=dict(
+ id='project_id',
+ metadata_items=10,
+ injected_file_path_bytes=255,
+ injected_file_content_bytes=20,
+ ram=50,
+ floating_ips=60,
+ fixed_ips=-1,
+ instances=70,
+ injected_files=80,
+ security_groups=10,
+ security_group_rules=20,
+ key_pairs=100,
+ cores=90))
+ text = self.serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('quota_set', tree.tag)
+ self.assertEqual('project_id', tree.get('id'))
+ self.assertEqual(len(exemplar['quota_set']) - 1, len(tree))
+ for child in tree:
+ self.assertIn(child.tag, exemplar['quota_set'])
+ self.assertEqual(int(child.text), exemplar['quota_set'][child.tag])
+
+ def test_deserializer(self):
+ exemplar = dict(quota_set=dict(
+ metadata_items='10',
+ injected_file_content_bytes='20',
+ ram='50',
+ floating_ips='60',
+ fixed_ips='-1',
+ instances='70',
+ injected_files='80',
+ security_groups='10',
+ security_group_rules='20',
+ key_pairs='100',
+ cores='90'))
+ intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<quota_set>'
+ '<metadata_items>10</metadata_items>'
+ '<injected_file_content_bytes>20'
+ '</injected_file_content_bytes>'
+ '<ram>50</ram>'
+ '<floating_ips>60</floating_ips>'
+ '<fixed_ips>-1</fixed_ips>'
+ '<instances>70</instances>'
+ '<injected_files>80</injected_files>'
+ '<security_groups>10</security_groups>'
+ '<security_group_rules>20</security_group_rules>'
+ '<key_pairs>100</key_pairs>'
+ '<cores>90</cores>'
+ '</quota_set>')
+
+ result = self.deserializer.deserialize(intext)['body']
+ self.assertEqual(result, exemplar)
+
+
+class ExtendedQuotasTestV21(BaseQuotaSetsTest):
+ plugin = quotas_v21
+
+ def setUp(self):
+ super(ExtendedQuotasTestV21, self).setUp()
+ self._setup_controller()
+ self.setup_mock_for_update()
+
+ fake_quotas = {'ram': {'limit': 51200,
+ 'in_use': 12800,
+ 'reserved': 12800},
+ 'cores': {'limit': 20,
+ 'in_use': 10,
+ 'reserved': 5},
+ 'instances': {'limit': 100,
+ 'in_use': 0,
+ 'reserved': 0}}
+
+ def _setup_controller(self):
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
+
+ def fake_get_quotas(self, context, id, user_id=None, usages=False):
+ if usages:
+ return self.fake_quotas
+ else:
+ return dict((k, v['limit']) for k, v in self.fake_quotas.items())
+
+ def fake_get_settable_quotas(self, context, project_id, user_id=None):
+ return {
+ 'ram': {'minimum': self.fake_quotas['ram']['in_use'] +
+ self.fake_quotas['ram']['reserved'],
+ 'maximum': -1},
+ 'cores': {'minimum': self.fake_quotas['cores']['in_use'] +
+ self.fake_quotas['cores']['reserved'],
+ 'maximum': -1},
+ 'instances': {'minimum': self.fake_quotas['instances']['in_use'] +
+ self.fake_quotas['instances']['reserved'],
+ 'maximum': -1},
+ }
+
+ def test_quotas_update_exceed_in_used(self):
+ patcher = mock.patch.object(quota.QUOTAS, 'get_settable_quotas')
+ get_settable_quotas = patcher.start()
+
+ body = {'quota_set': {'cores': 10}}
+
+ get_settable_quotas.side_effect = self.fake_get_settable_quotas
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 'update_me', body=body)
+ mock.patch.stopall()
+
+ def test_quotas_force_update_exceed_in_used(self):
+ patcher = mock.patch.object(quota.QUOTAS, 'get_settable_quotas')
+ get_settable_quotas = patcher.start()
+ patcher = mock.patch.object(self.plugin.QuotaSetsController,
+ '_get_quotas')
+ _get_quotas = patcher.start()
+
+ body = {'quota_set': {'cores': 10, 'force': 'True'}}
+
+ get_settable_quotas.side_effect = self.fake_get_settable_quotas
+ _get_quotas.side_effect = self.fake_get_quotas
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
+ use_admin_context=True)
+ self.controller.update(req, 'update_me', body=body)
+ mock.patch.stopall()
+
+
+class UserQuotasTestV21(BaseQuotaSetsTest):
+ plugin = quotas_v21
+ include_server_group_quotas = True
+
+ def setUp(self):
+ super(UserQuotasTestV21, self).setUp()
+ self._setup_controller()
+
+ def _setup_controller(self):
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
+
+ def test_user_quotas_show_as_admin(self):
+ self.setup_mock_for_show()
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1',
+ use_admin_context=True)
+ res_dict = self.controller.show(req, 1234)
+ ref_quota_set = quota_set('1234', self.include_server_group_quotas)
+ self.assertEqual(res_dict, ref_quota_set)
+
+ def test_user_quotas_show_as_unauthorized_user(self):
+ self.setup_mock_for_show()
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1')
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
+ req, 1234)
+
+ def test_user_quotas_update_as_admin(self):
+ self.setup_mock_for_update()
+ body = {'quota_set': {'instances': 10, 'cores': 20,
+ 'ram': 51200, 'floating_ips': 10,
+ 'fixed_ips': -1, 'metadata_items': 128,
+ 'injected_files': 5,
+ 'injected_file_content_bytes': 10240,
+ 'injected_file_path_bytes': 255,
+ 'security_groups': 10,
+ 'security_group_rules': 20,
+ 'key_pairs': 100}}
+ if self.include_server_group_quotas:
+ body['quota_set']['server_groups'] = 10
+ body['quota_set']['server_group_members'] = 10
+
+ expected_body = self.get_update_expected_response(body)
+
+ url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
+ req = fakes.HTTPRequest.blank(url, use_admin_context=True)
+ res_dict = self.controller.update(req, 'update_me', body=body)
+
+ self.assertEqual(expected_body, res_dict)
+
+ def test_user_quotas_update_as_user(self):
+ self.setup_mock_for_update()
+ body = {'quota_set': {'instances': 10, 'cores': 20,
+ 'ram': 51200, 'floating_ips': 10,
+ 'fixed_ips': -1, 'metadata_items': 128,
+ 'injected_files': 5,
+ 'injected_file_content_bytes': 10240,
+ 'security_groups': 10,
+ 'security_group_rules': 20,
+ 'key_pairs': 100,
+ 'server_groups': 10,
+ 'server_group_members': 10}}
+
+ url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
+ req = fakes.HTTPRequest.blank(url)
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
+ req, 'update_me', body=body)
+
+ def test_user_quotas_update_exceed_project(self):
+ self.setup_mock_for_update()
+ body = {'quota_set': {'instances': 20}}
+
+ url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
+ req = fakes.HTTPRequest.blank(url, use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 'update_me', body=body)
+
+ def test_user_quotas_delete_as_unauthorized_user(self):
+ self.setup_mock_for_update()
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1')
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
+ req, 1234)
+
+ def test_user_quotas_delete_as_admin(self):
+ if self._is_v20_api_test():
+ self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
+ self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True)
+ context = context_maker.get_admin_context()
+ url = '/v2/fake4/os-quota-sets/1234?user_id=1'
+ self.req = fakes.HTTPRequest.blank(url)
+ self.req.environ['nova.context'] = context
+ self.mox.StubOutWithMock(quota.QUOTAS,
+ "destroy_all_by_project_and_user")
+ quota.QUOTAS.destroy_all_by_project_and_user(context, 1234, '1')
+ self.mox.ReplayAll()
+ res = self.controller.delete(self.req, 1234)
+ self.mox.VerifyAll()
+ self.assertEqual(202, self.get_delete_status_int(res))
+
+
+class QuotaSetsTestV2(QuotaSetsTestV21):
+ plugin = quotas_v2
+ validation_error = webob.exc.HTTPBadRequest
+
+ def _setup_controller(self):
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes().\
+ AndReturn(self.include_server_group_quotas)
+ self.mox.ReplayAll()
+ self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
+ self.mox.ResetAll()
+
+ # NOTE: The following tests are tricky and v2.1 API does not allow
+ # this kind of input by strong input validation. Just for test coverage,
+ # we keep them now.
+ def test_quotas_update_invalid_value_json_fromat_empty_string(self):
+ self.setup_mock_for_update()
+ self.default_quotas.update({
+ 'instances': 50,
+ 'cores': 50
+ })
+ expected_resp = {'quota_set': self.default_quotas}
+
+ # when PUT JSON format with empty string for quota
+ body = copy.deepcopy(expected_resp)
+ body['quota_set']['ram'] = ''
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
+ use_admin_context=True)
+ res_dict = self.controller.update(req, 'update_me', body)
+ self.assertEqual(res_dict, expected_resp)
+
+ def test_quotas_update_invalid_value_xml_fromat_empty_string(self):
+ self.default_quotas.update({
+ 'instances': 50,
+ 'cores': 50
+ })
+ expected_resp = {'quota_set': self.default_quotas}
+
+ # when PUT XML format with empty string for quota
+ body = copy.deepcopy(expected_resp)
+ body['quota_set']['ram'] = {}
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
+ use_admin_context=True)
+ self.setup_mock_for_update()
+ res_dict = self.controller.update(req, 'update_me', body)
+ self.assertEqual(res_dict, expected_resp)
+
+ # NOTE: os-extended-quotas and os-user-quotas are only for v2.0.
+ # On v2.1, these features are always enable. So we need the following
+ # tests only for v2.0.
+ def test_delete_quotas_when_extension_not_loaded(self):
+ self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(False)
+ self.mox.ReplayAll()
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, 1234)
+
+ def test_delete_user_quotas_when_extension_not_loaded(self):
+ self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True)
+ self.ext_mgr.is_loaded('os-user-quotas').AndReturn(False)
+ self.mox.ReplayAll()
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, 1234)
+
+
+class QuotaSetsTestV2WithoutServerGroupQuotas(QuotaSetsTestV2):
+ include_server_group_quotas = False
+
+ # NOTE: os-server-group-quotas is only for v2.0. On v2.1 this feature
+ # is always enabled, so this test is only needed for v2.0
+ def test_quotas_update_without_server_group_quotas_extenstion(self):
+ self.setup_mock_for_update()
+ self.default_quotas.update({
+ 'server_groups': 50,
+ 'sever_group_members': 50
+ })
+ body = {'quota_set': self.default_quotas}
+
+ req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 'update_me', body=body)
+
+
+class ExtendedQuotasTestV2(ExtendedQuotasTestV21):
+ plugin = quotas_v2
+
+ def _setup_controller(self):
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes().\
+ AndReturn(False)
+ self.mox.ReplayAll()
+ self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
+ self.mox.ResetAll()
+
+
+class UserQuotasTestV2(UserQuotasTestV21):
+ plugin = quotas_v2
+
+ def _setup_controller(self):
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes().\
+ AndReturn(self.include_server_group_quotas)
+ self.mox.ReplayAll()
+ self.controller = self.plugin.QuotaSetsController(self.ext_mgr)
+ self.mox.ResetAll()
+
+
+class UserQuotasTestV2WithoutServerGroupQuotas(UserQuotasTestV2):
+ include_server_group_quotas = False
+
+ # NOTE: os-server-group-quotas is only for v2.0. On v2.1 this feature
+ # is always enabled, so this test is only needed for v2.0
+ def test_user_quotas_update_as_admin_without_sg_quota_extension(self):
+ self.setup_mock_for_update()
+ body = {'quota_set': {'instances': 10, 'cores': 20,
+ 'ram': 51200, 'floating_ips': 10,
+ 'fixed_ips': -1, 'metadata_items': 128,
+ 'injected_files': 5,
+ 'injected_file_content_bytes': 10240,
+ 'injected_file_path_bytes': 255,
+ 'security_groups': 10,
+ 'security_group_rules': 20,
+ 'key_pairs': 100,
+ 'server_groups': 100,
+ 'server_group_members': 200}}
+
+ url = '/v2/fake4/os-quota-sets/update_me?user_id=1'
+ req = fakes.HTTPRequest.blank(url, use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, 'update_me', body=body)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_rescue.py b/nova/tests/unit/api/openstack/compute/contrib/test_rescue.py
new file mode 100644
index 0000000000..f8de7de291
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_rescue.py
@@ -0,0 +1,270 @@
+# Copyright 2011 OpenStack Foundation
+#
+# 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 mock
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova import compute
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+CONF = cfg.CONF
+CONF.import_opt('password_length', 'nova.utils')
+
+
+def rescue(self, context, instance, rescue_password=None,
+ rescue_image_ref=None):
+ pass
+
+
+def unrescue(self, context, instance):
+ pass
+
+
+def fake_compute_get(*args, **kwargs):
+ uuid = '70f6db34-de8d-4fbd-aafb-4065bdfa6114'
+ return {'id': 1, 'uuid': uuid}
+
+
+class RescueTestV21(test.NoDBTestCase):
+ _prefix = '/v2/fake'
+
+ def setUp(self):
+ super(RescueTestV21, self).setUp()
+
+ self.stubs.Set(compute.api.API, "get", fake_compute_get)
+ self.stubs.Set(compute.api.API, "rescue", rescue)
+ self.stubs.Set(compute.api.API, "unrescue", unrescue)
+ self.app = self._get_app()
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21(init_only=('servers', 'os-rescue'))
+
+ def test_rescue_from_locked_server(self):
+ def fake_rescue_from_locked_server(self, context,
+ instance, rescue_password=None, rescue_image_ref=None):
+ raise exception.InstanceIsLocked(instance_uuid=instance['uuid'])
+
+ self.stubs.Set(compute.api.API,
+ 'rescue',
+ fake_rescue_from_locked_server)
+ body = {"rescue": {"adminPass": "AABBCC112233"}}
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 409)
+
+ def test_rescue_with_preset_password(self):
+ body = {"rescue": {"adminPass": "AABBCC112233"}}
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+ resp_json = jsonutils.loads(resp.body)
+ self.assertEqual("AABBCC112233", resp_json['adminPass'])
+
+ def test_rescue_generates_password(self):
+ body = dict(rescue=None)
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+ resp_json = jsonutils.loads(resp.body)
+ self.assertEqual(CONF.password_length, len(resp_json['adminPass']))
+
+ def test_rescue_of_rescued_instance(self):
+ body = dict(rescue=None)
+
+ def fake_rescue(*args, **kwargs):
+ raise exception.InstanceInvalidState('fake message')
+
+ self.stubs.Set(compute.api.API, "rescue", fake_rescue)
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 409)
+
+ def test_unrescue(self):
+ body = dict(unrescue=None)
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 202)
+
+ def test_unrescue_from_locked_server(self):
+ def fake_unrescue_from_locked_server(self, context,
+ instance):
+ raise exception.InstanceIsLocked(instance_uuid=instance['uuid'])
+
+ self.stubs.Set(compute.api.API,
+ 'unrescue',
+ fake_unrescue_from_locked_server)
+
+ body = dict(unrescue=None)
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 409)
+
+ def test_unrescue_of_active_instance(self):
+ body = dict(unrescue=None)
+
+ def fake_unrescue(*args, **kwargs):
+ raise exception.InstanceInvalidState('fake message')
+
+ self.stubs.Set(compute.api.API, "unrescue", fake_unrescue)
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 409)
+
+ def test_rescue_raises_unrescuable(self):
+ body = dict(rescue=None)
+
+ def fake_rescue(*args, **kwargs):
+ raise exception.InstanceNotRescuable('fake message')
+
+ self.stubs.Set(compute.api.API, "rescue", fake_rescue)
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 400)
+
+ @mock.patch('nova.compute.api.API.rescue')
+ def test_rescue_with_image_specified(self, mock_compute_api_rescue):
+ instance = fake_compute_get()
+ body = {"rescue": {"adminPass": "ABC123",
+ "rescue_image_ref": "img-id"}}
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+ resp_json = jsonutils.loads(resp.body)
+ self.assertEqual("ABC123", resp_json['adminPass'])
+
+ mock_compute_api_rescue.assert_called_with(mock.ANY, instance,
+ rescue_password=u'ABC123',
+ rescue_image_ref=u'img-id')
+
+ @mock.patch('nova.compute.api.API.rescue')
+ def test_rescue_without_image_specified(self, mock_compute_api_rescue):
+ instance = fake_compute_get()
+ body = {"rescue": {"adminPass": "ABC123"}}
+
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+ resp_json = jsonutils.loads(resp.body)
+ self.assertEqual("ABC123", resp_json['adminPass'])
+
+ mock_compute_api_rescue.assert_called_with(mock.ANY, instance,
+ rescue_password=u'ABC123',
+ rescue_image_ref=None)
+
+ def test_rescue_with_none(self):
+ body = dict(rescue=None)
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(200, resp.status_int)
+
+ def test_rescue_with_empty_dict(self):
+ body = dict(rescue=dict())
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(200, resp.status_int)
+
+ def test_rescue_disable_password(self):
+ self.flags(enable_instance_password=False)
+ body = dict(rescue=None)
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(200, resp.status_int)
+ resp_json = jsonutils.loads(resp.body)
+ self.assertNotIn('adminPass', resp_json)
+
+ def test_rescue_with_invalid_property(self):
+ body = {"rescue": {"test": "test"}}
+ req = webob.Request.blank(self._prefix + '/servers/test_inst/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ resp = req.get_response(self.app)
+ self.assertEqual(400, resp.status_int)
+
+
+class RescueTestV20(RescueTestV21):
+
+ def _get_app(self):
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=None)
+ return fakes.wsgi_app(init_only=('servers',))
+
+ def test_rescue_with_invalid_property(self):
+ # NOTE(cyeoh): input validation in original v2 code does not
+ # check for invalid properties.
+ pass
+
+ def test_rescue_disable_password(self):
+ # NOTE(cyeoh): Original v2.0 code does not support disabling
+ # the admin password being returned through a conf setting
+ pass
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_scheduler_hints.py b/nova/tests/unit/api/openstack/compute/contrib/test_scheduler_hints.py
new file mode 100644
index 0000000000..fba3a02eec
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_scheduler_hints.py
@@ -0,0 +1,220 @@
+# Copyright 2011 OpenStack Foundation
+# 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 datetime
+
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+
+from nova.api.openstack import compute
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import servers as servers_v21
+from nova.api.openstack.compute import servers as servers_v2
+from nova.api.openstack import extensions
+import nova.compute.api
+from nova.compute import flavors
+from nova import db
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+from nova.tests.unit.image import fake
+
+
+UUID = fakes.FAKE_UUID
+
+
+CONF = cfg.CONF
+
+
+class SchedulerHintsTestCaseV21(test.TestCase):
+
+ def setUp(self):
+ super(SchedulerHintsTestCaseV21, self).setUp()
+ self.fake_instance = fakes.stub_instance(1, uuid=UUID)
+ self._set_up_router()
+
+ def _set_up_router(self):
+ self.app = compute.APIRouterV3(init_only=('servers',
+ 'os-scheduler-hints'))
+
+ def _get_request(self):
+ return fakes.HTTPRequestV3.blank('/servers')
+
+ def test_create_server_without_hints(self):
+
+ def fake_create(*args, **kwargs):
+ self.assertEqual(kwargs['scheduler_hints'], {})
+ return ([self.fake_instance], '')
+
+ self.stubs.Set(nova.compute.api.API, 'create', fake_create)
+
+ req = self._get_request()
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'server': {
+ 'name': 'server_test',
+ 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175',
+ 'flavorRef': '1',
+ }}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(202, res.status_int)
+
+ def test_create_server_with_hints(self):
+
+ def fake_create(*args, **kwargs):
+ self.assertEqual(kwargs['scheduler_hints'], {'a': 'b'})
+ return ([self.fake_instance], '')
+
+ self.stubs.Set(nova.compute.api.API, 'create', fake_create)
+
+ req = self._get_request()
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175',
+ 'flavorRef': '1',
+ },
+ 'os:scheduler_hints': {'a': 'b'},
+ }
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(202, res.status_int)
+
+ def test_create_server_bad_hints(self):
+ req = self._get_request()
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175',
+ 'flavorRef': '1',
+ },
+ 'os:scheduler_hints': 'here',
+ }
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+
+class SchedulerHintsTestCaseV2(SchedulerHintsTestCaseV21):
+
+ def _set_up_router(self):
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Scheduler_hints'])
+ self.app = compute.APIRouter(init_only=('servers',))
+
+ def _get_request(self):
+ return fakes.HTTPRequest.blank('/fake/servers')
+
+
+class ServersControllerCreateTestV21(test.TestCase):
+
+ def setUp(self):
+ """Shared implementation for tests below that create instance."""
+ super(ServersControllerCreateTestV21, self).setUp()
+
+ self.instance_cache_num = 0
+ self._set_up_controller()
+
+ def instance_create(context, inst):
+ inst_type = flavors.get_flavor_by_flavor_id(3)
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ def_image_ref = 'http://localhost/images/%s' % image_uuid
+ self.instance_cache_num += 1
+ instance = fake_instance.fake_db_instance(**{
+ 'id': self.instance_cache_num,
+ 'display_name': inst['display_name'] or 'test',
+ 'uuid': fakes.FAKE_UUID,
+ 'instance_type': inst_type,
+ 'access_ip_v4': '1.2.3.4',
+ 'access_ip_v6': 'fead::1234',
+ 'image_ref': inst.get('image_ref', def_image_ref),
+ 'user_id': 'fake',
+ 'project_id': 'fake',
+ 'reservation_id': inst['reservation_id'],
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "progress": 0,
+ "fixed_ips": [],
+ "task_state": "",
+ "vm_state": "",
+ "root_device_name": inst.get('root_device_name', 'vda'),
+ })
+
+ return instance
+
+ fake.stub_out_image_service(self.stubs)
+ self.stubs.Set(db, 'instance_create', instance_create)
+
+ def _set_up_controller(self):
+ ext_info = plugins.LoadedExtensionInfo()
+ CONF.set_override('extensions_blacklist', 'os-scheduler-hints',
+ 'osapi_v3')
+ self.no_scheduler_hints_controller = servers_v21.ServersController(
+ extension_info=ext_info)
+
+ def _verify_availability_zone(self, **kwargs):
+ self.assertNotIn('scheduler_hints', kwargs)
+
+ def _get_request(self):
+ return fakes.HTTPRequestV3.blank('/servers')
+
+ def _test_create_extra(self, params):
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ server = dict(name='server_test', imageRef=image_uuid, flavorRef=2)
+ body = dict(server=server)
+ body.update(params)
+ req = self._get_request()
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ server = self.no_scheduler_hints_controller.create(
+ req, body=body).obj['server']
+
+ def test_create_instance_with_scheduler_hints_disabled(self):
+ hints = {'same_host': '48e6a9f6-30af-47e0-bc04-acaed113bb4e'}
+ params = {'OS-SCH-HNT:scheduler_hints': hints}
+ old_create = nova.compute.api.API.create
+
+ def create(*args, **kwargs):
+ self._verify_availability_zone(**kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(nova.compute.api.API, 'create', create)
+ self._test_create_extra(params)
+
+
+class ServersControllerCreateTestV2(ServersControllerCreateTestV21):
+
+ def _set_up_controller(self):
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.no_scheduler_hints_controller = servers_v2.Controller(
+ self.ext_mgr)
+
+ def _verify_availability_zone(self, **kwargs):
+ self.assertEqual(kwargs['scheduler_hints'], {})
+
+ def _get_request(self):
+ return fakes.HTTPRequest.blank('/fake/servers')
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_security_group_default_rules.py b/nova/tests/unit/api/openstack/compute/contrib/test_security_group_default_rules.py
new file mode 100644
index 0000000000..a735f4722e
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_security_group_default_rules.py
@@ -0,0 +1,515 @@
+# Copyright 2013 Metacloud, Inc
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.config import cfg
+import webob
+
+from nova.api.openstack.compute.contrib import \
+ security_group_default_rules as security_group_default_rules_v2
+from nova.api.openstack.compute.plugins.v3 import \
+ security_group_default_rules as security_group_default_rules_v21
+from nova.api.openstack import wsgi
+from nova import context
+import nova.db
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+CONF = cfg.CONF
+
+
+class AttrDict(dict):
+ def __getattr__(self, k):
+ return self[k]
+
+
+def security_group_default_rule_template(**kwargs):
+ rule = kwargs.copy()
+ rule.setdefault('ip_protocol', 'TCP')
+ rule.setdefault('from_port', 22)
+ rule.setdefault('to_port', 22)
+ rule.setdefault('cidr', '10.10.10.0/24')
+ return rule
+
+
+def security_group_default_rule_db(security_group_default_rule, id=None):
+ attrs = security_group_default_rule.copy()
+ if id is not None:
+ attrs['id'] = id
+ return AttrDict(attrs)
+
+
+class TestSecurityGroupDefaultRulesNeutronV21(test.TestCase):
+ controller_cls = (security_group_default_rules_v21.
+ SecurityGroupDefaultRulesController)
+
+ def setUp(self):
+ self.flags(security_group_api='neutron')
+ super(TestSecurityGroupDefaultRulesNeutronV21, self).setUp()
+ self.controller = self.controller_cls()
+
+ def test_create_security_group_default_rule_not_implemented_neutron(self):
+ sgr = security_group_default_rule_template()
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPNotImplemented, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_security_group_default_rules_list_not_implemented_neturon(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPNotImplemented, self.controller.index,
+ req)
+
+ def test_security_group_default_rules_show_not_implemented_neturon(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPNotImplemented, self.controller.show,
+ req, '602ed77c-a076-4f9b-a617-f93b847b62c5')
+
+ def test_security_group_default_rules_delete_not_implemented_neturon(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPNotImplemented, self.controller.delete,
+ req, '602ed77c-a076-4f9b-a617-f93b847b62c5')
+
+
+class TestSecurityGroupDefaultRulesNeutronV2(test.TestCase):
+ controller_cls = (security_group_default_rules_v2.
+ SecurityGroupDefaultRulesController)
+
+
+class TestSecurityGroupDefaultRulesV21(test.TestCase):
+ controller_cls = (security_group_default_rules_v21.
+ SecurityGroupDefaultRulesController)
+
+ def setUp(self):
+ super(TestSecurityGroupDefaultRulesV21, self).setUp()
+ self.controller = self.controller_cls()
+
+ def test_create_security_group_default_rule(self):
+ sgr = security_group_default_rule_template()
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ sgr_dict = dict(security_group_default_rule=sgr)
+ res_dict = self.controller.create(req, sgr_dict)
+ security_group_default_rule = res_dict['security_group_default_rule']
+ self.assertEqual(security_group_default_rule['ip_protocol'],
+ sgr['ip_protocol'])
+ self.assertEqual(security_group_default_rule['from_port'],
+ sgr['from_port'])
+ self.assertEqual(security_group_default_rule['to_port'],
+ sgr['to_port'])
+ self.assertEqual(security_group_default_rule['ip_range']['cidr'],
+ sgr['cidr'])
+
+ def test_create_security_group_default_rule_with_no_to_port(self):
+ sgr = security_group_default_rule_template()
+ del sgr['to_port']
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_with_no_from_port(self):
+ sgr = security_group_default_rule_template()
+ del sgr['from_port']
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_with_no_ip_protocol(self):
+ sgr = security_group_default_rule_template()
+ del sgr['ip_protocol']
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_with_no_cidr(self):
+ sgr = security_group_default_rule_template()
+ del sgr['cidr']
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ res_dict = self.controller.create(req,
+ {'security_group_default_rule': sgr})
+ security_group_default_rule = res_dict['security_group_default_rule']
+ self.assertNotEqual(security_group_default_rule['id'], 0)
+ self.assertEqual(security_group_default_rule['ip_range']['cidr'],
+ '0.0.0.0/0')
+
+ def test_create_security_group_default_rule_with_blank_to_port(self):
+ sgr = security_group_default_rule_template(to_port='')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_with_blank_from_port(self):
+ sgr = security_group_default_rule_template(from_port='')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_with_blank_ip_protocol(self):
+ sgr = security_group_default_rule_template(ip_protocol='')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_with_blank_cidr(self):
+ sgr = security_group_default_rule_template(cidr='')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ res_dict = self.controller.create(req,
+ {'security_group_default_rule': sgr})
+ security_group_default_rule = res_dict['security_group_default_rule']
+ self.assertNotEqual(security_group_default_rule['id'], 0)
+ self.assertEqual(security_group_default_rule['ip_range']['cidr'],
+ '0.0.0.0/0')
+
+ def test_create_security_group_default_rule_non_numerical_to_port(self):
+ sgr = security_group_default_rule_template(to_port='invalid')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_non_numerical_from_port(self):
+ sgr = security_group_default_rule_template(from_port='invalid')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_invalid_ip_protocol(self):
+ sgr = security_group_default_rule_template(ip_protocol='invalid')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_invalid_cidr(self):
+ sgr = security_group_default_rule_template(cidr='10.10.2222.0/24')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_invalid_to_port(self):
+ sgr = security_group_default_rule_template(to_port='666666')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_invalid_from_port(self):
+ sgr = security_group_default_rule_template(from_port='666666')
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_create_security_group_default_rule_with_no_body(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, None)
+
+ def test_create_duplicate_security_group_default_rule(self):
+ sgr = security_group_default_rule_template()
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.controller.create(req, {'security_group_default_rule': sgr})
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_default_rule': sgr})
+
+ def test_security_group_default_rules_list(self):
+ self.test_create_security_group_default_rule()
+ rules = [dict(id=1,
+ ip_protocol='TCP',
+ from_port=22,
+ to_port=22,
+ ip_range=dict(cidr='10.10.10.0/24'))]
+ expected = {'security_group_default_rules': rules}
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ res_dict = self.controller.index(req)
+ self.assertEqual(res_dict, expected)
+
+ def test_default_security_group_default_rule_show(self):
+ sgr = security_group_default_rule_template(id=1)
+
+ self.test_create_security_group_default_rule()
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ res_dict = self.controller.show(req, '1')
+
+ security_group_default_rule = res_dict['security_group_default_rule']
+
+ self.assertEqual(security_group_default_rule['ip_protocol'],
+ sgr['ip_protocol'])
+ self.assertEqual(security_group_default_rule['to_port'],
+ sgr['to_port'])
+ self.assertEqual(security_group_default_rule['from_port'],
+ sgr['from_port'])
+ self.assertEqual(security_group_default_rule['ip_range']['cidr'],
+ sgr['cidr'])
+
+ def test_delete_security_group_default_rule(self):
+ sgr = security_group_default_rule_template(id=1)
+
+ self.test_create_security_group_default_rule()
+
+ self.called = False
+
+ def security_group_default_rule_destroy(context, id):
+ self.called = True
+
+ def return_security_group_default_rule(context, id):
+ self.assertEqual(sgr['id'], id)
+ return security_group_default_rule_db(sgr)
+
+ self.stubs.Set(nova.db, 'security_group_default_rule_destroy',
+ security_group_default_rule_destroy)
+ self.stubs.Set(nova.db, 'security_group_default_rule_get',
+ return_security_group_default_rule)
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-security-group-default-rules', use_admin_context=True)
+ self.controller.delete(req, '1')
+
+ self.assertTrue(self.called)
+
+ def test_security_group_ensure_default(self):
+ sgr = security_group_default_rule_template(id=1)
+ self.test_create_security_group_default_rule()
+
+ ctxt = context.get_admin_context()
+
+ setattr(ctxt, 'project_id', 'new_project_id')
+
+ sg = nova.db.security_group_ensure_default(ctxt)
+ rules = nova.db.security_group_rule_get_by_security_group(ctxt, sg.id)
+ security_group_rule = rules[0]
+ self.assertEqual(sgr['id'], security_group_rule.id)
+ self.assertEqual(sgr['ip_protocol'], security_group_rule.protocol)
+ self.assertEqual(sgr['from_port'], security_group_rule.from_port)
+ self.assertEqual(sgr['to_port'], security_group_rule.to_port)
+ self.assertEqual(sgr['cidr'], security_group_rule.cidr)
+
+
+class TestSecurityGroupDefaultRulesV2(test.TestCase):
+ controller_cls = (security_group_default_rules_v2.
+ SecurityGroupDefaultRulesController)
+
+
+class TestSecurityGroupDefaultRulesXMLDeserializer(test.TestCase):
+ def setUp(self):
+ super(TestSecurityGroupDefaultRulesXMLDeserializer, self).setUp()
+ deserializer = security_group_default_rules_v2.\
+ SecurityGroupDefaultRulesXMLDeserializer()
+ self.deserializer = deserializer
+
+ def test_create_request(self):
+ serial_request = """
+<security_group_default_rule>
+ <from_port>22</from_port>
+ <to_port>22</to_port>
+ <ip_protocol>TCP</ip_protocol>
+ <cidr>10.10.10.0/24</cidr>
+</security_group_default_rule>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group_default_rule": {
+ "from_port": "22",
+ "to_port": "22",
+ "ip_protocol": "TCP",
+ "cidr": "10.10.10.0/24"
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_create_no_to_port_request(self):
+ serial_request = """
+<security_group_default_rule>
+ <from_port>22</from_port>
+ <ip_protocol>TCP</ip_protocol>
+ <cidr>10.10.10.0/24</cidr>
+</security_group_default_rule>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group_default_rule": {
+ "from_port": "22",
+ "ip_protocol": "TCP",
+ "cidr": "10.10.10.0/24"
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_create_no_from_port_request(self):
+ serial_request = """
+<security_group_default_rule>
+ <to_port>22</to_port>
+ <ip_protocol>TCP</ip_protocol>
+ <cidr>10.10.10.0/24</cidr>
+</security_group_default_rule>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group_default_rule": {
+ "to_port": "22",
+ "ip_protocol": "TCP",
+ "cidr": "10.10.10.0/24"
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_create_no_ip_protocol_request(self):
+ serial_request = """
+<security_group_default_rule>
+ <from_port>22</from_port>
+ <to_port>22</to_port>
+ <cidr>10.10.10.0/24</cidr>
+</security_group_default_rule>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group_default_rule": {
+ "from_port": "22",
+ "to_port": "22",
+ "cidr": "10.10.10.0/24"
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_create_no_cidr_request(self):
+ serial_request = """
+<security_group_default_rule>
+ <from_port>22</from_port>
+ <to_port>22</to_port>
+ <ip_protocol>TCP</ip_protocol>
+</security_group_default_rule>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group_default_rule": {
+ "from_port": "22",
+ "to_port": "22",
+ "ip_protocol": "TCP",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+
+class TestSecurityGroupDefaultRuleXMLSerializer(test.TestCase):
+ def setUp(self):
+ super(TestSecurityGroupDefaultRuleXMLSerializer, self).setUp()
+ self.namespace = wsgi.XMLNS_V11
+ self.rule_serializer =\
+ security_group_default_rules_v2.SecurityGroupDefaultRuleTemplate()
+ self.index_serializer =\
+ security_group_default_rules_v2.SecurityGroupDefaultRulesTemplate()
+
+ def _tag(self, elem):
+ tagname = elem.tag
+ self.assertEqual(tagname[0], '{')
+ tmp = tagname.partition('}')
+ namespace = tmp[0][1:]
+ self.assertEqual(namespace, self.namespace)
+ return tmp[2]
+
+ def _verify_security_group_default_rule(self, raw_rule, tree):
+ self.assertEqual(raw_rule['id'], tree.get('id'))
+
+ seen = set()
+ expected = set(['ip_protocol', 'from_port', 'to_port', 'ip_range',
+ 'ip_range/cidr'])
+
+ for child in tree:
+ child_tag = self._tag(child)
+ seen.add(child_tag)
+ if child_tag == 'ip_range':
+ for gr_child in child:
+ gr_child_tag = self._tag(gr_child)
+ self.assertIn(gr_child_tag, raw_rule[child_tag])
+ seen.add('%s/%s' % (child_tag, gr_child_tag))
+ self.assertEqual(gr_child.text,
+ raw_rule[child_tag][gr_child_tag])
+ else:
+ self.assertEqual(child.text, raw_rule[child_tag])
+ self.assertEqual(seen, expected)
+
+ def test_rule_serializer(self):
+ raw_rule = dict(id='123',
+ ip_protocol='TCP',
+ from_port='22',
+ to_port='22',
+ ip_range=dict(cidr='10.10.10.0/24'))
+ rule = dict(security_group_default_rule=raw_rule)
+ text = self.rule_serializer.serialize(rule)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('security_group_default_rule', self._tag(tree))
+ self._verify_security_group_default_rule(raw_rule, tree)
+
+ def test_index_serializer(self):
+ rules = [dict(id='123',
+ ip_protocol='TCP',
+ from_port='22',
+ to_port='22',
+ ip_range=dict(cidr='10.10.10.0/24')),
+ dict(id='234',
+ ip_protocol='UDP',
+ from_port='23456',
+ to_port='234567',
+ ip_range=dict(cidr='10.12.0.0/18')),
+ dict(id='345',
+ ip_protocol='tcp',
+ from_port='3456',
+ to_port='4567',
+ ip_range=dict(cidr='192.168.1.0/32'))]
+
+ rules_dict = dict(security_group_default_rules=rules)
+
+ text = self.index_serializer.serialize(rules_dict)
+
+ tree = etree.fromstring(text)
+ self.assertEqual('security_group_default_rules', self._tag(tree))
+ self.assertEqual(len(rules), len(tree))
+ for idx, child in enumerate(tree):
+ self._verify_security_group_default_rule(rules[idx], child)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_security_groups.py b/nova/tests/unit/api/openstack/compute/contrib/test_security_groups.py
new file mode 100644
index 0000000000..d1620b6a28
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_security_groups.py
@@ -0,0 +1,1767 @@
+# Copyright 2011 OpenStack Foundation
+# Copyright 2012 Justin Santa Barbara
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+import mock
+import mox
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import security_groups as secgroups_v2
+from nova.api.openstack.compute.plugins.v3 import security_groups as \
+ secgroups_v21
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova import compute
+from nova.compute import power_state
+from nova import context as context_maker
+import nova.db
+from nova import exception
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import quota
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+from nova.tests.unit import utils
+
+CONF = cfg.CONF
+FAKE_UUID1 = 'a47ae74e-ab08-447f-8eee-ffd43fc46c16'
+FAKE_UUID2 = 'c6e6430a-6563-4efa-9542-5e93c9e97d18'
+
+
+class AttrDict(dict):
+ def __getattr__(self, k):
+ return self[k]
+
+
+def security_group_template(**kwargs):
+ sg = kwargs.copy()
+ sg.setdefault('tenant_id', '123')
+ sg.setdefault('name', 'test')
+ sg.setdefault('description', 'test-description')
+ return sg
+
+
+def security_group_db(security_group, id=None):
+ attrs = security_group.copy()
+ if 'tenant_id' in attrs:
+ attrs['project_id'] = attrs.pop('tenant_id')
+ if id is not None:
+ attrs['id'] = id
+ attrs.setdefault('rules', [])
+ attrs.setdefault('instances', [])
+ return AttrDict(attrs)
+
+
+def security_group_rule_template(**kwargs):
+ rule = kwargs.copy()
+ rule.setdefault('ip_protocol', 'tcp')
+ rule.setdefault('from_port', 22)
+ rule.setdefault('to_port', 22)
+ rule.setdefault('parent_group_id', 2)
+ return rule
+
+
+def security_group_rule_db(rule, id=None):
+ attrs = rule.copy()
+ if 'ip_protocol' in attrs:
+ attrs['protocol'] = attrs.pop('ip_protocol')
+ return AttrDict(attrs)
+
+
+def return_server(context, server_id,
+ columns_to_join=None, use_slave=False):
+ return fake_instance.fake_db_instance(
+ **{'id': int(server_id),
+ 'power_state': 0x01,
+ 'host': "localhost",
+ 'uuid': FAKE_UUID1,
+ 'name': 'asdf'})
+
+
+def return_server_by_uuid(context, server_uuid,
+ columns_to_join=None,
+ use_slave=False):
+ return fake_instance.fake_db_instance(
+ **{'id': 1,
+ 'power_state': 0x01,
+ 'host': "localhost",
+ 'uuid': server_uuid,
+ 'name': 'asdf'})
+
+
+def return_non_running_server(context, server_id, columns_to_join=None):
+ return fake_instance.fake_db_instance(
+ **{'id': server_id, 'power_state': power_state.SHUTDOWN,
+ 'uuid': FAKE_UUID1, 'host': "localhost", 'name': 'asdf'})
+
+
+def return_security_group_by_name(context, project_id, group_name):
+ return {'id': 1, 'name': group_name,
+ "instances": [{'id': 1, 'uuid': FAKE_UUID1}]}
+
+
+def return_security_group_without_instances(context, project_id, group_name):
+ return {'id': 1, 'name': group_name}
+
+
+def return_server_nonexistent(context, server_id, columns_to_join=None):
+ raise exception.InstanceNotFound(instance_id=server_id)
+
+
+class TestSecurityGroupsV21(test.TestCase):
+ secgrp_ctl_cls = secgroups_v21.SecurityGroupController
+ server_secgrp_ctl_cls = secgroups_v21.ServerSecurityGroupController
+ secgrp_act_ctl_cls = secgroups_v21.SecurityGroupActionController
+
+ def setUp(self):
+ super(TestSecurityGroupsV21, self).setUp()
+
+ self.controller = self.secgrp_ctl_cls()
+ self.server_controller = self.server_secgrp_ctl_cls()
+ self.manager = self.secgrp_act_ctl_cls()
+
+ # This needs to be done here to set fake_id because the derived
+ # class needs to be called first if it wants to set
+ # 'security_group_api' and this setUp method needs to be called.
+ if self.controller.security_group_api.id_is_uuid:
+ self.fake_id = '11111111-1111-1111-1111-111111111111'
+ else:
+ self.fake_id = '11111111'
+
+ def _assert_no_security_groups_reserved(self, context):
+ """Check that no reservations are leaked during tests."""
+ result = quota.QUOTAS.get_project_quotas(context, context.project_id)
+ self.assertEqual(result['security_groups']['reserved'], 0)
+
+ def _assert_security_groups_in_use(self, project_id, user_id, in_use):
+ context = context_maker.get_admin_context()
+ result = quota.QUOTAS.get_user_quotas(context, project_id, user_id)
+ self.assertEqual(result['security_groups']['in_use'], in_use)
+
+ def test_create_security_group(self):
+ sg = security_group_template()
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ res_dict = self.controller.create(req, {'security_group': sg})
+ self.assertEqual(res_dict['security_group']['name'], 'test')
+ self.assertEqual(res_dict['security_group']['description'],
+ 'test-description')
+
+ def test_create_security_group_with_no_name(self):
+ sg = security_group_template()
+ del sg['name']
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, sg)
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_with_no_description(self):
+ sg = security_group_template()
+ del sg['description']
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_with_empty_description(self):
+ sg = security_group_template()
+ sg['description'] = ""
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ try:
+ self.controller.create(req, {'security_group': sg})
+ self.fail('Should have raised BadRequest exception')
+ except webob.exc.HTTPBadRequest as exc:
+ self.assertEqual('description has a minimum character requirement'
+ ' of 1.', exc.explanation)
+ except exception.InvalidInput as exc:
+ self.fail('Should have raised BadRequest exception instead of')
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_with_blank_name(self):
+ sg = security_group_template(name='')
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_with_whitespace_name(self):
+ sg = security_group_template(name=' ')
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_with_blank_description(self):
+ sg = security_group_template(description='')
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_with_whitespace_description(self):
+ sg = security_group_template(description=' ')
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_with_duplicate_name(self):
+ sg = security_group_template()
+
+ # FIXME: Stub out _get instead of creating twice
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.controller.create(req, {'security_group': sg})
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_with_no_body(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, None)
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_with_no_security_group(self):
+ body = {'no-securityGroup': None}
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, body)
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_above_255_characters_name(self):
+ sg = security_group_template(name='1234567890' * 26)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_above_255_characters_description(self):
+ sg = security_group_template(description='1234567890' * 26)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_non_string_name(self):
+ sg = security_group_template(name=12)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_non_string_description(self):
+ sg = security_group_template(description=12)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group': sg})
+
+ self._assert_no_security_groups_reserved(req.environ['nova.context'])
+
+ def test_create_security_group_quota_limit(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ for num in range(1, CONF.quota_security_groups):
+ name = 'test%s' % num
+ sg = security_group_template(name=name)
+ res_dict = self.controller.create(req, {'security_group': sg})
+ self.assertEqual(res_dict['security_group']['name'], name)
+
+ sg = security_group_template()
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.create,
+ req, {'security_group': sg})
+
+ def test_get_security_group_list(self):
+ groups = []
+ for i, name in enumerate(['default', 'test']):
+ sg = security_group_template(id=i + 1,
+ name=name,
+ description=name + '-desc',
+ rules=[])
+ groups.append(sg)
+ expected = {'security_groups': groups}
+
+ def return_security_groups(context, project_id):
+ return [security_group_db(sg) for sg in groups]
+
+ self.stubs.Set(nova.db, 'security_group_get_by_project',
+ return_security_groups)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ res_dict = self.controller.index(req)
+
+ self.assertEqual(res_dict, expected)
+
+ def test_get_security_group_list_missing_group_id_rule(self):
+ groups = []
+ rule1 = security_group_rule_template(cidr='10.2.3.124/24',
+ parent_group_id=1,
+ group_id={}, id=88,
+ protocol='TCP')
+ rule2 = security_group_rule_template(cidr='10.2.3.125/24',
+ parent_group_id=1,
+ id=99, protocol=88,
+ group_id='HAS_BEEN_DELETED')
+ sg = security_group_template(id=1,
+ name='test',
+ description='test-desc',
+ rules=[rule1, rule2])
+
+ groups.append(sg)
+ # An expected rule here needs to be created as the api returns
+ # different attributes on the rule for a response than what was
+ # passed in. For example:
+ # "cidr": "0.0.0.0/0" ->"ip_range": {"cidr": "0.0.0.0/0"}
+ expected_rule = security_group_rule_template(
+ ip_range={'cidr': '10.2.3.124/24'}, parent_group_id=1,
+ group={}, id=88, ip_protocol='TCP')
+ expected = security_group_template(id=1,
+ name='test',
+ description='test-desc',
+ rules=[expected_rule])
+
+ expected = {'security_groups': [expected]}
+
+ def return_security_groups(context, project, search_opts):
+ return [security_group_db(sg) for sg in groups]
+
+ self.stubs.Set(self.controller.security_group_api, 'list',
+ return_security_groups)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ res_dict = self.controller.index(req)
+
+ self.assertEqual(res_dict, expected)
+
+ def test_get_security_group_list_all_tenants(self):
+ all_groups = []
+ tenant_groups = []
+
+ for i, name in enumerate(['default', 'test']):
+ sg = security_group_template(id=i + 1,
+ name=name,
+ description=name + '-desc',
+ rules=[])
+ all_groups.append(sg)
+ if name == 'default':
+ tenant_groups.append(sg)
+
+ all = {'security_groups': all_groups}
+ tenant_specific = {'security_groups': tenant_groups}
+
+ def return_all_security_groups(context):
+ return [security_group_db(sg) for sg in all_groups]
+
+ self.stubs.Set(nova.db, 'security_group_get_all',
+ return_all_security_groups)
+
+ def return_tenant_security_groups(context, project_id):
+ return [security_group_db(sg) for sg in tenant_groups]
+
+ self.stubs.Set(nova.db, 'security_group_get_by_project',
+ return_tenant_security_groups)
+
+ path = '/v2/fake/os-security-groups'
+
+ req = fakes.HTTPRequest.blank(path, use_admin_context=True)
+ res_dict = self.controller.index(req)
+ self.assertEqual(res_dict, tenant_specific)
+
+ req = fakes.HTTPRequest.blank('%s?all_tenants=1' % path,
+ use_admin_context=True)
+ res_dict = self.controller.index(req)
+ self.assertEqual(res_dict, all)
+
+ def test_get_security_group_by_instance(self):
+ groups = []
+ for i, name in enumerate(['default', 'test']):
+ sg = security_group_template(id=i + 1,
+ name=name,
+ description=name + '-desc',
+ rules=[])
+ groups.append(sg)
+ expected = {'security_groups': groups}
+
+ def return_instance(context, server_id,
+ columns_to_join=None, use_slave=False):
+ self.assertEqual(server_id, FAKE_UUID1)
+ return return_server_by_uuid(context, server_id)
+
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_instance)
+
+ def return_security_groups(context, instance_uuid):
+ self.assertEqual(instance_uuid, FAKE_UUID1)
+ return [security_group_db(sg) for sg in groups]
+
+ self.stubs.Set(nova.db, 'security_group_get_by_instance',
+ return_security_groups)
+
+ req = fakes.HTTPRequest.blank('/v2/%s/servers/%s/os-security-groups' %
+ ('fake', FAKE_UUID1))
+ res_dict = self.server_controller.index(req, FAKE_UUID1)
+
+ self.assertEqual(res_dict, expected)
+
+ @mock.patch('nova.db.instance_get_by_uuid')
+ @mock.patch('nova.db.security_group_get_by_instance', return_value=[])
+ def test_get_security_group_empty_for_instance(self, mock_sec_group,
+ mock_db_get_ins):
+ expected = {'security_groups': []}
+
+ def return_instance(context, server_id,
+ columns_to_join=None, use_slave=False):
+ self.assertEqual(server_id, FAKE_UUID1)
+ return return_server_by_uuid(context, server_id)
+ mock_db_get_ins.side_effect = return_instance
+ req = fakes.HTTPRequest.blank('/v2/%s/servers/%s/os-security-groups' %
+ ('fake', FAKE_UUID1))
+ res_dict = self.server_controller.index(req, FAKE_UUID1)
+ self.assertEqual(expected, res_dict)
+ mock_sec_group.assert_called_once_with(req.environ['nova.context'],
+ FAKE_UUID1)
+
+ def test_get_security_group_by_instance_non_existing(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server_nonexistent)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_nonexistent)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/os-security-groups')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.server_controller.index, req, '1')
+
+ def test_get_security_group_by_instance_invalid_id(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/servers/invalid/os-security-groups')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.server_controller.index, req, 'invalid')
+
+ def test_get_security_group_by_id(self):
+ sg = security_group_template(id=2, rules=[])
+
+ def return_security_group(context, group_id):
+ self.assertEqual(sg['id'], group_id)
+ return security_group_db(sg)
+
+ self.stubs.Set(nova.db, 'security_group_get',
+ return_security_group)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/2')
+ res_dict = self.controller.show(req, '2')
+
+ expected = {'security_group': sg}
+ self.assertEqual(res_dict, expected)
+
+ def test_get_security_group_by_invalid_id(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/invalid')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
+ req, 'invalid')
+
+ def test_get_security_group_by_non_existing_id(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s' %
+ self.fake_id)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, self.fake_id)
+
+ def test_update_security_group(self):
+ sg = security_group_template(id=2, rules=[])
+ sg_update = security_group_template(id=2, rules=[],
+ name='update_name', description='update_desc')
+
+ def return_security_group(context, group_id):
+ self.assertEqual(sg['id'], group_id)
+ return security_group_db(sg)
+
+ def return_update_security_group(context, group_id, values,
+ columns_to_join=None):
+ self.assertEqual(sg_update['id'], group_id)
+ self.assertEqual(sg_update['name'], values['name'])
+ self.assertEqual(sg_update['description'], values['description'])
+ return security_group_db(sg_update)
+
+ self.stubs.Set(nova.db, 'security_group_update',
+ return_update_security_group)
+ self.stubs.Set(nova.db, 'security_group_get',
+ return_security_group)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/2')
+ res_dict = self.controller.update(req, '2',
+ {'security_group': sg_update})
+
+ expected = {'security_group': sg_update}
+ self.assertEqual(res_dict, expected)
+
+ def test_update_security_group_name_to_default(self):
+ sg = security_group_template(id=2, rules=[], name='default')
+
+ def return_security_group(context, group_id):
+ self.assertEqual(sg['id'], group_id)
+ return security_group_db(sg)
+
+ self.stubs.Set(nova.db, 'security_group_get',
+ return_security_group)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/2')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, '2', {'security_group': sg})
+
+ def test_update_default_security_group_fail(self):
+ sg = security_group_template()
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/1')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, '1', {'security_group': sg})
+
+ def test_delete_security_group_by_id(self):
+ sg = security_group_template(id=1, project_id='fake_project',
+ user_id='fake_user', rules=[])
+
+ self.called = False
+
+ def security_group_destroy(context, id):
+ self.called = True
+
+ def return_security_group(context, group_id):
+ self.assertEqual(sg['id'], group_id)
+ return security_group_db(sg)
+
+ self.stubs.Set(nova.db, 'security_group_destroy',
+ security_group_destroy)
+ self.stubs.Set(nova.db, 'security_group_get',
+ return_security_group)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/1')
+ self.controller.delete(req, '1')
+
+ self.assertTrue(self.called)
+
+ def test_delete_security_group_by_admin(self):
+ sg = security_group_template(id=2, rules=[])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups')
+ self.controller.create(req, {'security_group': sg})
+ context = req.environ['nova.context']
+
+ # Ensure quota usage for security group is correct.
+ self._assert_security_groups_in_use(context.project_id,
+ context.user_id, 2)
+
+ # Delete the security group by admin.
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/2',
+ use_admin_context=True)
+ self.controller.delete(req, '2')
+
+ # Ensure quota for security group in use is released.
+ self._assert_security_groups_in_use(context.project_id,
+ context.user_id, 1)
+
+ def test_delete_security_group_by_invalid_id(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/invalid')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
+ req, 'invalid')
+
+ def test_delete_security_group_by_non_existing_id(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s'
+ % self.fake_id)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, self.fake_id)
+
+ def test_delete_security_group_in_use(self):
+ sg = security_group_template(id=1, rules=[])
+
+ def security_group_in_use(context, id):
+ return True
+
+ def return_security_group(context, group_id):
+ self.assertEqual(sg['id'], group_id)
+ return security_group_db(sg)
+
+ self.stubs.Set(nova.db, 'security_group_in_use',
+ security_group_in_use)
+ self.stubs.Set(nova.db, 'security_group_get',
+ return_security_group)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/1')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
+ req, '1')
+
+ def test_associate_by_non_existing_security_group_name(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.assertEqual(return_server(None, '1'),
+ nova.db.instance_get(None, '1'))
+ body = dict(addSecurityGroup=dict(name='non-existing'))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._addSecurityGroup, req, '1', body)
+
+ def test_associate_by_invalid_server_id(self):
+ body = dict(addSecurityGroup=dict(name='test'))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/invalid/action')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._addSecurityGroup, req, 'invalid', body)
+
+ def test_associate_without_body(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ body = dict(addSecurityGroup=None)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._addSecurityGroup, req, '1', body)
+
+ def test_associate_no_security_group_name(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ body = dict(addSecurityGroup=dict())
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._addSecurityGroup, req, '1', body)
+
+ def test_associate_security_group_name_with_whitespaces(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ body = dict(addSecurityGroup=dict(name=" "))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._addSecurityGroup, req, '1', body)
+
+ def test_associate_non_existing_instance(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server_nonexistent)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_nonexistent)
+ body = dict(addSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._addSecurityGroup, req, '1', body)
+
+ def test_associate_non_running_instance(self):
+ self.stubs.Set(nova.db, 'instance_get', return_non_running_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_non_running_server)
+ self.stubs.Set(nova.db, 'security_group_get_by_name',
+ return_security_group_without_instances)
+ body = dict(addSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.manager._addSecurityGroup(req, '1', body)
+
+ def test_associate_already_associated_security_group_to_instance(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_by_uuid)
+ self.stubs.Set(nova.db, 'security_group_get_by_name',
+ return_security_group_by_name)
+ body = dict(addSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._addSecurityGroup, req, '1', body)
+
+ def test_associate(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_by_uuid)
+ self.mox.StubOutWithMock(nova.db, 'instance_add_security_group')
+ nova.db.instance_add_security_group(mox.IgnoreArg(),
+ mox.IgnoreArg(),
+ mox.IgnoreArg())
+ self.stubs.Set(nova.db, 'security_group_get_by_name',
+ return_security_group_without_instances)
+ self.mox.ReplayAll()
+
+ body = dict(addSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.manager._addSecurityGroup(req, '1', body)
+
+ def test_disassociate_by_non_existing_security_group_name(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.assertEqual(return_server(None, '1'),
+ nova.db.instance_get(None, '1'))
+ body = dict(removeSecurityGroup=dict(name='non-existing'))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._removeSecurityGroup, req, '1', body)
+
+ def test_disassociate_by_invalid_server_id(self):
+ self.stubs.Set(nova.db, 'security_group_get_by_name',
+ return_security_group_by_name)
+ body = dict(removeSecurityGroup=dict(name='test'))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/invalid/action')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._removeSecurityGroup, req, 'invalid',
+ body)
+
+ def test_disassociate_without_body(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ body = dict(removeSecurityGroup=None)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._removeSecurityGroup, req, '1', body)
+
+ def test_disassociate_no_security_group_name(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ body = dict(removeSecurityGroup=dict())
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._removeSecurityGroup, req, '1', body)
+
+ def test_disassociate_security_group_name_with_whitespaces(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ body = dict(removeSecurityGroup=dict(name=" "))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._removeSecurityGroup, req, '1', body)
+
+ def test_disassociate_non_existing_instance(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server_nonexistent)
+ self.stubs.Set(nova.db, 'security_group_get_by_name',
+ return_security_group_by_name)
+ body = dict(removeSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.manager._removeSecurityGroup, req, '1', body)
+
+ def test_disassociate_non_running_instance(self):
+ self.stubs.Set(nova.db, 'instance_get', return_non_running_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_non_running_server)
+ self.stubs.Set(nova.db, 'security_group_get_by_name',
+ return_security_group_by_name)
+ body = dict(removeSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.manager._removeSecurityGroup(req, '1', body)
+
+ def test_disassociate_already_associated_security_group_to_instance(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_by_uuid)
+ self.stubs.Set(nova.db, 'security_group_get_by_name',
+ return_security_group_without_instances)
+ body = dict(removeSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.manager._removeSecurityGroup, req, '1', body)
+
+ def test_disassociate(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_by_uuid)
+ self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group')
+ nova.db.instance_remove_security_group(mox.IgnoreArg(),
+ mox.IgnoreArg(),
+ mox.IgnoreArg())
+ self.stubs.Set(nova.db, 'security_group_get_by_name',
+ return_security_group_by_name)
+ self.mox.ReplayAll()
+
+ body = dict(removeSecurityGroup=dict(name="test"))
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action')
+ self.manager._removeSecurityGroup(req, '1', body)
+
+
+class TestSecurityGroupsV2(TestSecurityGroupsV21):
+ secgrp_ctl_cls = secgroups_v2.SecurityGroupController
+ server_secgrp_ctl_cls = secgroups_v2.ServerSecurityGroupController
+ secgrp_act_ctl_cls = secgroups_v2.SecurityGroupActionController
+
+
+class TestSecurityGroupRulesV21(test.TestCase):
+ secgrp_ctl_cls = secgroups_v21.SecurityGroupRulesController
+
+ def setUp(self):
+ super(TestSecurityGroupRulesV21, self).setUp()
+
+ self.controller = self.secgrp_ctl_cls()
+ if self.controller.security_group_api.id_is_uuid:
+ id1 = '11111111-1111-1111-1111-111111111111'
+ id2 = '22222222-2222-2222-2222-222222222222'
+ self.invalid_id = '33333333-3333-3333-3333-333333333333'
+ else:
+ id1 = 1
+ id2 = 2
+ self.invalid_id = '33333333'
+
+ self.sg1 = security_group_template(id=id1)
+ self.sg2 = security_group_template(
+ id=id2, name='authorize_revoke',
+ description='authorize-revoke testing')
+
+ db1 = security_group_db(self.sg1)
+ db2 = security_group_db(self.sg2)
+
+ def return_security_group(context, group_id, columns_to_join=None):
+ if group_id == db1['id']:
+ return db1
+ if group_id == db2['id']:
+ return db2
+ raise exception.SecurityGroupNotFound(security_group_id=group_id)
+
+ self.stubs.Set(nova.db, 'security_group_get',
+ return_security_group)
+
+ self.parent_security_group = db2
+
+ def test_create_by_cidr(self):
+ rule = security_group_rule_template(cidr='10.2.3.124/24',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+ security_group_rule = res_dict['security_group_rule']
+ self.assertNotEqual(security_group_rule['id'], 0)
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.sg2['id'])
+ self.assertEqual(security_group_rule['ip_range']['cidr'],
+ "10.2.3.124/24")
+
+ def test_create_by_group_id(self):
+ rule = security_group_rule_template(group_id=self.sg1['id'],
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+
+ security_group_rule = res_dict['security_group_rule']
+ self.assertNotEqual(security_group_rule['id'], 0)
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.sg2['id'])
+
+ def test_create_by_same_group_id(self):
+ rule1 = security_group_rule_template(group_id=self.sg1['id'],
+ from_port=80, to_port=80,
+ parent_group_id=self.sg2['id'])
+ self.parent_security_group['rules'] = [security_group_rule_db(rule1)]
+
+ rule2 = security_group_rule_template(group_id=self.sg1['id'],
+ from_port=81, to_port=81,
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule2})
+
+ security_group_rule = res_dict['security_group_rule']
+ self.assertNotEqual(security_group_rule['id'], 0)
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.sg2['id'])
+ self.assertEqual(security_group_rule['from_port'], 81)
+ self.assertEqual(security_group_rule['to_port'], 81)
+
+ def test_create_none_value_from_to_port(self):
+ rule = {'parent_group_id': self.sg1['id'],
+ 'group_id': self.sg1['id']}
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+ security_group_rule = res_dict['security_group_rule']
+ self.assertIsNone(security_group_rule['from_port'])
+ self.assertIsNone(security_group_rule['to_port'])
+ self.assertEqual(security_group_rule['group']['name'], 'test')
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.sg1['id'])
+
+ def test_create_none_value_from_to_port_icmp(self):
+ rule = {'parent_group_id': self.sg1['id'],
+ 'group_id': self.sg1['id'],
+ 'ip_protocol': 'ICMP'}
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+ security_group_rule = res_dict['security_group_rule']
+ self.assertEqual(security_group_rule['ip_protocol'], 'ICMP')
+ self.assertEqual(security_group_rule['from_port'], -1)
+ self.assertEqual(security_group_rule['to_port'], -1)
+ self.assertEqual(security_group_rule['group']['name'], 'test')
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.sg1['id'])
+
+ def test_create_none_value_from_to_port_tcp(self):
+ rule = {'parent_group_id': self.sg1['id'],
+ 'group_id': self.sg1['id'],
+ 'ip_protocol': 'TCP'}
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+ security_group_rule = res_dict['security_group_rule']
+ self.assertEqual(security_group_rule['ip_protocol'], 'TCP')
+ self.assertEqual(security_group_rule['from_port'], 1)
+ self.assertEqual(security_group_rule['to_port'], 65535)
+ self.assertEqual(security_group_rule['group']['name'], 'test')
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.sg1['id'])
+
+ def test_create_by_invalid_cidr_json(self):
+ rule = security_group_rule_template(
+ ip_protocol="tcp",
+ from_port=22,
+ to_port=22,
+ parent_group_id=self.sg2['id'],
+ cidr="10.2.3.124/2433")
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_by_invalid_tcp_port_json(self):
+ rule = security_group_rule_template(
+ ip_protocol="tcp",
+ from_port=75534,
+ to_port=22,
+ parent_group_id=self.sg2['id'],
+ cidr="10.2.3.124/24")
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_by_invalid_icmp_port_json(self):
+ rule = security_group_rule_template(
+ ip_protocol="icmp",
+ from_port=1,
+ to_port=256,
+ parent_group_id=self.sg2['id'],
+ cidr="10.2.3.124/24")
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_add_existing_rules_by_cidr(self):
+ rule = security_group_rule_template(cidr='10.0.0.0/24',
+ parent_group_id=self.sg2['id'])
+
+ self.parent_security_group['rules'] = [security_group_rule_db(rule)]
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_add_existing_rules_by_group_id(self):
+ rule = security_group_rule_template(group_id=1)
+
+ self.parent_security_group['rules'] = [security_group_rule_db(rule)]
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_no_body(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, None)
+
+ def test_create_with_no_security_group_rule_in_body(self):
+ rules = {'test': 'test'}
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, rules)
+
+ def test_create_with_invalid_parent_group_id(self):
+ rule = security_group_rule_template(parent_group_id='invalid')
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_non_existing_parent_group_id(self):
+ rule = security_group_rule_template(group_id=None,
+ parent_group_id=self.invalid_id)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_non_existing_group_id(self):
+ rule = security_group_rule_template(group_id='invalid',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_invalid_protocol(self):
+ rule = security_group_rule_template(ip_protocol='invalid-protocol',
+ cidr='10.2.2.0/24',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_no_protocol(self):
+ rule = security_group_rule_template(cidr='10.2.2.0/24',
+ parent_group_id=self.sg2['id'])
+ del rule['ip_protocol']
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_invalid_from_port(self):
+ rule = security_group_rule_template(from_port='666666',
+ cidr='10.2.2.0/24',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_invalid_to_port(self):
+ rule = security_group_rule_template(to_port='666666',
+ cidr='10.2.2.0/24',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_non_numerical_from_port(self):
+ rule = security_group_rule_template(from_port='invalid',
+ cidr='10.2.2.0/24',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_non_numerical_to_port(self):
+ rule = security_group_rule_template(to_port='invalid',
+ cidr='10.2.2.0/24',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_no_from_port(self):
+ rule = security_group_rule_template(cidr='10.2.2.0/24',
+ parent_group_id=self.sg2['id'])
+ del rule['from_port']
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_no_to_port(self):
+ rule = security_group_rule_template(cidr='10.2.2.0/24',
+ parent_group_id=self.sg2['id'])
+ del rule['to_port']
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_invalid_cidr(self):
+ rule = security_group_rule_template(cidr='10.2.2222.0/24',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_no_cidr_group(self):
+ rule = security_group_rule_template(parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+
+ security_group_rule = res_dict['security_group_rule']
+ self.assertNotEqual(security_group_rule['id'], 0)
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.parent_security_group['id'])
+ self.assertEqual(security_group_rule['ip_range']['cidr'],
+ "0.0.0.0/0")
+
+ def test_create_with_invalid_group_id(self):
+ rule = security_group_rule_template(group_id='invalid',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_empty_group_id(self):
+ rule = security_group_rule_template(group_id='',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_nonexist_group_id(self):
+ rule = security_group_rule_template(group_id=self.invalid_id,
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_with_same_group_parent_id_and_group_id(self):
+ rule = security_group_rule_template(group_id=self.sg1['id'],
+ parent_group_id=self.sg1['id'])
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+ security_group_rule = res_dict['security_group_rule']
+ self.assertNotEqual(security_group_rule['id'], 0)
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.sg1['id'])
+ self.assertEqual(security_group_rule['group']['name'],
+ self.sg1['name'])
+
+ def _test_create_with_no_ports_and_no_group(self, proto):
+ rule = {'ip_protocol': proto, 'parent_group_id': self.sg2['id']}
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def _test_create_with_no_ports(self, proto):
+ rule = {'ip_protocol': proto, 'parent_group_id': self.sg2['id'],
+ 'group_id': self.sg1['id']}
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+ security_group_rule = res_dict['security_group_rule']
+ expected_rule = {
+ 'from_port': 1, 'group': {'tenant_id': '123', 'name': 'test'},
+ 'ip_protocol': proto, 'to_port': 65535, 'parent_group_id':
+ self.sg2['id'], 'ip_range': {}, 'id': security_group_rule['id']
+ }
+ if proto == 'icmp':
+ expected_rule['to_port'] = -1
+ expected_rule['from_port'] = -1
+ self.assertEqual(expected_rule, security_group_rule)
+
+ def test_create_with_no_ports_icmp(self):
+ self._test_create_with_no_ports_and_no_group('icmp')
+ self._test_create_with_no_ports('icmp')
+
+ def test_create_with_no_ports_tcp(self):
+ self._test_create_with_no_ports_and_no_group('tcp')
+ self._test_create_with_no_ports('tcp')
+
+ def test_create_with_no_ports_udp(self):
+ self._test_create_with_no_ports_and_no_group('udp')
+ self._test_create_with_no_ports('udp')
+
+ def _test_create_with_ports(self, proto, from_port, to_port):
+ rule = {
+ 'ip_protocol': proto, 'from_port': from_port, 'to_port': to_port,
+ 'parent_group_id': self.sg2['id'], 'group_id': self.sg1['id']
+ }
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+
+ security_group_rule = res_dict['security_group_rule']
+ expected_rule = {
+ 'from_port': from_port,
+ 'group': {'tenant_id': '123', 'name': 'test'},
+ 'ip_protocol': proto, 'to_port': to_port, 'parent_group_id':
+ self.sg2['id'], 'ip_range': {}, 'id': security_group_rule['id']
+ }
+ self.assertEqual(proto, security_group_rule['ip_protocol'])
+ self.assertEqual(from_port, security_group_rule['from_port'])
+ self.assertEqual(to_port, security_group_rule['to_port'])
+ self.assertEqual(expected_rule, security_group_rule)
+
+ def test_create_with_ports_icmp(self):
+ self._test_create_with_ports('icmp', 0, 1)
+ self._test_create_with_ports('icmp', 0, 0)
+ self._test_create_with_ports('icmp', 1, 0)
+
+ def test_create_with_ports_tcp(self):
+ self._test_create_with_ports('tcp', 1, 1)
+ self._test_create_with_ports('tcp', 1, 65535)
+ self._test_create_with_ports('tcp', 65535, 65535)
+
+ def test_create_with_ports_udp(self):
+ self._test_create_with_ports('udp', 1, 1)
+ self._test_create_with_ports('udp', 1, 65535)
+ self._test_create_with_ports('udp', 65535, 65535)
+
+ def test_delete(self):
+ rule = security_group_rule_template(id=self.sg2['id'],
+ parent_group_id=self.sg2['id'])
+
+ def security_group_rule_get(context, id):
+ return security_group_rule_db(rule)
+
+ def security_group_rule_destroy(context, id):
+ pass
+
+ self.stubs.Set(nova.db, 'security_group_rule_get',
+ security_group_rule_get)
+ self.stubs.Set(nova.db, 'security_group_rule_destroy',
+ security_group_rule_destroy)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules/%s'
+ % self.sg2['id'])
+ self.controller.delete(req, self.sg2['id'])
+
+ def test_delete_invalid_rule_id(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules' +
+ '/invalid')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
+ req, 'invalid')
+
+ def test_delete_non_existing_rule_id(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules/%s'
+ % self.invalid_id)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, self.invalid_id)
+
+ def test_create_rule_quota_limit(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ for num in range(100, 100 + CONF.quota_security_group_rules):
+ rule = {
+ 'ip_protocol': 'tcp', 'from_port': num,
+ 'to_port': num, 'parent_group_id': self.sg2['id'],
+ 'group_id': self.sg1['id']
+ }
+ self.controller.create(req, {'security_group_rule': rule})
+
+ rule = {
+ 'ip_protocol': 'tcp', 'from_port': '121', 'to_port': '121',
+ 'parent_group_id': self.sg2['id'], 'group_id': self.sg1['id']
+ }
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.create,
+ req, {'security_group_rule': rule})
+
+ def test_create_rule_cidr_allow_all(self):
+ rule = security_group_rule_template(cidr='0.0.0.0/0',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+
+ security_group_rule = res_dict['security_group_rule']
+ self.assertNotEqual(security_group_rule['id'], 0)
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.parent_security_group['id'])
+ self.assertEqual(security_group_rule['ip_range']['cidr'],
+ "0.0.0.0/0")
+
+ def test_create_rule_cidr_ipv6_allow_all(self):
+ rule = security_group_rule_template(cidr='::/0',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+
+ security_group_rule = res_dict['security_group_rule']
+ self.assertNotEqual(security_group_rule['id'], 0)
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.parent_security_group['id'])
+ self.assertEqual(security_group_rule['ip_range']['cidr'],
+ "::/0")
+
+ def test_create_rule_cidr_allow_some(self):
+ rule = security_group_rule_template(cidr='15.0.0.0/8',
+ parent_group_id=self.sg2['id'])
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ res_dict = self.controller.create(req, {'security_group_rule': rule})
+
+ security_group_rule = res_dict['security_group_rule']
+ self.assertNotEqual(security_group_rule['id'], 0)
+ self.assertEqual(security_group_rule['parent_group_id'],
+ self.parent_security_group['id'])
+ self.assertEqual(security_group_rule['ip_range']['cidr'],
+ "15.0.0.0/8")
+
+ def test_create_rule_cidr_bad_netmask(self):
+ rule = security_group_rule_template(cidr='15.0.0.0/0')
+ req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'security_group_rule': rule})
+
+
+class TestSecurityGroupRulesV2(TestSecurityGroupRulesV21):
+ secgrp_ctl_cls = secgroups_v2.SecurityGroupRulesController
+
+
+class TestSecurityGroupRulesXMLDeserializer(test.TestCase):
+
+ def setUp(self):
+ super(TestSecurityGroupRulesXMLDeserializer, self).setUp()
+ self.deserializer = secgroups_v2.SecurityGroupRulesXMLDeserializer()
+
+ def test_create_request(self):
+ serial_request = """
+<security_group_rule>
+ <parent_group_id>12</parent_group_id>
+ <from_port>22</from_port>
+ <to_port>22</to_port>
+ <group_id></group_id>
+ <ip_protocol>tcp</ip_protocol>
+ <cidr>10.0.0.0/24</cidr>
+</security_group_rule>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group_rule": {
+ "parent_group_id": "12",
+ "from_port": "22",
+ "to_port": "22",
+ "ip_protocol": "tcp",
+ "group_id": "",
+ "cidr": "10.0.0.0/24",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_create_no_protocol_request(self):
+ serial_request = """
+<security_group_rule>
+ <parent_group_id>12</parent_group_id>
+ <from_port>22</from_port>
+ <to_port>22</to_port>
+ <group_id></group_id>
+ <cidr>10.0.0.0/24</cidr>
+</security_group_rule>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group_rule": {
+ "parent_group_id": "12",
+ "from_port": "22",
+ "to_port": "22",
+ "group_id": "",
+ "cidr": "10.0.0.0/24",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_corrupt_xml(self):
+ """Should throw a 400 error on corrupt xml."""
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ self.deserializer.deserialize,
+ utils.killer_xml_body())
+
+
+class TestSecurityGroupXMLDeserializer(test.TestCase):
+
+ def setUp(self):
+ super(TestSecurityGroupXMLDeserializer, self).setUp()
+ self.deserializer = secgroups_v2.SecurityGroupXMLDeserializer()
+
+ def test_create_request(self):
+ serial_request = """
+<security_group name="test">
+ <description>test</description>
+</security_group>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group": {
+ "name": "test",
+ "description": "test",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_create_no_description_request(self):
+ serial_request = """
+<security_group name="test">
+</security_group>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group": {
+ "name": "test",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_create_no_name_request(self):
+ serial_request = """
+<security_group>
+<description>test</description>
+</security_group>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "security_group": {
+ "description": "test",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_corrupt_xml(self):
+ """Should throw a 400 error on corrupt xml."""
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ self.deserializer.deserialize,
+ utils.killer_xml_body())
+
+
+class TestSecurityGroupXMLSerializer(test.TestCase):
+ def setUp(self):
+ super(TestSecurityGroupXMLSerializer, self).setUp()
+ self.namespace = wsgi.XMLNS_V11
+ self.rule_serializer = secgroups_v2.SecurityGroupRuleTemplate()
+ self.index_serializer = secgroups_v2.SecurityGroupsTemplate()
+ self.default_serializer = secgroups_v2.SecurityGroupTemplate()
+
+ def _tag(self, elem):
+ tagname = elem.tag
+ self.assertEqual(tagname[0], '{')
+ tmp = tagname.partition('}')
+ namespace = tmp[0][1:]
+ self.assertEqual(namespace, self.namespace)
+ return tmp[2]
+
+ def _verify_security_group_rule(self, raw_rule, tree):
+ self.assertEqual(raw_rule['id'], tree.get('id'))
+ self.assertEqual(raw_rule['parent_group_id'],
+ tree.get('parent_group_id'))
+
+ seen = set()
+ expected = set(['ip_protocol', 'from_port', 'to_port',
+ 'group', 'group/name', 'group/tenant_id',
+ 'ip_range', 'ip_range/cidr'])
+
+ for child in tree:
+ child_tag = self._tag(child)
+ self.assertIn(child_tag, raw_rule)
+ seen.add(child_tag)
+ if child_tag in ('group', 'ip_range'):
+ for gr_child in child:
+ gr_child_tag = self._tag(gr_child)
+ self.assertIn(gr_child_tag, raw_rule[child_tag])
+ seen.add('%s/%s' % (child_tag, gr_child_tag))
+ self.assertEqual(gr_child.text,
+ raw_rule[child_tag][gr_child_tag])
+ else:
+ self.assertEqual(child.text, raw_rule[child_tag])
+ self.assertEqual(seen, expected)
+
+ def _verify_security_group(self, raw_group, tree):
+ rules = raw_group['rules']
+ self.assertEqual('security_group', self._tag(tree))
+ self.assertEqual(raw_group['id'], tree.get('id'))
+ self.assertEqual(raw_group['tenant_id'], tree.get('tenant_id'))
+ self.assertEqual(raw_group['name'], tree.get('name'))
+ self.assertEqual(2, len(tree))
+ for child in tree:
+ child_tag = self._tag(child)
+ if child_tag == 'rules':
+ self.assertEqual(2, len(child))
+ for idx, gr_child in enumerate(child):
+ self.assertEqual(self._tag(gr_child), 'rule')
+ self._verify_security_group_rule(rules[idx], gr_child)
+ else:
+ self.assertEqual('description', child_tag)
+ self.assertEqual(raw_group['description'], child.text)
+
+ def test_rule_serializer(self):
+ raw_rule = dict(
+ id='123',
+ parent_group_id='456',
+ ip_protocol='tcp',
+ from_port='789',
+ to_port='987',
+ group=dict(name='group', tenant_id='tenant'),
+ ip_range=dict(cidr='10.0.0.0/8'))
+ rule = dict(security_group_rule=raw_rule)
+ text = self.rule_serializer.serialize(rule)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('security_group_rule', self._tag(tree))
+ self._verify_security_group_rule(raw_rule, tree)
+
+ def test_group_serializer(self):
+ rules = [dict(
+ id='123',
+ parent_group_id='456',
+ ip_protocol='tcp',
+ from_port='789',
+ to_port='987',
+ group=dict(name='group1', tenant_id='tenant1'),
+ ip_range=dict(cidr='10.55.44.0/24')),
+ dict(
+ id='654',
+ parent_group_id='321',
+ ip_protocol='udp',
+ from_port='234',
+ to_port='567',
+ group=dict(name='group2', tenant_id='tenant2'),
+ ip_range=dict(cidr='10.44.55.0/24'))]
+ raw_group = dict(
+ id='890',
+ description='description',
+ name='name',
+ tenant_id='tenant',
+ rules=rules)
+ sg_group = dict(security_group=raw_group)
+ text = self.default_serializer.serialize(sg_group)
+
+ tree = etree.fromstring(text)
+
+ self._verify_security_group(raw_group, tree)
+
+ def test_groups_serializer(self):
+ rules = [dict(
+ id='123',
+ parent_group_id='1234',
+ ip_protocol='tcp',
+ from_port='12345',
+ to_port='123456',
+ group=dict(name='group1', tenant_id='tenant1'),
+ ip_range=dict(cidr='10.123.0.0/24')),
+ dict(
+ id='234',
+ parent_group_id='2345',
+ ip_protocol='udp',
+ from_port='23456',
+ to_port='234567',
+ group=dict(name='group2', tenant_id='tenant2'),
+ ip_range=dict(cidr='10.234.0.0/24')),
+ dict(
+ id='345',
+ parent_group_id='3456',
+ ip_protocol='tcp',
+ from_port='34567',
+ to_port='345678',
+ group=dict(name='group3', tenant_id='tenant3'),
+ ip_range=dict(cidr='10.345.0.0/24')),
+ dict(
+ id='456',
+ parent_group_id='4567',
+ ip_protocol='udp',
+ from_port='45678',
+ to_port='456789',
+ group=dict(name='group4', tenant_id='tenant4'),
+ ip_range=dict(cidr='10.456.0.0/24'))]
+ groups = [dict(
+ id='567',
+ description='description1',
+ name='name1',
+ tenant_id='tenant1',
+ rules=rules[0:2]),
+ dict(
+ id='678',
+ description='description2',
+ name='name2',
+ tenant_id='tenant2',
+ rules=rules[2:4])]
+ sg_groups = dict(security_groups=groups)
+ text = self.index_serializer.serialize(sg_groups)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('security_groups', self._tag(tree))
+ self.assertEqual(len(groups), len(tree))
+ for idx, child in enumerate(tree):
+ self._verify_security_group(groups[idx], child)
+
+
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+UUID3 = '00000000-0000-0000-0000-000000000003'
+
+
+def fake_compute_get_all(*args, **kwargs):
+ base = {'id': 1, 'description': 'foo', 'user_id': 'bar',
+ 'project_id': 'baz', 'deleted': False, 'deleted_at': None,
+ 'updated_at': None, 'created_at': None}
+ db_list = [
+ fakes.stub_instance(
+ 1, uuid=UUID1,
+ security_groups=[dict(base, **{'name': 'fake-0-0'}),
+ dict(base, **{'name': 'fake-0-1'})]),
+ fakes.stub_instance(
+ 2, uuid=UUID2,
+ security_groups=[dict(base, **{'name': 'fake-1-0'}),
+ dict(base, **{'name': 'fake-1-1'})])
+ ]
+
+ return instance_obj._make_instance_list(args[1],
+ objects.InstanceList(),
+ db_list,
+ ['metadata', 'system_metadata',
+ 'security_groups', 'info_cache'])
+
+
+def fake_compute_get(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3,
+ security_groups=[{'name': 'fake-2-0'},
+ {'name': 'fake-2-1'}])
+ return fake_instance.fake_instance_obj(args[1],
+ expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS, **inst)
+
+
+def fake_compute_create(*args, **kwargs):
+ return ([fake_compute_get(*args, **kwargs)], '')
+
+
+def fake_get_instances_security_groups_bindings(inst, context, servers):
+ groups = {UUID1: [{'name': 'fake-0-0'}, {'name': 'fake-0-1'}],
+ UUID2: [{'name': 'fake-1-0'}, {'name': 'fake-1-1'}],
+ UUID3: [{'name': 'fake-2-0'}, {'name': 'fake-2-1'}]}
+ result = {}
+ for server in servers:
+ result[server['id']] = groups.get(server['id'])
+ return result
+
+
+class SecurityGroupsOutputTestV21(test.TestCase):
+ base_url = '/v2/fake/servers'
+ content_type = 'application/json'
+
+ def setUp(self):
+ super(SecurityGroupsOutputTestV21, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
+ self.stubs.Set(compute.api.API, 'create', fake_compute_create)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Security_groups'])
+ self.app = self._setup_app()
+
+ def _setup_app(self):
+ return fakes.wsgi_app_v21(init_only=('os-security-groups', 'servers'))
+
+ def _make_request(self, url, body=None):
+ req = webob.Request.blank(url)
+ if body:
+ req.method = 'POST'
+ req.body = self._encode_body(body)
+ req.content_type = self.content_type
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(self.app)
+ return res
+
+ def _encode_body(self, body):
+ return jsonutils.dumps(body)
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def _get_groups(self, server):
+ return server.get('security_groups')
+
+ def test_create(self):
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ server = dict(name='server_test', imageRef=image_uuid, flavorRef=2)
+ res = self._make_request(self.base_url, {'server': server})
+ self.assertEqual(res.status_int, 202)
+ server = self._get_server(res.body)
+ for i, group in enumerate(self._get_groups(server)):
+ name = 'fake-2-%s' % i
+ self.assertEqual(group.get('name'), name)
+
+ def test_show(self):
+ url = self.base_url + '/' + UUID3
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ server = self._get_server(res.body)
+ for i, group in enumerate(self._get_groups(server)):
+ name = 'fake-2-%s' % i
+ self.assertEqual(group.get('name'), name)
+
+ def test_detail(self):
+ url = self.base_url + '/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ for i, server in enumerate(self._get_servers(res.body)):
+ for j, group in enumerate(self._get_groups(server)):
+ name = 'fake-%s-%s' % (i, j)
+ self.assertEqual(group.get('name'), name)
+
+ def test_no_instance_passthrough_404(self):
+
+ def fake_compute_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ url = self.base_url + '/70f6db34-de8d-4fbd-aafb-4065bdfa6115'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 404)
+
+
+class SecurityGroupsOutputTestV2(SecurityGroupsOutputTestV21):
+
+ def _setup_app(self):
+ return fakes.wsgi_app(init_only=('servers',))
+
+
+class SecurityGroupsOutputXmlTest(SecurityGroupsOutputTestV2):
+ content_type = 'application/xml'
+
+ class MinimalCreateServerTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('server', selector='server')
+ root.set('name')
+ root.set('id')
+ root.set('imageRef')
+ root.set('flavorRef')
+ return xmlutil.MasterTemplate(root, 1,
+ nsmap={None: xmlutil.XMLNS_V11})
+
+ def _encode_body(self, body):
+ serializer = self.MinimalCreateServerTemplate()
+ return serializer.serialize(body)
+
+ def _get_server(self, body):
+ return etree.XML(body)
+
+ def _get_servers(self, body):
+ return etree.XML(body).getchildren()
+
+ def _get_groups(self, server):
+ # NOTE(vish): we are adding security groups without an extension
+ # namespace so we don't break people using the existing
+ # functionality, but that means we need to use find with
+ # the existing server namespace.
+ namespace = server.nsmap[None]
+ return server.find('{%s}security_groups' % namespace).getchildren()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_diagnostics.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_diagnostics.py
new file mode 100644
index 0000000000..535a1afa15
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_diagnostics.py
@@ -0,0 +1,132 @@
+# Copyright 2011 Eldar Nugaev
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+from lxml import etree
+import mock
+from oslo.serialization import jsonutils
+
+from nova.api.openstack import compute
+from nova.api.openstack.compute.contrib import server_diagnostics
+from nova.api.openstack import wsgi
+from nova.compute import api as compute_api
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+UUID = 'abc'
+
+
+def fake_get_diagnostics(self, _context, instance_uuid):
+ return {'data': 'Some diagnostic info'}
+
+
+def fake_instance_get(self, _context, instance_uuid, want_objects=False,
+ expected_attrs=None):
+ if instance_uuid != UUID:
+ raise Exception("Invalid UUID")
+ return {'uuid': instance_uuid}
+
+
+class ServerDiagnosticsTestV21(test.NoDBTestCase):
+
+ def _setup_router(self):
+ self.router = compute.APIRouterV3(init_only=('servers',
+ 'os-server-diagnostics'))
+
+ def _get_request(self):
+ return fakes.HTTPRequestV3.blank(
+ '/servers/%s/diagnostics' % UUID)
+
+ def setUp(self):
+ super(ServerDiagnosticsTestV21, self).setUp()
+ self._setup_router()
+
+ @mock.patch.object(compute_api.API, 'get_diagnostics',
+ fake_get_diagnostics)
+ @mock.patch.object(compute_api.API, 'get',
+ fake_instance_get)
+ def test_get_diagnostics(self):
+ req = self._get_request()
+ res = req.get_response(self.router)
+ output = jsonutils.loads(res.body)
+ self.assertEqual(output, {'data': 'Some diagnostic info'})
+
+ @mock.patch.object(compute_api.API, 'get_diagnostics',
+ fake_get_diagnostics)
+ @mock.patch.object(compute_api.API, 'get',
+ side_effect=exception.InstanceNotFound(instance_id=UUID))
+ def test_get_diagnostics_with_non_existed_instance(self, mock_get):
+ req = self._get_request()
+ res = req.get_response(self.router)
+ self.assertEqual(res.status_int, 404)
+
+ @mock.patch.object(compute_api.API, 'get_diagnostics',
+ side_effect=exception.InstanceInvalidState('fake message'))
+ @mock.patch.object(compute_api.API, 'get', fake_instance_get)
+ def test_get_diagnostics_raise_conflict_on_invalid_state(self,
+ mock_get_diagnostics):
+ req = self._get_request()
+ res = req.get_response(self.router)
+ self.assertEqual(409, res.status_int)
+
+ @mock.patch.object(compute_api.API, 'get_diagnostics',
+ side_effect=NotImplementedError)
+ @mock.patch.object(compute_api.API, 'get', fake_instance_get)
+ def test_get_diagnostics_raise_no_notimplementederror(self,
+ mock_get_diagnostics):
+ req = self._get_request()
+ res = req.get_response(self.router)
+ self.assertEqual(501, res.status_int)
+
+
+class ServerDiagnosticsTestV2(ServerDiagnosticsTestV21):
+
+ def _setup_router(self):
+ self.flags(verbose=True,
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Server_diagnostics'])
+
+ self.router = compute.APIRouter(init_only=('servers', 'diagnostics'))
+
+ def _get_request(self):
+ return fakes.HTTPRequest.blank(
+ '/fake/servers/%s/diagnostics' % UUID)
+
+
+class TestServerDiagnosticsXMLSerializer(test.NoDBTestCase):
+ namespace = wsgi.XMLNS_V11
+
+ def _tag(self, elem):
+ tagname = elem.tag
+ self.assertEqual(tagname[0], '{')
+ tmp = tagname.partition('}')
+ namespace = tmp[0][1:]
+ self.assertEqual(namespace, self.namespace)
+ return tmp[2]
+
+ def test_index_serializer(self):
+ serializer = server_diagnostics.ServerDiagnosticsTemplate()
+ exemplar = dict(diag1='foo', diag2='bar')
+ text = serializer.serialize(exemplar)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('diagnostics', self._tag(tree))
+ self.assertEqual(len(tree), len(exemplar))
+ for child in tree:
+ tag = self._tag(child)
+ self.assertIn(tag, exemplar)
+ self.assertEqual(child.text, exemplar[tag])
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_external_events.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_external_events.py
new file mode 100644
index 0000000000..61801ba648
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_external_events.py
@@ -0,0 +1,158 @@
+# Copyright 2014 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import mock
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import server_external_events
+from nova import context
+from nova import exception
+from nova import objects
+from nova import test
+
+fake_instances = {
+ '00000000-0000-0000-0000-000000000001': objects.Instance(
+ uuid='00000000-0000-0000-0000-000000000001', host='host1'),
+ '00000000-0000-0000-0000-000000000002': objects.Instance(
+ uuid='00000000-0000-0000-0000-000000000002', host='host1'),
+ '00000000-0000-0000-0000-000000000003': objects.Instance(
+ uuid='00000000-0000-0000-0000-000000000003', host='host2'),
+ '00000000-0000-0000-0000-000000000004': objects.Instance(
+ uuid='00000000-0000-0000-0000-000000000004', host=None),
+}
+fake_instance_uuids = sorted(fake_instances.keys())
+MISSING_UUID = '00000000-0000-0000-0000-000000000005'
+
+
+@classmethod
+def fake_get_by_uuid(cls, context, uuid):
+ try:
+ return fake_instances[uuid]
+ except KeyError:
+ raise exception.InstanceNotFound(instance_id=uuid)
+
+
+@mock.patch('nova.objects.instance.Instance.get_by_uuid', fake_get_by_uuid)
+class ServerExternalEventsTest(test.NoDBTestCase):
+ def setUp(self):
+ super(ServerExternalEventsTest, self).setUp()
+ self.api = server_external_events.ServerExternalEventsController()
+ self.context = context.get_admin_context()
+ self.event_1 = {'name': 'network-vif-plugged',
+ 'tag': 'foo',
+ 'server_uuid': fake_instance_uuids[0]}
+ self.event_2 = {'name': 'network-changed',
+ 'server_uuid': fake_instance_uuids[1]}
+ self.default_body = {'events': [self.event_1, self.event_2]}
+ self.resp_event_1 = dict(self.event_1)
+ self.resp_event_1['code'] = 200
+ self.resp_event_1['status'] = 'completed'
+ self.resp_event_2 = dict(self.event_2)
+ self.resp_event_2['code'] = 200
+ self.resp_event_2['status'] = 'completed'
+ self.default_resp_body = {'events': [self.resp_event_1,
+ self.resp_event_2]}
+
+ def _create_req(self, body):
+ req = webob.Request.blank('/v2/fake/os-server-external-events')
+ req.method = 'POST'
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ req.body = jsonutils.dumps(body)
+ return req
+
+ def _assert_call(self, req, body, expected_uuids, expected_events):
+ with mock.patch.object(self.api.compute_api,
+ 'external_instance_event') as api_method:
+ response = self.api.create(req, body)
+
+ result = response.obj
+ code = response._code
+
+ self.assertEqual(1, api_method.call_count)
+ for inst in api_method.call_args_list[0][0][1]:
+ expected_uuids.remove(inst.uuid)
+ self.assertEqual([], expected_uuids)
+ for event in api_method.call_args_list[0][0][2]:
+ expected_events.remove(event.name)
+ self.assertEqual([], expected_events)
+ return result, code
+
+ def test_create(self):
+ req = self._create_req(self.default_body)
+ result, code = self._assert_call(req, self.default_body,
+ fake_instance_uuids[:2],
+ ['network-vif-plugged',
+ 'network-changed'])
+ self.assertEqual(self.default_resp_body, result)
+ self.assertEqual(200, code)
+
+ def test_create_one_bad_instance(self):
+ body = self.default_body
+ body['events'][1]['server_uuid'] = MISSING_UUID
+ req = self._create_req(body)
+ result, code = self._assert_call(req, body, [fake_instance_uuids[0]],
+ ['network-vif-plugged'])
+ self.assertEqual('failed', result['events'][1]['status'])
+ self.assertEqual(200, result['events'][0]['code'])
+ self.assertEqual(404, result['events'][1]['code'])
+ self.assertEqual(207, code)
+
+ def test_create_event_instance_has_no_host(self):
+ body = self.default_body
+ body['events'][0]['server_uuid'] = fake_instance_uuids[-1]
+ req = self._create_req(body)
+ # the instance without host should not be passed to the compute layer
+ result, code = self._assert_call(req, body,
+ [fake_instance_uuids[1]],
+ ['network-changed'])
+ self.assertEqual(422, result['events'][0]['code'])
+ self.assertEqual('failed', result['events'][0]['status'])
+ self.assertEqual(200, result['events'][1]['code'])
+ self.assertEqual(207, code)
+
+ def test_create_no_good_instances(self):
+ body = self.default_body
+ body['events'][0]['server_uuid'] = MISSING_UUID
+ body['events'][1]['server_uuid'] = MISSING_UUID
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.api.create, req, body)
+
+ def test_create_bad_status(self):
+ body = self.default_body
+ body['events'][1]['status'] = 'foo'
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.api.create, req, body)
+
+ def test_create_extra_gorp(self):
+ body = self.default_body
+ body['events'][0]['foobar'] = 'bad stuff'
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.api.create, req, body)
+
+ def test_create_bad_events(self):
+ body = {'events': 'foo'}
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.api.create, req, body)
+
+ def test_create_bad_body(self):
+ body = {'foo': 'bar'}
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.api.create, req, body)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_group_quotas.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_group_quotas.py
new file mode 100644
index 0000000000..9e756cf157
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_group_quotas.py
@@ -0,0 +1,188 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.config import cfg
+import webob
+
+from nova.api.openstack.compute.contrib import server_groups
+from nova.api.openstack.compute.plugins.v3 import server_groups as sg_v3
+from nova.api.openstack import extensions
+from nova import context
+import nova.db
+from nova.openstack.common import uuidutils
+from nova import quota
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+CONF = cfg.CONF
+
+
+class AttrDict(dict):
+ def __getattr__(self, k):
+ return self[k]
+
+
+def server_group_template(**kwargs):
+ sgroup = kwargs.copy()
+ sgroup.setdefault('name', 'test')
+ return sgroup
+
+
+def server_group_db(sg):
+ attrs = sg.copy()
+ if 'id' in attrs:
+ attrs['uuid'] = attrs.pop('id')
+ if 'policies' in attrs:
+ policies = attrs.pop('policies')
+ attrs['policies'] = policies
+ else:
+ attrs['policies'] = []
+ if 'members' in attrs:
+ members = attrs.pop('members')
+ attrs['members'] = members
+ else:
+ attrs['members'] = []
+ if 'metadata' in attrs:
+ attrs['metadetails'] = attrs.pop('metadata')
+ else:
+ attrs['metadetails'] = {}
+ attrs['deleted'] = 0
+ attrs['deleted_at'] = None
+ attrs['created_at'] = None
+ attrs['updated_at'] = None
+ if 'user_id' not in attrs:
+ attrs['user_id'] = 'user_id'
+ if 'project_id' not in attrs:
+ attrs['project_id'] = 'project_id'
+ attrs['id'] = 7
+
+ return AttrDict(attrs)
+
+
+class ServerGroupQuotasTestV21(test.TestCase):
+
+ def setUp(self):
+ super(ServerGroupQuotasTestV21, self).setUp()
+ self._setup_controller()
+ self.app = self._get_app()
+
+ def _setup_controller(self):
+ self.controller = sg_v3.ServerGroupController()
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21(init_only=('os-server-groups',))
+
+ def _get_url(self):
+ return '/v2/fake'
+
+ def _setup_quotas(self):
+ pass
+
+ def _assert_server_groups_in_use(self, project_id, user_id, in_use):
+ ctxt = context.get_admin_context()
+ result = quota.QUOTAS.get_user_quotas(ctxt, project_id, user_id)
+ self.assertEqual(result['server_groups']['in_use'], in_use)
+
+ def test_create_server_group_normal(self):
+ self._setup_quotas()
+ req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups')
+ sgroup = server_group_template()
+ policies = ['anti-affinity']
+ sgroup['policies'] = policies
+ res_dict = self.controller.create(req, {'server_group': sgroup})
+ self.assertEqual(res_dict['server_group']['name'], 'test')
+ self.assertTrue(uuidutils.is_uuid_like(res_dict['server_group']['id']))
+ self.assertEqual(res_dict['server_group']['policies'], policies)
+
+ def test_create_server_group_quota_limit(self):
+ self._setup_quotas()
+ req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups')
+ sgroup = server_group_template()
+ policies = ['anti-affinity']
+ sgroup['policies'] = policies
+ # Start by creating as many server groups as we're allowed to.
+ for i in range(CONF.quota_server_groups):
+ self.controller.create(req, {'server_group': sgroup})
+
+ # Then, creating a server group should fail.
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.create,
+ req, {'server_group': sgroup})
+
+ def test_delete_server_group_by_admin(self):
+ self._setup_quotas()
+ sgroup = server_group_template()
+ policies = ['anti-affinity']
+ sgroup['policies'] = policies
+ req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups')
+ res = self.controller.create(req, {'server_group': sgroup})
+ sg_id = res['server_group']['id']
+ context = req.environ['nova.context']
+
+ self._assert_server_groups_in_use(context.project_id,
+ context.user_id, 1)
+
+ # Delete the server group we've just created.
+ req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups/%s' % sg_id,
+ use_admin_context=True)
+ self.controller.delete(req, sg_id)
+
+ # Make sure the quota in use has been released.
+ self._assert_server_groups_in_use(context.project_id,
+ context.user_id, 0)
+
+ def test_delete_server_group_by_id(self):
+ self._setup_quotas()
+ sg = server_group_template(id='123')
+ self.called = False
+
+ def server_group_delete(context, id):
+ self.called = True
+
+ def return_server_group(context, group_id):
+ self.assertEqual(sg['id'], group_id)
+ return server_group_db(sg)
+
+ self.stubs.Set(nova.db, 'instance_group_delete',
+ server_group_delete)
+ self.stubs.Set(nova.db, 'instance_group_get',
+ return_server_group)
+
+ req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups/123')
+ resp = self.controller.delete(req, '123')
+ self.assertTrue(self.called)
+
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.controller, sg_v3.ServerGroupController):
+ status_int = self.controller.delete.wsgi_code
+ else:
+ status_int = resp.status_int
+ self.assertEqual(204, status_int)
+
+
+class ServerGroupQuotasTestV2(ServerGroupQuotasTestV21):
+
+ def _setup_controller(self):
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.controller = server_groups.ServerGroupController(self.ext_mgr)
+
+ def _setup_quotas(self):
+ self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes()\
+ .AndReturn(True)
+ self.mox.ReplayAll()
+
+ def _get_app(self):
+ return fakes.wsgi_app(init_only=('os-server-groups',))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_groups.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_groups.py
new file mode 100644
index 0000000000..7dd2675c9e
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_groups.py
@@ -0,0 +1,521 @@
+# Copyright (c) 2014 Cisco Systems, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+import webob
+
+from nova.api.openstack.compute.contrib import server_groups
+from nova.api.openstack.compute.plugins.v3 import server_groups as sg_v3
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova import context
+import nova.db
+from nova import exception
+from nova import objects
+from nova.openstack.common import uuidutils
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import utils
+
+FAKE_UUID1 = 'a47ae74e-ab08-447f-8eee-ffd43fc46c16'
+FAKE_UUID2 = 'c6e6430a-6563-4efa-9542-5e93c9e97d18'
+FAKE_UUID3 = 'b8713410-9ba3-e913-901b-13410ca90121'
+
+
+class AttrDict(dict):
+ def __getattr__(self, k):
+ return self[k]
+
+
+def server_group_template(**kwargs):
+ sgroup = kwargs.copy()
+ sgroup.setdefault('name', 'test')
+ return sgroup
+
+
+def server_group_resp_template(**kwargs):
+ sgroup = kwargs.copy()
+ sgroup.setdefault('name', 'test')
+ sgroup.setdefault('policies', [])
+ sgroup.setdefault('members', [])
+ return sgroup
+
+
+def server_group_db(sg):
+ attrs = sg.copy()
+ if 'id' in attrs:
+ attrs['uuid'] = attrs.pop('id')
+ if 'policies' in attrs:
+ policies = attrs.pop('policies')
+ attrs['policies'] = policies
+ else:
+ attrs['policies'] = []
+ if 'members' in attrs:
+ members = attrs.pop('members')
+ attrs['members'] = members
+ else:
+ attrs['members'] = []
+ attrs['deleted'] = 0
+ attrs['deleted_at'] = None
+ attrs['created_at'] = None
+ attrs['updated_at'] = None
+ if 'user_id' not in attrs:
+ attrs['user_id'] = 'user_id'
+ if 'project_id' not in attrs:
+ attrs['project_id'] = 'project_id'
+ attrs['id'] = 7
+
+ return AttrDict(attrs)
+
+
+class ServerGroupTestV21(test.TestCase):
+
+ def setUp(self):
+ super(ServerGroupTestV21, self).setUp()
+ self._setup_controller()
+ self.app = self._get_app()
+
+ def _setup_controller(self):
+ self.controller = sg_v3.ServerGroupController()
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21(init_only=('os-server-groups',))
+
+ def _get_url(self):
+ return '/v2/fake'
+
+ def test_create_server_group_with_no_policies(self):
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ sgroup = server_group_template()
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ def test_create_server_group_normal(self):
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ sgroup = server_group_template()
+ policies = ['anti-affinity']
+ sgroup['policies'] = policies
+ res_dict = self.controller.create(req, {'server_group': sgroup})
+ self.assertEqual(res_dict['server_group']['name'], 'test')
+ self.assertTrue(uuidutils.is_uuid_like(res_dict['server_group']['id']))
+ self.assertEqual(res_dict['server_group']['policies'], policies)
+
+ def _create_instance(self, context):
+ instance = objects.Instance(image_ref=1, node='node1',
+ reservation_id='a', host='host1', project_id='fake',
+ vm_state='fake', system_metadata={'key': 'value'})
+ instance.create(context)
+ return instance
+
+ def _create_instance_group(self, context, members):
+ ig = objects.InstanceGroup(name='fake_name',
+ user_id='fake_user', project_id='fake',
+ members=members)
+ ig.create(context)
+ return ig.uuid
+
+ def _create_groups_and_instances(self, ctx):
+ instances = [self._create_instance(ctx), self._create_instance(ctx)]
+ members = [instance.uuid for instance in instances]
+ ig_uuid = self._create_instance_group(ctx, members)
+ return (ig_uuid, instances, members)
+
+ def test_display_members(self):
+ ctx = context.RequestContext('fake_user', 'fake')
+ (ig_uuid, instances, members) = self._create_groups_and_instances(ctx)
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ res_dict = self.controller.show(req, ig_uuid)
+ result_members = res_dict['server_group']['members']
+ self.assertEqual(2, len(result_members))
+ for member in members:
+ self.assertIn(member, result_members)
+
+ def test_display_active_members_only(self):
+ ctx = context.RequestContext('fake_user', 'fake')
+ (ig_uuid, instances, members) = self._create_groups_and_instances(ctx)
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+
+ # delete an instance
+ instances[1].destroy(ctx)
+ # check that the instance does not exist
+ self.assertRaises(exception.InstanceNotFound,
+ objects.Instance.get_by_uuid,
+ ctx, instances[1].uuid)
+ res_dict = self.controller.show(req, ig_uuid)
+ result_members = res_dict['server_group']['members']
+ # check that only the active instance is displayed
+ self.assertEqual(1, len(result_members))
+ self.assertIn(instances[0].uuid, result_members)
+
+ def test_create_server_group_with_illegal_name(self):
+ # blank name
+ sgroup = server_group_template(name='', policies=['test_policy'])
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ # name with length 256
+ sgroup = server_group_template(name='1234567890' * 26,
+ policies=['test_policy'])
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ # non-string name
+ sgroup = server_group_template(name=12, policies=['test_policy'])
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ # name with leading spaces
+ sgroup = server_group_template(name=' leading spaces',
+ policies=['test_policy'])
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ # name with trailing spaces
+ sgroup = server_group_template(name='trailing space ',
+ policies=['test_policy'])
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ # name with all spaces
+ sgroup = server_group_template(name=' ',
+ policies=['test_policy'])
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ def test_create_server_group_with_illegal_policies(self):
+ # blank policy
+ sgroup = server_group_template(name='fake-name', policies='')
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ # policy as integer
+ sgroup = server_group_template(name='fake-name', policies=7)
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ # policy as string
+ sgroup = server_group_template(name='fake-name', policies='invalid')
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ # policy as None
+ sgroup = server_group_template(name='fake-name', policies=None)
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ def test_create_server_group_conflicting_policies(self):
+ sgroup = server_group_template()
+ policies = ['anti-affinity', 'affinity']
+ sgroup['policies'] = policies
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ def test_create_server_group_with_duplicate_policies(self):
+ sgroup = server_group_template()
+ policies = ['affinity', 'affinity']
+ sgroup['policies'] = policies
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ def test_create_server_group_not_supported(self):
+ sgroup = server_group_template()
+ policies = ['storage-affinity', 'anti-affinity', 'rack-affinity']
+ sgroup['policies'] = policies
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, {'server_group': sgroup})
+
+ def test_create_server_group_with_no_body(self):
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, None)
+
+ def test_create_server_group_with_no_server_group(self):
+ body = {'no-instanceGroup': None}
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, body)
+
+ def test_list_server_group_by_tenant(self):
+ groups = []
+ policies = ['anti-affinity']
+ members = []
+ metadata = {} # always empty
+ names = ['default-x', 'test']
+ sg1 = server_group_resp_template(id=str(1345),
+ name=names[0],
+ policies=policies,
+ members=members,
+ metadata=metadata)
+ sg2 = server_group_resp_template(id=str(891),
+ name=names[1],
+ policies=policies,
+ members=members,
+ metadata=metadata)
+ groups = [sg1, sg2]
+ expected = {'server_groups': groups}
+
+ def return_server_groups(context, project_id):
+ return [server_group_db(sg) for sg in groups]
+
+ self.stubs.Set(nova.db, 'instance_group_get_all_by_project_id',
+ return_server_groups)
+
+ req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups')
+ res_dict = self.controller.index(req)
+ self.assertEqual(res_dict, expected)
+
+ def test_list_server_group_all(self):
+ all_groups = []
+ tenant_groups = []
+ policies = ['anti-affinity']
+ members = []
+ metadata = {} # always empty
+ names = ['default-x', 'test']
+ sg1 = server_group_resp_template(id=str(1345),
+ name=names[0],
+ policies=[],
+ members=members,
+ metadata=metadata)
+ sg2 = server_group_resp_template(id=str(891),
+ name=names[1],
+ policies=policies,
+ members=members,
+ metadata={})
+ tenant_groups = [sg2]
+ all_groups = [sg1, sg2]
+
+ all = {'server_groups': all_groups}
+ tenant_specific = {'server_groups': tenant_groups}
+
+ def return_all_server_groups(context):
+ return [server_group_db(sg) for sg in all_groups]
+
+ self.stubs.Set(nova.db, 'instance_group_get_all',
+ return_all_server_groups)
+
+ def return_tenant_server_groups(context, project_id):
+ return [server_group_db(sg) for sg in tenant_groups]
+
+ self.stubs.Set(nova.db, 'instance_group_get_all_by_project_id',
+ return_tenant_server_groups)
+
+ path = self._get_url() + '/os-server-groups?all_projects=True'
+
+ req = fakes.HTTPRequest.blank(path, use_admin_context=True)
+ res_dict = self.controller.index(req)
+ self.assertEqual(res_dict, all)
+ req = fakes.HTTPRequest.blank(path)
+ res_dict = self.controller.index(req)
+ self.assertEqual(res_dict, tenant_specific)
+
+ def test_delete_server_group_by_id(self):
+ sg = server_group_template(id='123')
+
+ self.called = False
+
+ def server_group_delete(context, id):
+ self.called = True
+
+ def return_server_group(context, group_id):
+ self.assertEqual(sg['id'], group_id)
+ return server_group_db(sg)
+
+ self.stubs.Set(nova.db, 'instance_group_delete',
+ server_group_delete)
+ self.stubs.Set(nova.db, 'instance_group_get',
+ return_server_group)
+
+ req = fakes.HTTPRequest.blank(self._get_url() +
+ '/os-server-groups/123')
+ resp = self.controller.delete(req, '123')
+ self.assertTrue(self.called)
+
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.controller, sg_v3.ServerGroupController):
+ status_int = self.controller.delete.wsgi_code
+ else:
+ status_int = resp.status_int
+ self.assertEqual(204, status_int)
+
+ def test_delete_non_existing_server_group(self):
+ req = fakes.HTTPRequest.blank(self._get_url() +
+ '/os-server-groups/invalid')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, 'invalid')
+
+
+class ServerGroupTestV2(ServerGroupTestV21):
+
+ def _setup_controller(self):
+ ext_mgr = extensions.ExtensionManager()
+ ext_mgr.extensions = {}
+ self.controller = server_groups.ServerGroupController(ext_mgr)
+
+ def _get_app(self):
+ return fakes.wsgi_app(init_only=('os-server-groups',))
+
+
+class TestServerGroupXMLDeserializer(test.TestCase):
+
+ def setUp(self):
+ super(TestServerGroupXMLDeserializer, self).setUp()
+ self.deserializer = server_groups.ServerGroupXMLDeserializer()
+
+ def test_create_request(self):
+ serial_request = """
+<server_group name="test">
+</server_group>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server_group": {
+ "name": "test",
+ "policies": []
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_update_request(self):
+ serial_request = """
+<server_group name="test">
+<policies>
+<policy>policy-1</policy>
+<policy>policy-2</policy>
+</policies>
+</server_group>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server_group": {
+ "name": 'test',
+ "policies": ['policy-1', 'policy-2']
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_create_request_no_name(self):
+ serial_request = """
+<server_group>
+</server_group>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server_group": {
+ "policies": []
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_corrupt_xml(self):
+ """Should throw a 400 error on corrupt xml."""
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ self.deserializer.deserialize,
+ utils.killer_xml_body())
+
+
+class TestServerGroupXMLSerializer(test.TestCase):
+ def setUp(self):
+ super(TestServerGroupXMLSerializer, self).setUp()
+ self.namespace = wsgi.XMLNS_V11
+ self.index_serializer = server_groups.ServerGroupsTemplate()
+ self.default_serializer = server_groups.ServerGroupTemplate()
+
+ def _tag(self, elem):
+ tagname = elem.tag
+ self.assertEqual(tagname[0], '{')
+ tmp = tagname.partition('}')
+ namespace = tmp[0][1:]
+ self.assertEqual(namespace, self.namespace)
+ return tmp[2]
+
+ def _verify_server_group(self, raw_group, tree):
+ policies = raw_group['policies']
+ members = raw_group['members']
+ self.assertEqual('server_group', self._tag(tree))
+ self.assertEqual(raw_group['id'], tree.get('id'))
+ self.assertEqual(raw_group['name'], tree.get('name'))
+ self.assertEqual(3, len(tree))
+ for child in tree:
+ child_tag = self._tag(child)
+ if child_tag == 'policies':
+ self.assertEqual(len(policies), len(child))
+ for idx, gr_child in enumerate(child):
+ self.assertEqual(self._tag(gr_child), 'policy')
+ self.assertEqual(policies[idx],
+ gr_child.text)
+ elif child_tag == 'members':
+ self.assertEqual(len(members), len(child))
+ for idx, gr_child in enumerate(child):
+ self.assertEqual(self._tag(gr_child), 'member')
+ self.assertEqual(members[idx],
+ gr_child.text)
+ elif child_tag == 'metadata':
+ self.assertEqual(0, len(child))
+
+ def _verify_server_group_brief(self, raw_group, tree):
+ self.assertEqual('server_group', self._tag(tree))
+ self.assertEqual(raw_group['id'], tree.get('id'))
+ self.assertEqual(raw_group['name'], tree.get('name'))
+
+ def test_group_serializer(self):
+ policies = ["policy-1", "policy-2"]
+ members = ["1", "2"]
+ raw_group = dict(
+ id='890',
+ name='name',
+ policies=policies,
+ members=members)
+ sg_group = dict(server_group=raw_group)
+ text = self.default_serializer.serialize(sg_group)
+
+ tree = etree.fromstring(text)
+
+ self._verify_server_group(raw_group, tree)
+
+ def test_groups_serializer(self):
+ policies = ["policy-1", "policy-2",
+ "policy-3"]
+ members = ["1", "2", "3"]
+ groups = [dict(
+ id='890',
+ name='test',
+ policies=policies[0:2],
+ members=members[0:2]),
+ dict(
+ id='123',
+ name='default',
+ policies=policies[2:],
+ members=members[2:])]
+ sg_groups = dict(server_groups=groups)
+ text = self.index_serializer.serialize(sg_groups)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('server_groups', self._tag(tree))
+ self.assertEqual(len(groups), len(tree))
+ for idx, child in enumerate(tree):
+ self._verify_server_group_brief(groups[idx], child)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_password.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_password.py
new file mode 100644
index 0000000000..d29b0480f3
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_password.py
@@ -0,0 +1,94 @@
+# Copyright 2012 Nebula, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.metadata import password
+from nova import compute
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+
+CONF = cfg.CONF
+CONF.import_opt('osapi_compute_ext_list', 'nova.api.openstack.compute.contrib')
+
+
+class ServerPasswordTest(test.TestCase):
+ content_type = 'application/json'
+
+ def setUp(self):
+ super(ServerPasswordTest, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(
+ compute.api.API, 'get',
+ lambda self, ctxt, *a, **kw:
+ fake_instance.fake_instance_obj(
+ ctxt,
+ system_metadata={},
+ expected_attrs=['system_metadata']))
+ self.password = 'fakepass'
+
+ def fake_extract_password(instance):
+ return self.password
+
+ def fake_convert_password(context, password):
+ self.password = password
+ return {}
+
+ self.stubs.Set(password, 'extract_password', fake_extract_password)
+ self.stubs.Set(password, 'convert_password', fake_convert_password)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Server_password'])
+
+ def _make_request(self, url, method='GET'):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ req.method = method
+ res = req.get_response(
+ fakes.wsgi_app(init_only=('servers', 'os-server-password')))
+ return res
+
+ def _get_pass(self, body):
+ return jsonutils.loads(body).get('password')
+
+ def test_get_password(self):
+ url = '/v2/fake/servers/fake/os-server-password'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(self._get_pass(res.body), 'fakepass')
+
+ def test_reset_password(self):
+ url = '/v2/fake/servers/fake/os-server-password'
+ res = self._make_request(url, 'DELETE')
+ self.assertEqual(res.status_int, 204)
+
+ res = self._make_request(url)
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(self._get_pass(res.body), '')
+
+
+class ServerPasswordXmlTest(ServerPasswordTest):
+ content_type = 'application/xml'
+
+ def _get_pass(self, body):
+ # NOTE(vish): first element is password
+ return etree.XML(body).text or ''
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_start_stop.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_start_stop.py
new file mode 100644
index 0000000000..6be2a52b86
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_start_stop.py
@@ -0,0 +1,183 @@
+# Copyright (c) 2012 Midokura Japan K.K.
+#
+# 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 mox
+import webob
+
+from nova.api.openstack.compute.contrib import server_start_stop \
+ as server_v2
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import servers \
+ as server_v21
+from nova.compute import api as compute_api
+from nova import db
+from nova import exception
+from nova.openstack.common import policy as common_policy
+from nova import policy
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+def fake_instance_get(context, instance_id,
+ columns_to_join=None, use_slave=False):
+ result = fakes.stub_instance(id=1, uuid=instance_id)
+ result['created_at'] = None
+ result['deleted_at'] = None
+ result['updated_at'] = None
+ result['deleted'] = 0
+ result['info_cache'] = {'network_info': '[]',
+ 'instance_uuid': result['uuid']}
+ return result
+
+
+def fake_start_stop_not_ready(self, context, instance):
+ raise exception.InstanceNotReady(instance_id=instance["uuid"])
+
+
+def fake_start_stop_locked_server(self, context, instance):
+ raise exception.InstanceIsLocked(instance_uuid=instance['uuid'])
+
+
+def fake_start_stop_invalid_state(self, context, instance):
+ raise exception.InstanceIsLocked(instance_uuid=instance['uuid'])
+
+
+class ServerStartStopTestV21(test.TestCase):
+ start_policy = "compute:v3:servers:start"
+ stop_policy = "compute:v3:servers:stop"
+
+ def setUp(self):
+ super(ServerStartStopTestV21, self).setUp()
+ self._setup_controller()
+
+ def _setup_controller(self):
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = server_v21.ServersController(
+ extension_info=ext_info)
+
+ def test_start(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ self.mox.StubOutWithMock(compute_api.API, 'start')
+ compute_api.API.start(mox.IgnoreArg(), mox.IgnoreArg())
+ self.mox.ReplayAll()
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(start="")
+ self.controller._start_server(req, 'test_inst', body)
+
+ def test_start_policy_failed(self):
+ rules = {
+ self.start_policy:
+ common_policy.parse_rule("project_id:non_fake")
+ }
+ policy.set_rules(rules)
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(start="")
+ exc = self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._start_server,
+ req, 'test_inst', body)
+ self.assertIn(self.start_policy, exc.format_message())
+
+ def test_start_not_ready(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ self.stubs.Set(compute_api.API, 'start', fake_start_stop_not_ready)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._start_server, req, 'test_inst', body)
+
+ def test_start_locked_server(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ self.stubs.Set(compute_api.API, 'start', fake_start_stop_locked_server)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._start_server, req, 'test_inst', body)
+
+ def test_start_invalid_state(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ self.stubs.Set(compute_api.API, 'start', fake_start_stop_invalid_state)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._start_server, req, 'test_inst', body)
+
+ def test_stop(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ self.mox.StubOutWithMock(compute_api.API, 'stop')
+ compute_api.API.stop(mox.IgnoreArg(), mox.IgnoreArg())
+ self.mox.ReplayAll()
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(stop="")
+ self.controller._stop_server(req, 'test_inst', body)
+
+ def test_stop_policy_failed(self):
+ rules = {
+ self.stop_policy:
+ common_policy.parse_rule("project_id:non_fake")
+ }
+ policy.set_rules(rules)
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(stop="")
+ exc = self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._stop_server,
+ req, 'test_inst', body)
+ self.assertIn(self.stop_policy, exc.format_message())
+
+ def test_stop_not_ready(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ self.stubs.Set(compute_api.API, 'stop', fake_start_stop_not_ready)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(stop="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._stop_server, req, 'test_inst', body)
+
+ def test_stop_locked_server(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ self.stubs.Set(compute_api.API, 'stop', fake_start_stop_locked_server)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(stop="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._stop_server, req, 'test_inst', body)
+
+ def test_stop_invalid_state(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+ self.stubs.Set(compute_api.API, 'stop', fake_start_stop_invalid_state)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._stop_server, req, 'test_inst', body)
+
+ def test_start_with_bogus_id(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._start_server, req, 'test_inst', body)
+
+ def test_stop_with_bogus_id(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action')
+ body = dict(stop="")
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._stop_server, req, 'test_inst', body)
+
+
+class ServerStartStopTestV2(ServerStartStopTestV21):
+ start_policy = "compute:start"
+ stop_policy = "compute:stop"
+
+ def _setup_controller(self):
+ self.controller = server_v2.ServerStartStopActionController()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_usage.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_usage.py
new file mode 100644
index 0000000000..ee0d9a0ef4
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_usage.py
@@ -0,0 +1,159 @@
+# Copyright 2013 OpenStack Foundation
+# 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 datetime
+
+from lxml import etree
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+
+from nova.api.openstack.compute.contrib import server_usage
+from nova import compute
+from nova import db
+from nova import exception
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+UUID3 = '00000000-0000-0000-0000-000000000003'
+
+DATE1 = datetime.datetime(year=2013, month=4, day=5, hour=12)
+DATE2 = datetime.datetime(year=2013, month=4, day=5, hour=13)
+DATE3 = datetime.datetime(year=2013, month=4, day=5, hour=14)
+
+
+def fake_compute_get(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID3, launched_at=DATE1,
+ terminated_at=DATE2)
+ return fake_instance.fake_instance_obj(args[1], **inst)
+
+
+def fake_compute_get_all(*args, **kwargs):
+ db_list = [
+ fakes.stub_instance(2, uuid=UUID1, launched_at=DATE2,
+ terminated_at=DATE3),
+ fakes.stub_instance(3, uuid=UUID2, launched_at=DATE1,
+ terminated_at=DATE3),
+ ]
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return instance_obj._make_instance_list(args[1],
+ objects.InstanceList(),
+ db_list, fields)
+
+
+class ServerUsageTestV21(test.TestCase):
+ content_type = 'application/json'
+ prefix = 'OS-SRV-USG:'
+ _prefix = "/v2/fake"
+
+ def setUp(self):
+ super(ServerUsageTestV21, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Server_usage'])
+ return_server = fakes.fake_instance_get()
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ def _make_request(self, url):
+ req = fakes.HTTPRequest.blank(url)
+ req.accept = self.content_type
+ res = req.get_response(self._get_app())
+ return res
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21(init_only=('servers', 'os-server-usage'))
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def assertServerUsage(self, server, launched_at, terminated_at):
+ resp_launched_at = timeutils.parse_isotime(
+ server.get('%slaunched_at' % self.prefix))
+ self.assertEqual(timeutils.normalize_time(resp_launched_at),
+ launched_at)
+ resp_terminated_at = timeutils.parse_isotime(
+ server.get('%sterminated_at' % self.prefix))
+ self.assertEqual(timeutils.normalize_time(resp_terminated_at),
+ terminated_at)
+
+ def test_show(self):
+ url = self._prefix + ('/servers/%s' % UUID3)
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ now = timeutils.utcnow()
+ timeutils.set_time_override(now)
+ self.assertServerUsage(self._get_server(res.body),
+ launched_at=DATE1,
+ terminated_at=DATE2)
+
+ def test_detail(self):
+ url = self._prefix + '/servers/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ servers = self._get_servers(res.body)
+ self.assertServerUsage(servers[0],
+ launched_at=DATE2,
+ terminated_at=DATE3)
+ self.assertServerUsage(servers[1],
+ launched_at=DATE1,
+ terminated_at=DATE3)
+
+ def test_no_instance_passthrough_404(self):
+
+ def fake_compute_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ url = self._prefix + '/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 404)
+
+
+class ServerUsageTestV20(ServerUsageTestV21):
+
+ def setUp(self):
+ super(ServerUsageTestV20, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Server_usage'])
+
+ def _get_app(self):
+ return fakes.wsgi_app(init_only=('servers',))
+
+
+class ServerUsageXmlTest(ServerUsageTestV20):
+ content_type = 'application/xml'
+ prefix = '{%s}' % server_usage.Server_usage.namespace
+
+ def _get_server(self, body):
+ return etree.XML(body)
+
+ def _get_servers(self, body):
+ return etree.XML(body).getchildren()
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_services.py b/nova/tests/unit/api/openstack/compute/contrib/test_services.py
new file mode 100644
index 0000000000..87297c567b
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_services.py
@@ -0,0 +1,576 @@
+# Copyright 2012 IBM Corp.
+#
+# 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 calendar
+import datetime
+
+import iso8601
+import mock
+from oslo.utils import timeutils
+import webob.exc
+
+from nova.api.openstack.compute.contrib import services
+from nova.api.openstack import extensions
+from nova import availability_zones
+from nova.compute import cells_api
+from nova import context
+from nova import db
+from nova import exception
+from nova.servicegroup.drivers import db as db_driver
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.objects import test_service
+
+
+fake_services_list = [
+ dict(test_service.fake_service,
+ binary='nova-scheduler',
+ host='host1',
+ id=1,
+ disabled=True,
+ topic='scheduler',
+ updated_at=datetime.datetime(2012, 10, 29, 13, 42, 2),
+ created_at=datetime.datetime(2012, 9, 18, 2, 46, 27),
+ disabled_reason='test1'),
+ dict(test_service.fake_service,
+ binary='nova-compute',
+ host='host1',
+ id=2,
+ disabled=True,
+ topic='compute',
+ updated_at=datetime.datetime(2012, 10, 29, 13, 42, 5),
+ created_at=datetime.datetime(2012, 9, 18, 2, 46, 27),
+ disabled_reason='test2'),
+ dict(test_service.fake_service,
+ binary='nova-scheduler',
+ host='host2',
+ id=3,
+ disabled=False,
+ topic='scheduler',
+ updated_at=datetime.datetime(2012, 9, 19, 6, 55, 34),
+ created_at=datetime.datetime(2012, 9, 18, 2, 46, 28),
+ disabled_reason=None),
+ dict(test_service.fake_service,
+ binary='nova-compute',
+ host='host2',
+ id=4,
+ disabled=True,
+ topic='compute',
+ updated_at=datetime.datetime(2012, 9, 18, 8, 3, 38),
+ created_at=datetime.datetime(2012, 9, 18, 2, 46, 28),
+ disabled_reason='test4'),
+ ]
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {}
+
+
+class FakeRequestWithService(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {"binary": "nova-compute"}
+
+
+class FakeRequestWithHost(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {"host": "host1"}
+
+
+class FakeRequestWithHostService(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {"host": "host1", "binary": "nova-compute"}
+
+
+def fake_service_get_all(services):
+ def service_get_all(context, filters=None, set_zones=False):
+ if set_zones or 'availability_zone' in filters:
+ return availability_zones.set_availability_zones(context,
+ services)
+ return services
+ return service_get_all
+
+
+def fake_db_api_service_get_all(context, disabled=None):
+ return fake_services_list
+
+
+def fake_db_service_get_by_host_binary(services):
+ def service_get_by_host_binary(context, host, binary):
+ for service in services:
+ if service['host'] == host and service['binary'] == binary:
+ return service
+ raise exception.HostBinaryNotFound(host=host, binary=binary)
+ return service_get_by_host_binary
+
+
+def fake_service_get_by_host_binary(context, host, binary):
+ fake = fake_db_service_get_by_host_binary(fake_services_list)
+ return fake(context, host, binary)
+
+
+def _service_get_by_id(services, value):
+ for service in services:
+ if service['id'] == value:
+ return service
+ return None
+
+
+def fake_db_service_update(services):
+ def service_update(context, service_id, values):
+ service = _service_get_by_id(services, service_id)
+ if service is None:
+ raise exception.ServiceNotFound(service_id=service_id)
+ return service
+ return service_update
+
+
+def fake_service_update(context, service_id, values):
+ fake = fake_db_service_update(fake_services_list)
+ return fake(context, service_id, values)
+
+
+def fake_utcnow():
+ return datetime.datetime(2012, 10, 29, 13, 42, 11)
+
+
+fake_utcnow.override_time = None
+
+
+def fake_utcnow_ts():
+ d = fake_utcnow()
+ return calendar.timegm(d.utctimetuple())
+
+
+class ServicesTest(test.TestCase):
+
+ def setUp(self):
+ super(ServicesTest, self).setUp()
+
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = services.ServiceController(self.ext_mgr)
+
+ self.stubs.Set(timeutils, "utcnow", fake_utcnow)
+ self.stubs.Set(timeutils, "utcnow_ts", fake_utcnow_ts)
+
+ self.stubs.Set(self.controller.host_api, "service_get_all",
+ fake_service_get_all(fake_services_list))
+
+ self.stubs.Set(db, "service_get_by_args",
+ fake_db_service_get_by_host_binary(fake_services_list))
+ self.stubs.Set(db, "service_update",
+ fake_db_service_update(fake_services_list))
+
+ def test_services_list(self):
+ req = FakeRequest()
+ res_dict = self.controller.index(req)
+
+ response = {'services': [
+ {'binary': 'nova-scheduler',
+ 'host': 'host1',
+ 'zone': 'internal',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)},
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)},
+ {'binary': 'nova-scheduler',
+ 'host': 'host2',
+ 'zone': 'internal',
+ 'status': 'enabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34)},
+ {'binary': 'nova-compute',
+ 'host': 'host2',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]}
+ self.assertEqual(res_dict, response)
+
+ def test_services_list_with_host(self):
+ req = FakeRequestWithHost()
+ res_dict = self.controller.index(req)
+
+ response = {'services': [
+ {'binary': 'nova-scheduler',
+ 'host': 'host1',
+ 'zone': 'internal',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)},
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]}
+ self.assertEqual(res_dict, response)
+
+ def test_services_list_with_service(self):
+ req = FakeRequestWithService()
+ res_dict = self.controller.index(req)
+
+ response = {'services': [
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)},
+ {'binary': 'nova-compute',
+ 'host': 'host2',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]}
+ self.assertEqual(res_dict, response)
+
+ def test_services_list_with_host_service(self):
+ req = FakeRequestWithHostService()
+ res_dict = self.controller.index(req)
+
+ response = {'services': [
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]}
+ self.assertEqual(res_dict, response)
+
+ def test_services_detail(self):
+ self.ext_mgr.extensions['os-extended-services'] = True
+ req = FakeRequest()
+ res_dict = self.controller.index(req)
+ response = {'services': [
+ {'binary': 'nova-scheduler',
+ 'host': 'host1',
+ 'zone': 'internal',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
+ 'disabled_reason': 'test1'},
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5),
+ 'disabled_reason': 'test2'},
+ {'binary': 'nova-scheduler',
+ 'host': 'host2',
+ 'zone': 'internal',
+ 'status': 'enabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34),
+ 'disabled_reason': None},
+ {'binary': 'nova-compute',
+ 'host': 'host2',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38),
+ 'disabled_reason': 'test4'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_service_detail_with_host(self):
+ self.ext_mgr.extensions['os-extended-services'] = True
+ req = FakeRequestWithHost()
+ res_dict = self.controller.index(req)
+ response = {'services': [
+ {'binary': 'nova-scheduler',
+ 'host': 'host1',
+ 'zone': 'internal',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
+ 'disabled_reason': 'test1'},
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5),
+ 'disabled_reason': 'test2'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_service_detail_with_service(self):
+ self.ext_mgr.extensions['os-extended-services'] = True
+ req = FakeRequestWithService()
+ res_dict = self.controller.index(req)
+ response = {'services': [
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5),
+ 'disabled_reason': 'test2'},
+ {'binary': 'nova-compute',
+ 'host': 'host2',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38),
+ 'disabled_reason': 'test4'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_service_detail_with_host_service(self):
+ self.ext_mgr.extensions['os-extended-services'] = True
+ req = FakeRequestWithHostService()
+ res_dict = self.controller.index(req)
+ response = {'services': [
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5),
+ 'disabled_reason': 'test2'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_services_detail_with_delete_extension(self):
+ self.ext_mgr.extensions['os-extended-services-delete'] = True
+ req = FakeRequest()
+ res_dict = self.controller.index(req)
+ response = {'services': [
+ {'binary': 'nova-scheduler',
+ 'host': 'host1',
+ 'id': 1,
+ 'zone': 'internal',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)},
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'id': 2,
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)},
+ {'binary': 'nova-scheduler',
+ 'host': 'host2',
+ 'id': 3,
+ 'zone': 'internal',
+ 'status': 'enabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34)},
+ {'binary': 'nova-compute',
+ 'host': 'host2',
+ 'id': 4,
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]}
+ self.assertEqual(res_dict, response)
+
+ def test_services_enable(self):
+ def _service_update(context, service_id, values):
+ self.assertIsNone(values['disabled_reason'])
+ return dict(test_service.fake_service, id=service_id, **values)
+
+ self.stubs.Set(db, "service_update", _service_update)
+
+ body = {'host': 'host1', 'binary': 'nova-compute'}
+ req = fakes.HTTPRequest.blank('/v2/fake/os-services/enable')
+
+ res_dict = self.controller.update(req, "enable", body)
+ self.assertEqual(res_dict['service']['status'], 'enabled')
+ self.assertNotIn('disabled_reason', res_dict['service'])
+
+ def test_services_enable_with_invalid_host(self):
+ body = {'host': 'invalid', 'binary': 'nova-compute'}
+ req = fakes.HTTPRequest.blank('/v2/fake/os-services/enable')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update,
+ req,
+ "enable",
+ body)
+
+ def test_services_enable_with_invalid_binary(self):
+ body = {'host': 'host1', 'binary': 'invalid'}
+ req = fakes.HTTPRequest.blank('/v2/fake/os-services/enable')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update,
+ req,
+ "enable",
+ body)
+
+ # This test is just to verify that the servicegroup API gets used when
+ # calling this API.
+ def test_services_with_exception(self):
+ def dummy_is_up(self, dummy):
+ raise KeyError()
+
+ self.stubs.Set(db_driver.DbDriver, 'is_up', dummy_is_up)
+ req = FakeRequestWithHostService()
+ self.assertRaises(KeyError, self.controller.index, req)
+
+ def test_services_disable(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-services/disable')
+ body = {'host': 'host1', 'binary': 'nova-compute'}
+ res_dict = self.controller.update(req, "disable", body)
+
+ self.assertEqual(res_dict['service']['status'], 'disabled')
+ self.assertNotIn('disabled_reason', res_dict['service'])
+
+ def test_services_disable_with_invalid_host(self):
+ body = {'host': 'invalid', 'binary': 'nova-compute'}
+ req = fakes.HTTPRequest.blank('/v2/fake/os-services/disable')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update,
+ req,
+ "disable",
+ body)
+
+ def test_services_disable_with_invalid_binary(self):
+ body = {'host': 'host1', 'binary': 'invalid'}
+ req = fakes.HTTPRequestV3.blank('/v2/fake/os-services/disable')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update,
+ req,
+ "disable",
+ body)
+
+ def test_services_disable_log_reason(self):
+ self.ext_mgr.extensions['os-extended-services'] = True
+ req = \
+ fakes.HTTPRequest.blank('v2/fakes/os-services/disable-log-reason')
+ body = {'host': 'host1',
+ 'binary': 'nova-compute',
+ 'disabled_reason': 'test-reason',
+ }
+ res_dict = self.controller.update(req, "disable-log-reason", body)
+
+ self.assertEqual(res_dict['service']['status'], 'disabled')
+ self.assertEqual(res_dict['service']['disabled_reason'], 'test-reason')
+
+ def test_mandatory_reason_field(self):
+ self.ext_mgr.extensions['os-extended-services'] = True
+ req = \
+ fakes.HTTPRequest.blank('v2/fakes/os-services/disable-log-reason')
+ body = {'host': 'host1',
+ 'binary': 'nova-compute',
+ }
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, "disable-log-reason", body)
+
+ def test_invalid_reason_field(self):
+ reason = ' '
+ self.assertFalse(self.controller._is_valid_as_reason(reason))
+ reason = 'a' * 256
+ self.assertFalse(self.controller._is_valid_as_reason(reason))
+ reason = 'it\'s a valid reason.'
+ self.assertTrue(self.controller._is_valid_as_reason(reason))
+
+ def test_services_delete(self):
+ self.ext_mgr.extensions['os-extended-services-delete'] = True
+
+ request = fakes.HTTPRequest.blank('/v2/fakes/os-services/1',
+ use_admin_context=True)
+ request.method = 'DELETE'
+
+ with mock.patch.object(self.controller.host_api,
+ 'service_delete') as service_delete:
+ self.controller.delete(request, '1')
+ service_delete.assert_called_once_with(
+ request.environ['nova.context'], '1')
+ self.assertEqual(self.controller.delete.wsgi_code, 204)
+
+ def test_services_delete_not_found(self):
+ self.ext_mgr.extensions['os-extended-services-delete'] = True
+
+ request = fakes.HTTPRequest.blank('/v2/fakes/os-services/abc',
+ use_admin_context=True)
+ request.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, request, 'abc')
+
+ def test_services_delete_not_enabled(self):
+ request = fakes.HTTPRequest.blank('/v2/fakes/os-services/300',
+ use_admin_context=True)
+ request.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPMethodNotAllowed,
+ self.controller.delete, request, '300')
+
+
+class ServicesCellsTest(test.TestCase):
+ def setUp(self):
+ super(ServicesCellsTest, self).setUp()
+
+ host_api = cells_api.HostAPI()
+
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = services.ServiceController(self.ext_mgr)
+ self.controller.host_api = host_api
+
+ self.stubs.Set(timeutils, "utcnow", fake_utcnow)
+ self.stubs.Set(timeutils, "utcnow_ts", fake_utcnow_ts)
+
+ services_list = []
+ for service in fake_services_list:
+ service = service.copy()
+ service['id'] = 'cell1@%d' % service['id']
+ services_list.append(service)
+
+ self.stubs.Set(host_api.cells_rpcapi, "service_get_all",
+ fake_service_get_all(services_list))
+
+ def test_services_detail(self):
+ self.ext_mgr.extensions['os-extended-services-delete'] = True
+ req = FakeRequest()
+ res_dict = self.controller.index(req)
+ utc = iso8601.iso8601.Utc()
+ response = {'services': [
+ {'id': 'cell1@1',
+ 'binary': 'nova-scheduler',
+ 'host': 'host1',
+ 'zone': 'internal',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2,
+ tzinfo=utc)},
+ {'id': 'cell1@2',
+ 'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5,
+ tzinfo=utc)},
+ {'id': 'cell1@3',
+ 'binary': 'nova-scheduler',
+ 'host': 'host2',
+ 'zone': 'internal',
+ 'status': 'enabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34,
+ tzinfo=utc)},
+ {'id': 'cell1@4',
+ 'binary': 'nova-compute',
+ 'host': 'host2',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38,
+ tzinfo=utc)}]}
+ self.assertEqual(res_dict, response)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_shelve.py b/nova/tests/unit/api/openstack/compute/contrib/test_shelve.py
new file mode 100644
index 0000000000..df1c6fc449
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_shelve.py
@@ -0,0 +1,148 @@
+# 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 uuid
+
+import webob
+
+from nova.api.openstack.compute.contrib import shelve as shelve_v2
+from nova.api.openstack.compute.plugins.v3 import shelve as shelve_v21
+from nova.compute import api as compute_api
+from nova import db
+from nova import exception
+from nova.openstack.common import policy as common_policy
+from nova import policy
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+
+def fake_instance_get_by_uuid(context, instance_id,
+ columns_to_join=None, use_slave=False):
+ return fake_instance.fake_db_instance(
+ **{'name': 'fake', 'project_id': '%s_unequal' % context.project_id})
+
+
+def fake_auth_context(context):
+ return True
+
+
+class ShelvePolicyTestV21(test.NoDBTestCase):
+ plugin = shelve_v21
+ prefix = 'v3:os-shelve:'
+ offload = 'shelve_offload'
+
+ def setUp(self):
+ super(ShelvePolicyTestV21, self).setUp()
+ self.controller = self.plugin.ShelveController()
+
+ def _fake_request(self):
+ return fakes.HTTPRequestV3.blank('/servers/12/os-shelve')
+
+ def test_shelve_restricted_by_role(self):
+ rules = {'compute_extension:%sshelve' % self.prefix:
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rules)
+
+ req = self._fake_request()
+ self.assertRaises(exception.Forbidden, self.controller._shelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_shelve_allowed(self):
+ rules = {'compute:get': common_policy.parse_rule(''),
+ 'compute_extension:%sshelve' % self.prefix:
+ common_policy.parse_rule('')}
+ policy.set_rules(rules)
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ req = self._fake_request()
+ self.assertRaises(exception.Forbidden, self.controller._shelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_shelve_locked_server(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ self.stubs.Set(self.plugin, 'auth_shelve', fake_auth_context)
+ self.stubs.Set(compute_api.API, 'shelve',
+ fakes.fake_actions_to_locked_server)
+ req = self._fake_request()
+ self.assertRaises(webob.exc.HTTPConflict, self.controller._shelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_unshelve_restricted_by_role(self):
+ rules = {'compute_extension:%sunshelve' % self.prefix:
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rules)
+
+ req = self._fake_request()
+ self.assertRaises(exception.Forbidden, self.controller._unshelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_unshelve_allowed(self):
+ rules = {'compute:get': common_policy.parse_rule(''),
+ 'compute_extension:%sunshelve' % self.prefix:
+ common_policy.parse_rule('')}
+ policy.set_rules(rules)
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ req = self._fake_request()
+ self.assertRaises(exception.Forbidden, self.controller._unshelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_unshelve_locked_server(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ self.stubs.Set(self.plugin, 'auth_unshelve', fake_auth_context)
+ self.stubs.Set(compute_api.API, 'unshelve',
+ fakes.fake_actions_to_locked_server)
+ req = self._fake_request()
+ self.assertRaises(webob.exc.HTTPConflict, self.controller._unshelve,
+ req, str(uuid.uuid4()), {})
+
+ def test_shelve_offload_restricted_by_role(self):
+ rules = {'compute_extension:%s%s' % (self.prefix, self.offload):
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rules)
+
+ req = self._fake_request()
+ self.assertRaises(exception.Forbidden,
+ self.controller._shelve_offload, req, str(uuid.uuid4()), {})
+
+ def test_shelve_offload_allowed(self):
+ rules = {'compute:get': common_policy.parse_rule(''),
+ 'compute_extension:%s%s' % (self.prefix, self.offload):
+ common_policy.parse_rule('')}
+ policy.set_rules(rules)
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ req = self._fake_request()
+ self.assertRaises(exception.Forbidden,
+ self.controller._shelve_offload, req, str(uuid.uuid4()), {})
+
+ def test_shelve_offload_locked_server(self):
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid)
+ self.stubs.Set(self.plugin, 'auth_shelve_offload', fake_auth_context)
+ self.stubs.Set(compute_api.API, 'shelve_offload',
+ fakes.fake_actions_to_locked_server)
+ req = self._fake_request()
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._shelve_offload,
+ req, str(uuid.uuid4()), {})
+
+
+class ShelvePolicyTestV2(ShelvePolicyTestV21):
+ plugin = shelve_v2
+ prefix = ''
+ offload = 'shelveOffload'
+
+ def _fake_request(self):
+ return fakes.HTTPRequest.blank('/v2/123/servers/12/os-shelve')
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_simple_tenant_usage.py b/nova/tests/unit/api/openstack/compute/contrib/test_simple_tenant_usage.py
new file mode 100644
index 0000000000..9639b886ae
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_simple_tenant_usage.py
@@ -0,0 +1,539 @@
+# Copyright 2011 OpenStack Foundation
+# 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 datetime
+
+from lxml import etree
+import mock
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+import webob
+
+from nova.api.openstack.compute.contrib import simple_tenant_usage as \
+ simple_tenant_usage_v2
+from nova.api.openstack.compute.plugins.v3 import simple_tenant_usage as \
+ simple_tenant_usage_v21
+from nova.compute import flavors
+from nova.compute import vm_states
+from nova import context
+from nova import db
+from nova import exception
+from nova import objects
+from nova.openstack.common import policy as common_policy
+from nova import policy
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova import utils
+
+SERVERS = 5
+TENANTS = 2
+HOURS = 24
+ROOT_GB = 10
+EPHEMERAL_GB = 20
+MEMORY_MB = 1024
+VCPUS = 2
+NOW = timeutils.utcnow()
+START = NOW - datetime.timedelta(hours=HOURS)
+STOP = NOW
+
+
+FAKE_INST_TYPE = {'id': 1,
+ 'vcpus': VCPUS,
+ 'root_gb': ROOT_GB,
+ 'ephemeral_gb': EPHEMERAL_GB,
+ 'memory_mb': MEMORY_MB,
+ 'name': 'fakeflavor',
+ 'flavorid': 'foo',
+ 'rxtx_factor': 1.0,
+ 'vcpu_weight': 1,
+ 'swap': 0,
+ 'created_at': None,
+ 'updated_at': None,
+ 'deleted_at': None,
+ 'deleted': 0,
+ 'disabled': False,
+ 'is_public': True,
+ 'extra_specs': {'foo': 'bar'}}
+
+
+def get_fake_db_instance(start, end, instance_id, tenant_id,
+ vm_state=vm_states.ACTIVE):
+ sys_meta = utils.dict_to_metadata(
+ flavors.save_flavor_info({}, FAKE_INST_TYPE))
+ # NOTE(mriedem): We use fakes.stub_instance since it sets the fields
+ # needed on the db instance for converting it to an object, but we still
+ # need to override system_metadata to use our fake flavor.
+ inst = fakes.stub_instance(
+ id=instance_id,
+ uuid='00000000-0000-0000-0000-00000000000000%02d' % instance_id,
+ image_ref='1',
+ project_id=tenant_id,
+ user_id='fakeuser',
+ display_name='name',
+ flavor_id=FAKE_INST_TYPE['id'],
+ launched_at=start,
+ terminated_at=end,
+ vm_state=vm_state,
+ memory_mb=MEMORY_MB,
+ vcpus=VCPUS,
+ root_gb=ROOT_GB,
+ ephemeral_gb=EPHEMERAL_GB,)
+ inst['system_metadata'] = sys_meta
+ return inst
+
+
+def fake_instance_get_active_by_window_joined(context, begin, end,
+ project_id, host):
+ return [get_fake_db_instance(START,
+ STOP,
+ x,
+ "faketenant_%s" % (x / SERVERS))
+ for x in xrange(TENANTS * SERVERS)]
+
+
+@mock.patch.object(db, 'instance_get_active_by_window_joined',
+ fake_instance_get_active_by_window_joined)
+class SimpleTenantUsageTestV21(test.TestCase):
+ url = '/v2/faketenant_0/os-simple-tenant-usage'
+ alt_url = '/v2/faketenant_1/os-simple-tenant-usage'
+ policy_rule_prefix = "compute_extension:v3:os-simple-tenant-usage"
+
+ def setUp(self):
+ super(SimpleTenantUsageTestV21, self).setUp()
+ self.admin_context = context.RequestContext('fakeadmin_0',
+ 'faketenant_0',
+ is_admin=True)
+ self.user_context = context.RequestContext('fakeadmin_0',
+ 'faketenant_0',
+ is_admin=False)
+ self.alt_user_context = context.RequestContext('fakeadmin_0',
+ 'faketenant_1',
+ is_admin=False)
+
+ def _get_wsgi_app(self, context):
+ return fakes.wsgi_app_v21(fake_auth_context=context,
+ init_only=('servers',
+ 'os-simple-tenant-usage'))
+
+ def _test_verify_index(self, start, stop):
+ req = webob.Request.blank(
+ self.url + '?start=%s&end=%s' %
+ (start.isoformat(), stop.isoformat()))
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self._get_wsgi_app(self.admin_context))
+
+ self.assertEqual(res.status_int, 200)
+ res_dict = jsonutils.loads(res.body)
+ usages = res_dict['tenant_usages']
+ for i in xrange(TENANTS):
+ self.assertEqual(int(usages[i]['total_hours']),
+ SERVERS * HOURS)
+ self.assertEqual(int(usages[i]['total_local_gb_usage']),
+ SERVERS * (ROOT_GB + EPHEMERAL_GB) * HOURS)
+ self.assertEqual(int(usages[i]['total_memory_mb_usage']),
+ SERVERS * MEMORY_MB * HOURS)
+ self.assertEqual(int(usages[i]['total_vcpus_usage']),
+ SERVERS * VCPUS * HOURS)
+ self.assertFalse(usages[i].get('server_usages'))
+
+ def test_verify_index(self):
+ self._test_verify_index(START, STOP)
+
+ def test_verify_index_future_end_time(self):
+ future = NOW + datetime.timedelta(hours=HOURS)
+ self._test_verify_index(START, future)
+
+ def test_verify_show(self):
+ self._test_verify_show(START, STOP)
+
+ def test_verify_show_future_end_time(self):
+ future = NOW + datetime.timedelta(hours=HOURS)
+ self._test_verify_show(START, future)
+
+ def _get_tenant_usages(self, detailed=''):
+ req = webob.Request.blank(
+ self.url + '?detailed=%s&start=%s&end=%s' %
+ (detailed, START.isoformat(), STOP.isoformat()))
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self._get_wsgi_app(self.admin_context))
+ self.assertEqual(res.status_int, 200)
+ res_dict = jsonutils.loads(res.body)
+ return res_dict['tenant_usages']
+
+ def test_verify_detailed_index(self):
+ usages = self._get_tenant_usages('1')
+ for i in xrange(TENANTS):
+ servers = usages[i]['server_usages']
+ for j in xrange(SERVERS):
+ self.assertEqual(int(servers[j]['hours']), HOURS)
+
+ def test_verify_simple_index(self):
+ usages = self._get_tenant_usages(detailed='0')
+ for i in xrange(TENANTS):
+ self.assertIsNone(usages[i].get('server_usages'))
+
+ def test_verify_simple_index_empty_param(self):
+ # NOTE(lzyeval): 'detailed=&start=..&end=..'
+ usages = self._get_tenant_usages()
+ for i in xrange(TENANTS):
+ self.assertIsNone(usages[i].get('server_usages'))
+
+ def _test_verify_show(self, start, stop):
+ tenant_id = 0
+ req = webob.Request.blank(
+ self.url + '/faketenant_%s?start=%s&end=%s' %
+ (tenant_id, start.isoformat(), stop.isoformat()))
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self._get_wsgi_app(self.user_context))
+ self.assertEqual(res.status_int, 200)
+ res_dict = jsonutils.loads(res.body)
+
+ usage = res_dict['tenant_usage']
+ servers = usage['server_usages']
+ self.assertEqual(len(usage['server_usages']), SERVERS)
+ uuids = ['00000000-0000-0000-0000-00000000000000%02d' %
+ (x + (tenant_id * SERVERS)) for x in xrange(SERVERS)]
+ for j in xrange(SERVERS):
+ delta = STOP - START
+ uptime = delta.days * 24 * 3600 + delta.seconds
+ self.assertEqual(int(servers[j]['uptime']), uptime)
+ self.assertEqual(int(servers[j]['hours']), HOURS)
+ self.assertIn(servers[j]['instance_id'], uuids)
+
+ def test_verify_show_cannot_view_other_tenant(self):
+ req = webob.Request.blank(
+ self.alt_url + '/faketenant_0?start=%s&end=%s' %
+ (START.isoformat(), STOP.isoformat()))
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+
+ rules = {
+ self.policy_rule_prefix + ":show":
+ common_policy.parse_rule([
+ ["role:admin"], ["project_id:%(project_id)s"]
+ ])
+ }
+ policy.set_rules(rules)
+
+ try:
+ res = req.get_response(self._get_wsgi_app(self.alt_user_context))
+ self.assertEqual(res.status_int, 403)
+ finally:
+ policy.reset()
+
+ def test_get_tenants_usage_with_bad_start_date(self):
+ future = NOW + datetime.timedelta(hours=HOURS)
+ tenant_id = 0
+ req = webob.Request.blank(
+ self.url + '/'
+ 'faketenant_%s?start=%s&end=%s' %
+ (tenant_id, future.isoformat(), NOW.isoformat()))
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self._get_wsgi_app(self.user_context))
+ self.assertEqual(res.status_int, 400)
+
+ def test_get_tenants_usage_with_invalid_start_date(self):
+ tenant_id = 0
+ req = webob.Request.blank(
+ self.url + '/'
+ 'faketenant_%s?start=%s&end=%s' %
+ (tenant_id, "xxxx", NOW.isoformat()))
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(self._get_wsgi_app(self.user_context))
+ self.assertEqual(res.status_int, 400)
+
+ def _test_get_tenants_usage_with_one_date(self, date_url_param):
+ req = webob.Request.blank(
+ self.url + '/'
+ 'faketenant_0?%s' % date_url_param)
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(self._get_wsgi_app(self.user_context))
+ self.assertEqual(200, res.status_int)
+
+ def test_get_tenants_usage_with_no_start_date(self):
+ self._test_get_tenants_usage_with_one_date(
+ 'end=%s' % (NOW + datetime.timedelta(5)).isoformat())
+
+ def test_get_tenants_usage_with_no_end_date(self):
+ self._test_get_tenants_usage_with_one_date(
+ 'start=%s' % (NOW - datetime.timedelta(5)).isoformat())
+
+
+class SimpleTenantUsageTestV2(SimpleTenantUsageTestV21):
+ policy_rule_prefix = "compute_extension:simple_tenant_usage"
+
+ def _get_wsgi_app(self, context):
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Simple_tenant_usage'])
+ return fakes.wsgi_app(fake_auth_context=context,
+ init_only=('os-simple-tenant-usage', ))
+
+
+class SimpleTenantUsageSerializerTest(test.TestCase):
+ def _verify_server_usage(self, raw_usage, tree):
+ self.assertEqual('server_usage', tree.tag)
+
+ # Figure out what fields we expect
+ not_seen = set(raw_usage.keys())
+
+ for child in tree:
+ self.assertIn(child.tag, not_seen)
+ not_seen.remove(child.tag)
+ self.assertEqual(str(raw_usage[child.tag]), child.text)
+
+ self.assertEqual(len(not_seen), 0)
+
+ def _verify_tenant_usage(self, raw_usage, tree):
+ self.assertEqual('tenant_usage', tree.tag)
+
+ # Figure out what fields we expect
+ not_seen = set(raw_usage.keys())
+
+ for child in tree:
+ self.assertIn(child.tag, not_seen)
+ not_seen.remove(child.tag)
+ if child.tag == 'server_usages':
+ for idx, gr_child in enumerate(child):
+ self._verify_server_usage(raw_usage['server_usages'][idx],
+ gr_child)
+ else:
+ self.assertEqual(str(raw_usage[child.tag]), child.text)
+
+ self.assertEqual(len(not_seen), 0)
+
+ def test_serializer_show(self):
+ serializer = simple_tenant_usage_v2.SimpleTenantUsageTemplate()
+ today = timeutils.utcnow()
+ yesterday = today - datetime.timedelta(days=1)
+ raw_usage = dict(
+ tenant_id='tenant',
+ total_local_gb_usage=789,
+ total_vcpus_usage=456,
+ total_memory_mb_usage=123,
+ total_hours=24,
+ start=yesterday,
+ stop=today,
+ server_usages=[dict(
+ instance_id='00000000-0000-0000-0000-0000000000000000',
+ name='test',
+ hours=24,
+ memory_mb=1024,
+ local_gb=50,
+ vcpus=1,
+ tenant_id='tenant',
+ flavor='m1.small',
+ started_at=yesterday,
+ ended_at=today,
+ state='terminated',
+ uptime=86400),
+ dict(
+ instance_id='00000000-0000-0000-0000-0000000000000002',
+ name='test2',
+ hours=12,
+ memory_mb=512,
+ local_gb=25,
+ vcpus=2,
+ tenant_id='tenant',
+ flavor='m1.tiny',
+ started_at=yesterday,
+ ended_at=today,
+ state='terminated',
+ uptime=43200),
+ ],
+ )
+ tenant_usage = dict(tenant_usage=raw_usage)
+ text = serializer.serialize(tenant_usage)
+
+ tree = etree.fromstring(text)
+
+ self._verify_tenant_usage(raw_usage, tree)
+
+ def test_serializer_index(self):
+ serializer = simple_tenant_usage_v2.SimpleTenantUsagesTemplate()
+ today = timeutils.utcnow()
+ yesterday = today - datetime.timedelta(days=1)
+ raw_usages = [dict(
+ tenant_id='tenant1',
+ total_local_gb_usage=1024,
+ total_vcpus_usage=23,
+ total_memory_mb_usage=512,
+ total_hours=24,
+ start=yesterday,
+ stop=today,
+ server_usages=[dict(
+ instance_id='00000000-0000-0000-0000-0000000000000001',
+ name='test1',
+ hours=24,
+ memory_mb=1024,
+ local_gb=50,
+ vcpus=2,
+ tenant_id='tenant1',
+ flavor='m1.small',
+ started_at=yesterday,
+ ended_at=today,
+ state='terminated',
+ uptime=86400),
+ dict(
+ instance_id='00000000-0000-0000-0000-0000000000000002',
+ name='test2',
+ hours=42,
+ memory_mb=4201,
+ local_gb=25,
+ vcpus=1,
+ tenant_id='tenant1',
+ flavor='m1.tiny',
+ started_at=today,
+ ended_at=yesterday,
+ state='terminated',
+ uptime=43200),
+ ],
+ ),
+ dict(
+ tenant_id='tenant2',
+ total_local_gb_usage=512,
+ total_vcpus_usage=32,
+ total_memory_mb_usage=1024,
+ total_hours=42,
+ start=today,
+ stop=yesterday,
+ server_usages=[dict(
+ instance_id='00000000-0000-0000-0000-0000000000000003',
+ name='test3',
+ hours=24,
+ memory_mb=1024,
+ local_gb=50,
+ vcpus=2,
+ tenant_id='tenant2',
+ flavor='m1.small',
+ started_at=yesterday,
+ ended_at=today,
+ state='terminated',
+ uptime=86400),
+ dict(
+ instance_id='00000000-0000-0000-0000-0000000000000002',
+ name='test2',
+ hours=42,
+ memory_mb=4201,
+ local_gb=25,
+ vcpus=1,
+ tenant_id='tenant4',
+ flavor='m1.tiny',
+ started_at=today,
+ ended_at=yesterday,
+ state='terminated',
+ uptime=43200),
+ ],
+ ),
+ ]
+ tenant_usages = dict(tenant_usages=raw_usages)
+ text = serializer.serialize(tenant_usages)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('tenant_usages', tree.tag)
+ self.assertEqual(len(raw_usages), len(tree))
+ for idx, child in enumerate(tree):
+ self._verify_tenant_usage(raw_usages[idx], child)
+
+
+class SimpleTenantUsageControllerTestV21(test.TestCase):
+ controller = simple_tenant_usage_v21.SimpleTenantUsageController()
+
+ def setUp(self):
+ super(SimpleTenantUsageControllerTestV21, self).setUp()
+
+ self.context = context.RequestContext('fakeuser', 'fake-project')
+
+ self.baseinst = get_fake_db_instance(START, STOP, instance_id=1,
+ tenant_id=self.context.project_id,
+ vm_state=vm_states.DELETED)
+ # convert the fake instance dict to an object
+ self.inst_obj = objects.Instance._from_db_object(
+ self.context, objects.Instance(), self.baseinst)
+
+ def test_get_flavor_from_sys_meta(self):
+ # Non-deleted instances get their type information from their
+ # system_metadata
+ with mock.patch.object(db, 'instance_get_by_uuid',
+ return_value=self.baseinst):
+ flavor = self.controller._get_flavor(self.context,
+ self.inst_obj, {})
+ self.assertEqual(objects.Flavor, type(flavor))
+ self.assertEqual(FAKE_INST_TYPE['id'], flavor.id)
+
+ def test_get_flavor_from_non_deleted_with_id_fails(self):
+ # If an instance is not deleted and missing type information from
+ # system_metadata, then that's a bug
+ self.inst_obj.system_metadata = {}
+ self.assertRaises(KeyError,
+ self.controller._get_flavor, self.context,
+ self.inst_obj, {})
+
+ def test_get_flavor_from_deleted_with_id(self):
+ # Deleted instances may not have type info in system_metadata,
+ # so verify that they get their type from a lookup of their
+ # instance_type_id
+ self.inst_obj.system_metadata = {}
+ self.inst_obj.deleted = 1
+ flavor = self.controller._get_flavor(self.context, self.inst_obj, {})
+ self.assertEqual(objects.Flavor, type(flavor))
+ self.assertEqual(FAKE_INST_TYPE['id'], flavor.id)
+
+ def test_get_flavor_from_deleted_with_id_of_deleted(self):
+ # Verify the legacy behavior of instance_type_id pointing to a
+ # missing type being non-fatal
+ self.inst_obj.system_metadata = {}
+ self.inst_obj.deleted = 1
+ self.inst_obj.instance_type_id = 99
+ flavor = self.controller._get_flavor(self.context, self.inst_obj, {})
+ self.assertIsNone(flavor)
+
+
+class SimpleTenantUsageControllerTestV2(SimpleTenantUsageControllerTestV21):
+ controller = simple_tenant_usage_v2.SimpleTenantUsageController()
+
+
+class SimpleTenantUsageUtilsV21(test.NoDBTestCase):
+ simple_tenant_usage = simple_tenant_usage_v21
+
+ def test_valid_string(self):
+ dt = self.simple_tenant_usage.parse_strtime(
+ "2014-02-21T13:47:20.824060", "%Y-%m-%dT%H:%M:%S.%f")
+ self.assertEqual(datetime.datetime(
+ microsecond=824060, second=20, minute=47, hour=13,
+ day=21, month=2, year=2014), dt)
+
+ def test_invalid_string(self):
+ self.assertRaises(exception.InvalidStrTime,
+ self.simple_tenant_usage.parse_strtime,
+ "2014-02-21 13:47:20.824060",
+ "%Y-%m-%dT%H:%M:%S.%f")
+
+
+class SimpleTenantUsageUtilsV2(SimpleTenantUsageUtilsV21):
+ simple_tenant_usage = simple_tenant_usage_v2
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_snapshots.py b/nova/tests/unit/api/openstack/compute/contrib/test_snapshots.py
new file mode 100644
index 0000000000..74bb1948e6
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_snapshots.py
@@ -0,0 +1,209 @@
+# Copyright 2011 Denali Systems, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+import webob
+
+from nova.api.openstack.compute.contrib import volumes
+from nova import context
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.volume import cinder
+
+
+class SnapshotApiTest(test.NoDBTestCase):
+ def setUp(self):
+ super(SnapshotApiTest, self).setUp()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+ self.stubs.Set(cinder.API, "create_snapshot",
+ fakes.stub_snapshot_create)
+ self.stubs.Set(cinder.API, "create_snapshot_force",
+ fakes.stub_snapshot_create)
+ self.stubs.Set(cinder.API, "delete_snapshot",
+ fakes.stub_snapshot_delete)
+ self.stubs.Set(cinder.API, "get_snapshot", fakes.stub_snapshot_get)
+ self.stubs.Set(cinder.API, "get_all_snapshots",
+ fakes.stub_snapshot_get_all)
+ self.stubs.Set(cinder.API, "get", fakes.stub_volume_get)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Volumes'])
+
+ self.context = context.get_admin_context()
+ self.app = fakes.wsgi_app(init_only=('os-snapshots',))
+
+ def test_snapshot_create(self):
+ snapshot = {"volume_id": 12,
+ "force": False,
+ "display_name": "Snapshot Test Name",
+ "display_description": "Snapshot Test Desc"}
+ body = dict(snapshot=snapshot)
+ req = webob.Request.blank('/v2/fake/os-snapshots')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+ resp_dict = jsonutils.loads(resp.body)
+ self.assertIn('snapshot', resp_dict)
+ self.assertEqual(resp_dict['snapshot']['displayName'],
+ snapshot['display_name'])
+ self.assertEqual(resp_dict['snapshot']['displayDescription'],
+ snapshot['display_description'])
+ self.assertEqual(resp_dict['snapshot']['volumeId'],
+ snapshot['volume_id'])
+
+ def test_snapshot_create_force(self):
+ snapshot = {"volume_id": 12,
+ "force": True,
+ "display_name": "Snapshot Test Name",
+ "display_description": "Snapshot Test Desc"}
+ body = dict(snapshot=snapshot)
+ req = webob.Request.blank('/v2/fake/os-snapshots')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+
+ resp_dict = jsonutils.loads(resp.body)
+ self.assertIn('snapshot', resp_dict)
+ self.assertEqual(resp_dict['snapshot']['displayName'],
+ snapshot['display_name'])
+ self.assertEqual(resp_dict['snapshot']['displayDescription'],
+ snapshot['display_description'])
+ self.assertEqual(resp_dict['snapshot']['volumeId'],
+ snapshot['volume_id'])
+
+ # Test invalid force paramter
+ snapshot = {"volume_id": 12,
+ "force": '**&&^^%%$$##@@'}
+ body = dict(snapshot=snapshot)
+ req = webob.Request.blank('/v2/fake/os-snapshots')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 400)
+
+ def test_snapshot_delete(self):
+ snapshot_id = 123
+ req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id)
+ req.method = 'DELETE'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 202)
+
+ def test_snapshot_delete_invalid_id(self):
+ snapshot_id = -1
+ req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id)
+ req.method = 'DELETE'
+
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 404)
+
+ def test_snapshot_show(self):
+ snapshot_id = 123
+ req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id)
+ req.method = 'GET'
+ resp = req.get_response(self.app)
+
+ self.assertEqual(resp.status_int, 200)
+ resp_dict = jsonutils.loads(resp.body)
+ self.assertIn('snapshot', resp_dict)
+ self.assertEqual(resp_dict['snapshot']['id'], str(snapshot_id))
+
+ def test_snapshot_show_invalid_id(self):
+ snapshot_id = -1
+ req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id)
+ req.method = 'GET'
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 404)
+
+ def test_snapshot_detail(self):
+ req = webob.Request.blank('/v2/fake/os-snapshots/detail')
+ req.method = 'GET'
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+
+ resp_dict = jsonutils.loads(resp.body)
+ self.assertIn('snapshots', resp_dict)
+ resp_snapshots = resp_dict['snapshots']
+ self.assertEqual(len(resp_snapshots), 3)
+
+ resp_snapshot = resp_snapshots.pop()
+ self.assertEqual(resp_snapshot['id'], 102)
+
+
+class SnapshotSerializerTest(test.NoDBTestCase):
+ def _verify_snapshot(self, snap, tree):
+ self.assertEqual(tree.tag, 'snapshot')
+
+ for attr in ('id', 'status', 'size', 'createdAt',
+ 'displayName', 'displayDescription', 'volumeId'):
+ self.assertEqual(str(snap[attr]), tree.get(attr))
+
+ def test_snapshot_show_create_serializer(self):
+ serializer = volumes.SnapshotTemplate()
+ raw_snapshot = dict(
+ id='snap_id',
+ status='snap_status',
+ size=1024,
+ createdAt=timeutils.utcnow(),
+ displayName='snap_name',
+ displayDescription='snap_desc',
+ volumeId='vol_id',
+ )
+ text = serializer.serialize(dict(snapshot=raw_snapshot))
+
+ tree = etree.fromstring(text)
+
+ self._verify_snapshot(raw_snapshot, tree)
+
+ def test_snapshot_index_detail_serializer(self):
+ serializer = volumes.SnapshotsTemplate()
+ raw_snapshots = [dict(
+ id='snap1_id',
+ status='snap1_status',
+ size=1024,
+ createdAt=timeutils.utcnow(),
+ displayName='snap1_name',
+ displayDescription='snap1_desc',
+ volumeId='vol1_id',
+ ),
+ dict(
+ id='snap2_id',
+ status='snap2_status',
+ size=1024,
+ createdAt=timeutils.utcnow(),
+ displayName='snap2_name',
+ displayDescription='snap2_desc',
+ volumeId='vol2_id',
+ )]
+ text = serializer.serialize(dict(snapshots=raw_snapshots))
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('snapshots', tree.tag)
+ self.assertEqual(len(raw_snapshots), len(tree))
+ for idx, child in enumerate(tree):
+ self._verify_snapshot(raw_snapshots[idx], child)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_tenant_networks.py b/nova/tests/unit/api/openstack/compute/contrib/test_tenant_networks.py
new file mode 100644
index 0000000000..30d4da6ba1
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_tenant_networks.py
@@ -0,0 +1,76 @@
+# Copyright 2014 IBM Corp.
+#
+# 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 mock
+import webob
+
+from nova.api.openstack.compute.contrib import os_tenant_networks as networks
+from nova.api.openstack.compute.plugins.v3 import tenant_networks \
+ as networks_v21
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+class TenantNetworksTestV21(test.NoDBTestCase):
+ ctrlr = networks_v21.TenantNetworkController
+
+ def setUp(self):
+ super(TenantNetworksTestV21, self).setUp()
+ self.controller = self.ctrlr()
+ self.flags(enable_network_quota=True)
+
+ @mock.patch('nova.network.api.API.delete',
+ side_effect=exception.NetworkInUse(network_id=1))
+ def test_network_delete_in_use(self, mock_delete):
+ req = fakes.HTTPRequest.blank('/v2/1234/os-tenant-networks/1')
+
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller.delete, req, 1)
+
+ @mock.patch('nova.quota.QUOTAS.reserve')
+ @mock.patch('nova.quota.QUOTAS.rollback')
+ @mock.patch('nova.network.api.API.delete')
+ def _test_network_delete_exception(self, ex, expex, delete_mock,
+ rollback_mock, reserve_mock):
+ req = fakes.HTTPRequest.blank('/v2/1234/os-tenant-networks')
+ ctxt = req.environ['nova.context']
+
+ reserve_mock.return_value = 'rv'
+ delete_mock.side_effect = ex
+
+ self.assertRaises(expex, self.controller.delete, req, 1)
+
+ delete_mock.assert_called_once_with(ctxt, 1)
+ rollback_mock.assert_called_once_with(ctxt, 'rv')
+ reserve_mock.assert_called_once_with(ctxt, networks=-1)
+
+ def test_network_delete_exception_network_not_found(self):
+ ex = exception.NetworkNotFound(network_id=1)
+ expex = webob.exc.HTTPNotFound
+ self._test_network_delete_exception(ex, expex)
+
+ def test_network_delete_exception_policy_failed(self):
+ ex = exception.PolicyNotAuthorized(action='dummy')
+ expex = webob.exc.HTTPForbidden
+ self._test_network_delete_exception(ex, expex)
+
+ def test_network_delete_exception_network_in_use(self):
+ ex = exception.NetworkInUse(network_id=1)
+ expex = webob.exc.HTTPConflict
+ self._test_network_delete_exception(ex, expex)
+
+
+class TenantNetworksTestV2(TenantNetworksTestV21):
+ ctrlr = networks.NetworkController
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_used_limits.py b/nova/tests/unit/api/openstack/compute/contrib/test_used_limits.py
new file mode 100644
index 0000000000..ee2b0d703b
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_used_limits.py
@@ -0,0 +1,306 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova.api.openstack.compute.contrib import used_limits as used_limits_v2
+from nova.api.openstack.compute import limits
+from nova.api.openstack.compute.plugins.v3 import used_limits as \
+ used_limits_v21
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+import nova.context
+from nova import exception
+from nova import quota
+from nova import test
+
+
+class FakeRequest(object):
+ def __init__(self, context, reserved=False):
+ self.environ = {'nova.context': context}
+ self.reserved = reserved
+ self.GET = {'reserved': 1} if reserved else {}
+
+
+class UsedLimitsTestCaseV21(test.NoDBTestCase):
+ used_limit_extension = "compute_extension:v3:os-used-limits:used_limits"
+ include_server_group_quotas = True
+
+ def setUp(self):
+ """Run before each test."""
+ super(UsedLimitsTestCaseV21, self).setUp()
+ self._set_up_controller()
+ self.fake_context = nova.context.RequestContext('fake', 'fake')
+
+ def _set_up_controller(self):
+ self.ext_mgr = None
+ self.controller = used_limits_v21.UsedLimitsController()
+ self.mox.StubOutWithMock(used_limits_v21, 'authorize')
+ self.authorize = used_limits_v21.authorize
+
+ def _do_test_used_limits(self, reserved):
+ fake_req = FakeRequest(self.fake_context, reserved=reserved)
+ obj = {
+ "limits": {
+ "rate": [],
+ "absolute": {},
+ },
+ }
+ res = wsgi.ResponseObject(obj)
+ quota_map = {
+ 'totalRAMUsed': 'ram',
+ 'totalCoresUsed': 'cores',
+ 'totalInstancesUsed': 'instances',
+ 'totalFloatingIpsUsed': 'floating_ips',
+ 'totalSecurityGroupsUsed': 'security_groups',
+ 'totalServerGroupsUsed': 'server_groups',
+ }
+ limits = {}
+ expected_abs_limits = []
+ for display_name, q in quota_map.iteritems():
+ limits[q] = {'limit': len(display_name),
+ 'in_use': len(display_name) / 2,
+ 'reserved': len(display_name) / 3}
+ if (self.include_server_group_quotas or
+ display_name != 'totalServerGroupsUsed'):
+ expected_abs_limits.append(display_name)
+
+ def stub_get_project_quotas(context, project_id, usages=True):
+ return limits
+
+ self.stubs.Set(quota.QUOTAS, "get_project_quotas",
+ stub_get_project_quotas)
+ if self.ext_mgr is not None:
+ self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
+ self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
+ self.include_server_group_quotas)
+ self.mox.ReplayAll()
+
+ self.controller.index(fake_req, res)
+ abs_limits = res.obj['limits']['absolute']
+ for limit in expected_abs_limits:
+ value = abs_limits[limit]
+ r = limits[quota_map[limit]]['reserved'] if reserved else 0
+ self.assertEqual(value,
+ limits[quota_map[limit]]['in_use'] + r)
+
+ def test_used_limits_basic(self):
+ self._do_test_used_limits(False)
+
+ def test_used_limits_with_reserved(self):
+ self._do_test_used_limits(True)
+
+ def test_admin_can_fetch_limits_for_a_given_tenant_id(self):
+ project_id = "123456"
+ user_id = "A1234"
+ tenant_id = 'abcd'
+ self.fake_context.project_id = project_id
+ self.fake_context.user_id = user_id
+ obj = {
+ "limits": {
+ "rate": [],
+ "absolute": {},
+ },
+ }
+ target = {
+ "project_id": tenant_id,
+ "user_id": user_id
+ }
+ fake_req = FakeRequest(self.fake_context)
+ fake_req.GET = {'tenant_id': tenant_id}
+ if self.ext_mgr is not None:
+ self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(True)
+ self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
+ self.include_server_group_quotas)
+ self.authorize(self.fake_context, target=target)
+ self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas')
+ quota.QUOTAS.get_project_quotas(self.fake_context, '%s' % tenant_id,
+ usages=True).AndReturn({})
+ self.mox.ReplayAll()
+ res = wsgi.ResponseObject(obj)
+ self.controller.index(fake_req, res)
+
+ def test_admin_can_fetch_used_limits_for_own_project(self):
+ project_id = "123456"
+ user_id = "A1234"
+ self.fake_context.project_id = project_id
+ self.fake_context.user_id = user_id
+ obj = {
+ "limits": {
+ "rate": [],
+ "absolute": {},
+ },
+ }
+ fake_req = FakeRequest(self.fake_context)
+ fake_req.GET = {}
+ if self.ext_mgr is not None:
+ self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(True)
+ self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
+ self.include_server_group_quotas)
+ self.mox.StubOutWithMock(extensions, 'extension_authorizer')
+ self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas')
+ quota.QUOTAS.get_project_quotas(self.fake_context, '%s' % project_id,
+ usages=True).AndReturn({})
+ self.mox.ReplayAll()
+ res = wsgi.ResponseObject(obj)
+ self.controller.index(fake_req, res)
+
+ def test_non_admin_cannot_fetch_used_limits_for_any_other_project(self):
+ project_id = "123456"
+ user_id = "A1234"
+ tenant_id = "abcd"
+ self.fake_context.project_id = project_id
+ self.fake_context.user_id = user_id
+ obj = {
+ "limits": {
+ "rate": [],
+ "absolute": {},
+ },
+ }
+ target = {
+ "project_id": tenant_id,
+ "user_id": user_id
+ }
+ fake_req = FakeRequest(self.fake_context)
+ fake_req.GET = {'tenant_id': tenant_id}
+ if self.ext_mgr is not None:
+ self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(True)
+ self.authorize(self.fake_context, target=target). \
+ AndRaise(exception.PolicyNotAuthorized(
+ action=self.used_limit_extension))
+ self.mox.ReplayAll()
+ res = wsgi.ResponseObject(obj)
+ self.assertRaises(exception.PolicyNotAuthorized, self.controller.index,
+ fake_req, res)
+
+ def test_used_limits_fetched_for_context_project_id(self):
+ project_id = "123456"
+ self.fake_context.project_id = project_id
+ obj = {
+ "limits": {
+ "rate": [],
+ "absolute": {},
+ },
+ }
+ fake_req = FakeRequest(self.fake_context)
+ if self.ext_mgr is not None:
+ self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
+ self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
+ self.include_server_group_quotas)
+ self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas')
+ quota.QUOTAS.get_project_quotas(self.fake_context, project_id,
+ usages=True).AndReturn({})
+ self.mox.ReplayAll()
+ res = wsgi.ResponseObject(obj)
+ self.controller.index(fake_req, res)
+
+ def test_used_ram_added(self):
+ fake_req = FakeRequest(self.fake_context)
+ obj = {
+ "limits": {
+ "rate": [],
+ "absolute": {
+ "maxTotalRAMSize": 512,
+ },
+ },
+ }
+ res = wsgi.ResponseObject(obj)
+
+ def stub_get_project_quotas(context, project_id, usages=True):
+ return {'ram': {'limit': 512, 'in_use': 256}}
+
+ if self.ext_mgr is not None:
+ self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
+ self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
+ self.include_server_group_quotas)
+ self.stubs.Set(quota.QUOTAS, "get_project_quotas",
+ stub_get_project_quotas)
+ self.mox.ReplayAll()
+
+ self.controller.index(fake_req, res)
+ abs_limits = res.obj['limits']['absolute']
+ self.assertIn('totalRAMUsed', abs_limits)
+ self.assertEqual(abs_limits['totalRAMUsed'], 256)
+
+ def test_no_ram_quota(self):
+ fake_req = FakeRequest(self.fake_context)
+ obj = {
+ "limits": {
+ "rate": [],
+ "absolute": {},
+ },
+ }
+ res = wsgi.ResponseObject(obj)
+
+ def stub_get_project_quotas(context, project_id, usages=True):
+ return {}
+
+ if self.ext_mgr is not None:
+ self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
+ self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(
+ self.include_server_group_quotas)
+ self.stubs.Set(quota.QUOTAS, "get_project_quotas",
+ stub_get_project_quotas)
+ self.mox.ReplayAll()
+
+ self.controller.index(fake_req, res)
+ abs_limits = res.obj['limits']['absolute']
+ self.assertNotIn('totalRAMUsed', abs_limits)
+
+
+class UsedLimitsTestCaseV2(UsedLimitsTestCaseV21):
+ used_limit_extension = "compute_extension:used_limits_for_admin"
+
+ def _set_up_controller(self):
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.controller = used_limits_v2.UsedLimitsController(self.ext_mgr)
+ self.mox.StubOutWithMock(used_limits_v2, 'authorize_for_admin')
+ self.authorize = used_limits_v2.authorize_for_admin
+
+
+class UsedLimitsTestCaseV2WithoutServerGroupQuotas(UsedLimitsTestCaseV2):
+ used_limit_extension = "compute_extension:used_limits_for_admin"
+ include_server_group_quotas = False
+
+
+class UsedLimitsTestCaseXml(test.NoDBTestCase):
+ def setUp(self):
+ """Run before each test."""
+ super(UsedLimitsTestCaseXml, self).setUp()
+ self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager)
+ self.controller = used_limits_v2.UsedLimitsController(self.ext_mgr)
+ self.fake_context = nova.context.RequestContext('fake', 'fake')
+
+ def test_used_limits_xmlns(self):
+ fake_req = FakeRequest(self.fake_context)
+ obj = {
+ "limits": {
+ "rate": [],
+ "absolute": {},
+ },
+ }
+ res = wsgi.ResponseObject(obj, xml=limits.LimitsTemplate)
+ res.preserialize('xml')
+
+ def stub_get_project_quotas(context, project_id, usages=True):
+ return {}
+
+ self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False)
+ self.stubs.Set(quota.QUOTAS, "get_project_quotas",
+ stub_get_project_quotas)
+ self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(False)
+ self.mox.ReplayAll()
+
+ self.controller.index(fake_req, res)
+ response = res.serialize(None, 'xml')
+ self.assertIn(used_limits_v2.XMLNS, response.body)
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py b/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py
new file mode 100644
index 0000000000..e8484d61b9
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py
@@ -0,0 +1,127 @@
+# Copyright (C) 2011 Midokura KK
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.contrib import virtual_interfaces
+from nova.api.openstack import wsgi
+from nova import compute
+from nova.compute import api as compute_api
+from nova import context
+from nova import exception
+from nova import network
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+
+
+def compute_api_get(self, context, instance_id, expected_attrs=None,
+ want_objects=False):
+ return dict(uuid=FAKE_UUID, id=instance_id, instance_type_id=1, host='bob')
+
+
+def get_vifs_by_instance(self, context, instance_id):
+ return [{'uuid': '00000000-0000-0000-0000-00000000000000000',
+ 'address': '00-00-00-00-00-00'},
+ {'uuid': '11111111-1111-1111-1111-11111111111111111',
+ 'address': '11-11-11-11-11-11'}]
+
+
+class FakeRequest(object):
+ def __init__(self, context):
+ self.environ = {'nova.context': context}
+
+
+class ServerVirtualInterfaceTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(ServerVirtualInterfaceTest, self).setUp()
+ self.stubs.Set(compute.api.API, "get",
+ compute_api_get)
+ self.stubs.Set(network.api.API, "get_vifs_by_instance",
+ get_vifs_by_instance)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Virtual_interfaces'])
+
+ def test_get_virtual_interfaces_list(self):
+ url = '/v2/fake/servers/abcd/os-virtual-interfaces'
+ req = webob.Request.blank(url)
+ res = req.get_response(fakes.wsgi_app(
+ init_only=('os-virtual-interfaces',)))
+ self.assertEqual(res.status_int, 200)
+ res_dict = jsonutils.loads(res.body)
+ response = {'virtual_interfaces': [
+ {'id': '00000000-0000-0000-0000-00000000000000000',
+ 'mac_address': '00-00-00-00-00-00'},
+ {'id': '11111111-1111-1111-1111-11111111111111111',
+ 'mac_address': '11-11-11-11-11-11'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_vif_instance_not_found(self):
+ self.mox.StubOutWithMock(compute_api.API, 'get')
+ fake_context = context.RequestContext('fake', 'fake')
+ fake_req = FakeRequest(fake_context)
+
+ compute_api.API.get(fake_context, 'fake_uuid',
+ expected_attrs=None,
+ want_objects=True).AndRaise(
+ exception.InstanceNotFound(instance_id='instance-0000'))
+
+ self.mox.ReplayAll()
+ self.assertRaises(
+ webob.exc.HTTPNotFound,
+ virtual_interfaces.ServerVirtualInterfaceController().index,
+ fake_req, 'fake_uuid')
+
+
+class ServerVirtualInterfaceSerializerTest(test.NoDBTestCase):
+ def setUp(self):
+ super(ServerVirtualInterfaceSerializerTest, self).setUp()
+ self.namespace = wsgi.XMLNS_V11
+ self.serializer = virtual_interfaces.VirtualInterfaceTemplate()
+
+ def _tag(self, elem):
+ tagname = elem.tag
+ self.assertEqual(tagname[0], '{')
+ tmp = tagname.partition('}')
+ namespace = tmp[0][1:]
+ self.assertEqual(namespace, self.namespace)
+ return tmp[2]
+
+ def test_serializer(self):
+ raw_vifs = [dict(
+ id='uuid1',
+ mac_address='aa:bb:cc:dd:ee:ff'),
+ dict(
+ id='uuid2',
+ mac_address='bb:aa:dd:cc:ff:ee')]
+ vifs = dict(virtual_interfaces=raw_vifs)
+ text = self.serializer.serialize(vifs)
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('virtual_interfaces', self._tag(tree))
+ self.assertEqual(len(raw_vifs), len(tree))
+ for idx, child in enumerate(tree):
+ self.assertEqual('virtual_interface', self._tag(child))
+ self.assertEqual(raw_vifs[idx]['id'], child.get('id'))
+ self.assertEqual(raw_vifs[idx]['mac_address'],
+ child.get('mac_address'))
diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py b/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py
new file mode 100644
index 0000000000..e3c5b8b071
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py
@@ -0,0 +1,1083 @@
+# Copyright 2013 Josh Durgin
+# Copyright 2013 Red Hat, Inc.
+# 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 datetime
+
+from lxml import etree
+import mock
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+import webob
+from webob import exc
+
+from nova.api.openstack.compute.contrib import assisted_volume_snapshots as \
+ assisted_snaps
+from nova.api.openstack.compute.contrib import volumes
+from nova.api.openstack.compute.plugins.v3 import volumes as volumes_v3
+from nova.api.openstack import extensions
+from nova.compute import api as compute_api
+from nova.compute import flavors
+from nova import context
+from nova import db
+from nova import exception
+from nova import objects
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_block_device
+from nova.tests.unit import fake_instance
+from nova.volume import cinder
+
+CONF = cfg.CONF
+CONF.import_opt('password_length', 'nova.utils')
+
+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+FAKE_UUID_A = '00000000-aaaa-aaaa-aaaa-000000000000'
+FAKE_UUID_B = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
+FAKE_UUID_C = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
+FAKE_UUID_D = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
+
+IMAGE_UUID = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+
+
+def fake_get_instance(self, context, instance_id, want_objects=False,
+ expected_attrs=None):
+ return fake_instance.fake_instance_obj(context, **{'uuid': instance_id})
+
+
+def fake_get_volume(self, context, id):
+ return {'id': 'woot'}
+
+
+def fake_attach_volume(self, context, instance, volume_id, device):
+ pass
+
+
+def fake_detach_volume(self, context, instance, volume):
+ pass
+
+
+def fake_swap_volume(self, context, instance,
+ old_volume_id, new_volume_id):
+ pass
+
+
+def fake_create_snapshot(self, context, volume, name, description):
+ return {'id': 123,
+ 'volume_id': 'fakeVolId',
+ 'status': 'available',
+ 'volume_size': 123,
+ 'created_at': '2013-01-01 00:00:01',
+ 'display_name': 'myVolumeName',
+ 'display_description': 'myVolumeDescription'}
+
+
+def fake_delete_snapshot(self, context, snapshot_id):
+ pass
+
+
+def fake_compute_volume_snapshot_delete(self, context, volume_id, snapshot_id,
+ delete_info):
+ pass
+
+
+def fake_compute_volume_snapshot_create(self, context, volume_id,
+ create_info):
+ pass
+
+
+def fake_bdms_get_all_by_instance(context, instance_uuid, use_slave=False):
+ return [fake_block_device.FakeDbBlockDeviceDict(
+ {'id': 1,
+ 'instance_uuid': instance_uuid,
+ 'device_name': '/dev/fake0',
+ 'delete_on_termination': 'False',
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'snapshot_id': None,
+ 'volume_id': FAKE_UUID_A,
+ 'volume_size': 1}),
+ fake_block_device.FakeDbBlockDeviceDict(
+ {'id': 2,
+ 'instance_uuid': instance_uuid,
+ 'device_name': '/dev/fake1',
+ 'delete_on_termination': 'False',
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'snapshot_id': None,
+ 'volume_id': FAKE_UUID_B,
+ 'volume_size': 1})]
+
+
+class BootFromVolumeTest(test.TestCase):
+
+ def setUp(self):
+ super(BootFromVolumeTest, self).setUp()
+ self.stubs.Set(compute_api.API, 'create',
+ self._get_fake_compute_api_create())
+ fakes.stub_out_nw_api(self.stubs)
+ self._block_device_mapping_seen = None
+ self._legacy_bdm_seen = True
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Volumes', 'Block_device_mapping_v2_boot'])
+
+ def _get_fake_compute_api_create(self):
+ def _fake_compute_api_create(cls, context, instance_type,
+ image_href, **kwargs):
+ self._block_device_mapping_seen = kwargs.get(
+ 'block_device_mapping')
+ self._legacy_bdm_seen = kwargs.get('legacy_bdm')
+
+ inst_type = flavors.get_flavor_by_flavor_id(2)
+ resv_id = None
+ return ([{'id': 1,
+ 'display_name': 'test_server',
+ 'uuid': FAKE_UUID,
+ 'instance_type': inst_type,
+ 'access_ip_v4': '1.2.3.4',
+ 'access_ip_v6': 'fead::1234',
+ 'image_ref': IMAGE_UUID,
+ 'user_id': 'fake',
+ 'project_id': 'fake',
+ 'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0),
+ 'updated_at': datetime.datetime(2010, 11, 11, 11, 0, 0),
+ 'progress': 0,
+ 'fixed_ips': []
+ }], resv_id)
+ return _fake_compute_api_create
+
+ def test_create_root_volume(self):
+ body = dict(server=dict(
+ name='test_server', imageRef=IMAGE_UUID,
+ flavorRef=2, min_count=1, max_count=1,
+ block_device_mapping=[dict(
+ volume_id=1,
+ device_name='/dev/vda',
+ virtual='root',
+ delete_on_termination=False,
+ )]
+ ))
+ req = webob.Request.blank('/v2/fake/os-volumes_boot')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app(
+ init_only=('os-volumes_boot', 'servers')))
+ self.assertEqual(res.status_int, 202)
+ server = jsonutils.loads(res.body)['server']
+ self.assertEqual(FAKE_UUID, server['id'])
+ self.assertEqual(CONF.password_length, len(server['adminPass']))
+ self.assertEqual(len(self._block_device_mapping_seen), 1)
+ self.assertTrue(self._legacy_bdm_seen)
+ self.assertEqual(self._block_device_mapping_seen[0]['volume_id'], 1)
+ self.assertEqual(self._block_device_mapping_seen[0]['device_name'],
+ '/dev/vda')
+
+ def test_create_root_volume_bdm_v2(self):
+ body = dict(server=dict(
+ name='test_server', imageRef=IMAGE_UUID,
+ flavorRef=2, min_count=1, max_count=1,
+ block_device_mapping_v2=[dict(
+ source_type='volume',
+ uuid=1,
+ device_name='/dev/vda',
+ boot_index=0,
+ delete_on_termination=False,
+ )]
+ ))
+ req = webob.Request.blank('/v2/fake/os-volumes_boot')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app(
+ init_only=('os-volumes_boot', 'servers')))
+ self.assertEqual(res.status_int, 202)
+ server = jsonutils.loads(res.body)['server']
+ self.assertEqual(FAKE_UUID, server['id'])
+ self.assertEqual(CONF.password_length, len(server['adminPass']))
+ self.assertEqual(len(self._block_device_mapping_seen), 1)
+ self.assertFalse(self._legacy_bdm_seen)
+ self.assertEqual(self._block_device_mapping_seen[0]['volume_id'], 1)
+ self.assertEqual(self._block_device_mapping_seen[0]['boot_index'],
+ 0)
+ self.assertEqual(self._block_device_mapping_seen[0]['device_name'],
+ '/dev/vda')
+
+
+class VolumeApiTestV21(test.TestCase):
+ url_prefix = '/v2/fake'
+
+ def setUp(self):
+ super(VolumeApiTestV21, self).setUp()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+
+ self.stubs.Set(cinder.API, "delete", fakes.stub_volume_delete)
+ self.stubs.Set(cinder.API, "get", fakes.stub_volume_get)
+ self.stubs.Set(cinder.API, "get_all", fakes.stub_volume_get_all)
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Volumes'])
+
+ self.context = context.get_admin_context()
+ self.app = self._get_app()
+
+ def _get_app(self):
+ return fakes.wsgi_app_v21()
+
+ def test_volume_create(self):
+ self.stubs.Set(cinder.API, "create", fakes.stub_volume_create)
+
+ vol = {"size": 100,
+ "display_name": "Volume Test Name",
+ "display_description": "Volume Test Desc",
+ "availability_zone": "zone1:host1"}
+ body = {"volume": vol}
+ req = webob.Request.blank(self.url_prefix + '/os-volumes')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers['content-type'] = 'application/json'
+ resp = req.get_response(self.app)
+
+ self.assertEqual(resp.status_int, 200)
+
+ resp_dict = jsonutils.loads(resp.body)
+ self.assertIn('volume', resp_dict)
+ self.assertEqual(resp_dict['volume']['size'],
+ vol['size'])
+ self.assertEqual(resp_dict['volume']['displayName'],
+ vol['display_name'])
+ self.assertEqual(resp_dict['volume']['displayDescription'],
+ vol['display_description'])
+ self.assertEqual(resp_dict['volume']['availabilityZone'],
+ vol['availability_zone'])
+
+ def test_volume_create_bad(self):
+ def fake_volume_create(self, context, size, name, description,
+ snapshot, **param):
+ raise exception.InvalidInput(reason="bad request data")
+
+ self.stubs.Set(cinder.API, "create", fake_volume_create)
+
+ vol = {"size": '#$?',
+ "display_name": "Volume Test Name",
+ "display_description": "Volume Test Desc",
+ "availability_zone": "zone1:host1"}
+ body = {"volume": vol}
+
+ req = fakes.HTTPRequest.blank(self.url_prefix + '/os-volumes')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ volumes.VolumeController().create, req, body)
+
+ def test_volume_index(self):
+ req = webob.Request.blank(self.url_prefix + '/os-volumes')
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+
+ def test_volume_detail(self):
+ req = webob.Request.blank(self.url_prefix + '/os-volumes/detail')
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+
+ def test_volume_show(self):
+ req = webob.Request.blank(self.url_prefix + '/os-volumes/123')
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 200)
+
+ def test_volume_show_no_volume(self):
+ self.stubs.Set(cinder.API, "get", fakes.stub_volume_notfound)
+
+ req = webob.Request.blank(self.url_prefix + '/os-volumes/456')
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 404)
+ self.assertIn('Volume 456 could not be found.', resp.body)
+
+ def test_volume_delete(self):
+ req = webob.Request.blank(self.url_prefix + '/os-volumes/123')
+ req.method = 'DELETE'
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 202)
+
+ def test_volume_delete_no_volume(self):
+ self.stubs.Set(cinder.API, "delete", fakes.stub_volume_notfound)
+
+ req = webob.Request.blank(self.url_prefix + '/os-volumes/456')
+ req.method = 'DELETE'
+ resp = req.get_response(self.app)
+ self.assertEqual(resp.status_int, 404)
+ self.assertIn('Volume 456 could not be found.', resp.body)
+
+
+class VolumeApiTestV2(VolumeApiTestV21):
+
+ def setUp(self):
+ super(VolumeApiTestV2, self).setUp()
+ self.flags(
+ osapi_compute_extension=[
+ 'nova.api.openstack.compute.contrib.select_extensions'],
+ osapi_compute_ext_list=['Volumes'])
+
+ self.context = context.get_admin_context()
+ self.app = self._get_app()
+
+ def _get_app(self):
+ return fakes.wsgi_app()
+
+
+class VolumeAttachTests(test.TestCase):
+ def setUp(self):
+ super(VolumeAttachTests, self).setUp()
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ fake_bdms_get_all_by_instance)
+ self.stubs.Set(compute_api.API, 'get', fake_get_instance)
+ self.stubs.Set(cinder.API, 'get', fake_get_volume)
+ self.context = context.get_admin_context()
+ self.expected_show = {'volumeAttachment':
+ {'device': '/dev/fake0',
+ 'serverId': FAKE_UUID,
+ 'id': FAKE_UUID_A,
+ 'volumeId': FAKE_UUID_A
+ }}
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.attachments = volumes.VolumeAttachmentController(self.ext_mgr)
+
+ def test_show(self):
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ result = self.attachments.show(req, FAKE_UUID, FAKE_UUID_A)
+ self.assertEqual(self.expected_show, result)
+
+ @mock.patch.object(compute_api.API, 'get',
+ side_effect=exception.InstanceNotFound(instance_id=FAKE_UUID))
+ def test_show_no_instance(self, mock_mr):
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(exc.HTTPNotFound,
+ self.attachments.show,
+ req,
+ FAKE_UUID,
+ FAKE_UUID_A)
+
+ @mock.patch.object(objects.BlockDeviceMappingList,
+ 'get_by_instance_uuid', return_value=None)
+ def test_show_no_bdms(self, mock_mr):
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(exc.HTTPNotFound,
+ self.attachments.show,
+ req,
+ FAKE_UUID,
+ FAKE_UUID_A)
+
+ def test_show_bdms_no_mountpoint(self):
+ FAKE_UUID_NOTEXIST = '00000000-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(exc.HTTPNotFound,
+ self.attachments.show,
+ req,
+ FAKE_UUID,
+ FAKE_UUID_NOTEXIST)
+
+ def test_detach(self):
+ self.stubs.Set(compute_api.API,
+ 'detach_volume',
+ fake_detach_volume)
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
+ req.method = 'DELETE'
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ result = self.attachments.delete(req, FAKE_UUID, FAKE_UUID_A)
+ self.assertEqual('202 Accepted', result.status)
+
+ def test_detach_vol_not_found(self):
+ self.stubs.Set(compute_api.API,
+ 'detach_volume',
+ fake_detach_volume)
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
+ req.method = 'DELETE'
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(exc.HTTPNotFound,
+ self.attachments.delete,
+ req,
+ FAKE_UUID,
+ FAKE_UUID_C)
+
+ @mock.patch('nova.objects.BlockDeviceMapping.is_root',
+ new_callable=mock.PropertyMock)
+ def test_detach_vol_root(self, mock_isroot):
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
+ req.method = 'DELETE'
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ mock_isroot.return_value = True
+ self.assertRaises(exc.HTTPForbidden,
+ self.attachments.delete,
+ req,
+ FAKE_UUID,
+ FAKE_UUID_A)
+
+ def test_detach_volume_from_locked_server(self):
+ def fake_detach_volume_from_locked_server(self, context,
+ instance, volume):
+ raise exception.InstanceIsLocked(instance_uuid=instance['uuid'])
+
+ self.stubs.Set(compute_api.API,
+ 'detach_volume',
+ fake_detach_volume_from_locked_server)
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
+ req.method = 'DELETE'
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(webob.exc.HTTPConflict, self.attachments.delete,
+ req, FAKE_UUID, FAKE_UUID_A)
+
+ def test_attach_volume(self):
+ self.stubs.Set(compute_api.API,
+ 'attach_volume',
+ fake_attach_volume)
+ body = {'volumeAttachment': {'volumeId': FAKE_UUID_A,
+ 'device': '/dev/fake'}}
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ result = self.attachments.create(req, FAKE_UUID, body)
+ self.assertEqual(result['volumeAttachment']['id'],
+ '00000000-aaaa-aaaa-aaaa-000000000000')
+
+ def test_attach_volume_to_locked_server(self):
+ def fake_attach_volume_to_locked_server(self, context, instance,
+ volume_id, device=None):
+ raise exception.InstanceIsLocked(instance_uuid=instance['uuid'])
+
+ self.stubs.Set(compute_api.API,
+ 'attach_volume',
+ fake_attach_volume_to_locked_server)
+ body = {'volumeAttachment': {'volumeId': FAKE_UUID_A,
+ 'device': '/dev/fake'}}
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(webob.exc.HTTPConflict, self.attachments.create,
+ req, FAKE_UUID, body)
+
+ def test_attach_volume_bad_id(self):
+ self.stubs.Set(compute_api.API,
+ 'attach_volume',
+ fake_attach_volume)
+
+ body = {
+ 'volumeAttachment': {
+ 'device': None,
+ 'volumeId': 'TESTVOLUME',
+ }
+ }
+
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(webob.exc.HTTPBadRequest, self.attachments.create,
+ req, FAKE_UUID, body)
+
+ def test_attach_volume_without_volumeId(self):
+ self.stubs.Set(compute_api.API,
+ 'attach_volume',
+ fake_attach_volume)
+
+ body = {
+ 'volumeAttachment': {
+ 'device': None
+ }
+ }
+
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments')
+ req.method = 'POST'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+
+ self.assertRaises(webob.exc.HTTPBadRequest, self.attachments.create,
+ req, FAKE_UUID, body)
+
+ def _test_swap(self, uuid=FAKE_UUID_A, fake_func=None, body=None):
+ fake_func = fake_func or fake_swap_volume
+ self.stubs.Set(compute_api.API,
+ 'swap_volume',
+ fake_func)
+ body = body or {'volumeAttachment': {'volumeId': FAKE_UUID_B,
+ 'device': '/dev/fake'}}
+
+ req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid')
+ req.method = 'PUT'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ return self.attachments.update(req, FAKE_UUID, uuid, body)
+
+ def test_swap_volume_for_locked_server(self):
+ self.ext_mgr.extensions['os-volume-attachment-update'] = True
+
+ def fake_swap_volume_for_locked_server(self, context, instance,
+ old_volume, new_volume):
+ raise exception.InstanceIsLocked(instance_uuid=instance['uuid'])
+
+ self.ext_mgr.extensions['os-volume-attachment-update'] = True
+ self.assertRaises(webob.exc.HTTPConflict, self._test_swap,
+ fake_func=fake_swap_volume_for_locked_server)
+
+ def test_swap_volume_no_extension(self):
+ self.assertRaises(webob.exc.HTTPBadRequest, self._test_swap)
+
+ def test_swap_volume(self):
+ self.ext_mgr.extensions['os-volume-attachment-update'] = True
+ result = self._test_swap()
+ self.assertEqual('202 Accepted', result.status)
+
+ def test_swap_volume_no_attachment(self):
+ self.ext_mgr.extensions['os-volume-attachment-update'] = True
+
+ self.assertRaises(exc.HTTPNotFound, self._test_swap, FAKE_UUID_C)
+
+ def test_swap_volume_without_volumeId(self):
+ self.ext_mgr.extensions['os-volume-attachment-update'] = True
+ body = {'volumeAttachment': {'device': '/dev/fake'}}
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_swap,
+ body=body)
+
+
+class VolumeSerializerTest(test.TestCase):
+ def _verify_volume_attachment(self, attach, tree):
+ for attr in ('id', 'volumeId', 'serverId', 'device'):
+ self.assertEqual(str(attach[attr]), tree.get(attr))
+
+ def _verify_volume(self, vol, tree):
+ self.assertEqual(tree.tag, 'volume')
+
+ for attr in ('id', 'status', 'size', 'availabilityZone', 'createdAt',
+ 'displayName', 'displayDescription', 'volumeType',
+ 'snapshotId'):
+ self.assertEqual(str(vol[attr]), tree.get(attr))
+
+ for child in tree:
+ self.assertIn(child.tag, ('attachments', 'metadata'))
+ if child.tag == 'attachments':
+ self.assertEqual(1, len(child))
+ self.assertEqual('attachment', child[0].tag)
+ self._verify_volume_attachment(vol['attachments'][0], child[0])
+ elif child.tag == 'metadata':
+ not_seen = set(vol['metadata'].keys())
+ for gr_child in child:
+ self.assertIn(gr_child.get("key"), not_seen)
+ self.assertEqual(str(vol['metadata'][gr_child.get("key")]),
+ gr_child.text)
+ not_seen.remove(gr_child.get("key"))
+ self.assertEqual(0, len(not_seen))
+
+ def test_attach_show_create_serializer(self):
+ serializer = volumes.VolumeAttachmentTemplate()
+ raw_attach = dict(
+ id='vol_id',
+ volumeId='vol_id',
+ serverId='instance_uuid',
+ device='/foo')
+ text = serializer.serialize(dict(volumeAttachment=raw_attach))
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('volumeAttachment', tree.tag)
+ self._verify_volume_attachment(raw_attach, tree)
+
+ def test_attach_index_serializer(self):
+ serializer = volumes.VolumeAttachmentsTemplate()
+ raw_attaches = [dict(
+ id='vol_id1',
+ volumeId='vol_id1',
+ serverId='instance1_uuid',
+ device='/foo1'),
+ dict(
+ id='vol_id2',
+ volumeId='vol_id2',
+ serverId='instance2_uuid',
+ device='/foo2')]
+ text = serializer.serialize(dict(volumeAttachments=raw_attaches))
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('volumeAttachments', tree.tag)
+ self.assertEqual(len(raw_attaches), len(tree))
+ for idx, child in enumerate(tree):
+ self.assertEqual('volumeAttachment', child.tag)
+ self._verify_volume_attachment(raw_attaches[idx], child)
+
+ def test_volume_show_create_serializer(self):
+ serializer = volumes.VolumeTemplate()
+ raw_volume = dict(
+ id='vol_id',
+ status='vol_status',
+ size=1024,
+ availabilityZone='vol_availability',
+ createdAt=timeutils.utcnow(),
+ attachments=[dict(
+ id='vol_id',
+ volumeId='vol_id',
+ serverId='instance_uuid',
+ device='/foo')],
+ displayName='vol_name',
+ displayDescription='vol_desc',
+ volumeType='vol_type',
+ snapshotId='snap_id',
+ metadata=dict(
+ foo='bar',
+ baz='quux',
+ ),
+ )
+ text = serializer.serialize(dict(volume=raw_volume))
+
+ tree = etree.fromstring(text)
+
+ self._verify_volume(raw_volume, tree)
+
+ def test_volume_index_detail_serializer(self):
+ serializer = volumes.VolumesTemplate()
+ raw_volumes = [dict(
+ id='vol1_id',
+ status='vol1_status',
+ size=1024,
+ availabilityZone='vol1_availability',
+ createdAt=timeutils.utcnow(),
+ attachments=[dict(
+ id='vol1_id',
+ volumeId='vol1_id',
+ serverId='instance_uuid',
+ device='/foo1')],
+ displayName='vol1_name',
+ displayDescription='vol1_desc',
+ volumeType='vol1_type',
+ snapshotId='snap1_id',
+ metadata=dict(
+ foo='vol1_foo',
+ bar='vol1_bar',
+ ),
+ ),
+ dict(
+ id='vol2_id',
+ status='vol2_status',
+ size=1024,
+ availabilityZone='vol2_availability',
+ createdAt=timeutils.utcnow(),
+ attachments=[dict(
+ id='vol2_id',
+ volumeId='vol2_id',
+ serverId='instance_uuid',
+ device='/foo2')],
+ displayName='vol2_name',
+ displayDescription='vol2_desc',
+ volumeType='vol2_type',
+ snapshotId='snap2_id',
+ metadata=dict(
+ foo='vol2_foo',
+ bar='vol2_bar',
+ ),
+ )]
+ text = serializer.serialize(dict(volumes=raw_volumes))
+
+ tree = etree.fromstring(text)
+
+ self.assertEqual('volumes', tree.tag)
+ self.assertEqual(len(raw_volumes), len(tree))
+ for idx, child in enumerate(tree):
+ self._verify_volume(raw_volumes[idx], child)
+
+
+class TestVolumeCreateRequestXMLDeserializer(test.TestCase):
+
+ def setUp(self):
+ super(TestVolumeCreateRequestXMLDeserializer, self).setUp()
+ self.deserializer = volumes.CreateDeserializer()
+
+ def test_minimal_volume(self):
+ self_request = """
+<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
+ size="1"></volume>"""
+ request = self.deserializer.deserialize(self_request)
+ expected = {
+ "volume": {
+ "size": "1",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_display_name(self):
+ self_request = """
+<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
+ size="1"
+ display_name="Volume-xml"></volume>"""
+ request = self.deserializer.deserialize(self_request)
+ expected = {
+ "volume": {
+ "size": "1",
+ "display_name": "Volume-xml",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_display_description(self):
+ self_request = """
+<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
+ size="1"
+ display_name="Volume-xml"
+ display_description="description"></volume>"""
+ request = self.deserializer.deserialize(self_request)
+ expected = {
+ "volume": {
+ "size": "1",
+ "display_name": "Volume-xml",
+ "display_description": "description",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_volume_type(self):
+ self_request = """
+<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
+ size="1"
+ display_name="Volume-xml"
+ display_description="description"
+ volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"></volume>"""
+ request = self.deserializer.deserialize(self_request)
+ expected = {
+ "volume": {
+ "size": "1",
+ "display_name": "Volume-xml",
+ "display_description": "description",
+ "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_availability_zone(self):
+ self_request = """
+<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
+ size="1"
+ display_name="Volume-xml"
+ display_description="description"
+ volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"
+ availability_zone="us-east1"></volume>"""
+ request = self.deserializer.deserialize(self_request)
+ expected = {
+ "volume": {
+ "size": "1",
+ "display_name": "Volume-xml",
+ "display_description": "description",
+ "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "availability_zone": "us-east1",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_metadata(self):
+ self_request = """
+<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
+ display_name="Volume-xml"
+ size="1">
+ <metadata><meta key="Type">work</meta></metadata></volume>"""
+ request = self.deserializer.deserialize(self_request)
+ expected = {
+ "volume": {
+ "display_name": "Volume-xml",
+ "size": "1",
+ "metadata": {
+ "Type": "work",
+ },
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_full_volume(self):
+ self_request = """
+<volume xmlns="http://docs.openstack.org/compute/api/v1.1"
+ size="1"
+ display_name="Volume-xml"
+ display_description="description"
+ volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"
+ availability_zone="us-east1">
+ <metadata><meta key="Type">work</meta></metadata></volume>"""
+ request = self.deserializer.deserialize(self_request)
+ expected = {
+ "volume": {
+ "size": "1",
+ "display_name": "Volume-xml",
+ "display_description": "description",
+ "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "availability_zone": "us-east1",
+ "metadata": {
+ "Type": "work",
+ },
+ },
+ }
+ self.maxDiff = None
+ self.assertEqual(request['body'], expected)
+
+
+class CommonBadRequestTestCase(object):
+
+ resource = None
+ entity_name = None
+ controller_cls = None
+ kwargs = {}
+
+ """
+ Tests of places we throw 400 Bad Request from
+ """
+
+ def setUp(self):
+ super(CommonBadRequestTestCase, self).setUp()
+ self.controller = self.controller_cls()
+
+ def _bad_request_create(self, body):
+ req = fakes.HTTPRequest.blank('/v2/fake/' + self.resource)
+ req.method = 'POST'
+
+ kwargs = self.kwargs.copy()
+ kwargs['body'] = body
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, req, **kwargs)
+
+ def test_create_no_body(self):
+ self._bad_request_create(body=None)
+
+ def test_create_missing_volume(self):
+ body = {'foo': {'a': 'b'}}
+ self._bad_request_create(body=body)
+
+ def test_create_malformed_entity(self):
+ body = {self.entity_name: 'string'}
+ self._bad_request_create(body=body)
+
+
+class BadRequestVolumeTestCaseV21(CommonBadRequestTestCase,
+ test.TestCase):
+
+ resource = 'os-volumes'
+ entity_name = 'volume'
+ controller_cls = volumes_v3.VolumeController
+
+
+class BadRequestVolumeTestCaseV2(BadRequestVolumeTestCaseV21):
+ controller_cls = volumes.VolumeController
+
+
+class BadRequestAttachmentTestCase(CommonBadRequestTestCase,
+ test.TestCase):
+ resource = 'servers/' + FAKE_UUID + '/os-volume_attachments'
+ entity_name = 'volumeAttachment'
+ controller_cls = volumes.VolumeAttachmentController
+ kwargs = {'server_id': FAKE_UUID}
+
+
+class BadRequestSnapshotTestCaseV21(CommonBadRequestTestCase,
+ test.TestCase):
+
+ resource = 'os-snapshots'
+ entity_name = 'snapshot'
+ controller_cls = volumes.SnapshotController
+
+
+class BadRequestSnapshotTestCaseV2(BadRequestSnapshotTestCaseV21):
+ controller_cls = volumes_v3.SnapshotController
+
+
+class ShowSnapshotTestCaseV21(test.TestCase):
+ snapshot_cls = volumes_v3.SnapshotController
+
+ def setUp(self):
+ super(ShowSnapshotTestCaseV21, self).setUp()
+ self.controller = self.snapshot_cls()
+ self.req = fakes.HTTPRequest.blank('/v2/fake/os-snapshots')
+ self.req.method = 'GET'
+
+ def test_show_snapshot_not_exist(self):
+ def fake_get_snapshot(self, context, id):
+ raise exception.SnapshotNotFound(snapshot_id=id)
+ self.stubs.Set(cinder.API, 'get_snapshot', fake_get_snapshot)
+ self.assertRaises(exc.HTTPNotFound,
+ self.controller.show, self.req, FAKE_UUID_A)
+
+
+class ShowSnapshotTestCaseV2(ShowSnapshotTestCaseV21):
+ snapshot_cls = volumes.SnapshotController
+
+
+class CreateSnapshotTestCaseV21(test.TestCase):
+ snapshot_cls = volumes_v3.SnapshotController
+
+ def setUp(self):
+ super(CreateSnapshotTestCaseV21, self).setUp()
+ self.controller = self.snapshot_cls()
+ self.stubs.Set(cinder.API, 'get', fake_get_volume)
+ self.stubs.Set(cinder.API, 'create_snapshot_force',
+ fake_create_snapshot)
+ self.stubs.Set(cinder.API, 'create_snapshot', fake_create_snapshot)
+ self.req = fakes.HTTPRequest.blank('/v2/fake/os-snapshots')
+ self.req.method = 'POST'
+ self.body = {'snapshot': {'volume_id': 1}}
+
+ def test_force_true(self):
+ self.body['snapshot']['force'] = 'True'
+ self.controller.create(self.req, body=self.body)
+
+ def test_force_false(self):
+ self.body['snapshot']['force'] = 'f'
+ self.controller.create(self.req, body=self.body)
+
+ def test_force_invalid(self):
+ self.body['snapshot']['force'] = 'foo'
+ self.assertRaises(exc.HTTPBadRequest,
+ self.controller.create, self.req, body=self.body)
+
+
+class CreateSnapshotTestCaseV2(CreateSnapshotTestCaseV21):
+ snapshot_cls = volumes.SnapshotController
+
+
+class DeleteSnapshotTestCaseV21(test.TestCase):
+ snapshot_cls = volumes_v3.SnapshotController
+
+ def setUp(self):
+ super(DeleteSnapshotTestCaseV21, self).setUp()
+ self.controller = self.snapshot_cls()
+ self.stubs.Set(cinder.API, 'get', fake_get_volume)
+ self.stubs.Set(cinder.API, 'create_snapshot_force',
+ fake_create_snapshot)
+ self.stubs.Set(cinder.API, 'create_snapshot', fake_create_snapshot)
+ self.stubs.Set(cinder.API, 'delete_snapshot', fake_delete_snapshot)
+ self.req = fakes.HTTPRequest.blank('/v2/fake/os-snapshots')
+
+ def test_normal_delete(self):
+ self.req.method = 'POST'
+ self.body = {'snapshot': {'volume_id': 1}}
+ result = self.controller.create(self.req, body=self.body)
+
+ self.req.method = 'DELETE'
+ result = self.controller.delete(self.req, result['snapshot']['id'])
+
+ # NOTE: on v2.1, http status code is set as wsgi_code of API
+ # method instead of status_int in a response object.
+ if isinstance(self.controller, volumes_v3.SnapshotController):
+ status_int = self.controller.delete.wsgi_code
+ else:
+ status_int = result.status_int
+ self.assertEqual(202, status_int)
+
+ def test_delete_snapshot_not_exists(self):
+ def fake_delete_snapshot_not_exist(self, context, snapshot_id):
+ raise exception.SnapshotNotFound(snapshot_id=snapshot_id)
+
+ self.stubs.Set(cinder.API, 'delete_snapshot',
+ fake_delete_snapshot_not_exist)
+ self.req.method = 'POST'
+ self.body = {'snapshot': {'volume_id': 1}}
+ result = self.controller.create(self.req, body=self.body)
+
+ self.req.method = 'DELETE'
+ self.assertRaises(exc.HTTPNotFound, self.controller.delete,
+ self.req, result['snapshot']['id'])
+
+
+class DeleteSnapshotTestCaseV2(DeleteSnapshotTestCaseV21):
+ snapshot_cls = volumes.SnapshotController
+
+
+class AssistedSnapshotCreateTestCase(test.TestCase):
+ def setUp(self):
+ super(AssistedSnapshotCreateTestCase, self).setUp()
+
+ self.controller = assisted_snaps.AssistedVolumeSnapshotsController()
+ self.stubs.Set(compute_api.API, 'volume_snapshot_create',
+ fake_compute_volume_snapshot_create)
+
+ def test_assisted_create(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-assisted-volume-snapshots')
+ body = {'snapshot': {'volume_id': 1, 'create_info': {}}}
+ req.method = 'POST'
+ self.controller.create(req, body=body)
+
+ def test_assisted_create_missing_create_info(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-assisted-volume-snapshots')
+ body = {'snapshot': {'volume_id': 1}}
+ req.method = 'POST'
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ req, body=body)
+
+
+class AssistedSnapshotDeleteTestCase(test.TestCase):
+ def setUp(self):
+ super(AssistedSnapshotDeleteTestCase, self).setUp()
+
+ self.controller = assisted_snaps.AssistedVolumeSnapshotsController()
+ self.stubs.Set(compute_api.API, 'volume_snapshot_delete',
+ fake_compute_volume_snapshot_delete)
+
+ def test_assisted_delete(self):
+ params = {
+ 'delete_info': jsonutils.dumps({'volume_id': 1}),
+ }
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake/os-assisted-volume-snapshots?%s' %
+ '&'.join(['%s=%s' % (k, v) for k, v in params.iteritems()]))
+ req.method = 'DELETE'
+ result = self.controller.delete(req, '5')
+ self.assertEqual(result.status_int, 204)
+
+ def test_assisted_delete_missing_delete_info(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/os-assisted-volume-snapshots')
+ req.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
+ req, '5')
diff --git a/nova/tests/unit/api/openstack/compute/extensions/__init__.py b/nova/tests/unit/api/openstack/compute/extensions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/extensions/__init__.py
diff --git a/nova/tests/unit/api/openstack/compute/extensions/foxinsocks.py b/nova/tests/unit/api/openstack/compute/extensions/foxinsocks.py
new file mode 100644
index 0000000000..7d1e273ea7
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/extensions/foxinsocks.py
@@ -0,0 +1,92 @@
+# Copyright 2011 OpenStack Foundation
+# 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.exc
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+
+
+class FoxInSocksController(object):
+
+ def index(self, req):
+ return "Try to say this Mr. Knox, sir..."
+
+
+class FoxInSocksServerControllerExtension(wsgi.Controller):
+ @wsgi.action('add_tweedle')
+ def _add_tweedle(self, req, id, body):
+
+ return "Tweedle Beetle Added."
+
+ @wsgi.action('delete_tweedle')
+ def _delete_tweedle(self, req, id, body):
+
+ return "Tweedle Beetle Deleted."
+
+ @wsgi.action('fail')
+ def _fail(self, req, id, body):
+
+ raise webob.exc.HTTPBadRequest(explanation='Tweedle fail')
+
+
+class FoxInSocksFlavorGooseControllerExtension(wsgi.Controller):
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ # NOTE: This only handles JSON responses.
+ # You can use content type header to test for XML.
+ resp_obj.obj['flavor']['googoose'] = req.GET.get('chewing')
+
+
+class FoxInSocksFlavorBandsControllerExtension(wsgi.Controller):
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ # NOTE: This only handles JSON responses.
+ # You can use content type header to test for XML.
+ resp_obj.obj['big_bands'] = 'Pig Bands!'
+
+
+class Foxinsocks(extensions.ExtensionDescriptor):
+ """The Fox In Socks Extension."""
+
+ name = "Fox In Socks"
+ alias = "FOXNSOX"
+ namespace = "http://www.fox.in.socks/api/ext/pie/v1.0"
+ updated = "2011-01-22T13:25:27-06:00"
+
+ def __init__(self, ext_mgr):
+ ext_mgr.register(self)
+
+ def get_resources(self):
+ resources = []
+ resource = extensions.ResourceExtension('foxnsocks',
+ FoxInSocksController())
+ resources.append(resource)
+ return resources
+
+ def get_controller_extensions(self):
+ extension_list = []
+
+ extension_set = [
+ (FoxInSocksServerControllerExtension, 'servers'),
+ (FoxInSocksFlavorGooseControllerExtension, 'flavors'),
+ (FoxInSocksFlavorBandsControllerExtension, 'flavors'),
+ ]
+ for klass, collection in extension_set:
+ controller = klass()
+ ext = extensions.ControllerExtension(self, collection, controller)
+ extension_list.append(ext)
+
+ return extension_list
diff --git a/nova/tests/unit/api/openstack/compute/plugins/__init__.py b/nova/tests/unit/api/openstack/compute/plugins/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/__init__.py
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/__init__.py b/nova/tests/unit/api/openstack/compute/plugins/v3/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/__init__.py
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/admin_only_action_common.py b/nova/tests/unit/api/openstack/compute/plugins/v3/admin_only_action_common.py
new file mode 100644
index 0000000000..ce99d1069b
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/admin_only_action_common.py
@@ -0,0 +1,263 @@
+# Copyright 2013 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+import webob
+
+from nova.compute import vm_states
+import nova.context
+from nova import exception
+from nova.openstack.common import uuidutils
+from nova import test
+from nova.tests.unit import fake_instance
+
+
+class CommonMixin(object):
+ def setUp(self):
+ super(CommonMixin, self).setUp()
+ self.compute_api = None
+ self.context = nova.context.RequestContext('fake', 'fake')
+
+ def _make_request(self, url, body):
+ req = webob.Request.blank('/v2/fake' + url)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.content_type = 'application/json'
+ return req.get_response(self.app)
+
+ def _stub_instance_get(self, uuid=None):
+ if uuid is None:
+ uuid = uuidutils.generate_uuid()
+ instance = fake_instance.fake_instance_obj(self.context,
+ id=1, uuid=uuid, vm_state=vm_states.ACTIVE,
+ task_state=None, launched_at=timeutils.utcnow())
+ self.compute_api.get(self.context, uuid, expected_attrs=None,
+ want_objects=True).AndReturn(instance)
+ return instance
+
+ def _stub_instance_get_failure(self, exc_info, uuid=None):
+ if uuid is None:
+ uuid = uuidutils.generate_uuid()
+ self.compute_api.get(self.context, uuid, expected_attrs=None,
+ want_objects=True).AndRaise(exc_info)
+ return uuid
+
+ def _test_non_existing_instance(self, action, body_map=None):
+ uuid = uuidutils.generate_uuid()
+ self._stub_instance_get_failure(
+ exception.InstanceNotFound(instance_id=uuid), uuid=uuid)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % uuid,
+ {action: body_map.get(action)})
+ self.assertEqual(404, res.status_int)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def _test_action(self, action, body=None, method=None,
+ compute_api_args_map=None):
+ if method is None:
+ method = action
+
+ compute_api_args_map = compute_api_args_map or {}
+
+ instance = self._stub_instance_get()
+
+ args, kwargs = compute_api_args_map.get(action, ((), {}))
+ getattr(self.compute_api, method)(self.context, instance, *args,
+ **kwargs)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance.uuid,
+ {action: body})
+ self.assertEqual(202, res.status_int)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def _test_not_implemented_state(self, action, method=None):
+ if method is None:
+ method = action
+
+ instance = self._stub_instance_get()
+ body = {}
+ compute_api_args_map = {}
+ args, kwargs = compute_api_args_map.get(action, ((), {}))
+ getattr(self.compute_api, method)(self.context, instance,
+ *args, **kwargs).AndRaise(
+ NotImplementedError())
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance.uuid,
+ {action: body})
+ self.assertEqual(501, res.status_int)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def _test_invalid_state(self, action, method=None, body_map=None,
+ compute_api_args_map=None):
+ if method is None:
+ method = action
+ if body_map is None:
+ body_map = {}
+ if compute_api_args_map is None:
+ compute_api_args_map = {}
+
+ instance = self._stub_instance_get()
+
+ args, kwargs = compute_api_args_map.get(action, ((), {}))
+
+ getattr(self.compute_api, method)(self.context, instance,
+ *args, **kwargs).AndRaise(
+ exception.InstanceInvalidState(
+ attr='vm_state', instance_uuid=instance.uuid,
+ state='foo', method=method))
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance.uuid,
+ {action: body_map.get(action)})
+ self.assertEqual(409, res.status_int)
+ self.assertIn("Cannot \'%(action)s\' instance %(id)s"
+ % {'action': action, 'id': instance.uuid}, res.body)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def _test_locked_instance(self, action, method=None, body=None,
+ compute_api_args_map=None):
+ if method is None:
+ method = action
+
+ compute_api_args_map = compute_api_args_map or {}
+ instance = self._stub_instance_get()
+
+ args, kwargs = compute_api_args_map.get(action, ((), {}))
+ getattr(self.compute_api, method)(self.context, instance, *args,
+ **kwargs).AndRaise(
+ exception.InstanceIsLocked(instance_uuid=instance.uuid))
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance.uuid,
+ {action: body})
+ self.assertEqual(409, res.status_int)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def _test_instance_not_found_in_compute_api(self, action,
+ method=None, body=None, compute_api_args_map=None):
+ if method is None:
+ method = action
+
+ compute_api_args_map = compute_api_args_map or {}
+
+ instance = self._stub_instance_get()
+
+ args, kwargs = compute_api_args_map.get(action, ((), {}))
+ getattr(self.compute_api, method)(self.context, instance, *args,
+ **kwargs).AndRaise(
+ exception.InstanceNotFound(instance_id=instance.uuid))
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance.uuid,
+ {action: body})
+ self.assertEqual(404, res.status_int)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+
+class CommonTests(CommonMixin, test.NoDBTestCase):
+ def _test_actions(self, actions, method_translations=None, body_map=None,
+ args_map=None):
+ method_translations = method_translations or {}
+ body_map = body_map or {}
+ args_map = args_map or {}
+ for action in actions:
+ method = method_translations.get(action)
+ body = body_map.get(action)
+ self.mox.StubOutWithMock(self.compute_api, method or action)
+ self._test_action(action, method=method, body=body,
+ compute_api_args_map=args_map)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def _test_actions_instance_not_found_in_compute_api(self,
+ actions, method_translations=None, body_map=None,
+ args_map=None):
+ method_translations = method_translations or {}
+ body_map = body_map or {}
+ args_map = args_map or {}
+ for action in actions:
+ method = method_translations.get(action)
+ body = body_map.get(action)
+ self.mox.StubOutWithMock(self.compute_api, method or action)
+ self._test_instance_not_found_in_compute_api(
+ action, method=method, body=body,
+ compute_api_args_map=args_map)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def _test_actions_with_non_existed_instance(self, actions, body_map=None):
+ body_map = body_map or {}
+ for action in actions:
+ self._test_non_existing_instance(action,
+ body_map=body_map)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def _test_actions_raise_conflict_on_invalid_state(
+ self, actions, method_translations=None, body_map=None,
+ args_map=None):
+ method_translations = method_translations or {}
+ body_map = body_map or {}
+ args_map = args_map or {}
+ for action in actions:
+ method = method_translations.get(action)
+ self.mox.StubOutWithMock(self.compute_api, method or action)
+ self._test_invalid_state(action, method=method,
+ body_map=body_map,
+ compute_api_args_map=args_map)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def _test_actions_with_locked_instance(self, actions,
+ method_translations=None,
+ body_map=None, args_map=None):
+ method_translations = method_translations or {}
+ body_map = body_map or {}
+ args_map = args_map or {}
+ for action in actions:
+ method = method_translations.get(action)
+ body = body_map.get(action)
+ self.mox.StubOutWithMock(self.compute_api, method or action)
+ self._test_locked_instance(action, method=method, body=body,
+ compute_api_args_map=args_map)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_access_ips.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_access_ips.py
new file mode 100644
index 0000000000..44c1d5b5cd
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_access_ips.py
@@ -0,0 +1,383 @@
+# Copyright 2013 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.serialization import jsonutils
+
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import access_ips
+from nova.api.openstack.compute.plugins.v3 import servers
+from nova.api.openstack import wsgi
+from nova.compute import api as compute_api
+from nova import db
+from nova import exception
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.image import fake
+
+
+class AccessIPsExtTest(test.NoDBTestCase):
+ def setUp(self):
+ super(AccessIPsExtTest, self).setUp()
+ self.access_ips_ext = access_ips.AccessIPs(None)
+
+ def _test(self, func):
+ server_dict = {access_ips.AccessIPs.v4_key: '1.1.1.1',
+ access_ips.AccessIPs.v6_key: 'fe80::'}
+ create_kwargs = {}
+ func(server_dict, create_kwargs)
+ self.assertEqual(create_kwargs, {'access_ip_v4': '1.1.1.1',
+ 'access_ip_v6': 'fe80::'})
+
+ def _test_with_ipv4_only(self, func):
+ server_dict = {access_ips.AccessIPs.v4_key: '1.1.1.1'}
+ create_kwargs = {}
+ func(server_dict, create_kwargs)
+ self.assertEqual(create_kwargs, {'access_ip_v4': '1.1.1.1'})
+
+ def _test_with_ipv6_only(self, func):
+ server_dict = {access_ips.AccessIPs.v6_key: 'fe80::'}
+ create_kwargs = {}
+ func(server_dict, create_kwargs)
+ self.assertEqual(create_kwargs, {'access_ip_v6': 'fe80::'})
+
+ def _test_without_ipv4_and_ipv6(self, func):
+ server_dict = {}
+ create_kwargs = {}
+ func(server_dict, create_kwargs)
+ self.assertEqual(create_kwargs, {})
+
+ def _test_with_ipv4_null(self, func):
+ server_dict = {access_ips.AccessIPs.v4_key: None}
+ create_kwargs = {}
+ func(server_dict, create_kwargs)
+ self.assertEqual(create_kwargs, {'access_ip_v4': None})
+
+ def _test_with_ipv6_null(self, func):
+ server_dict = {access_ips.AccessIPs.v6_key: None}
+ create_kwargs = {}
+ func(server_dict, create_kwargs)
+ self.assertEqual(create_kwargs, {'access_ip_v6': None})
+
+ def _test_with_ipv4_blank(self, func):
+ server_dict = {access_ips.AccessIPs.v4_key: ''}
+ create_kwargs = {}
+ func(server_dict, create_kwargs)
+ self.assertEqual(create_kwargs, {'access_ip_v4': None})
+
+ def _test_with_ipv6_blank(self, func):
+ server_dict = {access_ips.AccessIPs.v6_key: ''}
+ create_kwargs = {}
+ func(server_dict, create_kwargs)
+ self.assertEqual(create_kwargs, {'access_ip_v6': None})
+
+ def test_server_create(self):
+ self._test(self.access_ips_ext.server_create)
+
+ def test_server_create_with_ipv4_only(self):
+ self._test_with_ipv4_only(self.access_ips_ext.server_create)
+
+ def test_server_create_with_ipv6_only(self):
+ self._test_with_ipv6_only(self.access_ips_ext.server_create)
+
+ def test_server_create_without_ipv4_and_ipv6(self):
+ self._test_without_ipv4_and_ipv6(self.access_ips_ext.server_create)
+
+ def test_server_create_with_ipv4_null(self):
+ self._test_with_ipv4_null(self.access_ips_ext.server_create)
+
+ def test_server_create_with_ipv6_null(self):
+ self._test_with_ipv6_null(self.access_ips_ext.server_create)
+
+ def test_server_create_with_ipv4_blank(self):
+ self._test_with_ipv4_blank(self.access_ips_ext.server_create)
+
+ def test_server_create_with_ipv6_blank(self):
+ self._test_with_ipv6_blank(self.access_ips_ext.server_create)
+
+ def test_server_update(self):
+ self._test(self.access_ips_ext.server_update)
+
+ def test_server_update_with_ipv4_only(self):
+ self._test_with_ipv4_only(self.access_ips_ext.server_update)
+
+ def test_server_update_with_ipv6_only(self):
+ self._test_with_ipv6_only(self.access_ips_ext.server_update)
+
+ def test_server_update_without_ipv4_and_ipv6(self):
+ self._test_without_ipv4_and_ipv6(self.access_ips_ext.server_update)
+
+ def test_server_update_with_ipv4_null(self):
+ self._test_with_ipv4_null(self.access_ips_ext.server_update)
+
+ def test_server_update_with_ipv6_null(self):
+ self._test_with_ipv6_null(self.access_ips_ext.server_update)
+
+ def test_server_update_with_ipv4_blank(self):
+ self._test_with_ipv4_blank(self.access_ips_ext.server_update)
+
+ def test_server_update_with_ipv6_blank(self):
+ self._test_with_ipv6_blank(self.access_ips_ext.server_update)
+
+ def test_server_rebuild(self):
+ self._test(self.access_ips_ext.server_rebuild)
+
+ def test_server_rebuild_with_ipv4_only(self):
+ self._test_with_ipv4_only(self.access_ips_ext.server_rebuild)
+
+ def test_server_rebuild_with_ipv6_only(self):
+ self._test_with_ipv6_only(self.access_ips_ext.server_rebuild)
+
+ def test_server_rebuild_without_ipv4_and_ipv6(self):
+ self._test_without_ipv4_and_ipv6(self.access_ips_ext.server_rebuild)
+
+ def test_server_rebuild_with_ipv4_null(self):
+ self._test_with_ipv4_null(self.access_ips_ext.server_rebuild)
+
+ def test_server_rebuild_with_ipv6_null(self):
+ self._test_with_ipv6_null(self.access_ips_ext.server_rebuild)
+
+ def test_server_rebuild_with_ipv4_blank(self):
+ self._test_with_ipv4_blank(self.access_ips_ext.server_rebuild)
+
+ def test_server_rebuild_with_ipv6_blank(self):
+ self._test_with_ipv6_blank(self.access_ips_ext.server_rebuild)
+
+
+class AccessIPsExtAPIValidationTest(test.TestCase):
+ def setUp(self):
+ super(AccessIPsExtAPIValidationTest, self).setUp()
+
+ def fake_save(context, **kwargs):
+ pass
+
+ def fake_rebuild(*args, **kwargs):
+ pass
+
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+ fake.stub_out_image_service(self.stubs)
+ self.stubs.Set(db, 'instance_get_by_uuid', fakes.fake_instance_get())
+ self.stubs.Set(instance_obj.Instance, 'save', fake_save)
+ self.stubs.Set(compute_api.API, 'rebuild', fake_rebuild)
+
+ def _test_create(self, params):
+ body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ 'flavorRef': 'http://localhost/123/flavors/3',
+ },
+ }
+ body['server'].update(params)
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.headers['content-type'] = 'application/json'
+ req.body = jsonutils.dumps(body)
+ self.controller.create(req, body=body)
+
+ def _test_update(self, params):
+ body = {
+ 'server': {
+ },
+ }
+ body['server'].update(params)
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'PUT'
+ req.headers['content-type'] = 'application/json'
+ req.body = jsonutils.dumps(body)
+ self.controller.update(req, fakes.FAKE_UUID, body=body)
+
+ def _test_rebuild(self, params):
+ body = {
+ 'rebuild': {
+ 'imageRef': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ },
+ }
+ body['rebuild'].update(params)
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'PUT'
+ req.headers['content-type'] = 'application/json'
+ req.body = jsonutils.dumps(body)
+ self.controller._action_rebuild(req, fakes.FAKE_UUID, body=body)
+
+ def test_create_server_with_access_ipv4(self):
+ params = {access_ips.AccessIPs.v4_key: '192.168.0.10'}
+ self._test_create(params)
+
+ def test_create_server_with_invalid_access_ipv4(self):
+ params = {access_ips.AccessIPs.v4_key: '1.1.1.1.1.1'}
+ self.assertRaises(exception.ValidationError, self._test_create, params)
+
+ def test_create_server_with_access_ipv6(self):
+ params = {access_ips.AccessIPs.v6_key: '2001:db8::9abc'}
+ self._test_create(params)
+
+ def test_create_server_with_invalid_access_ipv6(self):
+ params = {access_ips.AccessIPs.v6_key: 'fe80:::::::'}
+ self.assertRaises(exception.ValidationError, self._test_create, params)
+
+ def test_update_server_with_access_ipv4(self):
+ params = {access_ips.AccessIPs.v4_key: '192.168.0.10'}
+ self._test_update(params)
+
+ def test_update_server_with_invalid_access_ipv4(self):
+ params = {access_ips.AccessIPs.v4_key: '1.1.1.1.1.1'}
+ self.assertRaises(exception.ValidationError, self._test_update, params)
+
+ def test_update_server_with_access_ipv6(self):
+ params = {access_ips.AccessIPs.v6_key: '2001:db8::9abc'}
+ self._test_update(params)
+
+ def test_update_server_with_invalid_access_ipv6(self):
+ params = {access_ips.AccessIPs.v6_key: 'fe80:::::::'}
+ self.assertRaises(exception.ValidationError, self._test_update, params)
+
+ def test_rebuild_server_with_access_ipv4(self):
+ params = {access_ips.AccessIPs.v4_key: '192.168.0.10'}
+ self._test_rebuild(params)
+
+ def test_rebuild_server_with_invalid_access_ipv4(self):
+ params = {access_ips.AccessIPs.v4_key: '1.1.1.1.1.1'}
+ self.assertRaises(exception.ValidationError, self._test_rebuild,
+ params)
+
+ def test_rebuild_server_with_access_ipv6(self):
+ params = {access_ips.AccessIPs.v6_key: '2001:db8::9abc'}
+ self._test_rebuild(params)
+
+ def test_rebuild_server_with_invalid_access_ipv6(self):
+ params = {access_ips.AccessIPs.v6_key: 'fe80:::::::'}
+ self.assertRaises(exception.ValidationError, self._test_rebuild,
+ params)
+
+
+class AccessIPsControllerTest(test.NoDBTestCase):
+ def setUp(self):
+ super(AccessIPsControllerTest, self).setUp()
+ self.controller = access_ips.AccessIPsController()
+
+ def _test_with_access_ips(self, func, kwargs={'id': 'fake'}):
+ req = wsgi.Request({'nova.context':
+ fakes.FakeRequestContext('fake_user', 'fake',
+ is_admin=True)})
+ instance = {'uuid': 'fake',
+ 'access_ip_v4': '1.1.1.1',
+ 'access_ip_v6': 'fe80::'}
+ req.cache_db_instance(instance)
+ resp_obj = wsgi.ResponseObject(
+ {"server": {'id': 'fake'}})
+ func(req, resp_obj, **kwargs)
+ self.assertEqual(resp_obj.obj['server'][access_ips.AccessIPs.v4_key],
+ '1.1.1.1')
+ self.assertEqual(resp_obj.obj['server'][access_ips.AccessIPs.v6_key],
+ 'fe80::')
+
+ def _test_without_access_ips(self, func, kwargs={'id': 'fake'}):
+ req = wsgi.Request({'nova.context':
+ fakes.FakeRequestContext('fake_user', 'fake',
+ is_admin=True)})
+ instance = {'uuid': 'fake',
+ 'access_ip_v4': None,
+ 'access_ip_v6': None}
+ req.cache_db_instance(instance)
+ resp_obj = wsgi.ResponseObject(
+ {"server": {'id': 'fake'}})
+ func(req, resp_obj, **kwargs)
+ self.assertEqual(resp_obj.obj['server'][access_ips.AccessIPs.v4_key],
+ '')
+ self.assertEqual(resp_obj.obj['server'][access_ips.AccessIPs.v6_key],
+ '')
+
+ def test_create(self):
+ self._test_with_access_ips(self.controller.create, {'body': {}})
+
+ def test_create_without_access_ips(self):
+ self._test_with_access_ips(self.controller.create, {'body': {}})
+
+ def test_show(self):
+ self._test_with_access_ips(self.controller.show)
+
+ def test_show_without_access_ips(self):
+ self._test_without_access_ips(self.controller.show)
+
+ def test_detail(self):
+ req = wsgi.Request({'nova.context':
+ fakes.FakeRequestContext('fake_user', 'fake',
+ is_admin=True)})
+ instance1 = {'uuid': 'fake1',
+ 'access_ip_v4': '1.1.1.1',
+ 'access_ip_v6': 'fe80::'}
+ instance2 = {'uuid': 'fake2',
+ 'access_ip_v4': '1.1.1.2',
+ 'access_ip_v6': 'fe81::'}
+ req.cache_db_instance(instance1)
+ req.cache_db_instance(instance2)
+ resp_obj = wsgi.ResponseObject(
+ {"servers": [{'id': 'fake1'}, {'id': 'fake2'}]})
+ self.controller.detail(req, resp_obj)
+ self.assertEqual(
+ resp_obj.obj['servers'][0][access_ips.AccessIPs.v4_key],
+ '1.1.1.1')
+ self.assertEqual(
+ resp_obj.obj['servers'][0][access_ips.AccessIPs.v6_key],
+ 'fe80::')
+ self.assertEqual(
+ resp_obj.obj['servers'][1][access_ips.AccessIPs.v4_key],
+ '1.1.1.2')
+ self.assertEqual(
+ resp_obj.obj['servers'][1][access_ips.AccessIPs.v6_key],
+ 'fe81::')
+
+ def test_detail_without_access_ips(self):
+ req = wsgi.Request({'nova.context':
+ fakes.FakeRequestContext('fake_user', 'fake',
+ is_admin=True)})
+ instance1 = {'uuid': 'fake1',
+ 'access_ip_v4': None,
+ 'access_ip_v6': None}
+ instance2 = {'uuid': 'fake2',
+ 'access_ip_v4': None,
+ 'access_ip_v6': None}
+ req.cache_db_instance(instance1)
+ req.cache_db_instance(instance2)
+ resp_obj = wsgi.ResponseObject(
+ {"servers": [{'id': 'fake1'}, {'id': 'fake2'}]})
+ self.controller.detail(req, resp_obj)
+ self.assertEqual(
+ resp_obj.obj['servers'][0][access_ips.AccessIPs.v4_key], '')
+ self.assertEqual(
+ resp_obj.obj['servers'][0][access_ips.AccessIPs.v6_key], '')
+ self.assertEqual(
+ resp_obj.obj['servers'][1][access_ips.AccessIPs.v4_key], '')
+ self.assertEqual(
+ resp_obj.obj['servers'][1][access_ips.AccessIPs.v6_key], '')
+
+ def test_update(self):
+ self._test_with_access_ips(self.controller.update, {'id': 'fake',
+ 'body': {}})
+
+ def test_update_without_access_ips(self):
+ self._test_without_access_ips(self.controller.update, {'id': 'fake',
+ 'body': {}})
+
+ def test_rebuild(self):
+ self._test_with_access_ips(self.controller.rebuild, {'id': 'fake',
+ 'body': {}})
+
+ def test_rebuild_without_access_ips(self):
+ self._test_without_access_ips(self.controller.rebuild, {'id': 'fake',
+ 'body': {}})
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_console_auth_tokens.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_console_auth_tokens.py
new file mode 100644
index 0000000000..259906c535
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_console_auth_tokens.py
@@ -0,0 +1,95 @@
+# Copyright 2013 Cloudbase Solutions Srl
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.serialization import jsonutils
+
+from nova.consoleauth import rpcapi as consoleauth_rpcapi
+from nova import context
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+_FAKE_CONNECT_INFO = {'instance_uuid': 'fake_instance_uuid',
+ 'host': 'fake_host',
+ 'port': 'fake_port',
+ 'internal_access_path': 'fake_access_path',
+ 'console_type': 'rdp-html5'}
+
+
+def _fake_check_token(self, context, token):
+ return _FAKE_CONNECT_INFO
+
+
+def _fake_check_token_not_found(self, context, token):
+ return None
+
+
+def _fake_check_token_unauthorized(self, context, token):
+ connect_info = _FAKE_CONNECT_INFO
+ connect_info['console_type'] = 'unauthorized_console_type'
+ return connect_info
+
+
+class ConsoleAuthTokensExtensionTest(test.TestCase):
+
+ _FAKE_URL = '/v2/fake/os-console-auth-tokens/1'
+
+ _EXPECTED_OUTPUT = {'console': {'instance_uuid': 'fake_instance_uuid',
+ 'host': 'fake_host',
+ 'port': 'fake_port',
+ 'internal_access_path':
+ 'fake_access_path'}}
+
+ def setUp(self):
+ super(ConsoleAuthTokensExtensionTest, self).setUp()
+ self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token',
+ _fake_check_token)
+
+ ctxt = self._get_admin_context()
+ self.app = fakes.wsgi_app_v21(init_only=('os-console-auth-tokens'),
+ fake_auth_context=ctxt)
+
+ def _get_admin_context(self):
+ ctxt = context.get_admin_context()
+ ctxt.user_id = 'fake'
+ ctxt.project_id = 'fake'
+ return ctxt
+
+ def _create_request(self):
+ req = fakes.HTTPRequestV3.blank(self._FAKE_URL)
+ req.method = "GET"
+ req.headers["content-type"] = "application/json"
+ return req
+
+ def test_get_console_connect_info(self):
+ req = self._create_request()
+ res = req.get_response(self.app)
+ self.assertEqual(200, res.status_int)
+ output = jsonutils.loads(res.body)
+ self.assertEqual(self._EXPECTED_OUTPUT, output)
+
+ def test_get_console_connect_info_token_not_found(self):
+ self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token',
+ _fake_check_token_not_found)
+ req = self._create_request()
+ res = req.get_response(self.app)
+ self.assertEqual(404, res.status_int)
+
+ def test_get_console_connect_info_unauthorized_console_type(self):
+ self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token',
+ _fake_check_token_unauthorized)
+ req = self._create_request()
+ res = req.get_response(self.app)
+ self.assertEqual(401, res.status_int)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_consoles.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_consoles.py
new file mode 100644
index 0000000000..d3ba83dcbc
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_consoles.py
@@ -0,0 +1,270 @@
+# Copyright 2010-2011 OpenStack Foundation
+# Copyright 2011 Piston Cloud Computing, Inc.
+# 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 datetime
+import uuid as stdlib_uuid
+
+from oslo.utils import timeutils
+import webob
+
+from nova.api.openstack.compute.plugins.v3 import consoles
+from nova.compute import vm_states
+from nova import console
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import matchers
+
+
+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+
+
+class FakeInstanceDB(object):
+
+ def __init__(self):
+ self.instances_by_id = {}
+ self.ids_by_uuid = {}
+ self.max_id = 0
+
+ def return_server_by_id(self, context, id):
+ if id not in self.instances_by_id:
+ self._add_server(id=id)
+ return dict(self.instances_by_id[id])
+
+ def return_server_by_uuid(self, context, uuid):
+ if uuid not in self.ids_by_uuid:
+ self._add_server(uuid=uuid)
+ return dict(self.instances_by_id[self.ids_by_uuid[uuid]])
+
+ def _add_server(self, id=None, uuid=None):
+ if id is None:
+ id = self.max_id + 1
+ if uuid is None:
+ uuid = str(stdlib_uuid.uuid4())
+ instance = stub_instance(id, uuid=uuid)
+ self.instances_by_id[id] = instance
+ self.ids_by_uuid[uuid] = id
+ if id > self.max_id:
+ self.max_id = id
+
+
+def stub_instance(id, user_id='fake', project_id='fake', host=None,
+ vm_state=None, task_state=None,
+ reservation_id="", uuid=FAKE_UUID, image_ref="10",
+ flavor_id="1", name=None, key_name='',
+ access_ipv4=None, access_ipv6=None, progress=0):
+
+ if host is not None:
+ host = str(host)
+
+ if key_name:
+ key_data = 'FAKE'
+ else:
+ key_data = ''
+
+ # ReservationID isn't sent back, hack it in there.
+ server_name = name or "server%s" % id
+ if reservation_id != "":
+ server_name = "reservation_%s" % (reservation_id, )
+
+ instance = {
+ "id": int(id),
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "admin_password": "",
+ "user_id": user_id,
+ "project_id": project_id,
+ "image_ref": image_ref,
+ "kernel_id": "",
+ "ramdisk_id": "",
+ "launch_index": 0,
+ "key_name": key_name,
+ "key_data": key_data,
+ "vm_state": vm_state or vm_states.BUILDING,
+ "task_state": task_state,
+ "memory_mb": 0,
+ "vcpus": 0,
+ "root_gb": 0,
+ "hostname": "",
+ "host": host,
+ "instance_type": {},
+ "user_data": "",
+ "reservation_id": reservation_id,
+ "mac_address": "",
+ "scheduled_at": timeutils.utcnow(),
+ "launched_at": timeutils.utcnow(),
+ "terminated_at": timeutils.utcnow(),
+ "availability_zone": "",
+ "display_name": server_name,
+ "display_description": "",
+ "locked": False,
+ "metadata": [],
+ "access_ip_v4": access_ipv4,
+ "access_ip_v6": access_ipv6,
+ "uuid": uuid,
+ "progress": progress}
+
+ return instance
+
+
+class ConsolesControllerTest(test.NoDBTestCase):
+ def setUp(self):
+ super(ConsolesControllerTest, self).setUp()
+ self.flags(verbose=True)
+ self.instance_db = FakeInstanceDB()
+ self.stubs.Set(db, 'instance_get',
+ self.instance_db.return_server_by_id)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ self.instance_db.return_server_by_uuid)
+ self.uuid = str(stdlib_uuid.uuid4())
+ self.url = '/v3/fake/servers/%s/consoles' % self.uuid
+ self.controller = consoles.ConsolesController()
+
+ def test_create_console(self):
+ def fake_create_console(cons_self, context, instance_id):
+ self.assertEqual(instance_id, self.uuid)
+ return {}
+ self.stubs.Set(console.api.API, 'create_console', fake_create_console)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.controller.create(req, self.uuid, None)
+ self.assertEqual(self.controller.create.wsgi_code, 201)
+
+ def test_create_console_unknown_instance(self):
+ def fake_create_console(cons_self, context, instance_id):
+ raise exception.InstanceNotFound(instance_id=instance_id)
+ self.stubs.Set(console.api.API, 'create_console', fake_create_console)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.create,
+ req, self.uuid, None)
+
+ def test_show_console(self):
+ def fake_get_console(cons_self, context, instance_id, console_id):
+ self.assertEqual(instance_id, self.uuid)
+ self.assertEqual(console_id, 20)
+ pool = dict(console_type='fake_type',
+ public_hostname='fake_hostname')
+ return dict(id=console_id, password='fake_password',
+ port='fake_port', pool=pool, instance_name='inst-0001')
+
+ expected = {'console': {'id': 20,
+ 'port': 'fake_port',
+ 'host': 'fake_hostname',
+ 'password': 'fake_password',
+ 'instance_name': 'inst-0001',
+ 'console_type': 'fake_type'}}
+
+ self.stubs.Set(console.api.API, 'get_console', fake_get_console)
+
+ req = fakes.HTTPRequestV3.blank(self.url + '/20')
+ res_dict = self.controller.show(req, self.uuid, '20')
+ self.assertThat(res_dict, matchers.DictMatches(expected))
+
+ def test_show_console_unknown_console(self):
+ def fake_get_console(cons_self, context, instance_id, console_id):
+ raise exception.ConsoleNotFound(console_id=console_id)
+
+ self.stubs.Set(console.api.API, 'get_console', fake_get_console)
+
+ req = fakes.HTTPRequestV3.blank(self.url + '/20')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
+ req, self.uuid, '20')
+
+ def test_show_console_unknown_instance(self):
+ def fake_get_console(cons_self, context, instance_id, console_id):
+ raise exception.ConsoleNotFoundForInstance(
+ instance_uuid=instance_id)
+
+ self.stubs.Set(console.api.API, 'get_console', fake_get_console)
+
+ req = fakes.HTTPRequestV3.blank(self.url + '/20')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
+ req, self.uuid, '20')
+
+ def test_list_consoles(self):
+ def fake_get_consoles(cons_self, context, instance_id):
+ self.assertEqual(instance_id, self.uuid)
+
+ pool1 = dict(console_type='fake_type',
+ public_hostname='fake_hostname')
+ cons1 = dict(id=10, password='fake_password',
+ port='fake_port', pool=pool1)
+ pool2 = dict(console_type='fake_type2',
+ public_hostname='fake_hostname2')
+ cons2 = dict(id=11, password='fake_password2',
+ port='fake_port2', pool=pool2)
+ return [cons1, cons2]
+
+ expected = {'consoles':
+ [{'id': 10, 'console_type': 'fake_type'},
+ {'id': 11, 'console_type': 'fake_type2'}]}
+
+ self.stubs.Set(console.api.API, 'get_consoles', fake_get_consoles)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ res_dict = self.controller.index(req, self.uuid)
+ self.assertThat(res_dict, matchers.DictMatches(expected))
+
+ def test_list_consoles_unknown_instance(self):
+ def fake_get_consoles(cons_self, context, instance_id):
+ raise exception.InstanceNotFound(instance_id=instance_id)
+ self.stubs.Set(console.api.API, 'get_consoles', fake_get_consoles)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.index,
+ req, self.uuid)
+
+ def test_delete_console(self):
+ def fake_get_console(cons_self, context, instance_id, console_id):
+ self.assertEqual(instance_id, self.uuid)
+ self.assertEqual(console_id, 20)
+ pool = dict(console_type='fake_type',
+ public_hostname='fake_hostname')
+ return dict(id=console_id, password='fake_password',
+ port='fake_port', pool=pool)
+
+ def fake_delete_console(cons_self, context, instance_id, console_id):
+ self.assertEqual(instance_id, self.uuid)
+ self.assertEqual(console_id, 20)
+
+ self.stubs.Set(console.api.API, 'get_console', fake_get_console)
+ self.stubs.Set(console.api.API, 'delete_console', fake_delete_console)
+
+ req = fakes.HTTPRequestV3.blank(self.url + '/20')
+ self.controller.delete(req, self.uuid, '20')
+
+ def test_delete_console_unknown_console(self):
+ def fake_delete_console(cons_self, context, instance_id, console_id):
+ raise exception.ConsoleNotFound(console_id=console_id)
+
+ self.stubs.Set(console.api.API, 'delete_console', fake_delete_console)
+
+ req = fakes.HTTPRequestV3.blank(self.url + '/20')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, self.uuid, '20')
+
+ def test_delete_console_unknown_instance(self):
+ def fake_delete_console(cons_self, context, instance_id, console_id):
+ raise exception.ConsoleNotFoundForInstance(
+ instance_uuid=instance_id)
+
+ self.stubs.Set(console.api.API, 'delete_console', fake_delete_console)
+
+ req = fakes.HTTPRequestV3.blank(self.url + '/20')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, self.uuid, '20')
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_create_backup.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_create_backup.py
new file mode 100644
index 0000000000..83701090f8
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_create_backup.py
@@ -0,0 +1,261 @@
+# Copyright 2011 OpenStack Foundation
+# Copyright 2013 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova.api.openstack import common
+from nova.api.openstack.compute.plugins.v3 import create_backup
+from nova.openstack.common import uuidutils
+from nova import test
+from nova.tests.unit.api.openstack.compute.plugins.v3 import \
+ admin_only_action_common
+from nova.tests.unit.api.openstack import fakes
+
+
+class CreateBackupTests(admin_only_action_common.CommonMixin,
+ test.NoDBTestCase):
+ def setUp(self):
+ super(CreateBackupTests, self).setUp()
+ self.controller = create_backup.CreateBackupController()
+ self.compute_api = self.controller.compute_api
+
+ def _fake_controller(*args, **kwargs):
+ return self.controller
+
+ self.stubs.Set(create_backup, 'CreateBackupController',
+ _fake_controller)
+ self.app = fakes.wsgi_app_v21(init_only=('servers',
+ 'os-create-backup'),
+ fake_auth_context=self.context)
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+ self.mox.StubOutWithMock(common,
+ 'check_img_metadata_properties_quota')
+ self.mox.StubOutWithMock(self.compute_api, 'backup')
+
+ def _make_url(self, uuid=None):
+ if uuid is None:
+ uuid = uuidutils.generate_uuid()
+ return '/servers/%s/action' % uuid
+
+ def test_create_backup_with_metadata(self):
+ metadata = {'123': 'asdf'}
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ 'metadata': metadata,
+ },
+ }
+
+ image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
+ properties=metadata)
+
+ common.check_img_metadata_properties_quota(self.context, metadata)
+ instance = self._stub_instance_get()
+ self.compute_api.backup(self.context, instance, 'Backup 1',
+ 'daily', 1,
+ extra_properties=metadata).AndReturn(image)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request(self._make_url(instance.uuid), body)
+ self.assertEqual(202, res.status_int)
+ self.assertIn('fake-image-id', res.headers['Location'])
+
+ def test_create_backup_no_name(self):
+ # Name is required for backups.
+ body = {
+ 'createBackup': {
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+ res = self._make_request(self._make_url(), body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_no_rotation(self):
+ # Rotation is required for backup requests.
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ },
+ }
+ res = self._make_request(self._make_url(), body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_negative_rotation(self):
+ """Rotation must be greater than or equal to zero
+ for backup requests
+ """
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': -1,
+ },
+ }
+ res = self._make_request(self._make_url(), body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_negative_rotation_with_string_number(self):
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': '-1',
+ },
+ }
+ res = self._make_request(self._make_url('fake'), body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_no_backup_type(self):
+ # Backup Type (daily or weekly) is required for backup requests.
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'rotation': 1,
+ },
+ }
+ res = self._make_request(self._make_url(), body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_non_dict_metadata(self):
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ 'metadata': 'non_dict',
+ },
+ }
+ res = self._make_request(self._make_url('fake'), body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_bad_entity(self):
+ body = {'createBackup': 'go'}
+ res = self._make_request(self._make_url(), body)
+ self.assertEqual(400, res.status_int)
+
+ def test_create_backup_rotation_is_zero(self):
+ # The happy path for creating backups if rotation is zero.
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 0,
+ },
+ }
+
+ image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
+ properties={})
+ common.check_img_metadata_properties_quota(self.context, {})
+ instance = self._stub_instance_get()
+ self.compute_api.backup(self.context, instance, 'Backup 1',
+ 'daily', 0,
+ extra_properties={}).AndReturn(image)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request(self._make_url(instance.uuid), body)
+ self.assertEqual(202, res.status_int)
+ self.assertNotIn('Location', res.headers)
+
+ def test_create_backup_rotation_is_positive(self):
+ # The happy path for creating backups if rotation is positive.
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+
+ image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
+ properties={})
+ common.check_img_metadata_properties_quota(self.context, {})
+ instance = self._stub_instance_get()
+ self.compute_api.backup(self.context, instance, 'Backup 1',
+ 'daily', 1,
+ extra_properties={}).AndReturn(image)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request(self._make_url(instance.uuid), body)
+ self.assertEqual(202, res.status_int)
+ self.assertIn('fake-image-id', res.headers['Location'])
+
+ def test_create_backup_rotation_is_string_number(self):
+ body = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': '1',
+ },
+ }
+
+ image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
+ properties={})
+ common.check_img_metadata_properties_quota(self.context, {})
+ instance = self._stub_instance_get()
+ self.compute_api.backup(self.context, instance, 'Backup 1',
+ 'daily', 1,
+ extra_properties={}).AndReturn(image)
+
+ self.mox.ReplayAll()
+
+ res = self._make_request(self._make_url(instance['uuid']), body)
+ self.assertEqual(202, res.status_int)
+ self.assertIn('fake-image-id', res.headers['Location'])
+
+ def test_create_backup_raises_conflict_on_invalid_state(self):
+ body_map = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+ args_map = {
+ 'createBackup': (
+ ('Backup 1', 'daily', 1), {'extra_properties': {}}
+ ),
+ }
+ common.check_img_metadata_properties_quota(self.context, {})
+ self._test_invalid_state('createBackup', method='backup',
+ body_map=body_map,
+ compute_api_args_map=args_map)
+
+ def test_create_backup_with_non_existed_instance(self):
+ body_map = {
+ 'createBackup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+ common.check_img_metadata_properties_quota(self.context, {})
+ self._test_non_existing_instance('createBackup',
+ body_map=body_map)
+
+ def test_create_backup_with_invalid_create_backup(self):
+ body = {
+ 'createBackupup': {
+ 'name': 'Backup 1',
+ 'backup_type': 'daily',
+ 'rotation': 1,
+ },
+ }
+ res = self._make_request(self._make_url(), body)
+ self.assertEqual(400, res.status_int)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_extended_volumes.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_extended_volumes.py
new file mode 100644
index 0000000000..dc6dd2898f
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_extended_volumes.py
@@ -0,0 +1,387 @@
+# Copyright 2013 OpenStack Foundation
+# 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 mock
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.plugins.v3 import extended_volumes
+from nova import compute
+from nova import context
+from nova import db
+from nova import exception
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_block_device
+from nova.tests.unit import fake_instance
+from nova import volume
+
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+UUID3 = '00000000-0000-0000-0000-000000000003'
+
+
+def fake_compute_get(*args, **kwargs):
+ inst = fakes.stub_instance(1, uuid=UUID1)
+ return fake_instance.fake_instance_obj(args[1], **inst)
+
+
+def fake_compute_get_not_found(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id=UUID1)
+
+
+def fake_compute_get_all(*args, **kwargs):
+ db_list = [fakes.stub_instance(1), fakes.stub_instance(2)]
+ fields = instance_obj.INSTANCE_DEFAULT_FIELDS
+ return instance_obj._make_instance_list(args[1],
+ objects.InstanceList(),
+ db_list, fields)
+
+
+def fake_bdms_get_all_by_instance(*args, **kwargs):
+ return [fake_block_device.FakeDbBlockDeviceDict(
+ {'volume_id': UUID1, 'source_type': 'volume',
+ 'destination_type': 'volume', 'id': 1}),
+ fake_block_device.FakeDbBlockDeviceDict(
+ {'volume_id': UUID2, 'source_type': 'volume',
+ 'destination_type': 'volume', 'id': 2})]
+
+
+def fake_attach_volume(self, context, instance, volume_id,
+ device, disk_bus, device_type):
+ pass
+
+
+def fake_attach_volume_not_found_vol(self, context, instance, volume_id,
+ device, disk_bus, device_type):
+ raise exception.VolumeNotFound(volume_id=volume_id)
+
+
+def fake_attach_volume_invalid_device_path(self, context, instance,
+ volume_id, device, disk_bus,
+ device_type):
+ raise exception.InvalidDevicePath(path=device)
+
+
+def fake_attach_volume_instance_invalid_state(self, context, instance,
+ volume_id, device, disk_bus,
+ device_type):
+ raise exception.InstanceInvalidState(instance_uuid=UUID1, state='',
+ method='', attr='')
+
+
+def fake_attach_volume_invalid_volume(self, context, instance,
+ volume_id, device, disk_bus,
+ device_type):
+ raise exception.InvalidVolume(reason='')
+
+
+def fake_detach_volume(self, context, instance, volume):
+ pass
+
+
+def fake_swap_volume(self, context, instance,
+ old_volume_id, new_volume_id):
+ pass
+
+
+def fake_swap_volume_invalid_volume(self, context, instance,
+ volume_id, device):
+ raise exception.InvalidVolume(reason='', volume_id=volume_id)
+
+
+def fake_swap_volume_unattached_volume(self, context, instance,
+ volume_id, device):
+ raise exception.VolumeUnattached(reason='', volume_id=volume_id)
+
+
+def fake_detach_volume_invalid_volume(self, context, instance, volume):
+ raise exception.InvalidVolume(reason='')
+
+
+def fake_swap_volume_instance_invalid_state(self, context, instance,
+ volume_id, device):
+ raise exception.InstanceInvalidState(instance_uuid=UUID1, state='',
+ method='', attr='')
+
+
+def fake_volume_get(*args, **kwargs):
+ pass
+
+
+def fake_volume_get_not_found(*args, **kwargs):
+ raise exception.VolumeNotFound(volume_id=UUID1)
+
+
+class ExtendedVolumesTest(test.TestCase):
+ content_type = 'application/json'
+ prefix = 'os-extended-volumes:'
+
+ def setUp(self):
+ super(ExtendedVolumesTest, self).setUp()
+ self.Controller = extended_volumes.ExtendedVolumesController()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get)
+ self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all)
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ fake_bdms_get_all_by_instance)
+ self.stubs.Set(volume.cinder.API, 'get', fake_volume_get)
+ self.stubs.Set(compute.api.API, 'detach_volume', fake_detach_volume)
+ self.stubs.Set(compute.api.API, 'attach_volume', fake_attach_volume)
+ self.app = fakes.wsgi_app_v21(init_only=('os-extended-volumes',
+ 'servers'))
+ return_server = fakes.fake_instance_get()
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ def _make_request(self, url, body=None):
+ base_url = '/v2/fake/servers'
+ req = webob.Request.blank(base_url + url)
+ req.headers['Accept'] = self.content_type
+ if body:
+ req.body = jsonutils.dumps(body)
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ res = req.get_response(self.app)
+ return res
+
+ def _get_server(self, body):
+ return jsonutils.loads(body).get('server')
+
+ def _get_servers(self, body):
+ return jsonutils.loads(body).get('servers')
+
+ def test_show(self):
+ url = '/%s' % UUID1
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ server = self._get_server(res.body)
+ exp_volumes = [{'id': UUID1}, {'id': UUID2}]
+ if self.content_type == 'application/json':
+ actual = server.get('%svolumes_attached' % self.prefix)
+ self.assertEqual(exp_volumes, actual)
+
+ def test_detail(self):
+ url = '/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ exp_volumes = [{'id': UUID1}, {'id': UUID2}]
+ for i, server in enumerate(self._get_servers(res.body)):
+ if self.content_type == 'application/json':
+ actual = server.get('%svolumes_attached' % self.prefix)
+ self.assertEqual(exp_volumes, actual)
+
+ def test_detach(self):
+ url = "/%s/action" % UUID1
+ res = self._make_request(url, {"detach": {"volume_id": UUID1}})
+ self.assertEqual(res.status_int, 202)
+
+ def test_detach_volume_from_locked_server(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'detach_volume',
+ fakes.fake_actions_to_locked_server)
+ res = self._make_request(url, {"detach": {"volume_id": UUID1}})
+ self.assertEqual(res.status_int, 409)
+
+ def test_detach_with_non_existed_vol(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(volume.cinder.API, 'get', fake_volume_get_not_found)
+ res = self._make_request(url, {"detach": {"volume_id": UUID2}})
+ self.assertEqual(res.status_int, 404)
+
+ def test_detach_with_non_existed_instance(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get_not_found)
+ res = self._make_request(url, {"detach": {"volume_id": UUID2}})
+ self.assertEqual(res.status_int, 404)
+
+ def test_detach_with_invalid_vol(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'detach_volume',
+ fake_detach_volume_invalid_volume)
+ res = self._make_request(url, {"detach": {"volume_id": UUID2}})
+ self.assertEqual(res.status_int, 400)
+
+ def test_detach_with_bad_id(self):
+ url = "/%s/action" % UUID1
+ res = self._make_request(url, {"detach": {"volume_id": 'xxx'}})
+ self.assertEqual(res.status_int, 400)
+
+ def test_detach_without_id(self):
+ url = "/%s/action" % UUID1
+ res = self._make_request(url, {"detach": {}})
+ self.assertEqual(res.status_int, 400)
+
+ def test_detach_volume_with_invalid_request(self):
+ url = "/%s/action" % UUID1
+ res = self._make_request(url, {"detach": None})
+ self.assertEqual(res.status_int, 400)
+
+ @mock.patch('nova.objects.BlockDeviceMapping.is_root',
+ new_callable=mock.PropertyMock)
+ def test_detach_volume_root(self, mock_isroot):
+ url = "/%s/action" % UUID1
+ mock_isroot.return_value = True
+ res = self._make_request(url, {"detach": {"volume_id": UUID1}})
+ self.assertEqual(res.status_int, 403)
+
+ def test_attach_volume(self):
+ url = "/%s/action" % UUID1
+ res = self._make_request(url, {"attach": {"volume_id": UUID1}})
+ self.assertEqual(res.status_int, 202)
+
+ def test_attach_volume_to_locked_server(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'attach_volume',
+ fakes.fake_actions_to_locked_server)
+ res = self._make_request(url, {"attach": {"volume_id": UUID1}})
+ self.assertEqual(res.status_int, 409)
+
+ def test_attach_volume_disk_bus_and_disk_dev(self):
+ url = "/%s/action" % UUID1
+ self._make_request(url, {"attach": {"volume_id": UUID1,
+ "device": "/dev/vdb",
+ "disk_bus": "ide",
+ "device_type": "cdrom"}})
+
+ def test_attach_volume_with_bad_id(self):
+ url = "/%s/action" % UUID1
+ res = self._make_request(url, {"attach": {"volume_id": 'xxx'}})
+ self.assertEqual(res.status_int, 400)
+
+ def test_attach_volume_without_id(self):
+ url = "/%s/action" % UUID1
+ res = self._make_request(url, {"attach": {}})
+ self.assertEqual(res.status_int, 400)
+
+ def test_attach_volume_with_invalid_request(self):
+ url = "/%s/action" % UUID1
+ res = self._make_request(url, {"attach": None})
+ self.assertEqual(res.status_int, 400)
+
+ def test_attach_volume_with_non_existe_vol(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'attach_volume',
+ fake_attach_volume_not_found_vol)
+ res = self._make_request(url, {"attach": {"volume_id": UUID1}})
+ self.assertEqual(res.status_int, 404)
+
+ def test_attach_volume_with_non_existed_instance(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get_not_found)
+ res = self._make_request(url, {"attach": {"volume_id": UUID1}})
+ self.assertEqual(res.status_int, 404)
+
+ def test_attach_volume_with_invalid_device_path(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'attach_volume',
+ fake_attach_volume_invalid_device_path)
+ res = self._make_request(url, {"attach": {"volume_id": UUID1,
+ 'device': 'xxx'}})
+ self.assertEqual(res.status_int, 400)
+
+ def test_attach_volume_with_instance_invalid_state(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'attach_volume',
+ fake_attach_volume_instance_invalid_state)
+ res = self._make_request(url, {"attach": {"volume_id": UUID1}})
+ self.assertEqual(res.status_int, 409)
+
+ def test_attach_volume_with_invalid_volume(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'attach_volume',
+ fake_attach_volume_invalid_volume)
+ res = self._make_request(url, {"attach": {"volume_id": UUID1}})
+ self.assertEqual(res.status_int, 400)
+
+ def test_attach_volume_with_invalid_request_body(self):
+ url = "/%s/action" % UUID1
+ self.stubs.Set(compute.api.API, 'attach_volume',
+ fake_attach_volume_invalid_volume)
+ res = self._make_request(url, {"attach": None})
+ self.assertEqual(res.status_int, 400)
+
+ def _test_swap(self, uuid=UUID1, body=None):
+ body = body or {'swap_volume_attachment': {'old_volume_id': uuid,
+ 'new_volume_id': UUID2}}
+ req = webob.Request.blank('/v2/fake/servers/%s/action' % UUID1)
+ req.method = 'PUT'
+ req.body = jsonutils.dumps({})
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = context.get_admin_context()
+ return self.Controller.swap(req, UUID1, body=body)
+
+ def test_swap_volume(self):
+ self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume)
+ # Check any exceptions don't happen and status code
+ self._test_swap()
+ self.assertEqual(202, self.Controller.swap.wsgi_code)
+
+ def test_swap_volume_for_locked_server(self):
+ def fake_swap_volume_for_locked_server(self, context, instance,
+ old_volume, new_volume):
+ raise exception.InstanceIsLocked(instance_uuid=instance['uuid'])
+ self.stubs.Set(compute.api.API, 'swap_volume',
+ fake_swap_volume_for_locked_server)
+ self.assertRaises(webob.exc.HTTPConflict, self._test_swap)
+
+ def test_swap_volume_for_locked_server_new(self):
+ self.stubs.Set(compute.api.API, 'swap_volume',
+ fakes.fake_actions_to_locked_server)
+ self.assertRaises(webob.exc.HTTPConflict, self._test_swap)
+
+ def test_swap_volume_instance_not_found(self):
+ self.stubs.Set(compute.api.API, 'get', fake_compute_get_not_found)
+ self.assertRaises(webob.exc.HTTPNotFound, self._test_swap)
+
+ def test_swap_volume_with_bad_action(self):
+ self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume)
+ body = {'swap_volume_attachment_bad_action': None}
+ self.assertRaises(exception.ValidationError, self._test_swap,
+ body=body)
+
+ def test_swap_volume_with_invalid_body(self):
+ self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume)
+ body = {'swap_volume_attachment': {'bad_volume_id_body': UUID1,
+ 'new_volume_id': UUID2}}
+ self.assertRaises(exception.ValidationError, self._test_swap,
+ body=body)
+
+ def test_swap_volume_with_invalid_volume(self):
+ self.stubs.Set(compute.api.API, 'swap_volume',
+ fake_swap_volume_invalid_volume)
+ self.assertRaises(webob.exc.HTTPBadRequest, self._test_swap)
+
+ def test_swap_volume_with_unattached_volume(self):
+ self.stubs.Set(compute.api.API, 'swap_volume',
+ fake_swap_volume_unattached_volume)
+ self.assertRaises(webob.exc.HTTPNotFound, self._test_swap)
+
+ def test_swap_volume_with_bad_state_instance(self):
+ self.stubs.Set(compute.api.API, 'swap_volume',
+ fake_swap_volume_instance_invalid_state)
+ self.assertRaises(webob.exc.HTTPConflict, self._test_swap)
+
+ def test_swap_volume_no_attachment(self):
+ self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume)
+ self.assertRaises(webob.exc.HTTPNotFound, self._test_swap, UUID3)
+
+ def test_swap_volume_not_found(self):
+ self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume)
+ self.stubs.Set(volume.cinder.API, 'get', fake_volume_get_not_found)
+ self.assertRaises(webob.exc.HTTPNotFound, self._test_swap)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_extension_info.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_extension_info.py
new file mode 100644
index 0000000000..ee4e9d18b9
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_extension_info.py
@@ -0,0 +1,98 @@
+# Copyright 2013 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import extension_info
+from nova import exception
+from nova import policy
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+class fake_extension(object):
+ def __init__(self, name, alias, description, version):
+ self.name = name
+ self.alias = alias
+ self.__doc__ = description
+ self.version = version
+
+
+fake_extensions = {
+ 'ext1-alias': fake_extension('ext1', 'ext1-alias', 'ext1 description', 1),
+ 'ext2-alias': fake_extension('ext2', 'ext2-alias', 'ext2 description', 2),
+ 'ext3-alias': fake_extension('ext3', 'ext3-alias', 'ext3 description', 1)
+}
+
+
+def fake_policy_enforce(context, action, target, do_raise=True):
+ return True
+
+
+def fake_policy_enforce_selective(context, action, target, do_raise=True):
+ if action == 'compute_extension:v3:ext1-alias:discoverable':
+ raise exception.Forbidden
+ else:
+ return True
+
+
+class ExtensionInfoTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(ExtensionInfoTest, self).setUp()
+ ext_info = plugins.LoadedExtensionInfo()
+ ext_info.extensions = fake_extensions
+ self.controller = extension_info.ExtensionInfoController(ext_info)
+
+ def test_extension_info_list(self):
+ self.stubs.Set(policy, 'enforce', fake_policy_enforce)
+ req = fakes.HTTPRequestV3.blank('/extensions')
+ res_dict = self.controller.index(req)
+ self.assertEqual(3, len(res_dict['extensions']))
+ for e in res_dict['extensions']:
+ self.assertIn(e['alias'], fake_extensions)
+ self.assertEqual(e['name'], fake_extensions[e['alias']].name)
+ self.assertEqual(e['alias'], fake_extensions[e['alias']].alias)
+ self.assertEqual(e['description'],
+ fake_extensions[e['alias']].__doc__)
+ self.assertEqual(e['version'],
+ fake_extensions[e['alias']].version)
+
+ def test_extension_info_show(self):
+ self.stubs.Set(policy, 'enforce', fake_policy_enforce)
+ req = fakes.HTTPRequestV3.blank('/extensions/ext1-alias')
+ res_dict = self.controller.show(req, 'ext1-alias')
+ self.assertEqual(1, len(res_dict))
+ self.assertEqual(res_dict['extension']['name'],
+ fake_extensions['ext1-alias'].name)
+ self.assertEqual(res_dict['extension']['alias'],
+ fake_extensions['ext1-alias'].alias)
+ self.assertEqual(res_dict['extension']['description'],
+ fake_extensions['ext1-alias'].__doc__)
+ self.assertEqual(res_dict['extension']['version'],
+ fake_extensions['ext1-alias'].version)
+
+ def test_extension_info_list_not_all_discoverable(self):
+ self.stubs.Set(policy, 'enforce', fake_policy_enforce_selective)
+ req = fakes.HTTPRequestV3.blank('/extensions')
+ res_dict = self.controller.index(req)
+ self.assertEqual(2, len(res_dict['extensions']))
+ for e in res_dict['extensions']:
+ self.assertNotEqual('ext1-alias', e['alias'])
+ self.assertIn(e['alias'], fake_extensions)
+ self.assertEqual(e['name'], fake_extensions[e['alias']].name)
+ self.assertEqual(e['alias'], fake_extensions[e['alias']].alias)
+ self.assertEqual(e['description'],
+ fake_extensions[e['alias']].__doc__)
+ self.assertEqual(e['version'],
+ fake_extensions[e['alias']].version)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_lock_server.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_lock_server.py
new file mode 100644
index 0000000000..ff5817ba19
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_lock_server.py
@@ -0,0 +1,57 @@
+# Copyright 2011 OpenStack Foundation
+# Copyright 2013 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova.api.openstack.compute.plugins.v3 import lock_server
+from nova import exception
+from nova.tests.unit.api.openstack.compute.plugins.v3 import \
+ admin_only_action_common
+from nova.tests.unit.api.openstack import fakes
+
+
+class LockServerTests(admin_only_action_common.CommonTests):
+ def setUp(self):
+ super(LockServerTests, self).setUp()
+ self.controller = lock_server.LockServerController()
+ self.compute_api = self.controller.compute_api
+
+ def _fake_controller(*args, **kwargs):
+ return self.controller
+
+ self.stubs.Set(lock_server, 'LockServerController',
+ _fake_controller)
+ self.app = fakes.wsgi_app_v21(init_only=('servers',
+ 'os-lock-server'),
+ fake_auth_context=self.context)
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_lock_unlock(self):
+ self._test_actions(['lock', 'unlock'])
+
+ def test_lock_unlock_with_non_existed_instance(self):
+ self._test_actions_with_non_existed_instance(['lock', 'unlock'])
+
+ def test_unlock_not_authorized(self):
+ self.mox.StubOutWithMock(self.compute_api, 'unlock')
+
+ instance = self._stub_instance_get()
+
+ self.compute_api.unlock(self.context, instance).AndRaise(
+ exception.PolicyNotAuthorized(action='unlock'))
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance.uuid,
+ {'unlock': None})
+ self.assertEqual(403, res.status_int)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_migrations.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_migrations.py
new file mode 100644
index 0000000000..c735e87fea
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_migrations.py
@@ -0,0 +1,115 @@
+# 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 datetime
+
+from nova.api.openstack.compute.plugins.v3 import migrations
+from nova import context
+from nova import exception
+from nova import objects
+from nova.objects import base
+from nova.openstack.common.fixture import moxstubout
+from nova import test
+
+
+fake_migrations = [
+ {
+ 'id': 1234,
+ 'source_node': 'node1',
+ 'dest_node': 'node2',
+ 'source_compute': 'compute1',
+ 'dest_compute': 'compute2',
+ 'dest_host': '1.2.3.4',
+ 'status': 'Done',
+ 'instance_uuid': 'instance_id_123',
+ 'old_instance_type_id': 1,
+ 'new_instance_type_id': 2,
+ 'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
+ 'deleted_at': None,
+ 'deleted': False
+ },
+ {
+ 'id': 5678,
+ 'source_node': 'node10',
+ 'dest_node': 'node20',
+ 'source_compute': 'compute10',
+ 'dest_compute': 'compute20',
+ 'dest_host': '5.6.7.8',
+ 'status': 'Done',
+ 'instance_uuid': 'instance_id_456',
+ 'old_instance_type_id': 5,
+ 'new_instance_type_id': 6,
+ 'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
+ 'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
+ 'deleted_at': None,
+ 'deleted': False
+ }
+]
+
+migrations_obj = base.obj_make_list(
+ 'fake-context',
+ objects.MigrationList(),
+ objects.Migration,
+ fake_migrations)
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {}
+
+
+class MigrationsTestCase(test.NoDBTestCase):
+ def setUp(self):
+ """Run before each test."""
+ super(MigrationsTestCase, self).setUp()
+ self.controller = migrations.MigrationsController()
+ self.context = context.get_admin_context()
+ self.req = FakeRequest()
+ self.req.environ['nova.context'] = self.context
+ mox_fixture = self.useFixture(moxstubout.MoxStubout())
+ self.mox = mox_fixture.mox
+
+ def test_index(self):
+ migrations_in_progress = {
+ 'migrations': migrations.output(migrations_obj)}
+
+ for mig in migrations_in_progress['migrations']:
+ self.assertIn('id', mig)
+ self.assertNotIn('deleted', mig)
+ self.assertNotIn('deleted_at', mig)
+
+ filters = {'host': 'host1', 'status': 'migrating',
+ 'cell_name': 'ChildCell'}
+ self.req.GET = filters
+ self.mox.StubOutWithMock(self.controller.compute_api,
+ "get_migrations")
+
+ self.controller.compute_api.get_migrations(
+ self.context, filters).AndReturn(migrations_obj)
+ self.mox.ReplayAll()
+
+ response = self.controller.index(self.req)
+ self.assertEqual(migrations_in_progress, response)
+
+ def test_index_needs_authorization(self):
+ user_context = context.RequestContext(user_id=None,
+ project_id=None,
+ is_admin=False,
+ read_deleted="no",
+ overwrite=False)
+ self.req.environ['nova.context'] = user_context
+
+ self.assertRaises(exception.PolicyNotAuthorized, self.controller.index,
+ self.req)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_multiple_create.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_multiple_create.py
new file mode 100644
index 0000000000..35a559c668
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_multiple_create.py
@@ -0,0 +1,547 @@
+# Copyright 2013 IBM Corp.
+# 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 datetime
+import uuid
+
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import block_device_mapping
+from nova.api.openstack.compute.plugins.v3 import multiple_create
+from nova.api.openstack.compute.plugins.v3 import servers
+from nova.compute import api as compute_api
+from nova.compute import flavors
+from nova import db
+from nova import exception
+from nova.network import manager
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+from nova.tests.unit.image import fake
+
+CONF = cfg.CONF
+FAKE_UUID = fakes.FAKE_UUID
+
+
+def fake_gen_uuid():
+ return FAKE_UUID
+
+
+def return_security_group(context, instance_id, security_group_id):
+ pass
+
+
+class ServersControllerCreateTest(test.TestCase):
+
+ def setUp(self):
+ """Shared implementation for tests below that create instance."""
+ super(ServersControllerCreateTest, self).setUp()
+
+ self.flags(verbose=True,
+ enable_instance_password=True)
+ self.instance_cache_num = 0
+ self.instance_cache_by_id = {}
+ self.instance_cache_by_uuid = {}
+
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+ CONF.set_override('extensions_blacklist', 'os-multiple-create',
+ 'osapi_v3')
+ self.no_mult_create_controller = servers.ServersController(
+ extension_info=ext_info)
+
+ def instance_create(context, inst):
+ inst_type = flavors.get_flavor_by_flavor_id(3)
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ def_image_ref = 'http://localhost/images/%s' % image_uuid
+ self.instance_cache_num += 1
+ instance = fake_instance.fake_db_instance(**{
+ 'id': self.instance_cache_num,
+ 'display_name': inst['display_name'] or 'test',
+ 'uuid': FAKE_UUID,
+ 'instance_type': inst_type,
+ 'access_ip_v4': '1.2.3.4',
+ 'access_ip_v6': 'fead::1234',
+ 'image_ref': inst.get('image_ref', def_image_ref),
+ 'user_id': 'fake',
+ 'project_id': 'fake',
+ 'reservation_id': inst['reservation_id'],
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "progress": 0,
+ "fixed_ips": [],
+ "task_state": "",
+ "vm_state": "",
+ "security_groups": inst['security_groups'],
+ })
+
+ self.instance_cache_by_id[instance['id']] = instance
+ self.instance_cache_by_uuid[instance['uuid']] = instance
+ return instance
+
+ def instance_get(context, instance_id):
+ """Stub for compute/api create() pulling in instance after
+ scheduling
+ """
+ return self.instance_cache_by_id[instance_id]
+
+ def instance_update(context, uuid, values):
+ instance = self.instance_cache_by_uuid[uuid]
+ instance.update(values)
+ return instance
+
+ def server_update(context, instance_uuid, params, update_cells=True,
+ columns_to_join=None):
+ inst = self.instance_cache_by_uuid[instance_uuid]
+ inst.update(params)
+ return (inst, inst)
+
+ def fake_method(*args, **kwargs):
+ pass
+
+ def project_get_networks(context, user_id):
+ return dict(id='1', host='localhost')
+
+ def queue_get_for(context, *args):
+ return 'network_topic'
+
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ fake.stub_out_image_service(self.stubs)
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(uuid, 'uuid4', fake_gen_uuid)
+ self.stubs.Set(db, 'instance_add_security_group',
+ return_security_group)
+ self.stubs.Set(db, 'project_get_networks',
+ project_get_networks)
+ self.stubs.Set(db, 'instance_create', instance_create)
+ self.stubs.Set(db, 'instance_system_metadata_update',
+ fake_method)
+ self.stubs.Set(db, 'instance_get', instance_get)
+ self.stubs.Set(db, 'instance_update', instance_update)
+ self.stubs.Set(db, 'instance_update_and_get_original',
+ server_update)
+ self.stubs.Set(manager.VlanManager, 'allocate_fixed_ip',
+ fake_method)
+
+ def _test_create_extra(self, params, no_image=False,
+ override_controller=None):
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ server = dict(name='server_test', imageRef=image_uuid, flavorRef=2)
+ if no_image:
+ server.pop('imageRef', None)
+ server.update(params)
+ body = dict(server=server)
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ if override_controller:
+ server = override_controller.create(req, body=body).obj['server']
+ else:
+ server = self.controller.create(req, body=body).obj['server']
+
+ def test_create_instance_with_multiple_create_disabled(self):
+ min_count = 2
+ max_count = 3
+ params = {
+ multiple_create.MIN_ATTRIBUTE_NAME: min_count,
+ multiple_create.MAX_ATTRIBUTE_NAME: max_count,
+ }
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertNotIn('min_count', kwargs)
+ self.assertNotIn('max_count', kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(
+ params,
+ override_controller=self.no_mult_create_controller)
+
+ def test_multiple_create_with_string_type_min_and_max(self):
+ min_count = '2'
+ max_count = '3'
+ params = {
+ multiple_create.MIN_ATTRIBUTE_NAME: min_count,
+ multiple_create.MAX_ATTRIBUTE_NAME: max_count,
+ }
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIsInstance(kwargs['min_count'], int)
+ self.assertIsInstance(kwargs['max_count'], int)
+ self.assertEqual(kwargs['min_count'], 2)
+ self.assertEqual(kwargs['max_count'], 3)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_multiple_create_enabled(self):
+ min_count = 2
+ max_count = 3
+ params = {
+ multiple_create.MIN_ATTRIBUTE_NAME: min_count,
+ multiple_create.MAX_ATTRIBUTE_NAME: max_count,
+ }
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['min_count'], 2)
+ self.assertEqual(kwargs['max_count'], 3)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_invalid_negative_min(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ body = {
+ 'server': {
+ multiple_create.MIN_ATTRIBUTE_NAME: -1,
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ }
+ }
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(exception.ValidationError,
+ self.controller.create,
+ req,
+ body=body)
+
+ def test_create_instance_invalid_negative_max(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ body = {
+ 'server': {
+ multiple_create.MAX_ATTRIBUTE_NAME: -1,
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ }
+ }
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(exception.ValidationError,
+ self.controller.create,
+ req,
+ body=body)
+
+ def test_create_instance_with_blank_min(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ body = {
+ 'server': {
+ multiple_create.MIN_ATTRIBUTE_NAME: '',
+ 'name': 'server_test',
+ 'image_ref': image_href,
+ 'flavor_ref': flavor_ref,
+ }
+ }
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(exception.ValidationError,
+ self.controller.create,
+ req,
+ body=body)
+
+ def test_create_instance_with_blank_max(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ body = {
+ 'server': {
+ multiple_create.MAX_ATTRIBUTE_NAME: '',
+ 'name': 'server_test',
+ 'image_ref': image_href,
+ 'flavor_ref': flavor_ref,
+ }
+ }
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(exception.ValidationError,
+ self.controller.create,
+ req,
+ body=body)
+
+ def test_create_instance_invalid_min_greater_than_max(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ body = {
+ 'server': {
+ multiple_create.MIN_ATTRIBUTE_NAME: 4,
+ multiple_create.MAX_ATTRIBUTE_NAME: 2,
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ }
+ }
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ req,
+ body=body)
+
+ def test_create_instance_invalid_alpha_min(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ body = {
+ 'server': {
+ multiple_create.MIN_ATTRIBUTE_NAME: 'abcd',
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ }
+ }
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(exception.ValidationError,
+ self.controller.create,
+ req,
+ body=body)
+
+ def test_create_instance_invalid_alpha_max(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ body = {
+ 'server': {
+ multiple_create.MAX_ATTRIBUTE_NAME: 'abcd',
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ }
+ }
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(exception.ValidationError,
+ self.controller.create,
+ req,
+ body=body)
+
+ def test_create_multiple_instances(self):
+ """Test creating multiple instances but not asking for
+ reservation_id
+ """
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+ body = {
+ 'server': {
+ multiple_create.MIN_ATTRIBUTE_NAME: 2,
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {'hello': 'world',
+ 'open': 'stack'},
+ }
+ }
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = self.controller.create(req, body=body).obj
+
+ self.assertEqual(FAKE_UUID, res["server"]["id"])
+ self._check_admin_password_len(res["server"])
+
+ def test_create_multiple_instances_pass_disabled(self):
+ """Test creating multiple instances but not asking for
+ reservation_id
+ """
+ self.flags(enable_instance_password=False)
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+ body = {
+ 'server': {
+ multiple_create.MIN_ATTRIBUTE_NAME: 2,
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {'hello': 'world',
+ 'open': 'stack'},
+ }
+ }
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = self.controller.create(req, body=body).obj
+
+ self.assertEqual(FAKE_UUID, res["server"]["id"])
+ self._check_admin_password_missing(res["server"])
+
+ def _check_admin_password_len(self, server_dict):
+ """utility function - check server_dict for admin_password length."""
+ self.assertEqual(CONF.password_length,
+ len(server_dict["adminPass"]))
+
+ def _check_admin_password_missing(self, server_dict):
+ """utility function - check server_dict for admin_password absence."""
+ self.assertNotIn("admin_password", server_dict)
+
+ def _create_multiple_instances_resv_id_return(self, resv_id_return):
+ """Test creating multiple instances with asking for
+ reservation_id
+ """
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+ body = {
+ 'server': {
+ multiple_create.MIN_ATTRIBUTE_NAME: 2,
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {'hello': 'world',
+ 'open': 'stack'},
+ multiple_create.RRID_ATTRIBUTE_NAME: resv_id_return
+ }
+ }
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = self.controller.create(req, body=body)
+ reservation_id = res.obj['reservation_id']
+ self.assertNotEqual(reservation_id, "")
+ self.assertIsNotNone(reservation_id)
+ self.assertTrue(len(reservation_id) > 1)
+
+ def test_create_multiple_instances_with_resv_id_return(self):
+ self._create_multiple_instances_resv_id_return(True)
+
+ def test_create_multiple_instances_with_string_resv_id_return(self):
+ self._create_multiple_instances_resv_id_return("True")
+
+ def test_create_multiple_instances_with_multiple_volume_bdm(self):
+ """Test that a BadRequest is raised if multiple instances
+ are requested with a list of block device mappings for volumes.
+ """
+ min_count = 2
+ bdm = [{'source_type': 'volume', 'uuid': 'vol-xxxx'},
+ {'source_type': 'volume', 'uuid': 'vol-yyyy'}
+ ]
+ params = {
+ block_device_mapping.ATTRIBUTE_NAME: bdm,
+ multiple_create.MIN_ATTRIBUTE_NAME: min_count
+ }
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['min_count'], 2)
+ self.assertEqual(len(kwargs['block_device_mapping']), 2)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ exc = self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params, no_image=True)
+ self.assertEqual("Cannot attach one or more volumes to multiple "
+ "instances", exc.explanation)
+
+ def test_create_multiple_instances_with_single_volume_bdm(self):
+ """Test that a BadRequest is raised if multiple instances
+ are requested to boot from a single volume.
+ """
+ min_count = 2
+ bdm = [{'source_type': 'volume', 'uuid': 'vol-xxxx'}]
+ params = {
+ block_device_mapping.ATTRIBUTE_NAME: bdm,
+ multiple_create.MIN_ATTRIBUTE_NAME: min_count
+ }
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['min_count'], 2)
+ self.assertEqual(kwargs['block_device_mapping'][0]['volume_id'],
+ 'vol-xxxx')
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ exc = self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params, no_image=True)
+ self.assertEqual("Cannot attach one or more volumes to multiple "
+ "instances", exc.explanation)
+
+ def test_create_multiple_instance_with_non_integer_max_count(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+ body = {
+ 'server': {
+ multiple_create.MAX_ATTRIBUTE_NAME: 2.5,
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {'hello': 'world',
+ 'open': 'stack'},
+ }
+ }
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, req, body=body)
+
+ def test_create_multiple_instance_with_non_integer_min_count(self):
+ image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+ body = {
+ 'server': {
+ multiple_create.MIN_ATTRIBUTE_NAME: 2.5,
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {'hello': 'world',
+ 'open': 'stack'},
+ }
+ }
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, req, body=body)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_pause_server.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_pause_server.py
new file mode 100644
index 0000000000..5364fb45b3
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_pause_server.py
@@ -0,0 +1,60 @@
+# Copyright 2011 OpenStack Foundation
+# Copyright 2013 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova.api.openstack.compute.plugins.v3 import pause_server
+from nova.tests.unit.api.openstack.compute.plugins.v3 import \
+ admin_only_action_common
+from nova.tests.unit.api.openstack import fakes
+
+
+class PauseServerTests(admin_only_action_common.CommonTests):
+ def setUp(self):
+ super(PauseServerTests, self).setUp()
+ self.controller = pause_server.PauseServerController()
+ self.compute_api = self.controller.compute_api
+
+ def _fake_controller(*args, **kwargs):
+ return self.controller
+
+ self.stubs.Set(pause_server, 'PauseServerController',
+ _fake_controller)
+ self.app = fakes.wsgi_app_v21(init_only=('servers',
+ 'os-pause-server'),
+ fake_auth_context=self.context)
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_pause_unpause(self):
+ self._test_actions(['pause', 'unpause'])
+
+ def test_actions_raise_on_not_implemented(self):
+ for action in ['pause', 'unpause']:
+ self.mox.StubOutWithMock(self.compute_api, action)
+ self._test_not_implemented_state(action)
+ # Re-mock this.
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_pause_unpause_with_non_existed_instance(self):
+ self._test_actions_with_non_existed_instance(['pause', 'unpause'])
+
+ def test_pause_unpause_with_non_existed_instance_in_compute_api(self):
+ self._test_actions_instance_not_found_in_compute_api(['pause',
+ 'unpause'])
+
+ def test_pause_unpause_raise_conflict_on_invalid_state(self):
+ self._test_actions_raise_conflict_on_invalid_state(['pause',
+ 'unpause'])
+
+ def test_actions_with_locked_instance(self):
+ self._test_actions_with_locked_instance(['pause', 'unpause'])
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_pci.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_pci.py
new file mode 100644
index 0000000000..6ac6269195
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_pci.py
@@ -0,0 +1,236 @@
+# Copyright 2013 Intel Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+from oslo.serialization import jsonutils
+from webob import exc
+
+from nova.api.openstack.compute.plugins.v3 import pci
+from nova.api.openstack import wsgi
+from nova import context
+from nova import db
+from nova import exception
+from nova import objects
+from nova.pci import device
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.objects import test_pci_device
+
+
+fake_compute_node = {
+ 'pci_stats': [{"count": 3,
+ "vendor_id": "8086",
+ "product_id": "1520",
+ "extra_info": {"phys_function": '[["0x0000", "0x04", '
+ '"0x00", "0x1"]]'}}]}
+
+
+class FakeResponse(wsgi.ResponseObject):
+ pass
+
+
+class PciServerControllerTest(test.NoDBTestCase):
+ def setUp(self):
+ super(PciServerControllerTest, self).setUp()
+ self.controller = pci.PciServerController()
+ self.fake_obj = {'server': {'addresses': {},
+ 'id': 'fb08',
+ 'name': 'a3',
+ 'status': 'ACTIVE',
+ 'tenant_id': '9a3af784c',
+ 'user_id': 'e992080ac0',
+ }}
+ self.fake_list = {'servers': [{'addresses': {},
+ 'id': 'fb08',
+ 'name': 'a3',
+ 'status': 'ACTIVE',
+ 'tenant_id': '9a3af784c',
+ 'user_id': 'e992080ac',
+ }]}
+ self._create_fake_instance()
+ self._create_fake_pci_device()
+ device.claim(self.pci_device, self.inst)
+ device.allocate(self.pci_device, self.inst)
+
+ def _create_fake_instance(self):
+ self.inst = objects.Instance()
+ self.inst.uuid = 'fake-inst-uuid'
+ self.inst.pci_devices = objects.PciDeviceList()
+
+ def _create_fake_pci_device(self):
+ def fake_pci_device_get_by_addr(ctxt, id, addr):
+ return test_pci_device.fake_db_dev
+
+ ctxt = context.get_admin_context()
+ self.stubs.Set(db, 'pci_device_get_by_addr',
+ fake_pci_device_get_by_addr)
+ self.pci_device = objects.PciDevice.get_by_dev_addr(ctxt, 1, 'a')
+
+ def test_show(self):
+ def fake_get_db_instance(id):
+ return self.inst
+
+ resp = FakeResponse(self.fake_obj, '')
+ req = fakes.HTTPRequestV3.blank('/os-pci/1', use_admin_context=True)
+ self.stubs.Set(req, 'get_db_instance', fake_get_db_instance)
+ self.controller.show(req, resp, '1')
+ self.assertEqual([{'id': 1}],
+ resp.obj['server']['os-pci:pci_devices'])
+
+ def test_detail(self):
+ def fake_get_db_instance(id):
+ return self.inst
+
+ resp = FakeResponse(self.fake_list, '')
+ req = fakes.HTTPRequestV3.blank('/os-pci/detail',
+ use_admin_context=True)
+ self.stubs.Set(req, 'get_db_instance', fake_get_db_instance)
+ self.controller.detail(req, resp)
+ self.assertEqual([{'id': 1}],
+ resp.obj['servers'][0]['os-pci:pci_devices'])
+
+
+class PciHypervisorControllerTest(test.NoDBTestCase):
+ def setUp(self):
+ super(PciHypervisorControllerTest, self).setUp()
+ self.controller = pci.PciHypervisorController()
+ self.fake_objs = dict(hypervisors=[
+ dict(id=1,
+ service=dict(id=1, host="compute1"),
+ hypervisor_type="xen",
+ hypervisor_version=3,
+ hypervisor_hostname="hyper1")])
+ self.fake_obj = dict(hypervisor=dict(
+ id=1,
+ service=dict(id=1, host="compute1"),
+ hypervisor_type="xen",
+ hypervisor_version=3,
+ hypervisor_hostname="hyper1"))
+
+ def test_show(self):
+ def fake_get_db_compute_node(id):
+ fake_compute_node['pci_stats'] = jsonutils.dumps(
+ fake_compute_node['pci_stats'])
+ return fake_compute_node
+
+ req = fakes.HTTPRequestV3.blank('/os-hypervisors/1',
+ use_admin_context=True)
+ resp = FakeResponse(self.fake_obj, '')
+ self.stubs.Set(req, 'get_db_compute_node', fake_get_db_compute_node)
+ self.controller.show(req, resp, '1')
+ self.assertIn('os-pci:pci_stats', resp.obj['hypervisor'])
+ fake_compute_node['pci_stats'] = jsonutils.loads(
+ fake_compute_node['pci_stats'])
+ self.assertEqual(fake_compute_node['pci_stats'][0],
+ resp.obj['hypervisor']['os-pci:pci_stats'][0])
+
+ def test_detail(self):
+ def fake_get_db_compute_node(id):
+ fake_compute_node['pci_stats'] = jsonutils.dumps(
+ fake_compute_node['pci_stats'])
+ return fake_compute_node
+
+ req = fakes.HTTPRequestV3.blank('/os-hypervisors/detail',
+ use_admin_context=True)
+ resp = FakeResponse(self.fake_objs, '')
+ self.stubs.Set(req, 'get_db_compute_node', fake_get_db_compute_node)
+ self.controller.detail(req, resp)
+ fake_compute_node['pci_stats'] = jsonutils.loads(
+ fake_compute_node['pci_stats'])
+ self.assertIn('os-pci:pci_stats', resp.obj['hypervisors'][0])
+ self.assertEqual(fake_compute_node['pci_stats'][0],
+ resp.obj['hypervisors'][0]['os-pci:pci_stats'][0])
+
+
+class PciControlletest(test.NoDBTestCase):
+ def setUp(self):
+ super(PciControlletest, self).setUp()
+ self.controller = pci.PciController()
+
+ def test_show(self):
+ def fake_pci_device_get_by_id(context, id):
+ return test_pci_device.fake_db_dev
+
+ self.stubs.Set(db, 'pci_device_get_by_id', fake_pci_device_get_by_id)
+ req = fakes.HTTPRequestV3.blank('/os-pci/1', use_admin_context=True)
+ result = self.controller.show(req, '1')
+ dist = {'pci_device': {'address': 'a',
+ 'compute_node_id': 1,
+ 'dev_id': 'i',
+ 'extra_info': {},
+ 'dev_type': 't',
+ 'id': 1,
+ 'server_uuid': None,
+ 'label': 'l',
+ 'product_id': 'p',
+ 'status': 'available',
+ 'vendor_id': 'v'}}
+ self.assertEqual(dist, result)
+
+ def test_show_error_id(self):
+ def fake_pci_device_get_by_id(context, id):
+ raise exception.PciDeviceNotFoundById(id=id)
+
+ self.stubs.Set(db, 'pci_device_get_by_id', fake_pci_device_get_by_id)
+ req = fakes.HTTPRequestV3.blank('/os-pci/0', use_admin_context=True)
+ self.assertRaises(exc.HTTPNotFound, self.controller.show, req, '0')
+
+ def _fake_compute_node_get_all(self, context):
+ return [dict(id=1,
+ service_id=1,
+ cpu_info='cpu_info',
+ disk_available_least=100)]
+
+ def _fake_pci_device_get_all_by_node(self, context, node):
+ return [test_pci_device.fake_db_dev, test_pci_device.fake_db_dev_1]
+
+ def test_index(self):
+ self.stubs.Set(db, 'compute_node_get_all',
+ self._fake_compute_node_get_all)
+ self.stubs.Set(db, 'pci_device_get_all_by_node',
+ self._fake_pci_device_get_all_by_node)
+
+ req = fakes.HTTPRequestV3.blank('/os-pci', use_admin_context=True)
+ result = self.controller.index(req)
+ dist = {'pci_devices': [test_pci_device.fake_db_dev,
+ test_pci_device.fake_db_dev_1]}
+ for i in range(len(result['pci_devices'])):
+ self.assertEqual(dist['pci_devices'][i]['vendor_id'],
+ result['pci_devices'][i]['vendor_id'])
+ self.assertEqual(dist['pci_devices'][i]['id'],
+ result['pci_devices'][i]['id'])
+ self.assertEqual(dist['pci_devices'][i]['status'],
+ result['pci_devices'][i]['status'])
+ self.assertEqual(dist['pci_devices'][i]['address'],
+ result['pci_devices'][i]['address'])
+
+ def test_detail(self):
+ self.stubs.Set(db, 'compute_node_get_all',
+ self._fake_compute_node_get_all)
+ self.stubs.Set(db, 'pci_device_get_all_by_node',
+ self._fake_pci_device_get_all_by_node)
+ req = fakes.HTTPRequestV3.blank('/os-pci/detail',
+ use_admin_context=True)
+ result = self.controller.detail(req)
+ dist = {'pci_devices': [test_pci_device.fake_db_dev,
+ test_pci_device.fake_db_dev_1]}
+ for i in range(len(result['pci_devices'])):
+ self.assertEqual(dist['pci_devices'][i]['vendor_id'],
+ result['pci_devices'][i]['vendor_id'])
+ self.assertEqual(dist['pci_devices'][i]['id'],
+ result['pci_devices'][i]['id'])
+ self.assertEqual(dist['pci_devices'][i]['label'],
+ result['pci_devices'][i]['label'])
+ self.assertEqual(dist['pci_devices'][i]['dev_id'],
+ result['pci_devices'][i]['dev_id'])
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_actions.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_actions.py
new file mode 100644
index 0000000000..0bfe0eb2d4
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_actions.py
@@ -0,0 +1,1131 @@
+# Copyright 2011 OpenStack Foundation
+# 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 uuid
+
+import mock
+import mox
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import servers
+from nova.compute import api as compute_api
+from nova.compute import task_states
+from nova.compute import vm_states
+from nova import context
+from nova import db
+from nova import exception
+from nova.image import glance
+from nova import objects
+from nova.openstack.common import uuidutils
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_block_device
+from nova.tests.unit import fake_instance
+from nova.tests.unit.image import fake
+
+CONF = cfg.CONF
+CONF.import_opt('password_length', 'nova.utils')
+FAKE_UUID = fakes.FAKE_UUID
+INSTANCE_IDS = {FAKE_UUID: 1}
+
+
+def return_server_not_found(*arg, **kwarg):
+ raise exception.InstanceNotFound(instance_id='42')
+
+
+def instance_update_and_get_original(context, instance_uuid, values,
+ update_cells=True,
+ columns_to_join=None,
+ ):
+ inst = fakes.stub_instance(INSTANCE_IDS[instance_uuid], host='fake_host')
+ inst = dict(inst, **values)
+ return (inst, inst)
+
+
+def instance_update(context, instance_uuid, kwargs, update_cells=True):
+ inst = fakes.stub_instance(INSTANCE_IDS[instance_uuid], host='fake_host')
+ return inst
+
+
+class MockSetAdminPassword(object):
+ def __init__(self):
+ self.instance_id = None
+ self.password = None
+
+ def __call__(self, context, instance, password):
+ self.instance_id = instance['uuid']
+ self.password = password
+
+
+class ServerActionsControllerTest(test.TestCase):
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ image_href = 'http://localhost/v2/fake/images/%s' % image_uuid
+
+ def setUp(self):
+ super(ServerActionsControllerTest, self).setUp()
+
+ CONF.set_override('host', 'localhost', group='glance')
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ host='fake_host'))
+ self.stubs.Set(db, 'instance_update_and_get_original',
+ instance_update_and_get_original)
+
+ fakes.stub_out_nw_api(self.stubs)
+ fakes.stub_out_compute_api_snapshot(self.stubs)
+ fake.stub_out_image_service(self.stubs)
+ self.flags(allow_instance_snapshots=True,
+ enable_instance_password=True)
+ self.uuid = FAKE_UUID
+ self.url = '/servers/%s/action' % self.uuid
+ self._image_href = '155d900f-4e14-4e4c-a73d-069cbf4541e6'
+
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+ self.compute_api = self.controller.compute_api
+ self.context = context.RequestContext('fake', 'fake')
+ self.app = fakes.wsgi_app_v21(init_only=('servers',),
+ fake_auth_context=self.context)
+
+ def _make_request(self, url, body):
+ req = webob.Request.blank('/v2/fake' + url)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.content_type = 'application/json'
+ return req.get_response(self.app)
+
+ def _stub_instance_get(self, uuid=None):
+ self.mox.StubOutWithMock(compute_api.API, 'get')
+ if uuid is None:
+ uuid = uuidutils.generate_uuid()
+ instance = fake_instance.fake_db_instance(
+ id=1, uuid=uuid, vm_state=vm_states.ACTIVE, task_state=None)
+ instance = objects.Instance._from_db_object(
+ self.context, objects.Instance(), instance)
+
+ self.compute_api.get(self.context, uuid, want_objects=True,
+ expected_attrs=['pci_devices']).AndReturn(instance)
+ return instance
+
+ def _test_locked_instance(self, action, method=None, body_map=None,
+ compute_api_args_map=None):
+ if method is None:
+ method = action
+ if body_map is None:
+ body_map = {}
+ if compute_api_args_map is None:
+ compute_api_args_map = {}
+
+ instance = self._stub_instance_get()
+ args, kwargs = compute_api_args_map.get(action, ((), {}))
+
+ getattr(compute_api.API, method)(self.context, instance,
+ *args, **kwargs).AndRaise(
+ exception.InstanceIsLocked(instance_uuid=instance['uuid']))
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance['uuid'],
+ {action: body_map.get(action)})
+ self.assertEqual(409, res.status_int)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def test_actions_with_locked_instance(self):
+ actions = ['resize', 'confirmResize', 'revertResize', 'reboot',
+ 'rebuild']
+
+ method_translations = {'confirmResize': 'confirm_resize',
+ 'revertResize': 'revert_resize'}
+
+ body_map = {'resize': {'flavorRef': '2'},
+ 'reboot': {'type': 'HARD'},
+ 'rebuild': {'imageRef': self.image_uuid,
+ 'adminPass': 'TNc53Dr8s7vw'}}
+
+ args_map = {'resize': (('2'), {}),
+ 'confirmResize': ((), {}),
+ 'reboot': (('HARD',), {}),
+ 'rebuild': ((self.image_uuid, 'TNc53Dr8s7vw'), {})}
+
+ for action in actions:
+ method = method_translations.get(action)
+ self.mox.StubOutWithMock(compute_api.API, method or action)
+ self._test_locked_instance(action, method=method,
+ body_map=body_map,
+ compute_api_args_map=args_map)
+
+ def test_reboot_hard(self):
+ body = dict(reboot=dict(type="HARD"))
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.controller._action_reboot(req, FAKE_UUID, body)
+
+ def test_reboot_soft(self):
+ body = dict(reboot=dict(type="SOFT"))
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.controller._action_reboot(req, FAKE_UUID, body)
+
+ def test_reboot_incorrect_type(self):
+ body = dict(reboot=dict(type="NOT_A_TYPE"))
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_missing_type(self):
+ body = dict(reboot=dict())
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_none(self):
+ body = dict(reboot=dict(type=None))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_not_found(self):
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ return_server_not_found)
+
+ body = dict(reboot=dict(type="HARD"))
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._action_reboot,
+ req, str(uuid.uuid4()), body)
+
+ def test_reboot_raises_conflict_on_invalid_state(self):
+ body = dict(reboot=dict(type="HARD"))
+
+ def fake_reboot(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'reboot', fake_reboot)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_soft_with_soft_in_progress_raises_conflict(self):
+ body = dict(reboot=dict(type="SOFT"))
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.REBOOTING))
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_hard_with_soft_in_progress_does_not_raise(self):
+ body = dict(reboot=dict(type="HARD"))
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.REBOOTING))
+ self.controller._action_reboot(req, FAKE_UUID, body)
+
+ def test_reboot_hard_with_hard_in_progress_raises_conflict(self):
+ body = dict(reboot=dict(type="HARD"))
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.REBOOTING_HARD))
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_rebuild_accepted_minimum(self):
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+ self_href = 'http://localhost/v3/servers/%s' % FAKE_UUID
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ robj = self.controller._action_rebuild(req, FAKE_UUID, body=body)
+ body = robj.obj
+
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertEqual(len(body['server']['adminPass']),
+ CONF.password_length)
+
+ self.assertEqual(robj['location'], self_href)
+
+ def test_rebuild_instance_with_image_uuid(self):
+ info = dict(image_href_in_call=None)
+
+ def rebuild(self2, context, instance, image_href, *args, **kwargs):
+ info['image_href_in_call'] = image_href
+
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE))
+ self.stubs.Set(compute_api.API, 'rebuild', rebuild)
+
+ body = {
+ 'rebuild': {
+ 'imageRef': self.image_uuid,
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank('/v2/fake/servers/a/action')
+ self.controller._action_rebuild(req, FAKE_UUID, body=body)
+ self.assertEqual(info['image_href_in_call'], self.image_uuid)
+
+ def test_rebuild_instance_with_image_href_uses_uuid(self):
+ info = dict(image_href_in_call=None)
+
+ def rebuild(self2, context, instance, image_href, *args, **kwargs):
+ info['image_href_in_call'] = image_href
+
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE))
+ self.stubs.Set(compute_api.API, 'rebuild', rebuild)
+
+ body = {
+ 'rebuild': {
+ 'imageRef': self.image_href,
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank('/v2/fake/servers/a/action')
+ self.controller._action_rebuild(req, FAKE_UUID, body=body)
+ self.assertEqual(info['image_href_in_call'], self.image_uuid)
+
+ def test_rebuild_accepted_minimum_pass_disabled(self):
+ # run with enable_instance_password disabled to verify admin_password
+ # is missing from response. See lp bug 921814
+ self.flags(enable_instance_password=False)
+
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+ self_href = 'http://localhost/v3/servers/%s' % FAKE_UUID
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ robj = self.controller._action_rebuild(req, FAKE_UUID, body=body)
+ body = robj.obj
+
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertNotIn("admin_password", body['server'])
+
+ self.assertEqual(robj['location'], self_href)
+
+ def test_rebuild_raises_conflict_on_invalid_state(self):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ def fake_rebuild(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'rebuild', fake_rebuild)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body=body)
+
+ def test_rebuild_accepted_with_metadata(self):
+ metadata = {'new': 'metadata'}
+
+ return_server = fakes.fake_instance_get(metadata=metadata,
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "metadata": metadata,
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ body = self.controller._action_rebuild(req, FAKE_UUID, body=body).obj
+
+ self.assertEqual(body['server']['metadata'], metadata)
+
+ def test_rebuild_accepted_with_bad_metadata(self):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "metadata": "stack",
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body=body)
+
+ def test_rebuild_with_too_large_metadata(self):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "metadata": {
+ 256 * "k": "value"
+ }
+ }
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_rebuild, req,
+ FAKE_UUID, body=body)
+
+ def test_rebuild_bad_entity(self):
+ body = {
+ "rebuild": {
+ "imageId": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body=body)
+
+ def test_rebuild_admin_password(self):
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "adminPass": "asdf",
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ body = self.controller._action_rebuild(req, FAKE_UUID, body=body).obj
+
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertEqual(body['server']['adminPass'], 'asdf')
+
+ def test_rebuild_admin_password_pass_disabled(self):
+ # run with enable_instance_password disabled to verify admin_password
+ # is missing from response. See lp bug 921814
+ self.flags(enable_instance_password=False)
+
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "admin_password": "asdf",
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ body = self.controller._action_rebuild(req, FAKE_UUID, body=body).obj
+
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertNotIn('adminPass', body['server'])
+
+ def test_rebuild_server_not_found(self):
+ def server_not_found(self, instance_id,
+ columns_to_join=None, use_slave=False):
+ raise exception.InstanceNotFound(instance_id=instance_id)
+ self.stubs.Set(db, 'instance_get_by_uuid', server_not_found)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body=body)
+
+ def test_rebuild_with_bad_image(self):
+ body = {
+ "rebuild": {
+ "imageRef": "foo",
+ },
+ }
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body=body)
+
+ def test_rebuild_when_kernel_not_exists(self):
+
+ def return_image_meta(*args, **kwargs):
+ image_meta_table = {
+ '2': {'id': 2, 'status': 'active', 'container_format': 'ari'},
+ '155d900f-4e14-4e4c-a73d-069cbf4541e6':
+ {'id': 3, 'status': 'active', 'container_format': 'raw',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 2}},
+ }
+ image_id = args[2]
+ try:
+ image_meta = image_meta_table[str(image_id)]
+ except KeyError:
+ raise exception.ImageNotFound(image_id=image_id)
+
+ return image_meta
+
+ self.stubs.Set(fake._FakeImageService, 'show', return_image_meta)
+ body = {
+ "rebuild": {
+ "imageRef": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
+ },
+ }
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body=body)
+
+ def test_rebuild_proper_kernel_ram(self):
+ instance_meta = {'kernel_id': None, 'ramdisk_id': None}
+
+ orig_get = compute_api.API.get
+
+ def wrap_get(*args, **kwargs):
+ inst = orig_get(*args, **kwargs)
+ instance_meta['instance'] = inst
+ return inst
+
+ def fake_save(context, **kwargs):
+ instance = instance_meta['instance']
+ for key in instance_meta.keys():
+ if key in instance.obj_what_changed():
+ instance_meta[key] = instance[key]
+
+ def return_image_meta(*args, **kwargs):
+ image_meta_table = {
+ '1': {'id': 1, 'status': 'active', 'container_format': 'aki'},
+ '2': {'id': 2, 'status': 'active', 'container_format': 'ari'},
+ '155d900f-4e14-4e4c-a73d-069cbf4541e6':
+ {'id': 3, 'status': 'active', 'container_format': 'raw',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 2}},
+ }
+ image_id = args[2]
+ try:
+ image_meta = image_meta_table[str(image_id)]
+ except KeyError:
+ raise exception.ImageNotFound(image_id=image_id)
+
+ return image_meta
+
+ self.stubs.Set(fake._FakeImageService, 'show', return_image_meta)
+ self.stubs.Set(compute_api.API, 'get', wrap_get)
+ self.stubs.Set(objects.Instance, 'save', fake_save)
+ body = {
+ "rebuild": {
+ "imageRef": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
+ },
+ }
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.controller._action_rebuild(req, FAKE_UUID, body=body).obj
+ self.assertEqual(instance_meta['kernel_id'], '1')
+ self.assertEqual(instance_meta['ramdisk_id'], '2')
+
+ def _test_rebuild_preserve_ephemeral(self, value=None):
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE,
+ host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+ if value is not None:
+ body['rebuild']['preserve_ephemeral'] = value
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ context = req.environ['nova.context']
+
+ self.mox.StubOutWithMock(compute_api.API, 'rebuild')
+ if value is not None:
+ compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href,
+ mox.IgnoreArg(), preserve_ephemeral=value)
+ else:
+ compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href,
+ mox.IgnoreArg())
+ self.mox.ReplayAll()
+
+ self.controller._action_rebuild(req, FAKE_UUID, body=body)
+
+ def test_rebuild_preserve_ephemeral_true(self):
+ self._test_rebuild_preserve_ephemeral(True)
+
+ def test_rebuild_preserve_ephemeral_false(self):
+ self._test_rebuild_preserve_ephemeral(False)
+
+ def test_rebuild_preserve_ephemeral_default(self):
+ self._test_rebuild_preserve_ephemeral()
+
+ @mock.patch.object(compute_api.API, 'rebuild',
+ side_effect=exception.AutoDiskConfigDisabledByImage(
+ image='dummy'))
+ def test_rebuild_instance_raise_auto_disk_config_exc(self, mock_rebuild):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body=body)
+
+ def test_resize_server(self):
+
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(compute_api.API, 'resize', resize_mock)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ body = self.controller._action_resize(req, FAKE_UUID, body=body)
+
+ self.assertEqual(self.resize_called, True)
+
+ def test_resize_server_no_flavor(self):
+ body = dict(resize=dict())
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_resize,
+ req, FAKE_UUID, body=body)
+
+ def test_resize_server_no_flavor_ref(self):
+ body = dict(resize=dict(flavorRef=None))
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_resize,
+ req, FAKE_UUID, body=body)
+
+ def test_resize_with_server_not_found(self):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ self.stubs.Set(compute_api.API, 'get', return_server_not_found)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._action_resize,
+ req, FAKE_UUID, body=body)
+
+ def test_resize_with_image_exceptions(self):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+ self.resize_called = 0
+ image_id = 'fake_image_id'
+
+ exceptions = [
+ (exception.ImageNotAuthorized(image_id=image_id),
+ webob.exc.HTTPUnauthorized),
+ (exception.ImageNotFound(image_id=image_id),
+ webob.exc.HTTPBadRequest),
+ (exception.Invalid, webob.exc.HTTPBadRequest),
+ (exception.NoValidHost(reason='Bad host'),
+ webob.exc.HTTPBadRequest),
+ (exception.AutoDiskConfigDisabledByImage(image=image_id),
+ webob.exc.HTTPBadRequest),
+ ]
+
+ raised, expected = map(iter, zip(*exceptions))
+
+ def _fake_resize(obj, context, instance, flavor_id):
+ self.resize_called += 1
+ raise raised.next()
+
+ self.stubs.Set(compute_api.API, 'resize', _fake_resize)
+
+ for call_no in range(len(exceptions)):
+ req = fakes.HTTPRequestV3.blank(self.url)
+ next_exception = expected.next()
+ actual = self.assertRaises(next_exception,
+ self.controller._action_resize,
+ req, FAKE_UUID, body=body)
+ if (isinstance(exceptions[call_no][0],
+ exception.NoValidHost)):
+ self.assertEqual(actual.explanation,
+ 'No valid host was found. Bad host')
+ elif (isinstance(exceptions[call_no][0],
+ exception.AutoDiskConfigDisabledByImage)):
+ self.assertEqual(actual.explanation,
+ 'Requested image fake_image_id has automatic'
+ ' disk resize disabled.')
+ self.assertEqual(self.resize_called, call_no + 1)
+
+ def test_resize_with_too_many_instances(self):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ def fake_resize(*args, **kwargs):
+ raise exception.TooManyInstances(message="TooManyInstance")
+
+ self.stubs.Set(compute_api.API, 'resize', fake_resize)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller._action_resize,
+ req, FAKE_UUID, body=body)
+
+ @mock.patch('nova.compute.api.API.resize',
+ side_effect=exception.CannotResizeDisk(reason=''))
+ def test_resize_raises_cannot_resize_disk(self, mock_resize):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_resize,
+ req, FAKE_UUID, body=body)
+
+ @mock.patch('nova.compute.api.API.resize',
+ side_effect=exception.FlavorNotFound(reason='',
+ flavor_id='fake_id'))
+ def test_resize_raises_flavor_not_found(self, mock_resize):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_resize,
+ req, FAKE_UUID, body=body)
+
+ def test_resize_raises_conflict_on_invalid_state(self):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ def fake_resize(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'resize', fake_resize)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_resize,
+ req, FAKE_UUID, body=body)
+
+ def test_confirm_resize_server(self):
+ body = dict(confirmResize=None)
+
+ self.confirm_resize_called = False
+
+ def cr_mock(*args):
+ self.confirm_resize_called = True
+
+ self.stubs.Set(compute_api.API, 'confirm_resize', cr_mock)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ body = self.controller._action_confirm_resize(req, FAKE_UUID, body)
+
+ self.assertEqual(self.confirm_resize_called, True)
+
+ def test_confirm_resize_migration_not_found(self):
+ body = dict(confirmResize=None)
+
+ def confirm_resize_mock(*args):
+ raise exception.MigrationNotFoundByStatus(instance_id=1,
+ status='finished')
+
+ self.stubs.Set(compute_api.API,
+ 'confirm_resize',
+ confirm_resize_mock)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_confirm_resize,
+ req, FAKE_UUID, body)
+
+ def test_confirm_resize_raises_conflict_on_invalid_state(self):
+ body = dict(confirmResize=None)
+
+ def fake_confirm_resize(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'confirm_resize',
+ fake_confirm_resize)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_confirm_resize,
+ req, FAKE_UUID, body)
+
+ def test_revert_resize_migration_not_found(self):
+ body = dict(revertResize=None)
+
+ def revert_resize_mock(*args):
+ raise exception.MigrationNotFoundByStatus(instance_id=1,
+ status='finished')
+
+ self.stubs.Set(compute_api.API,
+ 'revert_resize',
+ revert_resize_mock)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_revert_resize,
+ req, FAKE_UUID, body)
+
+ def test_revert_resize_server_not_found(self):
+ body = dict(revertResize=None)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob. exc.HTTPNotFound,
+ self.controller._action_revert_resize,
+ req, "bad_server_id", body)
+
+ def test_revert_resize_server(self):
+ body = dict(revertResize=None)
+
+ self.revert_resize_called = False
+
+ def revert_mock(*args):
+ self.revert_resize_called = True
+
+ self.stubs.Set(compute_api.API, 'revert_resize', revert_mock)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ body = self.controller._action_revert_resize(req, FAKE_UUID, body)
+
+ self.assertEqual(self.revert_resize_called, True)
+
+ def test_revert_resize_raises_conflict_on_invalid_state(self):
+ body = dict(revertResize=None)
+
+ def fake_revert_resize(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'revert_resize',
+ fake_revert_resize)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_revert_resize,
+ req, FAKE_UUID, body)
+
+ def test_create_image(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ response = self.controller._action_create_image(req, FAKE_UUID, body)
+
+ location = response.headers['Location']
+ self.assertEqual(glance.generate_image_url('123'), location)
+
+ def test_create_image_name_too_long(self):
+ long_name = 'a' * 260
+ body = {
+ 'createImage': {
+ 'name': long_name,
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image, req,
+ FAKE_UUID, body)
+
+ def _do_test_create_volume_backed_image(self, extra_properties):
+
+ def _fake_id(x):
+ return '%s-%s-%s-%s' % (x * 8, x * 4, x * 4, x * 12)
+
+ body = dict(createImage=dict(name='snapshot_of_volume_backed'))
+
+ if extra_properties:
+ body['createImage']['metadata'] = extra_properties
+
+ image_service = glance.get_default_image_service()
+
+ bdm = [dict(volume_id=_fake_id('a'),
+ volume_size=1,
+ device_name='vda',
+ delete_on_termination=False)]
+ props = dict(kernel_id=_fake_id('b'),
+ ramdisk_id=_fake_id('c'),
+ root_device_name='/dev/vda',
+ block_device_mapping=bdm)
+ original_image = dict(properties=props,
+ container_format='ami',
+ status='active',
+ is_public=True)
+
+ image_service.create(None, original_image)
+
+ def fake_block_device_mapping_get_all_by_instance(context, inst_id,
+ use_slave=False):
+ return [fake_block_device.FakeDbBlockDeviceDict(
+ {'volume_id': _fake_id('a'),
+ 'source_type': 'snapshot',
+ 'destination_type': 'volume',
+ 'volume_size': 1,
+ 'device_name': 'vda',
+ 'snapshot_id': 1,
+ 'boot_index': 0,
+ 'delete_on_termination': False,
+ 'no_device': None})]
+
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ fake_block_device_mapping_get_all_by_instance)
+
+ instance = fakes.fake_instance_get(image_ref=original_image['id'],
+ vm_state=vm_states.ACTIVE,
+ root_device_name='/dev/vda')
+ self.stubs.Set(db, 'instance_get_by_uuid', instance)
+
+ volume = dict(id=_fake_id('a'),
+ size=1,
+ host='fake',
+ display_description='fake')
+ snapshot = dict(id=_fake_id('d'))
+ self.mox.StubOutWithMock(self.controller.compute_api, 'volume_api')
+ volume_api = self.controller.compute_api.volume_api
+ volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume)
+ volume_api.create_snapshot_force(mox.IgnoreArg(), volume['id'],
+ mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(snapshot)
+
+ self.mox.ReplayAll()
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ response = self.controller._action_create_image(req, FAKE_UUID, body)
+
+ location = response.headers['Location']
+ image_id = location.replace(glance.generate_image_url(''), '')
+ image = image_service.show(None, image_id)
+
+ self.assertEqual(image['name'], 'snapshot_of_volume_backed')
+ properties = image['properties']
+ self.assertEqual(properties['kernel_id'], _fake_id('b'))
+ self.assertEqual(properties['ramdisk_id'], _fake_id('c'))
+ self.assertEqual(properties['root_device_name'], '/dev/vda')
+ self.assertEqual(properties['bdm_v2'], True)
+ bdms = properties['block_device_mapping']
+ self.assertEqual(len(bdms), 1)
+ self.assertEqual(bdms[0]['boot_index'], 0)
+ self.assertEqual(bdms[0]['source_type'], 'snapshot')
+ self.assertEqual(bdms[0]['destination_type'], 'volume')
+ self.assertEqual(bdms[0]['snapshot_id'], snapshot['id'])
+ for fld in ('connection_info', 'id',
+ 'instance_uuid', 'device_name'):
+ self.assertNotIn(fld, bdms[0])
+ for k in extra_properties.keys():
+ self.assertEqual(properties[k], extra_properties[k])
+
+ def test_create_volume_backed_image_no_metadata(self):
+ self._do_test_create_volume_backed_image({})
+
+ def test_create_volume_backed_image_with_metadata(self):
+ self._do_test_create_volume_backed_image(dict(ImageType='Gold',
+ ImageVersion='2.0'))
+
+ def _test_create_volume_backed_image_with_metadata_from_volume(
+ self, extra_metadata=None):
+
+ def _fake_id(x):
+ return '%s-%s-%s-%s' % (x * 8, x * 4, x * 4, x * 12)
+
+ body = dict(createImage=dict(name='snapshot_of_volume_backed'))
+ if extra_metadata:
+ body['createImage']['metadata'] = extra_metadata
+
+ image_service = glance.get_default_image_service()
+
+ def fake_block_device_mapping_get_all_by_instance(context, inst_id,
+ use_slave=False):
+ return [fake_block_device.FakeDbBlockDeviceDict(
+ {'volume_id': _fake_id('a'),
+ 'source_type': 'snapshot',
+ 'destination_type': 'volume',
+ 'volume_size': 1,
+ 'device_name': 'vda',
+ 'snapshot_id': 1,
+ 'boot_index': 0,
+ 'delete_on_termination': False,
+ 'no_device': None})]
+
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ fake_block_device_mapping_get_all_by_instance)
+
+ instance = fakes.fake_instance_get(image_ref='',
+ vm_state=vm_states.ACTIVE,
+ root_device_name='/dev/vda')
+ self.stubs.Set(db, 'instance_get_by_uuid', instance)
+
+ fake_metadata = {'test_key1': 'test_value1',
+ 'test_key2': 'test_value2'}
+ volume = dict(id=_fake_id('a'),
+ size=1,
+ host='fake',
+ display_description='fake',
+ volume_image_metadata=fake_metadata)
+ snapshot = dict(id=_fake_id('d'))
+ self.mox.StubOutWithMock(self.controller.compute_api, 'volume_api')
+ volume_api = self.controller.compute_api.volume_api
+ volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume)
+ volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume)
+ volume_api.create_snapshot_force(mox.IgnoreArg(), volume['id'],
+ mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(snapshot)
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+
+ self.mox.ReplayAll()
+ response = self.controller._action_create_image(req, FAKE_UUID, body)
+ location = response.headers['Location']
+ image_id = location.replace('http://localhost:9292/images/', '')
+ image = image_service.show(None, image_id)
+
+ properties = image['properties']
+ self.assertEqual(properties['test_key1'], 'test_value1')
+ self.assertEqual(properties['test_key2'], 'test_value2')
+ if extra_metadata:
+ for key, val in extra_metadata.items():
+ self.assertEqual(properties[key], val)
+
+ def test_create_vol_backed_img_with_meta_from_vol_without_extra_meta(self):
+ self._test_create_volume_backed_image_with_metadata_from_volume()
+
+ def test_create_vol_backed_img_with_meta_from_vol_with_extra_meta(self):
+ self._test_create_volume_backed_image_with_metadata_from_volume(
+ extra_metadata={'a': 'b'})
+
+ def test_create_image_snapshots_disabled(self):
+ """Don't permit a snapshot if the allow_instance_snapshots flag is
+ False
+ """
+ self.flags(allow_instance_snapshots=False)
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ },
+ }
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_with_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ 'metadata': {'key': 'asdf'},
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ response = self.controller._action_create_image(req, FAKE_UUID, body)
+
+ location = response.headers['Location']
+ self.assertEqual(glance.generate_image_url('123'), location)
+
+ def test_create_image_with_too_much_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ 'metadata': {},
+ },
+ }
+ for num in range(CONF.quota_metadata_items + 1):
+ body['createImage']['metadata']['foo%i' % num] = "bar"
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_no_name(self):
+ body = {
+ 'createImage': {},
+ }
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_blank_name(self):
+ body = {
+ 'createImage': {
+ 'name': '',
+ }
+ }
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_bad_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'geoff',
+ 'metadata': 'henry',
+ },
+ }
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_raises_conflict_on_invalid_state(self):
+ def snapshot(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+ self.stubs.Set(compute_api.API, 'snapshot', snapshot)
+
+ body = {
+ "createImage": {
+ "name": "test_snapshot",
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_external_events.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_external_events.py
new file mode 100644
index 0000000000..e9bd4538a0
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_external_events.py
@@ -0,0 +1,140 @@
+# Copyright 2014 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import mock
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute.plugins.v3 import server_external_events
+from nova import context
+from nova import exception
+from nova import objects
+from nova import test
+
+fake_instances = {
+ '00000000-0000-0000-0000-000000000001': objects.Instance(
+ uuid='00000000-0000-0000-0000-000000000001', host='host1'),
+ '00000000-0000-0000-0000-000000000002': objects.Instance(
+ uuid='00000000-0000-0000-0000-000000000002', host='host1'),
+ '00000000-0000-0000-0000-000000000003': objects.Instance(
+ uuid='00000000-0000-0000-0000-000000000003', host='host2'),
+}
+fake_instance_uuids = sorted(fake_instances.keys())
+MISSING_UUID = '00000000-0000-0000-0000-000000000004'
+
+
+@classmethod
+def fake_get_by_uuid(cls, context, uuid):
+ try:
+ return fake_instances[uuid]
+ except KeyError:
+ raise exception.InstanceNotFound(instance_id=uuid)
+
+
+@mock.patch('nova.objects.instance.Instance.get_by_uuid', fake_get_by_uuid)
+class ServerExternalEventsTest(test.NoDBTestCase):
+ def setUp(self):
+ super(ServerExternalEventsTest, self).setUp()
+ self.api = server_external_events.ServerExternalEventsController()
+ self.context = context.get_admin_context()
+ self.default_body = {
+ 'events': [
+ {'name': 'network-vif-plugged',
+ 'tag': 'foo',
+ 'status': 'completed',
+ 'server_uuid': fake_instance_uuids[0]},
+ {'name': 'network-changed',
+ 'status': 'completed',
+ 'server_uuid': fake_instance_uuids[1]},
+ ]
+ }
+
+ def _create_req(self, body):
+ req = webob.Request.blank('/v2/fake/os-server-external-events')
+ req.method = 'POST'
+ req.headers['content-type'] = 'application/json'
+ req.environ['nova.context'] = self.context
+ req.body = jsonutils.dumps(body)
+ return req
+
+ def _assert_call(self, req, body, expected_uuids, expected_events):
+ with mock.patch.object(self.api.compute_api,
+ 'external_instance_event') as api_method:
+ response = self.api.create(req, body)
+
+ result = response.obj
+ code = response._code
+
+ self.assertEqual(1, api_method.call_count)
+ for inst in api_method.call_args_list[0][0][1]:
+ expected_uuids.remove(inst.uuid)
+ self.assertEqual([], expected_uuids)
+ for event in api_method.call_args_list[0][0][2]:
+ expected_events.remove(event.name)
+ self.assertEqual([], expected_events)
+ return result, code
+
+ def test_create(self):
+ req = self._create_req(self.default_body)
+ result, code = self._assert_call(req, self.default_body,
+ fake_instance_uuids[:2],
+ ['network-vif-plugged',
+ 'network-changed'])
+ self.assertEqual(self.default_body, result)
+ self.assertEqual(200, code)
+
+ def test_create_one_bad_instance(self):
+ body = self.default_body
+ body['events'][1]['server_uuid'] = MISSING_UUID
+ req = self._create_req(body)
+ result, code = self._assert_call(req, body, [fake_instance_uuids[0]],
+ ['network-vif-plugged'])
+ self.assertEqual('failed', result['events'][1]['status'])
+ self.assertEqual(200, result['events'][0]['code'])
+ self.assertEqual(404, result['events'][1]['code'])
+ self.assertEqual(207, code)
+
+ def test_create_no_good_instances(self):
+ body = self.default_body
+ body['events'][0]['server_uuid'] = MISSING_UUID
+ body['events'][1]['server_uuid'] = MISSING_UUID
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.api.create, req, body)
+
+ def test_create_bad_status(self):
+ body = self.default_body
+ body['events'][1]['status'] = 'foo'
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.api.create, req, body)
+
+ def test_create_extra_gorp(self):
+ body = self.default_body
+ body['events'][0]['foobar'] = 'bad stuff'
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.api.create, req, body)
+
+ def test_create_bad_events(self):
+ body = {'events': 'foo'}
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.api.create, req, body)
+
+ def test_create_bad_body(self):
+ body = {'foo': 'bar'}
+ req = self._create_req(body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.api.create, req, body)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_password.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_password.py
new file mode 100644
index 0000000000..20a8c1e0a1
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_password.py
@@ -0,0 +1,80 @@
+# Copyright 2012 Nebula, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.metadata import password
+from nova import compute
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+
+CONF = cfg.CONF
+
+
+class ServerPasswordTest(test.TestCase):
+ content_type = 'application/json'
+
+ def setUp(self):
+ super(ServerPasswordTest, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(
+ compute.api.API, 'get',
+ lambda self, ctxt, *a, **kw:
+ fake_instance.fake_instance_obj(
+ ctxt,
+ system_metadata={},
+ expected_attrs=['system_metadata']))
+ self.password = 'fakepass'
+
+ def fake_extract_password(instance):
+ return self.password
+
+ def fake_convert_password(context, password):
+ self.password = password
+ return {}
+
+ self.stubs.Set(password, 'extract_password', fake_extract_password)
+ self.stubs.Set(password, 'convert_password', fake_convert_password)
+
+ def _make_request(self, url, method='GET'):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ req.method = method
+ res = req.get_response(
+ fakes.wsgi_app_v21(init_only=('servers', 'os-server-password')))
+ return res
+
+ def _get_pass(self, body):
+ return jsonutils.loads(body).get('password')
+
+ def test_get_password(self):
+ url = '/v2/fake/servers/fake/os-server-password'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(self._get_pass(res.body), 'fakepass')
+
+ def test_reset_password(self):
+ url = '/v2/fake/servers/fake/os-server-password'
+ res = self._make_request(url, 'DELETE')
+ self.assertEqual(res.status_int, 204)
+
+ res = self._make_request(url)
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(self._get_pass(res.body), '')
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py
new file mode 100644
index 0000000000..6eb92902fe
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py
@@ -0,0 +1,3353 @@
+# Copyright 2010-2011 OpenStack Foundation
+# Copyright 2011 Piston Cloud Computing, Inc.
+# 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 base64
+import contextlib
+import copy
+import datetime
+import uuid
+
+import iso8601
+import mock
+import mox
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+import six.moves.urllib.parse as urlparse
+import testtools
+import webob
+
+from nova.api.openstack import compute
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import disk_config
+from nova.api.openstack.compute.plugins.v3 import ips
+from nova.api.openstack.compute.plugins.v3 import keypairs
+from nova.api.openstack.compute.plugins.v3 import servers
+from nova.api.openstack.compute.schemas.v3 import disk_config as \
+ disk_config_schema
+from nova.api.openstack.compute.schemas.v3 import servers as servers_schema
+from nova.api.openstack.compute import views
+from nova.api.openstack import extensions
+from nova.compute import api as compute_api
+from nova.compute import delete_types
+from nova.compute import flavors
+from nova.compute import task_states
+from nova.compute import vm_states
+from nova import context
+from nova import db
+from nova.db.sqlalchemy import models
+from nova import exception
+from nova.i18n import _
+from nova.image import glance
+from nova.network import manager
+from nova.network.neutronv2 import api as neutron_api
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova.openstack.common import policy as common_policy
+from nova import policy
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+from nova.tests.unit import fake_network
+from nova.tests.unit.image import fake
+from nova.tests.unit import matchers
+from nova import utils as nova_utils
+
+CONF = cfg.CONF
+CONF.import_opt('password_length', 'nova.utils')
+
+FAKE_UUID = fakes.FAKE_UUID
+
+INSTANCE_IDS = {FAKE_UUID: 1}
+FIELDS = instance_obj.INSTANCE_DEFAULT_FIELDS
+
+
+def fake_gen_uuid():
+ return FAKE_UUID
+
+
+def return_servers_empty(context, *args, **kwargs):
+ return []
+
+
+def instance_update_and_get_original(context, instance_uuid, values,
+ update_cells=True,
+ columns_to_join=None,
+ ):
+ inst = fakes.stub_instance(INSTANCE_IDS.get(instance_uuid),
+ name=values.get('display_name'))
+ inst = dict(inst, **values)
+ return (inst, inst)
+
+
+def instance_update(context, instance_uuid, values, update_cells=True):
+ inst = fakes.stub_instance(INSTANCE_IDS.get(instance_uuid),
+ name=values.get('display_name'))
+ inst = dict(inst, **values)
+ return inst
+
+
+def fake_compute_api(cls, req, id):
+ return True
+
+
+def fake_start_stop_not_ready(self, context, instance):
+ raise exception.InstanceNotReady(instance_id=instance["uuid"])
+
+
+def fake_start_stop_invalid_state(self, context, instance):
+ raise exception.InstanceInvalidState(
+ instance_uuid=instance['uuid'], attr='fake_attr',
+ method='fake_method', state='fake_state')
+
+
+def fake_instance_get_by_uuid_not_found(context, uuid,
+ columns_to_join, use_slave=False):
+ raise exception.InstanceNotFound(instance_id=uuid)
+
+
+class MockSetAdminPassword(object):
+ def __init__(self):
+ self.instance_id = None
+ self.password = None
+
+ def __call__(self, context, instance_id, password):
+ self.instance_id = instance_id
+ self.password = password
+
+
+class Base64ValidationTest(test.TestCase):
+ def setUp(self):
+ super(Base64ValidationTest, self).setUp()
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+
+ def test_decode_base64(self):
+ value = "A random string"
+ result = self.controller._decode_base64(base64.b64encode(value))
+ self.assertEqual(result, value)
+
+ def test_decode_base64_binary(self):
+ value = "\x00\x12\x75\x99"
+ result = self.controller._decode_base64(base64.b64encode(value))
+ self.assertEqual(result, value)
+
+ def test_decode_base64_whitespace(self):
+ value = "A random string"
+ encoded = base64.b64encode(value)
+ white = "\n \n%s\t%s\n" % (encoded[:2], encoded[2:])
+ result = self.controller._decode_base64(white)
+ self.assertEqual(result, value)
+
+ def test_decode_base64_invalid(self):
+ invalid = "A random string"
+ result = self.controller._decode_base64(invalid)
+ self.assertIsNone(result)
+
+ def test_decode_base64_illegal_bytes(self):
+ value = "A random string"
+ encoded = base64.b64encode(value)
+ white = ">\x01%s*%s()" % (encoded[:2], encoded[2:])
+ result = self.controller._decode_base64(white)
+ self.assertIsNone(result)
+
+
+class NeutronV2Subclass(neutron_api.API):
+ """Used to ensure that API handles subclasses properly."""
+ pass
+
+
+class ControllerTest(test.TestCase):
+
+ def setUp(self):
+ super(ControllerTest, self).setUp()
+ self.flags(verbose=True, use_ipv6=False)
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ fake.stub_out_image_service(self.stubs)
+ return_server = fakes.fake_instance_get()
+ return_servers = fakes.fake_instance_get_all_by_filters()
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ return_servers)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ return_server)
+ self.stubs.Set(db, 'instance_update_and_get_original',
+ instance_update_and_get_original)
+
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+ self.ips_controller = ips.IPsController()
+ policy.reset()
+ policy.init()
+ fake_network.stub_out_nw_api_get_instance_nw_info(self.stubs)
+
+
+class ServersControllerTest(ControllerTest):
+
+ def setUp(self):
+ super(ServersControllerTest, self).setUp()
+ CONF.set_override('host', 'localhost', group='glance')
+
+ def test_requested_networks_prefix(self):
+ uuid = 'br-00000000-0000-0000-0000-000000000000'
+ requested_networks = [{'uuid': uuid}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertIn((uuid, None), res.as_tuples())
+
+ def test_requested_networks_neutronv2_enabled_with_port(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'port': port}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(None, None, port, None)], res.as_tuples())
+
+ def test_requested_networks_neutronv2_enabled_with_network(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ requested_networks = [{'uuid': network}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(network, None, None, None)], res.as_tuples())
+
+ def test_requested_networks_neutronv2_enabled_with_network_and_port(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network, 'port': port}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(None, None, port, None)], res.as_tuples())
+
+ def test_requested_networks_neutronv2_enabled_conflict_on_fixed_ip(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ addr = '10.0.0.1'
+ requested_networks = [{'uuid': network,
+ 'fixed_ip': addr,
+ 'port': port}]
+ self.assertRaises(
+ webob.exc.HTTPBadRequest,
+ self.controller._get_requested_networks,
+ requested_networks)
+
+ def test_requested_networks_neutronv2_disabled_with_port(self):
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'port': port}]
+ self.assertRaises(
+ webob.exc.HTTPBadRequest,
+ self.controller._get_requested_networks,
+ requested_networks)
+
+ def test_requested_networks_api_enabled_with_v2_subclass(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network, 'port': port}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(None, None, port, None)], res.as_tuples())
+
+ def test_requested_networks_neutronv2_subclass_with_port(self):
+ cls = ('nova.tests.unit.api.openstack.compute' +
+ '.test_servers.NeutronV2Subclass')
+ self.flags(network_api_class=cls)
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'port': port}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(None, None, port, None)], res.as_tuples())
+
+ def test_get_server_by_uuid(self):
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID)
+ res_dict = self.controller.show(req, FAKE_UUID)
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+
+ def test_get_server_joins_pci_devices(self):
+ self.expected_attrs = None
+
+ def fake_get(_self, *args, **kwargs):
+ self.expected_attrs = kwargs['expected_attrs']
+ ctxt = context.RequestContext('fake', 'fake')
+ return fake_instance.fake_instance_obj(ctxt)
+
+ self.stubs.Set(compute_api.API, 'get', fake_get)
+
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID)
+ self.controller.show(req, FAKE_UUID)
+
+ self.assertIn('pci_devices', self.expected_attrs)
+
+ def test_unique_host_id(self):
+ """Create two servers with the same host and different
+ project_ids and check that the host_id's are unique.
+ """
+ def return_instance_with_host(self, *args, **kwargs):
+ project_id = str(uuid.uuid4())
+ return fakes.stub_instance(id=1, uuid=FAKE_UUID,
+ project_id=project_id,
+ host='fake_host')
+
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ return_instance_with_host)
+ self.stubs.Set(db, 'instance_get',
+ return_instance_with_host)
+
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID)
+ server1 = self.controller.show(req, FAKE_UUID)
+ server2 = self.controller.show(req, FAKE_UUID)
+
+ self.assertNotEqual(server1['server']['hostId'],
+ server2['server']['hostId'])
+
+ def _get_server_data_dict(self, uuid, image_bookmark, flavor_bookmark,
+ status="ACTIVE", progress=100):
+ return {
+ "server": {
+ "id": uuid,
+ "user_id": "fake_user",
+ "tenant_id": "fake_project",
+ "updated": "2010-11-11T11:00:00Z",
+ "created": "2010-10-10T12:00:00Z",
+ "progress": progress,
+ "name": "server1",
+ "status": status,
+ "hostId": '',
+ "image": {
+ "id": "10",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": image_bookmark,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": flavor_bookmark,
+ },
+ ],
+ },
+ "addresses": {
+ 'test1': [
+ {'version': 4, 'addr': '192.168.1.100',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ {'version': 6, 'addr': '2001:db8:0:1::1',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}
+ ]
+ },
+ "metadata": {
+ "seq": "1",
+ },
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/servers/%s" % uuid,
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/servers/%s" % uuid,
+ },
+ ],
+ }
+ }
+
+ def test_get_server_by_id(self):
+ self.flags(use_ipv6=True)
+ image_bookmark = "http://localhost/images/10"
+ flavor_bookmark = "http://localhost/flavors/1"
+
+ uuid = FAKE_UUID
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % uuid)
+ res_dict = self.controller.show(req, uuid)
+
+ expected_server = self._get_server_data_dict(uuid,
+ image_bookmark,
+ flavor_bookmark,
+ status="BUILD",
+ progress=0)
+
+ self.assertThat(res_dict, matchers.DictMatches(expected_server))
+
+ def test_get_server_with_active_status_by_id(self):
+ image_bookmark = "http://localhost/images/10"
+ flavor_bookmark = "http://localhost/flavors/1"
+
+ new_return_server = fakes.fake_instance_get(
+ vm_state=vm_states.ACTIVE, progress=100)
+ self.stubs.Set(db, 'instance_get_by_uuid', new_return_server)
+
+ uuid = FAKE_UUID
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % uuid)
+ res_dict = self.controller.show(req, uuid)
+ expected_server = self._get_server_data_dict(uuid,
+ image_bookmark,
+ flavor_bookmark)
+ self.assertThat(res_dict, matchers.DictMatches(expected_server))
+
+ def test_get_server_with_id_image_ref_by_id(self):
+ image_ref = "10"
+ image_bookmark = "http://localhost/images/10"
+ flavor_id = "1"
+ flavor_bookmark = "http://localhost/flavors/1"
+
+ new_return_server = fakes.fake_instance_get(
+ vm_state=vm_states.ACTIVE, image_ref=image_ref,
+ flavor_id=flavor_id, progress=100)
+ self.stubs.Set(db, 'instance_get_by_uuid', new_return_server)
+
+ uuid = FAKE_UUID
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % uuid)
+ res_dict = self.controller.show(req, uuid)
+ expected_server = self._get_server_data_dict(uuid,
+ image_bookmark,
+ flavor_bookmark)
+
+ self.assertThat(res_dict, matchers.DictMatches(expected_server))
+
+ def test_get_server_addresses_from_cache(self):
+ pub0 = ('172.19.0.1', '172.19.0.2',)
+ pub1 = ('1.2.3.4',)
+ pub2 = ('b33f::fdee:ddff:fecc:bbaa',)
+ priv0 = ('192.168.0.3', '192.168.0.4',)
+
+ def _ip(ip):
+ return {'address': ip, 'type': 'fixed'}
+
+ nw_cache = [
+ {'address': 'aa:aa:aa:aa:aa:aa',
+ 'id': 1,
+ 'network': {'bridge': 'br0',
+ 'id': 1,
+ 'label': 'public',
+ 'subnets': [{'cidr': '172.19.0.0/24',
+ 'ips': [_ip(ip) for ip in pub0]},
+ {'cidr': '1.2.3.0/16',
+ 'ips': [_ip(ip) for ip in pub1]},
+ {'cidr': 'b33f::/64',
+ 'ips': [_ip(ip) for ip in pub2]}]}},
+ {'address': 'bb:bb:bb:bb:bb:bb',
+ 'id': 2,
+ 'network': {'bridge': 'br1',
+ 'id': 2,
+ 'label': 'private',
+ 'subnets': [{'cidr': '192.168.0.0/24',
+ 'ips': [_ip(ip) for ip in priv0]}]}}]
+
+ return_server = fakes.fake_instance_get(nw_cache=nw_cache)
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ req = fakes.HTTPRequestV3.blank('/servers/%s/ips' % FAKE_UUID)
+ res_dict = self.ips_controller.index(req, FAKE_UUID)
+
+ expected = {
+ 'addresses': {
+ 'private': [
+ {'version': 4, 'addr': '192.168.0.3',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'bb:bb:bb:bb:bb:bb'},
+ {'version': 4, 'addr': '192.168.0.4',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'bb:bb:bb:bb:bb:bb'},
+ ],
+ 'public': [
+ {'version': 4, 'addr': '172.19.0.1',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ {'version': 4, 'addr': '172.19.0.2',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ {'version': 4, 'addr': '1.2.3.4',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ ],
+ },
+ }
+ self.assertThat(res_dict, matchers.DictMatches(expected))
+
+ def test_get_server_addresses_nonexistent_network(self):
+ url = '/v3/servers/%s/ips/network_0' % FAKE_UUID
+ req = fakes.HTTPRequestV3.blank(url)
+ self.assertRaises(webob.exc.HTTPNotFound, self.ips_controller.show,
+ req, FAKE_UUID, 'network_0')
+
+ def test_get_server_addresses_nonexistent_server(self):
+ def fake_instance_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+
+ server_id = str(uuid.uuid4())
+ req = fakes.HTTPRequestV3.blank('/servers/%s/ips' % server_id)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.ips_controller.index, req, server_id)
+
+ def test_get_server_list_empty(self):
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ return_servers_empty)
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ res_dict = self.controller.index(req)
+
+ num_servers = len(res_dict['servers'])
+ self.assertEqual(0, num_servers)
+
+ def test_get_server_list_with_reservation_id(self):
+ req = fakes.HTTPRequestV3.blank('/servers?reservation_id=foo')
+ res_dict = self.controller.index(req)
+
+ i = 0
+ for s in res_dict['servers']:
+ self.assertEqual(s.get('name'), 'server%d' % (i + 1))
+ i += 1
+
+ def test_get_server_list_with_reservation_id_empty(self):
+ req = fakes.HTTPRequestV3.blank('/servers/detail?'
+ 'reservation_id=foo')
+ res_dict = self.controller.detail(req)
+
+ i = 0
+ for s in res_dict['servers']:
+ self.assertEqual(s.get('name'), 'server%d' % (i + 1))
+ i += 1
+
+ def test_get_server_list_with_reservation_id_details(self):
+ req = fakes.HTTPRequestV3.blank('/servers/detail?'
+ 'reservation_id=foo')
+ res_dict = self.controller.detail(req)
+
+ i = 0
+ for s in res_dict['servers']:
+ self.assertEqual(s.get('name'), 'server%d' % (i + 1))
+ i += 1
+
+ def test_get_server_list(self):
+ req = fakes.HTTPRequestV3.blank('/servers')
+ res_dict = self.controller.index(req)
+
+ self.assertEqual(len(res_dict['servers']), 5)
+ for i, s in enumerate(res_dict['servers']):
+ self.assertEqual(s['id'], fakes.get_fake_uuid(i))
+ self.assertEqual(s['name'], 'server%d' % (i + 1))
+ self.assertIsNone(s.get('image', None))
+
+ expected_links = [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/servers/%s" % s['id'],
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/servers/%s" % s['id'],
+ },
+ ]
+
+ self.assertEqual(s['links'], expected_links)
+
+ def test_get_servers_with_limit(self):
+ req = fakes.HTTPRequestV3.blank('/servers?limit=3')
+ res_dict = self.controller.index(req)
+
+ servers = res_dict['servers']
+ self.assertEqual([s['id'] for s in servers],
+ [fakes.get_fake_uuid(i) for i in xrange(len(servers))])
+
+ servers_links = res_dict['servers_links']
+ self.assertEqual(servers_links[0]['rel'], 'next')
+ href_parts = urlparse.urlparse(servers_links[0]['href'])
+ self.assertEqual('/v3/servers', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ expected_params = {'limit': ['3'],
+ 'marker': [fakes.get_fake_uuid(2)]}
+ self.assertThat(params, matchers.DictMatches(expected_params))
+
+ def test_get_servers_with_limit_bad_value(self):
+ req = fakes.HTTPRequestV3.blank('/servers?limit=aaa')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_server_details_empty(self):
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ return_servers_empty)
+
+ req = fakes.HTTPRequestV3.blank('/servers/detail')
+ res_dict = self.controller.detail(req)
+
+ num_servers = len(res_dict['servers'])
+ self.assertEqual(0, num_servers)
+
+ def test_get_server_details_with_limit(self):
+ req = fakes.HTTPRequestV3.blank('/servers/detail?limit=3')
+ res = self.controller.detail(req)
+
+ servers = res['servers']
+ self.assertEqual([s['id'] for s in servers],
+ [fakes.get_fake_uuid(i) for i in xrange(len(servers))])
+
+ servers_links = res['servers_links']
+ self.assertEqual(servers_links[0]['rel'], 'next')
+
+ href_parts = urlparse.urlparse(servers_links[0]['href'])
+ self.assertEqual('/v3/servers/detail', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ expected = {'limit': ['3'], 'marker': [fakes.get_fake_uuid(2)]}
+ self.assertThat(params, matchers.DictMatches(expected))
+
+ def test_get_server_details_with_limit_bad_value(self):
+ req = fakes.HTTPRequestV3.blank('/servers/detail?limit=aaa')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.detail, req)
+
+ def test_get_server_details_with_limit_and_other_params(self):
+ req = fakes.HTTPRequestV3.blank('/servers/detail'
+ '?limit=3&blah=2:t')
+ res = self.controller.detail(req)
+
+ servers = res['servers']
+ self.assertEqual([s['id'] for s in servers],
+ [fakes.get_fake_uuid(i) for i in xrange(len(servers))])
+
+ servers_links = res['servers_links']
+ self.assertEqual(servers_links[0]['rel'], 'next')
+
+ href_parts = urlparse.urlparse(servers_links[0]['href'])
+ self.assertEqual('/v3/servers/detail', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ expected = {'limit': ['3'], 'blah': ['2:t'],
+ 'marker': [fakes.get_fake_uuid(2)]}
+ self.assertThat(params, matchers.DictMatches(expected))
+
+ def test_get_servers_with_too_big_limit(self):
+ req = fakes.HTTPRequestV3.blank('/servers?limit=30')
+ res_dict = self.controller.index(req)
+ self.assertNotIn('servers_links', res_dict)
+
+ def test_get_servers_with_bad_limit(self):
+ req = fakes.HTTPRequestV3.blank('/servers?limit=asdf')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_servers_with_marker(self):
+ url = '/v3/servers?marker=%s' % fakes.get_fake_uuid(2)
+ req = fakes.HTTPRequestV3.blank(url)
+ servers = self.controller.index(req)['servers']
+ self.assertEqual([s['name'] for s in servers], ["server4", "server5"])
+
+ def test_get_servers_with_limit_and_marker(self):
+ url = '/v3/servers?limit=2&marker=%s' % fakes.get_fake_uuid(1)
+ req = fakes.HTTPRequestV3.blank(url)
+ servers = self.controller.index(req)['servers']
+ self.assertEqual([s['name'] for s in servers], ['server3', 'server4'])
+
+ def test_get_servers_with_bad_marker(self):
+ req = fakes.HTTPRequestV3.blank('/servers?limit=2&marker=asdf')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_servers_with_bad_option(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?unknownoption=whee')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_image(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('image', search_opts)
+ self.assertEqual(search_opts['image'], '12345')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?image=12345')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_tenant_id_filter_converts_to_project_id_for_admin(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False,
+ expected_attrs=None):
+ self.assertIsNotNone(filters)
+ self.assertEqual(filters['project_id'], 'newfake')
+ self.assertFalse(filters.get('tenant_id'))
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers'
+ '?all_tenants=1&tenant_id=newfake',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_tenant_id_filter_no_admin_context(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False,
+ expected_attrs=None):
+ self.assertNotEqual(filters, None)
+ self.assertEqual(filters['project_id'], 'fake')
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?tenant_id=newfake')
+ res = self.controller.index(req)
+ self.assertIn('servers', res)
+
+ def test_tenant_id_filter_implies_all_tenants(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False,
+ expected_attrs=None):
+ self.assertNotEqual(filters, None)
+ # The project_id assertion checks that the project_id
+ # filter is set to that specified in the request url and
+ # not that of the context, verifying that the all_tenants
+ # flag was enabled
+ self.assertEqual(filters['project_id'], 'newfake')
+ self.assertFalse(filters.get('tenant_id'))
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?tenant_id=newfake',
+ use_admin_context=True)
+ res = self.controller.index(req)
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_normal(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False,
+ expected_attrs=None):
+ self.assertNotIn('project_id', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?all_tenants',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_one(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False,
+ expected_attrs=None):
+ self.assertNotIn('project_id', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?all_tenants=1',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_zero(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False,
+ expected_attrs=None):
+ self.assertNotIn('all_tenants', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?all_tenants=0',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_false(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False,
+ expected_attrs=None):
+ self.assertNotIn('all_tenants', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?all_tenants=false',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_invalid(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None,
+ expected_attrs=None):
+ self.assertNotIn('all_tenants', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?all_tenants=xxx',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_admin_restricted_tenant(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False,
+ expected_attrs=None):
+ self.assertIsNotNone(filters)
+ self.assertEqual(filters['project_id'], 'fake')
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_pass_policy(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False,
+ expected_attrs=None):
+ self.assertIsNotNone(filters)
+ self.assertNotIn('project_id', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ rules = {
+ "compute:get_all_tenants":
+ common_policy.parse_rule("project_id:fake"),
+ "compute:get_all":
+ common_policy.parse_rule("project_id:fake"),
+ }
+
+ policy.set_rules(rules)
+
+ req = fakes.HTTPRequestV3.blank('/servers?all_tenants=1')
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_fail_policy(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None):
+ self.assertIsNotNone(filters)
+ return [fakes.stub_instance(100)]
+
+ rules = {
+ "compute:get_all_tenants":
+ common_policy.parse_rule("project_id:non_fake"),
+ "compute:get_all":
+ common_policy.parse_rule("project_id:fake"),
+ }
+
+ policy.set_rules(rules)
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?all_tenants=1')
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.index, req)
+
+ def test_get_servers_allows_flavor(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('flavor', search_opts)
+ # flavor is an integer ID
+ self.assertEqual(search_opts['flavor'], '12345')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?flavor=12345')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_with_bad_flavor(self):
+ req = fakes.HTTPRequestV3.blank('/servers?flavor=abcde')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 0)
+
+ def test_get_server_details_with_bad_flavor(self):
+ req = fakes.HTTPRequestV3.blank('/servers?flavor=abcde')
+ servers = self.controller.detail(req)['servers']
+
+ self.assertThat(servers, testtools.matchers.HasLength(0))
+
+ def test_get_servers_allows_status(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('vm_state', search_opts)
+ self.assertEqual(search_opts['vm_state'], [vm_states.ACTIVE])
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?status=active')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_task_status(self):
+ server_uuid = str(uuid.uuid4())
+ task_state = task_states.REBOOTING
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('task_state', search_opts)
+ self.assertEqual([task_states.REBOOT_PENDING,
+ task_states.REBOOT_STARTED,
+ task_states.REBOOTING],
+ search_opts['task_state'])
+ db_list = [fakes.stub_instance(100, uuid=server_uuid,
+ task_state=task_state)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?status=reboot')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_resize_status(self):
+ # Test when resize status, it maps list of vm states.
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIn('vm_state', search_opts)
+ self.assertEqual(search_opts['vm_state'],
+ [vm_states.ACTIVE, vm_states.STOPPED])
+
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?status=resize')
+
+ servers = self.controller.detail(req)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_invalid_status(self):
+ # Test getting servers by invalid status.
+ req = fakes.HTTPRequestV3.blank('/servers?status=baloney',
+ use_admin_context=False)
+ servers = self.controller.index(req)['servers']
+ self.assertEqual(len(servers), 0)
+
+ def test_get_servers_deleted_status_as_user(self):
+ req = fakes.HTTPRequestV3.blank('/servers?status=deleted',
+ use_admin_context=False)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.detail, req)
+
+ def test_get_servers_deleted_status_as_admin(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIn('vm_state', search_opts)
+ self.assertEqual(search_opts['vm_state'], ['deleted'])
+
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?status=deleted',
+ use_admin_context=True)
+
+ servers = self.controller.detail(req)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_name(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('name', search_opts)
+ self.assertEqual(search_opts['name'], 'whee.*')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?name=whee.*')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ @mock.patch.object(compute_api.API, 'get_all')
+ def test_get_servers_flavor_not_found(self, get_all_mock):
+ get_all_mock.side_effect = exception.FlavorNotFound(flavor_id=1)
+
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers?status=active&flavor=abc')
+ servers = self.controller.index(req)['servers']
+ self.assertEqual(0, len(servers))
+
+ def test_get_servers_allows_changes_since(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('changes-since', search_opts)
+ changes_since = datetime.datetime(2011, 1, 24, 17, 8, 1,
+ tzinfo=iso8601.iso8601.UTC)
+ self.assertEqual(search_opts['changes-since'], changes_since)
+ self.assertNotIn('deleted', search_opts)
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ params = 'changes-since=2011-01-24T17:08:01Z'
+ req = fakes.HTTPRequestV3.blank('/servers?%s' % params)
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_changes_since_bad_value(self):
+ params = 'changes-since=asdf'
+ req = fakes.HTTPRequestV3.blank('/servers?%s' % params)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req)
+
+ def test_get_servers_admin_filters_as_user(self):
+ """Test getting servers by admin-only or unknown options when
+ context is not admin. Make sure the admin and unknown options
+ are stripped before they get to compute_api.get_all()
+ """
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ # Allowed by user
+ self.assertIn('name', search_opts)
+ self.assertIn('ip', search_opts)
+ # OSAPI converts status to vm_state
+ self.assertIn('vm_state', search_opts)
+ # Allowed only by admins with admin API on
+ self.assertNotIn('unknown_option', search_opts)
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = fakes.HTTPRequest.blank('/servers?%s' % query_str)
+ res = self.controller.index(req)
+
+ servers = res['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_admin_options_as_admin(self):
+ """Test getting servers by admin-only or unknown options when
+ context is admin. All options should be passed
+ """
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ # Allowed by user
+ self.assertIn('name', search_opts)
+ # OSAPI converts status to vm_state
+ self.assertIn('vm_state', search_opts)
+ # Allowed only by admins with admin API on
+ self.assertIn('ip', search_opts)
+ self.assertIn('unknown_option', search_opts)
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = fakes.HTTPRequestV3.blank('/servers?%s' % query_str,
+ use_admin_context=True)
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_ip(self):
+ """Test getting servers by ip."""
+
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('ip', search_opts)
+ self.assertEqual(search_opts['ip'], '10\..*')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?ip=10\..*')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_admin_allows_ip6(self):
+ """Test getting servers by ip6 with admin_api enabled and
+ admin context
+ """
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('ip6', search_opts)
+ self.assertEqual(search_opts['ip6'], 'ffff.*')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers?ip6=ffff.*',
+ use_admin_context=True)
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_all_server_details(self):
+ expected_flavor = {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": 'http://localhost/flavors/1',
+ },
+ ],
+ }
+ expected_image = {
+ "id": "10",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": 'http://localhost/images/10',
+ },
+ ],
+ }
+ req = fakes.HTTPRequestV3.blank('/servers/detail')
+ res_dict = self.controller.detail(req)
+
+ for i, s in enumerate(res_dict['servers']):
+ self.assertEqual(s['id'], fakes.get_fake_uuid(i))
+ self.assertEqual(s['hostId'], '')
+ self.assertEqual(s['name'], 'server%d' % (i + 1))
+ self.assertEqual(s['image'], expected_image)
+ self.assertEqual(s['flavor'], expected_flavor)
+ self.assertEqual(s['status'], 'BUILD')
+ self.assertEqual(s['metadata']['seq'], str(i + 1))
+
+ def test_get_all_server_details_with_host(self):
+ """We want to make sure that if two instances are on the same host,
+ then they return the same hostId. If two instances are on different
+ hosts, they should return different hostIds. In this test,
+ there are 5 instances - 2 on one host and 3 on another.
+ """
+
+ def return_servers_with_host(context, *args, **kwargs):
+ return [fakes.stub_instance(i + 1, 'fake', 'fake', host=i % 2,
+ uuid=fakes.get_fake_uuid(i))
+ for i in xrange(5)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ return_servers_with_host)
+
+ req = fakes.HTTPRequestV3.blank('/servers/detail')
+ res_dict = self.controller.detail(req)
+
+ server_list = res_dict['servers']
+ host_ids = [server_list[0]['hostId'], server_list[1]['hostId']]
+ self.assertTrue(host_ids[0] and host_ids[1])
+ self.assertNotEqual(host_ids[0], host_ids[1])
+
+ for i, s in enumerate(server_list):
+ self.assertEqual(s['id'], fakes.get_fake_uuid(i))
+ self.assertEqual(s['hostId'], host_ids[i % 2])
+ self.assertEqual(s['name'], 'server%d' % (i + 1))
+
+ def test_get_servers_joins_pci_devices(self):
+ self.expected_attrs = None
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False,
+ expected_attrs=None):
+ self.expected_attrs = expected_attrs
+ return []
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequestV3.blank('/servers', use_admin_context=True)
+ self.assertIn('servers', self.controller.index(req))
+ self.assertIn('pci_devices', self.expected_attrs)
+
+
+class ServersControllerDeleteTest(ControllerTest):
+
+ def setUp(self):
+ super(ServersControllerDeleteTest, self).setUp()
+ self.server_delete_called = False
+
+ def instance_destroy_mock(*args, **kwargs):
+ self.server_delete_called = True
+ deleted_at = timeutils.utcnow()
+ return fake_instance.fake_db_instance(deleted_at=deleted_at)
+
+ self.stubs.Set(db, 'instance_destroy', instance_destroy_mock)
+
+ def _create_delete_request(self, uuid):
+ fakes.stub_out_instance_quota(self.stubs, 0, 10)
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % uuid)
+ req.method = 'DELETE'
+ return req
+
+ def _delete_server_instance(self, uuid=FAKE_UUID):
+ req = self._create_delete_request(uuid)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE))
+ self.controller.delete(req, uuid)
+
+ def test_delete_server_instance(self):
+ self._delete_server_instance()
+ self.assertTrue(self.server_delete_called)
+
+ def test_delete_server_instance_not_found(self):
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self._delete_server_instance,
+ uuid='non-existent-uuid')
+
+ def test_delete_server_instance_while_building(self):
+ req = self._create_delete_request(FAKE_UUID)
+ self.controller.delete(req, FAKE_UUID)
+
+ self.assertTrue(self.server_delete_called)
+
+ def test_delete_locked_server(self):
+ req = self._create_delete_request(FAKE_UUID)
+ self.stubs.Set(compute_api.API, delete_types.SOFT_DELETE,
+ fakes.fake_actions_to_locked_server)
+ self.stubs.Set(compute_api.API, delete_types.DELETE,
+ fakes.fake_actions_to_locked_server)
+
+ self.assertRaises(webob.exc.HTTPConflict, self.controller.delete,
+ req, FAKE_UUID)
+
+ def test_delete_server_instance_while_resize(self):
+ req = self._create_delete_request(FAKE_UUID)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.RESIZE_PREP))
+
+ self.controller.delete(req, FAKE_UUID)
+ # Delete shoud be allowed in any case, even during resizing,
+ # because it may get stuck.
+ self.assertTrue(self.server_delete_called)
+
+ def test_delete_server_instance_if_not_launched(self):
+ self.flags(reclaim_instance_interval=3600)
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID)
+ req.method = 'DELETE'
+
+ self.server_delete_called = False
+
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(launched_at=None))
+
+ def instance_destroy_mock(*args, **kwargs):
+ self.server_delete_called = True
+ deleted_at = timeutils.utcnow()
+ return fake_instance.fake_db_instance(deleted_at=deleted_at)
+ self.stubs.Set(db, 'instance_destroy', instance_destroy_mock)
+
+ self.controller.delete(req, FAKE_UUID)
+ # delete() should be called for instance which has never been active,
+ # even if reclaim_instance_interval has been set.
+ self.assertEqual(self.server_delete_called, True)
+
+
+class ServersControllerRebuildInstanceTest(ControllerTest):
+
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ image_href = 'http://localhost/v3/fake/images/%s' % image_uuid
+
+ def setUp(self):
+ super(ServersControllerRebuildInstanceTest, self).setUp()
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE))
+ self.body = {
+ 'rebuild': {
+ 'name': 'new_name',
+ 'imageRef': self.image_href,
+ 'metadata': {
+ 'open': 'stack',
+ },
+ },
+ }
+ self.req = fakes.HTTPRequest.blank('/fake/servers/a/action')
+ self.req.method = 'POST'
+ self.req.headers["content-type"] = "application/json"
+
+ def test_rebuild_instance_with_blank_metadata_key(self):
+ self.body['rebuild']['metadata'][''] = 'world'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_with_metadata_key_too_long(self):
+ self.body['rebuild']['metadata'][('a' * 260)] = 'world'
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_with_metadata_value_too_long(self):
+ self.body['rebuild']['metadata']['key1'] = ('a' * 260)
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_rebuild, self.req,
+ FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_with_metadata_value_not_string(self):
+ self.body['rebuild']['metadata']['key1'] = 1
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_rebuild, self.req,
+ FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_fails_when_min_ram_too_small(self):
+ # make min_ram larger than our instance ram size
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True,
+ status='active', properties={'key1': 'value1'},
+ min_ram="4096", min_disk="10")
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_get_image)
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_fails_when_min_disk_too_small(self):
+ # make min_disk larger than our instance disk size
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True,
+ status='active', properties={'key1': 'value1'},
+ min_ram="128", min_disk="100000")
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_get_image)
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild, self.req,
+ FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_image_too_large(self):
+ # make image size larger than our instance disk size
+ size = str(1000 * (1024 ** 3))
+
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True,
+ status='active', size=size)
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_get_image)
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_name_all_blank(self):
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True, status='active')
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_get_image)
+ self.body['rebuild']['name'] = ' '
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_with_deleted_image(self):
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True,
+ status='DELETED')
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_get_image)
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_onset_file_limit_over_quota(self):
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True, status='active')
+
+ with contextlib.nested(
+ mock.patch.object(fake._FakeImageService, 'show',
+ side_effect=fake_get_image),
+ mock.patch.object(self.controller.compute_api, 'rebuild',
+ side_effect=exception.OnsetFileLimitExceeded)
+ ) as (
+ show_mock, rebuild_mock
+ ):
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, body=self.body)
+
+ def test_start(self):
+ self.mox.StubOutWithMock(compute_api.API, 'start')
+ compute_api.API.start(mox.IgnoreArg(), mox.IgnoreArg())
+ self.mox.ReplayAll()
+
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(start="")
+ self.controller._start_server(req, FAKE_UUID, body)
+
+ def test_start_policy_failed(self):
+ rules = {
+ "compute:v3:servers:start":
+ common_policy.parse_rule("project_id:non_fake")
+ }
+ policy.set_rules(rules)
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(start="")
+ exc = self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._start_server,
+ req, FAKE_UUID, body)
+ self.assertIn("compute:v3:servers:start", exc.format_message())
+
+ def test_start_not_ready(self):
+ self.stubs.Set(compute_api.API, 'start', fake_start_stop_not_ready)
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._start_server, req, FAKE_UUID, body)
+
+ def test_start_locked_server(self):
+ self.stubs.Set(compute_api.API, 'start',
+ fakes.fake_actions_to_locked_server)
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._start_server, req, FAKE_UUID, body)
+
+ def test_start_invalid(self):
+ self.stubs.Set(compute_api.API, 'start', fake_start_stop_invalid_state)
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._start_server, req, FAKE_UUID, body)
+
+ def test_stop(self):
+ self.mox.StubOutWithMock(compute_api.API, 'stop')
+ compute_api.API.stop(mox.IgnoreArg(), mox.IgnoreArg())
+ self.mox.ReplayAll()
+
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(stop="")
+ self.controller._stop_server(req, FAKE_UUID, body)
+
+ def test_stop_policy_failed(self):
+ rules = {
+ "compute:v3:servers:stop":
+ common_policy.parse_rule("project_id:non_fake")
+ }
+ policy.set_rules(rules)
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(stop='')
+ exc = self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._stop_server,
+ req, FAKE_UUID, body)
+ self.assertIn("compute:v3:servers:stop", exc.format_message())
+
+ def test_stop_not_ready(self):
+ self.stubs.Set(compute_api.API, 'stop', fake_start_stop_not_ready)
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(stop="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._stop_server, req, FAKE_UUID, body)
+
+ def test_stop_locked_server(self):
+ self.stubs.Set(compute_api.API, 'stop',
+ fakes.fake_actions_to_locked_server)
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(stop="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._stop_server, req, FAKE_UUID, body)
+
+ def test_stop_invalid_state(self):
+ self.stubs.Set(compute_api.API, 'stop', fake_start_stop_invalid_state)
+ req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID)
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._stop_server, req, FAKE_UUID, body)
+
+ def test_start_with_bogus_id(self):
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fake_instance_get_by_uuid_not_found)
+ req = fakes.HTTPRequestV3.blank('/servers/test_inst/action')
+ body = dict(start="")
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._start_server, req, 'test_inst', body)
+
+ def test_stop_with_bogus_id(self):
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fake_instance_get_by_uuid_not_found)
+ req = fakes.HTTPRequestV3.blank('/servers/test_inst/action')
+ body = dict(stop="")
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._stop_server, req, 'test_inst', body)
+
+
+class ServersControllerUpdateTest(ControllerTest):
+
+ def _get_request(self, body=None, options=None):
+ if options:
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get(**options))
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID)
+ req.method = 'PUT'
+ req.content_type = 'application/json'
+ req.body = jsonutils.dumps(body)
+ return req
+
+ def test_update_server_all_attributes(self):
+ body = {'server': {
+ 'name': 'server_test',
+ }}
+ req = self._get_request(body, {'name': 'server_test'})
+ res_dict = self.controller.update(req, FAKE_UUID, body=body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['name'], 'server_test')
+
+ def test_update_server_name(self):
+ body = {'server': {'name': 'server_test'}}
+ req = self._get_request(body, {'name': 'server_test'})
+ res_dict = self.controller.update(req, FAKE_UUID, body=body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['name'], 'server_test')
+
+ def test_update_server_name_too_long(self):
+ body = {'server': {'name': 'x' * 256}}
+ req = self._get_request(body, {'name': 'server_test'})
+ self.assertRaises(exception.ValidationError, self.controller.update,
+ req, FAKE_UUID, body=body)
+
+ def test_update_server_name_all_blank_spaces(self):
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get(name='server_test'))
+ req = fakes.HTTPRequest.blank('/v3/servers/%s' % FAKE_UUID)
+ req.method = 'PUT'
+ req.content_type = 'application/json'
+ body = {'server': {'name': ' ' * 64}}
+ req.body = jsonutils.dumps(body)
+ self.assertRaises(exception.ValidationError, self.controller.update,
+ req, FAKE_UUID, body=body)
+
+ def test_update_server_admin_password_ignored(self):
+ inst_dict = dict(name='server_test', admin_password='bacon')
+ body = dict(server=inst_dict)
+
+ def server_update(context, id, params):
+ filtered_dict = {
+ 'display_name': 'server_test',
+ }
+ self.assertEqual(params, filtered_dict)
+ filtered_dict['uuid'] = id
+ return filtered_dict
+
+ self.stubs.Set(db, 'instance_update', server_update)
+ # FIXME (comstud)
+ # self.stubs.Set(db, 'instance_get',
+ # return_server_with_attributes(name='server_test'))
+
+ req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID)
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ req.body = jsonutils.dumps(body)
+ res_dict = self.controller.update(req, FAKE_UUID, body=body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['name'], 'server_test')
+
+ def test_update_server_not_found(self):
+ def fake_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(compute_api.API, 'get', fake_get)
+ body = {'server': {'name': 'server_test'}}
+ req = self._get_request(body)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
+ req, FAKE_UUID, body=body)
+
+ def test_update_server_not_found_on_update(self):
+ def fake_update(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(db, 'instance_update_and_get_original', fake_update)
+ body = {'server': {'name': 'server_test'}}
+ req = self._get_request(body)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
+ req, FAKE_UUID, body=body)
+
+ def test_update_server_policy_fail(self):
+ rule = {'compute:update': common_policy.parse_rule('role:admin')}
+ policy.set_rules(rule)
+ body = {'server': {'name': 'server_test'}}
+ req = self._get_request(body, {'name': 'server_test'})
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.update, req, FAKE_UUID, body=body)
+
+
+class ServerStatusTest(test.TestCase):
+
+ def setUp(self):
+ super(ServerStatusTest, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+
+ def _get_with_state(self, vm_state, task_state=None):
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_state,
+ task_state=task_state))
+
+ request = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID)
+ return self.controller.show(request, FAKE_UUID)
+
+ def test_active(self):
+ response = self._get_with_state(vm_states.ACTIVE)
+ self.assertEqual(response['server']['status'], 'ACTIVE')
+
+ def test_reboot(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.REBOOTING)
+ self.assertEqual(response['server']['status'], 'REBOOT')
+
+ def test_reboot_hard(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.REBOOTING_HARD)
+ self.assertEqual(response['server']['status'], 'HARD_REBOOT')
+
+ def test_reboot_resize_policy_fail(self):
+ def fake_get_server(context, req, id):
+ return fakes.stub_instance(id)
+
+ self.stubs.Set(self.controller, '_get_server', fake_get_server)
+
+ rule = {'compute:reboot':
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rule)
+ req = fakes.HTTPRequestV3.blank('/servers/1234/action')
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._action_reboot, req, '1234',
+ {'reboot': {'type': 'HARD'}})
+
+ def test_rebuild(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.REBUILDING)
+ self.assertEqual(response['server']['status'], 'REBUILD')
+
+ def test_rebuild_error(self):
+ response = self._get_with_state(vm_states.ERROR)
+ self.assertEqual(response['server']['status'], 'ERROR')
+
+ def test_resize(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.RESIZE_PREP)
+ self.assertEqual(response['server']['status'], 'RESIZE')
+
+ def test_confirm_resize_policy_fail(self):
+ def fake_get_server(context, req, id):
+ return fakes.stub_instance(id)
+
+ self.stubs.Set(self.controller, '_get_server', fake_get_server)
+
+ rule = {'compute:confirm_resize':
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rule)
+ req = fakes.HTTPRequestV3.blank('/servers/1234/action')
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._action_confirm_resize, req, '1234', {})
+
+ def test_verify_resize(self):
+ response = self._get_with_state(vm_states.RESIZED, None)
+ self.assertEqual(response['server']['status'], 'VERIFY_RESIZE')
+
+ def test_revert_resize(self):
+ response = self._get_with_state(vm_states.RESIZED,
+ task_states.RESIZE_REVERTING)
+ self.assertEqual(response['server']['status'], 'REVERT_RESIZE')
+
+ def test_revert_resize_policy_fail(self):
+ def fake_get_server(context, req, id):
+ return fakes.stub_instance(id)
+
+ self.stubs.Set(self.controller, '_get_server', fake_get_server)
+
+ rule = {'compute:revert_resize':
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rule)
+ req = fakes.HTTPRequestV3.blank('/servers/1234/action')
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._action_revert_resize, req, '1234', {})
+
+ def test_password_update(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.UPDATING_PASSWORD)
+ self.assertEqual(response['server']['status'], 'PASSWORD')
+
+ def test_stopped(self):
+ response = self._get_with_state(vm_states.STOPPED)
+ self.assertEqual(response['server']['status'], 'SHUTOFF')
+
+
+class ServersControllerCreateTest(test.TestCase):
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ def setUp(self):
+ """Shared implementation for tests below that create instance."""
+ super(ServersControllerCreateTest, self).setUp()
+
+ self.flags(verbose=True,
+ enable_instance_password=True)
+ self.instance_cache_num = 0
+ self.instance_cache_by_id = {}
+ self.instance_cache_by_uuid = {}
+
+ fakes.stub_out_nw_api(self.stubs)
+
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+
+ def instance_create(context, inst):
+ inst_type = flavors.get_flavor_by_flavor_id(3)
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ def_image_ref = 'http://localhost/images/%s' % image_uuid
+ self.instance_cache_num += 1
+ instance = fake_instance.fake_db_instance(**{
+ 'id': self.instance_cache_num,
+ 'display_name': inst['display_name'] or 'test',
+ 'uuid': FAKE_UUID,
+ 'instance_type': inst_type,
+ 'image_ref': inst.get('image_ref', def_image_ref),
+ 'user_id': 'fake',
+ 'project_id': 'fake',
+ 'reservation_id': inst['reservation_id'],
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "config_drive": None,
+ "progress": 0,
+ "fixed_ips": [],
+ "task_state": "",
+ "vm_state": "",
+ "root_device_name": inst.get('root_device_name', 'vda'),
+ })
+
+ self.instance_cache_by_id[instance['id']] = instance
+ self.instance_cache_by_uuid[instance['uuid']] = instance
+ return instance
+
+ def instance_get(context, instance_id):
+ """Stub for compute/api create() pulling in instance after
+ scheduling
+ """
+ return self.instance_cache_by_id[instance_id]
+
+ def instance_update(context, uuid, values):
+ instance = self.instance_cache_by_uuid[uuid]
+ instance.update(values)
+ return instance
+
+ def server_update(context, instance_uuid, params, update_cells=True):
+ inst = self.instance_cache_by_uuid[instance_uuid]
+ inst.update(params)
+ return inst
+
+ def server_update_and_get_original(
+ context, instance_uuid, params, update_cells=False,
+ columns_to_join=None):
+ inst = self.instance_cache_by_uuid[instance_uuid]
+ inst.update(params)
+ return (inst, inst)
+
+ def fake_method(*args, **kwargs):
+ pass
+
+ def project_get_networks(context, user_id):
+ return dict(id='1', host='localhost')
+
+ def queue_get_for(context, *args):
+ return 'network_topic'
+
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ fake.stub_out_image_service(self.stubs)
+ self.stubs.Set(uuid, 'uuid4', fake_gen_uuid)
+ self.stubs.Set(db, 'project_get_networks',
+ project_get_networks)
+ self.stubs.Set(db, 'instance_create', instance_create)
+ self.stubs.Set(db, 'instance_system_metadata_update',
+ fake_method)
+ self.stubs.Set(db, 'instance_get', instance_get)
+ self.stubs.Set(db, 'instance_update', instance_update)
+ self.stubs.Set(db, 'instance_update_and_get_original',
+ server_update_and_get_original)
+ self.stubs.Set(manager.VlanManager, 'allocate_fixed_ip',
+ fake_method)
+ self.body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': self.image_uuid,
+ 'flavorRef': self.flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ },
+ }
+ self.bdm = [{'delete_on_termination': 1,
+ 'device_name': 123,
+ 'volume_size': 1,
+ 'volume_id': '11111111-1111-1111-1111-111111111111'}]
+
+ self.req = fakes.HTTPRequest.blank('/fake/servers')
+ self.req.method = 'POST'
+ self.req.headers["content-type"] = "application/json"
+
+ def _check_admin_password_len(self, server_dict):
+ """utility function - check server_dict for admin_password length."""
+ self.assertEqual(CONF.password_length,
+ len(server_dict["adminPass"]))
+
+ def _check_admin_password_missing(self, server_dict):
+ """utility function - check server_dict for admin_password absence."""
+ self.assertNotIn("adminPass", server_dict)
+
+ def _test_create_instance(self, flavor=2):
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ self.body['server']['imageRef'] = image_uuid
+ self.body['server']['flavorRef'] = flavor
+ self.req.body = jsonutils.dumps(self.body)
+ server = self.controller.create(self.req, body=self.body).obj['server']
+ self._check_admin_password_len(server)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_private_flavor(self):
+ values = {
+ 'name': 'fake_name',
+ 'memory_mb': 512,
+ 'vcpus': 1,
+ 'root_gb': 10,
+ 'ephemeral_gb': 10,
+ 'flavorid': '1324',
+ 'swap': 0,
+ 'rxtx_factor': 0.5,
+ 'vcpu_weight': 1,
+ 'disabled': False,
+ 'is_public': False,
+ }
+ db.flavor_create(context.get_admin_context(), values)
+ self.assertRaises(webob.exc.HTTPBadRequest, self._test_create_instance,
+ flavor=1324)
+
+ def test_create_server_bad_image_href(self):
+ image_href = 1
+ self.body['server']['min_count'] = 1
+ self.body['server']['imageRef'] = image_href,
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create,
+ self.req, body=self.body)
+ # TODO(cyeoh): bp-v3-api-unittests
+ # This needs to be ported to the os-networks extension tests
+ # def test_create_server_with_invalid_networks_parameter(self):
+ # self.ext_mgr.extensions = {'os-networks': 'fake'}
+ # image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ # flavor_ref = 'http://localhost/123/flavors/3'
+ # body = {
+ # 'server': {
+ # 'name': 'server_test',
+ # 'imageRef': image_href,
+ # 'flavorRef': flavor_ref,
+ # 'networks': {'uuid': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'},
+ # }
+ # }
+ # req = fakes.HTTPRequest.blank('/v2/fake/servers')
+ # req.method = 'POST'
+ # req.body = jsonutils.dumps(body)
+ # req.headers["content-type"] = "application/json"
+ # self.assertRaises(webob.exc.HTTPBadRequest,
+ # self.controller.create,
+ # req,
+ # body)
+
+ def test_create_server_with_deleted_image(self):
+ # Get the fake image service so we can set the status to deleted
+ (image_service, image_id) = glance.get_remote_image_service(
+ context, '')
+ image_service.update(context, self.image_uuid, {'status': 'DELETED'})
+ self.addCleanup(image_service.update, context, self.image_uuid,
+ {'status': 'active'})
+
+ self.body['server']['flavorRef'] = 2
+ self.req.body = jsonutils.dumps(self.body)
+ with testtools.ExpectedException(
+ webob.exc.HTTPBadRequest,
+ 'Image 76fa36fc-c930-4bf3-8c8a-ea2a2420deb6 is not active.'):
+ self.controller.create(self.req, body=self.body)
+
+ def test_create_server_image_too_large(self):
+ # Get the fake image service so we can set the status to deleted
+ (image_service, image_id) = glance.get_remote_image_service(
+ context, self.image_uuid)
+
+ image = image_service.show(context, image_id)
+
+ orig_size = image['size']
+ new_size = str(1000 * (1024 ** 3))
+ image_service.update(context, self.image_uuid, {'size': new_size})
+
+ self.addCleanup(image_service.update, context, self.image_uuid,
+ {'size': orig_size})
+
+ self.body['server']['flavorRef'] = 2
+ self.req.body = jsonutils.dumps(self.body)
+
+ with testtools.ExpectedException(
+ webob.exc.HTTPBadRequest,
+ "Flavor's disk is too small for requested image."):
+ self.controller.create(self.req, body=self.body)
+
+ def test_create_instance_image_ref_is_bookmark(self):
+ image_href = 'http://localhost/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, body=self.body).obj
+
+ server = res['server']
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_image_ref_is_invalid(self):
+ image_uuid = 'this_is_not_a_valid_uuid'
+ image_href = 'http://localhost/images/%s' % image_uuid
+ flavor_ref = 'http://localhost/flavors/3'
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['flavorRef'] = flavor_ref
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ self.req, body=self.body)
+
+ def test_create_instance_no_key_pair(self):
+ fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False)
+ self._test_create_instance()
+
+ def _test_create_extra(self, params, no_image=False):
+ self.body['server']['flavorRef'] = 2
+ if no_image:
+ self.body['server'].pop('imageRef', None)
+ self.body['server'].update(params)
+ self.req.body = jsonutils.dumps(self.body)
+ self.req.headers["content-type"] = "application/json"
+ self.controller.create(self.req, body=self.body).obj['server']
+
+ # TODO(cyeoh): bp-v3-api-unittests
+ # This needs to be ported to the os-keypairs extension tests
+ # def test_create_instance_with_keypairs_enabled(self):
+ # self.ext_mgr.extensions = {'os-keypairs': 'fake'}
+ # key_name = 'green'
+ #
+ # params = {'key_name': key_name}
+ # old_create = compute_api.API.create
+ #
+ # # NOTE(sdague): key pair goes back to the database,
+ # # so we need to stub it out for tests
+ # def key_pair_get(context, user_id, name):
+ # return {'public_key': 'FAKE_KEY',
+ # 'fingerprint': 'FAKE_FINGERPRINT',
+ # 'name': name}
+ #
+ # def create(*args, **kwargs):
+ # self.assertEqual(kwargs['key_name'], key_name)
+ # return old_create(*args, **kwargs)
+ #
+ # self.stubs.Set(db, 'key_pair_get', key_pair_get)
+ # self.stubs.Set(compute_api.API, 'create', create)
+ # self._test_create_extra(params)
+ #
+ # TODO(cyeoh): bp-v3-api-unittests
+ # This needs to be ported to the os-networks extension tests
+ # def test_create_instance_with_networks_enabled(self):
+ # self.ext_mgr.extensions = {'os-networks': 'fake'}
+ # net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ # requested_networks = [{'uuid': net_uuid}]
+ # params = {'networks': requested_networks}
+ # old_create = compute_api.API.create
+
+ # def create(*args, **kwargs):
+ # result = [('76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', None)]
+ # self.assertEqual(kwargs['requested_networks'], result)
+ # return old_create(*args, **kwargs)
+
+ # self.stubs.Set(compute_api.API, 'create', create)
+ # self._test_create_extra(params)
+
+ def test_create_instance_with_port_with_no_fixed_ips(self):
+ port_id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'port': port_id}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.PortRequiresFixedIP(port_id=port_id)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_instance_raise_user_data_too_large(self, mock_create):
+ mock_create.side_effect = exception.InstanceUserDataTooLarge(
+ maxsize=1, length=2)
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ self.req, body=self.body)
+
+ def test_create_instance_with_network_with_no_subnet(self):
+ network = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.NetworkRequiresSubnet(network_uuid=network)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ def test_create_instance_with_non_unique_secgroup_name(self):
+ network = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network}]
+ params = {'networks': requested_networks,
+ 'security_groups': [{'name': 'dup'}, {'name': 'dup'}]}
+
+ def fake_create(*args, **kwargs):
+ raise exception.NoUniqueMatch("No Unique match found for ...")
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self._test_create_extra, params)
+
+ def test_create_instance_with_networks_disabled_neutronv2(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ requested_networks = [{'uuid': net_uuid}]
+ params = {'networks': requested_networks}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ result = [('76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', None,
+ None, None)]
+ self.assertEqual(result, kwargs['requested_networks'].as_tuples())
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_networks_disabled(self):
+ net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ requested_networks = [{'uuid': net_uuid}]
+ params = {'networks': requested_networks}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIsNone(kwargs['requested_networks'])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_pass_disabled(self):
+ # test with admin passwords disabled See lp bug 921814
+ self.flags(enable_instance_password=False)
+
+ # proper local hrefs must start with 'http://localhost/v3/'
+ self.flags(enable_instance_password=False)
+ image_href = 'http://localhost/v2/fake/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, body=self.body).obj
+
+ server = res['server']
+ self._check_admin_password_missing(server)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_name_too_long(self):
+ # proper local hrefs must start with 'http://localhost/v3/'
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['name'] = 'X' * 256
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError, self.controller.create,
+ self.req, body=self.body)
+
+ def test_create_instance_name_all_blank_spaces(self):
+ # proper local hrefs must start with 'http://localhost/v2/'
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ image_href = 'http://localhost/v3/images/%s' % image_uuid
+ flavor_ref = 'http://localhost/flavors/3'
+ body = {
+ 'server': {
+ 'name': ' ' * 64,
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ },
+ }
+
+ req = fakes.HTTPRequest.blank('/v3/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, req, body=body)
+
+ def test_create_instance(self):
+ # proper local hrefs must start with 'http://localhost/v3/'
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, body=self.body).obj
+
+ server = res['server']
+ self._check_admin_password_len(server)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_extension_create_exception(self):
+ def fake_keypair_server_create(self, server_dict,
+ create_kwargs):
+ raise KeyError
+
+ self.stubs.Set(keypairs.Keypairs, 'server_create',
+ fake_keypair_server_create)
+ # proper local hrefs must start with 'http://localhost/v3/'
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ image_href = 'http://localhost/v3/images/%s' % image_uuid
+ flavor_ref = 'http://localhost/123/flavors/3'
+ body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': image_href,
+ 'flavorRef': flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ },
+ }
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ self.assertRaises(webob.exc.HTTPInternalServerError,
+ self.controller.create, req, body=body)
+
+ def test_create_instance_pass_disabled(self):
+ self.flags(enable_instance_password=False)
+ # proper local hrefs must start with 'http://localhost/v3/'
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, body=self.body).obj
+
+ server = res['server']
+ self._check_admin_password_missing(server)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_too_much_metadata(self):
+ self.flags(quota_metadata_items=1)
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['metadata']['vote'] = 'fiddletown'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_metadata_key_too_long(self):
+ self.flags(quota_metadata_items=1)
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['metadata'] = {('a' * 260): '12345'}
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_metadata_value_too_long(self):
+ self.flags(quota_metadata_items=1)
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['metadata'] = {'key1': ('a' * 260)}
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_metadata_key_blank(self):
+ self.flags(quota_metadata_items=1)
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['metadata'] = {'': 'abcd'}
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_metadata_not_dict(self):
+ self.flags(quota_metadata_items=1)
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['metadata'] = 'string'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_metadata_key_not_string(self):
+ self.flags(quota_metadata_items=1)
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['metadata'] = {1: 'test'}
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_metadata_value_not_string(self):
+ self.flags(quota_metadata_items=1)
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['metadata'] = {'test': ['a', 'list']}
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_user_data_malformed_bad_request(self):
+ params = {'user_data': 'u1234'}
+ self.assertRaises(exception.ValidationError,
+ self._test_create_extra, params)
+
+ def test_create_instance_invalid_key_name(self):
+ image_href = 'http://localhost/v2/images/2'
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['key_name'] = 'nonexistentkey'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_valid_key_name(self):
+ self.body['server']['key_name'] = 'key'
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, body=self.body).obj
+
+ self.assertEqual(FAKE_UUID, res["server"]["id"])
+ self._check_admin_password_len(res["server"])
+
+ def test_create_instance_invalid_flavor_href(self):
+ image_href = 'http://localhost/v2/images/2'
+ flavor_ref = 'http://localhost/v2/flavors/asdf'
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['flavorRef'] = flavor_ref
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_invalid_flavor_id_int(self):
+ image_href = 'http://localhost/v2/images/2'
+ flavor_ref = -1
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['flavorRef'] = flavor_ref
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_bad_flavor_href(self):
+ image_href = 'http://localhost/v2/images/2'
+ flavor_ref = 'http://localhost/v2/flavors/17'
+ self.body['server']['imageRef'] = image_href
+ self.body['server']['flavorRef'] = flavor_ref
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_bad_href(self):
+ image_href = 'asdf'
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, body=self.body)
+
+ def test_create_instance_local_href(self):
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, body=self.body).obj
+
+ server = res['server']
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_admin_password(self):
+ self.body['server']['flavorRef'] = 3
+ self.body['server']['adminPass'] = 'testpass'
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, body=self.body).obj
+
+ server = res['server']
+ self.assertEqual(server['adminPass'],
+ self.body['server']['adminPass'])
+
+ def test_create_instance_admin_password_pass_disabled(self):
+ self.flags(enable_instance_password=False)
+ self.body['server']['flavorRef'] = 3
+ self.body['server']['adminPass'] = 'testpass'
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, body=self.body).obj
+
+ self.assertIn('server', res)
+ self.assertIn('adminPass', self.body['server'])
+
+ def test_create_instance_admin_password_empty(self):
+ self.body['server']['flavorRef'] = 3
+ self.body['server']['adminPass'] = ''
+ self.req.body = jsonutils.dumps(self.body)
+
+ # The fact that the action doesn't raise is enough validation
+ self.controller.create(self.req, body=self.body)
+
+ def test_create_location(self):
+ selfhref = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID
+ self.req.body = jsonutils.dumps(self.body)
+ robj = self.controller.create(self.req, body=self.body)
+
+ self.assertEqual(robj['Location'], selfhref)
+
+ def _do_test_create_instance_above_quota(self, resource, allowed, quota,
+ expected_msg):
+ fakes.stub_out_instance_quota(self.stubs, allowed, quota, resource)
+ self.body['server']['flavorRef'] = 3
+ self.req.body = jsonutils.dumps(self.body)
+ try:
+ self.controller.create(self.req, body=self.body).obj['server']
+ self.fail('expected quota to be exceeded')
+ except webob.exc.HTTPForbidden as e:
+ self.assertEqual(e.explanation, expected_msg)
+
+ def test_create_instance_above_quota_instances(self):
+ msg = _('Quota exceeded for instances: Requested 1, but'
+ ' already used 10 of 10 instances')
+ self._do_test_create_instance_above_quota('instances', 0, 10, msg)
+
+ def test_create_instance_above_quota_ram(self):
+ msg = _('Quota exceeded for ram: Requested 4096, but'
+ ' already used 8192 of 10240 ram')
+ self._do_test_create_instance_above_quota('ram', 2048, 10 * 1024, msg)
+
+ def test_create_instance_above_quota_cores(self):
+ msg = _('Quota exceeded for cores: Requested 2, but'
+ ' already used 9 of 10 cores')
+ self._do_test_create_instance_above_quota('cores', 1, 10, msg)
+
+ def test_create_instance_above_quota_server_group_members(self):
+ ctxt = context.get_admin_context()
+ fake_group = objects.InstanceGroup(ctxt)
+ fake_group.create()
+
+ def fake_count(context, name, group, user_id):
+ self.assertEqual(name, "server_group_members")
+ self.assertEqual(group.uuid, fake_group.uuid)
+ self.assertEqual(user_id,
+ self.req.environ['nova.context'].user_id)
+ return 10
+
+ def fake_limit_check(context, **kwargs):
+ if 'server_group_members' in kwargs:
+ raise exception.OverQuota(overs={})
+
+ def fake_instance_destroy(context, uuid, constraint):
+ return fakes.stub_instance(1)
+
+ self.stubs.Set(fakes.QUOTAS, 'count', fake_count)
+ self.stubs.Set(fakes.QUOTAS, 'limit_check', fake_limit_check)
+ self.stubs.Set(db, 'instance_destroy', fake_instance_destroy)
+ self.body['os:scheduler_hints'] = {'group': fake_group.uuid}
+ self.req.body = jsonutils.dumps(self.body)
+ expected_msg = "Quota exceeded, too many servers in group"
+
+ try:
+ self.controller.create(self.req, body=self.body).obj
+ self.fail('expected quota to be exceeded')
+ except webob.exc.HTTPForbidden as e:
+ self.assertEqual(e.explanation, expected_msg)
+
+ def test_create_instance_above_quota_server_groups(self):
+
+ def fake_reserve(contex, **deltas):
+ if 'server_groups' in deltas:
+ raise exception.OverQuota(overs={})
+
+ def fake_instance_destroy(context, uuid, constraint):
+ return fakes.stub_instance(1)
+
+ self.stubs.Set(fakes.QUOTAS, 'reserve', fake_reserve)
+ self.stubs.Set(db, 'instance_destroy', fake_instance_destroy)
+ self.body['os:scheduler_hints'] = {'group': 'fake_group'}
+ self.req.body = jsonutils.dumps(self.body)
+
+ expected_msg = "Quota exceeded, too many server groups."
+
+ try:
+ self.controller.create(self.req, body=self.body).obj
+ self.fail('expected quota to be exceeded')
+ except webob.exc.HTTPForbidden as e:
+ self.assertEqual(e.explanation, expected_msg)
+
+ def test_create_instance_with_neutronv2_port_in_use(self):
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network, 'port': port}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.PortInUse(port_id=port)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self._test_create_extra, params)
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_instance_public_network_non_admin(self, mock_create):
+ public_network_uuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ params = {'networks': [{'uuid': public_network_uuid}]}
+ self.req.body = jsonutils.dumps(self.body)
+ mock_create.side_effect = exception.ExternalNetworkAttachForbidden(
+ network_uuid=public_network_uuid)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self._test_create_extra, params)
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_multiple_instance_with_specified_ip_neutronv2(self,
+ _api_mock):
+ _api_mock.side_effect = exception.InvalidFixedIpAndMaxCountRequest(
+ reason="")
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ address = '10.0.0.1'
+ requested_networks = [{'uuid': network, 'fixed_ip': address,
+ 'port': port}]
+ params = {'networks': requested_networks}
+ self.body['server']['max_count'] = 2
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ def test_create_multiple_instance_with_neutronv2_port(self):
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network, 'port': port}]
+ params = {'networks': requested_networks}
+ self.body['server']['max_count'] = 2
+
+ def fake_create(*args, **kwargs):
+ msg = _("Unable to launch multiple instances with"
+ " a single configured port ID. Please launch your"
+ " instance one by one with different ports.")
+ raise exception.MultiplePortsNotApplicable(reason=msg)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ def test_create_instance_with_neturonv2_not_found_network(self):
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ requested_networks = [{'uuid': network}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.NetworkNotFound(network_id=network)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ def test_create_instance_with_neutronv2_port_not_found(self):
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network, 'port': port}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.PortNotFound(port_id=port)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_instance_with_network_ambiguous(self, mock_create):
+ mock_create.side_effect = exception.NetworkAmbiguous()
+ self.assertRaises(webob.exc.HTTPConflict,
+ self._test_create_extra, {})
+
+ @mock.patch.object(compute_api.API, 'create',
+ side_effect=exception.InstanceExists(
+ name='instance-name'))
+ def test_create_instance_raise_instance_exists(self, mock_create):
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller.create,
+ self.req, body=self.body)
+
+
+class ServersControllerCreateTestWithMock(test.TestCase):
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ def setUp(self):
+ """Shared implementation for tests below that create instance."""
+ super(ServersControllerCreateTestWithMock, self).setUp()
+
+ self.flags(verbose=True,
+ enable_instance_password=True)
+ self.instance_cache_num = 0
+ self.instance_cache_by_id = {}
+ self.instance_cache_by_uuid = {}
+
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+
+ self.body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': self.image_uuid,
+ 'flavorRef': self.flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ },
+ }
+ self.req = fakes.HTTPRequest.blank('/fake/servers')
+ self.req.method = 'POST'
+ self.req.headers["content-type"] = "application/json"
+
+ def _test_create_extra(self, params, no_image=False):
+ self.body['server']['flavorRef'] = 2
+ if no_image:
+ self.body['server'].pop('imageRef', None)
+ self.body['server'].update(params)
+ self.req.body = jsonutils.dumps(self.body)
+ self.req.headers["content-type"] = "application/json"
+ self.controller.create(self.req, body=self.body).obj['server']
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_instance_with_neutronv2_fixed_ip_already_in_use(self,
+ create_mock):
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ address = '10.0.2.3'
+ requested_networks = [{'uuid': network, 'fixed_ip': address}]
+ params = {'networks': requested_networks}
+ create_mock.side_effect = exception.FixedIpAlreadyInUse(
+ address=address,
+ instance_uuid=network)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+ self.assertEqual(1, len(create_mock.call_args_list))
+
+ @mock.patch.object(compute_api.API, 'create',
+ side_effect=exception.InvalidVolume(reason='error'))
+ def test_create_instance_with_invalid_volume_error(self, create_mock):
+ # Tests that InvalidVolume is translated to a 400 error.
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, {})
+
+
+class ServersViewBuilderTest(test.TestCase):
+
+ def setUp(self):
+ super(ServersViewBuilderTest, self).setUp()
+ CONF.set_override('host', 'localhost', group='glance')
+ self.flags(use_ipv6=True)
+ db_inst = fakes.stub_instance(
+ id=1,
+ image_ref="5",
+ uuid="deadbeef-feed-edee-beef-d0ea7beefedd",
+ display_name="test_server",
+ include_fake_metadata=False)
+
+ privates = ['172.19.0.1']
+ publics = ['192.168.0.3']
+ public6s = ['b33f::fdee:ddff:fecc:bbaa']
+
+ def nw_info(*args, **kwargs):
+ return [(None, {'label': 'public',
+ 'ips': [dict(ip=ip) for ip in publics],
+ 'ip6s': [dict(ip=ip) for ip in public6s]}),
+ (None, {'label': 'private',
+ 'ips': [dict(ip=ip) for ip in privates]})]
+
+ def floaters(*args, **kwargs):
+ return []
+
+ fakes.stub_out_nw_api_get_instance_nw_info(self.stubs, nw_info)
+ fakes.stub_out_nw_api_get_floating_ips_by_fixed_address(self.stubs,
+ floaters)
+
+ self.uuid = db_inst['uuid']
+ self.view_builder = views.servers.ViewBuilderV3()
+ self.request = fakes.HTTPRequestV3.blank("")
+ self.request.context = context.RequestContext('fake', 'fake')
+ self.instance = fake_instance.fake_instance_obj(
+ self.request.context,
+ expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS,
+ **db_inst)
+
+ def test_get_flavor_valid_instance_type(self):
+ flavor_bookmark = "http://localhost/flavors/1"
+ expected = {"id": "1",
+ "links": [{"rel": "bookmark",
+ "href": flavor_bookmark}]}
+ result = self.view_builder._get_flavor(self.request, self.instance)
+ self.assertEqual(result, expected)
+
+ def test_build_server(self):
+ self_link = "http://localhost/v3/servers/%s" % self.uuid
+ bookmark_link = "http://localhost/servers/%s" % self.uuid
+ expected_server = {
+ "server": {
+ "id": self.uuid,
+ "name": "test_server",
+ "links": [
+ {
+ "rel": "self",
+ "href": self_link,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark_link,
+ },
+ ],
+ }
+ }
+
+ output = self.view_builder.basic(self.request, self.instance)
+ self.assertThat(output, matchers.DictMatches(expected_server))
+
+ def test_build_server_with_project_id(self):
+ expected_server = {
+ "server": {
+ "id": self.uuid,
+ "name": "test_server",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v3/servers/%s" %
+ self.uuid,
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/servers/%s" % self.uuid,
+ },
+ ],
+ }
+ }
+
+ output = self.view_builder.basic(self.request, self.instance)
+ self.assertThat(output, matchers.DictMatches(expected_server))
+
+ def test_build_server_detail(self):
+ image_bookmark = "http://localhost/images/5"
+ flavor_bookmark = "http://localhost/flavors/1"
+ self_link = "http://localhost/v3/servers/%s" % self.uuid
+ bookmark_link = "http://localhost/servers/%s" % self.uuid
+ expected_server = {
+ "server": {
+ "id": self.uuid,
+ "user_id": "fake_user",
+ "tenant_id": "fake_project",
+ "updated": "2010-11-11T11:00:00Z",
+ "created": "2010-10-10T12:00:00Z",
+ "progress": 0,
+ "name": "test_server",
+ "status": "BUILD",
+ "hostId": '',
+ "image": {
+ "id": "5",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": image_bookmark,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": flavor_bookmark,
+ },
+ ],
+ },
+ "addresses": {
+ 'test1': [
+ {'version': 4, 'addr': '192.168.1.100',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ {'version': 6, 'addr': '2001:db8:0:1::1',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}
+ ]
+ },
+ "metadata": {},
+ "links": [
+ {
+ "rel": "self",
+ "href": self_link,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark_link,
+ },
+ ],
+ }
+ }
+
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output, matchers.DictMatches(expected_server))
+
+ def test_build_server_detail_with_fault(self):
+ self.instance['vm_state'] = vm_states.ERROR
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context, self.uuid)
+
+ image_bookmark = "http://localhost/images/5"
+ flavor_bookmark = "http://localhost/flavors/1"
+ self_link = "http://localhost/v3/servers/%s" % self.uuid
+ bookmark_link = "http://localhost/servers/%s" % self.uuid
+ expected_server = {
+ "server": {
+ "id": self.uuid,
+ "user_id": "fake_user",
+ "tenant_id": "fake_project",
+ "updated": "2010-11-11T11:00:00Z",
+ "created": "2010-10-10T12:00:00Z",
+ "name": "test_server",
+ "status": "ERROR",
+ "hostId": '',
+ "image": {
+ "id": "5",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": image_bookmark,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": flavor_bookmark,
+ },
+ ],
+ },
+ "addresses": {
+ 'test1': [
+ {'version': 4, 'addr': '192.168.1.100',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ {'version': 6, 'addr': '2001:db8:0:1::1',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}
+ ]
+ },
+ "metadata": {},
+ "links": [
+ {
+ "rel": "self",
+ "href": self_link,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark_link,
+ },
+ ],
+ "fault": {
+ "code": 404,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "HTTPNotFound",
+ "details": "Stock details for test",
+ },
+ }
+ }
+
+ self.request.context = context.RequestContext('fake', 'fake')
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output, matchers.DictMatches(expected_server))
+
+ def test_build_server_detail_with_fault_that_has_been_deleted(self):
+ self.instance['deleted'] = 1
+ self.instance['vm_state'] = vm_states.ERROR
+ fault = fake_instance.fake_fault_obj(self.request.context,
+ self.uuid, code=500,
+ message="No valid host was found")
+ self.instance['fault'] = fault
+
+ expected_fault = {"code": 500,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "No valid host was found"}
+
+ self.request.context = context.RequestContext('fake', 'fake')
+ output = self.view_builder.show(self.request, self.instance)
+ # Regardless of vm_state deleted servers sholud be DELETED
+ self.assertEqual("DELETED", output['server']['status'])
+ self.assertThat(output['server']['fault'],
+ matchers.DictMatches(expected_fault))
+
+ def test_build_server_detail_with_fault_no_details_not_admin(self):
+ self.instance['vm_state'] = vm_states.ERROR
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context,
+ self.uuid,
+ code=500,
+ message='Error')
+
+ expected_fault = {"code": 500,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "Error"}
+
+ self.request.context = context.RequestContext('fake', 'fake')
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output['server']['fault'],
+ matchers.DictMatches(expected_fault))
+
+ def test_build_server_detail_with_fault_admin(self):
+ self.instance['vm_state'] = vm_states.ERROR
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context,
+ self.uuid,
+ code=500,
+ message='Error')
+
+ expected_fault = {"code": 500,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "Error",
+ 'details': 'Stock details for test'}
+
+ self.request.environ['nova.context'].is_admin = True
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output['server']['fault'],
+ matchers.DictMatches(expected_fault))
+
+ def test_build_server_detail_with_fault_no_details_admin(self):
+ self.instance['vm_state'] = vm_states.ERROR
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context,
+ self.uuid,
+ code=500,
+ message='Error',
+ details='')
+
+ expected_fault = {"code": 500,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "Error"}
+
+ self.request.environ['nova.context'].is_admin = True
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output['server']['fault'],
+ matchers.DictMatches(expected_fault))
+
+ def test_build_server_detail_with_fault_but_active(self):
+ self.instance['vm_state'] = vm_states.ACTIVE
+ self.instance['progress'] = 100
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context, self.uuid)
+
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertNotIn('fault', output['server'])
+
+ def test_build_server_detail_active_status(self):
+ # set the power state of the instance to running
+ self.instance['vm_state'] = vm_states.ACTIVE
+ self.instance['progress'] = 100
+ image_bookmark = "http://localhost/images/5"
+ flavor_bookmark = "http://localhost/flavors/1"
+ self_link = "http://localhost/v3/servers/%s" % self.uuid
+ bookmark_link = "http://localhost/servers/%s" % self.uuid
+ expected_server = {
+ "server": {
+ "id": self.uuid,
+ "user_id": "fake_user",
+ "tenant_id": "fake_project",
+ "updated": "2010-11-11T11:00:00Z",
+ "created": "2010-10-10T12:00:00Z",
+ "progress": 100,
+ "name": "test_server",
+ "status": "ACTIVE",
+ "hostId": '',
+ "image": {
+ "id": "5",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": image_bookmark,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": flavor_bookmark,
+ },
+ ],
+ },
+ "addresses": {
+ 'test1': [
+ {'version': 4, 'addr': '192.168.1.100',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ {'version': 6, 'addr': '2001:db8:0:1::1',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}
+ ]
+ },
+ "metadata": {},
+ "links": [
+ {
+ "rel": "self",
+ "href": self_link,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark_link,
+ },
+ ],
+ }
+ }
+
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output, matchers.DictMatches(expected_server))
+
+ def test_build_server_detail_with_metadata(self):
+
+ metadata = []
+ metadata.append(models.InstanceMetadata(key="Open", value="Stack"))
+ metadata = nova_utils.metadata_to_dict(metadata)
+ self.instance['metadata'] = metadata
+
+ image_bookmark = "http://localhost/images/5"
+ flavor_bookmark = "http://localhost/flavors/1"
+ self_link = "http://localhost/v3/servers/%s" % self.uuid
+ bookmark_link = "http://localhost/servers/%s" % self.uuid
+ expected_server = {
+ "server": {
+ "id": self.uuid,
+ "user_id": "fake_user",
+ "tenant_id": "fake_project",
+ "updated": "2010-11-11T11:00:00Z",
+ "created": "2010-10-10T12:00:00Z",
+ "progress": 0,
+ "name": "test_server",
+ "status": "BUILD",
+ "hostId": '',
+ "image": {
+ "id": "5",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": image_bookmark,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": flavor_bookmark,
+ },
+ ],
+ },
+ "addresses": {
+ 'test1': [
+ {'version': 4, 'addr': '192.168.1.100',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ {'version': 6, 'addr': '2001:db8:0:1::1',
+ 'OS-EXT-IPS:type': 'fixed',
+ 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
+ ]
+ },
+ "metadata": {"Open": "Stack"},
+ "links": [
+ {
+ "rel": "self",
+ "href": self_link,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark_link,
+ },
+ ],
+ }
+ }
+
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output, matchers.DictMatches(expected_server))
+
+
+class ServersAllExtensionsTestCase(test.TestCase):
+ """Servers tests using default API router with all extensions enabled.
+
+ The intent here is to catch cases where extensions end up throwing
+ an exception because of a malformed request before the core API
+ gets a chance to validate the request and return a 422 response.
+
+ For example, AccessIPsController extends servers.Controller::
+
+ | @wsgi.extends
+ | def create(self, req, resp_obj, body):
+ | context = req.environ['nova.context']
+ | if authorize(context) and 'server' in resp_obj.obj:
+ | resp_obj.attach(xml=AccessIPTemplate())
+ | server = resp_obj.obj['server']
+ | self._extend_server(req, server)
+
+ we want to ensure that the extension isn't barfing on an invalid
+ body.
+ """
+
+ def setUp(self):
+ super(ServersAllExtensionsTestCase, self).setUp()
+ self.app = compute.APIRouterV3()
+
+ def test_create_missing_server(self):
+ # Test create with malformed body.
+
+ def fake_create(*args, **kwargs):
+ raise test.TestingException("Should not reach the compute API.")
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'foo': {'a': 'b'}}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+ def test_update_missing_server(self):
+ # Test update with malformed body.
+
+ def fake_update(*args, **kwargs):
+ raise test.TestingException("Should not reach the compute API.")
+
+ self.stubs.Set(compute_api.API, 'update', fake_update)
+
+ req = fakes.HTTPRequestV3.blank('/servers/1')
+ req.method = 'PUT'
+ req.content_type = 'application/json'
+ body = {'foo': {'a': 'b'}}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(400, res.status_int)
+
+
+class ServersInvalidRequestTestCase(test.TestCase):
+ """Tests of places we throw 400 Bad Request from."""
+
+ def setUp(self):
+ super(ServersInvalidRequestTestCase, self).setUp()
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+
+ def _invalid_server_create(self, body):
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+
+ self.assertRaises(exception.ValidationError,
+ self.controller.create, req, body=body)
+
+ def test_create_server_no_body(self):
+ self._invalid_server_create(body=None)
+
+ def test_create_server_missing_server(self):
+ body = {'foo': {'a': 'b'}}
+ self._invalid_server_create(body=body)
+
+ def test_create_server_malformed_entity(self):
+ body = {'server': 'string'}
+ self._invalid_server_create(body=body)
+
+ def _unprocessable_server_update(self, body):
+ req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID)
+ req.method = 'PUT'
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, FAKE_UUID, body=body)
+
+ def test_update_server_no_body(self):
+ self._invalid_server_create(body=None)
+
+ def test_update_server_missing_server(self):
+ body = {'foo': {'a': 'b'}}
+ self._invalid_server_create(body=body)
+
+ def test_create_update_malformed_entity(self):
+ body = {'server': 'string'}
+ self._invalid_server_create(body=body)
+
+
+class FakeExt(extensions.V3APIExtensionBase):
+ name = "DiskConfig"
+ alias = 'os-disk-config'
+ version = 1
+
+ def fake_extension_point(self, *args, **kwargs):
+ pass
+
+ def get_controller_extensions(self):
+ return []
+
+ def get_resources(self):
+ return []
+
+
+class TestServersExtensionPoint(test.NoDBTestCase):
+ def setUp(self):
+ super(TestServersExtensionPoint, self).setUp()
+ CONF.set_override('extensions_whitelist', ['os-disk-config'],
+ 'osapi_v3')
+ self.stubs.Set(disk_config, 'DiskConfig', FakeExt)
+
+ def _test_load_extension_point(self, name):
+ setattr(FakeExt, 'server_%s' % name,
+ FakeExt.fake_extension_point)
+ ext_info = plugins.LoadedExtensionInfo()
+ controller = servers.ServersController(extension_info=ext_info)
+ self.assertEqual(
+ 'os-disk-config',
+ list(getattr(controller,
+ '%s_extension_manager' % name))[0].obj.alias)
+ delattr(FakeExt, 'server_%s' % name)
+
+ def test_load_update_extension_point(self):
+ self._test_load_extension_point('update')
+
+ def test_load_rebuild_extension_point(self):
+ self._test_load_extension_point('rebuild')
+
+ def test_load_create_extension_point(self):
+ self._test_load_extension_point('create')
+
+ def test_load_resize_extension_point(self):
+ self._test_load_extension_point('resize')
+
+
+class TestServersExtensionSchema(test.NoDBTestCase):
+ def setUp(self):
+ super(TestServersExtensionSchema, self).setUp()
+ CONF.set_override('extensions_whitelist', ['disk_config'], 'osapi_v3')
+
+ def _test_load_extension_schema(self, name):
+ setattr(FakeExt, 'get_server_%s_schema' % name,
+ FakeExt.fake_extension_point)
+ ext_info = plugins.LoadedExtensionInfo()
+ controller = servers.ServersController(extension_info=ext_info)
+ self.assertTrue(hasattr(controller, '%s_schema_manager' % name))
+
+ delattr(FakeExt, 'get_server_%s_schema' % name)
+ return getattr(controller, 'schema_server_%s' % name)
+
+ def test_load_create_extension_point(self):
+ # The expected is the schema combination of base and keypairs
+ # because of the above extensions_whitelist.
+ expected_schema = copy.deepcopy(servers_schema.base_create)
+ expected_schema['properties']['server']['properties'].update(
+ disk_config_schema.server_create)
+
+ actual_schema = self._test_load_extension_schema('create')
+ self.assertEqual(expected_schema, actual_schema)
+
+ def test_load_update_extension_point(self):
+ # keypair extension does not contain update_server() and
+ # here checks that any extension is not added to the schema.
+ expected_schema = copy.deepcopy(servers_schema.base_update)
+ expected_schema['properties']['server']['properties'].update(
+ disk_config_schema.server_create)
+
+ actual_schema = self._test_load_extension_schema('update')
+ self.assertEqual(expected_schema, actual_schema)
+
+ def test_load_rebuild_extension_point(self):
+ # keypair extension does not contain rebuild_server() and
+ # here checks that any extension is not added to the schema.
+ expected_schema = copy.deepcopy(servers_schema.base_rebuild)
+ expected_schema['properties']['rebuild']['properties'].update(
+ disk_config_schema.server_create)
+
+ actual_schema = self._test_load_extension_schema('rebuild')
+ self.assertEqual(expected_schema, actual_schema)
+
+ def test_load_resize_extension_point(self):
+ # keypair extension does not contain resize_server() and
+ # here checks that any extension is not added to the schema.
+ expected_schema = copy.deepcopy(servers_schema.base_resize)
+ expected_schema['properties']['resize']['properties'].update(
+ disk_config_schema.server_create)
+
+ actual_schema = self._test_load_extension_schema('resize')
+ self.assertEqual(expected_schema, actual_schema)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_services.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_services.py
new file mode 100644
index 0000000000..072992cbb6
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_services.py
@@ -0,0 +1,453 @@
+# Copyright 2012 IBM Corp.
+#
+# 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 calendar
+import datetime
+
+import iso8601
+import mock
+from oslo.utils import timeutils
+import webob.exc
+
+from nova.api.openstack.compute.plugins.v3 import services
+from nova import availability_zones
+from nova.compute import cells_api
+from nova import context
+from nova import db
+from nova import exception
+from nova.servicegroup.drivers import db as db_driver
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit.objects import test_service
+
+
+fake_services_list = [
+ dict(test_service.fake_service,
+ binary='nova-scheduler',
+ host='host1',
+ id=1,
+ disabled=True,
+ topic='scheduler',
+ updated_at=datetime.datetime(2012, 10, 29, 13, 42, 2),
+ created_at=datetime.datetime(2012, 9, 18, 2, 46, 27),
+ disabled_reason='test1'),
+ dict(test_service.fake_service,
+ binary='nova-compute',
+ host='host1',
+ id=2,
+ disabled=True,
+ topic='compute',
+ updated_at=datetime.datetime(2012, 10, 29, 13, 42, 5),
+ created_at=datetime.datetime(2012, 9, 18, 2, 46, 27),
+ disabled_reason='test2'),
+ dict(test_service.fake_service,
+ binary='nova-scheduler',
+ host='host2',
+ id=3,
+ disabled=False,
+ topic='scheduler',
+ updated_at=datetime.datetime(2012, 9, 19, 6, 55, 34),
+ created_at=datetime.datetime(2012, 9, 18, 2, 46, 28),
+ disabled_reason=None),
+ dict(test_service.fake_service,
+ binary='nova-compute',
+ host='host2',
+ id=4,
+ disabled=True,
+ topic='compute',
+ updated_at=datetime.datetime(2012, 9, 18, 8, 3, 38),
+ created_at=datetime.datetime(2012, 9, 18, 2, 46, 28),
+ disabled_reason='test4'),
+ ]
+
+
+class FakeRequest(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {}
+
+
+class FakeRequestWithService(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {"binary": "nova-compute"}
+
+
+class FakeRequestWithHost(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {"host": "host1"}
+
+
+class FakeRequestWithHostService(object):
+ environ = {"nova.context": context.get_admin_context()}
+ GET = {"host": "host1", "binary": "nova-compute"}
+
+
+def fake_service_get_all(services):
+ def service_get_all(context, filters=None, set_zones=False):
+ if set_zones or 'availability_zone' in filters:
+ return availability_zones.set_availability_zones(context,
+ services)
+ return services
+ return service_get_all
+
+
+def fake_db_api_service_get_all(context, disabled=None):
+ return fake_services_list
+
+
+def fake_db_service_get_by_host_binary(services):
+ def service_get_by_host_binary(context, host, binary):
+ for service in services:
+ if service['host'] == host and service['binary'] == binary:
+ return service
+ raise exception.HostBinaryNotFound(host=host, binary=binary)
+ return service_get_by_host_binary
+
+
+def fake_service_get_by_host_binary(context, host, binary):
+ fake = fake_db_service_get_by_host_binary(fake_services_list)
+ return fake(context, host, binary)
+
+
+def _service_get_by_id(services, value):
+ for service in services:
+ if service['id'] == value:
+ return service
+ return None
+
+
+def fake_db_service_update(services):
+ def service_update(context, service_id, values):
+ service = _service_get_by_id(services, service_id)
+ if service is None:
+ raise exception.ServiceNotFound(service_id=service_id)
+ return service
+ return service_update
+
+
+def fake_service_update(context, service_id, values):
+ fake = fake_db_service_update(fake_services_list)
+ return fake(context, service_id, values)
+
+
+def fake_utcnow():
+ return datetime.datetime(2012, 10, 29, 13, 42, 11)
+
+
+fake_utcnow.override_time = None
+
+
+def fake_utcnow_ts():
+ d = fake_utcnow()
+ return calendar.timegm(d.utctimetuple())
+
+
+class ServicesTest(test.TestCase):
+
+ def setUp(self):
+ super(ServicesTest, self).setUp()
+
+ self.controller = services.ServiceController()
+
+ self.stubs.Set(timeutils, "utcnow", fake_utcnow)
+ self.stubs.Set(timeutils, "utcnow_ts", fake_utcnow_ts)
+
+ self.stubs.Set(self.controller.host_api, "service_get_all",
+ fake_service_get_all(fake_services_list))
+
+ self.stubs.Set(db, "service_get_by_args",
+ fake_db_service_get_by_host_binary(fake_services_list))
+ self.stubs.Set(db, "service_update",
+ fake_db_service_update(fake_services_list))
+
+ def test_services_list(self):
+ req = FakeRequest()
+ res_dict = self.controller.index(req)
+ response = {'services': [
+ {'binary': 'nova-scheduler',
+ 'id': 1,
+ 'host': 'host1',
+ 'zone': 'internal',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
+ 'disabled_reason': 'test1'},
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'id': 2,
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5),
+ 'disabled_reason': 'test2'},
+ {'binary': 'nova-scheduler',
+ 'host': 'host2',
+ 'id': 3,
+ 'zone': 'internal',
+ 'status': 'enabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34),
+ 'disabled_reason': None},
+ {'binary': 'nova-compute',
+ 'host': 'host2',
+ 'id': 4,
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38),
+ 'disabled_reason': 'test4'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_service_list_with_host(self):
+ req = FakeRequestWithHost()
+ res_dict = self.controller.index(req)
+ response = {'services': [
+ {'binary': 'nova-scheduler',
+ 'host': 'host1',
+ 'id': 1,
+ 'zone': 'internal',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
+ 'disabled_reason': 'test1'},
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'id': 2,
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5),
+ 'disabled_reason': 'test2'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_service_list_with_service(self):
+ req = FakeRequestWithService()
+ res_dict = self.controller.index(req)
+ response = {'services': [
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'id': 2,
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5),
+ 'disabled_reason': 'test2'},
+ {'binary': 'nova-compute',
+ 'host': 'host2',
+ 'id': 4,
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38),
+ 'disabled_reason': 'test4'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_service_list_with_host_service(self):
+ req = FakeRequestWithHostService()
+ res_dict = self.controller.index(req)
+ response = {'services': [
+ {'binary': 'nova-compute',
+ 'host': 'host1',
+ 'id': 2,
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5),
+ 'disabled_reason': 'test2'}]}
+ self.assertEqual(res_dict, response)
+
+ def test_services_enable(self):
+ def _service_update(context, service_id, values):
+ self.assertIsNone(values['disabled_reason'])
+ return dict(test_service.fake_service, id=service_id)
+
+ self.stubs.Set(db, "service_update", _service_update)
+
+ body = {'service': {'host': 'host1',
+ 'binary': 'nova-compute'}}
+ req = fakes.HTTPRequestV3.blank('/os-services/enable')
+ res_dict = self.controller.update(req, "enable", body)
+
+ self.assertEqual(res_dict['service']['status'], 'enabled')
+ self.assertNotIn('disabled_reason', res_dict['service'])
+
+ def test_services_enable_with_invalid_host(self):
+ body = {'service': {'host': 'invalid',
+ 'binary': 'nova-compute'}}
+ req = fakes.HTTPRequestV3.blank('/os-services/enable')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update,
+ req,
+ "enable",
+ body)
+
+ def test_services_enable_with_invalid_binary(self):
+ body = {'service': {'host': 'host1',
+ 'binary': 'invalid'}}
+ req = fakes.HTTPRequestV3.blank('/os-services/enable')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update,
+ req,
+ "enable",
+ body)
+
+ # This test is just to verify that the servicegroup API gets used when
+ # calling this API.
+ def test_services_with_exception(self):
+ def dummy_is_up(self, dummy):
+ raise KeyError()
+
+ self.stubs.Set(db_driver.DbDriver, 'is_up', dummy_is_up)
+ req = FakeRequestWithHostService()
+ self.assertRaises(webob.exc.HTTPInternalServerError,
+ self.controller.index, req)
+
+ def test_services_disable(self):
+ req = fakes.HTTPRequestV3.blank('/os-services/disable')
+ body = {'service': {'host': 'host1',
+ 'binary': 'nova-compute'}}
+ res_dict = self.controller.update(req, "disable", body)
+
+ self.assertEqual(res_dict['service']['status'], 'disabled')
+ self.assertNotIn('disabled_reason', res_dict['service'])
+
+ def test_services_disable_with_invalid_host(self):
+ body = {'service': {'host': 'invalid',
+ 'binary': 'nova-compute'}}
+ req = fakes.HTTPRequestV3.blank('/os-services/disable')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update,
+ req,
+ "disable",
+ body)
+
+ def test_services_disable_with_invalid_binary(self):
+ body = {'service': {'host': 'host1',
+ 'binary': 'invalid'}}
+ req = fakes.HTTPRequestV3.blank('/os-services/disable')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update,
+ req,
+ "disable",
+ body)
+
+ def test_services_disable_log_reason(self):
+ req = \
+ fakes.HTTPRequestV3.blank('/os-services/disable-log-reason')
+ body = {'service': {'host': 'host1',
+ 'binary': 'nova-compute',
+ 'disabled_reason': 'test-reason'}}
+ res_dict = self.controller.update(req, "disable-log-reason", body)
+
+ self.assertEqual(res_dict['service']['status'], 'disabled')
+ self.assertEqual(res_dict['service']['disabled_reason'], 'test-reason')
+
+ def test_mandatory_reason_field(self):
+ req = \
+ fakes.HTTPRequestV3.blank('/os-services/disable-log-reason')
+ body = {'service': {'host': 'host1',
+ 'binary': 'nova-compute'}}
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, "disable-log-reason", body)
+
+ def test_invalid_reason_field(self):
+ reason = ' '
+ self.assertFalse(self.controller._is_valid_as_reason(reason))
+ reason = 'a' * 256
+ self.assertFalse(self.controller._is_valid_as_reason(reason))
+ reason = 'it\'s a valid reason.'
+ self.assertTrue(self.controller._is_valid_as_reason(reason))
+
+ def test_services_delete(self):
+ request = fakes.HTTPRequestV3.blank('/v3/os-services/1',
+ use_admin_context=True)
+ request.method = 'DELETE'
+
+ with mock.patch.object(self.controller.host_api,
+ 'service_delete') as service_delete:
+ self.controller.delete(request, '1')
+ service_delete.assert_called_once_with(
+ request.environ['nova.context'], '1')
+ self.assertEqual(self.controller.delete.wsgi_code, 204)
+
+ def test_services_delete_not_found(self):
+ request = fakes.HTTPRequestV3.blank('/v3/os-services/abc',
+ use_admin_context=True)
+ request.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, request, 'abc')
+
+
+class ServicesCellsTest(test.TestCase):
+ def setUp(self):
+ super(ServicesCellsTest, self).setUp()
+
+ host_api = cells_api.HostAPI()
+
+ self.controller = services.ServiceController()
+ self.controller.host_api = host_api
+
+ self.stubs.Set(timeutils, "utcnow", fake_utcnow)
+ self.stubs.Set(timeutils, "utcnow_ts", fake_utcnow_ts)
+
+ services_list = []
+ for service in fake_services_list:
+ service = service.copy()
+ service['id'] = 'cell1@%d' % service['id']
+ services_list.append(service)
+
+ self.stubs.Set(host_api.cells_rpcapi, "service_get_all",
+ fake_service_get_all(services_list))
+
+ def test_services_detail(self):
+ req = FakeRequest()
+ res_dict = self.controller.index(req)
+ utc = iso8601.iso8601.Utc()
+ response = {'services': [
+ {'id': 'cell1@1',
+ 'binary': 'nova-scheduler',
+ 'host': 'host1',
+ 'zone': 'internal',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2,
+ tzinfo=utc),
+ 'disabled_reason': 'test1'},
+ {'id': 'cell1@2',
+ 'binary': 'nova-compute',
+ 'host': 'host1',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'up',
+ 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5,
+ tzinfo=utc),
+ 'disabled_reason': 'test2'},
+ {'id': 'cell1@3',
+ 'binary': 'nova-scheduler',
+ 'host': 'host2',
+ 'zone': 'internal',
+ 'status': 'enabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34,
+ tzinfo=utc),
+ 'disabled_reason': None},
+ {'id': 'cell1@4',
+ 'binary': 'nova-compute',
+ 'host': 'host2',
+ 'zone': 'nova',
+ 'status': 'disabled',
+ 'state': 'down',
+ 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38,
+ tzinfo=utc),
+ 'disabled_reason': 'test4'}]}
+ self.assertEqual(res_dict, response)
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_suspend_server.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_suspend_server.py
new file mode 100644
index 0000000000..b0b71a0229
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_suspend_server.py
@@ -0,0 +1,48 @@
+# Copyright 2013 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova.api.openstack.compute.plugins.v3 import suspend_server
+from nova.tests.unit.api.openstack.compute.plugins.v3 import \
+ admin_only_action_common
+from nova.tests.unit.api.openstack import fakes
+
+
+class SuspendServerTests(admin_only_action_common.CommonTests):
+ def setUp(self):
+ super(SuspendServerTests, self).setUp()
+ self.controller = suspend_server.SuspendServerController()
+ self.compute_api = self.controller.compute_api
+
+ def _fake_controller(*args, **kwargs):
+ return self.controller
+
+ self.stubs.Set(suspend_server, 'SuspendServerController',
+ _fake_controller)
+ self.app = fakes.wsgi_app_v21(init_only=('servers',
+ 'os-suspend-server'),
+ fake_auth_context=self.context)
+ self.mox.StubOutWithMock(self.compute_api, 'get')
+
+ def test_suspend_resume(self):
+ self._test_actions(['suspend', 'resume'])
+
+ def test_suspend_resume_with_non_existed_instance(self):
+ self._test_actions_with_non_existed_instance(['suspend', 'resume'])
+
+ def test_suspend_resume_raise_conflict_on_invalid_state(self):
+ self._test_actions_raise_conflict_on_invalid_state(['suspend',
+ 'resume'])
+
+ def test_actions_with_locked_instance(self):
+ self._test_actions_with_locked_instance(['suspend', 'resume'])
diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_user_data.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_user_data.py
new file mode 100644
index 0000000000..0e10c283f7
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_user_data.py
@@ -0,0 +1,195 @@
+# Copyright 2012 OpenStack Foundation
+# Copyright 2013 IBM Corp.
+# 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 base64
+import datetime
+import uuid
+
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+
+from nova.api.openstack.compute import plugins
+from nova.api.openstack.compute.plugins.v3 import servers
+from nova.api.openstack.compute.plugins.v3 import user_data
+from nova.compute import api as compute_api
+from nova.compute import flavors
+from nova import db
+from nova import exception
+from nova.network import manager
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+from nova.tests.unit.image import fake
+
+
+CONF = cfg.CONF
+FAKE_UUID = fakes.FAKE_UUID
+
+
+def fake_gen_uuid():
+ return FAKE_UUID
+
+
+def return_security_group(context, instance_id, security_group_id):
+ pass
+
+
+class ServersControllerCreateTest(test.TestCase):
+
+ def setUp(self):
+ """Shared implementation for tests below that create instance."""
+ super(ServersControllerCreateTest, self).setUp()
+
+ self.flags(verbose=True,
+ enable_instance_password=True)
+ self.instance_cache_num = 0
+ self.instance_cache_by_id = {}
+ self.instance_cache_by_uuid = {}
+
+ ext_info = plugins.LoadedExtensionInfo()
+ self.controller = servers.ServersController(extension_info=ext_info)
+ CONF.set_override('extensions_blacklist', 'os-user-data',
+ 'osapi_v3')
+ self.no_user_data_controller = servers.ServersController(
+ extension_info=ext_info)
+
+ def instance_create(context, inst):
+ inst_type = flavors.get_flavor_by_flavor_id(3)
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ def_image_ref = 'http://localhost/images/%s' % image_uuid
+ self.instance_cache_num += 1
+ instance = fake_instance.fake_db_instance(**{
+ 'id': self.instance_cache_num,
+ 'display_name': inst['display_name'] or 'test',
+ 'uuid': FAKE_UUID,
+ 'instance_type': inst_type,
+ 'access_ip_v4': '1.2.3.4',
+ 'access_ip_v6': 'fead::1234',
+ 'image_ref': inst.get('image_ref', def_image_ref),
+ 'user_id': 'fake',
+ 'project_id': 'fake',
+ 'reservation_id': inst['reservation_id'],
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ user_data.ATTRIBUTE_NAME: None,
+ "progress": 0,
+ "fixed_ips": [],
+ "task_state": "",
+ "vm_state": "",
+ "root_device_name": inst.get('root_device_name', 'vda'),
+ })
+
+ self.instance_cache_by_id[instance['id']] = instance
+ self.instance_cache_by_uuid[instance['uuid']] = instance
+ return instance
+
+ def instance_get(context, instance_id):
+ """Stub for compute/api create() pulling in instance after
+ scheduling
+ """
+ return self.instance_cache_by_id[instance_id]
+
+ def instance_update(context, uuid, values):
+ instance = self.instance_cache_by_uuid[uuid]
+ instance.update(values)
+ return instance
+
+ def server_update(context, instance_uuid, params):
+ inst = self.instance_cache_by_uuid[instance_uuid]
+ inst.update(params)
+ return (inst, inst)
+
+ def fake_method(*args, **kwargs):
+ pass
+
+ def project_get_networks(context, user_id):
+ return dict(id='1', host='localhost')
+
+ def queue_get_for(context, *args):
+ return 'network_topic'
+
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ fake.stub_out_image_service(self.stubs)
+ fakes.stub_out_nw_api(self.stubs)
+ self.stubs.Set(uuid, 'uuid4', fake_gen_uuid)
+ self.stubs.Set(db, 'instance_add_security_group',
+ return_security_group)
+ self.stubs.Set(db, 'project_get_networks',
+ project_get_networks)
+ self.stubs.Set(db, 'instance_create', instance_create)
+ self.stubs.Set(db, 'instance_system_metadata_update',
+ fake_method)
+ self.stubs.Set(db, 'instance_get', instance_get)
+ self.stubs.Set(db, 'instance_update', instance_update)
+ self.stubs.Set(db, 'instance_update_and_get_original',
+ server_update)
+ self.stubs.Set(manager.VlanManager, 'allocate_fixed_ip',
+ fake_method)
+
+ def _test_create_extra(self, params, no_image=False,
+ override_controller=None):
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ server = dict(name='server_test', imageRef=image_uuid, flavorRef=2)
+ if no_image:
+ server.pop('imageRef', None)
+ server.update(params)
+ body = dict(server=server)
+ req = fakes.HTTPRequestV3.blank('/servers')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ if override_controller:
+ server = override_controller.create(req, body=body).obj['server']
+ else:
+ server = self.controller.create(req, body=body).obj['server']
+ return server
+
+ def test_create_instance_with_user_data_disabled(self):
+ params = {user_data.ATTRIBUTE_NAME: base64.b64encode('fake')}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertNotIn('user_data', kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(
+ params,
+ override_controller=self.no_user_data_controller)
+
+ def test_create_instance_with_user_data_enabled(self):
+ params = {user_data.ATTRIBUTE_NAME: base64.b64encode('fake')}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIn('user_data', kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_user_data(self):
+ value = base64.b64encode("A random string")
+ params = {user_data.ATTRIBUTE_NAME: value}
+ server = self._test_create_extra(params)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_with_bad_user_data(self):
+ value = "A random string"
+ params = {user_data.ATTRIBUTE_NAME: value}
+ self.assertRaises(exception.ValidationError,
+ self._test_create_extra, params)
diff --git a/nova/tests/unit/api/openstack/compute/schemas/__init__.py b/nova/tests/unit/api/openstack/compute/schemas/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/__init__.py
diff --git a/nova/tests/unit/api/openstack/compute/schemas/test_schemas.py b/nova/tests/unit/api/openstack/compute/schemas/test_schemas.py
new file mode 100644
index 0000000000..c6ce82057e
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/test_schemas.py
@@ -0,0 +1,106 @@
+# Copyright 2012 OpenStack Foundation
+# 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 glob
+import os
+
+import lxml.etree
+
+from nova import test
+
+SCHEMAS = "nova/api/openstack/compute/schemas"
+
+
+class RelaxNGSchemaTestCase(test.NoDBTestCase):
+ """various validation tasks for the RelaxNG schemas
+
+ lxml.etree has no built-in way to validate an entire namespace
+ (i.e., multiple RelaxNG schema files defining elements in the same
+ namespace), so we define a few tests that should hopefully reduce
+ the risk of an inconsistent namespace
+ """
+
+ def _load_schema(self, schemafile):
+ return lxml.etree.RelaxNG(lxml.etree.parse(schemafile))
+
+ def _load_test_cases(self, path):
+ """load test cases from the given path."""
+ rv = dict(valid=[], invalid=[])
+ path = os.path.join(os.path.dirname(__file__), path)
+ for ctype in rv.keys():
+ for cfile in glob.glob(os.path.join(path, ctype, "*.xml")):
+ rv[ctype].append(lxml.etree.parse(cfile))
+ return rv
+
+ def _validate_schema(self, schemafile):
+ """validate a single RelaxNG schema file."""
+ try:
+ self._load_schema(schemafile)
+ except lxml.etree.RelaxNGParseError as err:
+ self.fail("%s is not a valid RelaxNG schema: %s" %
+ (schemafile, err))
+
+ def _api_versions(self):
+ """get a list of API versions."""
+ return [''] + [os.path.basename(v)
+ for v in glob.glob(os.path.join(SCHEMAS, "v*"))]
+
+ def _schema_files(self, api_version):
+ return glob.glob(os.path.join(SCHEMAS, api_version, "*.rng"))
+
+ def test_schema_validity(self):
+ for api_version in self._api_versions():
+ for schema in self._schema_files(api_version):
+ self._validate_schema(schema)
+
+ def test_schema_duplicate_elements(self):
+ for api_version in self._api_versions():
+ elements = dict()
+ duplicates = dict()
+ for schemafile in self._schema_files(api_version):
+ schema = lxml.etree.parse(schemafile)
+ fname = os.path.basename(schemafile)
+ if schema.getroot().tag != "element":
+ # we don't do any sort of validation on grammars
+ # yet
+ continue
+ el_name = schema.getroot().get("name")
+ if el_name in elements:
+ duplicates.setdefault(el_name,
+ [elements[el_name]]).append(fname)
+ else:
+ elements[el_name] = fname
+ self.assertEqual(len(duplicates), 0,
+ "Duplicate element definitions found: %s" %
+ "; ".join("%s in %s" % dup
+ for dup in duplicates.items()))
+
+ def test_schema_explicit_cases(self):
+ cases = {'v1.1/flavors.rng': self._load_test_cases("v1.1/flavors"),
+ 'v1.1/images.rng': self._load_test_cases("v1.1/images"),
+ 'v1.1/servers.rng': self._load_test_cases("v1.1/servers")}
+
+ for schemafile, caselists in cases.items():
+ schema = self._load_schema(os.path.join(SCHEMAS, schemafile))
+ for case in caselists['valid']:
+ self.assertTrue(schema.validate(case),
+ "Schema validation failed against %s: %s\n%s" %
+ (schemafile, schema.error_log, case))
+
+ for case in caselists['invalid']:
+ self.assertFalse(
+ schema.validate(case),
+ "Schema validation succeeded unexpectedly against %s: %s"
+ "\n%s" % (schemafile, schema.error_log, case))
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/mixed.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/mixed.xml
new file mode 100644
index 0000000000..df4368bf41
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/mixed.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<flavors xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- you cannot mix flavor references (i.e., with only name and id)
+ and specifications (with all attributes) in the same document
+ -->
+ <flavor name="foo" id="foo"/>
+ <flavor name="bar" id="bar" ram="bar" disk="bar" vcpus="bar"/>
+</flavors>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial.xml
new file mode 100644
index 0000000000..3343a7be59
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<flavors xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- this flavor is only partially specified -->
+ <flavor name="foo"/>
+</flavors>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial2.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial2.xml
new file mode 100644
index 0000000000..f67c5a82fe
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial2.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<flavors xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- a flavor can either have *only* name and id, or needs to also
+ have disk and vcpus in addition to ram -->
+ <flavor name="foo" id="foo" ram="foo"/>
+</flavors>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/empty.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/empty.xml
new file mode 100644
index 0000000000..36aa3936e7
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/empty.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<flavors xmlns="http://docs.openstack.org/compute/api/v1.1"/>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/full.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/full.xml
new file mode 100644
index 0000000000..59eafc8608
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/full.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<flavors xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <flavor name="foo" id="foo" ram="foo" disk="foo" vcpus="foo"/>
+</flavors>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/refs.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/refs.xml
new file mode 100644
index 0000000000..751b626258
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/refs.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<flavors xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <flavor name="foo" id="foo"/>
+ <flavor name="bar" id="bar"/>
+</flavors>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/mixed.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/mixed.xml
new file mode 100644
index 0000000000..8f7bf208ae
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/mixed.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<images xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- cannot mix refs and specs in the same document -->
+ <image name="foo" id="foo"/>
+ <image name="bar" id="bar" updated="1401991486" created="1401991486"
+ status="foo">
+ <metadata/>
+ </image>
+</images>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/no-metadata.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/no-metadata.xml
new file mode 100644
index 0000000000..435294e27c
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/no-metadata.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<images xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- image specs require a metadata child -->
+ <image name="foo" id="foo" updated="1401991486" created="1401991486"
+ status="foo"/>
+</images>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial.xml
new file mode 100644
index 0000000000..5637cce787
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<images xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- image refs require id -->
+ <image name="foo"/>
+</images>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial2.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial2.xml
new file mode 100644
index 0000000000..db5e974621
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial2.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<images xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- an image must be either a ref, with *only* name and id attrs,
+ or fully specified, with name, id, updated, created, status,
+ and a metadata child -->
+ <image name="foo" id="foo" updated="foo"/>
+</images>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/empty.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/empty.xml
new file mode 100644
index 0000000000..05e0b8241c
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/empty.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<images xmlns="http://docs.openstack.org/compute/api/v1.1"/>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/full.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/full.xml
new file mode 100644
index 0000000000..4f148db625
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/full.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<images xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <image name="foo" id="foo" updated="1401991486" created="1401991486"
+ status="foo">
+ <metadata/>
+ </image>
+ <image name="bar" id="bar" updated="1401991486" created="1401991486"
+ status="bar" progress="bar" minDisk="100" minRam="100">
+ <server id="bar"/>
+ <metadata>
+ <meta key="baz">baz</meta>
+ </metadata>
+ </image>
+</images>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/refs.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/refs.xml
new file mode 100644
index 0000000000..1dfedd2c77
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/refs.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<images xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <image name="foo" id="foo"/>
+ <image name="bar" id="bar"/>
+</images>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/mixed.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/mixed.xml
new file mode 100644
index 0000000000..c941472beb
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/mixed.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<servers xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom">
+ <!-- you cannot mix server refs and specs in the same document -->
+ <server name="foo" id="foo"/>
+ <server name="foo" userId="foo" tenantId="foo" id="foo" updated="1401991486"
+ created="1401991486" hostId="foo" accessIPv4="1.2.3.4"
+ accessIPv6="::1" status="foo">
+ <image id="foo">
+ <atom:link href="/compute/api/v1.1/image/foo"/>
+ </image>
+ <flavor id="foo">
+ <atom:link href="/compute/api/v1.1/flavor/foo"/>
+ </flavor>
+ <metadata/>
+ <addresses/>
+ </server>
+</servers>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial.xml
new file mode 100644
index 0000000000..721ce84327
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<servers xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- server refs require the id attr -->
+ <server name="foo"/>
+</servers>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial2.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial2.xml
new file mode 100644
index 0000000000..474b3a084e
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial2.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<servers xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- server tags must either be refs, with *only* name and id, or
+ full specifications, with loads more detail -->
+ <server name="foo" id="foo" updated="foo"/>
+</servers>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial3.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial3.xml
new file mode 100644
index 0000000000..6455fe899a
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial3.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<servers xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <!-- the server specification requires a number of children -->
+ <server name="foo" userId="foo" tenantId="foo" id="foo" updated="1401991486"
+ created="1401991486" hostId="foo" accessIPv4="1.2.3.4"
+ accessIPv6="::1" status="foo"/>
+</servers>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/detailed.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/detailed.xml
new file mode 100644
index 0000000000..97f5ee44e6
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/detailed.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<servers xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom">
+ <server name="bar" userId="bar" tenantId="bar" id="bar" updated="1401991486"
+ created="1401991486" hostId="bar" accessIPv4="1.2.3.4"
+ accessIPv6="::1" status="bar" progress="10" adminPass="bar">
+ <image id="foo">
+ <atom:link href="/compute/api/v1.1/image/foo"/>
+ </image>
+ <flavor id="foo">
+ <atom:link href="/compute/api/v1.1/flavor/foo"/>
+ </flavor>
+ <fault code="1" created="1401991486">
+ <message>fault</message>
+ <details>fault</details>
+ </fault>
+ <metadata>
+ <meta key="bar">bar</meta>
+ </metadata>
+ <addresses>
+ <network id="bar"/>
+ <network id="baz">
+ <ip version="4" addr="1.2.3.4"/>
+ </network>
+ </addresses>
+ </server>
+</servers>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/empty.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/empty.xml
new file mode 100644
index 0000000000..b2f3666245
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/empty.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<servers xmlns="http://docs.openstack.org/compute/api/v1.1"/>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/full.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/full.xml
new file mode 100644
index 0000000000..fbd6202a76
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/full.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<servers xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:atom="http://www.w3.org/2005/Atom">
+ <server name="foo" userId="foo" tenantId="foo" id="foo" updated="1401991486"
+ created="1401991486" hostId="foo" accessIPv4="1.2.3.4"
+ accessIPv6="::1" status="foo">
+ <image id="foo">
+ <atom:link href="/compute/api/v1.1/image/foo"/>
+ </image>
+ <flavor id="foo">
+ <atom:link href="/compute/api/v1.1/flavor/foo"/>
+ </flavor>
+ <metadata/>
+ <addresses/>
+ </server>
+</servers>
diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/refs.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/refs.xml
new file mode 100644
index 0000000000..e1212e985f
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/refs.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<servers xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <server name="foo" id="foo"/>
+ <server name="bar" id="bar"/>
+</servers>
diff --git a/nova/tests/unit/api/openstack/compute/test_api.py b/nova/tests/unit/api/openstack/compute/test_api.py
new file mode 100644
index 0000000000..f86c04d4bd
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_api.py
@@ -0,0 +1,186 @@
+# Copyright 2010 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+import webob.dec
+import webob.exc
+
+from nova.api import openstack as openstack_api
+from nova.api.openstack import wsgi
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+class APITest(test.NoDBTestCase):
+
+ def _wsgi_app(self, inner_app):
+ # simpler version of the app than fakes.wsgi_app
+ return openstack_api.FaultWrapper(inner_app)
+
+ def test_malformed_json(self):
+ req = webob.Request.blank('/')
+ req.method = 'POST'
+ req.body = '{'
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_malformed_xml(self):
+ req = webob.Request.blank('/')
+ req.method = 'POST'
+ req.body = '<hi im not xml>'
+ req.headers["content-type"] = "application/xml"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_vendor_content_type_json(self):
+ ctype = 'application/vnd.openstack.compute+json'
+
+ req = webob.Request.blank('/')
+ req.headers['Accept'] = ctype
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, ctype)
+
+ jsonutils.loads(res.body)
+
+ def test_vendor_content_type_xml(self):
+ ctype = 'application/vnd.openstack.compute+xml'
+
+ req = webob.Request.blank('/')
+ req.headers['Accept'] = ctype
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, ctype)
+
+ etree.XML(res.body)
+
+ def test_exceptions_are_converted_to_faults_webob_exc(self):
+ @webob.dec.wsgify
+ def raise_webob_exc(req):
+ raise webob.exc.HTTPNotFound(explanation='Raised a webob.exc')
+
+ # api.application = raise_webob_exc
+ api = self._wsgi_app(raise_webob_exc)
+ resp = webob.Request.blank('/').get_response(api)
+ self.assertEqual(resp.status_int, 404, resp.body)
+
+ def test_exceptions_are_converted_to_faults_api_fault(self):
+ @webob.dec.wsgify
+ def raise_api_fault(req):
+ exc = webob.exc.HTTPNotFound(explanation='Raised a webob.exc')
+ return wsgi.Fault(exc)
+
+ # api.application = raise_api_fault
+ api = self._wsgi_app(raise_api_fault)
+ resp = webob.Request.blank('/').get_response(api)
+ self.assertIn('itemNotFound', resp.body)
+ self.assertEqual(resp.status_int, 404, resp.body)
+
+ def test_exceptions_are_converted_to_faults_exception(self):
+ @webob.dec.wsgify
+ def fail(req):
+ raise Exception("Threw an exception")
+
+ # api.application = fail
+ api = self._wsgi_app(fail)
+ resp = webob.Request.blank('/').get_response(api)
+ self.assertIn('{"computeFault', resp.body)
+ self.assertEqual(resp.status_int, 500, resp.body)
+
+ def test_exceptions_are_converted_to_faults_exception_xml(self):
+ @webob.dec.wsgify
+ def fail(req):
+ raise Exception("Threw an exception")
+
+ # api.application = fail
+ api = self._wsgi_app(fail)
+ resp = webob.Request.blank('/.xml').get_response(api)
+ self.assertIn('<computeFault', resp.body)
+ self.assertEqual(resp.status_int, 500, resp.body)
+
+ def _do_test_exception_safety_reflected_in_faults(self, expose):
+ class ExceptionWithSafety(exception.NovaException):
+ safe = expose
+
+ @webob.dec.wsgify
+ def fail(req):
+ raise ExceptionWithSafety('some explanation')
+
+ api = self._wsgi_app(fail)
+ resp = webob.Request.blank('/').get_response(api)
+ self.assertIn('{"computeFault', resp.body)
+ expected = ('ExceptionWithSafety: some explanation' if expose else
+ 'The server has either erred or is incapable '
+ 'of performing the requested operation.')
+ self.assertIn(expected, resp.body)
+ self.assertEqual(resp.status_int, 500, resp.body)
+
+ def test_safe_exceptions_are_described_in_faults(self):
+ self._do_test_exception_safety_reflected_in_faults(True)
+
+ def test_unsafe_exceptions_are_not_described_in_faults(self):
+ self._do_test_exception_safety_reflected_in_faults(False)
+
+ def _do_test_exception_mapping(self, exception_type, msg):
+ @webob.dec.wsgify
+ def fail(req):
+ raise exception_type(msg)
+
+ api = self._wsgi_app(fail)
+ resp = webob.Request.blank('/').get_response(api)
+ self.assertIn(msg, resp.body)
+ self.assertEqual(resp.status_int, exception_type.code, resp.body)
+
+ if hasattr(exception_type, 'headers'):
+ for (key, value) in exception_type.headers.iteritems():
+ self.assertIn(key, resp.headers)
+ self.assertEqual(resp.headers[key], str(value))
+
+ def test_quota_error_mapping(self):
+ self._do_test_exception_mapping(exception.QuotaError, 'too many used')
+
+ def test_non_nova_notfound_exception_mapping(self):
+ class ExceptionWithCode(Exception):
+ code = 404
+
+ self._do_test_exception_mapping(ExceptionWithCode,
+ 'NotFound')
+
+ def test_non_nova_exception_mapping(self):
+ class ExceptionWithCode(Exception):
+ code = 417
+
+ self._do_test_exception_mapping(ExceptionWithCode,
+ 'Expectation failed')
+
+ def test_exception_with_none_code_throws_500(self):
+ class ExceptionWithNoneCode(Exception):
+ code = None
+
+ @webob.dec.wsgify
+ def fail(req):
+ raise ExceptionWithNoneCode()
+
+ api = self._wsgi_app(fail)
+ resp = webob.Request.blank('/').get_response(api)
+ self.assertEqual(500, resp.status_int)
diff --git a/nova/tests/unit/api/openstack/compute/test_auth.py b/nova/tests/unit/api/openstack/compute/test_auth.py
new file mode 100644
index 0000000000..0386623b5d
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_auth.py
@@ -0,0 +1,61 @@
+# Copyright 2010 OpenStack Foundation
+# 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
+import webob.dec
+
+from nova import context
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+class TestNoAuthMiddleware(test.NoDBTestCase):
+
+ def setUp(self):
+ super(TestNoAuthMiddleware, self).setUp()
+ self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext)
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_networking(self.stubs)
+
+ def test_authorize_user(self):
+ req = webob.Request.blank('/v2')
+ req.headers['X-Auth-User'] = 'user1'
+ req.headers['X-Auth-Key'] = 'user1_key'
+ req.headers['X-Auth-Project-Id'] = 'user1_project'
+ result = req.get_response(fakes.wsgi_app(use_no_auth=True))
+ self.assertEqual(result.status, '204 No Content')
+ self.assertEqual(result.headers['X-Server-Management-Url'],
+ "http://localhost/v2/user1_project")
+
+ def test_authorize_user_trailing_slash(self):
+ # make sure it works with trailing slash on the request
+ req = webob.Request.blank('/v2/')
+ req.headers['X-Auth-User'] = 'user1'
+ req.headers['X-Auth-Key'] = 'user1_key'
+ req.headers['X-Auth-Project-Id'] = 'user1_project'
+ result = req.get_response(fakes.wsgi_app(use_no_auth=True))
+ self.assertEqual(result.status, '204 No Content')
+ self.assertEqual(result.headers['X-Server-Management-Url'],
+ "http://localhost/v2/user1_project")
+
+ def test_auth_token_no_empty_headers(self):
+ req = webob.Request.blank('/v2')
+ req.headers['X-Auth-User'] = 'user1'
+ req.headers['X-Auth-Key'] = 'user1_key'
+ req.headers['X-Auth-Project-Id'] = 'user1_project'
+ result = req.get_response(fakes.wsgi_app(use_no_auth=True))
+ self.assertEqual(result.status, '204 No Content')
+ self.assertNotIn('X-CDN-Management-Url', result.headers)
+ self.assertNotIn('X-Storage-Url', result.headers)
diff --git a/nova/tests/unit/api/openstack/compute/test_consoles.py b/nova/tests/unit/api/openstack/compute/test_consoles.py
new file mode 100644
index 0000000000..3ba99899c0
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_consoles.py
@@ -0,0 +1,293 @@
+# Copyright 2010-2011 OpenStack Foundation
+# Copyright 2011 Piston Cloud Computing, Inc.
+# 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 datetime
+import uuid as stdlib_uuid
+
+from lxml import etree
+from oslo.utils import timeutils
+import webob
+
+from nova.api.openstack.compute import consoles
+from nova.compute import vm_states
+from nova import console
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import matchers
+
+
+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+
+
+class FakeInstanceDB(object):
+
+ def __init__(self):
+ self.instances_by_id = {}
+ self.ids_by_uuid = {}
+ self.max_id = 0
+
+ def return_server_by_id(self, context, id):
+ if id not in self.instances_by_id:
+ self._add_server(id=id)
+ return dict(self.instances_by_id[id])
+
+ def return_server_by_uuid(self, context, uuid):
+ if uuid not in self.ids_by_uuid:
+ self._add_server(uuid=uuid)
+ return dict(self.instances_by_id[self.ids_by_uuid[uuid]])
+
+ def _add_server(self, id=None, uuid=None):
+ if id is None:
+ id = self.max_id + 1
+ if uuid is None:
+ uuid = str(stdlib_uuid.uuid4())
+ instance = stub_instance(id, uuid=uuid)
+ self.instances_by_id[id] = instance
+ self.ids_by_uuid[uuid] = id
+ if id > self.max_id:
+ self.max_id = id
+
+
+def stub_instance(id, user_id='fake', project_id='fake', host=None,
+ vm_state=None, task_state=None,
+ reservation_id="", uuid=FAKE_UUID, image_ref="10",
+ flavor_id="1", name=None, key_name='',
+ access_ipv4=None, access_ipv6=None, progress=0):
+
+ if host is not None:
+ host = str(host)
+
+ if key_name:
+ key_data = 'FAKE'
+ else:
+ key_data = ''
+
+ # ReservationID isn't sent back, hack it in there.
+ server_name = name or "server%s" % id
+ if reservation_id != "":
+ server_name = "reservation_%s" % (reservation_id, )
+
+ instance = {
+ "id": int(id),
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "admin_pass": "",
+ "user_id": user_id,
+ "project_id": project_id,
+ "image_ref": image_ref,
+ "kernel_id": "",
+ "ramdisk_id": "",
+ "launch_index": 0,
+ "key_name": key_name,
+ "key_data": key_data,
+ "vm_state": vm_state or vm_states.BUILDING,
+ "task_state": task_state,
+ "memory_mb": 0,
+ "vcpus": 0,
+ "root_gb": 0,
+ "hostname": "",
+ "host": host,
+ "instance_type": {},
+ "user_data": "",
+ "reservation_id": reservation_id,
+ "mac_address": "",
+ "scheduled_at": timeutils.utcnow(),
+ "launched_at": timeutils.utcnow(),
+ "terminated_at": timeutils.utcnow(),
+ "availability_zone": "",
+ "display_name": server_name,
+ "display_description": "",
+ "locked": False,
+ "metadata": [],
+ "access_ip_v4": access_ipv4,
+ "access_ip_v6": access_ipv6,
+ "uuid": uuid,
+ "progress": progress}
+
+ return instance
+
+
+class ConsolesControllerTest(test.NoDBTestCase):
+ def setUp(self):
+ super(ConsolesControllerTest, self).setUp()
+ self.flags(verbose=True)
+ self.instance_db = FakeInstanceDB()
+ self.stubs.Set(db, 'instance_get',
+ self.instance_db.return_server_by_id)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ self.instance_db.return_server_by_uuid)
+ self.uuid = str(stdlib_uuid.uuid4())
+ self.url = '/v2/fake/servers/%s/consoles' % self.uuid
+ self.controller = consoles.Controller()
+
+ def test_create_console(self):
+ def fake_create_console(cons_self, context, instance_id):
+ self.assertEqual(instance_id, self.uuid)
+ return {}
+ self.stubs.Set(console.api.API, 'create_console', fake_create_console)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.controller.create(req, self.uuid, None)
+
+ def test_show_console(self):
+ def fake_get_console(cons_self, context, instance_id, console_id):
+ self.assertEqual(instance_id, self.uuid)
+ self.assertEqual(console_id, 20)
+ pool = dict(console_type='fake_type',
+ public_hostname='fake_hostname')
+ return dict(id=console_id, password='fake_password',
+ port='fake_port', pool=pool, instance_name='inst-0001')
+
+ expected = {'console': {'id': 20,
+ 'port': 'fake_port',
+ 'host': 'fake_hostname',
+ 'password': 'fake_password',
+ 'instance_name': 'inst-0001',
+ 'console_type': 'fake_type'}}
+
+ self.stubs.Set(console.api.API, 'get_console', fake_get_console)
+
+ req = fakes.HTTPRequest.blank(self.url + '/20')
+ res_dict = self.controller.show(req, self.uuid, '20')
+ self.assertThat(res_dict, matchers.DictMatches(expected))
+
+ def test_show_console_unknown_console(self):
+ def fake_get_console(cons_self, context, instance_id, console_id):
+ raise exception.ConsoleNotFound(console_id=console_id)
+
+ self.stubs.Set(console.api.API, 'get_console', fake_get_console)
+
+ req = fakes.HTTPRequest.blank(self.url + '/20')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
+ req, self.uuid, '20')
+
+ def test_show_console_unknown_instance(self):
+ def fake_get_console(cons_self, context, instance_id, console_id):
+ raise exception.InstanceNotFound(instance_id=instance_id)
+
+ self.stubs.Set(console.api.API, 'get_console', fake_get_console)
+
+ req = fakes.HTTPRequest.blank(self.url + '/20')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
+ req, self.uuid, '20')
+
+ def test_list_consoles(self):
+ def fake_get_consoles(cons_self, context, instance_id):
+ self.assertEqual(instance_id, self.uuid)
+
+ pool1 = dict(console_type='fake_type',
+ public_hostname='fake_hostname')
+ cons1 = dict(id=10, password='fake_password',
+ port='fake_port', pool=pool1)
+ pool2 = dict(console_type='fake_type2',
+ public_hostname='fake_hostname2')
+ cons2 = dict(id=11, password='fake_password2',
+ port='fake_port2', pool=pool2)
+ return [cons1, cons2]
+
+ expected = {'consoles':
+ [{'console': {'id': 10, 'console_type': 'fake_type'}},
+ {'console': {'id': 11, 'console_type': 'fake_type2'}}]}
+
+ self.stubs.Set(console.api.API, 'get_consoles', fake_get_consoles)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ res_dict = self.controller.index(req, self.uuid)
+ self.assertThat(res_dict, matchers.DictMatches(expected))
+
+ def test_delete_console(self):
+ def fake_get_console(cons_self, context, instance_id, console_id):
+ self.assertEqual(instance_id, self.uuid)
+ self.assertEqual(console_id, 20)
+ pool = dict(console_type='fake_type',
+ public_hostname='fake_hostname')
+ return dict(id=console_id, password='fake_password',
+ port='fake_port', pool=pool)
+
+ def fake_delete_console(cons_self, context, instance_id, console_id):
+ self.assertEqual(instance_id, self.uuid)
+ self.assertEqual(console_id, 20)
+
+ self.stubs.Set(console.api.API, 'get_console', fake_get_console)
+ self.stubs.Set(console.api.API, 'delete_console', fake_delete_console)
+
+ req = fakes.HTTPRequest.blank(self.url + '/20')
+ self.controller.delete(req, self.uuid, '20')
+
+ def test_delete_console_unknown_console(self):
+ def fake_delete_console(cons_self, context, instance_id, console_id):
+ raise exception.ConsoleNotFound(console_id=console_id)
+
+ self.stubs.Set(console.api.API, 'delete_console', fake_delete_console)
+
+ req = fakes.HTTPRequest.blank(self.url + '/20')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, self.uuid, '20')
+
+ def test_delete_console_unknown_instance(self):
+ def fake_delete_console(cons_self, context, instance_id, console_id):
+ raise exception.InstanceNotFound(instance_id=instance_id)
+
+ self.stubs.Set(console.api.API, 'delete_console', fake_delete_console)
+
+ req = fakes.HTTPRequest.blank(self.url + '/20')
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+ req, self.uuid, '20')
+
+
+class TestConsolesXMLSerializer(test.NoDBTestCase):
+ def test_show(self):
+ fixture = {'console': {'id': 20,
+ 'password': 'fake_password',
+ 'port': 'fake_port',
+ 'host': 'fake_hostname',
+ 'console_type': 'fake_type'}}
+
+ output = consoles.ConsoleTemplate().serialize(fixture)
+ res_tree = etree.XML(output)
+
+ self.assertEqual(res_tree.tag, 'console')
+ self.assertEqual(res_tree.xpath('id')[0].text, '20')
+ self.assertEqual(res_tree.xpath('port')[0].text, 'fake_port')
+ self.assertEqual(res_tree.xpath('host')[0].text, 'fake_hostname')
+ self.assertEqual(res_tree.xpath('password')[0].text, 'fake_password')
+ self.assertEqual(res_tree.xpath('console_type')[0].text, 'fake_type')
+
+ def test_index(self):
+ fixture = {'consoles': [{'console': {'id': 10,
+ 'console_type': 'fake_type'}},
+ {'console': {'id': 11,
+ 'console_type': 'fake_type2'}}]}
+
+ output = consoles.ConsolesTemplate().serialize(fixture)
+ res_tree = etree.XML(output)
+
+ self.assertEqual(res_tree.tag, 'consoles')
+ self.assertEqual(len(res_tree), 2)
+ self.assertEqual(res_tree[0].tag, 'console')
+ self.assertEqual(res_tree[1].tag, 'console')
+ self.assertEqual(len(res_tree[0]), 1)
+ self.assertEqual(res_tree[0][0].tag, 'console')
+ self.assertEqual(len(res_tree[1]), 1)
+ self.assertEqual(res_tree[1][0].tag, 'console')
+ self.assertEqual(res_tree[0][0].xpath('id')[0].text, '10')
+ self.assertEqual(res_tree[1][0].xpath('id')[0].text, '11')
+ self.assertEqual(res_tree[0][0].xpath('console_type')[0].text,
+ 'fake_type')
+ self.assertEqual(res_tree[1][0].xpath('console_type')[0].text,
+ 'fake_type2')
diff --git a/nova/tests/unit/api/openstack/compute/test_extensions.py b/nova/tests/unit/api/openstack/compute/test_extensions.py
new file mode 100644
index 0000000000..cf84fc1f84
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_extensions.py
@@ -0,0 +1,747 @@
+# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
+# Copyright 2011 OpenStack Foundation
+# 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 iso8601
+from lxml import etree
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack import compute
+from nova.api.openstack.compute import extensions as compute_extensions
+from nova.api.openstack import extensions as base_extensions
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova import exception
+import nova.policy
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import matchers
+
+CONF = cfg.CONF
+
+NS = "{http://docs.openstack.org/common/api/v1.0}"
+ATOMNS = "{http://www.w3.org/2005/Atom}"
+response_body = "Try to say this Mr. Knox, sir..."
+extension_body = "I am not a fox!"
+
+
+class StubController(object):
+
+ def __init__(self, body):
+ self.body = body
+
+ def index(self, req):
+ return self.body
+
+ def create(self, req, body):
+ msg = 'All aboard the fail train!'
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+
+ def show(self, req, id):
+ raise webob.exc.HTTPNotFound()
+
+
+class StubActionController(wsgi.Controller):
+ def __init__(self, body):
+ self.body = body
+
+ @wsgi.action('fooAction')
+ def _action_foo(self, req, id, body):
+ return self.body
+
+
+class StubControllerExtension(base_extensions.ExtensionDescriptor):
+ name = 'twaadle'
+
+ def __init__(self):
+ pass
+
+
+class StubEarlyExtensionController(wsgi.Controller):
+ def __init__(self, body):
+ self.body = body
+
+ @wsgi.extends
+ def index(self, req):
+ yield self.body
+
+ @wsgi.extends(action='fooAction')
+ def _action_foo(self, req, id, body):
+ yield self.body
+
+
+class StubLateExtensionController(wsgi.Controller):
+ def __init__(self, body):
+ self.body = body
+
+ @wsgi.extends
+ def index(self, req, resp_obj):
+ return self.body
+
+ @wsgi.extends(action='fooAction')
+ def _action_foo(self, req, resp_obj, id, body):
+ return self.body
+
+
+class StubExtensionManager(object):
+ """Provides access to Tweedle Beetles."""
+
+ name = "Tweedle Beetle Extension"
+ alias = "TWDLBETL"
+
+ def __init__(self, resource_ext=None, action_ext=None, request_ext=None,
+ controller_ext=None):
+ self.resource_ext = resource_ext
+ self.action_ext = action_ext
+ self.request_ext = request_ext
+ self.controller_ext = controller_ext
+ self.extra_resource_ext = None
+
+ def get_resources(self):
+ resource_exts = []
+ if self.resource_ext:
+ resource_exts.append(self.resource_ext)
+ if self.extra_resource_ext:
+ resource_exts.append(self.extra_resource_ext)
+ return resource_exts
+
+ def get_actions(self):
+ action_exts = []
+ if self.action_ext:
+ action_exts.append(self.action_ext)
+ return action_exts
+
+ def get_request_extensions(self):
+ request_extensions = []
+ if self.request_ext:
+ request_extensions.append(self.request_ext)
+ return request_extensions
+
+ def get_controller_extensions(self):
+ controller_extensions = []
+ if self.controller_ext:
+ controller_extensions.append(self.controller_ext)
+ return controller_extensions
+
+
+class ExtensionTestCase(test.TestCase):
+ def setUp(self):
+ super(ExtensionTestCase, self).setUp()
+ ext_list = CONF.osapi_compute_extension[:]
+ fox = ('nova.tests.unit.api.openstack.compute.extensions.'
+ 'foxinsocks.Foxinsocks')
+ if fox not in ext_list:
+ ext_list.append(fox)
+ self.flags(osapi_compute_extension=ext_list)
+ self.fake_context = nova.context.RequestContext('fake', 'fake')
+
+ def test_extension_authorizer_throws_exception_if_policy_fails(self):
+ target = {'project_id': '1234',
+ 'user_id': '5678'}
+ self.mox.StubOutWithMock(nova.policy, 'enforce')
+ nova.policy.enforce(self.fake_context,
+ "compute_extension:used_limits_for_admin",
+ target).AndRaise(
+ exception.PolicyNotAuthorized(
+ action="compute_extension:used_limits_for_admin"))
+ self.mox.ReplayAll()
+ authorize = base_extensions.extension_authorizer('compute',
+ 'used_limits_for_admin'
+ )
+ self.assertRaises(exception.PolicyNotAuthorized, authorize,
+ self.fake_context, target=target)
+
+ def test_core_authorizer_throws_exception_if_policy_fails(self):
+ target = {'project_id': '1234',
+ 'user_id': '5678'}
+ self.mox.StubOutWithMock(nova.policy, 'enforce')
+ nova.policy.enforce(self.fake_context,
+ "compute:used_limits_for_admin",
+ target).AndRaise(
+ exception.PolicyNotAuthorized(
+ action="compute:used_limits_for_admin"))
+ self.mox.ReplayAll()
+ authorize = base_extensions.core_authorizer('compute',
+ 'used_limits_for_admin'
+ )
+ self.assertRaises(exception.PolicyNotAuthorized, authorize,
+ self.fake_context, target=target)
+
+
+class ExtensionControllerTest(ExtensionTestCase):
+
+ def setUp(self):
+ super(ExtensionControllerTest, self).setUp()
+ self.ext_list = [
+ "AdminActions",
+ "Aggregates",
+ "AssistedVolumeSnapshots",
+ "AvailabilityZone",
+ "Agents",
+ "Certificates",
+ "Cloudpipe",
+ "CloudpipeUpdate",
+ "ConsoleOutput",
+ "Consoles",
+ "Createserverext",
+ "DeferredDelete",
+ "DiskConfig",
+ "ExtendedAvailabilityZone",
+ "ExtendedFloatingIps",
+ "ExtendedIps",
+ "ExtendedIpsMac",
+ "ExtendedVIFNet",
+ "Evacuate",
+ "ExtendedStatus",
+ "ExtendedVolumes",
+ "ExtendedServerAttributes",
+ "FixedIPs",
+ "FlavorAccess",
+ "FlavorDisabled",
+ "FlavorExtraSpecs",
+ "FlavorExtraData",
+ "FlavorManage",
+ "FlavorRxtx",
+ "FlavorSwap",
+ "FloatingIps",
+ "FloatingIpDns",
+ "FloatingIpPools",
+ "FloatingIpsBulk",
+ "Fox In Socks",
+ "Hosts",
+ "ImageSize",
+ "InstanceActions",
+ "Keypairs",
+ "Multinic",
+ "MultipleCreate",
+ "QuotaClasses",
+ "Quotas",
+ "ExtendedQuotas",
+ "Rescue",
+ "SchedulerHints",
+ "SecurityGroupDefaultRules",
+ "SecurityGroups",
+ "ServerDiagnostics",
+ "ServerListMultiStatus",
+ "ServerPassword",
+ "ServerStartStop",
+ "Services",
+ "SimpleTenantUsage",
+ "UsedLimits",
+ "UserData",
+ "VirtualInterfaces",
+ "VolumeAttachmentUpdate",
+ "Volumes",
+ ]
+ self.ext_list.sort()
+
+ def test_list_extensions_json(self):
+ app = compute.APIRouter(init_only=('extensions',))
+ request = webob.Request.blank("/fake/extensions")
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+
+ # Make sure we have all the extensions, extra extensions being OK.
+ data = jsonutils.loads(response.body)
+ names = [str(x['name']) for x in data['extensions']
+ if str(x['name']) in self.ext_list]
+ names.sort()
+ self.assertEqual(names, self.ext_list)
+
+ # Ensure all the timestamps are valid according to iso8601
+ for ext in data['extensions']:
+ iso8601.parse_date(ext['updated'])
+
+ # Make sure that at least Fox in Sox is correct.
+ (fox_ext, ) = [
+ x for x in data['extensions'] if x['alias'] == 'FOXNSOX']
+ self.assertEqual(fox_ext, {
+ 'namespace': 'http://www.fox.in.socks/api/ext/pie/v1.0',
+ 'name': 'Fox In Socks',
+ 'updated': '2011-01-22T13:25:27-06:00',
+ 'description': 'The Fox In Socks Extension.',
+ 'alias': 'FOXNSOX',
+ 'links': []
+ },
+ )
+
+ for ext in data['extensions']:
+ url = '/fake/extensions/%s' % ext['alias']
+ request = webob.Request.blank(url)
+ response = request.get_response(app)
+ output = jsonutils.loads(response.body)
+ self.assertEqual(output['extension']['alias'], ext['alias'])
+
+ def test_get_extension_json(self):
+ app = compute.APIRouter(init_only=('extensions',))
+ request = webob.Request.blank("/fake/extensions/FOXNSOX")
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+
+ data = jsonutils.loads(response.body)
+ self.assertEqual(data['extension'], {
+ "namespace": "http://www.fox.in.socks/api/ext/pie/v1.0",
+ "name": "Fox In Socks",
+ "updated": "2011-01-22T13:25:27-06:00",
+ "description": "The Fox In Socks Extension.",
+ "alias": "FOXNSOX",
+ "links": []})
+
+ def test_get_non_existing_extension_json(self):
+ app = compute.APIRouter(init_only=('extensions',))
+ request = webob.Request.blank("/fake/extensions/4")
+ response = request.get_response(app)
+ self.assertEqual(404, response.status_int)
+
+ def test_list_extensions_xml(self):
+ app = compute.APIRouter(init_only=('servers', 'flavors', 'extensions'))
+ request = webob.Request.blank("/fake/extensions")
+ request.accept = "application/xml"
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+
+ root = etree.XML(response.body)
+ self.assertEqual(root.tag.split('extensions')[0], NS)
+
+ # Make sure we have all the extensions, extras extensions being OK.
+ exts = root.findall('{0}extension'.format(NS))
+ self.assertTrue(len(exts) >= len(self.ext_list))
+
+ # Make sure that at least Fox in Sox is correct.
+ (fox_ext, ) = [x for x in exts if x.get('alias') == 'FOXNSOX']
+ self.assertEqual(fox_ext.get('name'), 'Fox In Socks')
+ self.assertEqual(fox_ext.get('namespace'),
+ 'http://www.fox.in.socks/api/ext/pie/v1.0')
+ self.assertEqual(fox_ext.get('updated'), '2011-01-22T13:25:27-06:00')
+ self.assertEqual(fox_ext.findtext('{0}description'.format(NS)),
+ 'The Fox In Socks Extension.')
+
+ xmlutil.validate_schema(root, 'extensions')
+
+ def test_get_extension_xml(self):
+ app = compute.APIRouter(init_only=('servers', 'flavors', 'extensions'))
+ request = webob.Request.blank("/fake/extensions/FOXNSOX")
+ request.accept = "application/xml"
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ xml = response.body
+
+ root = etree.XML(xml)
+ self.assertEqual(root.tag.split('extension')[0], NS)
+ self.assertEqual(root.get('alias'), 'FOXNSOX')
+ self.assertEqual(root.get('name'), 'Fox In Socks')
+ self.assertEqual(root.get('namespace'),
+ 'http://www.fox.in.socks/api/ext/pie/v1.0')
+ self.assertEqual(root.get('updated'), '2011-01-22T13:25:27-06:00')
+ self.assertEqual(root.findtext('{0}description'.format(NS)),
+ 'The Fox In Socks Extension.')
+
+ xmlutil.validate_schema(root, 'extension')
+
+
+class ResourceExtensionTest(ExtensionTestCase):
+
+ def test_no_extension_present(self):
+ manager = StubExtensionManager(None)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/blah")
+ response = request.get_response(app)
+ self.assertEqual(404, response.status_int)
+
+ def test_get_resources(self):
+ res_ext = base_extensions.ResourceExtension('tweedles',
+ StubController(response_body))
+ manager = StubExtensionManager(res_ext)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/tweedles")
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(response_body, response.body)
+
+ def test_get_resources_with_controller(self):
+ res_ext = base_extensions.ResourceExtension('tweedles',
+ StubController(response_body))
+ manager = StubExtensionManager(res_ext)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/tweedles")
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(response_body, response.body)
+
+ def test_bad_request(self):
+ res_ext = base_extensions.ResourceExtension('tweedles',
+ StubController(response_body))
+ manager = StubExtensionManager(res_ext)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/tweedles")
+ request.method = "POST"
+ response = request.get_response(app)
+ self.assertEqual(400, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+ body = jsonutils.loads(response.body)
+ expected = {
+ "badRequest": {
+ "message": "All aboard the fail train!",
+ "code": 400
+ }
+ }
+ self.assertThat(expected, matchers.DictMatches(body))
+
+ def test_non_exist_resource(self):
+ res_ext = base_extensions.ResourceExtension('tweedles',
+ StubController(response_body))
+ manager = StubExtensionManager(res_ext)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/tweedles/1")
+ response = request.get_response(app)
+ self.assertEqual(404, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+ body = jsonutils.loads(response.body)
+ expected = {
+ "itemNotFound": {
+ "message": "The resource could not be found.",
+ "code": 404
+ }
+ }
+ self.assertThat(expected, matchers.DictMatches(body))
+
+
+class InvalidExtension(object):
+
+ alias = "THIRD"
+
+
+class ExtensionManagerTest(ExtensionTestCase):
+
+ response_body = "Try to say this Mr. Knox, sir..."
+
+ def test_get_resources(self):
+ app = compute.APIRouter()
+ request = webob.Request.blank("/fake/foxnsocks")
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(response_body, response.body)
+
+ def test_invalid_extensions(self):
+ # Don't need the serialization middleware here because we're
+ # not testing any serialization
+ compute.APIRouter()
+ ext_mgr = compute_extensions.ExtensionManager()
+ ext_mgr.register(InvalidExtension())
+ self.assertTrue(ext_mgr.is_loaded('FOXNSOX'))
+ self.assertFalse(ext_mgr.is_loaded('THIRD'))
+
+
+class ActionExtensionTest(ExtensionTestCase):
+
+ def _send_server_action_request(self, url, body):
+ app = compute.APIRouter(init_only=('servers',))
+ request = webob.Request.blank(url)
+ request.method = 'POST'
+ request.content_type = 'application/json'
+ request.body = jsonutils.dumps(body)
+ response = request.get_response(app)
+ return response
+
+ def test_extended_action(self):
+ body = dict(add_tweedle=dict(name="test"))
+ url = "/fake/servers/abcd/action"
+ response = self._send_server_action_request(url, body)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual("Tweedle Beetle Added.", response.body)
+
+ body = dict(delete_tweedle=dict(name="test"))
+ response = self._send_server_action_request(url, body)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual("Tweedle Beetle Deleted.", response.body)
+
+ def test_invalid_action(self):
+ body = dict(blah=dict(name="test")) # Doesn't exist
+ url = "/fake/servers/abcd/action"
+ response = self._send_server_action_request(url, body)
+ self.assertEqual(400, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+ body = jsonutils.loads(response.body)
+ expected = {
+ "badRequest": {
+ "message": "There is no such action: blah",
+ "code": 400
+ }
+ }
+ self.assertThat(expected, matchers.DictMatches(body))
+
+ def test_non_exist_action(self):
+ body = dict(blah=dict(name="test"))
+ url = "/fake/fdsa/1/action"
+ response = self._send_server_action_request(url, body)
+ self.assertEqual(404, response.status_int)
+
+ def test_failed_action(self):
+ body = dict(fail=dict(name="test"))
+ url = "/fake/servers/abcd/action"
+ response = self._send_server_action_request(url, body)
+ self.assertEqual(400, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+ body = jsonutils.loads(response.body)
+ expected = {
+ "badRequest": {
+ "message": "Tweedle fail",
+ "code": 400
+ }
+ }
+ self.assertThat(expected, matchers.DictMatches(body))
+
+
+class RequestExtensionTest(ExtensionTestCase):
+
+ def test_get_resources_with_stub_mgr(self):
+ class GooGoose(wsgi.Controller):
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ # only handle JSON responses
+ resp_obj.obj['flavor']['googoose'] = req.GET.get('chewing')
+
+ req_ext = base_extensions.ControllerExtension(
+ StubControllerExtension(), 'flavors', GooGoose())
+
+ manager = StubExtensionManager(None, None, None, req_ext)
+ app = fakes.wsgi_app(ext_mgr=manager)
+ request = webob.Request.blank("/v2/fake/flavors/1?chewing=bluegoo")
+ request.environ['api.version'] = '2'
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ response_data = jsonutils.loads(response.body)
+ self.assertEqual('bluegoo', response_data['flavor']['googoose'])
+
+ def test_get_resources_with_mgr(self):
+
+ app = fakes.wsgi_app(init_only=('flavors',))
+ request = webob.Request.blank("/v2/fake/flavors/1?chewing=newblue")
+ request.environ['api.version'] = '2'
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ response_data = jsonutils.loads(response.body)
+ self.assertEqual('newblue', response_data['flavor']['googoose'])
+ self.assertEqual("Pig Bands!", response_data['big_bands'])
+
+
+class ControllerExtensionTest(ExtensionTestCase):
+ def test_controller_extension_early(self):
+ controller = StubController(response_body)
+ res_ext = base_extensions.ResourceExtension('tweedles', controller)
+ ext_controller = StubEarlyExtensionController(extension_body)
+ extension = StubControllerExtension()
+ cont_ext = base_extensions.ControllerExtension(extension, 'tweedles',
+ ext_controller)
+ manager = StubExtensionManager(resource_ext=res_ext,
+ controller_ext=cont_ext)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/tweedles")
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(extension_body, response.body)
+
+ def test_controller_extension_late(self):
+ # Need a dict for the body to convert to a ResponseObject
+ controller = StubController(dict(foo=response_body))
+ res_ext = base_extensions.ResourceExtension('tweedles', controller)
+
+ ext_controller = StubLateExtensionController(extension_body)
+ extension = StubControllerExtension()
+ cont_ext = base_extensions.ControllerExtension(extension, 'tweedles',
+ ext_controller)
+
+ manager = StubExtensionManager(resource_ext=res_ext,
+ controller_ext=cont_ext)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/tweedles")
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(extension_body, response.body)
+
+ def test_controller_extension_late_inherited_resource(self):
+ # Need a dict for the body to convert to a ResponseObject
+ controller = StubController(dict(foo=response_body))
+ parent_ext = base_extensions.ResourceExtension('tweedles', controller)
+
+ ext_controller = StubLateExtensionController(extension_body)
+ extension = StubControllerExtension()
+ cont_ext = base_extensions.ControllerExtension(extension, 'tweedles',
+ ext_controller)
+
+ manager = StubExtensionManager(resource_ext=parent_ext,
+ controller_ext=cont_ext)
+ child_ext = base_extensions.ResourceExtension('beetles', controller,
+ inherits='tweedles')
+ manager.extra_resource_ext = child_ext
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/beetles")
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(extension_body, response.body)
+
+ def test_controller_action_extension_early(self):
+ controller = StubActionController(response_body)
+ actions = dict(action='POST')
+ res_ext = base_extensions.ResourceExtension('tweedles', controller,
+ member_actions=actions)
+ ext_controller = StubEarlyExtensionController(extension_body)
+ extension = StubControllerExtension()
+ cont_ext = base_extensions.ControllerExtension(extension, 'tweedles',
+ ext_controller)
+ manager = StubExtensionManager(resource_ext=res_ext,
+ controller_ext=cont_ext)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/tweedles/foo/action")
+ request.method = 'POST'
+ request.headers['Content-Type'] = 'application/json'
+ request.body = jsonutils.dumps(dict(fooAction=True))
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(extension_body, response.body)
+
+ def test_controller_action_extension_late(self):
+ # Need a dict for the body to convert to a ResponseObject
+ controller = StubActionController(dict(foo=response_body))
+ actions = dict(action='POST')
+ res_ext = base_extensions.ResourceExtension('tweedles', controller,
+ member_actions=actions)
+
+ ext_controller = StubLateExtensionController(extension_body)
+ extension = StubControllerExtension()
+ cont_ext = base_extensions.ControllerExtension(extension, 'tweedles',
+ ext_controller)
+
+ manager = StubExtensionManager(resource_ext=res_ext,
+ controller_ext=cont_ext)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/tweedles/foo/action")
+ request.method = 'POST'
+ request.headers['Content-Type'] = 'application/json'
+ request.body = jsonutils.dumps(dict(fooAction=True))
+ response = request.get_response(app)
+ self.assertEqual(200, response.status_int)
+ self.assertEqual(extension_body, response.body)
+
+
+class ExtensionsXMLSerializerTest(test.TestCase):
+
+ def test_serialize_extension(self):
+ serializer = base_extensions.ExtensionTemplate()
+ data = {'extension': {
+ 'name': 'ext1',
+ 'namespace': 'http://docs.rack.com/servers/api/ext/pie/v1.0',
+ 'alias': 'RS-PIE',
+ 'updated': '2011-01-22T13:25:27-06:00',
+ 'description': 'Adds the capability to share an image.',
+ 'links': [{'rel': 'describedby',
+ 'type': 'application/pdf',
+ 'href': 'http://docs.rack.com/servers/api/ext/cs.pdf'},
+ {'rel': 'describedby',
+ 'type': 'application/vnd.sun.wadl+xml',
+ 'href': 'http://docs.rack.com/servers/api/ext/cs.wadl'}]}}
+
+ xml = serializer.serialize(data)
+ root = etree.XML(xml)
+ ext_dict = data['extension']
+ self.assertEqual(root.findtext('{0}description'.format(NS)),
+ ext_dict['description'])
+
+ for key in ['name', 'namespace', 'alias', 'updated']:
+ self.assertEqual(root.get(key), ext_dict[key])
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(ext_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ xmlutil.validate_schema(root, 'extension')
+
+ def test_serialize_extensions(self):
+ serializer = base_extensions.ExtensionsTemplate()
+ data = {"extensions": [{
+ "name": "Public Image Extension",
+ "namespace": "http://foo.com/api/ext/pie/v1.0",
+ "alias": "RS-PIE",
+ "updated": "2011-01-22T13:25:27-06:00",
+ "description": "Adds the capability to share an image.",
+ "links": [{"rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://foo.com/api/ext/cs-pie.pdf"},
+ {"rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://foo.com/api/ext/cs-pie.wadl"}]},
+ {"name": "Cloud Block Storage",
+ "namespace": "http://foo.com/api/ext/cbs/v1.0",
+ "alias": "RS-CBS",
+ "updated": "2011-01-12T11:22:33-06:00",
+ "description": "Allows mounting cloud block storage.",
+ "links": [{"rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://foo.com/api/ext/cs-cbs.pdf"},
+ {"rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://foo.com/api/ext/cs-cbs.wadl"}]}]}
+
+ xml = serializer.serialize(data)
+ root = etree.XML(xml)
+ ext_elems = root.findall('{0}extension'.format(NS))
+ self.assertEqual(len(ext_elems), 2)
+ for i, ext_elem in enumerate(ext_elems):
+ ext_dict = data['extensions'][i]
+ self.assertEqual(ext_elem.findtext('{0}description'.format(NS)),
+ ext_dict['description'])
+
+ for key in ['name', 'namespace', 'alias', 'updated']:
+ self.assertEqual(ext_elem.get(key), ext_dict[key])
+
+ link_nodes = ext_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(ext_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ xmlutil.validate_schema(root, 'extensions')
+
+
+class ExtensionControllerIdFormatTest(test.TestCase):
+
+ def _bounce_id(self, test_id):
+
+ class BounceController(object):
+ def show(self, req, id):
+ return id
+ res_ext = base_extensions.ResourceExtension('bounce',
+ BounceController())
+ manager = StubExtensionManager(res_ext)
+ app = compute.APIRouter(manager)
+ request = webob.Request.blank("/fake/bounce/%s" % test_id)
+ response = request.get_response(app)
+ return response.body
+
+ def test_id_with_xml_format(self):
+ result = self._bounce_id('foo.xml')
+ self.assertEqual(result, 'foo')
+
+ def test_id_with_json_format(self):
+ result = self._bounce_id('foo.json')
+ self.assertEqual(result, 'foo')
+
+ def test_id_with_bad_format(self):
+ result = self._bounce_id('foo.bad')
+ self.assertEqual(result, 'foo.bad')
diff --git a/nova/tests/unit/api/openstack/compute/test_flavors.py b/nova/tests/unit/api/openstack/compute/test_flavors.py
new file mode 100644
index 0000000000..265b50ac85
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_flavors.py
@@ -0,0 +1,943 @@
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from lxml import etree
+import six.moves.urllib.parse as urlparse
+import webob
+
+from nova.api.openstack import common
+from nova.api.openstack.compute import flavors as flavors_v2
+from nova.api.openstack.compute.plugins.v3 import flavors as flavors_v3
+from nova.api.openstack import xmlutil
+import nova.compute.flavors
+from nova import context
+from nova import db
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import matchers
+
+NS = "{http://docs.openstack.org/compute/api/v1.1}"
+ATOMNS = "{http://www.w3.org/2005/Atom}"
+
+
+FAKE_FLAVORS = {
+ 'flavor 1': {
+ "flavorid": '1',
+ "name": 'flavor 1',
+ "memory_mb": '256',
+ "root_gb": '10',
+ "ephemeral_gb": '20',
+ "swap": '10',
+ "disabled": False,
+ "vcpus": '',
+ },
+ 'flavor 2': {
+ "flavorid": '2',
+ "name": 'flavor 2',
+ "memory_mb": '512',
+ "root_gb": '20',
+ "ephemeral_gb": '10',
+ "swap": '5',
+ "disabled": False,
+ "vcpus": '',
+ },
+}
+
+
+def fake_flavor_get_by_flavor_id(flavorid, ctxt=None):
+ return FAKE_FLAVORS['flavor %s' % flavorid]
+
+
+def fake_get_all_flavors_sorted_list(context=None, inactive=False,
+ filters=None, sort_key='flavorid',
+ sort_dir='asc', limit=None, marker=None):
+ if marker in ['99999']:
+ raise exception.MarkerNotFound(marker)
+
+ def reject_min(db_attr, filter_attr):
+ return (filter_attr in filters and
+ int(flavor[db_attr]) < int(filters[filter_attr]))
+
+ filters = filters or {}
+ res = []
+ for (flavor_name, flavor) in FAKE_FLAVORS.items():
+ if reject_min('memory_mb', 'min_memory_mb'):
+ continue
+ elif reject_min('root_gb', 'min_root_gb'):
+ continue
+
+ res.append(flavor)
+
+ res = sorted(res, key=lambda item: item[sort_key])
+ output = []
+ marker_found = True if marker is None else False
+ for flavor in res:
+ if not marker_found and marker == flavor['flavorid']:
+ marker_found = True
+ elif marker_found:
+ if limit is None or len(output) < int(limit):
+ output.append(flavor)
+
+ return output
+
+
+def fake_get_limit_and_marker(request, max_limit=1):
+ params = common.get_pagination_params(request)
+ limit = params.get('limit', max_limit)
+ limit = min(max_limit, limit)
+ marker = params.get('marker')
+
+ return limit, marker
+
+
+def empty_get_all_flavors_sorted_list(context=None, inactive=False,
+ filters=None, sort_key='flavorid',
+ sort_dir='asc', limit=None, marker=None):
+ return []
+
+
+def return_flavor_not_found(flavor_id, ctxt=None):
+ raise exception.FlavorNotFound(flavor_id=flavor_id)
+
+
+class FlavorsTestV21(test.TestCase):
+ _prefix = "/v3"
+ Controller = flavors_v3.FlavorsController
+ fake_request = fakes.HTTPRequestV3
+ _rspv = "v3"
+ _fake = ""
+
+ def setUp(self):
+ super(FlavorsTestV21, self).setUp()
+ self.flags(osapi_compute_extension=[])
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+ self.stubs.Set(nova.compute.flavors, "get_all_flavors_sorted_list",
+ fake_get_all_flavors_sorted_list)
+ self.stubs.Set(nova.compute.flavors,
+ "get_flavor_by_flavor_id",
+ fake_flavor_get_by_flavor_id)
+ self.controller = self.Controller()
+
+ def _set_expected_body(self, expected, ephemeral, swap, disabled):
+ # NOTE(oomichi): On v2.1 API, some extensions of v2.0 are merged
+ # as core features and we can get the following parameters as the
+ # default.
+ expected['OS-FLV-EXT-DATA:ephemeral'] = ephemeral
+ expected['OS-FLV-DISABLED:disabled'] = disabled
+ expected['swap'] = swap
+
+ def test_get_flavor_by_invalid_id(self):
+ self.stubs.Set(nova.compute.flavors,
+ "get_flavor_by_flavor_id",
+ return_flavor_not_found)
+ req = self.fake_request.blank(self._prefix + '/flavors/asdf')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, 'asdf')
+
+ def test_get_flavor_by_id(self):
+ req = self.fake_request.blank(self._prefix + '/flavors/1')
+ flavor = self.controller.show(req, '1')
+ expected = {
+ "flavor": {
+ "id": "1",
+ "name": "flavor 1",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/1",
+ },
+ ],
+ },
+ }
+ self._set_expected_body(expected['flavor'], ephemeral='20',
+ swap='10', disabled=False)
+ self.assertEqual(flavor, expected)
+
+ def test_get_flavor_with_custom_link_prefix(self):
+ self.flags(osapi_compute_link_prefix='http://zoo.com:42',
+ osapi_glance_link_prefix='http://circus.com:34')
+ req = self.fake_request.blank(self._prefix + '/flavors/1')
+ flavor = self.controller.show(req, '1')
+ expected = {
+ "flavor": {
+ "id": "1",
+ "name": "flavor 1",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://zoo.com:42/" + self._rspv +
+ "/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://zoo.com:42" + self._fake +
+ "/flavors/1",
+ },
+ ],
+ },
+ }
+ self._set_expected_body(expected['flavor'], ephemeral='20',
+ swap='10', disabled=False)
+ self.assertEqual(expected, flavor)
+
+ def test_get_flavor_list(self):
+ req = self.fake_request.blank(self._prefix + '/flavors')
+ flavor = self.controller.index(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/1",
+ },
+ ],
+ },
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self.assertEqual(flavor, expected)
+
+ def test_get_flavor_list_with_marker(self):
+ self.maxDiff = None
+ url = self._prefix + '/flavors?limit=1&marker=1'
+ req = self.fake_request.blank(url)
+ flavor = self.controller.index(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/2",
+ },
+ ],
+ },
+ ],
+ 'flavors_links': [
+ {'href': 'http://localhost/' + self._rspv +
+ '/flavors?limit=1&marker=2',
+ 'rel': 'next'}
+ ]
+ }
+ self.assertThat(flavor, matchers.DictMatches(expected))
+
+ def test_get_flavor_list_with_invalid_marker(self):
+ req = self.fake_request.blank(self._prefix + '/flavors?marker=99999')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_flavor_detail_with_limit(self):
+ url = self._prefix + '/flavors/detail?limit=1'
+ req = self.fake_request.blank(url)
+ response = self.controller.index(req)
+ response_list = response["flavors"]
+ response_links = response["flavors_links"]
+
+ expected_flavors = [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/1",
+ },
+ ],
+ },
+ ]
+ self.assertEqual(response_list, expected_flavors)
+ self.assertEqual(response_links[0]['rel'], 'next')
+
+ href_parts = urlparse.urlparse(response_links[0]['href'])
+ self.assertEqual('/' + self._rspv + '/flavors', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ self.assertThat({'limit': ['1'], 'marker': ['1']},
+ matchers.DictMatches(params))
+
+ def test_get_flavor_with_limit(self):
+ req = self.fake_request.blank(self._prefix + '/flavors?limit=2')
+ response = self.controller.index(req)
+ response_list = response["flavors"]
+ response_links = response["flavors_links"]
+
+ expected_flavors = [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/1",
+ },
+ ],
+ },
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/2",
+ },
+ ],
+ }
+ ]
+ self.assertEqual(response_list, expected_flavors)
+ self.assertEqual(response_links[0]['rel'], 'next')
+
+ href_parts = urlparse.urlparse(response_links[0]['href'])
+ self.assertEqual('/' + self._rspv + '/flavors', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ self.assertThat({'limit': ['2'], 'marker': ['2']},
+ matchers.DictMatches(params))
+
+ def test_get_flavor_with_default_limit(self):
+ self.stubs.Set(common, "get_limit_and_marker",
+ fake_get_limit_and_marker)
+ self.flags(osapi_max_limit=1)
+ req = fakes.HTTPRequest.blank('/v2/fake/flavors?limit=2')
+ response = self.controller.index(req)
+ response_list = response["flavors"]
+ response_links = response["flavors_links"]
+
+ expected_flavors = [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/flavors/1",
+ }
+ ]
+ }
+ ]
+
+ self.assertEqual(response_list, expected_flavors)
+ self.assertEqual(response_links[0]['rel'], 'next')
+ href_parts = urlparse.urlparse(response_links[0]['href'])
+ self.assertEqual('/v2/fake/flavors', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ self.assertThat({'limit': ['2'], 'marker': ['1']},
+ matchers.DictMatches(params))
+
+ def test_get_flavor_list_detail(self):
+ req = self.fake_request.blank(self._prefix + '/flavors/detail')
+ flavor = self.controller.detail(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/1",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/1",
+ },
+ ],
+ },
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "ram": "512",
+ "disk": "20",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self._set_expected_body(expected['flavors'][0], ephemeral='20',
+ swap='10', disabled=False)
+ self._set_expected_body(expected['flavors'][1], ephemeral='10',
+ swap='5', disabled=False)
+ self.assertEqual(expected, flavor)
+
+ def test_get_empty_flavor_list(self):
+ self.stubs.Set(nova.compute.flavors, "get_all_flavors_sorted_list",
+ empty_get_all_flavors_sorted_list)
+
+ req = self.fake_request.blank(self._prefix + '/flavors')
+ flavors = self.controller.index(req)
+ expected = {'flavors': []}
+ self.assertEqual(flavors, expected)
+
+ def test_get_flavor_list_filter_min_ram(self):
+ # Flavor lists may be filtered by minRam.
+ req = self.fake_request.blank(self._prefix + '/flavors?minRam=512')
+ flavor = self.controller.index(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self.assertEqual(flavor, expected)
+
+ def test_get_flavor_list_filter_invalid_min_ram(self):
+ # Ensure you cannot list flavors with invalid minRam param.
+ req = self.fake_request.blank(self._prefix + '/flavors?minRam=NaN')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_flavor_list_filter_min_disk(self):
+ # Flavor lists may be filtered by minDisk.
+ req = self.fake_request.blank(self._prefix + '/flavors?minDisk=20')
+ flavor = self.controller.index(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self.assertEqual(flavor, expected)
+
+ def test_get_flavor_list_filter_invalid_min_disk(self):
+ # Ensure you cannot list flavors with invalid minDisk param.
+ req = self.fake_request.blank(self._prefix + '/flavors?minDisk=NaN')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_flavor_list_detail_min_ram_and_min_disk(self):
+ """Tests that filtering work on flavor details and that minRam and
+ minDisk filters can be combined
+ """
+ req = self.fake_request.blank(self._prefix + '/flavors/detail'
+ '?minRam=256&minDisk=20')
+ flavor = self.controller.detail(req)
+ expected = {
+ "flavors": [
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "ram": "512",
+ "disk": "20",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/" + self._rspv +
+ "/flavors/2",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost" + self._fake +
+ "/flavors/2",
+ },
+ ],
+ },
+ ],
+ }
+ self._set_expected_body(expected['flavors'][0], ephemeral='10',
+ swap='5', disabled=False)
+ self.assertEqual(expected, flavor)
+
+
+class FlavorsTestV20(FlavorsTestV21):
+ _prefix = "/v2/fake"
+ Controller = flavors_v2.Controller
+ fake_request = fakes.HTTPRequest
+ _rspv = "v2/fake"
+ _fake = "/fake"
+
+ def _set_expected_body(self, expected, ephemeral, swap, disabled):
+ pass
+
+
+class FlavorsXMLSerializationTest(test.TestCase):
+
+ def test_xml_declaration(self):
+ serializer = flavors_v2.FlavorTemplate()
+
+ fixture = {
+ "flavor": {
+ "id": "12",
+ "name": "asdf",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/flavors/12",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/flavors/12",
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
+ self.assertTrue(has_dec)
+
+ def test_show(self):
+ serializer = flavors_v2.FlavorTemplate()
+
+ fixture = {
+ "flavor": {
+ "id": "12",
+ "name": "asdf",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/flavors/12",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/flavors/12",
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavor')
+ flavor_dict = fixture['flavor']
+
+ for key in ['name', 'id', 'ram', 'disk']:
+ self.assertEqual(root.get(key), str(flavor_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(flavor_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_show_handles_integers(self):
+ serializer = flavors_v2.FlavorTemplate()
+
+ fixture = {
+ "flavor": {
+ "id": 12,
+ "name": "asdf",
+ "ram": 256,
+ "disk": 10,
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/flavors/12",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/flavors/12",
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavor')
+ flavor_dict = fixture['flavor']
+
+ for key in ['name', 'id', 'ram', 'disk']:
+ self.assertEqual(root.get(key), str(flavor_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(flavor_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_detail(self):
+ serializer = flavors_v2.FlavorsTemplate()
+
+ fixture = {
+ "flavors": [
+ {
+ "id": "23",
+ "name": "flavor 23",
+ "ram": "512",
+ "disk": "20",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/flavors/23",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/flavors/23",
+ },
+ ],
+ },
+ {
+ "id": "13",
+ "name": "flavor 13",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/flavors/13",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/flavors/13",
+ },
+ ],
+ },
+ ],
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavors')
+ flavor_elems = root.findall('{0}flavor'.format(NS))
+ self.assertEqual(len(flavor_elems), 2)
+ for i, flavor_elem in enumerate(flavor_elems):
+ flavor_dict = fixture['flavors'][i]
+
+ for key in ['name', 'id', 'ram', 'disk']:
+ self.assertEqual(flavor_elem.get(key), str(flavor_dict[key]))
+
+ link_nodes = flavor_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(flavor_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_index(self):
+ serializer = flavors_v2.MinimalFlavorsTemplate()
+
+ fixture = {
+ "flavors": [
+ {
+ "id": "23",
+ "name": "flavor 23",
+ "ram": "512",
+ "disk": "20",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/flavors/23",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/flavors/23",
+ },
+ ],
+ },
+ {
+ "id": "13",
+ "name": "flavor 13",
+ "ram": "256",
+ "disk": "10",
+ "vcpus": "",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/flavors/13",
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/flavors/13",
+ },
+ ],
+ },
+ ],
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavors')
+ flavor_elems = root.findall('{0}flavor'.format(NS))
+ self.assertEqual(len(flavor_elems), 2)
+ for i, flavor_elem in enumerate(flavor_elems):
+ flavor_dict = fixture['flavors'][i]
+
+ for key in ['name', 'id']:
+ self.assertEqual(flavor_elem.get(key), str(flavor_dict[key]))
+
+ link_nodes = flavor_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(flavor_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_index_empty(self):
+ serializer = flavors_v2.MinimalFlavorsTemplate()
+
+ fixture = {
+ "flavors": [],
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'flavors')
+ flavor_elems = root.findall('{0}flavor'.format(NS))
+ self.assertEqual(len(flavor_elems), 0)
+
+
+class DisabledFlavorsWithRealDBTestV21(test.TestCase):
+ """Tests that disabled flavors should not be shown nor listed."""
+ Controller = flavors_v3.FlavorsController
+ _prefix = "/v3"
+ fake_request = fakes.HTTPRequestV3
+
+ def setUp(self):
+ super(DisabledFlavorsWithRealDBTestV21, self).setUp()
+
+ # Add a new disabled type to the list of flavors
+ self.req = self.fake_request.blank(self._prefix + '/flavors')
+ self.context = self.req.environ['nova.context']
+ self.admin_context = context.get_admin_context()
+
+ self.disabled_type = self._create_disabled_instance_type()
+ self.inst_types = db.flavor_get_all(
+ self.admin_context)
+ self.controller = self.Controller()
+
+ def tearDown(self):
+ db.flavor_destroy(
+ self.admin_context, self.disabled_type['name'])
+
+ super(DisabledFlavorsWithRealDBTestV21, self).tearDown()
+
+ def _create_disabled_instance_type(self):
+ inst_types = db.flavor_get_all(self.admin_context)
+
+ inst_type = inst_types[0]
+
+ del inst_type['id']
+ inst_type['name'] += '.disabled'
+ inst_type['flavorid'] = unicode(max(
+ [int(flavor['flavorid']) for flavor in inst_types]) + 1)
+ inst_type['disabled'] = True
+
+ disabled_type = db.flavor_create(
+ self.admin_context, inst_type)
+
+ return disabled_type
+
+ def test_index_should_not_list_disabled_flavors_to_user(self):
+ self.context.is_admin = False
+
+ flavor_list = self.controller.index(self.req)['flavors']
+ api_flavorids = set(f['id'] for f in flavor_list)
+
+ db_flavorids = set(i['flavorid'] for i in self.inst_types)
+ disabled_flavorid = str(self.disabled_type['flavorid'])
+
+ self.assertIn(disabled_flavorid, db_flavorids)
+ self.assertEqual(db_flavorids - set([disabled_flavorid]),
+ api_flavorids)
+
+ def test_index_should_list_disabled_flavors_to_admin(self):
+ self.context.is_admin = True
+
+ flavor_list = self.controller.index(self.req)['flavors']
+ api_flavorids = set(f['id'] for f in flavor_list)
+
+ db_flavorids = set(i['flavorid'] for i in self.inst_types)
+ disabled_flavorid = str(self.disabled_type['flavorid'])
+
+ self.assertIn(disabled_flavorid, db_flavorids)
+ self.assertEqual(db_flavorids, api_flavorids)
+
+ def test_show_should_include_disabled_flavor_for_user(self):
+ """Counterintuitively we should show disabled flavors to all users and
+ not just admins. The reason is that, when a user performs a server-show
+ request, we want to be able to display the pretty flavor name ('512 MB
+ Instance') and not just the flavor-id even if the flavor id has been
+ marked disabled.
+ """
+ self.context.is_admin = False
+
+ flavor = self.controller.show(
+ self.req, self.disabled_type['flavorid'])['flavor']
+
+ self.assertEqual(flavor['name'], self.disabled_type['name'])
+
+ def test_show_should_include_disabled_flavor_for_admin(self):
+ self.context.is_admin = True
+
+ flavor = self.controller.show(
+ self.req, self.disabled_type['flavorid'])['flavor']
+
+ self.assertEqual(flavor['name'], self.disabled_type['name'])
+
+
+class DisabledFlavorsWithRealDBTestV20(DisabledFlavorsWithRealDBTestV21):
+ """Tests that disabled flavors should not be shown nor listed."""
+ Controller = flavors_v2.Controller
+ _prefix = "/v2/fake"
+ fake_request = fakes.HTTPRequest
+
+
+class ParseIsPublicTestV21(test.TestCase):
+ Controller = flavors_v3.FlavorsController
+
+ def setUp(self):
+ super(ParseIsPublicTestV21, self).setUp()
+ self.controller = self.Controller()
+
+ def assertPublic(self, expected, is_public):
+ self.assertIs(expected, self.controller._parse_is_public(is_public),
+ '%s did not return %s' % (is_public, expected))
+
+ def test_None(self):
+ self.assertPublic(True, None)
+
+ def test_truthy(self):
+ self.assertPublic(True, True)
+ self.assertPublic(True, 't')
+ self.assertPublic(True, 'true')
+ self.assertPublic(True, 'yes')
+ self.assertPublic(True, '1')
+
+ def test_falsey(self):
+ self.assertPublic(False, False)
+ self.assertPublic(False, 'f')
+ self.assertPublic(False, 'false')
+ self.assertPublic(False, 'no')
+ self.assertPublic(False, '0')
+
+ def test_string_none(self):
+ self.assertPublic(None, 'none')
+ self.assertPublic(None, 'None')
+
+ def test_other(self):
+ self.assertRaises(
+ webob.exc.HTTPBadRequest, self.assertPublic, None, 'other')
+
+
+class ParseIsPublicTestV20(ParseIsPublicTestV21):
+ Controller = flavors_v2.Controller
diff --git a/nova/tests/unit/api/openstack/compute/test_image_metadata.py b/nova/tests/unit/api/openstack/compute/test_image_metadata.py
new file mode 100644
index 0000000000..6de8ddf6f6
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_image_metadata.py
@@ -0,0 +1,366 @@
+# Copyright 2011 OpenStack Foundation
+# 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 copy
+
+import mock
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute import image_metadata
+from nova.api.openstack.compute.plugins.v3 import image_metadata \
+ as image_metadata_v21
+from nova import exception
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import image_fixtures
+
+IMAGE_FIXTURES = image_fixtures.get_image_fixtures()
+CHK_QUOTA_STR = 'nova.api.openstack.common.check_img_metadata_properties_quota'
+
+
+def get_image_123():
+ return copy.deepcopy(IMAGE_FIXTURES)[0]
+
+
+class ImageMetaDataTestV21(test.NoDBTestCase):
+ controller_class = image_metadata_v21.ImageMetadataController
+
+ def setUp(self):
+ super(ImageMetaDataTestV21, self).setUp()
+ self.controller = self.controller_class()
+
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_index(self, get_all_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata')
+ res_dict = self.controller.index(req, '123')
+ expected = {'metadata': {'key1': 'value1'}}
+ self.assertEqual(res_dict, expected)
+ get_all_mocked.assert_called_once_with(mock.ANY, '123')
+
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_show(self, get_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1')
+ res_dict = self.controller.show(req, '123', 'key1')
+ self.assertIn('meta', res_dict)
+ self.assertEqual(len(res_dict['meta']), 1)
+ self.assertEqual('value1', res_dict['meta']['key1'])
+ get_mocked.assert_called_once_with(mock.ANY, '123')
+
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_show_not_found(self, _get_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key9')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, '123', 'key9')
+
+ @mock.patch('nova.image.api.API.get',
+ side_effect=exception.ImageNotFound(image_id='100'))
+ def test_show_image_not_found(self, _get_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, '100', 'key9')
+
+ @mock.patch(CHK_QUOTA_STR)
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_create(self, get_mocked, update_mocked, quota_mocked):
+ mock_result = copy.deepcopy(get_image_123())
+ mock_result['properties']['key7'] = 'value7'
+ update_mocked.return_value = mock_result
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata')
+ req.method = 'POST'
+ body = {"metadata": {"key7": "value7"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = self.controller.create(req, '123', body)
+ get_mocked.assert_called_once_with(mock.ANY, '123')
+ expected = copy.deepcopy(get_image_123())
+ expected['properties'] = {
+ 'key1': 'value1', # existing meta
+ 'key7': 'value7' # new meta
+ }
+ quota_mocked.assert_called_once_with(mock.ANY, expected["properties"])
+ update_mocked.assert_called_once_with(mock.ANY, '123', expected,
+ data=None, purge_props=True)
+
+ expected_output = {'metadata': {'key1': 'value1', 'key7': 'value7'}}
+ self.assertEqual(expected_output, res)
+
+ @mock.patch(CHK_QUOTA_STR)
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get',
+ side_effect=exception.ImageNotFound(image_id='100'))
+ def test_create_image_not_found(self, _get_mocked, update_mocked,
+ quota_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata')
+ req.method = 'POST'
+ body = {"metadata": {"key7": "value7"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.create, req, '100', body)
+ self.assertFalse(quota_mocked.called)
+ self.assertFalse(update_mocked.called)
+
+ @mock.patch(CHK_QUOTA_STR)
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_update_all(self, get_mocked, update_mocked, quota_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata')
+ req.method = 'PUT'
+ body = {"metadata": {"key9": "value9"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = self.controller.update_all(req, '123', body)
+ get_mocked.assert_called_once_with(mock.ANY, '123')
+ expected = copy.deepcopy(get_image_123())
+ expected['properties'] = {
+ 'key9': 'value9' # replace meta
+ }
+ quota_mocked.assert_called_once_with(mock.ANY, expected["properties"])
+ update_mocked.assert_called_once_with(mock.ANY, '123', expected,
+ data=None, purge_props=True)
+
+ expected_output = {'metadata': {'key9': 'value9'}}
+ self.assertEqual(expected_output, res)
+
+ @mock.patch(CHK_QUOTA_STR)
+ @mock.patch('nova.image.api.API.get',
+ side_effect=exception.ImageNotFound(image_id='100'))
+ def test_update_all_image_not_found(self, _get_mocked, quota_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata')
+ req.method = 'PUT'
+ body = {"metadata": {"key9": "value9"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update_all, req, '100', body)
+ self.assertFalse(quota_mocked.called)
+
+ @mock.patch(CHK_QUOTA_STR)
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_update_item(self, _get_mocked, update_mocked, quota_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "zz"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = self.controller.update(req, '123', 'key1', body)
+ expected = copy.deepcopy(get_image_123())
+ expected['properties'] = {
+ 'key1': 'zz' # changed meta
+ }
+ quota_mocked.assert_called_once_with(mock.ANY, expected["properties"])
+ update_mocked.assert_called_once_with(mock.ANY, '123', expected,
+ data=None, purge_props=True)
+
+ expected_output = {'meta': {'key1': 'zz'}}
+ self.assertEqual(res, expected_output)
+
+ @mock.patch(CHK_QUOTA_STR)
+ @mock.patch('nova.image.api.API.get',
+ side_effect=exception.ImageNotFound(image_id='100'))
+ def test_update_item_image_not_found(self, _get_mocked, quota_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "zz"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update, req, '100', 'key1', body)
+ self.assertFalse(quota_mocked.called)
+
+ @mock.patch(CHK_QUOTA_STR)
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get')
+ def test_update_item_bad_body(self, get_mocked, update_mocked,
+ quota_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1')
+ req.method = 'PUT'
+ body = {"key1": "zz"}
+ req.body = ''
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, '123', 'key1', body)
+ self.assertFalse(get_mocked.called)
+ self.assertFalse(quota_mocked.called)
+ self.assertFalse(update_mocked.called)
+
+ @mock.patch(CHK_QUOTA_STR,
+ side_effect=webob.exc.HTTPRequestEntityTooLarge(
+ explanation='', headers={'Retry-After': 0}))
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get')
+ def test_update_item_too_many_keys(self, get_mocked, update_mocked,
+ _quota_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1')
+ req.method = 'PUT'
+ body = {"metadata": {"foo": "bar"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, '123', 'key1', body)
+ self.assertFalse(get_mocked.called)
+ self.assertFalse(update_mocked.called)
+
+ @mock.patch(CHK_QUOTA_STR)
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_update_item_body_uri_mismatch(self, _get_mocked, update_mocked,
+ quota_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/bad')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, '123', 'bad', body)
+ self.assertFalse(quota_mocked.called)
+ self.assertFalse(update_mocked.called)
+
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_delete(self, _get_mocked, update_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1')
+ req.method = 'DELETE'
+ res = self.controller.delete(req, '123', 'key1')
+ expected = copy.deepcopy(get_image_123())
+ expected['properties'] = {}
+ update_mocked.assert_called_once_with(mock.ANY, '123', expected,
+ data=None, purge_props=True)
+
+ self.assertIsNone(res)
+
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_delete_not_found(self, _get_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/blah')
+ req.method = 'DELETE'
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, req, '123', 'blah')
+
+ @mock.patch('nova.image.api.API.get',
+ side_effect=exception.ImageNotFound(image_id='100'))
+ def test_delete_image_not_found(self, _get_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1')
+ req.method = 'DELETE'
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, req, '100', 'key1')
+
+ @mock.patch(CHK_QUOTA_STR,
+ side_effect=webob.exc.HTTPForbidden(
+ explanation='', headers={'Retry-After': 0}))
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_too_many_metadata_items_on_create(self, _get_mocked,
+ update_mocked, _quota_mocked):
+ body = {"metadata": {"foo": "bar"}}
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata')
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.create, req, '123', body)
+ self.assertFalse(update_mocked.called)
+
+ @mock.patch(CHK_QUOTA_STR,
+ side_effect=webob.exc.HTTPForbidden(
+ explanation='', headers={'Retry-After': 0}))
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_too_many_metadata_items_on_put(self, _get_mocked,
+ update_mocked, _quota_mocked):
+ body = {"metadata": {"foo": "bar"}}
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/blah')
+ req.method = 'PUT'
+ body = {"meta": {"blah": "blah"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.update, req, '123', 'blah', body)
+ self.assertFalse(update_mocked.called)
+
+ @mock.patch('nova.image.api.API.get',
+ side_effect=exception.ImageNotAuthorized(image_id='123'))
+ def test_image_not_authorized_update(self, _get_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.update, req, '123', 'key1', body)
+
+ @mock.patch('nova.image.api.API.get',
+ side_effect=exception.ImageNotAuthorized(image_id='123'))
+ def test_image_not_authorized_update_all(self, _get_mocked):
+ image_id = 131
+ # see nova.tests.unit.api.openstack.fakes:_make_image_fixtures
+
+ req = fakes.HTTPRequest.blank('/v2/fake/images/%s/metadata/key1'
+ % image_id)
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.update_all, req, image_id, body)
+
+ @mock.patch('nova.image.api.API.get',
+ side_effect=exception.ImageNotAuthorized(image_id='123'))
+ def test_image_not_authorized_create(self, _get_mocked):
+ image_id = 131
+ # see nova.tests.unit.api.openstack.fakes:_make_image_fixtures
+
+ req = fakes.HTTPRequest.blank('/v2/fake/images/%s/metadata/key1'
+ % image_id)
+ req.method = 'POST'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.create, req, image_id, body)
+
+
+class ImageMetaDataTestV2(ImageMetaDataTestV21):
+ controller_class = image_metadata.Controller
+
+ # NOTE(cyeoh): This duplicate unittest is necessary for a race condition
+ # with the V21 unittests. It's mock issue.
+ @mock.patch('nova.image.api.API.update')
+ @mock.patch('nova.image.api.API.get', return_value=get_image_123())
+ def test_delete(self, _get_mocked, update_mocked):
+ req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1')
+ req.method = 'DELETE'
+ res = self.controller.delete(req, '123', 'key1')
+ expected = copy.deepcopy(get_image_123())
+ expected['properties'] = {}
+ update_mocked.assert_called_once_with(mock.ANY, '123', expected,
+ data=None, purge_props=True)
+
+ self.assertIsNone(res)
diff --git a/nova/tests/unit/api/openstack/compute/test_images.py b/nova/tests/unit/api/openstack/compute/test_images.py
new file mode 100644
index 0000000000..ad55f9a86e
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_images.py
@@ -0,0 +1,1046 @@
+# Copyright 2010 OpenStack Foundation
+# 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.
+
+"""
+Tests of the new image services, both as a service layer,
+and as a WSGI layer
+"""
+
+import copy
+
+from lxml import etree
+import mock
+import webob
+
+from nova.api.openstack.compute import images
+from nova.api.openstack.compute.plugins.v3 import images as images_v21
+from nova.api.openstack.compute.views import images as images_view
+from nova.api.openstack import xmlutil
+from nova import exception
+from nova.image import glance
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import image_fixtures
+from nova.tests.unit import matchers
+
+NS = "{http://docs.openstack.org/compute/api/v1.1}"
+ATOMNS = "{http://www.w3.org/2005/Atom}"
+NOW_API_FORMAT = "2010-10-11T10:30:22Z"
+IMAGE_FIXTURES = image_fixtures.get_image_fixtures()
+
+
+class ImagesControllerTestV21(test.NoDBTestCase):
+ """Test of the OpenStack API /images application controller w/Glance.
+ """
+ image_controller_class = images_v21.ImagesController
+ url_base = '/v3'
+ bookmark_base = ''
+ http_request = fakes.HTTPRequestV3
+
+ def setUp(self):
+ """Run before each test."""
+ super(ImagesControllerTestV21, self).setUp()
+ fakes.stub_out_networking(self.stubs)
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ fakes.stub_out_compute_api_snapshot(self.stubs)
+ fakes.stub_out_compute_api_backup(self.stubs)
+
+ self.controller = self.image_controller_class()
+ self.url_prefix = "http://localhost%s/images" % self.url_base
+ self.bookmark_prefix = "http://localhost%s/images" % self.bookmark_base
+ self.uuid = 'fa95aaf5-ab3b-4cd8-88c0-2be7dd051aaf'
+ self.server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74"
+ self.server_href = (
+ "http://localhost%s/servers/%s" % (self.url_base,
+ self.server_uuid))
+ self.server_bookmark = (
+ "http://localhost%s/servers/%s" % (self.bookmark_base,
+ self.server_uuid))
+ self.alternate = "%s/images/%s"
+
+ self.expected_image_123 = {
+ "image": {'id': '123',
+ 'name': 'public image',
+ 'metadata': {'key1': 'value1'},
+ 'updated': NOW_API_FORMAT,
+ 'created': NOW_API_FORMAT,
+ 'status': 'ACTIVE',
+ 'minDisk': 10,
+ 'progress': 100,
+ 'minRam': 128,
+ "links": [{
+ "rel": "self",
+ "href": "%s/123" % self.url_prefix
+ },
+ {
+ "rel": "bookmark",
+ "href":
+ "%s/123" % self.bookmark_prefix
+ },
+ {
+ "rel": "alternate",
+ "type": "application/vnd.openstack.image",
+ "href": self.alternate %
+ (glance.generate_glance_url(),
+ 123),
+ }],
+ },
+ }
+
+ self.expected_image_124 = {
+ "image": {'id': '124',
+ 'name': 'queued snapshot',
+ 'metadata': {
+ u'instance_uuid': self.server_uuid,
+ u'user_id': u'fake',
+ },
+ 'updated': NOW_API_FORMAT,
+ 'created': NOW_API_FORMAT,
+ 'status': 'SAVING',
+ 'progress': 25,
+ 'minDisk': 0,
+ 'minRam': 0,
+ 'server': {
+ 'id': self.server_uuid,
+ "links": [{
+ "rel": "self",
+ "href": self.server_href,
+ },
+ {
+ "rel": "bookmark",
+ "href": self.server_bookmark,
+ }],
+ },
+ "links": [{
+ "rel": "self",
+ "href": "%s/124" % self.url_prefix
+ },
+ {
+ "rel": "bookmark",
+ "href":
+ "%s/124" % self.bookmark_prefix
+ },
+ {
+ "rel": "alternate",
+ "type":
+ "application/vnd.openstack.image",
+ "href": self.alternate %
+ (glance.generate_glance_url(),
+ 124),
+ }],
+ },
+ }
+
+ @mock.patch('nova.image.api.API.get', return_value=IMAGE_FIXTURES[0])
+ def test_get_image(self, get_mocked):
+ request = self.http_request.blank(self.url_base + 'images/123')
+ actual_image = self.controller.show(request, '123')
+ self.assertThat(actual_image,
+ matchers.DictMatches(self.expected_image_123))
+ get_mocked.assert_called_once_with(mock.ANY, '123')
+
+ @mock.patch('nova.image.api.API.get', return_value=IMAGE_FIXTURES[1])
+ def test_get_image_with_custom_prefix(self, _get_mocked):
+ self.flags(osapi_compute_link_prefix='https://zoo.com:42',
+ osapi_glance_link_prefix='http://circus.com:34')
+ fake_req = self.http_request.blank(self.url_base + 'images/124')
+ actual_image = self.controller.show(fake_req, '124')
+
+ expected_image = self.expected_image_124
+ expected_image["image"]["links"][0]["href"] = (
+ "https://zoo.com:42%s/images/124" % self.url_base)
+ expected_image["image"]["links"][1]["href"] = (
+ "https://zoo.com:42%s/images/124" % self.bookmark_base)
+ expected_image["image"]["links"][2]["href"] = (
+ "http://circus.com:34/images/124")
+ expected_image["image"]["server"]["links"][0]["href"] = (
+ "https://zoo.com:42%s/servers/%s" % (self.url_base,
+ self.server_uuid))
+ expected_image["image"]["server"]["links"][1]["href"] = (
+ "https://zoo.com:42%s/servers/%s" % (self.bookmark_base,
+ self.server_uuid))
+
+ self.assertThat(actual_image, matchers.DictMatches(expected_image))
+
+ @mock.patch('nova.image.api.API.get', side_effect=exception.NotFound)
+ def test_get_image_404(self, _get_mocked):
+ fake_req = self.http_request.blank(self.url_base + 'images/unknown')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, fake_req, 'unknown')
+
+ @mock.patch('nova.image.api.API.get_all', return_value=IMAGE_FIXTURES)
+ def test_get_image_details(self, get_all_mocked):
+ request = self.http_request.blank(self.url_base + 'images/detail')
+ response = self.controller.detail(request)
+
+ get_all_mocked.assert_called_once_with(mock.ANY, filters={})
+ response_list = response["images"]
+
+ image_125 = copy.deepcopy(self.expected_image_124["image"])
+ image_125['id'] = '125'
+ image_125['name'] = 'saving snapshot'
+ image_125['progress'] = 50
+ image_125["links"][0]["href"] = "%s/125" % self.url_prefix
+ image_125["links"][1]["href"] = "%s/125" % self.bookmark_prefix
+ image_125["links"][2]["href"] = (
+ "%s/images/125" % glance.generate_glance_url())
+
+ image_126 = copy.deepcopy(self.expected_image_124["image"])
+ image_126['id'] = '126'
+ image_126['name'] = 'active snapshot'
+ image_126['status'] = 'ACTIVE'
+ image_126['progress'] = 100
+ image_126["links"][0]["href"] = "%s/126" % self.url_prefix
+ image_126["links"][1]["href"] = "%s/126" % self.bookmark_prefix
+ image_126["links"][2]["href"] = (
+ "%s/images/126" % glance.generate_glance_url())
+
+ image_127 = copy.deepcopy(self.expected_image_124["image"])
+ image_127['id'] = '127'
+ image_127['name'] = 'killed snapshot'
+ image_127['status'] = 'ERROR'
+ image_127['progress'] = 0
+ image_127["links"][0]["href"] = "%s/127" % self.url_prefix
+ image_127["links"][1]["href"] = "%s/127" % self.bookmark_prefix
+ image_127["links"][2]["href"] = (
+ "%s/images/127" % glance.generate_glance_url())
+
+ image_128 = copy.deepcopy(self.expected_image_124["image"])
+ image_128['id'] = '128'
+ image_128['name'] = 'deleted snapshot'
+ image_128['status'] = 'DELETED'
+ image_128['progress'] = 0
+ image_128["links"][0]["href"] = "%s/128" % self.url_prefix
+ image_128["links"][1]["href"] = "%s/128" % self.bookmark_prefix
+ image_128["links"][2]["href"] = (
+ "%s/images/128" % glance.generate_glance_url())
+
+ image_129 = copy.deepcopy(self.expected_image_124["image"])
+ image_129['id'] = '129'
+ image_129['name'] = 'pending_delete snapshot'
+ image_129['status'] = 'DELETED'
+ image_129['progress'] = 0
+ image_129["links"][0]["href"] = "%s/129" % self.url_prefix
+ image_129["links"][1]["href"] = "%s/129" % self.bookmark_prefix
+ image_129["links"][2]["href"] = (
+ "%s/images/129" % glance.generate_glance_url())
+
+ image_130 = copy.deepcopy(self.expected_image_123["image"])
+ image_130['id'] = '130'
+ image_130['name'] = None
+ image_130['metadata'] = {}
+ image_130['minDisk'] = 0
+ image_130['minRam'] = 0
+ image_130["links"][0]["href"] = "%s/130" % self.url_prefix
+ image_130["links"][1]["href"] = "%s/130" % self.bookmark_prefix
+ image_130["links"][2]["href"] = (
+ "%s/images/130" % glance.generate_glance_url())
+
+ image_131 = copy.deepcopy(self.expected_image_123["image"])
+ image_131['id'] = '131'
+ image_131['name'] = None
+ image_131['metadata'] = {}
+ image_131['minDisk'] = 0
+ image_131['minRam'] = 0
+ image_131["links"][0]["href"] = "%s/131" % self.url_prefix
+ image_131["links"][1]["href"] = "%s/131" % self.bookmark_prefix
+ image_131["links"][2]["href"] = (
+ "%s/images/131" % glance.generate_glance_url())
+
+ expected = [self.expected_image_123["image"],
+ self.expected_image_124["image"],
+ image_125, image_126, image_127,
+ image_128, image_129, image_130,
+ image_131]
+
+ self.assertThat(expected, matchers.DictListMatches(response_list))
+
+ @mock.patch('nova.image.api.API.get_all')
+ def test_get_image_details_with_limit(self, get_all_mocked):
+ request = self.http_request.blank(self.url_base +
+ 'images/detail?limit=2')
+ self.controller.detail(request)
+ get_all_mocked.assert_called_once_with(mock.ANY, limit=2, filters={})
+
+ @mock.patch('nova.image.api.API.get_all')
+ def test_get_image_details_with_limit_and_page_size(self, get_all_mocked):
+ request = self.http_request.blank(
+ self.url_base + 'images/detail?limit=2&page_size=1')
+ self.controller.detail(request)
+ get_all_mocked.assert_called_once_with(mock.ANY, limit=2, filters={},
+ page_size=1)
+
+ @mock.patch('nova.image.api.API.get_all')
+ def _detail_request(self, filters, request, get_all_mocked):
+ self.controller.detail(request)
+ get_all_mocked.assert_called_once_with(mock.ANY, filters=filters)
+
+ def test_image_detail_filter_with_name(self):
+ filters = {'name': 'testname'}
+ request = self.http_request.blank(self.url_base + 'images/detail'
+ '?name=testname')
+ self._detail_request(filters, request)
+
+ def test_image_detail_filter_with_status(self):
+ filters = {'status': 'active'}
+ request = self.http_request.blank(self.url_base + 'images/detail'
+ '?status=ACTIVE')
+ self._detail_request(filters, request)
+
+ def test_image_detail_filter_with_property(self):
+ filters = {'property-test': '3'}
+ request = self.http_request.blank(self.url_base + 'images/detail'
+ '?property-test=3')
+ self._detail_request(filters, request)
+
+ def test_image_detail_filter_server_href(self):
+ filters = {'property-instance_uuid': self.uuid}
+ request = self.http_request.blank(
+ self.url_base + 'images/detail?server=' + self.uuid)
+ self._detail_request(filters, request)
+
+ def test_image_detail_filter_server_uuid(self):
+ filters = {'property-instance_uuid': self.uuid}
+ request = self.http_request.blank(
+ self.url_base + 'images/detail?server=' + self.uuid)
+ self._detail_request(filters, request)
+
+ def test_image_detail_filter_changes_since(self):
+ filters = {'changes-since': '2011-01-24T17:08Z'}
+ request = self.http_request.blank(self.url_base + 'images/detail'
+ '?changes-since=2011-01-24T17:08Z')
+ self._detail_request(filters, request)
+
+ def test_image_detail_filter_with_type(self):
+ filters = {'property-image_type': 'BASE'}
+ request = self.http_request.blank(
+ self.url_base + 'images/detail?type=BASE')
+ self._detail_request(filters, request)
+
+ def test_image_detail_filter_not_supported(self):
+ filters = {'status': 'active'}
+ request = self.http_request.blank(
+ self.url_base + 'images/detail?status='
+ 'ACTIVE&UNSUPPORTEDFILTER=testname')
+ self._detail_request(filters, request)
+
+ def test_image_detail_no_filters(self):
+ filters = {}
+ request = self.http_request.blank(self.url_base + 'images/detail')
+ self._detail_request(filters, request)
+
+ @mock.patch('nova.image.api.API.get_all', side_effect=exception.Invalid)
+ def test_image_detail_invalid_marker(self, _get_all_mocked):
+ request = self.http_request.blank(self.url_base + '?marker=invalid')
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.detail,
+ request)
+
+ def test_generate_alternate_link(self):
+ view = images_view.ViewBuilder()
+ request = self.http_request.blank(self.url_base + 'images/1')
+ generated_url = view._get_alternate_link(request, 1)
+ actual_url = "%s/images/1" % glance.generate_glance_url()
+ self.assertEqual(generated_url, actual_url)
+
+ def _check_response(self, controller_method, response, expected_code):
+ self.assertEqual(expected_code, controller_method.wsgi_code)
+
+ @mock.patch('nova.image.api.API.delete')
+ def test_delete_image(self, delete_mocked):
+ request = self.http_request.blank(self.url_base + 'images/124')
+ request.method = 'DELETE'
+ response = self.controller.delete(request, '124')
+ self._check_response(self.controller.delete, response, 204)
+ delete_mocked.assert_called_once_with(mock.ANY, '124')
+
+ @mock.patch('nova.image.api.API.delete',
+ side_effect=exception.ImageNotAuthorized(image_id='123'))
+ def test_delete_deleted_image(self, _delete_mocked):
+ # If you try to delete a deleted image, you get back 403 Forbidden.
+ request = self.http_request.blank(self.url_base + 'images/123')
+ request.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
+ request, '123')
+
+ @mock.patch('nova.image.api.API.delete',
+ side_effect=exception.ImageNotFound(image_id='123'))
+ def test_delete_image_not_found(self, _delete_mocked):
+ request = self.http_request.blank(self.url_base + 'images/300')
+ request.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, request, '300')
+
+
+class ImagesControllerTestV2(ImagesControllerTestV21):
+ image_controller_class = images.Controller
+ url_base = '/v2/fake'
+ bookmark_base = '/fake'
+ http_request = fakes.HTTPRequest
+
+ def _check_response(self, controller_method, response, expected_code):
+ self.assertEqual(expected_code, response.status_int)
+
+
+class ImageXMLSerializationTest(test.NoDBTestCase):
+
+ TIMESTAMP = "2010-10-11T10:30:22Z"
+ SERVER_UUID = 'aa640691-d1a7-4a67-9d3c-d35ee6b3cc74'
+ SERVER_HREF = 'http://localhost/v2/fake/servers/' + SERVER_UUID
+ SERVER_BOOKMARK = 'http://localhost/fake/servers/' + SERVER_UUID
+ IMAGE_HREF = 'http://localhost/v2/fake/images/%s'
+ IMAGE_NEXT = 'http://localhost/v2/fake/images?limit=%s&marker=%s'
+ IMAGE_BOOKMARK = 'http://localhost/fake/images/%s'
+
+ def test_xml_declaration(self):
+ serializer = images.ImageTemplate()
+
+ fixture = {
+ 'image': {
+ 'id': 1,
+ 'name': 'Image1',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'ACTIVE',
+ 'progress': 80,
+ 'server': {
+ 'id': self.SERVER_UUID,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ 'metadata': {
+ 'key1': 'value1',
+ },
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
+ self.assertTrue(has_dec)
+
+ def test_show(self):
+ serializer = images.ImageTemplate()
+
+ fixture = {
+ 'image': {
+ 'id': 1,
+ 'name': 'Image1',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'ACTIVE',
+ 'progress': 80,
+ 'minRam': 10,
+ 'minDisk': 100,
+ 'server': {
+ 'id': self.SERVER_UUID,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ 'metadata': {
+ 'key1': 'value1',
+ },
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'image')
+ image_dict = fixture['image']
+
+ for key in ['name', 'id', 'updated', 'created', 'status', 'progress']:
+ self.assertEqual(root.get(key), str(image_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ metadata_root = root.find('{0}metadata'.format(NS))
+ metadata_elems = metadata_root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 1)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = image_dict['metadata'].items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+
+ server_root = root.find('{0}server'.format(NS))
+ self.assertEqual(server_root.get('id'), image_dict['server']['id'])
+ link_nodes = server_root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['server']['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_show_zero_metadata(self):
+ serializer = images.ImageTemplate()
+
+ fixture = {
+ 'image': {
+ 'id': 1,
+ 'name': 'Image1',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'ACTIVE',
+ 'server': {
+ 'id': self.SERVER_UUID,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ 'metadata': {},
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'image')
+ image_dict = fixture['image']
+
+ for key in ['name', 'id', 'updated', 'created', 'status']:
+ self.assertEqual(root.get(key), str(image_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ meta_nodes = root.findall('{0}meta'.format(ATOMNS))
+ self.assertEqual(len(meta_nodes), 0)
+
+ server_root = root.find('{0}server'.format(NS))
+ self.assertEqual(server_root.get('id'), image_dict['server']['id'])
+ link_nodes = server_root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['server']['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_show_image_no_metadata_key(self):
+ serializer = images.ImageTemplate()
+
+ fixture = {
+ 'image': {
+ 'id': 1,
+ 'name': 'Image1',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'ACTIVE',
+ 'server': {
+ 'id': self.SERVER_UUID,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'image')
+ image_dict = fixture['image']
+
+ for key in ['name', 'id', 'updated', 'created', 'status']:
+ self.assertEqual(root.get(key), str(image_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ meta_nodes = root.findall('{0}meta'.format(ATOMNS))
+ self.assertEqual(len(meta_nodes), 0)
+
+ server_root = root.find('{0}server'.format(NS))
+ self.assertEqual(server_root.get('id'), image_dict['server']['id'])
+ link_nodes = server_root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['server']['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_show_no_server(self):
+ serializer = images.ImageTemplate()
+
+ fixture = {
+ 'image': {
+ 'id': 1,
+ 'name': 'Image1',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'ACTIVE',
+ 'metadata': {
+ 'key1': 'value1',
+ },
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'image')
+ image_dict = fixture['image']
+
+ for key in ['name', 'id', 'updated', 'created', 'status']:
+ self.assertEqual(root.get(key), str(image_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ metadata_root = root.find('{0}metadata'.format(NS))
+ metadata_elems = metadata_root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 1)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = image_dict['metadata'].items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+
+ server_root = root.find('{0}server'.format(NS))
+ self.assertIsNone(server_root)
+
+ def test_show_with_min_ram(self):
+ serializer = images.ImageTemplate()
+
+ fixture = {
+ 'image': {
+ 'id': 1,
+ 'name': 'Image1',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'ACTIVE',
+ 'progress': 80,
+ 'minRam': 256,
+ 'server': {
+ 'id': self.SERVER_UUID,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ 'metadata': {
+ 'key1': 'value1',
+ },
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'image')
+ image_dict = fixture['image']
+
+ for key in ['name', 'id', 'updated', 'created', 'status', 'progress',
+ 'minRam']:
+ self.assertEqual(root.get(key), str(image_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ metadata_root = root.find('{0}metadata'.format(NS))
+ metadata_elems = metadata_root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 1)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = image_dict['metadata'].items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+
+ server_root = root.find('{0}server'.format(NS))
+ self.assertEqual(server_root.get('id'), image_dict['server']['id'])
+ link_nodes = server_root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['server']['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_show_with_min_disk(self):
+ serializer = images.ImageTemplate()
+
+ fixture = {
+ 'image': {
+ 'id': 1,
+ 'name': 'Image1',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'ACTIVE',
+ 'progress': 80,
+ 'minDisk': 5,
+ 'server': {
+ 'id': self.SERVER_UUID,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ 'metadata': {
+ 'key1': 'value1',
+ },
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'image')
+ image_dict = fixture['image']
+
+ for key in ['name', 'id', 'updated', 'created', 'status', 'progress',
+ 'minDisk']:
+ self.assertEqual(root.get(key), str(image_dict[key]))
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ metadata_root = root.find('{0}metadata'.format(NS))
+ metadata_elems = metadata_root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 1)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = image_dict['metadata'].items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+
+ server_root = root.find('{0}server'.format(NS))
+ self.assertEqual(server_root.get('id'), image_dict['server']['id'])
+ link_nodes = server_root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['server']['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_index(self):
+ serializer = images.MinimalImagesTemplate()
+
+ fixture = {
+ 'images': [
+ {
+ 'id': 1,
+ 'name': 'Image1',
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ {
+ 'id': 2,
+ 'name': 'Image2',
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 2,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 2,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ ]
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'images')
+ image_elems = root.findall('{0}image'.format(NS))
+ self.assertEqual(len(image_elems), 2)
+ for i, image_elem in enumerate(image_elems):
+ image_dict = fixture['images'][i]
+
+ for key in ['name', 'id']:
+ self.assertEqual(image_elem.get(key), str(image_dict[key]))
+
+ link_nodes = image_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_index_with_links(self):
+ serializer = images.MinimalImagesTemplate()
+
+ fixture = {
+ 'images': [
+ {
+ 'id': 1,
+ 'name': 'Image1',
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ {
+ 'id': 2,
+ 'name': 'Image2',
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 2,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 2,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ ],
+ 'images_links': [
+ {
+ 'rel': 'next',
+ 'href': self.IMAGE_NEXT % (2, 2),
+ }
+ ],
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'images')
+ image_elems = root.findall('{0}image'.format(NS))
+ self.assertEqual(len(image_elems), 2)
+ for i, image_elem in enumerate(image_elems):
+ image_dict = fixture['images'][i]
+
+ for key in ['name', 'id']:
+ self.assertEqual(image_elem.get(key), str(image_dict[key]))
+
+ link_nodes = image_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ # Check images_links
+ images_links = root.findall('{0}link'.format(ATOMNS))
+ for i, link in enumerate(fixture['images_links']):
+ for key, value in link.items():
+ self.assertEqual(images_links[i].get(key), value)
+
+ def test_index_zero_images(self):
+ serializer = images.MinimalImagesTemplate()
+
+ fixtures = {
+ 'images': [],
+ }
+
+ output = serializer.serialize(fixtures)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'images')
+ image_elems = root.findall('{0}image'.format(NS))
+ self.assertEqual(len(image_elems), 0)
+
+ def test_detail(self):
+ serializer = images.ImagesTemplate()
+
+ fixture = {
+ 'images': [
+ {
+ 'id': 1,
+ 'name': 'Image1',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'ACTIVE',
+ 'server': {
+ 'id': self.SERVER_UUID,
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 1,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 1,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ {
+ 'id': '2',
+ 'name': 'Image2',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ 'status': 'SAVING',
+ 'progress': 80,
+ 'metadata': {
+ 'key1': 'value1',
+ },
+ 'links': [
+ {
+ 'href': self.IMAGE_HREF % 2,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.IMAGE_BOOKMARK % 2,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ ]
+ }
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'images')
+ image_elems = root.findall('{0}image'.format(NS))
+ self.assertEqual(len(image_elems), 2)
+ for i, image_elem in enumerate(image_elems):
+ image_dict = fixture['images'][i]
+
+ for key in ['name', 'id', 'updated', 'created', 'status']:
+ self.assertEqual(image_elem.get(key), str(image_dict[key]))
+
+ link_nodes = image_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(image_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
diff --git a/nova/tests/unit/api/openstack/compute/test_limits.py b/nova/tests/unit/api/openstack/compute/test_limits.py
new file mode 100644
index 0000000000..47da849b28
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_limits.py
@@ -0,0 +1,1016 @@
+# Copyright 2011 OpenStack Foundation
+# 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.
+
+"""
+Tests dealing with HTTP rate-limiting.
+"""
+
+import httplib
+import StringIO
+from xml.dom import minidom
+
+from lxml import etree
+import mock
+from oslo.serialization import jsonutils
+import six
+import webob
+
+from nova.api.openstack.compute import limits
+from nova.api.openstack.compute.plugins.v3 import limits as limits_v3
+from nova.api.openstack.compute import views
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+import nova.context
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import matchers
+from nova import utils
+
+
+TEST_LIMITS = [
+ limits.Limit("GET", "/delayed", "^/delayed", 1,
+ utils.TIME_UNITS['MINUTE']),
+ limits.Limit("POST", "*", ".*", 7, utils.TIME_UNITS['MINUTE']),
+ limits.Limit("POST", "/servers", "^/servers", 3,
+ utils.TIME_UNITS['MINUTE']),
+ limits.Limit("PUT", "*", "", 10, utils.TIME_UNITS['MINUTE']),
+ limits.Limit("PUT", "/servers", "^/servers", 5,
+ utils.TIME_UNITS['MINUTE']),
+]
+NS = {
+ 'atom': 'http://www.w3.org/2005/Atom',
+ 'ns': 'http://docs.openstack.org/common/api/v1.0'
+}
+
+
+class BaseLimitTestSuite(test.NoDBTestCase):
+ """Base test suite which provides relevant stubs and time abstraction."""
+
+ def setUp(self):
+ super(BaseLimitTestSuite, self).setUp()
+ self.time = 0.0
+ self.stubs.Set(limits.Limit, "_get_time", self._get_time)
+ self.absolute_limits = {}
+
+ def stub_get_project_quotas(context, project_id, usages=True):
+ return dict((k, dict(limit=v))
+ for k, v in self.absolute_limits.items())
+
+ self.stubs.Set(nova.quota.QUOTAS, "get_project_quotas",
+ stub_get_project_quotas)
+
+ def _get_time(self):
+ """Return the "time" according to this test suite."""
+ return self.time
+
+
+class LimitsControllerTestV21(BaseLimitTestSuite):
+ """Tests for `limits.LimitsController` class."""
+ limits_controller = limits_v3.LimitsController
+
+ def setUp(self):
+ """Run before each test."""
+ super(LimitsControllerTestV21, self).setUp()
+ self.controller = wsgi.Resource(self.limits_controller())
+ self.ctrler = self.limits_controller()
+
+ def _get_index_request(self, accept_header="application/json",
+ tenant_id=None):
+ """Helper to set routing arguments."""
+ request = webob.Request.blank("/")
+ if tenant_id:
+ request = webob.Request.blank("/?tenant_id=%s" % tenant_id)
+
+ request.accept = accept_header
+ request.environ["wsgiorg.routing_args"] = (None, {
+ "action": "index",
+ "controller": "",
+ })
+ context = nova.context.RequestContext('testuser', 'testproject')
+ request.environ["nova.context"] = context
+ return request
+
+ def _populate_limits(self, request):
+ """Put limit info into a request."""
+ _limits = [
+ limits.Limit("GET", "*", ".*", 10, 60).display(),
+ limits.Limit("POST", "*", ".*", 5, 60 * 60).display(),
+ limits.Limit("GET", "changes-since*", "changes-since",
+ 5, 60).display(),
+ ]
+ request.environ["nova.limits"] = _limits
+ return request
+
+ def test_empty_index_json(self):
+ # Test getting empty limit details in JSON.
+ request = self._get_index_request()
+ response = request.get_response(self.controller)
+ expected = {
+ "limits": {
+ "rate": [],
+ "absolute": {},
+ },
+ }
+ body = jsonutils.loads(response.body)
+ self.assertEqual(expected, body)
+
+ def test_index_json(self):
+ self._test_index_json()
+
+ def test_index_json_by_tenant(self):
+ self._test_index_json('faketenant')
+
+ def _test_index_json(self, tenant_id=None):
+ # Test getting limit details in JSON.
+ request = self._get_index_request(tenant_id=tenant_id)
+ context = request.environ["nova.context"]
+ if tenant_id is None:
+ tenant_id = context.project_id
+
+ request = self._populate_limits(request)
+ self.absolute_limits = {
+ 'ram': 512,
+ 'instances': 5,
+ 'cores': 21,
+ 'key_pairs': 10,
+ 'floating_ips': 10,
+ 'security_groups': 10,
+ 'security_group_rules': 20,
+ }
+ expected = {
+ "limits": {
+ "rate": [
+ {
+ "regex": ".*",
+ "uri": "*",
+ "limit": [
+ {
+ "verb": "GET",
+ "next-available": "1970-01-01T00:00:00Z",
+ "unit": "MINUTE",
+ "value": 10,
+ "remaining": 10,
+ },
+ {
+ "verb": "POST",
+ "next-available": "1970-01-01T00:00:00Z",
+ "unit": "HOUR",
+ "value": 5,
+ "remaining": 5,
+ },
+ ],
+ },
+ {
+ "regex": "changes-since",
+ "uri": "changes-since*",
+ "limit": [
+ {
+ "verb": "GET",
+ "next-available": "1970-01-01T00:00:00Z",
+ "unit": "MINUTE",
+ "value": 5,
+ "remaining": 5,
+ },
+ ],
+ },
+
+ ],
+ "absolute": {
+ "maxTotalRAMSize": 512,
+ "maxTotalInstances": 5,
+ "maxTotalCores": 21,
+ "maxTotalKeypairs": 10,
+ "maxTotalFloatingIps": 10,
+ "maxSecurityGroups": 10,
+ "maxSecurityGroupRules": 20,
+ },
+ },
+ }
+
+ def _get_project_quotas(context, project_id, usages=True):
+ return dict((k, dict(limit=v))
+ for k, v in self.absolute_limits.items())
+
+ with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \
+ get_project_quotas:
+ get_project_quotas.side_effect = _get_project_quotas
+
+ response = request.get_response(self.controller)
+
+ body = jsonutils.loads(response.body)
+ self.assertEqual(expected, body)
+ get_project_quotas.assert_called_once_with(context, tenant_id,
+ usages=False)
+
+
+class LimitsControllerTestV2(LimitsControllerTestV21):
+ limits_controller = limits.LimitsController
+
+ def _populate_limits_diff_regex(self, request):
+ """Put limit info into a request."""
+ _limits = [
+ limits.Limit("GET", "*", ".*", 10, 60).display(),
+ limits.Limit("GET", "*", "*.*", 10, 60).display(),
+ ]
+ request.environ["nova.limits"] = _limits
+ return request
+
+ def test_index_diff_regex(self):
+ # Test getting limit details in JSON.
+ request = self._get_index_request()
+ request = self._populate_limits_diff_regex(request)
+ response = request.get_response(self.controller)
+ expected = {
+ "limits": {
+ "rate": [
+ {
+ "regex": ".*",
+ "uri": "*",
+ "limit": [
+ {
+ "verb": "GET",
+ "next-available": "1970-01-01T00:00:00Z",
+ "unit": "MINUTE",
+ "value": 10,
+ "remaining": 10,
+ },
+ ],
+ },
+ {
+ "regex": "*.*",
+ "uri": "*",
+ "limit": [
+ {
+ "verb": "GET",
+ "next-available": "1970-01-01T00:00:00Z",
+ "unit": "MINUTE",
+ "value": 10,
+ "remaining": 10,
+ },
+ ],
+ },
+
+ ],
+ "absolute": {},
+ },
+ }
+ body = jsonutils.loads(response.body)
+ self.assertEqual(expected, body)
+
+ def _test_index_absolute_limits_json(self, expected):
+ request = self._get_index_request()
+ response = request.get_response(self.controller)
+ body = jsonutils.loads(response.body)
+ self.assertEqual(expected, body['limits']['absolute'])
+
+ def test_index_ignores_extra_absolute_limits_json(self):
+ self.absolute_limits = {'unknown_limit': 9001}
+ self._test_index_absolute_limits_json({})
+
+ def test_index_absolute_ram_json(self):
+ self.absolute_limits = {'ram': 1024}
+ self._test_index_absolute_limits_json({'maxTotalRAMSize': 1024})
+
+ def test_index_absolute_cores_json(self):
+ self.absolute_limits = {'cores': 17}
+ self._test_index_absolute_limits_json({'maxTotalCores': 17})
+
+ def test_index_absolute_instances_json(self):
+ self.absolute_limits = {'instances': 19}
+ self._test_index_absolute_limits_json({'maxTotalInstances': 19})
+
+ def test_index_absolute_metadata_json(self):
+ # NOTE: both server metadata and image metadata are overloaded
+ # into metadata_items
+ self.absolute_limits = {'metadata_items': 23}
+ expected = {
+ 'maxServerMeta': 23,
+ 'maxImageMeta': 23,
+ }
+ self._test_index_absolute_limits_json(expected)
+
+ def test_index_absolute_injected_files(self):
+ self.absolute_limits = {
+ 'injected_files': 17,
+ 'injected_file_content_bytes': 86753,
+ }
+ expected = {
+ 'maxPersonality': 17,
+ 'maxPersonalitySize': 86753,
+ }
+ self._test_index_absolute_limits_json(expected)
+
+ def test_index_absolute_security_groups(self):
+ self.absolute_limits = {
+ 'security_groups': 8,
+ 'security_group_rules': 16,
+ }
+ expected = {
+ 'maxSecurityGroups': 8,
+ 'maxSecurityGroupRules': 16,
+ }
+ self._test_index_absolute_limits_json(expected)
+
+ def test_limit_create(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/limits')
+ self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.create,
+ req, {})
+
+ def test_limit_delete(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/limits')
+ self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.delete,
+ req, 1)
+
+ def test_limit_detail(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/limits')
+ self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.detail,
+ req)
+
+ def test_limit_show(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/limits')
+ self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.show,
+ req, 1)
+
+ def test_limit_update(self):
+ req = fakes.HTTPRequest.blank('/v2/fake/limits')
+ self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.update,
+ req, 1, {})
+
+
+class MockLimiter(limits.Limiter):
+ pass
+
+
+class LimitMiddlewareTest(BaseLimitTestSuite):
+ """Tests for the `limits.RateLimitingMiddleware` class."""
+
+ @webob.dec.wsgify
+ def _empty_app(self, request):
+ """Do-nothing WSGI app."""
+ pass
+
+ def setUp(self):
+ """Prepare middleware for use through fake WSGI app."""
+ super(LimitMiddlewareTest, self).setUp()
+ _limits = '(GET, *, .*, 1, MINUTE)'
+ self.app = limits.RateLimitingMiddleware(self._empty_app, _limits,
+ "%s.MockLimiter" %
+ self.__class__.__module__)
+
+ def test_limit_class(self):
+ # Test that middleware selected correct limiter class.
+ self.assertIsInstance(self.app._limiter, MockLimiter)
+
+ def test_good_request(self):
+ # Test successful GET request through middleware.
+ request = webob.Request.blank("/")
+ response = request.get_response(self.app)
+ self.assertEqual(200, response.status_int)
+
+ def test_limited_request_json(self):
+ # Test a rate-limited (429) GET request through middleware.
+ request = webob.Request.blank("/")
+ response = request.get_response(self.app)
+ self.assertEqual(200, response.status_int)
+
+ request = webob.Request.blank("/")
+ response = request.get_response(self.app)
+ self.assertEqual(response.status_int, 429)
+
+ self.assertIn('Retry-After', response.headers)
+ retry_after = int(response.headers['Retry-After'])
+ self.assertAlmostEqual(retry_after, 60, 1)
+
+ body = jsonutils.loads(response.body)
+ expected = "Only 1 GET request(s) can be made to * every minute."
+ value = body["overLimit"]["details"].strip()
+ self.assertEqual(value, expected)
+
+ self.assertIn("retryAfter", body["overLimit"])
+ retryAfter = body["overLimit"]["retryAfter"]
+ self.assertEqual(retryAfter, "60")
+
+ def test_limited_request_xml(self):
+ # Test a rate-limited (429) response as XML.
+ request = webob.Request.blank("/")
+ response = request.get_response(self.app)
+ self.assertEqual(200, response.status_int)
+
+ request = webob.Request.blank("/")
+ request.accept = "application/xml"
+ response = request.get_response(self.app)
+ self.assertEqual(response.status_int, 429)
+
+ root = minidom.parseString(response.body).childNodes[0]
+ expected = "Only 1 GET request(s) can be made to * every minute."
+
+ self.assertIsNotNone(root.attributes.getNamedItem("retryAfter"))
+ retryAfter = root.attributes.getNamedItem("retryAfter").value
+ self.assertEqual(retryAfter, "60")
+
+ details = root.getElementsByTagName("details")
+ self.assertEqual(details.length, 1)
+
+ value = details.item(0).firstChild.data.strip()
+ self.assertEqual(value, expected)
+
+
+class LimitTest(BaseLimitTestSuite):
+ """Tests for the `limits.Limit` class."""
+
+ def test_GET_no_delay(self):
+ # Test a limit handles 1 GET per second.
+ limit = limits.Limit("GET", "*", ".*", 1, 1)
+ delay = limit("GET", "/anything")
+ self.assertIsNone(delay)
+ self.assertEqual(0, limit.next_request)
+ self.assertEqual(0, limit.last_request)
+
+ def test_GET_delay(self):
+ # Test two calls to 1 GET per second limit.
+ limit = limits.Limit("GET", "*", ".*", 1, 1)
+ delay = limit("GET", "/anything")
+ self.assertIsNone(delay)
+
+ delay = limit("GET", "/anything")
+ self.assertEqual(1, delay)
+ self.assertEqual(1, limit.next_request)
+ self.assertEqual(0, limit.last_request)
+
+ self.time += 4
+
+ delay = limit("GET", "/anything")
+ self.assertIsNone(delay)
+ self.assertEqual(4, limit.next_request)
+ self.assertEqual(4, limit.last_request)
+
+
+class ParseLimitsTest(BaseLimitTestSuite):
+ """Tests for the default limits parser in the in-memory
+ `limits.Limiter` class.
+ """
+
+ def test_invalid(self):
+ # Test that parse_limits() handles invalid input correctly.
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ ';;;;;')
+
+ def test_bad_rule(self):
+ # Test that parse_limits() handles bad rules correctly.
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ 'GET, *, .*, 20, minute')
+
+ def test_missing_arg(self):
+ # Test that parse_limits() handles missing args correctly.
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ '(GET, *, .*, 20)')
+
+ def test_bad_value(self):
+ # Test that parse_limits() handles bad values correctly.
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ '(GET, *, .*, foo, minute)')
+
+ def test_bad_unit(self):
+ # Test that parse_limits() handles bad units correctly.
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ '(GET, *, .*, 20, lightyears)')
+
+ def test_multiple_rules(self):
+ # Test that parse_limits() handles multiple rules correctly.
+ try:
+ l = limits.Limiter.parse_limits('(get, *, .*, 20, minute);'
+ '(PUT, /foo*, /foo.*, 10, hour);'
+ '(POST, /bar*, /bar.*, 5, second);'
+ '(Say, /derp*, /derp.*, 1, day)')
+ except ValueError as e:
+ assert False, six.text_type(e)
+
+ # Make sure the number of returned limits are correct
+ self.assertEqual(len(l), 4)
+
+ # Check all the verbs...
+ expected = ['GET', 'PUT', 'POST', 'SAY']
+ self.assertEqual([t.verb for t in l], expected)
+
+ # ...the URIs...
+ expected = ['*', '/foo*', '/bar*', '/derp*']
+ self.assertEqual([t.uri for t in l], expected)
+
+ # ...the regexes...
+ expected = ['.*', '/foo.*', '/bar.*', '/derp.*']
+ self.assertEqual([t.regex for t in l], expected)
+
+ # ...the values...
+ expected = [20, 10, 5, 1]
+ self.assertEqual([t.value for t in l], expected)
+
+ # ...and the units...
+ expected = [utils.TIME_UNITS['MINUTE'], utils.TIME_UNITS['HOUR'],
+ utils.TIME_UNITS['SECOND'], utils.TIME_UNITS['DAY']]
+ self.assertEqual([t.unit for t in l], expected)
+
+
+class LimiterTest(BaseLimitTestSuite):
+ """Tests for the in-memory `limits.Limiter` class."""
+
+ def setUp(self):
+ """Run before each test."""
+ super(LimiterTest, self).setUp()
+ userlimits = {'limits.user3': '',
+ 'limits.user0': '(get, *, .*, 4, minute);'
+ '(put, *, .*, 2, minute)'}
+ self.limiter = limits.Limiter(TEST_LIMITS, **userlimits)
+
+ def _check(self, num, verb, url, username=None):
+ """Check and yield results from checks."""
+ for x in xrange(num):
+ yield self.limiter.check_for_delay(verb, url, username)[0]
+
+ def _check_sum(self, num, verb, url, username=None):
+ """Check and sum results from checks."""
+ results = self._check(num, verb, url, username)
+ return sum(item for item in results if item)
+
+ def test_no_delay_GET(self):
+ """Simple test to ensure no delay on a single call for a limit verb we
+ didn"t set.
+ """
+ delay = self.limiter.check_for_delay("GET", "/anything")
+ self.assertEqual(delay, (None, None))
+
+ def test_no_delay_PUT(self):
+ # Simple test to ensure no delay on a single call for a known limit.
+ delay = self.limiter.check_for_delay("PUT", "/anything")
+ self.assertEqual(delay, (None, None))
+
+ def test_delay_PUT(self):
+ """Ensure the 11th PUT will result in a delay of 6.0 seconds until
+ the next request will be granced.
+ """
+ expected = [None] * 10 + [6.0]
+ results = list(self._check(11, "PUT", "/anything"))
+
+ self.assertEqual(expected, results)
+
+ def test_delay_POST(self):
+ """Ensure the 8th POST will result in a delay of 6.0 seconds until
+ the next request will be granced.
+ """
+ expected = [None] * 7
+ results = list(self._check(7, "POST", "/anything"))
+ self.assertEqual(expected, results)
+
+ expected = 60.0 / 7.0
+ results = self._check_sum(1, "POST", "/anything")
+ self.assertAlmostEqual(expected, results, 8)
+
+ def test_delay_GET(self):
+ # Ensure the 11th GET will result in NO delay.
+ expected = [None] * 11
+ results = list(self._check(11, "GET", "/anything"))
+ self.assertEqual(expected, results)
+
+ expected = [None] * 4 + [15.0]
+ results = list(self._check(5, "GET", "/foo", "user0"))
+ self.assertEqual(expected, results)
+
+ def test_delay_PUT_servers(self):
+ """Ensure PUT on /servers limits at 5 requests, and PUT elsewhere is
+ still OK after 5 requests...but then after 11 total requests, PUT
+ limiting kicks in.
+ """
+ # First 6 requests on PUT /servers
+ expected = [None] * 5 + [12.0]
+ results = list(self._check(6, "PUT", "/servers"))
+ self.assertEqual(expected, results)
+
+ # Next 5 request on PUT /anything
+ expected = [None] * 4 + [6.0]
+ results = list(self._check(5, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ def test_delay_PUT_wait(self):
+ """Ensure after hitting the limit and then waiting for the correct
+ amount of time, the limit will be lifted.
+ """
+ expected = [None] * 10 + [6.0]
+ results = list(self._check(11, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ # Advance time
+ self.time += 6.0
+
+ expected = [None, 6.0]
+ results = list(self._check(2, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ def test_multiple_delays(self):
+ # Ensure multiple requests still get a delay.
+ expected = [None] * 10 + [6.0] * 10
+ results = list(self._check(20, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ self.time += 1.0
+
+ expected = [5.0] * 10
+ results = list(self._check(10, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ expected = [None] * 2 + [30.0] * 8
+ results = list(self._check(10, "PUT", "/anything", "user0"))
+ self.assertEqual(expected, results)
+
+ def test_user_limit(self):
+ # Test user-specific limits.
+ self.assertEqual(self.limiter.levels['user3'], [])
+ self.assertEqual(len(self.limiter.levels['user0']), 2)
+
+ def test_multiple_users(self):
+ # Tests involving multiple users.
+ # User0
+ expected = [None] * 2 + [30.0] * 8
+ results = list(self._check(10, "PUT", "/anything", "user0"))
+ self.assertEqual(expected, results)
+
+ # User1
+ expected = [None] * 10 + [6.0] * 10
+ results = list(self._check(20, "PUT", "/anything", "user1"))
+ self.assertEqual(expected, results)
+
+ # User2
+ expected = [None] * 10 + [6.0] * 5
+ results = list(self._check(15, "PUT", "/anything", "user2"))
+ self.assertEqual(expected, results)
+
+ # User3
+ expected = [None] * 20
+ results = list(self._check(20, "PUT", "/anything", "user3"))
+ self.assertEqual(expected, results)
+
+ self.time += 1.0
+
+ # User1 again
+ expected = [5.0] * 10
+ results = list(self._check(10, "PUT", "/anything", "user1"))
+ self.assertEqual(expected, results)
+
+ self.time += 1.0
+
+ # User1 again
+ expected = [4.0] * 5
+ results = list(self._check(5, "PUT", "/anything", "user2"))
+ self.assertEqual(expected, results)
+
+ # User0 again
+ expected = [28.0]
+ results = list(self._check(1, "PUT", "/anything", "user0"))
+ self.assertEqual(expected, results)
+
+ self.time += 28.0
+
+ expected = [None, 30.0]
+ results = list(self._check(2, "PUT", "/anything", "user0"))
+ self.assertEqual(expected, results)
+
+
+class WsgiLimiterTest(BaseLimitTestSuite):
+ """Tests for `limits.WsgiLimiter` class."""
+
+ def setUp(self):
+ """Run before each test."""
+ super(WsgiLimiterTest, self).setUp()
+ self.app = limits.WsgiLimiter(TEST_LIMITS)
+
+ def _request_data(self, verb, path):
+ """Get data describing a limit request verb/path."""
+ return jsonutils.dumps({"verb": verb, "path": path})
+
+ def _request(self, verb, url, username=None):
+ """Make sure that POSTing to the given url causes the given username
+ to perform the given action. Make the internal rate limiter return
+ delay and make sure that the WSGI app returns the correct response.
+ """
+ if username:
+ request = webob.Request.blank("/%s" % username)
+ else:
+ request = webob.Request.blank("/")
+
+ request.method = "POST"
+ request.body = self._request_data(verb, url)
+ response = request.get_response(self.app)
+
+ if "X-Wait-Seconds" in response.headers:
+ self.assertEqual(response.status_int, 403)
+ return response.headers["X-Wait-Seconds"]
+
+ self.assertEqual(response.status_int, 204)
+
+ def test_invalid_methods(self):
+ # Only POSTs should work.
+ for method in ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]:
+ request = webob.Request.blank("/", method=method)
+ response = request.get_response(self.app)
+ self.assertEqual(response.status_int, 405)
+
+ def test_good_url(self):
+ delay = self._request("GET", "/something")
+ self.assertIsNone(delay)
+
+ def test_escaping(self):
+ delay = self._request("GET", "/something/jump%20up")
+ self.assertIsNone(delay)
+
+ def test_response_to_delays(self):
+ delay = self._request("GET", "/delayed")
+ self.assertIsNone(delay)
+
+ delay = self._request("GET", "/delayed")
+ self.assertEqual(delay, '60.00')
+
+ def test_response_to_delays_usernames(self):
+ delay = self._request("GET", "/delayed", "user1")
+ self.assertIsNone(delay)
+
+ delay = self._request("GET", "/delayed", "user2")
+ self.assertIsNone(delay)
+
+ delay = self._request("GET", "/delayed", "user1")
+ self.assertEqual(delay, '60.00')
+
+ delay = self._request("GET", "/delayed", "user2")
+ self.assertEqual(delay, '60.00')
+
+
+class FakeHttplibSocket(object):
+ """Fake `httplib.HTTPResponse` replacement."""
+
+ def __init__(self, response_string):
+ """Initialize new `FakeHttplibSocket`."""
+ self._buffer = StringIO.StringIO(response_string)
+
+ def makefile(self, _mode, _other):
+ """Returns the socket's internal buffer."""
+ return self._buffer
+
+
+class FakeHttplibConnection(object):
+ """Fake `httplib.HTTPConnection`."""
+
+ def __init__(self, app, host):
+ """Initialize `FakeHttplibConnection`."""
+ self.app = app
+ self.host = host
+
+ def request(self, method, path, body="", headers=None):
+ """Requests made via this connection actually get translated and routed
+ into our WSGI app, we then wait for the response and turn it back into
+ an `httplib.HTTPResponse`.
+ """
+ if not headers:
+ headers = {}
+
+ req = webob.Request.blank(path)
+ req.method = method
+ req.headers = headers
+ req.host = self.host
+ req.body = body
+
+ resp = str(req.get_response(self.app))
+ resp = "HTTP/1.0 %s" % resp
+ sock = FakeHttplibSocket(resp)
+ self.http_response = httplib.HTTPResponse(sock)
+ self.http_response.begin()
+
+ def getresponse(self):
+ """Return our generated response from the request."""
+ return self.http_response
+
+
+def wire_HTTPConnection_to_WSGI(host, app):
+ """Monkeypatches HTTPConnection so that if you try to connect to host, you
+ are instead routed straight to the given WSGI app.
+
+ After calling this method, when any code calls
+
+ httplib.HTTPConnection(host)
+
+ the connection object will be a fake. Its requests will be sent directly
+ to the given WSGI app rather than through a socket.
+
+ Code connecting to hosts other than host will not be affected.
+
+ This method may be called multiple times to map different hosts to
+ different apps.
+
+ This method returns the original HTTPConnection object, so that the caller
+ can restore the default HTTPConnection interface (for all hosts).
+ """
+ class HTTPConnectionDecorator(object):
+ """Wraps the real HTTPConnection class so that when you instantiate
+ the class you might instead get a fake instance.
+ """
+
+ def __init__(self, wrapped):
+ self.wrapped = wrapped
+
+ def __call__(self, connection_host, *args, **kwargs):
+ if connection_host == host:
+ return FakeHttplibConnection(app, host)
+ else:
+ return self.wrapped(connection_host, *args, **kwargs)
+
+ oldHTTPConnection = httplib.HTTPConnection
+ httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection)
+ return oldHTTPConnection
+
+
+class WsgiLimiterProxyTest(BaseLimitTestSuite):
+ """Tests for the `limits.WsgiLimiterProxy` class."""
+
+ def setUp(self):
+ """Do some nifty HTTP/WSGI magic which allows for WSGI to be called
+ directly by something like the `httplib` library.
+ """
+ super(WsgiLimiterProxyTest, self).setUp()
+ self.app = limits.WsgiLimiter(TEST_LIMITS)
+ self.oldHTTPConnection = (
+ wire_HTTPConnection_to_WSGI("169.254.0.1:80", self.app))
+ self.proxy = limits.WsgiLimiterProxy("169.254.0.1:80")
+
+ def test_200(self):
+ # Successful request test.
+ delay = self.proxy.check_for_delay("GET", "/anything")
+ self.assertEqual(delay, (None, None))
+
+ def test_403(self):
+ # Forbidden request test.
+ delay = self.proxy.check_for_delay("GET", "/delayed")
+ self.assertEqual(delay, (None, None))
+
+ delay, error = self.proxy.check_for_delay("GET", "/delayed")
+ error = error.strip()
+
+ expected = ("60.00", "403 Forbidden\n\nOnly 1 GET request(s) can be "
+ "made to /delayed every minute.")
+
+ self.assertEqual((delay, error), expected)
+
+ def tearDown(self):
+ # restore original HTTPConnection object
+ httplib.HTTPConnection = self.oldHTTPConnection
+ super(WsgiLimiterProxyTest, self).tearDown()
+
+
+class LimitsViewBuilderTest(test.NoDBTestCase):
+ def setUp(self):
+ super(LimitsViewBuilderTest, self).setUp()
+ self.view_builder = views.limits.ViewBuilder()
+ self.rate_limits = [{"URI": "*",
+ "regex": ".*",
+ "value": 10,
+ "verb": "POST",
+ "remaining": 2,
+ "unit": "MINUTE",
+ "resetTime": 1311272226},
+ {"URI": "*/servers",
+ "regex": "^/servers",
+ "value": 50,
+ "verb": "POST",
+ "remaining": 10,
+ "unit": "DAY",
+ "resetTime": 1311272226}]
+ self.absolute_limits = {"metadata_items": 1,
+ "injected_files": 5,
+ "injected_file_content_bytes": 5}
+
+ def test_build_limits(self):
+ expected_limits = {"limits": {
+ "rate": [{
+ "uri": "*",
+ "regex": ".*",
+ "limit": [{"value": 10,
+ "verb": "POST",
+ "remaining": 2,
+ "unit": "MINUTE",
+ "next-available": "2011-07-21T18:17:06Z"}]},
+ {"uri": "*/servers",
+ "regex": "^/servers",
+ "limit": [{"value": 50,
+ "verb": "POST",
+ "remaining": 10,
+ "unit": "DAY",
+ "next-available": "2011-07-21T18:17:06Z"}]}],
+ "absolute": {"maxServerMeta": 1,
+ "maxImageMeta": 1,
+ "maxPersonality": 5,
+ "maxPersonalitySize": 5}}}
+
+ output = self.view_builder.build(self.rate_limits,
+ self.absolute_limits)
+ self.assertThat(output, matchers.DictMatches(expected_limits))
+
+ def test_build_limits_empty_limits(self):
+ expected_limits = {"limits": {"rate": [],
+ "absolute": {}}}
+
+ abs_limits = {}
+ rate_limits = []
+ output = self.view_builder.build(rate_limits, abs_limits)
+ self.assertThat(output, matchers.DictMatches(expected_limits))
+
+
+class LimitsXMLSerializationTest(test.NoDBTestCase):
+ def test_xml_declaration(self):
+ serializer = limits.LimitsTemplate()
+
+ fixture = {"limits": {
+ "rate": [],
+ "absolute": {}}}
+
+ output = serializer.serialize(fixture)
+ has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
+ self.assertTrue(has_dec)
+
+ def test_index(self):
+ serializer = limits.LimitsTemplate()
+ fixture = {
+ "limits": {
+ "rate": [{
+ "uri": "*",
+ "regex": ".*",
+ "limit": [{
+ "value": 10,
+ "verb": "POST",
+ "remaining": 2,
+ "unit": "MINUTE",
+ "next-available": "2011-12-15T22:42:45Z"}]},
+ {"uri": "*/servers",
+ "regex": "^/servers",
+ "limit": [{
+ "value": 50,
+ "verb": "POST",
+ "remaining": 10,
+ "unit": "DAY",
+ "next-available": "2011-12-15T22:42:45Z"}]}],
+ "absolute": {"maxServerMeta": 1,
+ "maxImageMeta": 1,
+ "maxPersonality": 5,
+ "maxPersonalitySize": 10240}}}
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'limits')
+
+ # verify absolute limits
+ absolutes = root.xpath('ns:absolute/ns:limit', namespaces=NS)
+ self.assertEqual(len(absolutes), 4)
+ for limit in absolutes:
+ name = limit.get('name')
+ value = limit.get('value')
+ self.assertEqual(value, str(fixture['limits']['absolute'][name]))
+
+ # verify rate limits
+ rates = root.xpath('ns:rates/ns:rate', namespaces=NS)
+ self.assertEqual(len(rates), 2)
+ for i, rate in enumerate(rates):
+ for key in ['uri', 'regex']:
+ self.assertEqual(rate.get(key),
+ str(fixture['limits']['rate'][i][key]))
+ rate_limits = rate.xpath('ns:limit', namespaces=NS)
+ self.assertEqual(len(rate_limits), 1)
+ for j, limit in enumerate(rate_limits):
+ for key in ['verb', 'value', 'remaining', 'unit',
+ 'next-available']:
+ self.assertEqual(limit.get(key),
+ str(fixture['limits']['rate'][i]['limit'][j][key]))
+
+ def test_index_no_limits(self):
+ serializer = limits.LimitsTemplate()
+
+ fixture = {"limits": {
+ "rate": [],
+ "absolute": {}}}
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'limits')
+
+ # verify absolute limits
+ absolutes = root.xpath('ns:absolute/ns:limit', namespaces=NS)
+ self.assertEqual(len(absolutes), 0)
+
+ # verify rate limits
+ rates = root.xpath('ns:rates/ns:rate', namespaces=NS)
+ self.assertEqual(len(rates), 0)
diff --git a/nova/tests/unit/api/openstack/compute/test_server_actions.py b/nova/tests/unit/api/openstack/compute/test_server_actions.py
new file mode 100644
index 0000000000..16f8ce14bf
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_server_actions.py
@@ -0,0 +1,1556 @@
+# Copyright 2011 OpenStack Foundation
+# 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 base64
+import uuid
+
+import mock
+import mox
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute import servers
+from nova.compute import api as compute_api
+from nova.compute import task_states
+from nova.compute import vm_states
+from nova import context
+from nova import db
+from nova import exception
+from nova.image import glance
+from nova import objects
+from nova.openstack.common import uuidutils
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_block_device
+from nova.tests.unit import fake_instance
+from nova.tests.unit.image import fake
+from nova.tests.unit import matchers
+from nova.tests.unit import utils
+
+CONF = cfg.CONF
+CONF.import_opt('password_length', 'nova.utils')
+FAKE_UUID = fakes.FAKE_UUID
+INSTANCE_IDS = {FAKE_UUID: 1}
+
+
+def return_server_not_found(*arg, **kwarg):
+ raise exception.NotFound()
+
+
+def instance_update_and_get_original(context, instance_uuid, values,
+ update_cells=True,
+ columns_to_join=None,
+ ):
+ inst = fakes.stub_instance(INSTANCE_IDS[instance_uuid], host='fake_host')
+ inst = dict(inst, **values)
+ return (inst, inst)
+
+
+def instance_update(context, instance_uuid, kwargs, update_cells=True):
+ inst = fakes.stub_instance(INSTANCE_IDS[instance_uuid], host='fake_host')
+ return inst
+
+
+class MockSetAdminPassword(object):
+ def __init__(self):
+ self.instance_id = None
+ self.password = None
+
+ def __call__(self, context, instance, password):
+ self.instance_id = instance['uuid']
+ self.password = password
+
+
+class ServerActionsControllerTest(test.TestCase):
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ image_href = 'http://localhost/v2/fake/images/%s' % image_uuid
+
+ def setUp(self):
+ super(ServerActionsControllerTest, self).setUp()
+
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ host='fake_host'))
+ self.stubs.Set(db, 'instance_update_and_get_original',
+ instance_update_and_get_original)
+
+ fakes.stub_out_nw_api(self.stubs)
+ fakes.stub_out_compute_api_snapshot(self.stubs)
+ fake.stub_out_image_service(self.stubs)
+ self.flags(allow_instance_snapshots=True,
+ enable_instance_password=True)
+ self.uuid = FAKE_UUID
+ self.url = '/v2/fake/servers/%s/action' % self.uuid
+ self._image_href = '155d900f-4e14-4e4c-a73d-069cbf4541e6'
+
+ class FakeExtManager(object):
+ def is_loaded(self, ext):
+ return False
+
+ self.controller = servers.Controller(ext_mgr=FakeExtManager())
+ self.compute_api = self.controller.compute_api
+ self.context = context.RequestContext('fake', 'fake')
+ self.app = fakes.wsgi_app(init_only=('servers',),
+ fake_auth_context=self.context)
+
+ def _make_request(self, url, body):
+ req = webob.Request.blank('/v2/fake' + url)
+ req.method = 'POST'
+ req.body = jsonutils.dumps(body)
+ req.content_type = 'application/json'
+ return req.get_response(self.app)
+
+ def _stub_instance_get(self, uuid=None):
+ self.mox.StubOutWithMock(compute_api.API, 'get')
+ if uuid is None:
+ uuid = uuidutils.generate_uuid()
+ instance = fake_instance.fake_db_instance(
+ id=1, uuid=uuid, vm_state=vm_states.ACTIVE, task_state=None)
+ instance = objects.Instance._from_db_object(
+ self.context, objects.Instance(), instance)
+
+ self.compute_api.get(self.context, uuid,
+ want_objects=True).AndReturn(instance)
+ return instance
+
+ def _test_locked_instance(self, action, method=None, body_map=None,
+ compute_api_args_map=None):
+ if method is None:
+ method = action
+ if body_map is None:
+ body_map = {}
+ if compute_api_args_map is None:
+ compute_api_args_map = {}
+
+ instance = self._stub_instance_get()
+ args, kwargs = compute_api_args_map.get(action, ((), {}))
+
+ getattr(compute_api.API, method)(self.context, instance,
+ *args, **kwargs).AndRaise(
+ exception.InstanceIsLocked(instance_uuid=instance['uuid']))
+
+ self.mox.ReplayAll()
+
+ res = self._make_request('/servers/%s/action' % instance['uuid'],
+ {action: body_map.get(action)})
+ self.assertEqual(409, res.status_int)
+ # Do these here instead of tearDown because this method is called
+ # more than once for the same test case
+ self.mox.VerifyAll()
+ self.mox.UnsetStubs()
+
+ def test_actions_with_locked_instance(self):
+ actions = ['resize', 'confirmResize', 'revertResize', 'reboot',
+ 'rebuild']
+
+ method_translations = {'confirmResize': 'confirm_resize',
+ 'revertResize': 'revert_resize'}
+
+ body_map = {'resize': {'flavorRef': '2'},
+ 'reboot': {'type': 'HARD'},
+ 'rebuild': {'imageRef': self.image_uuid,
+ 'adminPass': 'TNc53Dr8s7vw'}}
+
+ args_map = {'resize': (('2'), {}),
+ 'confirmResize': ((), {}),
+ 'reboot': (('HARD',), {}),
+ 'rebuild': ((self.image_uuid, 'TNc53Dr8s7vw'),
+ {'files_to_inject': None})}
+
+ for action in actions:
+ method = method_translations.get(action)
+ self.mox.StubOutWithMock(compute_api.API, method or action)
+ self._test_locked_instance(action, method=method,
+ body_map=body_map,
+ compute_api_args_map=args_map)
+
+ def test_server_change_password(self):
+ mock_method = MockSetAdminPassword()
+ self.stubs.Set(compute_api.API, 'set_admin_password', mock_method)
+ body = {'changePassword': {'adminPass': '1234pass'}}
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.controller._action_change_password(req, FAKE_UUID, body)
+
+ self.assertEqual(mock_method.instance_id, self.uuid)
+ self.assertEqual(mock_method.password, '1234pass')
+
+ def test_server_change_password_pass_disabled(self):
+ # run with enable_instance_password disabled to verify adminPass
+ # is missing from response. See lp bug 921814
+ self.flags(enable_instance_password=False)
+
+ mock_method = MockSetAdminPassword()
+ self.stubs.Set(compute_api.API, 'set_admin_password', mock_method)
+ body = {'changePassword': {'adminPass': '1234pass'}}
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.controller._action_change_password(req, FAKE_UUID, body)
+
+ self.assertEqual(mock_method.instance_id, self.uuid)
+ # note,the mock still contains the password.
+ self.assertEqual(mock_method.password, '1234pass')
+
+ def test_server_change_password_not_a_string(self):
+ body = {'changePassword': {'adminPass': 1234}}
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_change_password,
+ req, FAKE_UUID, body)
+
+ def test_server_change_password_bad_request(self):
+ body = {'changePassword': {'pass': '12345'}}
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_change_password,
+ req, FAKE_UUID, body)
+
+ def test_server_change_password_empty_string(self):
+ mock_method = MockSetAdminPassword()
+ self.stubs.Set(compute_api.API, 'set_admin_password', mock_method)
+ body = {'changePassword': {'adminPass': ''}}
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.controller._action_change_password(req, FAKE_UUID, body)
+
+ self.assertEqual(mock_method.instance_id, self.uuid)
+ self.assertEqual(mock_method.password, '')
+
+ def test_server_change_password_none(self):
+ body = {'changePassword': {'adminPass': None}}
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_change_password,
+ req, FAKE_UUID, body)
+
+ def test_reboot_hard(self):
+ body = dict(reboot=dict(type="HARD"))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.controller._action_reboot(req, FAKE_UUID, body)
+
+ def test_reboot_soft(self):
+ body = dict(reboot=dict(type="SOFT"))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.controller._action_reboot(req, FAKE_UUID, body)
+
+ def test_reboot_incorrect_type(self):
+ body = dict(reboot=dict(type="NOT_A_TYPE"))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_missing_type(self):
+ body = dict(reboot=dict())
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_none(self):
+ body = dict(reboot=dict(type=None))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_not_found(self):
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ return_server_not_found)
+
+ body = dict(reboot=dict(type="HARD"))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._action_reboot,
+ req, str(uuid.uuid4()), body)
+
+ def test_reboot_raises_conflict_on_invalid_state(self):
+ body = dict(reboot=dict(type="HARD"))
+
+ def fake_reboot(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'reboot', fake_reboot)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_soft_with_soft_in_progress_raises_conflict(self):
+ body = dict(reboot=dict(type="SOFT"))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.REBOOTING))
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_reboot_hard_with_soft_in_progress_does_not_raise(self):
+ body = dict(reboot=dict(type="HARD"))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.REBOOTING))
+ self.controller._action_reboot(req, FAKE_UUID, body)
+
+ def test_reboot_hard_with_hard_in_progress_raises_conflict(self):
+ body = dict(reboot=dict(type="HARD"))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.REBOOTING_HARD))
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_reboot,
+ req, FAKE_UUID, body)
+
+ def test_rebuild_preserve_ephemeral_is_ignored_when_ext_not_loaded(self):
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE,
+ host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "preserve_ephemeral": False,
+ },
+ }
+ req = fakes.HTTPRequest.blank(self.url)
+ context = req.environ['nova.context']
+
+ self.mox.StubOutWithMock(compute_api.API, 'rebuild')
+ compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href,
+ mox.IgnoreArg(), files_to_inject=None)
+ self.mox.ReplayAll()
+
+ self.controller._action_rebuild(req, FAKE_UUID, body)
+
+ def _test_rebuild_preserve_ephemeral(self, value=None):
+ def fake_is_loaded(ext):
+ return ext == 'os-preserve-ephemeral-rebuild'
+ self.stubs.Set(self.controller.ext_mgr, 'is_loaded', fake_is_loaded)
+
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE,
+ host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+ if value is not None:
+ body['rebuild']['preserve_ephemeral'] = value
+
+ req = fakes.HTTPRequest.blank(self.url)
+ context = req.environ['nova.context']
+
+ self.mox.StubOutWithMock(compute_api.API, 'rebuild')
+
+ if value is not None:
+ compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href,
+ mox.IgnoreArg(), preserve_ephemeral=value,
+ files_to_inject=None)
+ else:
+ compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href,
+ mox.IgnoreArg(), files_to_inject=None)
+ self.mox.ReplayAll()
+
+ self.controller._action_rebuild(req, FAKE_UUID, body)
+
+ def test_rebuild_preserve_ephemeral_true(self):
+ self._test_rebuild_preserve_ephemeral(True)
+
+ def test_rebuild_preserve_ephemeral_false(self):
+ self._test_rebuild_preserve_ephemeral(False)
+
+ def test_rebuild_preserve_ephemeral_default(self):
+ self._test_rebuild_preserve_ephemeral()
+
+ def test_rebuild_accepted_minimum(self):
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+ self_href = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ robj = self.controller._action_rebuild(req, FAKE_UUID, body)
+ body = robj.obj
+
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertEqual(len(body['server']['adminPass']),
+ CONF.password_length)
+
+ self.assertEqual(robj['location'], self_href)
+
+ def test_rebuild_instance_with_image_uuid(self):
+ info = dict(image_href_in_call=None)
+
+ def rebuild(self2, context, instance, image_href, *args, **kwargs):
+ info['image_href_in_call'] = image_href
+
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE))
+ self.stubs.Set(compute_api.API, 'rebuild', rebuild)
+
+ # proper local hrefs must start with 'http://localhost/v2/'
+ body = {
+ 'rebuild': {
+ 'imageRef': self.image_uuid,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/a/action')
+ self.controller._action_rebuild(req, FAKE_UUID, body)
+ self.assertEqual(info['image_href_in_call'], self.image_uuid)
+
+ def test_rebuild_instance_with_image_href_uses_uuid(self):
+ info = dict(image_href_in_call=None)
+
+ def rebuild(self2, context, instance, image_href, *args, **kwargs):
+ info['image_href_in_call'] = image_href
+
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE))
+ self.stubs.Set(compute_api.API, 'rebuild', rebuild)
+
+ # proper local hrefs must start with 'http://localhost/v2/'
+ body = {
+ 'rebuild': {
+ 'imageRef': self.image_href,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/a/action')
+ self.controller._action_rebuild(req, FAKE_UUID, body)
+ self.assertEqual(info['image_href_in_call'], self.image_uuid)
+
+ def test_rebuild_accepted_minimum_pass_disabled(self):
+ # run with enable_instance_password disabled to verify adminPass
+ # is missing from response. See lp bug 921814
+ self.flags(enable_instance_password=False)
+
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+ self_href = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ robj = self.controller._action_rebuild(req, FAKE_UUID, body)
+ body = robj.obj
+
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertNotIn("adminPass", body['server'])
+
+ self.assertEqual(robj['location'], self_href)
+
+ def test_rebuild_raises_conflict_on_invalid_state(self):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ def fake_rebuild(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'rebuild', fake_rebuild)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body)
+
+ def test_rebuild_accepted_with_metadata(self):
+ metadata = {'new': 'metadata'}
+
+ return_server = fakes.fake_instance_get(metadata=metadata,
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "metadata": metadata,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ body = self.controller._action_rebuild(req, FAKE_UUID, body).obj
+
+ self.assertEqual(body['server']['metadata'], metadata)
+
+ def test_rebuild_accepted_with_bad_metadata(self):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "metadata": "stack",
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body)
+
+ def test_rebuild_with_too_large_metadata(self):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "metadata": {
+ 256 * "k": "value"
+ }
+ }
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
+ self.controller._action_rebuild, req,
+ FAKE_UUID, body)
+
+ def test_rebuild_bad_entity(self):
+ body = {
+ "rebuild": {
+ "imageId": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body)
+
+ def test_rebuild_bad_personality(self):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "personality": [{
+ "path": "/path/to/file",
+ "contents": "INVALID b64",
+ }]
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body)
+
+ def test_rebuild_personality(self):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "personality": [{
+ "path": "/path/to/file",
+ "contents": base64.b64encode("Test String"),
+ }]
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ body = self.controller._action_rebuild(req, FAKE_UUID, body).obj
+
+ self.assertNotIn('personality', body['server'])
+
+ def test_rebuild_admin_pass(self):
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "adminPass": "asdf",
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ body = self.controller._action_rebuild(req, FAKE_UUID, body).obj
+
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertEqual(body['server']['adminPass'], 'asdf')
+
+ def test_rebuild_admin_pass_pass_disabled(self):
+ # run with enable_instance_password disabled to verify adminPass
+ # is missing from response. See lp bug 921814
+ self.flags(enable_instance_password=False)
+
+ return_server = fakes.fake_instance_get(image_ref='2',
+ vm_state=vm_states.ACTIVE, host='fake_host')
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "adminPass": "asdf",
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ body = self.controller._action_rebuild(req, FAKE_UUID, body).obj
+
+ self.assertEqual(body['server']['image']['id'], '2')
+ self.assertNotIn('adminPass', body['server'])
+
+ def test_rebuild_server_not_found(self):
+ def server_not_found(self, instance_id,
+ columns_to_join=None, use_slave=False):
+ raise exception.InstanceNotFound(instance_id=instance_id)
+ self.stubs.Set(db, 'instance_get_by_uuid', server_not_found)
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body)
+
+ def test_rebuild_with_bad_image(self):
+ body = {
+ "rebuild": {
+ "imageRef": "foo",
+ },
+ }
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body)
+
+ def test_rebuild_accessIP(self):
+ attributes = {
+ 'access_ip_v4': '172.19.0.1',
+ 'access_ip_v6': 'fe80::1',
+ }
+
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ "accessIPv4": "172.19.0.1",
+ "accessIPv6": "fe80::1",
+ },
+ }
+
+ data = {'changes': {}}
+ orig_get = compute_api.API.get
+
+ def wrap_get(*args, **kwargs):
+ data['instance'] = orig_get(*args, **kwargs)
+ return data['instance']
+
+ def fake_save(context, **kwargs):
+ data['changes'].update(data['instance'].obj_get_changes())
+
+ self.stubs.Set(compute_api.API, 'get', wrap_get)
+ self.stubs.Set(objects.Instance, 'save', fake_save)
+ req = fakes.HTTPRequest.blank(self.url)
+
+ self.controller._action_rebuild(req, FAKE_UUID, body)
+
+ self.assertEqual(self._image_href, data['changes']['image_ref'])
+ self.assertEqual("", data['changes']['kernel_id'])
+ self.assertEqual("", data['changes']['ramdisk_id'])
+ self.assertEqual(task_states.REBUILDING, data['changes']['task_state'])
+ self.assertEqual(0, data['changes']['progress'])
+ for attr, value in attributes.items():
+ self.assertEqual(value, str(data['changes'][attr]))
+
+ def test_rebuild_when_kernel_not_exists(self):
+
+ def return_image_meta(*args, **kwargs):
+ image_meta_table = {
+ '2': {'id': 2, 'status': 'active', 'container_format': 'ari'},
+ '155d900f-4e14-4e4c-a73d-069cbf4541e6':
+ {'id': 3, 'status': 'active', 'container_format': 'raw',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 2}},
+ }
+ image_id = args[2]
+ try:
+ image_meta = image_meta_table[str(image_id)]
+ except KeyError:
+ raise exception.ImageNotFound(image_id=image_id)
+
+ return image_meta
+
+ self.stubs.Set(fake._FakeImageService, 'show', return_image_meta)
+ body = {
+ "rebuild": {
+ "imageRef": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
+ },
+ }
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body)
+
+ def test_rebuild_proper_kernel_ram(self):
+ instance_meta = {'kernel_id': None, 'ramdisk_id': None}
+
+ orig_get = compute_api.API.get
+
+ def wrap_get(*args, **kwargs):
+ inst = orig_get(*args, **kwargs)
+ instance_meta['instance'] = inst
+ return inst
+
+ def fake_save(context, **kwargs):
+ instance = instance_meta['instance']
+ for key in instance_meta.keys():
+ if key in instance.obj_what_changed():
+ instance_meta[key] = instance[key]
+
+ def return_image_meta(*args, **kwargs):
+ image_meta_table = {
+ '1': {'id': 1, 'status': 'active', 'container_format': 'aki'},
+ '2': {'id': 2, 'status': 'active', 'container_format': 'ari'},
+ '155d900f-4e14-4e4c-a73d-069cbf4541e6':
+ {'id': 3, 'status': 'active', 'container_format': 'raw',
+ 'properties': {'kernel_id': 1, 'ramdisk_id': 2}},
+ }
+ image_id = args[2]
+ try:
+ image_meta = image_meta_table[str(image_id)]
+ except KeyError:
+ raise exception.ImageNotFound(image_id=image_id)
+
+ return image_meta
+
+ self.stubs.Set(fake._FakeImageService, 'show', return_image_meta)
+ self.stubs.Set(compute_api.API, 'get', wrap_get)
+ self.stubs.Set(objects.Instance, 'save', fake_save)
+ body = {
+ "rebuild": {
+ "imageRef": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
+ },
+ }
+ req = fakes.HTTPRequest.blank(self.url)
+ self.controller._action_rebuild(req, FAKE_UUID, body).obj
+ self.assertEqual(instance_meta['kernel_id'], '1')
+ self.assertEqual(instance_meta['ramdisk_id'], '2')
+
+ @mock.patch.object(compute_api.API, 'rebuild')
+ def test_rebuild_instance_raise_auto_disk_config_exc(self, mock_rebuild):
+ body = {
+ "rebuild": {
+ "imageRef": self._image_href,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ mock_rebuild.side_effect = exception.AutoDiskConfigDisabledByImage(
+ image='dummy')
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ req, FAKE_UUID, body)
+
+ def test_resize_server(self):
+
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ self.resize_called = False
+
+ def resize_mock(*args):
+ self.resize_called = True
+
+ self.stubs.Set(compute_api.API, 'resize', resize_mock)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ body = self.controller._action_resize(req, FAKE_UUID, body)
+
+ self.assertEqual(self.resize_called, True)
+
+ def test_resize_server_no_flavor(self):
+ body = dict(resize=dict())
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+
+ def test_resize_server_no_flavor_ref(self):
+ body = dict(resize=dict(flavorRef=None))
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+
+ def test_resize_with_server_not_found(self):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ self.stubs.Set(compute_api.API, 'get', return_server_not_found)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+
+ def test_resize_with_image_exceptions(self):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+ self.resize_called = 0
+ image_id = 'fake_image_id'
+
+ exceptions = [
+ (exception.ImageNotAuthorized(image_id=image_id),
+ webob.exc.HTTPUnauthorized),
+ (exception.ImageNotFound(image_id=image_id),
+ webob.exc.HTTPBadRequest),
+ (exception.Invalid, webob.exc.HTTPBadRequest),
+ (exception.NoValidHost(reason='Bad host'),
+ webob.exc.HTTPBadRequest),
+ (exception.AutoDiskConfigDisabledByImage(image=image_id),
+ webob.exc.HTTPBadRequest),
+ ]
+
+ raised, expected = map(iter, zip(*exceptions))
+
+ def _fake_resize(obj, context, instance, flavor_id):
+ self.resize_called += 1
+ raise raised.next()
+
+ self.stubs.Set(compute_api.API, 'resize', _fake_resize)
+
+ for call_no in range(len(exceptions)):
+ req = fakes.HTTPRequest.blank(self.url)
+ next_exception = expected.next()
+ actual = self.assertRaises(next_exception,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+ if (isinstance(exceptions[call_no][0],
+ exception.NoValidHost)):
+ self.assertEqual(actual.explanation,
+ 'No valid host was found. Bad host')
+ elif (isinstance(exceptions[call_no][0],
+ exception.AutoDiskConfigDisabledByImage)):
+ self.assertEqual(actual.explanation,
+ 'Requested image fake_image_id has automatic'
+ ' disk resize disabled.')
+ self.assertEqual(self.resize_called, call_no + 1)
+
+ @mock.patch('nova.compute.api.API.resize',
+ side_effect=exception.CannotResizeDisk(reason=''))
+ def test_resize_raises_cannot_resize_disk(self, mock_resize):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+
+ @mock.patch('nova.compute.api.API.resize',
+ side_effect=exception.FlavorNotFound(reason='',
+ flavor_id='fake_id'))
+ def test_resize_raises_flavor_not_found(self, mock_resize):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+
+ def test_resize_with_too_many_instances(self):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ def fake_resize(*args, **kwargs):
+ raise exception.TooManyInstances(message="TooManyInstance")
+
+ self.stubs.Set(compute_api.API, 'resize', fake_resize)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+
+ def test_resize_raises_conflict_on_invalid_state(self):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ def fake_resize(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'resize', fake_resize)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+
+ @mock.patch('nova.compute.api.API.resize',
+ side_effect=exception.NoValidHost(reason=''))
+ def test_resize_raises_no_valid_host(self, mock_resize):
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+
+ @mock.patch.object(compute_api.API, 'resize')
+ def test_resize_instance_raise_auto_disk_config_exc(self, mock_resize):
+ mock_resize.side_effect = exception.AutoDiskConfigDisabledByImage(
+ image='dummy')
+
+ body = dict(resize=dict(flavorRef="http://localhost/3"))
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_resize,
+ req, FAKE_UUID, body)
+
+ def test_confirm_resize_server(self):
+ body = dict(confirmResize=None)
+
+ self.confirm_resize_called = False
+
+ def cr_mock(*args):
+ self.confirm_resize_called = True
+
+ self.stubs.Set(compute_api.API, 'confirm_resize', cr_mock)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ body = self.controller._action_confirm_resize(req, FAKE_UUID, body)
+
+ self.assertEqual(self.confirm_resize_called, True)
+
+ def test_confirm_resize_migration_not_found(self):
+ body = dict(confirmResize=None)
+
+ def confirm_resize_mock(*args):
+ raise exception.MigrationNotFoundByStatus(instance_id=1,
+ status='finished')
+
+ self.stubs.Set(compute_api.API,
+ 'confirm_resize',
+ confirm_resize_mock)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_confirm_resize,
+ req, FAKE_UUID, body)
+
+ def test_confirm_resize_raises_conflict_on_invalid_state(self):
+ body = dict(confirmResize=None)
+
+ def fake_confirm_resize(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'confirm_resize',
+ fake_confirm_resize)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_confirm_resize,
+ req, FAKE_UUID, body)
+
+ def test_revert_resize_migration_not_found(self):
+ body = dict(revertResize=None)
+
+ def revert_resize_mock(*args):
+ raise exception.MigrationNotFoundByStatus(instance_id=1,
+ status='finished')
+
+ self.stubs.Set(compute_api.API,
+ 'revert_resize',
+ revert_resize_mock)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_revert_resize,
+ req, FAKE_UUID, body)
+
+ def test_revert_resize_server_not_found(self):
+ body = dict(revertResize=None)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob. exc.HTTPNotFound,
+ self.controller._action_revert_resize,
+ req, "bad_server_id", body)
+
+ def test_revert_resize_server(self):
+ body = dict(revertResize=None)
+
+ self.revert_resize_called = False
+
+ def revert_mock(*args):
+ self.revert_resize_called = True
+
+ self.stubs.Set(compute_api.API, 'revert_resize', revert_mock)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ body = self.controller._action_revert_resize(req, FAKE_UUID, body)
+
+ self.assertEqual(self.revert_resize_called, True)
+
+ def test_revert_resize_raises_conflict_on_invalid_state(self):
+ body = dict(revertResize=None)
+
+ def fake_revert_resize(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+
+ self.stubs.Set(compute_api.API, 'revert_resize',
+ fake_revert_resize)
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_revert_resize,
+ req, FAKE_UUID, body)
+
+ def test_create_image(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ response = self.controller._action_create_image(req, FAKE_UUID, body)
+
+ location = response.headers['Location']
+ self.assertEqual('http://localhost/v2/fake/images/123', location)
+
+ def test_create_image_glance_link_prefix(self):
+ self.flags(osapi_glance_link_prefix='https://glancehost')
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ response = self.controller._action_create_image(req, FAKE_UUID, body)
+
+ location = response.headers['Location']
+ self.assertEqual('https://glancehost/v2/fake/images/123', location)
+
+ def test_create_image_name_too_long(self):
+ long_name = 'a' * 260
+ body = {
+ 'createImage': {
+ 'name': long_name,
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image, req,
+ FAKE_UUID, body)
+
+ def _do_test_create_volume_backed_image(self, extra_properties):
+
+ def _fake_id(x):
+ return '%s-%s-%s-%s' % (x * 8, x * 4, x * 4, x * 12)
+
+ body = dict(createImage=dict(name='snapshot_of_volume_backed'))
+
+ if extra_properties:
+ body['createImage']['metadata'] = extra_properties
+
+ image_service = glance.get_default_image_service()
+
+ bdm = [dict(volume_id=_fake_id('a'),
+ volume_size=1,
+ device_name='vda',
+ delete_on_termination=False)]
+ props = dict(kernel_id=_fake_id('b'),
+ ramdisk_id=_fake_id('c'),
+ root_device_name='/dev/vda',
+ block_device_mapping=bdm)
+ original_image = dict(properties=props,
+ container_format='ami',
+ status='active',
+ is_public=True)
+
+ image_service.create(None, original_image)
+
+ def fake_block_device_mapping_get_all_by_instance(context, inst_id,
+ use_slave=False):
+ return [fake_block_device.FakeDbBlockDeviceDict(
+ {'volume_id': _fake_id('a'),
+ 'source_type': 'snapshot',
+ 'destination_type': 'volume',
+ 'volume_size': 1,
+ 'device_name': 'vda',
+ 'snapshot_id': 1,
+ 'boot_index': 0,
+ 'delete_on_termination': False,
+ 'no_device': None})]
+
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ fake_block_device_mapping_get_all_by_instance)
+
+ instance = fakes.fake_instance_get(image_ref=original_image['id'],
+ vm_state=vm_states.ACTIVE,
+ root_device_name='/dev/vda')
+ self.stubs.Set(db, 'instance_get_by_uuid', instance)
+
+ volume = dict(id=_fake_id('a'),
+ size=1,
+ host='fake',
+ display_description='fake')
+ snapshot = dict(id=_fake_id('d'))
+ self.mox.StubOutWithMock(self.controller.compute_api, 'volume_api')
+ volume_api = self.controller.compute_api.volume_api
+ volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume)
+ volume_api.create_snapshot_force(mox.IgnoreArg(), volume['id'],
+ mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(snapshot)
+
+ self.mox.ReplayAll()
+
+ req = fakes.HTTPRequest.blank(self.url)
+ response = self.controller._action_create_image(req, FAKE_UUID, body)
+
+ location = response.headers['Location']
+ image_id = location.replace('http://localhost/v2/fake/images/', '')
+ image = image_service.show(None, image_id)
+
+ self.assertEqual(image['name'], 'snapshot_of_volume_backed')
+ properties = image['properties']
+ self.assertEqual(properties['kernel_id'], _fake_id('b'))
+ self.assertEqual(properties['ramdisk_id'], _fake_id('c'))
+ self.assertEqual(properties['root_device_name'], '/dev/vda')
+ self.assertEqual(properties['bdm_v2'], True)
+ bdms = properties['block_device_mapping']
+ self.assertEqual(len(bdms), 1)
+ self.assertEqual(bdms[0]['boot_index'], 0)
+ self.assertEqual(bdms[0]['source_type'], 'snapshot')
+ self.assertEqual(bdms[0]['destination_type'], 'volume')
+ self.assertEqual(bdms[0]['snapshot_id'], snapshot['id'])
+ for fld in ('connection_info', 'id',
+ 'instance_uuid', 'device_name'):
+ self.assertNotIn(fld, bdms[0])
+ for k in extra_properties.keys():
+ self.assertEqual(properties[k], extra_properties[k])
+
+ def test_create_volume_backed_image_no_metadata(self):
+ self._do_test_create_volume_backed_image({})
+
+ def test_create_volume_backed_image_with_metadata(self):
+ self._do_test_create_volume_backed_image(dict(ImageType='Gold',
+ ImageVersion='2.0'))
+
+ def _test_create_volume_backed_image_with_metadata_from_volume(
+ self, extra_metadata=None):
+
+ def _fake_id(x):
+ return '%s-%s-%s-%s' % (x * 8, x * 4, x * 4, x * 12)
+
+ body = dict(createImage=dict(name='snapshot_of_volume_backed'))
+ if extra_metadata:
+ body['createImage']['metadata'] = extra_metadata
+
+ image_service = glance.get_default_image_service()
+
+ def fake_block_device_mapping_get_all_by_instance(context, inst_id,
+ use_slave=False):
+ return [fake_block_device.FakeDbBlockDeviceDict(
+ {'volume_id': _fake_id('a'),
+ 'source_type': 'snapshot',
+ 'destination_type': 'volume',
+ 'volume_size': 1,
+ 'device_name': 'vda',
+ 'snapshot_id': 1,
+ 'boot_index': 0,
+ 'delete_on_termination': False,
+ 'no_device': None})]
+
+ self.stubs.Set(db, 'block_device_mapping_get_all_by_instance',
+ fake_block_device_mapping_get_all_by_instance)
+
+ instance = fakes.fake_instance_get(image_ref='',
+ vm_state=vm_states.ACTIVE,
+ root_device_name='/dev/vda')
+ self.stubs.Set(db, 'instance_get_by_uuid', instance)
+
+ fake_metadata = {'test_key1': 'test_value1',
+ 'test_key2': 'test_value2'}
+ volume = dict(id=_fake_id('a'),
+ size=1,
+ host='fake',
+ display_description='fake',
+ volume_image_metadata=fake_metadata)
+ snapshot = dict(id=_fake_id('d'))
+ self.mox.StubOutWithMock(self.controller.compute_api, 'volume_api')
+ volume_api = self.controller.compute_api.volume_api
+ volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume)
+ volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume)
+ volume_api.create_snapshot_force(mox.IgnoreArg(), volume['id'],
+ mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(snapshot)
+
+ req = fakes.HTTPRequest.blank(self.url)
+
+ self.mox.ReplayAll()
+ response = self.controller._action_create_image(req, FAKE_UUID, body)
+ location = response.headers['Location']
+ image_id = location.replace('http://localhost/v2/fake/images/', '')
+ image = image_service.show(None, image_id)
+
+ properties = image['properties']
+ self.assertEqual(properties['test_key1'], 'test_value1')
+ self.assertEqual(properties['test_key2'], 'test_value2')
+ if extra_metadata:
+ for key, val in extra_metadata.items():
+ self.assertEqual(properties[key], val)
+
+ def test_create_vol_backed_img_with_meta_from_vol_without_extra_meta(self):
+ self._test_create_volume_backed_image_with_metadata_from_volume()
+
+ def test_create_vol_backed_img_with_meta_from_vol_with_extra_meta(self):
+ self._test_create_volume_backed_image_with_metadata_from_volume(
+ extra_metadata={'a': 'b'})
+
+ def test_create_image_snapshots_disabled(self):
+ """Don't permit a snapshot if the allow_instance_snapshots flag is
+ False
+ """
+ self.flags(allow_instance_snapshots=False)
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ },
+ }
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_with_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ 'metadata': {'key': 'asdf'},
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ response = self.controller._action_create_image(req, FAKE_UUID, body)
+
+ location = response.headers['Location']
+ self.assertEqual('http://localhost/v2/fake/images/123', location)
+
+ def test_create_image_with_too_much_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'Snapshot 1',
+ 'metadata': {},
+ },
+ }
+ for num in range(CONF.quota_metadata_items + 1):
+ body['createImage']['metadata']['foo%i' % num] = "bar"
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_no_name(self):
+ body = {
+ 'createImage': {},
+ }
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_blank_name(self):
+ body = {
+ 'createImage': {
+ 'name': '',
+ }
+ }
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_bad_metadata(self):
+ body = {
+ 'createImage': {
+ 'name': 'geoff',
+ 'metadata': 'henry',
+ },
+ }
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+ def test_create_image_raises_conflict_on_invalid_state(self):
+ def snapshot(*args, **kwargs):
+ raise exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+ self.stubs.Set(compute_api.API, 'snapshot', snapshot)
+
+ body = {
+ "createImage": {
+ "name": "test_snapshot",
+ },
+ }
+
+ req = fakes.HTTPRequest.blank(self.url)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller._action_create_image,
+ req, FAKE_UUID, body)
+
+
+class TestServerActionXMLDeserializer(test.TestCase):
+
+ def setUp(self):
+ super(TestServerActionXMLDeserializer, self).setUp()
+ self.deserializer = servers.ActionDeserializer()
+
+ def test_create_image(self):
+ serial_request = """
+<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "createImage": {
+ "name": "new-server-test",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_create_image_with_metadata(self):
+ serial_request = """
+<createImage xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test">
+ <metadata>
+ <meta key="key1">value1</meta>
+ </metadata>
+</createImage>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "createImage": {
+ "name": "new-server-test",
+ "metadata": {"key1": "value1"},
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_change_pass(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <changePassword
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ adminPass="1234pass"/> """
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "changePassword": {
+ "adminPass": "1234pass",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_change_pass_no_pass(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <changePassword
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/> """
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_change_pass_empty_pass(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <changePassword
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ adminPass=""/> """
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "changePassword": {
+ "adminPass": "",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_reboot(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <reboot
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ type="HARD"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "reboot": {
+ "type": "HARD",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_reboot_no_type(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <reboot
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_resize(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <resize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ flavorRef="http://localhost/flavors/3"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "resize": {"flavorRef": "http://localhost/flavors/3"},
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_resize_no_flavor_ref(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <resize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_confirm_resize(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <confirmResize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "confirmResize": None,
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_revert_resize(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <revertResize
+ xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "revertResize": None,
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_rebuild(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test"
+ imageRef="http://localhost/images/1">
+ <metadata>
+ <meta key="My Server Name">Apache1</meta>
+ </metadata>
+ <personality>
+ <file path="/etc/banner.txt">Mg==</file>
+ </personality>
+ </rebuild>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "rebuild": {
+ "name": "new-server-test",
+ "imageRef": "http://localhost/images/1",
+ "metadata": {
+ "My Server Name": "Apache1",
+ },
+ "personality": [
+ {"path": "/etc/banner.txt", "contents": "Mg=="},
+ ],
+ },
+ }
+ self.assertThat(request['body'], matchers.DictMatches(expected))
+
+ def test_rebuild_minimum(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ imageRef="http://localhost/images/1"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/1",
+ },
+ }
+ self.assertThat(request['body'], matchers.DictMatches(expected))
+
+ def test_rebuild_no_imageRef(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ name="new-server-test">
+ <metadata>
+ <meta key="My Server Name">Apache1</meta>
+ </metadata>
+ <personality>
+ <file path="/etc/banner.txt">Mg==</file>
+ </personality>
+ </rebuild>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_rebuild_blank_name(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ imageRef="http://localhost/images/1"
+ name=""/>"""
+ self.assertRaises(AttributeError,
+ self.deserializer.deserialize,
+ serial_request,
+ 'action')
+
+ def test_rebuild_preserve_ephemeral_passed(self):
+ serial_request = """<?xml version="1.0" encoding="UTF-8"?>
+ <rebuild
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ imageRef="http://localhost/images/1"
+ preserve_ephemeral="true"/>"""
+ request = self.deserializer.deserialize(serial_request, 'action')
+ expected = {
+ "rebuild": {
+ "imageRef": "http://localhost/images/1",
+ "preserve_ephemeral": True,
+ },
+ }
+ self.assertThat(request['body'], matchers.DictMatches(expected))
+
+ def test_corrupt_xml(self):
+ """Should throw a 400 error on corrupt xml."""
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ self.deserializer.deserialize,
+ utils.killer_xml_body())
diff --git a/nova/tests/unit/api/openstack/compute/test_server_metadata.py b/nova/tests/unit/api/openstack/compute/test_server_metadata.py
new file mode 100644
index 0000000000..ba9126f0f1
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_server_metadata.py
@@ -0,0 +1,771 @@
+# Copyright 2011 OpenStack Foundation
+# 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 uuid
+
+import mock
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+import six
+import webob
+
+from nova.api.openstack.compute.plugins.v3 import server_metadata \
+ as server_metadata_v21
+from nova.api.openstack.compute import server_metadata as server_metadata_v2
+from nova.compute import rpcapi as compute_rpcapi
+from nova.compute import vm_states
+import nova.db
+from nova import exception
+from nova import objects
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+
+
+CONF = cfg.CONF
+
+
+def return_create_instance_metadata_max(context, server_id, metadata, delete):
+ return stub_max_server_metadata()
+
+
+def return_create_instance_metadata(context, server_id, metadata, delete):
+ return stub_server_metadata()
+
+
+def fake_instance_save(inst, **kwargs):
+ inst.metadata = stub_server_metadata()
+ inst.obj_reset_changes()
+
+
+def return_server_metadata(context, server_id):
+ if not isinstance(server_id, six.string_types) or not len(server_id) == 36:
+ msg = 'id %s must be a uuid in return server metadata' % server_id
+ raise Exception(msg)
+ return stub_server_metadata()
+
+
+def return_empty_server_metadata(context, server_id):
+ return {}
+
+
+def delete_server_metadata(context, server_id, key):
+ pass
+
+
+def stub_server_metadata():
+ metadata = {
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ }
+ return metadata
+
+
+def stub_max_server_metadata():
+ metadata = {"metadata": {}}
+ for num in range(CONF.quota_metadata_items):
+ metadata['metadata']['key%i' % num] = "blah"
+ return metadata
+
+
+def return_server(context, server_id, columns_to_join=None):
+ return fake_instance.fake_db_instance(
+ **{'id': server_id,
+ 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64',
+ 'name': 'fake',
+ 'locked': False,
+ 'launched_at': timeutils.utcnow(),
+ 'vm_state': vm_states.ACTIVE})
+
+
+def return_server_by_uuid(context, server_uuid,
+ columns_to_join=None, use_slave=False):
+ return fake_instance.fake_db_instance(
+ **{'id': 1,
+ 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64',
+ 'name': 'fake',
+ 'locked': False,
+ 'launched_at': timeutils.utcnow(),
+ 'metadata': stub_server_metadata(),
+ 'vm_state': vm_states.ACTIVE})
+
+
+def return_server_nonexistent(context, server_id,
+ columns_to_join=None, use_slave=False):
+ raise exception.InstanceNotFound(instance_id=server_id)
+
+
+def fake_change_instance_metadata(self, context, instance, diff):
+ pass
+
+
+class ServerMetaDataTestV21(test.TestCase):
+ validation_ex = exception.ValidationError
+ validation_ex_large = validation_ex
+
+ def setUp(self):
+ super(ServerMetaDataTestV21, self).setUp()
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ self.stubs.Set(nova.db, 'instance_get', return_server)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_by_uuid)
+
+ self.stubs.Set(nova.db, 'instance_metadata_get',
+ return_server_metadata)
+
+ self.stubs.Set(compute_rpcapi.ComputeAPI, 'change_instance_metadata',
+ fake_change_instance_metadata)
+ self._set_up_resources()
+
+ def _set_up_resources(self):
+ self.controller = server_metadata_v21.ServerMetadataController()
+ self.uuid = str(uuid.uuid4())
+ self.url = '/fake/servers/%s/metadata' % self.uuid
+
+ def _get_request(self, param_url=''):
+ return fakes.HTTPRequestV3.blank(self.url + param_url)
+
+ def test_index(self):
+ req = self._get_request()
+ res_dict = self.controller.index(req, self.uuid)
+
+ expected = {
+ 'metadata': {
+ 'key1': 'value1',
+ 'key2': 'value2',
+ 'key3': 'value3',
+ },
+ }
+ self.assertEqual(expected, res_dict)
+
+ def test_index_nonexistent_server(self):
+ self.stubs.Set(nova.db, 'instance_metadata_get',
+ return_server_nonexistent)
+ req = self._get_request()
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.index, req, self.url)
+
+ def test_index_no_data(self):
+ self.stubs.Set(nova.db, 'instance_metadata_get',
+ return_empty_server_metadata)
+ req = self._get_request()
+ res_dict = self.controller.index(req, self.uuid)
+ expected = {'metadata': {}}
+ self.assertEqual(expected, res_dict)
+
+ def test_show(self):
+ req = self._get_request('/key2')
+ res_dict = self.controller.show(req, self.uuid, 'key2')
+ expected = {"meta": {'key2': 'value2'}}
+ self.assertEqual(expected, res_dict)
+
+ def test_show_nonexistent_server(self):
+ self.stubs.Set(nova.db, 'instance_metadata_get',
+ return_server_nonexistent)
+ req = self._get_request('/key2')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, self.uuid, 'key2')
+
+ def test_show_meta_not_found(self):
+ self.stubs.Set(nova.db, 'instance_metadata_get',
+ return_empty_server_metadata)
+ req = self._get_request('/key6')
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.show, req, self.uuid, 'key6')
+
+ def test_delete(self):
+ self.stubs.Set(nova.db, 'instance_metadata_get',
+ return_server_metadata)
+ self.stubs.Set(nova.db, 'instance_metadata_delete',
+ delete_server_metadata)
+ req = self._get_request('/key2')
+ req.method = 'DELETE'
+ res = self.controller.delete(req, self.uuid, 'key2')
+
+ self.assertIsNone(res)
+
+ def test_delete_nonexistent_server(self):
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_nonexistent)
+ req = self._get_request('/key1')
+ req.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, req, self.uuid, 'key1')
+
+ def test_delete_meta_not_found(self):
+ self.stubs.Set(nova.db, 'instance_metadata_get',
+ return_empty_server_metadata)
+ req = self._get_request('/key6')
+ req.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.delete, req, self.uuid, 'key6')
+
+ def test_create(self):
+ self.stubs.Set(objects.Instance, 'save', fake_instance_save)
+ req = self._get_request()
+ req.method = 'POST'
+ req.content_type = "application/json"
+ body = {"metadata": {"key9": "value9"}}
+ req.body = jsonutils.dumps(body)
+ res_dict = self.controller.create(req, self.uuid, body=body)
+
+ body['metadata'].update({
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ })
+ self.assertEqual(body, res_dict)
+
+ def test_create_empty_body(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request()
+ req.method = 'POST'
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.create, req, self.uuid, body=None)
+
+ def test_create_item_empty_key(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ body = {"metadata": {"": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.create, req, self.uuid, body=body)
+
+ def test_create_item_non_dict(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ body = {"metadata": None}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.create, req, self.uuid, body=body)
+
+ def test_create_item_key_too_long(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ body = {"metadata": {("a" * 260): "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex_large,
+ self.controller.create,
+ req, self.uuid, body=body)
+
+ def test_create_malformed_container(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ body = {"meta": {}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.create, req, self.uuid, body=body)
+
+ def test_create_malformed_data(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ body = {"metadata": ['asdf']}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.create, req, self.uuid, body=body)
+
+ def test_create_nonexistent_server(self):
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_nonexistent)
+ req = self._get_request()
+ req.method = 'POST'
+ body = {"metadata": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.create, req, self.uuid, body=body)
+
+ def test_update_metadata(self):
+ self.stubs.Set(objects.Instance, 'save', fake_instance_save)
+ req = self._get_request()
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ expected = {
+ 'metadata': {
+ 'key1': 'updatedvalue',
+ 'key29': 'newkey',
+ }
+ }
+ req.body = jsonutils.dumps(expected)
+ response = self.controller.update_all(req, self.uuid, body=expected)
+ self.assertEqual(expected, response)
+
+ def test_update_all(self):
+ self.stubs.Set(objects.Instance, 'save', fake_instance_save)
+ req = self._get_request()
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {
+ 'metadata': {
+ 'key10': 'value10',
+ 'key99': 'value99',
+ },
+ }
+ req.body = jsonutils.dumps(expected)
+ res_dict = self.controller.update_all(req, self.uuid, body=expected)
+
+ self.assertEqual(expected, res_dict)
+
+ def test_update_all_empty_container(self):
+ self.stubs.Set(objects.Instance, 'save', fake_instance_save)
+ req = self._get_request()
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'metadata': {}}
+ req.body = jsonutils.dumps(expected)
+ res_dict = self.controller.update_all(req, self.uuid, body=expected)
+
+ self.assertEqual(expected, res_dict)
+
+ def test_update_all_empty_body_item(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/key1')
+ req.method = 'PUT'
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update_all, req, self.uuid,
+ body=None)
+
+ def test_update_all_with_non_dict_item(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = fakes.HTTPRequest.blank(self.url + '/bad')
+ req.method = 'PUT'
+ body = {"metadata": None}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update_all, req, self.uuid,
+ body=body)
+
+ def test_update_all_malformed_container(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request()
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'meta': {}}
+ req.body = jsonutils.dumps(expected)
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update_all, req, self.uuid,
+ body=expected)
+
+ def test_update_all_malformed_data(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request()
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ expected = {'metadata': ['asdf']}
+ req.body = jsonutils.dumps(expected)
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update_all, req, self.uuid,
+ body=expected)
+
+ def test_update_all_nonexistent_server(self):
+ self.stubs.Set(nova.db, 'instance_get', return_server_nonexistent)
+ req = self._get_request()
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ body = {'metadata': {'key10': 'value10'}}
+ req.body = jsonutils.dumps(body)
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update_all, req, '100', body=body)
+
+ def test_update_all_non_dict(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request()
+ req.method = 'PUT'
+ body = {"metadata": None}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex, self.controller.update_all,
+ req, self.uuid, body=body)
+
+ def test_update_item(self):
+ self.stubs.Set(objects.Instance, 'save', fake_instance_save)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res_dict = self.controller.update(req, self.uuid, 'key1', body=body)
+ expected = {"meta": {'key1': 'value1'}}
+ self.assertEqual(expected, res_dict)
+
+ def test_update_item_nonexistent_server(self):
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ return_server_nonexistent)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.controller.update, req, self.uuid, 'key1',
+ body=body)
+
+ def test_update_item_empty_body(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update, req, self.uuid, 'key1',
+ body=None)
+
+ def test_update_malformed_container(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'PUT'
+ expected = {'meta': {}}
+ req.body = jsonutils.dumps(expected)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update, req, self.uuid, 'key1',
+ body=expected)
+
+ def test_update_malformed_data(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'PUT'
+ expected = {'metadata': ['asdf']}
+ req.body = jsonutils.dumps(expected)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update, req, self.uuid, 'key1',
+ body=expected)
+
+ def test_update_item_empty_key(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ body = {"meta": {"": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update, req, self.uuid, '',
+ body=body)
+
+ def test_update_item_key_too_long(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ body = {"meta": {("a" * 260): "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex_large,
+ self.controller.update,
+ req, self.uuid, ("a" * 260), body=body)
+
+ def test_update_item_value_too_long(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": ("a" * 260)}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex_large,
+ self.controller.update,
+ req, self.uuid, "key1", body=body)
+
+ def test_update_item_too_many_keys(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/key1')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1", "key2": "value2"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update, req, self.uuid, 'key1',
+ body=body)
+
+ def test_update_item_body_uri_mismatch(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/bad')
+ req.method = 'PUT'
+ body = {"meta": {"key1": "value1"}}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, self.uuid, 'bad',
+ body=body)
+
+ def test_update_item_non_dict(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request('/bad')
+ req.method = 'PUT'
+ body = {"meta": None}
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update, req, self.uuid, 'bad',
+ body=body)
+
+ def test_update_empty_container(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = fakes.HTTPRequest.blank(self.url)
+ req.method = 'PUT'
+ expected = {'metadata': {}}
+ req.body = jsonutils.dumps(expected)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(self.validation_ex,
+ self.controller.update, req, self.uuid, 'bad',
+ body=expected)
+
+ def test_too_many_metadata_items_on_create(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ data = {"metadata": {}}
+ for num in range(CONF.quota_metadata_items + 1):
+ data['metadata']['key%i' % num] = "blah"
+ req = self._get_request()
+ req.method = 'POST'
+ req.body = jsonutils.dumps(data)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.create, req, self.uuid, body=data)
+
+ def test_invalid_metadata_items_on_create(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request()
+ req.method = 'POST'
+ req.headers["content-type"] = "application/json"
+
+ # test for long key
+ data = {"metadata": {"a" * 260: "value1"}}
+ req.body = jsonutils.dumps(data)
+ self.assertRaises(self.validation_ex_large,
+ self.controller.create, req, self.uuid, body=data)
+
+ # test for long value
+ data = {"metadata": {"key": "v" * 260}}
+ req.body = jsonutils.dumps(data)
+ self.assertRaises(self.validation_ex_large,
+ self.controller.create, req, self.uuid, body=data)
+
+ # test for empty key.
+ data = {"metadata": {"": "value1"}}
+ req.body = jsonutils.dumps(data)
+ self.assertRaises(self.validation_ex,
+ self.controller.create, req, self.uuid, body=data)
+
+ def test_too_many_metadata_items_on_update_item(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ data = {"metadata": {}}
+ for num in range(CONF.quota_metadata_items + 1):
+ data['metadata']['key%i' % num] = "blah"
+ req = self._get_request()
+ req.method = 'PUT'
+ req.body = jsonutils.dumps(data)
+ req.headers["content-type"] = "application/json"
+
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.update_all,
+ req, self.uuid, body=data)
+
+ def test_invalid_metadata_items_on_update_item(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ data = {"metadata": {}}
+ for num in range(CONF.quota_metadata_items + 1):
+ data['metadata']['key%i' % num] = "blah"
+ req = self._get_request()
+ req.method = 'PUT'
+ req.body = jsonutils.dumps(data)
+ req.headers["content-type"] = "application/json"
+
+ # test for long key
+ data = {"metadata": {"a" * 260: "value1"}}
+ req.body = jsonutils.dumps(data)
+ self.assertRaises(self.validation_ex_large,
+ self.controller.update_all, req, self.uuid,
+ body=data)
+
+ # test for long value
+ data = {"metadata": {"key": "v" * 260}}
+ req.body = jsonutils.dumps(data)
+ self.assertRaises(self.validation_ex_large,
+ self.controller.update_all, req, self.uuid,
+ body=data)
+
+ # test for empty key.
+ data = {"metadata": {"": "value1"}}
+ req.body = jsonutils.dumps(data)
+ self.assertRaises(self.validation_ex,
+ self.controller.update_all, req, self.uuid,
+ body=data)
+
+
+class ServerMetaDataTestV2(ServerMetaDataTestV21):
+ validation_ex = webob.exc.HTTPBadRequest
+ validation_ex_large = webob.exc.HTTPRequestEntityTooLarge
+
+ def _set_up_resources(self):
+ self.controller = server_metadata_v2.Controller()
+ self.uuid = str(uuid.uuid4())
+ self.url = '/v1.1/fake/servers/%s/metadata' % self.uuid
+
+ def _get_request(self, param_url=''):
+ return fakes.HTTPRequest.blank(self.url + param_url)
+
+
+class BadStateServerMetaDataTestV21(test.TestCase):
+
+ def setUp(self):
+ super(BadStateServerMetaDataTestV21, self).setUp()
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ self.stubs.Set(nova.db, 'instance_metadata_get',
+ return_server_metadata)
+ self.stubs.Set(compute_rpcapi.ComputeAPI, 'change_instance_metadata',
+ fake_change_instance_metadata)
+ self.stubs.Set(nova.db, 'instance_get', self._return_server_in_build)
+ self.stubs.Set(nova.db, 'instance_get_by_uuid',
+ self._return_server_in_build_by_uuid)
+ self.stubs.Set(nova.db, 'instance_metadata_delete',
+ delete_server_metadata)
+ self._set_up_resources()
+
+ def _set_up_resources(self):
+ self.controller = server_metadata_v21.ServerMetadataController()
+ self.uuid = str(uuid.uuid4())
+ self.url = '/fake/servers/%s/metadata' % self.uuid
+
+ def _get_request(self, param_url=''):
+ return fakes.HTTPRequestV3.blank(self.url + param_url)
+
+ def test_invalid_state_on_delete(self):
+ req = self._get_request('/key2')
+ req.method = 'DELETE'
+ self.assertRaises(webob.exc.HTTPConflict, self.controller.delete,
+ req, self.uuid, 'key2')
+
+ def test_invalid_state_on_update_metadata(self):
+ self.stubs.Set(nova.db, 'instance_metadata_update',
+ return_create_instance_metadata)
+ req = self._get_request()
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ expected = {
+ 'metadata': {
+ 'key1': 'updatedvalue',
+ 'key29': 'newkey',
+ }
+ }
+ req.body = jsonutils.dumps(expected)
+ self.assertRaises(webob.exc.HTTPConflict, self.controller.update_all,
+ req, self.uuid, body=expected)
+
+ def _return_server_in_build(self, context, server_id,
+ columns_to_join=None):
+ return fake_instance.fake_db_instance(
+ **{'id': server_id,
+ 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64',
+ 'name': 'fake',
+ 'locked': False,
+ 'vm_state': vm_states.BUILDING})
+
+ def _return_server_in_build_by_uuid(self, context, server_uuid,
+ columns_to_join=None, use_slave=False):
+ return fake_instance.fake_db_instance(
+ **{'id': 1,
+ 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64',
+ 'name': 'fake',
+ 'locked': False,
+ 'vm_state': vm_states.BUILDING})
+
+ @mock.patch.object(nova.compute.api.API, 'update_instance_metadata',
+ side_effect=exception.InstanceIsLocked(instance_uuid=0))
+ def test_instance_lock_update_metadata(self, mock_update):
+ req = self._get_request()
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ expected = {
+ 'metadata': {
+ 'keydummy': 'newkey',
+ }
+ }
+ req.body = jsonutils.dumps(expected)
+ self.assertRaises(webob.exc.HTTPConflict, self.controller.update_all,
+ req, self.uuid, body=expected)
+
+
+class BadStateServerMetaDataTestV2(BadStateServerMetaDataTestV21):
+ def _set_up_resources(self):
+ self.controller = server_metadata_v2.Controller()
+ self.uuid = str(uuid.uuid4())
+ self.url = '/v1.1/fake/servers/%s/metadata' % self.uuid
+
+ def _get_request(self, param_url=''):
+ return fakes.HTTPRequest.blank(self.url + param_url)
diff --git a/nova/tests/unit/api/openstack/compute/test_servers.py b/nova/tests/unit/api/openstack/compute/test_servers.py
new file mode 100644
index 0000000000..c37df741ec
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_servers.py
@@ -0,0 +1,4625 @@
+# Copyright 2010-2011 OpenStack Foundation
+# Copyright 2011 Piston Cloud Computing, Inc.
+# All Rights Reserved.
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import base64
+import contextlib
+import datetime
+import urllib
+import uuid
+
+import iso8601
+from lxml import etree
+import mock
+from oslo.config import cfg
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+import six
+import six.moves.urllib.parse as urlparse
+import testtools
+import webob
+
+from nova.api.openstack import compute
+from nova.api.openstack.compute import ips
+from nova.api.openstack.compute import servers
+from nova.api.openstack.compute import views
+from nova.api.openstack import extensions
+from nova.api.openstack import xmlutil
+from nova.compute import api as compute_api
+from nova.compute import delete_types
+from nova.compute import flavors
+from nova.compute import task_states
+from nova.compute import vm_states
+from nova import context
+from nova import db
+from nova.db.sqlalchemy import models
+from nova import exception
+from nova.i18n import _
+from nova.image import glance
+from nova.network import manager
+from nova.network.neutronv2 import api as neutron_api
+from nova import objects
+from nova.objects import instance as instance_obj
+from nova.openstack.common import policy as common_policy
+from nova import policy
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import fake_instance
+from nova.tests.unit import fake_network
+from nova.tests.unit.image import fake
+from nova.tests.unit import matchers
+from nova.tests.unit.objects import test_keypair
+from nova.tests.unit import utils
+from nova import utils as nova_utils
+
+CONF = cfg.CONF
+CONF.import_opt('password_length', 'nova.utils')
+
+FAKE_UUID = fakes.FAKE_UUID
+NS = "{http://docs.openstack.org/compute/api/v1.1}"
+ATOMNS = "{http://www.w3.org/2005/Atom}"
+XPATH_NS = {
+ 'atom': 'http://www.w3.org/2005/Atom',
+ 'ns': 'http://docs.openstack.org/compute/api/v1.1'
+}
+
+INSTANCE_IDS = {FAKE_UUID: 1}
+
+FIELDS = instance_obj.INSTANCE_DEFAULT_FIELDS
+
+
+def fake_gen_uuid():
+ return FAKE_UUID
+
+
+def return_servers_empty(context, *args, **kwargs):
+ return []
+
+
+def return_security_group(context, instance_id, security_group_id):
+ pass
+
+
+def instance_update_and_get_original(context, instance_uuid, values,
+ update_cells=True,
+ columns_to_join=None,
+ ):
+ inst = fakes.stub_instance(INSTANCE_IDS.get(instance_uuid),
+ name=values.get('display_name'))
+ inst = dict(inst, **values)
+ return (inst, inst)
+
+
+def instance_update(context, instance_uuid, values, update_cells=True):
+ inst = fakes.stub_instance(INSTANCE_IDS.get(instance_uuid),
+ name=values.get('display_name'))
+ inst = dict(inst, **values)
+ return inst
+
+
+def fake_compute_api(cls, req, id):
+ return True
+
+
+class MockSetAdminPassword(object):
+ def __init__(self):
+ self.instance_id = None
+ self.password = None
+
+ def __call__(self, context, instance_id, password):
+ self.instance_id = instance_id
+ self.password = password
+
+
+class Base64ValidationTest(test.TestCase):
+ def setUp(self):
+ super(Base64ValidationTest, self).setUp()
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = servers.Controller(self.ext_mgr)
+
+ def test_decode_base64(self):
+ value = "A random string"
+ result = self.controller._decode_base64(base64.b64encode(value))
+ self.assertEqual(result, value)
+
+ def test_decode_base64_binary(self):
+ value = "\x00\x12\x75\x99"
+ result = self.controller._decode_base64(base64.b64encode(value))
+ self.assertEqual(result, value)
+
+ def test_decode_base64_whitespace(self):
+ value = "A random string"
+ encoded = base64.b64encode(value)
+ white = "\n \n%s\t%s\n" % (encoded[:2], encoded[2:])
+ result = self.controller._decode_base64(white)
+ self.assertEqual(result, value)
+
+ def test_decode_base64_invalid(self):
+ invalid = "A random string"
+ result = self.controller._decode_base64(invalid)
+ self.assertIsNone(result)
+
+ def test_decode_base64_illegal_bytes(self):
+ value = "A random string"
+ encoded = base64.b64encode(value)
+ white = ">\x01%s*%s()" % (encoded[:2], encoded[2:])
+ result = self.controller._decode_base64(white)
+ self.assertIsNone(result)
+
+
+class NeutronV2Subclass(neutron_api.API):
+ """Used to ensure that API handles subclasses properly."""
+ pass
+
+
+class ControllerTest(test.TestCase):
+
+ def setUp(self):
+ super(ControllerTest, self).setUp()
+ self.flags(verbose=True, use_ipv6=False)
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ fake.stub_out_image_service(self.stubs)
+ return_server = fakes.fake_instance_get()
+ return_servers = fakes.fake_instance_get_all_by_filters()
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ return_servers)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ return_server)
+ self.stubs.Set(db, 'instance_add_security_group',
+ return_security_group)
+ self.stubs.Set(db, 'instance_update_and_get_original',
+ instance_update_and_get_original)
+
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = servers.Controller(self.ext_mgr)
+ self.ips_controller = ips.Controller()
+ policy.reset()
+ policy.init()
+ fake_network.stub_out_nw_api_get_instance_nw_info(self.stubs)
+
+
+class ServersControllerTest(ControllerTest):
+ def test_can_check_loaded_extensions(self):
+ self.ext_mgr.extensions = {'os-fake': None}
+ self.assertTrue(self.controller.ext_mgr.is_loaded('os-fake'))
+ self.assertFalse(self.controller.ext_mgr.is_loaded('os-not-loaded'))
+
+ def test_requested_networks_prefix(self):
+ uuid = 'br-00000000-0000-0000-0000-000000000000'
+ requested_networks = [{'uuid': uuid}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertIn((uuid, None), res.as_tuples())
+
+ def test_requested_networks_neutronv2_enabled_with_port(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'port': port}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(None, None, port, None)], res.as_tuples())
+
+ def test_requested_networks_neutronv2_enabled_with_network(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ requested_networks = [{'uuid': network}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(network, None, None, None)], res.as_tuples())
+
+ def test_requested_networks_neutronv2_enabled_with_network_and_port(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network, 'port': port}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(None, None, port, None)], res.as_tuples())
+
+ def test_requested_networks_neutronv2_disabled_with_port(self):
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'port': port}]
+ self.assertRaises(
+ webob.exc.HTTPBadRequest,
+ self.controller._get_requested_networks,
+ requested_networks)
+
+ def test_requested_networks_api_enabled_with_v2_subclass(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network, 'port': port}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(None, None, port, None)], res.as_tuples())
+
+ def test_requested_networks_neutronv2_subclass_with_port(self):
+ cls = ('nova.tests.unit.api.openstack.compute' +
+ '.test_servers.NeutronV2Subclass')
+ self.flags(network_api_class=cls)
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'port': port}]
+ res = self.controller._get_requested_networks(requested_networks)
+ self.assertEqual([(None, None, port, None)], res.as_tuples())
+
+ def test_get_server_by_uuid(self):
+ req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID)
+ res_dict = self.controller.show(req, FAKE_UUID)
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+
+ def test_unique_host_id(self):
+ """Create two servers with the same host and different
+ project_ids and check that the hostId's are unique.
+ """
+ def return_instance_with_host(self, *args, **kwargs):
+ project_id = str(uuid.uuid4())
+ return fakes.stub_instance(id=1, uuid=FAKE_UUID,
+ project_id=project_id,
+ host='fake_host')
+
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ return_instance_with_host)
+ self.stubs.Set(db, 'instance_get',
+ return_instance_with_host)
+
+ req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID)
+ server1 = self.controller.show(req, FAKE_UUID)
+ server2 = self.controller.show(req, FAKE_UUID)
+
+ self.assertNotEqual(server1['server']['hostId'],
+ server2['server']['hostId'])
+
+ def _get_server_data_dict(self, uuid, image_bookmark, flavor_bookmark,
+ status="ACTIVE", progress=100):
+ return {
+ "server": {
+ "id": uuid,
+ "user_id": "fake_user",
+ "tenant_id": "fake_project",
+ "updated": "2010-11-11T11:00:00Z",
+ "created": "2010-10-10T12:00:00Z",
+ "progress": progress,
+ "name": "server1",
+ "status": status,
+ "accessIPv4": "",
+ "accessIPv6": "",
+ "hostId": '',
+ "image": {
+ "id": "10",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": image_bookmark,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": flavor_bookmark,
+ },
+ ],
+ },
+ "addresses": {
+ 'test1': [
+ {'version': 4, 'addr': '192.168.1.100'},
+ {'version': 6, 'addr': '2001:db8:0:1::1'}
+ ]
+ },
+ "metadata": {
+ "seq": "1",
+ },
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/servers/%s" % uuid,
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/servers/%s" % uuid,
+ },
+ ],
+ }
+ }
+
+ def test_get_server_by_id(self):
+ self.flags(use_ipv6=True)
+ image_bookmark = "http://localhost/fake/images/10"
+ flavor_bookmark = "http://localhost/fake/flavors/1"
+
+ uuid = FAKE_UUID
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % uuid)
+ res_dict = self.controller.show(req, uuid)
+
+ expected_server = self._get_server_data_dict(uuid,
+ image_bookmark,
+ flavor_bookmark,
+ status="BUILD",
+ progress=0)
+ self.assertThat(res_dict, matchers.DictMatches(expected_server))
+
+ def test_get_server_with_active_status_by_id(self):
+ image_bookmark = "http://localhost/fake/images/10"
+ flavor_bookmark = "http://localhost/fake/flavors/1"
+
+ new_return_server = fakes.fake_instance_get(
+ vm_state=vm_states.ACTIVE, progress=100)
+ self.stubs.Set(db, 'instance_get_by_uuid', new_return_server)
+
+ uuid = FAKE_UUID
+ req = fakes.HTTPRequest.blank('/fake/servers/%s' % uuid)
+ res_dict = self.controller.show(req, uuid)
+ expected_server = self._get_server_data_dict(uuid,
+ image_bookmark,
+ flavor_bookmark)
+ self.assertThat(res_dict, matchers.DictMatches(expected_server))
+
+ def test_get_server_with_id_image_ref_by_id(self):
+ image_ref = "10"
+ image_bookmark = "http://localhost/fake/images/10"
+ flavor_id = "1"
+ flavor_bookmark = "http://localhost/fake/flavors/1"
+
+ new_return_server = fakes.fake_instance_get(
+ vm_state=vm_states.ACTIVE, image_ref=image_ref,
+ flavor_id=flavor_id, progress=100)
+ self.stubs.Set(db, 'instance_get_by_uuid', new_return_server)
+
+ uuid = FAKE_UUID
+ req = fakes.HTTPRequest.blank('/fake/servers/%s' % uuid)
+ res_dict = self.controller.show(req, uuid)
+ expected_server = self._get_server_data_dict(uuid,
+ image_bookmark,
+ flavor_bookmark)
+ self.assertThat(res_dict, matchers.DictMatches(expected_server))
+
+ def test_get_server_addresses_from_cache(self):
+ pub0 = ('172.19.0.1', '172.19.0.2',)
+ pub1 = ('1.2.3.4',)
+ pub2 = ('b33f::fdee:ddff:fecc:bbaa',)
+ priv0 = ('192.168.0.3', '192.168.0.4',)
+
+ def _ip(ip):
+ return {'address': ip, 'type': 'fixed'}
+
+ nw_cache = [
+ {'address': 'aa:aa:aa:aa:aa:aa',
+ 'id': 1,
+ 'network': {'bridge': 'br0',
+ 'id': 1,
+ 'label': 'public',
+ 'subnets': [{'cidr': '172.19.0.0/24',
+ 'ips': [_ip(ip) for ip in pub0]},
+ {'cidr': '1.2.3.0/16',
+ 'ips': [_ip(ip) for ip in pub1]},
+ {'cidr': 'b33f::/64',
+ 'ips': [_ip(ip) for ip in pub2]}]}},
+ {'address': 'bb:bb:bb:bb:bb:bb',
+ 'id': 2,
+ 'network': {'bridge': 'br1',
+ 'id': 2,
+ 'label': 'private',
+ 'subnets': [{'cidr': '192.168.0.0/24',
+ 'ips': [_ip(ip) for ip in priv0]}]}}]
+
+ return_server = fakes.fake_instance_get(nw_cache=nw_cache)
+ self.stubs.Set(db, 'instance_get_by_uuid', return_server)
+
+ req = fakes.HTTPRequest.blank('/fake/servers/%s/ips' % FAKE_UUID)
+ res_dict = self.ips_controller.index(req, FAKE_UUID)
+
+ expected = {
+ 'addresses': {
+ 'private': [
+ {'version': 4, 'addr': '192.168.0.3'},
+ {'version': 4, 'addr': '192.168.0.4'},
+ ],
+ 'public': [
+ {'version': 4, 'addr': '172.19.0.1'},
+ {'version': 4, 'addr': '172.19.0.2'},
+ {'version': 4, 'addr': '1.2.3.4'},
+ {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'},
+ ],
+ },
+ }
+ self.assertThat(res_dict, matchers.DictMatches(expected))
+
+ def test_get_server_addresses_nonexistent_network(self):
+ url = '/fake/servers/%s/ips/network_0' % FAKE_UUID
+ req = fakes.HTTPRequest.blank(url)
+ self.assertRaises(webob.exc.HTTPNotFound, self.ips_controller.show,
+ req, FAKE_UUID, 'network_0')
+
+ def test_get_server_addresses_nonexistent_server(self):
+ def fake_instance_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get)
+
+ server_id = str(uuid.uuid4())
+ req = fakes.HTTPRequest.blank('/fake/servers/%s/ips' % server_id)
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self.ips_controller.index, req, server_id)
+
+ def test_get_server_list_empty(self):
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ return_servers_empty)
+
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ res_dict = self.controller.index(req)
+
+ num_servers = len(res_dict['servers'])
+ self.assertEqual(0, num_servers)
+
+ def test_get_server_list_with_reservation_id(self):
+ req = fakes.HTTPRequest.blank('/fake/servers?reservation_id=foo')
+ res_dict = self.controller.index(req)
+
+ i = 0
+ for s in res_dict['servers']:
+ self.assertEqual(s.get('name'), 'server%d' % (i + 1))
+ i += 1
+
+ def test_get_server_list_with_reservation_id_empty(self):
+ req = fakes.HTTPRequest.blank('/fake/servers/detail?'
+ 'reservation_id=foo')
+ res_dict = self.controller.detail(req)
+
+ i = 0
+ for s in res_dict['servers']:
+ self.assertEqual(s.get('name'), 'server%d' % (i + 1))
+ i += 1
+
+ def test_get_server_list_with_reservation_id_details(self):
+ req = fakes.HTTPRequest.blank('/fake/servers/detail?'
+ 'reservation_id=foo')
+ res_dict = self.controller.detail(req)
+
+ i = 0
+ for s in res_dict['servers']:
+ self.assertEqual(s.get('name'), 'server%d' % (i + 1))
+ i += 1
+
+ def test_get_server_list(self):
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ res_dict = self.controller.index(req)
+
+ self.assertEqual(len(res_dict['servers']), 5)
+ for i, s in enumerate(res_dict['servers']):
+ self.assertEqual(s['id'], fakes.get_fake_uuid(i))
+ self.assertEqual(s['name'], 'server%d' % (i + 1))
+ self.assertIsNone(s.get('image', None))
+
+ expected_links = [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/fake/servers/%s" % s['id'],
+ },
+ {
+ "rel": "bookmark",
+ "href": "http://localhost/fake/servers/%s" % s['id'],
+ },
+ ]
+
+ self.assertEqual(s['links'], expected_links)
+
+ def test_get_servers_with_limit(self):
+ req = fakes.HTTPRequest.blank('/fake/servers?limit=3')
+ res_dict = self.controller.index(req)
+
+ servers = res_dict['servers']
+ self.assertEqual([s['id'] for s in servers],
+ [fakes.get_fake_uuid(i) for i in xrange(len(servers))])
+
+ servers_links = res_dict['servers_links']
+ self.assertEqual(servers_links[0]['rel'], 'next')
+ href_parts = urlparse.urlparse(servers_links[0]['href'])
+ self.assertEqual('/v2/fake/servers', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ expected_params = {'limit': ['3'],
+ 'marker': [fakes.get_fake_uuid(2)]}
+ self.assertThat(params, matchers.DictMatches(expected_params))
+
+ def test_get_servers_with_limit_bad_value(self):
+ req = fakes.HTTPRequest.blank('/fake/servers?limit=aaa')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_server_details_empty(self):
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ return_servers_empty)
+
+ req = fakes.HTTPRequest.blank('/fake/servers/detail')
+ res_dict = self.controller.detail(req)
+
+ num_servers = len(res_dict['servers'])
+ self.assertEqual(0, num_servers)
+
+ def test_get_server_details_with_limit(self):
+ req = fakes.HTTPRequest.blank('/fake/servers/detail?limit=3')
+ res = self.controller.detail(req)
+
+ servers = res['servers']
+ self.assertEqual([s['id'] for s in servers],
+ [fakes.get_fake_uuid(i) for i in xrange(len(servers))])
+
+ servers_links = res['servers_links']
+ self.assertEqual(servers_links[0]['rel'], 'next')
+
+ href_parts = urlparse.urlparse(servers_links[0]['href'])
+ self.assertEqual('/v2/fake/servers/detail', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ expected = {'limit': ['3'], 'marker': [fakes.get_fake_uuid(2)]}
+ self.assertThat(params, matchers.DictMatches(expected))
+
+ def test_get_server_details_with_limit_bad_value(self):
+ req = fakes.HTTPRequest.blank('/fake/servers/detail?limit=aaa')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.detail, req)
+
+ def test_get_server_details_with_limit_and_other_params(self):
+ req = fakes.HTTPRequest.blank('/fake/servers/detail'
+ '?limit=3&blah=2:t')
+ res = self.controller.detail(req)
+
+ servers = res['servers']
+ self.assertEqual([s['id'] for s in servers],
+ [fakes.get_fake_uuid(i) for i in xrange(len(servers))])
+
+ servers_links = res['servers_links']
+ self.assertEqual(servers_links[0]['rel'], 'next')
+
+ href_parts = urlparse.urlparse(servers_links[0]['href'])
+ self.assertEqual('/v2/fake/servers/detail', href_parts.path)
+ params = urlparse.parse_qs(href_parts.query)
+ expected = {'limit': ['3'], 'blah': ['2:t'],
+ 'marker': [fakes.get_fake_uuid(2)]}
+ self.assertThat(params, matchers.DictMatches(expected))
+
+ def test_get_servers_with_too_big_limit(self):
+ req = fakes.HTTPRequest.blank('/fake/servers?limit=30')
+ res_dict = self.controller.index(req)
+ self.assertNotIn('servers_links', res_dict)
+
+ def test_get_servers_with_bad_limit(self):
+ req = fakes.HTTPRequest.blank('/fake/servers?limit=asdf')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_servers_with_marker(self):
+ url = '/v2/fake/servers?marker=%s' % fakes.get_fake_uuid(2)
+ req = fakes.HTTPRequest.blank(url)
+ servers = self.controller.index(req)['servers']
+ self.assertEqual([s['name'] for s in servers], ["server4", "server5"])
+
+ def test_get_servers_with_limit_and_marker(self):
+ url = '/v2/fake/servers?limit=2&marker=%s' % fakes.get_fake_uuid(1)
+ req = fakes.HTTPRequest.blank(url)
+ servers = self.controller.index(req)['servers']
+ self.assertEqual([s['name'] for s in servers], ['server3', 'server4'])
+
+ def test_get_servers_with_bad_marker(self):
+ req = fakes.HTTPRequest.blank('/fake/servers?limit=2&marker=asdf')
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_get_servers_with_bad_option(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?unknownoption=whee')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_image(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('image', search_opts)
+ self.assertEqual(search_opts['image'], '12345')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?image=12345')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_tenant_id_filter_converts_to_project_id_for_admin(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False):
+ self.assertIsNotNone(filters)
+ self.assertEqual(filters['project_id'], 'newfake')
+ self.assertFalse(filters.get('tenant_id'))
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers'
+ '?all_tenants=1&tenant_id=newfake',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_normal(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False):
+ self.assertNotIn('project_id', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?all_tenants',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_one(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False):
+ self.assertNotIn('project_id', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=1',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_zero(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False):
+ self.assertNotIn('all_tenants', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=0',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_false(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False):
+ self.assertNotIn('all_tenants', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=false',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_param_invalid(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None):
+ self.assertNotIn('all_tenants', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=xxx',
+ use_admin_context=True)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.index, req)
+
+ def test_admin_restricted_tenant(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False):
+ self.assertIsNotNone(filters)
+ self.assertEqual(filters['project_id'], 'fake')
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers',
+ use_admin_context=True)
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_pass_policy(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None, use_slave=False):
+ self.assertIsNotNone(filters)
+ self.assertNotIn('project_id', filters)
+ return [fakes.stub_instance(100)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ rules = {
+ "compute:get_all_tenants":
+ common_policy.parse_rule("project_id:fake"),
+ "compute:get_all":
+ common_policy.parse_rule("project_id:fake"),
+ }
+
+ policy.set_rules(rules)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=1')
+ res = self.controller.index(req)
+
+ self.assertIn('servers', res)
+
+ def test_all_tenants_fail_policy(self):
+ def fake_get_all(context, filters=None, sort_key=None,
+ sort_dir='desc', limit=None, marker=None,
+ columns_to_join=None):
+ self.assertIsNotNone(filters)
+ return [fakes.stub_instance(100)]
+
+ rules = {
+ "compute:get_all_tenants":
+ common_policy.parse_rule("project_id:non_fake"),
+ "compute:get_all":
+ common_policy.parse_rule("project_id:fake"),
+ }
+
+ policy.set_rules(rules)
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=1')
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.index, req)
+
+ def test_get_servers_allows_flavor(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('flavor', search_opts)
+ # flavor is an integer ID
+ self.assertEqual(search_opts['flavor'], '12345')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?flavor=12345')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_with_bad_flavor(self):
+ req = fakes.HTTPRequest.blank('/fake/servers?flavor=abcde')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 0)
+
+ def test_get_server_details_with_bad_flavor(self):
+ req = fakes.HTTPRequest.blank('/fake/servers/detail?flavor=abcde')
+ servers = self.controller.detail(req)['servers']
+
+ self.assertThat(servers, testtools.matchers.HasLength(0))
+
+ def test_get_servers_allows_status(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('vm_state', search_opts)
+ self.assertEqual(search_opts['vm_state'], [vm_states.ACTIVE])
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?status=active')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ @mock.patch.object(compute_api.API, 'get_all')
+ def test_get_servers_allows_multi_status(self, get_all_mock):
+ server_uuid0 = str(uuid.uuid4())
+ server_uuid1 = str(uuid.uuid4())
+ db_list = [fakes.stub_instance(100, uuid=server_uuid0),
+ fakes.stub_instance(101, uuid=server_uuid1)]
+ get_all_mock.return_value = instance_obj._make_instance_list(
+ context, instance_obj.InstanceList(), db_list, FIELDS)
+
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers?status=active&status=error')
+ servers = self.controller.index(req)['servers']
+ self.assertEqual(2, len(servers))
+ self.assertEqual(server_uuid0, servers[0]['id'])
+ self.assertEqual(server_uuid1, servers[1]['id'])
+ expected_search_opts = dict(deleted=False,
+ vm_state=[vm_states.ACTIVE,
+ vm_states.ERROR],
+ project_id='fake')
+ get_all_mock.assert_called_once_with(mock.ANY,
+ search_opts=expected_search_opts, limit=mock.ANY,
+ marker=mock.ANY, want_objects=mock.ANY)
+
+ @mock.patch.object(compute_api.API, 'get_all')
+ def test_get_servers_system_metadata_filter(self, get_all_mock):
+ server_uuid0 = str(uuid.uuid4())
+ server_uuid1 = str(uuid.uuid4())
+ expected_system_metadata = u'{"some_value": "some_key"}'
+ db_list = [fakes.stub_instance(100, uuid=server_uuid0),
+ fakes.stub_instance(101, uuid=server_uuid1)]
+ get_all_mock.return_value = instance_obj._make_instance_list(
+ context, instance_obj.InstanceList(), db_list, FIELDS)
+
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers?status=active&status=error&system_metadata=' +
+ urllib.quote(expected_system_metadata),
+ use_admin_context=True)
+ servers = self.controller.index(req)['servers']
+ self.assertEqual(2, len(servers))
+ self.assertEqual(server_uuid0, servers[0]['id'])
+ self.assertEqual(server_uuid1, servers[1]['id'])
+ expected_search_opts = dict(
+ deleted=False, vm_state=[vm_states.ACTIVE, vm_states.ERROR],
+ system_metadata=expected_system_metadata, project_id='fake')
+ get_all_mock.assert_called_once_with(mock.ANY,
+ search_opts=expected_search_opts, limit=mock.ANY,
+ marker=mock.ANY, want_objects=mock.ANY)
+
+ @mock.patch.object(compute_api.API, 'get_all')
+ def test_get_servers_flavor_not_found(self, get_all_mock):
+ get_all_mock.side_effect = exception.FlavorNotFound(flavor_id=1)
+
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers?status=active&flavor=abc')
+ servers = self.controller.index(req)['servers']
+ self.assertEqual(0, len(servers))
+
+ @mock.patch.object(compute_api.API, 'get_all')
+ def test_get_servers_allows_invalid_status(self, get_all_mock):
+ server_uuid0 = str(uuid.uuid4())
+ server_uuid1 = str(uuid.uuid4())
+ db_list = [fakes.stub_instance(100, uuid=server_uuid0),
+ fakes.stub_instance(101, uuid=server_uuid1)]
+ get_all_mock.return_value = instance_obj._make_instance_list(
+ context, instance_obj.InstanceList(), db_list, FIELDS)
+
+ req = fakes.HTTPRequest.blank(
+ '/fake/servers?status=active&status=invalid')
+ servers = self.controller.index(req)['servers']
+ self.assertEqual(2, len(servers))
+ self.assertEqual(server_uuid0, servers[0]['id'])
+ self.assertEqual(server_uuid1, servers[1]['id'])
+ expected_search_opts = dict(deleted=False,
+ vm_state=[vm_states.ACTIVE],
+ project_id='fake')
+ get_all_mock.assert_called_once_with(mock.ANY,
+ search_opts=expected_search_opts, limit=mock.ANY,
+ marker=mock.ANY, want_objects=mock.ANY)
+
+ def test_get_servers_allows_task_status(self):
+ server_uuid = str(uuid.uuid4())
+ task_state = task_states.REBOOTING
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('task_state', search_opts)
+ self.assertEqual([task_states.REBOOT_PENDING,
+ task_states.REBOOT_STARTED,
+ task_states.REBOOTING],
+ search_opts['task_state'])
+ db_list = [fakes.stub_instance(100, uuid=server_uuid,
+ task_state=task_state)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/servers?status=reboot')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_resize_status(self):
+ # Test when resize status, it maps list of vm states.
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIn('vm_state', search_opts)
+ self.assertEqual(search_opts['vm_state'],
+ [vm_states.ACTIVE, vm_states.STOPPED])
+
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?status=resize')
+
+ servers = self.controller.detail(req)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_invalid_status(self):
+ # Test getting servers by invalid status.
+ req = fakes.HTTPRequest.blank('/fake/servers?status=baloney',
+ use_admin_context=False)
+ servers = self.controller.index(req)['servers']
+ self.assertEqual(len(servers), 0)
+
+ def test_get_servers_deleted_status_as_user(self):
+ req = fakes.HTTPRequest.blank('/fake/servers?status=deleted',
+ use_admin_context=False)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.detail, req)
+
+ def test_get_servers_deleted_status_as_admin(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIn('vm_state', search_opts)
+ self.assertEqual(search_opts['vm_state'], ['deleted'])
+
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?status=deleted',
+ use_admin_context=True)
+
+ servers = self.controller.detail(req)['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_name(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('name', search_opts)
+ self.assertEqual(search_opts['name'], 'whee.*')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?name=whee.*')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_changes_since(self):
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('changes-since', search_opts)
+ changes_since = datetime.datetime(2011, 1, 24, 17, 8, 1,
+ tzinfo=iso8601.iso8601.UTC)
+ self.assertEqual(search_opts['changes-since'], changes_since)
+ self.assertNotIn('deleted', search_opts)
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ params = 'changes-since=2011-01-24T17:08:01Z'
+ req = fakes.HTTPRequest.blank('/fake/servers?%s' % params)
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_changes_since_bad_value(self):
+ params = 'changes-since=asdf'
+ req = fakes.HTTPRequest.blank('/fake/servers?%s' % params)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req)
+
+ def test_get_servers_admin_filters_as_user(self):
+ """Test getting servers by admin-only or unknown options when
+ context is not admin. Make sure the admin and unknown options
+ are stripped before they get to compute_api.get_all()
+ """
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ # Allowed by user
+ self.assertIn('name', search_opts)
+ self.assertIn('ip', search_opts)
+ # OSAPI converts status to vm_state
+ self.assertIn('vm_state', search_opts)
+ # Allowed only by admins with admin API on
+ self.assertNotIn('unknown_option', search_opts)
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = fakes.HTTPRequest.blank('/fake/servers?%s' % query_str)
+ res = self.controller.index(req)
+
+ servers = res['servers']
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_admin_options_as_admin(self):
+ """Test getting servers by admin-only or unknown options when
+ context is admin. All options should be passed
+ """
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ # Allowed by user
+ self.assertIn('name', search_opts)
+ # OSAPI converts status to vm_state
+ self.assertIn('vm_state', search_opts)
+ # Allowed only by admins with admin API on
+ self.assertIn('ip', search_opts)
+ self.assertIn('unknown_option', search_opts)
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ query_str = "name=foo&ip=10.*&status=active&unknown_option=meow"
+ req = fakes.HTTPRequest.blank('/fake/servers?%s' % query_str,
+ use_admin_context=True)
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_allows_ip(self):
+ """Test getting servers by ip."""
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('ip', search_opts)
+ self.assertEqual(search_opts['ip'], '10\..*')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?ip=10\..*')
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_servers_admin_allows_ip6(self):
+ """Test getting servers by ip6 with admin_api enabled and
+ admin context
+ """
+ server_uuid = str(uuid.uuid4())
+
+ def fake_get_all(compute_self, context, search_opts=None,
+ sort_key=None, sort_dir='desc',
+ limit=None, marker=None, want_objects=False):
+ self.assertIsNotNone(search_opts)
+ self.assertIn('ip6', search_opts)
+ self.assertEqual(search_opts['ip6'], 'ffff.*')
+ db_list = [fakes.stub_instance(100, uuid=server_uuid)]
+ return instance_obj._make_instance_list(
+ context, objects.InstanceList(), db_list, FIELDS)
+
+ self.stubs.Set(compute_api.API, 'get_all', fake_get_all)
+
+ req = fakes.HTTPRequest.blank('/fake/servers?ip6=ffff.*',
+ use_admin_context=True)
+ servers = self.controller.index(req)['servers']
+
+ self.assertEqual(len(servers), 1)
+ self.assertEqual(servers[0]['id'], server_uuid)
+
+ def test_get_all_server_details(self):
+ expected_flavor = {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": 'http://localhost/fake/flavors/1',
+ },
+ ],
+ }
+ expected_image = {
+ "id": "10",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": 'http://localhost/fake/images/10',
+ },
+ ],
+ }
+ req = fakes.HTTPRequest.blank('/fake/servers/detail')
+ res_dict = self.controller.detail(req)
+
+ for i, s in enumerate(res_dict['servers']):
+ self.assertEqual(s['id'], fakes.get_fake_uuid(i))
+ self.assertEqual(s['hostId'], '')
+ self.assertEqual(s['name'], 'server%d' % (i + 1))
+ self.assertEqual(s['image'], expected_image)
+ self.assertEqual(s['flavor'], expected_flavor)
+ self.assertEqual(s['status'], 'BUILD')
+ self.assertEqual(s['metadata']['seq'], str(i + 1))
+
+ def test_get_all_server_details_with_host(self):
+ """We want to make sure that if two instances are on the same host,
+ then they return the same hostId. If two instances are on different
+ hosts, they should return different hostId's. In this test, there
+ are 5 instances - 2 on one host and 3 on another.
+ """
+
+ def return_servers_with_host(context, *args, **kwargs):
+ return [fakes.stub_instance(i + 1, 'fake', 'fake', host=i % 2,
+ uuid=fakes.get_fake_uuid(i))
+ for i in xrange(5)]
+
+ self.stubs.Set(db, 'instance_get_all_by_filters',
+ return_servers_with_host)
+
+ req = fakes.HTTPRequest.blank('/fake/servers/detail')
+ res_dict = self.controller.detail(req)
+
+ server_list = res_dict['servers']
+ host_ids = [server_list[0]['hostId'], server_list[1]['hostId']]
+ self.assertTrue(host_ids[0] and host_ids[1])
+ self.assertNotEqual(host_ids[0], host_ids[1])
+
+ for i, s in enumerate(server_list):
+ self.assertEqual(s['id'], fakes.get_fake_uuid(i))
+ self.assertEqual(s['hostId'], host_ids[i % 2])
+ self.assertEqual(s['name'], 'server%d' % (i + 1))
+
+
+class ServersControllerUpdateTest(ControllerTest):
+
+ def _get_request(self, body=None, content_type='json', options=None):
+ if options:
+ self.stubs.Set(db, 'instance_get',
+ fakes.fake_instance_get(**options))
+ req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID)
+ req.method = 'PUT'
+ req.content_type = 'application/%s' % content_type
+ req.body = jsonutils.dumps(body)
+ return req
+
+ def test_update_server_all_attributes(self):
+ body = {'server': {
+ 'name': 'server_test',
+ 'accessIPv4': '0.0.0.0',
+ 'accessIPv6': 'beef::0123',
+ }}
+ req = self._get_request(body, {'name': 'server_test',
+ 'access_ipv4': '0.0.0.0',
+ 'access_ipv6': 'beef::0123'})
+ res_dict = self.controller.update(req, FAKE_UUID, body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['name'], 'server_test')
+ self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0')
+ self.assertEqual(res_dict['server']['accessIPv6'], 'beef::123')
+
+ def test_update_server_invalid_xml_raises_lookup(self):
+ body = """<?xml version="1.0" encoding="TF-8"?>
+ <metadata
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="Label"></meta>"""
+ req = self._get_request(body, content_type='xml')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_update_server_invalid_xml_raises_expat(self):
+ body = """<?xml version="1.0" encoding="UTF-8"?>
+ <metadata
+ xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key="Label"></meta>"""
+ req = self._get_request(body, content_type='xml')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_update_server_name(self):
+ body = {'server': {'name': 'server_test'}}
+ req = self._get_request(body, {'name': 'server_test'})
+ res_dict = self.controller.update(req, FAKE_UUID, body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['name'], 'server_test')
+
+ def test_update_server_name_too_long(self):
+ body = {'server': {'name': 'x' * 256}}
+ req = self._get_request(body, {'name': 'server_test'})
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, FAKE_UUID, body)
+
+ def test_update_server_name_all_blank_spaces(self):
+ body = {'server': {'name': ' ' * 64}}
+ req = self._get_request(body, {'name': 'server_test'})
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, FAKE_UUID, body)
+
+ def test_update_server_access_ipv4(self):
+ body = {'server': {'accessIPv4': '0.0.0.0'}}
+ req = self._get_request(body, {'access_ipv4': '0.0.0.0'})
+ res_dict = self.controller.update(req, FAKE_UUID, body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0')
+
+ def test_update_server_access_ipv4_bad_format(self):
+ body = {'server': {'accessIPv4': 'bad_format'}}
+ req = self._get_request(body, {'access_ipv4': '0.0.0.0'})
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, FAKE_UUID, body)
+
+ def test_update_server_access_ipv4_none(self):
+ body = {'server': {'accessIPv4': None}}
+ req = self._get_request(body, {'access_ipv4': '0.0.0.0'})
+ res_dict = self.controller.update(req, FAKE_UUID, body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['accessIPv4'], '')
+
+ def test_update_server_access_ipv4_blank(self):
+ body = {'server': {'accessIPv4': ''}}
+ req = self._get_request(body, {'access_ipv4': '0.0.0.0'})
+ res_dict = self.controller.update(req, FAKE_UUID, body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['accessIPv4'], '')
+
+ def test_update_server_access_ipv6(self):
+ body = {'server': {'accessIPv6': 'beef::0123'}}
+ req = self._get_request(body, {'access_ipv6': 'beef::0123'})
+ res_dict = self.controller.update(req, FAKE_UUID, body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['accessIPv6'], 'beef::123')
+
+ def test_update_server_access_ipv6_bad_format(self):
+ body = {'server': {'accessIPv6': 'bad_format'}}
+ req = self._get_request(body, {'access_ipv6': 'beef::0123'})
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+ req, FAKE_UUID, body)
+
+ def test_update_server_access_ipv6_none(self):
+ body = {'server': {'accessIPv6': None}}
+ req = self._get_request(body, {'access_ipv6': 'beef::0123'})
+ res_dict = self.controller.update(req, FAKE_UUID, body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['accessIPv6'], '')
+
+ def test_update_server_access_ipv6_blank(self):
+ body = {'server': {'accessIPv6': ''}}
+ req = self._get_request(body, {'access_ipv6': 'beef::0123'})
+ res_dict = self.controller.update(req, FAKE_UUID, body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['accessIPv6'], '')
+
+ def test_update_server_personality(self):
+ body = {
+ 'server': {
+ 'personality': []
+ }
+ }
+ req = self._get_request(body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.update, req, FAKE_UUID, body)
+
+ def test_update_server_adminPass_ignored(self):
+ inst_dict = dict(name='server_test', adminPass='bacon')
+ body = dict(server=inst_dict)
+
+ def server_update(context, id, params):
+ filtered_dict = {
+ 'display_name': 'server_test',
+ }
+ self.assertEqual(params, filtered_dict)
+ filtered_dict['uuid'] = id
+ return filtered_dict
+
+ self.stubs.Set(db, 'instance_update', server_update)
+ # FIXME (comstud)
+ # self.stubs.Set(db, 'instance_get',
+ # return_server_with_attributes(name='server_test'))
+
+ req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID)
+ req.method = 'PUT'
+ req.content_type = "application/json"
+ req.body = jsonutils.dumps(body)
+ res_dict = self.controller.update(req, FAKE_UUID, body)
+
+ self.assertEqual(res_dict['server']['id'], FAKE_UUID)
+ self.assertEqual(res_dict['server']['name'], 'server_test')
+
+ def test_update_server_not_found(self):
+ def fake_get(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(compute_api.API, 'get', fake_get)
+ body = {'server': {'name': 'server_test'}}
+ req = self._get_request(body)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
+ req, FAKE_UUID, body)
+
+ def test_update_server_not_found_on_update(self):
+ def fake_update(*args, **kwargs):
+ raise exception.InstanceNotFound(instance_id='fake')
+
+ self.stubs.Set(db, 'instance_update_and_get_original', fake_update)
+ body = {'server': {'name': 'server_test'}}
+ req = self._get_request(body)
+ self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
+ req, FAKE_UUID, body)
+
+ def test_update_server_policy_fail(self):
+ rule = {'compute:update': common_policy.parse_rule('role:admin')}
+ policy.set_rules(rule)
+ body = {'server': {'name': 'server_test'}}
+ req = self._get_request(body, {'name': 'server_test'})
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller.update, req, FAKE_UUID, body)
+
+
+class ServersControllerDeleteTest(ControllerTest):
+
+ def setUp(self):
+ super(ServersControllerDeleteTest, self).setUp()
+ self.server_delete_called = False
+
+ def instance_destroy_mock(*args, **kwargs):
+ self.server_delete_called = True
+ deleted_at = timeutils.utcnow()
+ return fake_instance.fake_db_instance(deleted_at=deleted_at)
+
+ self.stubs.Set(db, 'instance_destroy', instance_destroy_mock)
+
+ def _create_delete_request(self, uuid):
+ fakes.stub_out_instance_quota(self.stubs, 0, 10)
+ req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % uuid)
+ req.method = 'DELETE'
+ return req
+
+ def _delete_server_instance(self, uuid=FAKE_UUID):
+ req = self._create_delete_request(uuid)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE))
+ self.controller.delete(req, uuid)
+
+ def test_delete_server_instance(self):
+ self._delete_server_instance()
+ self.assertTrue(self.server_delete_called)
+
+ def test_delete_server_instance_not_found(self):
+ self.assertRaises(webob.exc.HTTPNotFound,
+ self._delete_server_instance,
+ uuid='non-existent-uuid')
+
+ def test_delete_locked_server(self):
+ req = self._create_delete_request(FAKE_UUID)
+ self.stubs.Set(compute_api.API, delete_types.SOFT_DELETE,
+ fakes.fake_actions_to_locked_server)
+ self.stubs.Set(compute_api.API, delete_types.DELETE,
+ fakes.fake_actions_to_locked_server)
+
+ self.assertRaises(webob.exc.HTTPConflict, self.controller.delete,
+ req, FAKE_UUID)
+
+ def test_delete_server_instance_while_building(self):
+ fakes.stub_out_instance_quota(self.stubs, 0, 10)
+ request = self._create_delete_request(FAKE_UUID)
+ self.controller.delete(request, FAKE_UUID)
+
+ self.assertTrue(self.server_delete_called)
+
+ def test_delete_server_instance_while_deleting_host_up(self):
+ req = self._create_delete_request(FAKE_UUID)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.DELETING,
+ host='fake_host'))
+ self.stubs.Set(objects.Instance, 'save',
+ lambda *args, **kwargs: None)
+
+ @classmethod
+ def fake_get_by_compute_host(cls, context, host):
+ return {'updated_at': timeutils.utcnow()}
+ self.stubs.Set(objects.Service, 'get_by_compute_host',
+ fake_get_by_compute_host)
+
+ self.controller.delete(req, FAKE_UUID)
+ # Delete request can be ignored, because it's been accepted and
+ # forwarded to the compute service already.
+ self.assertFalse(self.server_delete_called)
+
+ def test_delete_server_instance_while_deleting_host_down(self):
+ fake_network.stub_out_network_cleanup(self.stubs)
+ req = self._create_delete_request(FAKE_UUID)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.DELETING,
+ host='fake_host'))
+ self.stubs.Set(objects.Instance, 'save',
+ lambda *args, **kwargs: None)
+
+ @classmethod
+ def fake_get_by_compute_host(cls, context, host):
+ return {'updated_at': datetime.datetime.min}
+ self.stubs.Set(objects.Service, 'get_by_compute_host',
+ fake_get_by_compute_host)
+
+ self.controller.delete(req, FAKE_UUID)
+ # Delete request would be ignored, because it's been accepted before
+ # but since the host is down, api should remove the instance anyway.
+ self.assertTrue(self.server_delete_called)
+
+ def test_delete_server_instance_while_resize(self):
+ req = self._create_delete_request(FAKE_UUID)
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE,
+ task_state=task_states.RESIZE_PREP))
+
+ self.controller.delete(req, FAKE_UUID)
+ # Delete shoud be allowed in any case, even during resizing,
+ # because it may get stuck.
+ self.assertTrue(self.server_delete_called)
+
+ def test_delete_server_instance_if_not_launched(self):
+ self.flags(reclaim_instance_interval=3600)
+ req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID)
+ req.method = 'DELETE'
+
+ self.server_delete_called = False
+
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(launched_at=None))
+
+ def instance_destroy_mock(*args, **kwargs):
+ self.server_delete_called = True
+ deleted_at = timeutils.utcnow()
+ return fake_instance.fake_db_instance(deleted_at=deleted_at)
+ self.stubs.Set(db, 'instance_destroy', instance_destroy_mock)
+
+ self.controller.delete(req, FAKE_UUID)
+ # delete() should be called for instance which has never been active,
+ # even if reclaim_instance_interval has been set.
+ self.assertEqual(self.server_delete_called, True)
+
+
+class ServersControllerRebuildInstanceTest(ControllerTest):
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ image_href = 'http://localhost/v2/fake/images/%s' % image_uuid
+
+ def setUp(self):
+ super(ServersControllerRebuildInstanceTest, self).setUp()
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_states.ACTIVE))
+ self.body = {
+ 'rebuild': {
+ 'name': 'new_name',
+ 'imageRef': self.image_href,
+ 'metadata': {
+ 'open': 'stack',
+ },
+ 'personality': [
+ {
+ "path": "/etc/banner.txt",
+ "contents": "MQ==",
+ },
+ ],
+ },
+ }
+ self.req = fakes.HTTPRequest.blank('/fake/servers/a/action')
+ self.req.method = 'POST'
+ self.req.headers["content-type"] = "application/json"
+
+ def test_rebuild_instance_with_access_ipv4_bad_format(self):
+ # proper local hrefs must start with 'http://localhost/v2/'
+ self.body['rebuild']['accessIPv4'] = 'bad_format'
+ self.body['rebuild']['accessIPv6'] = 'fead::1234'
+ self.body['rebuild']['metadata']['hello'] = 'world'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, self.body)
+
+ def test_rebuild_instance_with_blank_metadata_key(self):
+ self.body['rebuild']['accessIPv4'] = '0.0.0.0'
+ self.body['rebuild']['accessIPv6'] = 'fead::1234'
+ self.body['rebuild']['metadata'][''] = 'world'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, self.body)
+
+ def test_rebuild_instance_with_metadata_key_too_long(self):
+ self.body['rebuild']['accessIPv4'] = '0.0.0.0'
+ self.body['rebuild']['accessIPv6'] = 'fead::1234'
+ self.body['rebuild']['metadata'][('a' * 260)] = 'world'
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, self.body)
+
+ def test_rebuild_instance_with_metadata_value_too_long(self):
+ self.body['rebuild']['accessIPv4'] = '0.0.0.0'
+ self.body['rebuild']['accessIPv6'] = 'fead::1234'
+ self.body['rebuild']['metadata']['key1'] = ('a' * 260)
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, self.body)
+
+ def test_rebuild_instance_fails_when_min_ram_too_small(self):
+ # make min_ram larger than our instance ram size
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True,
+ status='active', properties={'key1': 'value1'},
+ min_ram="4096", min_disk="10")
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_get_image)
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, self.body)
+
+ def test_rebuild_instance_fails_when_min_disk_too_small(self):
+ # make min_disk larger than our instance disk size
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True,
+ status='active', properties={'key1': 'value1'},
+ min_ram="128", min_disk="100000")
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_get_image)
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild, self.req,
+ FAKE_UUID, self.body)
+
+ def test_rebuild_instance_image_too_large(self):
+ # make image size larger than our instance disk size
+ size = str(1000 * (1024 ** 3))
+
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True,
+ status='active', size=size)
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_get_image)
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild, self.req, FAKE_UUID, self.body)
+
+ def test_rebuild_instance_with_deleted_image(self):
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True,
+ status='DELETED')
+
+ self.stubs.Set(fake._FakeImageService, 'show', fake_get_image)
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild, self.req, FAKE_UUID, self.body)
+
+ def test_rebuild_instance_onset_file_limit_over_quota(self):
+ def fake_get_image(self, context, image_href, **kwargs):
+ return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
+ name='public image', is_public=True, status='active')
+
+ with contextlib.nested(
+ mock.patch.object(fake._FakeImageService, 'show',
+ side_effect=fake_get_image),
+ mock.patch.object(self.controller.compute_api, 'rebuild',
+ side_effect=exception.OnsetFileLimitExceeded)
+ ) as (
+ show_mock, rebuild_mock
+ ):
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, body=self.body)
+
+ def test_rebuild_instance_with_access_ipv6_bad_format(self):
+ # proper local hrefs must start with 'http://localhost/v2/'
+ self.body['rebuild']['accessIPv4'] = '1.2.3.4'
+ self.body['rebuild']['accessIPv6'] = 'bad_format'
+ self.body['rebuild']['metadata']['hello'] = 'world'
+ self.req.body = jsonutils.dumps(self.body)
+ self.req.headers["content-type"] = "application/json"
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild,
+ self.req, FAKE_UUID, self.body)
+
+ def test_rebuild_instance_with_null_image_ref(self):
+ self.body['rebuild']['imageRef'] = None
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller._action_rebuild, self.req, FAKE_UUID,
+ self.body)
+
+
+class ServerStatusTest(test.TestCase):
+
+ def setUp(self):
+ super(ServerStatusTest, self).setUp()
+ fakes.stub_out_nw_api(self.stubs)
+
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = servers.Controller(self.ext_mgr)
+
+ def _fake_get_server(context, req, id):
+ return fakes.stub_instance(id)
+
+ self.stubs.Set(self.controller, '_get_server', _fake_get_server)
+
+ def _get_with_state(self, vm_state, task_state=None):
+ self.stubs.Set(db, 'instance_get_by_uuid',
+ fakes.fake_instance_get(vm_state=vm_state,
+ task_state=task_state))
+
+ request = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID)
+ return self.controller.show(request, FAKE_UUID)
+
+ def _req_with_policy_fail(self, policy_rule_name):
+ rule = {'compute:%s' % policy_rule_name:
+ common_policy.parse_rule('role:admin')}
+ policy.set_rules(rule)
+ return fakes.HTTPRequest.blank('/fake/servers/1234/action')
+
+ def test_active(self):
+ response = self._get_with_state(vm_states.ACTIVE)
+ self.assertEqual(response['server']['status'], 'ACTIVE')
+
+ def test_reboot(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.REBOOTING)
+ self.assertEqual(response['server']['status'], 'REBOOT')
+
+ def test_reboot_hard(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.REBOOTING_HARD)
+ self.assertEqual(response['server']['status'], 'HARD_REBOOT')
+
+ def test_reboot_resize_policy_fail(self):
+ req = self._req_with_policy_fail('reboot')
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._action_reboot, req, '1234',
+ {'reboot': {'type': 'HARD'}})
+
+ def test_rebuild(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.REBUILDING)
+ self.assertEqual(response['server']['status'], 'REBUILD')
+
+ def test_rebuild_error(self):
+ response = self._get_with_state(vm_states.ERROR)
+ self.assertEqual(response['server']['status'], 'ERROR')
+
+ def test_resize(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.RESIZE_PREP)
+ self.assertEqual(response['server']['status'], 'RESIZE')
+
+ def test_confirm_resize_policy_fail(self):
+ req = self._req_with_policy_fail('confirm_resize')
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._action_confirm_resize, req, '1234', {})
+
+ def test_verify_resize(self):
+ response = self._get_with_state(vm_states.RESIZED, None)
+ self.assertEqual(response['server']['status'], 'VERIFY_RESIZE')
+
+ def test_revert_resize(self):
+ response = self._get_with_state(vm_states.RESIZED,
+ task_states.RESIZE_REVERTING)
+ self.assertEqual(response['server']['status'], 'REVERT_RESIZE')
+
+ def test_revert_resize_policy_fail(self):
+ req = self._req_with_policy_fail('revert_resize')
+ self.assertRaises(exception.PolicyNotAuthorized,
+ self.controller._action_revert_resize, req, '1234', {})
+
+ def test_password_update(self):
+ response = self._get_with_state(vm_states.ACTIVE,
+ task_states.UPDATING_PASSWORD)
+ self.assertEqual(response['server']['status'], 'PASSWORD')
+
+ def test_stopped(self):
+ response = self._get_with_state(vm_states.STOPPED)
+ self.assertEqual(response['server']['status'], 'SHUTOFF')
+
+
+class ServersControllerCreateTest(test.TestCase):
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ def setUp(self):
+ """Shared implementation for tests below that create instance."""
+ super(ServersControllerCreateTest, self).setUp()
+
+ self.flags(verbose=True,
+ enable_instance_password=True)
+ self.instance_cache_num = 0
+ self.instance_cache_by_id = {}
+ self.instance_cache_by_uuid = {}
+
+ fakes.stub_out_nw_api(self.stubs)
+
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = servers.Controller(self.ext_mgr)
+
+ self.volume_id = 'fake'
+
+ def instance_create(context, inst):
+ inst_type = flavors.get_flavor_by_flavor_id(3)
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ def_image_ref = 'http://localhost/images/%s' % image_uuid
+ self.instance_cache_num += 1
+ instance = fake_instance.fake_db_instance(**{
+ 'id': self.instance_cache_num,
+ 'display_name': inst['display_name'] or 'test',
+ 'uuid': FAKE_UUID,
+ 'instance_type': inst_type,
+ 'access_ip_v4': '1.2.3.4',
+ 'access_ip_v6': 'fead::1234',
+ 'image_ref': inst.get('image_ref', def_image_ref),
+ 'user_id': 'fake',
+ 'project_id': 'fake',
+ 'reservation_id': inst['reservation_id'],
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "config_drive": None,
+ "progress": 0,
+ "fixed_ips": [],
+ "task_state": "",
+ "vm_state": "",
+ "root_device_name": inst.get('root_device_name', 'vda'),
+ "security_groups": inst['security_groups'],
+ })
+
+ self.instance_cache_by_id[instance['id']] = instance
+ self.instance_cache_by_uuid[instance['uuid']] = instance
+ return instance
+
+ def instance_get(context, instance_id):
+ """Stub for compute/api create() pulling in instance after
+ scheduling
+ """
+ return self.instance_cache_by_id[instance_id]
+
+ def instance_update(context, uuid, values):
+ instance = self.instance_cache_by_uuid[uuid]
+ instance.update(values)
+ return instance
+
+ def server_update(context, instance_uuid, params, update_cells=False):
+ inst = self.instance_cache_by_uuid[instance_uuid]
+ inst.update(params)
+ return inst
+
+ def server_update_and_get_original(
+ context, instance_uuid, params, update_cells=False,
+ columns_to_join=None):
+ inst = self.instance_cache_by_uuid[instance_uuid]
+ inst.update(params)
+ return (inst, inst)
+
+ def fake_method(*args, **kwargs):
+ pass
+
+ def project_get_networks(context, user_id):
+ return dict(id='1', host='localhost')
+
+ def queue_get_for(context, *args):
+ return 'network_topic'
+
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_key_pair_funcs(self.stubs)
+ fake.stub_out_image_service(self.stubs)
+ self.stubs.Set(uuid, 'uuid4', fake_gen_uuid)
+ self.stubs.Set(db, 'instance_add_security_group',
+ return_security_group)
+ self.stubs.Set(db, 'project_get_networks',
+ project_get_networks)
+ self.stubs.Set(db, 'instance_create', instance_create)
+ self.stubs.Set(db, 'instance_system_metadata_update',
+ fake_method)
+ self.stubs.Set(db, 'instance_get', instance_get)
+ self.stubs.Set(db, 'instance_update', instance_update)
+ self.stubs.Set(db, 'instance_update_and_get_original',
+ server_update_and_get_original)
+ self.stubs.Set(manager.VlanManager, 'allocate_fixed_ip',
+ fake_method)
+ self.body = {
+ 'server': {
+ 'min_count': 2,
+ 'name': 'server_test',
+ 'imageRef': self.image_uuid,
+ 'flavorRef': self.flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ 'personality': [
+ {
+ "path": "/etc/banner.txt",
+ "contents": "MQ==",
+ },
+ ],
+ },
+ }
+ self.bdm = [{'delete_on_termination': 1,
+ 'device_name': 123,
+ 'volume_size': 1,
+ 'volume_id': '11111111-1111-1111-1111-111111111111'}]
+
+ self.req = fakes.HTTPRequest.blank('/fake/servers')
+ self.req.method = 'POST'
+ self.req.headers["content-type"] = "application/json"
+
+ def _check_admin_pass_len(self, server_dict):
+ """utility function - check server_dict for adminPass length."""
+ self.assertEqual(CONF.password_length,
+ len(server_dict["adminPass"]))
+
+ def _check_admin_pass_missing(self, server_dict):
+ """utility function - check server_dict for absence of adminPass."""
+ self.assertNotIn("adminPass", server_dict)
+
+ def _test_create_instance(self, flavor=2):
+ image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
+ self.body['server']['imageRef'] = image_uuid
+ self.body['server']['flavorRef'] = flavor
+ self.req.body = jsonutils.dumps(self.body)
+ server = self.controller.create(self.req, self.body).obj['server']
+ self._check_admin_pass_len(server)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_private_flavor(self):
+ values = {
+ 'name': 'fake_name',
+ 'memory_mb': 512,
+ 'vcpus': 1,
+ 'root_gb': 10,
+ 'ephemeral_gb': 10,
+ 'flavorid': '1324',
+ 'swap': 0,
+ 'rxtx_factor': 0.5,
+ 'vcpu_weight': 1,
+ 'disabled': False,
+ 'is_public': False,
+ }
+ db.flavor_create(context.get_admin_context(), values)
+ self.assertRaises(webob.exc.HTTPBadRequest, self._test_create_instance,
+ flavor=1324)
+
+ def test_create_server_bad_image_href(self):
+ image_href = 1
+ self.body['server']['imageRef'] = image_href,
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ self.req, self.body)
+
+ def test_create_server_with_invalid_networks_parameter(self):
+ self.ext_mgr.extensions = {'os-networks': 'fake'}
+ self.body['server']['networks'] = {
+ 'uuid': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'}
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ self.req,
+ self.body)
+
+ def test_create_server_with_deleted_image(self):
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ # Get the fake image service so we can set the status to deleted
+ (image_service, image_id) = glance.get_remote_image_service(
+ context, '')
+ image_service.update(context, image_uuid, {'status': 'DELETED'})
+ self.addCleanup(image_service.update, context, image_uuid,
+ {'status': 'active'})
+
+ self.body['server']['flavorRef'] = 2
+ self.req.body = jsonutils.dumps(self.body)
+ with testtools.ExpectedException(
+ webob.exc.HTTPBadRequest,
+ 'Image 76fa36fc-c930-4bf3-8c8a-ea2a2420deb6 is not active.'):
+ self.controller.create(self.req, self.body)
+
+ def test_create_server_image_too_large(self):
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ # Get the fake image service so we can set the status to deleted
+ (image_service, image_id) = glance.get_remote_image_service(context,
+ image_uuid)
+ image = image_service.show(context, image_id)
+ orig_size = image['size']
+ new_size = str(1000 * (1024 ** 3))
+ image_service.update(context, image_uuid, {'size': new_size})
+
+ self.addCleanup(image_service.update, context, image_uuid,
+ {'size': orig_size})
+
+ self.body['server']['flavorRef'] = 2
+ self.req.body = jsonutils.dumps(self.body)
+ with testtools.ExpectedException(
+ webob.exc.HTTPBadRequest,
+ "Flavor's disk is too small for requested image."):
+ self.controller.create(self.req, self.body)
+
+ def test_create_instance_invalid_negative_min(self):
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ self.body['server']['min_count'] = -1
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ self.req,
+ self.body)
+
+ def test_create_instance_invalid_negative_max(self):
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ self.body['server']['max_count'] = -1
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ self.req,
+ self.body)
+
+ def test_create_instance_invalid_alpha_min(self):
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ self.body['server']['min_count'] = 'abcd',
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ self.req,
+ self.body)
+
+ def test_create_instance_invalid_alpha_max(self):
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ self.body['server']['max_count'] = 'abcd',
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ self.req,
+ self.body)
+
+ def test_create_multiple_instances(self):
+ """Test creating multiple instances but not asking for
+ reservation_id
+ """
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+ self.assertEqual(FAKE_UUID, res["server"]["id"])
+ self._check_admin_pass_len(res["server"])
+
+ def test_create_multiple_instances_pass_disabled(self):
+ """Test creating multiple instances but not asking for
+ reservation_id
+ """
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ self.flags(enable_instance_password=False)
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+ self.assertEqual(FAKE_UUID, res["server"]["id"])
+ self._check_admin_pass_missing(res["server"])
+
+ def test_create_multiple_instances_resv_id_return(self):
+ """Test creating multiple instances with asking for
+ reservation_id
+ """
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ self.body['server']['return_reservation_id'] = True
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body)
+ reservation_id = res.obj.get('reservation_id')
+ self.assertNotEqual(reservation_id, "")
+ self.assertIsNotNone(reservation_id)
+ self.assertTrue(len(reservation_id) > 1)
+
+ def test_create_multiple_instances_with_multiple_volume_bdm(self):
+ """Test that a BadRequest is raised if multiple instances
+ are requested with a list of block device mappings for volumes.
+ """
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ min_count = 2
+ bdm = [{'device_name': 'foo1', 'volume_id': 'vol-xxxx'},
+ {'device_name': 'foo2', 'volume_id': 'vol-yyyy'}
+ ]
+ params = {
+ 'block_device_mapping': bdm,
+ 'min_count': min_count
+ }
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['min_count'], 2)
+ self.assertEqual(len(kwargs['block_device_mapping']), 2)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params, no_image=True)
+
+ def test_create_multiple_instances_with_single_volume_bdm(self):
+ """Test that a BadRequest is raised if multiple instances
+ are requested to boot from a single volume.
+ """
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ min_count = 2
+ bdm = [{'device_name': 'foo1', 'volume_id': 'vol-xxxx'}]
+ params = {
+ 'block_device_mapping': bdm,
+ 'min_count': min_count
+ }
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['min_count'], 2)
+ self.assertEqual(kwargs['block_device_mapping']['volume_id'],
+ 'vol-xxxx')
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params, no_image=True)
+
+ def test_create_multiple_instance_with_non_integer_max_count(self):
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ self.body['server']['max_count'] = 2.5
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_multiple_instance_with_non_integer_min_count(self):
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ self.body['server']['min_count'] = 2.5
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_image_ref_is_bookmark(self):
+ image_href = 'http://localhost/fake/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+ server = res['server']
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_image_ref_is_invalid(self):
+ image_uuid = 'this_is_not_a_valid_uuid'
+ image_href = 'http://localhost/fake/images/%s' % image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ self.req, self.body)
+
+ def test_create_instance_no_key_pair(self):
+ fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False)
+ self._test_create_instance()
+
+ def _test_create_extra(self, params, no_image=False):
+ self.body['server']['flavorRef'] = 2
+ if no_image:
+ self.body['server'].pop('imageRef', None)
+ self.body['server'].update(params)
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertIn('server',
+ self.controller.create(self.req, self.body).obj)
+
+ def test_create_instance_with_security_group_enabled(self):
+ self.ext_mgr.extensions = {'os-security-groups': 'fake'}
+ group = 'foo'
+ old_create = compute_api.API.create
+
+ def sec_group_get(ctx, proj, name):
+ if name == group:
+ return True
+ else:
+ raise exception.SecurityGroupNotFoundForProject(
+ project_id=proj, security_group_id=name)
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['security_group'], [group])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(db, 'security_group_get_by_name', sec_group_get)
+ # negative test
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra,
+ {'security_groups': [{'name': 'bogus'}]})
+ # positive test - extra assert in create path
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra({'security_groups': [{'name': group}]})
+
+ def test_create_instance_with_non_unique_secgroup_name(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network}]
+ params = {'networks': requested_networks,
+ 'security_groups': [{'name': 'dup'}, {'name': 'dup'}]}
+
+ def fake_create(*args, **kwargs):
+ raise exception.NoUniqueMatch("No Unique match found for ...")
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self._test_create_extra, params)
+
+ def test_create_instance_with_port_with_no_fixed_ips(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ port_id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'port': port_id}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.PortRequiresFixedIP(port_id=port_id)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_instance_raise_user_data_too_large(self, mock_create):
+ mock_create.side_effect = exception.InstanceUserDataTooLarge(
+ maxsize=1, length=2)
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ self.req, self.body)
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_instance_raise_auto_disk_config_exc(self, mock_create):
+ mock_create.side_effect = exception.AutoDiskConfigDisabledByImage(
+ image='dummy')
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create,
+ self.req, self.body)
+
+ @mock.patch.object(compute_api.API, 'create',
+ side_effect=exception.InstanceExists(
+ name='instance-name'))
+ def test_create_instance_raise_instance_exists(self, mock_create):
+ self.assertRaises(webob.exc.HTTPConflict,
+ self.controller.create,
+ self.req, self.body)
+
+ def test_create_instance_with_network_with_no_subnet(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.NetworkRequiresSubnet(network_uuid=network)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ def test_create_instance_with_access_ip(self):
+ self.body['server']['accessIPv4'] = '1.2.3.4'
+ self.body['server']['accessIPv6'] = 'fead::1234'
+
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+ server = res['server']
+ self._check_admin_pass_len(server)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_with_access_ip_pass_disabled(self):
+ # test with admin passwords disabled See lp bug 921814
+ self.flags(enable_instance_password=False)
+ self.body['server']['accessIPv4'] = '1.2.3.4'
+ self.body['server']['accessIPv6'] = 'fead::1234'
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+
+ server = res['server']
+ self._check_admin_pass_missing(server)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_bad_format_access_ip_v4(self):
+ self.body['server']['accessIPv4'] = 'bad_format'
+ self.body['server']['accessIPv6'] = 'fead::1234'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ self.req, self.body)
+
+ def test_create_instance_bad_format_access_ip_v6(self):
+ self.body['server']['accessIPv4'] = '1.2.3.4'
+ self.body['server']['accessIPv6'] = 'bad_format'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ self.req, self.body)
+
+ def test_create_instance_name_all_blank_spaces(self):
+ self.body['server']['name'] = ' ' * 64
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_name_too_long(self):
+ self.body['server']['name'] = 'X' * 256
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
+ self.req, self.body)
+
+ def test_create_instance(self):
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+
+ server = res['server']
+ self._check_admin_pass_len(server)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_pass_disabled(self):
+ self.flags(enable_instance_password=False)
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+
+ server = res['server']
+ self._check_admin_pass_missing(server)
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ @mock.patch('nova.virt.hardware.VirtNUMAInstanceTopology.get_constraints')
+ def test_create_instance_numa_topology_wrong(self, numa_constraints_mock):
+ numa_constraints_mock.side_effect = (
+ exception.ImageNUMATopologyIncomplete)
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_too_much_metadata(self):
+ self.flags(quota_metadata_items=1)
+ self.body['server']['metadata']['vote'] = 'fiddletown'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPForbidden,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_metadata_key_too_long(self):
+ self.flags(quota_metadata_items=1)
+ self.body['server']['metadata'] = {('a' * 260): '12345'}
+
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_metadata_value_too_long(self):
+ self.flags(quota_metadata_items=1)
+ self.body['server']['metadata'] = {'key1': ('a' * 260)}
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_metadata_key_blank(self):
+ self.flags(quota_metadata_items=1)
+ self.body['server']['metadata'] = {'': 'abcd'}
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_metadata_not_dict(self):
+ self.flags(quota_metadata_items=1)
+ self.body['server']['metadata'] = 'string'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_metadata_key_not_string(self):
+ self.flags(quota_metadata_items=1)
+ self.body['server']['metadata'] = {1: 'test'}
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_metadata_value_not_string(self):
+ self.flags(quota_metadata_items=1)
+ self.body['server']['metadata'] = {'test': ['a', 'list']}
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_user_data_malformed_bad_request(self):
+ self.ext_mgr.extensions = {'os-user-data': 'fake'}
+ params = {'user_data': 'u1234!'}
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ @mock.patch('nova.compute.api.API.create',
+ side_effect=exception.KeypairNotFound(name='nonexistentkey',
+ user_id=1))
+ def test_create_instance_invalid_key_name(self, mock_create):
+ self.body['server']['key_name'] = 'nonexistentkey'
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_valid_key_name(self):
+ self.body['server']['key_name'] = 'key'
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+
+ self.assertEqual(FAKE_UUID, res["server"]["id"])
+ self._check_admin_pass_len(res["server"])
+
+ def test_create_instance_invalid_flavor_href(self):
+ flavor_ref = 'http://localhost/v2/flavors/asdf'
+ self.body['server']['flavorRef'] = flavor_ref
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_invalid_flavor_id_int(self):
+ flavor_ref = -1
+ self.body['server']['flavorRef'] = flavor_ref
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_bad_flavor_href(self):
+ flavor_ref = 'http://localhost/v2/flavors/17'
+ self.body['server']['flavorRef'] = flavor_ref
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_with_config_drive(self):
+ self.ext_mgr.extensions = {'os-config-drive': 'fake'}
+ self.body['server']['config_drive'] = "true"
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+ server = res['server']
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_with_bad_config_drive(self):
+ self.ext_mgr.extensions = {'os-config-drive': 'fake'}
+ self.body['server']['config_drive'] = 'adcd'
+ self.req.body = jsonutils.dumps(self.body)
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_without_config_drive(self):
+ self.ext_mgr.extensions = {'os-config-drive': 'fake'}
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+ server = res['server']
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_with_config_drive_disabled(self):
+ config_drive = [{'config_drive': 'foo'}]
+ params = {'config_drive': config_drive}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIsNone(kwargs['config_drive'])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_bad_href(self):
+ image_href = 'asdf'
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_instance_local_href(self):
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+
+ server = res['server']
+ self.assertEqual(FAKE_UUID, server['id'])
+
+ def test_create_instance_admin_pass(self):
+ self.body['server']['flavorRef'] = 3,
+ self.body['server']['adminPass'] = 'testpass'
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+
+ server = res['server']
+ self.assertEqual(server['adminPass'], self.body['server']['adminPass'])
+
+ def test_create_instance_admin_pass_pass_disabled(self):
+ self.flags(enable_instance_password=False)
+ self.body['server']['flavorRef'] = 3,
+ self.body['server']['adminPass'] = 'testpass'
+ self.req.body = jsonutils.dumps(self.body)
+ res = self.controller.create(self.req, self.body).obj
+
+ server = res['server']
+ self.assertIn('adminPass', self.body['server'])
+ self.assertNotIn('adminPass', server)
+
+ def test_create_instance_admin_pass_empty(self):
+ self.body['server']['flavorRef'] = 3,
+ self.body['server']['adminPass'] = ''
+ self.req.body = jsonutils.dumps(self.body)
+
+ # The fact that the action doesn't raise is enough validation
+ self.controller.create(self.req, self.body)
+
+ def test_create_instance_with_security_group_disabled(self):
+ group = 'foo'
+ params = {'security_groups': [{'name': group}]}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ # NOTE(vish): if the security groups extension is not
+ # enabled, then security groups passed in
+ # are ignored.
+ self.assertEqual(kwargs['security_group'], ['default'])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_disk_config_enabled(self):
+ self.ext_mgr.extensions = {'OS-DCF': 'fake'}
+ # NOTE(vish): the extension converts OS-DCF:disk_config into
+ # auto_disk_config, so we are testing with
+ # the_internal_value
+ params = {'auto_disk_config': 'AUTO'}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['auto_disk_config'], 'AUTO')
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_disk_config_disabled(self):
+ params = {'auto_disk_config': True}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['auto_disk_config'], False)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_scheduler_hints_enabled(self):
+ self.ext_mgr.extensions = {'OS-SCH-HNT': 'fake'}
+ hints = {'a': 'b'}
+ params = {'scheduler_hints': hints}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['scheduler_hints'], hints)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_scheduler_hints_disabled(self):
+ hints = {'a': 'b'}
+ params = {'scheduler_hints': hints}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['scheduler_hints'], {})
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_volumes_enabled_no_image(self):
+ """Test that the create will fail if there is no image
+ and no bdms supplied in the request
+ """
+ self.ext_mgr.extensions = {'os-volumes': 'fake'}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertNotIn('imageRef', kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, {}, no_image=True)
+
+ def test_create_instance_with_bdm_v2_enabled_no_image(self):
+ self.ext_mgr.extensions = {'os-block-device-mapping-v2-boot': 'fake'}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertNotIn('imageRef', kwargs)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, {}, no_image=True)
+
+ def test_create_instance_with_user_data_enabled(self):
+ self.ext_mgr.extensions = {'os-user-data': 'fake'}
+ user_data = 'fake'
+ params = {'user_data': user_data}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['user_data'], user_data)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_user_data_disabled(self):
+ user_data = 'fake'
+ params = {'user_data': user_data}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIsNone(kwargs['user_data'])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_keypairs_enabled(self):
+ self.ext_mgr.extensions = {'os-keypairs': 'fake'}
+ key_name = 'green'
+
+ params = {'key_name': key_name}
+ old_create = compute_api.API.create
+
+ # NOTE(sdague): key pair goes back to the database,
+ # so we need to stub it out for tests
+ def key_pair_get(context, user_id, name):
+ return dict(test_keypair.fake_keypair,
+ public_key='FAKE_KEY',
+ fingerprint='FAKE_FINGERPRINT',
+ name=name)
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['key_name'], key_name)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(db, 'key_pair_get', key_pair_get)
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_keypairs_disabled(self):
+ key_name = 'green'
+
+ params = {'key_name': key_name}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIsNone(kwargs['key_name'])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_availability_zone_enabled(self):
+ self.ext_mgr.extensions = {'os-availability-zone': 'fake'}
+ availability_zone = 'fake'
+ params = {'availability_zone': availability_zone}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['availability_zone'], availability_zone)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+
+ try:
+ self._test_create_extra(params)
+ except webob.exc.HTTPBadRequest as e:
+ expected = 'The requested availability zone is not available'
+ self.assertEqual(e.explanation, expected)
+ admin_context = context.get_admin_context()
+ db.service_create(admin_context, {'host': 'host1_zones',
+ 'binary': "nova-compute",
+ 'topic': 'compute',
+ 'report_count': 0})
+ agg = db.aggregate_create(admin_context,
+ {'name': 'agg1'}, {'availability_zone': availability_zone})
+ db.aggregate_host_add(admin_context, agg['id'], 'host1_zones')
+ self._test_create_extra(params)
+
+ def test_create_instance_with_availability_zone_disabled(self):
+ availability_zone = 'fake'
+ params = {'availability_zone': availability_zone}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIsNone(kwargs['availability_zone'])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_multiple_create_enabled(self):
+ self.ext_mgr.extensions = {'os-multiple-create': 'fake'}
+ min_count = 2
+ max_count = 3
+ params = {
+ 'min_count': min_count,
+ 'max_count': max_count,
+ }
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['min_count'], 2)
+ self.assertEqual(kwargs['max_count'], 3)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_multiple_create_disabled(self):
+ min_count = 2
+ max_count = 3
+ params = {
+ 'min_count': min_count,
+ 'max_count': max_count,
+ }
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertEqual(kwargs['min_count'], 1)
+ self.assertEqual(kwargs['max_count'], 1)
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_networks_enabled(self):
+ self.ext_mgr.extensions = {'os-networks': 'fake'}
+ net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ requested_networks = [{'uuid': net_uuid}]
+ params = {'networks': requested_networks}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ result = [('76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', None)]
+ self.assertEqual(result, kwargs['requested_networks'].as_tuples())
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_neutronv2_port_in_use(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network, 'port': port}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.PortInUse(port_id=port)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPConflict,
+ self._test_create_extra, params)
+
+ def test_create_instance_with_neturonv2_not_found_network(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ requested_networks = [{'uuid': network}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.NetworkNotFound(network_id=network)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ def test_create_instance_with_neutronv2_port_not_found(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ requested_networks = [{'uuid': network, 'port': port}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ raise exception.PortNotFound(port_id=port)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_multiple_instance_with_specified_ip_neutronv2(self,
+ _api_mock):
+ _api_mock.side_effect = exception.InvalidFixedIpAndMaxCountRequest(
+ reason="")
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ address = '10.0.0.1'
+ self.body['server']['max_count'] = 2
+ requested_networks = [{'uuid': network, 'fixed_ip': address,
+ 'port': port}]
+ params = {'networks': requested_networks}
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ def test_create_multiple_instance_with_neutronv2_port(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
+ self.body['server']['max_count'] = 2
+ requested_networks = [{'uuid': network, 'port': port}]
+ params = {'networks': requested_networks}
+
+ def fake_create(*args, **kwargs):
+ msg = _("Unable to launch multiple instances with"
+ " a single configured port ID. Please launch your"
+ " instance one by one with different ports.")
+ raise exception.MultiplePortsNotApplicable(reason=msg)
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+
+ def test_create_instance_with_networks_disabled_neutronv2(self):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ requested_networks = [{'uuid': net_uuid}]
+ params = {'networks': requested_networks}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ result = [('76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', None,
+ None, None)]
+ self.assertEqual(result, kwargs['requested_networks'].as_tuples())
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_with_networks_disabled(self):
+ self.ext_mgr.extensions = {}
+ net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ requested_networks = [{'uuid': net_uuid}]
+ params = {'networks': requested_networks}
+ old_create = compute_api.API.create
+
+ def create(*args, **kwargs):
+ self.assertIsNone(kwargs['requested_networks'])
+ return old_create(*args, **kwargs)
+
+ self.stubs.Set(compute_api.API, 'create', create)
+ self._test_create_extra(params)
+
+ def test_create_instance_invalid_personality(self):
+
+ def fake_create(*args, **kwargs):
+ codec = 'utf8'
+ content = 'b25zLiINCg0KLVJpY2hhcmQgQ$$%QQmFjaA=='
+ start_position = 19
+ end_position = 20
+ msg = 'invalid start byte'
+ raise UnicodeDecodeError(codec, content, start_position,
+ end_position, msg)
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+ self.body['server']['personality'] = [
+ {
+ "path": "/etc/banner.txt",
+ "contents": "b25zLiINCg0KLVJpY2hhcmQgQ$$%QQmFjaA==",
+ },
+ ]
+ self.req.body = jsonutils.dumps(self.body)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self.controller.create, self.req, self.body)
+
+ def test_create_location(self):
+ selfhref = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID
+ image_href = 'http://localhost/v2/images/%s' % self.image_uuid
+ self.body['server']['imageRef'] = image_href
+ self.req.body = jsonutils.dumps(self.body)
+ robj = self.controller.create(self.req, self.body)
+ self.assertEqual(robj['Location'], selfhref)
+
+ def _do_test_create_instance_above_quota(self, resource, allowed, quota,
+ expected_msg):
+ fakes.stub_out_instance_quota(self.stubs, allowed, quota, resource)
+ self.body['server']['flavorRef'] = 3
+ self.req.body = jsonutils.dumps(self.body)
+ try:
+ self.controller.create(self.req, self.body).obj['server']
+ self.fail('expected quota to be exceeded')
+ except webob.exc.HTTPForbidden as e:
+ self.assertEqual(e.explanation, expected_msg)
+
+ def test_create_instance_above_quota_instances(self):
+ msg = _('Quota exceeded for instances: Requested 1, but'
+ ' already used 10 of 10 instances')
+ self._do_test_create_instance_above_quota('instances', 0, 10, msg)
+
+ def test_create_instance_above_quota_ram(self):
+ msg = _('Quota exceeded for ram: Requested 4096, but'
+ ' already used 8192 of 10240 ram')
+ self._do_test_create_instance_above_quota('ram', 2048, 10 * 1024, msg)
+
+ def test_create_instance_above_quota_cores(self):
+ msg = _('Quota exceeded for cores: Requested 2, but'
+ ' already used 9 of 10 cores')
+ self._do_test_create_instance_above_quota('cores', 1, 10, msg)
+
+ def test_create_instance_above_quota_group_members(self):
+ ctxt = context.get_admin_context()
+ fake_group = objects.InstanceGroup(ctxt)
+ fake_group.create()
+
+ def fake_count(context, name, group, user_id):
+ self.assertEqual(name, "server_group_members")
+ self.assertEqual(group.uuid, fake_group.uuid)
+ self.assertEqual(user_id,
+ self.req.environ['nova.context'].user_id)
+ return 10
+
+ def fake_limit_check(context, **kwargs):
+ if 'server_group_members' in kwargs:
+ raise exception.OverQuota(overs={})
+
+ def fake_instance_destroy(context, uuid, constraint):
+ return fakes.stub_instance(1)
+
+ self.stubs.Set(fakes.QUOTAS, 'count', fake_count)
+ self.stubs.Set(fakes.QUOTAS, 'limit_check', fake_limit_check)
+ self.stubs.Set(db, 'instance_destroy', fake_instance_destroy)
+ self.ext_mgr.extensions = {'OS-SCH-HNT': 'fake',
+ 'os-server-group-quotas': 'fake'}
+ self.body['server']['scheduler_hints'] = {'group': fake_group.uuid}
+ self.req.body = jsonutils.dumps(self.body)
+
+ expected_msg = "Quota exceeded, too many servers in group"
+
+ try:
+ self.controller.create(self.req, self.body).obj['server']
+ self.fail('expected quota to be exceeded')
+ except webob.exc.HTTPForbidden as e:
+ self.assertEqual(e.explanation, expected_msg)
+
+ def test_create_instance_above_quota_server_groups(self):
+
+ def fake_reserve(contex, **deltas):
+ if 'server_groups' in deltas:
+ raise exception.OverQuota(overs={})
+
+ def fake_instance_destroy(context, uuid, constraint):
+ return fakes.stub_instance(1)
+
+ self.stubs.Set(fakes.QUOTAS, 'reserve', fake_reserve)
+ self.stubs.Set(db, 'instance_destroy', fake_instance_destroy)
+ self.ext_mgr.extensions = {'OS-SCH-HNT': 'fake',
+ 'os-server-group-quotas': 'fake'}
+ self.body['server']['scheduler_hints'] = {'group': 'fake-group'}
+ self.req.body = jsonutils.dumps(self.body)
+
+ expected_msg = "Quota exceeded, too many server groups."
+
+ try:
+ self.controller.create(self.req, self.body).obj['server']
+ self.fail('expected quota to be exceeded')
+ except webob.exc.HTTPForbidden as e:
+ self.assertEqual(e.explanation, expected_msg)
+
+
+class ServersControllerCreateTestWithMock(test.TestCase):
+ image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
+ flavor_ref = 'http://localhost/123/flavors/3'
+
+ def setUp(self):
+ """Shared implementation for tests below that create instance."""
+ super(ServersControllerCreateTestWithMock, self).setUp()
+
+ self.flags(verbose=True,
+ enable_instance_password=True)
+ self.instance_cache_num = 0
+ self.instance_cache_by_id = {}
+ self.instance_cache_by_uuid = {}
+
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = servers.Controller(self.ext_mgr)
+
+ self.volume_id = 'fake'
+
+ self.body = {
+ 'server': {
+ 'min_count': 2,
+ 'name': 'server_test',
+ 'imageRef': self.image_uuid,
+ 'flavorRef': self.flavor_ref,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ 'personality': [
+ {
+ "path": "/etc/banner.txt",
+ "contents": "MQ==",
+ },
+ ],
+ },
+ }
+
+ self.req = fakes.HTTPRequest.blank('/fake/servers')
+ self.req.method = 'POST'
+ self.req.headers["content-type"] = "application/json"
+
+ def _test_create_extra(self, params, no_image=False):
+ self.body['server']['flavorRef'] = 2
+ if no_image:
+ self.body['server'].pop('imageRef', None)
+ self.body['server'].update(params)
+ self.req.body = jsonutils.dumps(self.body)
+ self.controller.create(self.req, self.body).obj['server']
+
+ @mock.patch.object(compute_api.API, 'create')
+ def test_create_instance_with_neutronv2_fixed_ip_already_in_use(self,
+ create_mock):
+ self.flags(network_api_class='nova.network.neutronv2.api.API')
+ network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+ address = '10.0.2.3'
+ requested_networks = [{'uuid': network, 'fixed_ip': address}]
+ params = {'networks': requested_networks}
+ create_mock.side_effect = exception.FixedIpAlreadyInUse(
+ address=address,
+ instance_uuid=network)
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, params)
+ self.assertEqual(1, len(create_mock.call_args_list))
+
+ @mock.patch.object(compute_api.API, 'create',
+ side_effect=exception.InvalidVolume(reason='error'))
+ def test_create_instance_with_invalid_volume_error(self, create_mock):
+ # Tests that InvalidVolume is translated to a 400 error.
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ self._test_create_extra, {})
+
+
+class TestServerCreateRequestXMLDeserializer(test.TestCase):
+
+ def setUp(self):
+ super(TestServerCreateRequestXMLDeserializer, self).setUp()
+ self.deserializer = servers.CreateDeserializer()
+
+ def test_minimal_request(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2"/>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_alternate_namespace_prefix(self):
+ serial_request = """
+<ns2:server xmlns:ns2="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2">
+ <ns2:metadata><ns2:meta key="hello">world</ns2:meta></ns2:metadata>
+ </ns2:server>
+ """
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ 'metadata': {"hello": "world"},
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_scheduler_hints_and_alternate_namespace_prefix(self):
+ serial_request = """
+<ns2:server xmlns:ns2="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2">
+ <ns2:metadata><ns2:meta key="hello">world</ns2:meta></ns2:metadata>
+ <os:scheduler_hints
+ xmlns:os="http://docs.openstack.org/compute/ext/scheduler-hints/api/v2">
+ <hypervisor>xen</hypervisor>
+ <near>eb999657-dd6b-464e-8713-95c532ac3b18</near>
+ </os:scheduler_hints>
+ </ns2:server>
+ """
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ 'OS-SCH-HNT:scheduler_hints': {
+ 'hypervisor': ['xen'],
+ 'near': ['eb999657-dd6b-464e-8713-95c532ac3b18']
+ },
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ "metadata": {
+ "hello": "world"
+ }
+ }
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_access_ipv4(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2"
+ accessIPv4="1.2.3.4"/>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ "accessIPv4": "1.2.3.4",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_access_ipv6(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2"
+ accessIPv6="fead::1234"/>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ "accessIPv6": "fead::1234",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_access_ip(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2"
+ accessIPv4="1.2.3.4"
+ accessIPv6="fead::1234"/>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ "accessIPv4": "1.2.3.4",
+ "accessIPv6": "fead::1234",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_admin_pass(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2"
+ adminPass="1234"/>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ "adminPass": "1234",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_image_link(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="http://localhost:8774/v2/images/2"
+ flavorRef="3"/>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "http://localhost:8774/v2/images/2",
+ "flavorRef": "3",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_flavor_link(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="http://localhost:8774/v2/flavors/3"/>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "http://localhost:8774/v2/flavors/3",
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_empty_metadata_personality(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2">
+ <metadata/>
+ <personality/>
+</server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ "metadata": {},
+ "personality": [],
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_multiple_metadata_items(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2">
+ <metadata>
+ <meta key="one">two</meta>
+ <meta key="open">snack</meta>
+ </metadata>
+</server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ "metadata": {"one": "two", "open": "snack"},
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_multiple_personality_files(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test"
+ imageRef="1"
+ flavorRef="2">
+ <personality>
+ <file path="/etc/banner.txt">MQ==</file>
+ <file path="/etc/hosts">Mg==</file>
+ </personality>
+</server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "2",
+ "personality": [
+ {"path": "/etc/banner.txt", "contents": "MQ=="},
+ {"path": "/etc/hosts", "contents": "Mg=="},
+ ],
+ },
+ }
+ self.assertThat(request['body'], matchers.DictMatches(expected))
+
+ def test_spec_request(self):
+ image_bookmark_link = ("http://servers.api.openstack.org/1234/"
+ "images/52415800-8b69-11e0-9b19-734f6f006e54")
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ imageRef="%s"
+ flavorRef="52415800-8b69-11e0-9b19-734f1195ff37"
+ name="new-server-test">
+ <metadata>
+ <meta key="My Server Name">Apache1</meta>
+ </metadata>
+ <personality>
+ <file path="/etc/banner.txt">Mg==</file>
+ </personality>
+</server>""" % (image_bookmark_link)
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "new-server-test",
+ "imageRef": ("http://servers.api.openstack.org/1234/"
+ "images/52415800-8b69-11e0-9b19-734f6f006e54"),
+ "flavorRef": "52415800-8b69-11e0-9b19-734f1195ff37",
+ "metadata": {"My Server Name": "Apache1"},
+ "personality": [
+ {
+ "path": "/etc/banner.txt",
+ "contents": "Mg==",
+ },
+ ],
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_empty_networks(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <networks/>
+</server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "networks": [],
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_one_network(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <networks>
+ <network uuid="1" fixed_ip="10.0.1.12"/>
+ </networks>
+</server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}],
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_two_networks(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <networks>
+ <network uuid="1" fixed_ip="10.0.1.12"/>
+ <network uuid="2" fixed_ip="10.0.2.12"/>
+ </networks>
+</server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"},
+ {"uuid": "2", "fixed_ip": "10.0.2.12"}],
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_second_network_node_ignored(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <networks>
+ <network uuid="1" fixed_ip="10.0.1.12"/>
+ </networks>
+ <networks>
+ <network uuid="2" fixed_ip="10.0.2.12"/>
+ </networks>
+</server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}],
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_one_network_missing_id(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <networks>
+ <network fixed_ip="10.0.1.12"/>
+ </networks>
+</server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "networks": [{"fixed_ip": "10.0.1.12"}],
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_one_network_missing_fixed_ip(self):
+ serial_request = """
+<server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <networks>
+ <network uuid="1"/>
+ </networks>
+</server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "networks": [{"uuid": "1"}],
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_one_network_empty_id(self):
+ serial_request = """
+ <server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <networks>
+ <network uuid="" fixed_ip="10.0.1.12"/>
+ </networks>
+ </server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "networks": [{"uuid": "", "fixed_ip": "10.0.1.12"}],
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_one_network_empty_fixed_ip(self):
+ serial_request = """
+ <server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <networks>
+ <network uuid="1" fixed_ip=""/>
+ </networks>
+ </server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "networks": [{"uuid": "1", "fixed_ip": ""}],
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_networks_duplicate_ids(self):
+ serial_request = """
+ <server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <networks>
+ <network uuid="1" fixed_ip="10.0.1.12"/>
+ <network uuid="1" fixed_ip="10.0.2.12"/>
+ </networks>
+ </server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"},
+ {"uuid": "1", "fixed_ip": "10.0.2.12"}],
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_availability_zone(self):
+ serial_request = """
+ <server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1"
+ availability_zone="some_zone:some_host">
+ </server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "availability_zone": "some_zone:some_host",
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_multiple_create_args(self):
+ serial_request = """
+ <server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1"
+ min_count="1" max_count="3" return_reservation_id="True">
+ </server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "min_count": "1",
+ "max_count": "3",
+ "return_reservation_id": True,
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_disk_config(self):
+ serial_request = """
+ <server xmlns="http://docs.openstack.org/compute/api/v2"
+ xmlns:OS-DCF="http://docs.openstack.org/compute/ext/disk_config/api/v1.1"
+ name="new-server-test" imageRef="1" flavorRef="1"
+ OS-DCF:diskConfig="AUTO">
+ </server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "OS-DCF:diskConfig": "AUTO",
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_scheduler_hints(self):
+ serial_request = """
+ <server xmlns="http://docs.openstack.org/compute/api/v2"
+ xmlns:OS-SCH-HNT=
+ "http://docs.openstack.org/compute/ext/scheduler-hints/api/v2"
+ name="new-server-test" imageRef="1" flavorRef="1">
+ <OS-SCH-HNT:scheduler_hints>
+ <different_host>
+ 7329b667-50c7-46a6-b913-cb2a09dfeee0
+ </different_host>
+ <different_host>
+ f31efb24-34d2-43e1-8b44-316052956a39
+ </different_host>
+ </OS-SCH-HNT:scheduler_hints>
+ </server>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {"server": {
+ "name": "new-server-test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "OS-SCH-HNT:scheduler_hints": {
+ "different_host": [
+ "7329b667-50c7-46a6-b913-cb2a09dfeee0",
+ "f31efb24-34d2-43e1-8b44-316052956a39",
+ ]
+ }
+ }}
+ self.assertEqual(request['body'], expected)
+
+ def test_request_with_config_drive(self):
+ serial_request = """
+ <server xmlns="http://docs.openstack.org/compute/api/v2"
+ name="config_drive_test"
+ imageRef="1"
+ flavorRef="1"
+ config_drive="true"/>"""
+ request = self.deserializer.deserialize(serial_request)
+ expected = {
+ "server": {
+ "name": "config_drive_test",
+ "imageRef": "1",
+ "flavorRef": "1",
+ "config_drive": "true"
+ },
+ }
+ self.assertEqual(request['body'], expected)
+
+ def test_corrupt_xml(self):
+ """Should throw a 400 error on corrupt xml."""
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ self.deserializer.deserialize,
+ utils.killer_xml_body())
+
+
+class TestServerActionRequestXMLDeserializer(test.TestCase):
+
+ def setUp(self):
+ super(TestServerActionRequestXMLDeserializer, self).setUp()
+ self.deserializer = servers.ActionDeserializer()
+
+ def _generate_request(self, action, disk_cfg, ref):
+ return """
+<%(action)s xmlns="http://docs.openstack.org/compute/api/v1.1"
+ xmlns:OS-DCF="http://docs.openstack.org/compute/ext/disk_config/api/v1.1"
+ %(disk_config)s="MANUAL" %(ref)s="1"/>""" % (
+ {'action': action, 'disk_config': disk_cfg, 'ref': ref})
+
+ def _generate_expected(self, action, ref):
+ return {
+ "%s" % action: {
+ "%s" % ref: "1",
+ "OS-DCF:diskConfig": "MANUAL",
+ },
+ }
+
+ def test_rebuild_request(self):
+ serial_request = self._generate_request("rebuild", "OS-DCF:diskConfig",
+ "imageRef")
+ request = self.deserializer.deserialize(serial_request)
+ expected = self._generate_expected("rebuild", "imageRef")
+ self.assertEqual(request['body'], expected)
+
+ def test_rebuild_request_auto_disk_config_compat(self):
+ serial_request = self._generate_request("rebuild", "auto_disk_config",
+ "imageRef")
+ request = self.deserializer.deserialize(serial_request)
+ expected = self._generate_expected("rebuild", "imageRef")
+ self.assertEqual(request['body'], expected)
+
+ def test_resize_request(self):
+ serial_request = self._generate_request("resize", "OS-DCF:diskConfig",
+ "flavorRef")
+ request = self.deserializer.deserialize(serial_request)
+ expected = self._generate_expected("resize", "flavorRef")
+ self.assertEqual(request['body'], expected)
+
+ def test_resize_request_auto_disk_config_compat(self):
+ serial_request = self._generate_request("resize", "auto_disk_config",
+ "flavorRef")
+ request = self.deserializer.deserialize(serial_request)
+ expected = self._generate_expected("resize", "flavorRef")
+ self.assertEqual(request['body'], expected)
+
+
+class TestAddressesXMLSerialization(test.TestCase):
+
+ index_serializer = ips.AddressesTemplate()
+ show_serializer = ips.NetworkTemplate()
+
+ def _serializer_test_data(self):
+ return {
+ 'network_2': [
+ {'addr': '192.168.0.1', 'version': 4},
+ {'addr': 'fe80::beef', 'version': 6},
+ ],
+ }
+
+ def test_xml_declaration(self):
+ output = self.show_serializer.serialize(self._serializer_test_data())
+ has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
+ self.assertTrue(has_dec)
+
+ def test_show(self):
+ output = self.show_serializer.serialize(self._serializer_test_data())
+ root = etree.XML(output)
+ network = self._serializer_test_data()['network_2']
+ self.assertEqual(str(root.get('id')), 'network_2')
+ ip_elems = root.findall('{0}ip'.format(NS))
+ for z, ip_elem in enumerate(ip_elems):
+ ip = network[z]
+ self.assertEqual(str(ip_elem.get('version')),
+ str(ip['version']))
+ self.assertEqual(str(ip_elem.get('addr')),
+ str(ip['addr']))
+
+ def test_index(self):
+ fixture = {
+ 'addresses': {
+ 'network_1': [
+ {'addr': '192.168.0.3', 'version': 4},
+ {'addr': '192.168.0.5', 'version': 4},
+ ],
+ 'network_2': [
+ {'addr': '192.168.0.1', 'version': 4},
+ {'addr': 'fe80::beef', 'version': 6},
+ ],
+ },
+ }
+ output = self.index_serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'addresses')
+ addresses_dict = fixture['addresses']
+ network_elems = root.findall('{0}network'.format(NS))
+ self.assertEqual(len(network_elems), 2)
+ for i, network_elem in enumerate(network_elems):
+ network = addresses_dict.items()[i]
+ self.assertEqual(str(network_elem.get('id')), str(network[0]))
+ ip_elems = network_elem.findall('{0}ip'.format(NS))
+ for z, ip_elem in enumerate(ip_elems):
+ ip = network[1][z]
+ self.assertEqual(str(ip_elem.get('version')),
+ str(ip['version']))
+ self.assertEqual(str(ip_elem.get('addr')),
+ str(ip['addr']))
+
+
+class ServersViewBuilderTest(test.TestCase):
+
+ image_bookmark = "http://localhost/fake/images/5"
+ flavor_bookmark = "http://localhost/fake/flavors/1"
+
+ def setUp(self):
+ super(ServersViewBuilderTest, self).setUp()
+ self.flags(use_ipv6=True)
+ db_inst = fakes.stub_instance(
+ id=1,
+ image_ref="5",
+ uuid="deadbeef-feed-edee-beef-d0ea7beefedd",
+ display_name="test_server",
+ include_fake_metadata=False)
+
+ privates = ['172.19.0.1']
+ publics = ['192.168.0.3']
+ public6s = ['b33f::fdee:ddff:fecc:bbaa']
+
+ def nw_info(*args, **kwargs):
+ return [(None, {'label': 'public',
+ 'ips': [dict(ip=ip) for ip in publics],
+ 'ip6s': [dict(ip=ip) for ip in public6s]}),
+ (None, {'label': 'private',
+ 'ips': [dict(ip=ip) for ip in privates]})]
+
+ def floaters(*args, **kwargs):
+ return []
+
+ fakes.stub_out_nw_api_get_instance_nw_info(self.stubs, nw_info)
+ fakes.stub_out_nw_api_get_floating_ips_by_fixed_address(self.stubs,
+ floaters)
+
+ self.uuid = db_inst['uuid']
+ self.view_builder = views.servers.ViewBuilder()
+ self.request = fakes.HTTPRequest.blank("/v2/fake")
+ self.request.context = context.RequestContext('fake', 'fake')
+ self.instance = fake_instance.fake_instance_obj(
+ self.request.context,
+ expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS,
+ **db_inst)
+ self.self_link = "http://localhost/v2/fake/servers/%s" % self.uuid
+ self.bookmark_link = "http://localhost/fake/servers/%s" % self.uuid
+ self.expected_detailed_server = {
+ "server": {
+ "id": self.uuid,
+ "user_id": "fake_user",
+ "tenant_id": "fake_project",
+ "updated": "2010-11-11T11:00:00Z",
+ "created": "2010-10-10T12:00:00Z",
+ "progress": 0,
+ "name": "test_server",
+ "status": "BUILD",
+ "accessIPv4": "",
+ "accessIPv6": "",
+ "hostId": '',
+ "image": {
+ "id": "5",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": self.image_bookmark,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": self.flavor_bookmark,
+ },
+ ],
+ },
+ "addresses": {
+ 'test1': [
+ {'version': 4, 'addr': '192.168.1.100'},
+ {'version': 6, 'addr': '2001:db8:0:1::1'}
+ ]
+ },
+ "metadata": {},
+ "links": [
+ {
+ "rel": "self",
+ "href": self.self_link,
+ },
+ {
+ "rel": "bookmark",
+ "href": self.bookmark_link,
+ },
+ ],
+ }
+ }
+
+ self.expected_server = {
+ "server": {
+ "id": self.uuid,
+ "name": "test_server",
+ "links": [
+ {
+ "rel": "self",
+ "href": self.self_link,
+ },
+ {
+ "rel": "bookmark",
+ "href": self.bookmark_link,
+ },
+ ],
+ }
+ }
+
+ def test_get_flavor_valid_flavor(self):
+ expected = {"id": "1",
+ "links": [{"rel": "bookmark",
+ "href": self.flavor_bookmark}]}
+ result = self.view_builder._get_flavor(self.request, self.instance)
+ self.assertEqual(result, expected)
+
+ def test_build_server(self):
+ output = self.view_builder.basic(self.request, self.instance)
+ self.assertThat(output,
+ matchers.DictMatches(self.expected_server))
+
+ def test_build_server_with_project_id(self):
+
+ output = self.view_builder.basic(self.request, self.instance)
+ self.assertThat(output,
+ matchers.DictMatches(self.expected_server))
+
+ def test_build_server_detail(self):
+
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output,
+ matchers.DictMatches(self.expected_detailed_server))
+
+ def test_build_server_no_image(self):
+ self.instance["image_ref"] = ""
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertEqual(output['server']['image'], "")
+
+ def test_build_server_detail_with_fault(self):
+ self.instance['vm_state'] = vm_states.ERROR
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context, self.uuid)
+
+ self.expected_detailed_server["server"]["status"] = "ERROR"
+ self.expected_detailed_server["server"]["fault"] = {
+ "code": 404,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "HTTPNotFound",
+ "details": "Stock details for test",
+ }
+ del self.expected_detailed_server["server"]["progress"]
+
+ self.request.context = context.RequestContext('fake', 'fake')
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output,
+ matchers.DictMatches(self.expected_detailed_server))
+
+ def test_build_server_detail_with_fault_that_has_been_deleted(self):
+ self.instance['deleted'] = 1
+ self.instance['vm_state'] = vm_states.ERROR
+ fault = fake_instance.fake_fault_obj(self.request.context,
+ self.uuid, code=500,
+ message="No valid host was found")
+ self.instance['fault'] = fault
+
+ # Regardless of the vm_state deleted servers sholud have DELETED status
+ self.expected_detailed_server["server"]["status"] = "DELETED"
+ self.expected_detailed_server["server"]["fault"] = {
+ "code": 500,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "No valid host was found",
+ }
+ del self.expected_detailed_server["server"]["progress"]
+
+ self.request.context = context.RequestContext('fake', 'fake')
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output,
+ matchers.DictMatches(self.expected_detailed_server))
+
+ def test_build_server_detail_with_fault_no_details_not_admin(self):
+ self.instance['vm_state'] = vm_states.ERROR
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context,
+ self.uuid,
+ code=500,
+ message='Error')
+
+ expected_fault = {"code": 500,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "Error"}
+
+ self.request.context = context.RequestContext('fake', 'fake')
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output['server']['fault'],
+ matchers.DictMatches(expected_fault))
+
+ def test_build_server_detail_with_fault_admin(self):
+ self.instance['vm_state'] = vm_states.ERROR
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context,
+ self.uuid,
+ code=500,
+ message='Error')
+
+ expected_fault = {"code": 500,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "Error",
+ 'details': 'Stock details for test'}
+
+ self.request.environ['nova.context'].is_admin = True
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output['server']['fault'],
+ matchers.DictMatches(expected_fault))
+
+ def test_build_server_detail_with_fault_no_details_admin(self):
+ self.instance['vm_state'] = vm_states.ERROR
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context,
+ self.uuid,
+ code=500,
+ message='Error',
+ details='')
+
+ expected_fault = {"code": 500,
+ "created": "2010-10-10T12:00:00Z",
+ "message": "Error"}
+
+ self.request.environ['nova.context'].is_admin = True
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output['server']['fault'],
+ matchers.DictMatches(expected_fault))
+
+ def test_build_server_detail_with_fault_but_active(self):
+ self.instance['vm_state'] = vm_states.ACTIVE
+ self.instance['progress'] = 100
+ self.instance['fault'] = fake_instance.fake_fault_obj(
+ self.request.context, self.uuid)
+
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertNotIn('fault', output['server'])
+
+ def test_build_server_detail_active_status(self):
+ # set the power state of the instance to running
+ self.instance['vm_state'] = vm_states.ACTIVE
+ self.instance['progress'] = 100
+
+ self.expected_detailed_server["server"]["status"] = "ACTIVE"
+ self.expected_detailed_server["server"]["progress"] = 100
+
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output,
+ matchers.DictMatches(self.expected_detailed_server))
+
+ def test_build_server_detail_with_accessipv4(self):
+
+ access_ip_v4 = '1.2.3.4'
+ self.instance['access_ip_v4'] = access_ip_v4
+
+ self.expected_detailed_server["server"]["accessIPv4"] = access_ip_v4
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output,
+ matchers.DictMatches(self.expected_detailed_server))
+
+ def test_build_server_detail_with_accessipv6(self):
+
+ access_ip_v6 = 'fead::1234'
+ self.instance['access_ip_v6'] = access_ip_v6
+
+ self.expected_detailed_server["server"]["accessIPv6"] = access_ip_v6
+
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output,
+ matchers.DictMatches(self.expected_detailed_server))
+
+ def test_build_server_detail_with_metadata(self):
+
+ metadata = []
+ metadata.append(models.InstanceMetadata(key="Open", value="Stack"))
+ metadata = nova_utils.metadata_to_dict(metadata)
+ self.instance['metadata'] = metadata
+
+ self.expected_detailed_server["server"]["metadata"] = {"Open": "Stack"}
+ output = self.view_builder.show(self.request, self.instance)
+ self.assertThat(output,
+ matchers.DictMatches(self.expected_detailed_server))
+
+
+class ServerXMLSerializationTest(test.TestCase):
+
+ TIMESTAMP = "2010-10-11T10:30:22Z"
+ SERVER_HREF = 'http://localhost/v2/servers/%s' % FAKE_UUID
+ SERVER_NEXT = 'http://localhost/v2/servers?limit=%s&marker=%s'
+ SERVER_BOOKMARK = 'http://localhost/servers/%s' % FAKE_UUID
+ IMAGE_BOOKMARK = 'http://localhost/images/5'
+ FLAVOR_BOOKMARK = 'http://localhost/flavors/1'
+ USERS_ATTRIBUTES = ['name', 'id', 'created', 'accessIPv4',
+ 'updated', 'progress', 'status', 'hostId',
+ 'accessIPv6']
+ ADMINS_ATTRIBUTES = USERS_ATTRIBUTES + ['adminPass']
+
+ def setUp(self):
+ super(ServerXMLSerializationTest, self).setUp()
+ self.body = {
+ "server": {
+ 'id': FAKE_UUID,
+ 'user_id': 'fake_user_id',
+ 'tenant_id': 'fake_tenant_id',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ "progress": 0,
+ "name": "test_server-" + u'\u89e3\u7801',
+ "status": "BUILD",
+ "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0',
+ "accessIPv4": "1.2.3.4",
+ "accessIPv6": "fead::1234",
+ "image": {
+ "id": "5",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": self.IMAGE_BOOKMARK,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": self.FLAVOR_BOOKMARK,
+ },
+ ],
+ },
+ "addresses": {
+ "network_one": [
+ {
+ "version": 4,
+ "addr": "67.23.10.138",
+ },
+ {
+ "version": 6,
+ "addr": "::babe:67.23.10.138",
+ },
+ ],
+ "network_two": [
+ {
+ "version": 4,
+ "addr": "67.23.10.139",
+ },
+ {
+ "version": 6,
+ "addr": "::babe:67.23.10.139",
+ },
+ ],
+ },
+ "metadata": {
+ "Open": "Stack",
+ "Number": "1",
+ },
+ 'links': [
+ {
+ 'href': self.SERVER_HREF,
+ 'rel': 'self',
+ },
+ {
+ 'href': self.SERVER_BOOKMARK,
+ 'rel': 'bookmark',
+ },
+ ],
+ }
+ }
+
+ def _validate_xml(self, root, server_dict):
+
+ link_nodes = root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(server_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ metadata_root = root.find('{0}metadata'.format(NS))
+ metadata_elems = metadata_root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 2)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = server_dict['metadata'].items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+
+ image_root = root.find('{0}image'.format(NS))
+ self.assertEqual(image_root.get('id'), server_dict['image']['id'])
+ link_nodes = image_root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 1)
+ for i, link in enumerate(server_dict['image']['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ flavor_root = root.find('{0}flavor'.format(NS))
+ self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id'])
+ link_nodes = flavor_root.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 1)
+ for i, link in enumerate(server_dict['flavor']['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ addresses_root = root.find('{0}addresses'.format(NS))
+ addresses_dict = server_dict['addresses']
+ network_elems = addresses_root.findall('{0}network'.format(NS))
+ self.assertEqual(len(network_elems), 2)
+ for i, network_elem in enumerate(network_elems):
+ network = addresses_dict.items()[i]
+ self.assertEqual(str(network_elem.get('id')), str(network[0]))
+ ip_elems = network_elem.findall('{0}ip'.format(NS))
+ for z, ip_elem in enumerate(ip_elems):
+ ip = network[1][z]
+ self.assertEqual(str(ip_elem.get('version')),
+ str(ip['version']))
+ self.assertEqual(str(ip_elem.get('addr')),
+ str(ip['addr']))
+
+ def _validate_required_attributes(self, root, server_dict, attributes):
+ for key in attributes:
+ expected = server_dict[key]
+ if not isinstance(expected, six.text_type):
+ expected = str(expected)
+ self.assertEqual(expected, root.get(key))
+
+ def test_xml_declaration(self):
+ serializer = servers.ServerTemplate()
+
+ output = serializer.serialize(self.body)
+ has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
+ self.assertTrue(has_dec)
+
+ def test_show(self):
+ serializer = servers.ServerTemplate()
+
+ output = serializer.serialize(self.body)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'server')
+
+ server_dict = self.body['server']
+
+ self._validate_required_attributes(root, server_dict,
+ self.USERS_ATTRIBUTES)
+ self._validate_xml(root, server_dict)
+
+ def test_create(self):
+ serializer = servers.FullServerTemplate()
+
+ self.body["server"]["adminPass"] = "test_password"
+
+ output = serializer.serialize(self.body)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'server')
+
+ server_dict = self.body['server']
+
+ self._validate_required_attributes(root, server_dict,
+ self.ADMINS_ATTRIBUTES)
+ self._validate_xml(root, server_dict)
+
+ def test_index(self):
+ serializer = servers.MinimalServersTemplate()
+
+ uuid1 = fakes.get_fake_uuid(1)
+ uuid2 = fakes.get_fake_uuid(2)
+ expected_server_href = 'http://localhost/v2/servers/%s' % uuid1
+ expected_server_bookmark = 'http://localhost/servers/%s' % uuid1
+ expected_server_href_2 = 'http://localhost/v2/servers/%s' % uuid2
+ expected_server_bookmark_2 = 'http://localhost/servers/%s' % uuid2
+ fixture = {"servers": [
+ {
+ "id": fakes.get_fake_uuid(1),
+ "name": "test_server",
+ 'links': [
+ {
+ 'href': expected_server_href,
+ 'rel': 'self',
+ },
+ {
+ 'href': expected_server_bookmark,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ {
+ "id": fakes.get_fake_uuid(2),
+ "name": "test_server_2",
+ 'links': [
+ {
+ 'href': expected_server_href_2,
+ 'rel': 'self',
+ },
+ {
+ 'href': expected_server_bookmark_2,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ ]}
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'servers')
+ server_elems = root.findall('{0}server'.format(NS))
+ self.assertEqual(len(server_elems), 2)
+ for i, server_elem in enumerate(server_elems):
+ server_dict = fixture['servers'][i]
+ for key in ['name', 'id']:
+ self.assertEqual(server_elem.get(key), str(server_dict[key]))
+
+ link_nodes = server_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(server_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ def test_index_with_servers_links(self):
+ serializer = servers.MinimalServersTemplate()
+
+ uuid1 = fakes.get_fake_uuid(1)
+ uuid2 = fakes.get_fake_uuid(2)
+ expected_server_href = 'http://localhost/v2/servers/%s' % uuid1
+ expected_server_next = self.SERVER_NEXT % (2, 2)
+ expected_server_bookmark = 'http://localhost/servers/%s' % uuid1
+ expected_server_href_2 = 'http://localhost/v2/servers/%s' % uuid2
+ expected_server_bookmark_2 = 'http://localhost/servers/%s' % uuid2
+ fixture = {"servers": [
+ {
+ "id": fakes.get_fake_uuid(1),
+ "name": "test_server",
+ 'links': [
+ {
+ 'href': expected_server_href,
+ 'rel': 'self',
+ },
+ {
+ 'href': expected_server_bookmark,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ {
+ "id": fakes.get_fake_uuid(2),
+ "name": "test_server_2",
+ 'links': [
+ {
+ 'href': expected_server_href_2,
+ 'rel': 'self',
+ },
+ {
+ 'href': expected_server_bookmark_2,
+ 'rel': 'bookmark',
+ },
+ ],
+ },
+ ],
+ "servers_links": [
+ {
+ 'rel': 'next',
+ 'href': expected_server_next,
+ },
+ ]}
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'servers')
+ server_elems = root.findall('{0}server'.format(NS))
+ self.assertEqual(len(server_elems), 2)
+ for i, server_elem in enumerate(server_elems):
+ server_dict = fixture['servers'][i]
+ for key in ['name', 'id']:
+ self.assertEqual(server_elem.get(key), str(server_dict[key]))
+
+ link_nodes = server_elem.findall('{0}link'.format(ATOMNS))
+ self.assertEqual(len(link_nodes), 2)
+ for i, link in enumerate(server_dict['links']):
+ for key, value in link.items():
+ self.assertEqual(link_nodes[i].get(key), value)
+
+ # Check servers_links
+ servers_links = root.findall('{0}link'.format(ATOMNS))
+ for i, link in enumerate(fixture['servers_links']):
+ for key, value in link.items():
+ self.assertEqual(servers_links[i].get(key), value)
+
+ def test_detail(self):
+ serializer = servers.ServersTemplate()
+
+ uuid1 = fakes.get_fake_uuid(1)
+ expected_server_href = 'http://localhost/v2/servers/%s' % uuid1
+ expected_server_bookmark = 'http://localhost/servers/%s' % uuid1
+ expected_image_bookmark = self.IMAGE_BOOKMARK
+ expected_flavor_bookmark = self.FLAVOR_BOOKMARK
+
+ uuid2 = fakes.get_fake_uuid(2)
+ expected_server_href_2 = 'http://localhost/v2/servers/%s' % uuid2
+ expected_server_bookmark_2 = 'http://localhost/servers/%s' % uuid2
+ fixture = {"servers": [
+ {
+ "id": fakes.get_fake_uuid(1),
+ "user_id": "fake",
+ "tenant_id": "fake",
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ "progress": 0,
+ "name": "test_server",
+ "status": "BUILD",
+ "accessIPv4": "1.2.3.4",
+ "accessIPv6": "fead::1234",
+ "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0',
+ "image": {
+ "id": "5",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": expected_image_bookmark,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": expected_flavor_bookmark,
+ },
+ ],
+ },
+ "addresses": {
+ "network_one": [
+ {
+ "version": 4,
+ "addr": "67.23.10.138",
+ },
+ {
+ "version": 6,
+ "addr": "::babe:67.23.10.138",
+ },
+ ],
+ "network_two": [
+ {
+ "version": 4,
+ "addr": "67.23.10.139",
+ },
+ {
+ "version": 6,
+ "addr": "::babe:67.23.10.139",
+ },
+ ],
+ },
+ "metadata": {
+ "Open": "Stack",
+ "Number": "1",
+ },
+ "links": [
+ {
+ "href": expected_server_href,
+ "rel": "self",
+ },
+ {
+ "href": expected_server_bookmark,
+ "rel": "bookmark",
+ },
+ ],
+ },
+ {
+ "id": fakes.get_fake_uuid(2),
+ "user_id": 'fake',
+ "tenant_id": 'fake',
+ 'created': self.TIMESTAMP,
+ 'updated': self.TIMESTAMP,
+ "progress": 100,
+ "name": "test_server_2",
+ "status": "ACTIVE",
+ "accessIPv4": "1.2.3.4",
+ "accessIPv6": "fead::1234",
+ "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0',
+ "image": {
+ "id": "5",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": expected_image_bookmark,
+ },
+ ],
+ },
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "rel": "bookmark",
+ "href": expected_flavor_bookmark,
+ },
+ ],
+ },
+ "addresses": {
+ "network_one": [
+ {
+ "version": 4,
+ "addr": "67.23.10.138",
+ },
+ {
+ "version": 6,
+ "addr": "::babe:67.23.10.138",
+ },
+ ],
+ "network_two": [
+ {
+ "version": 4,
+ "addr": "67.23.10.139",
+ },
+ {
+ "version": 6,
+ "addr": "::babe:67.23.10.139",
+ },
+ ],
+ },
+ "metadata": {
+ "Open": "Stack",
+ "Number": "2",
+ },
+ "links": [
+ {
+ "href": expected_server_href_2,
+ "rel": "self",
+ },
+ {
+ "href": expected_server_bookmark_2,
+ "rel": "bookmark",
+ },
+ ],
+ },
+ ]}
+
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'servers')
+ server_elems = root.findall('{0}server'.format(NS))
+ self.assertEqual(len(server_elems), 2)
+ for i, server_elem in enumerate(server_elems):
+ server_dict = fixture['servers'][i]
+ self._validate_required_attributes(server_elem, server_dict,
+ self.USERS_ATTRIBUTES)
+ self._validate_xml(server_elem, server_dict)
+
+ def test_update(self):
+ serializer = servers.ServerTemplate()
+
+ self.body["server"]["fault"] = {
+ "code": 500,
+ "created": self.TIMESTAMP,
+ "message": "Error Message",
+ "details": "Fault details",
+ }
+ output = serializer.serialize(self.body)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'server')
+
+ server_dict = self.body['server']
+
+ self._validate_required_attributes(root, server_dict,
+ self.USERS_ATTRIBUTES)
+
+ self._validate_xml(root, server_dict)
+ fault_root = root.find('{0}fault'.format(NS))
+ fault_dict = server_dict['fault']
+ self.assertEqual(fault_root.get("code"), str(fault_dict["code"]))
+ self.assertEqual(fault_root.get("created"), fault_dict["created"])
+ msg_elem = fault_root.find('{0}message'.format(NS))
+ self.assertEqual(msg_elem.text, fault_dict["message"])
+ det_elem = fault_root.find('{0}details'.format(NS))
+ self.assertEqual(det_elem.text, fault_dict["details"])
+
+ def test_action(self):
+ serializer = servers.FullServerTemplate()
+
+ self.body["server"]["adminPass"] = u'\u89e3\u7801'
+ output = serializer.serialize(self.body)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'server')
+
+ server_dict = self.body['server']
+
+ self._validate_required_attributes(root, server_dict,
+ self.ADMINS_ATTRIBUTES)
+
+ self._validate_xml(root, server_dict)
+
+
+class ServersAllExtensionsTestCase(test.TestCase):
+ """Servers tests using default API router with all extensions enabled.
+
+ The intent here is to catch cases where extensions end up throwing
+ an exception because of a malformed request before the core API
+ gets a chance to validate the request and return a 422 response.
+
+ For example, ServerDiskConfigController extends servers.Controller::
+
+ | @wsgi.extends
+ | def create(self, req, body):
+ | if 'server' in body:
+ | self._set_disk_config(body['server'])
+ | resp_obj = (yield)
+ | self._show(req, resp_obj)
+
+ we want to ensure that the extension isn't barfing on an invalid
+ body.
+ """
+
+ def setUp(self):
+ super(ServersAllExtensionsTestCase, self).setUp()
+ self.app = compute.APIRouter()
+
+ def test_create_missing_server(self):
+ # Test create with malformed body.
+
+ def fake_create(*args, **kwargs):
+ raise test.TestingException("Should not reach the compute API.")
+
+ self.stubs.Set(compute_api.API, 'create', fake_create)
+
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ body = {'foo': {'a': 'b'}}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(422, res.status_int)
+
+ def test_update_missing_server(self):
+ # Test update with malformed body.
+
+ def fake_update(*args, **kwargs):
+ raise test.TestingException("Should not reach the compute API.")
+
+ self.stubs.Set(compute_api.API, 'update', fake_update)
+
+ req = fakes.HTTPRequest.blank('/fake/servers/1')
+ req.method = 'PUT'
+ req.content_type = 'application/json'
+ body = {'foo': {'a': 'b'}}
+
+ req.body = jsonutils.dumps(body)
+ res = req.get_response(self.app)
+ self.assertEqual(422, res.status_int)
+
+
+class ServersUnprocessableEntityTestCase(test.TestCase):
+ """Tests of places we throw 422 Unprocessable Entity from."""
+
+ def setUp(self):
+ super(ServersUnprocessableEntityTestCase, self).setUp()
+ self.ext_mgr = extensions.ExtensionManager()
+ self.ext_mgr.extensions = {}
+ self.controller = servers.Controller(self.ext_mgr)
+
+ def _unprocessable_server_create(self, body):
+ req = fakes.HTTPRequest.blank('/fake/servers')
+ req.method = 'POST'
+
+ self.assertRaises(webob.exc.HTTPUnprocessableEntity,
+ self.controller.create, req, body)
+
+ def test_create_server_no_body(self):
+ self._unprocessable_server_create(body=None)
+
+ def test_create_server_missing_server(self):
+ body = {'foo': {'a': 'b'}}
+ self._unprocessable_server_create(body=body)
+
+ def test_create_server_malformed_entity(self):
+ body = {'server': 'string'}
+ self._unprocessable_server_create(body=body)
+
+ def _unprocessable_server_update(self, body):
+ req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID)
+ req.method = 'PUT'
+
+ self.assertRaises(webob.exc.HTTPUnprocessableEntity,
+ self.controller.update, req, FAKE_UUID, body)
+
+ def test_update_server_no_body(self):
+ self._unprocessable_server_update(body=None)
+
+ def test_update_server_missing_server(self):
+ body = {'foo': {'a': 'b'}}
+ self._unprocessable_server_update(body=body)
+
+ def test_create_update_malformed_entity(self):
+ body = {'server': 'string'}
+ self._unprocessable_server_update(body=body)
diff --git a/nova/tests/unit/api/openstack/compute/test_urlmap.py b/nova/tests/unit/api/openstack/compute/test_urlmap.py
new file mode 100644
index 0000000000..c95cb95d2c
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_urlmap.py
@@ -0,0 +1,171 @@
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.serialization import jsonutils
+import webob
+
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+import nova.tests.unit.image.fake
+
+
+class UrlmapTest(test.NoDBTestCase):
+ def setUp(self):
+ super(UrlmapTest, self).setUp()
+ fakes.stub_out_rate_limiting(self.stubs)
+ nova.tests.unit.image.fake.stub_out_image_service(self.stubs)
+
+ def tearDown(self):
+ super(UrlmapTest, self).tearDown()
+ nova.tests.unit.image.fake.FakeImageService_reset()
+
+ def test_path_version_v1_1(self):
+ # Test URL path specifying v1.1 returns v2 content.
+ req = webob.Request.blank('/v1.1/')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app(init_only=('versions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v2.0')
+
+ def test_content_type_version_v1_1(self):
+ # Test Content-Type specifying v1.1 returns v2 content.
+ req = webob.Request.blank('/')
+ req.content_type = "application/json;version=1.1"
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app(init_only=('versions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v2.0')
+
+ def test_accept_version_v1_1(self):
+ # Test Accept header specifying v1.1 returns v2 content.
+ req = webob.Request.blank('/')
+ req.accept = "application/json;version=1.1"
+ res = req.get_response(fakes.wsgi_app(init_only=('versions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v2.0')
+
+ def test_path_version_v2(self):
+ # Test URL path specifying v2 returns v2 content.
+ req = webob.Request.blank('/v2/')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app(init_only=('versions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v2.0')
+
+ def test_content_type_version_v2(self):
+ # Test Content-Type specifying v2 returns v2 content.
+ req = webob.Request.blank('/')
+ req.content_type = "application/json;version=2"
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app(init_only=('versions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v2.0')
+
+ def test_accept_version_v2(self):
+ # Test Accept header specifying v2 returns v2 content.
+ req = webob.Request.blank('/')
+ req.accept = "application/json;version=2"
+ res = req.get_response(fakes.wsgi_app(init_only=('versions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v2.0')
+
+ def test_path_content_type(self):
+ # Test URL path specifying JSON returns JSON content.
+ url = '/v2/fake/images/cedef40a-ed67-4d10-800e-17455edce175.json'
+ req = webob.Request.blank(url)
+ req.accept = "application/xml"
+ res = req.get_response(fakes.wsgi_app(init_only=('images',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['image']['id'],
+ 'cedef40a-ed67-4d10-800e-17455edce175')
+
+ def test_accept_content_type(self):
+ # Test Accept header specifying JSON returns JSON content.
+ url = '/v2/fake/images/cedef40a-ed67-4d10-800e-17455edce175'
+ req = webob.Request.blank(url)
+ req.accept = "application/xml;q=0.8, application/json"
+ res = req.get_response(fakes.wsgi_app(init_only=('images',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['image']['id'],
+ 'cedef40a-ed67-4d10-800e-17455edce175')
+
+ def test_path_version_v21(self):
+ # Test URL path specifying v2.1 returns v2.1 content.
+ req = webob.Request.blank('/v2.1/')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app_v21(init_only=('versions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v2.1')
+
+ def test_content_type_version_v21(self):
+ # Test Content-Type specifying v2.1 returns v2 content.
+ req = webob.Request.blank('/')
+ req.content_type = "application/json;version=2.1"
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app_v21(init_only=('versions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v2.1')
+
+ def test_accept_version_v21(self):
+ # Test Accept header specifying v2.1 returns v2.1 content.
+ req = webob.Request.blank('/')
+ req.accept = "application/json;version=2.1"
+ res = req.get_response(fakes.wsgi_app_v21(init_only=('versions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['version']['id'], 'v2.1')
+
+ def test_path_content_type_v21(self):
+ # Test URL path specifying JSON returns JSON content.
+ url = '/v2.1/fake/extensions/extensions.json'
+ req = webob.Request.blank(url)
+ req.accept = "application/xml"
+ res = req.get_response(fakes.wsgi_app_v21())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['extension']['name'], 'Extensions')
+
+ def test_accept_content_type_v21(self):
+ # Test Accept header specifying JSON returns JSON content.
+ url = '/v2.1/fake/extensions/extensions'
+ req = webob.Request.blank(url)
+ req.accept = "application/xml;q=0.8, application/json"
+ res = req.get_response(fakes.wsgi_app_v21(init_only=('extensions',)))
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ body = jsonutils.loads(res.body)
+ self.assertEqual(body['extension']['name'], 'Extensions')
diff --git a/nova/tests/unit/api/openstack/compute/test_v21_extensions.py b/nova/tests/unit/api/openstack/compute/test_v21_extensions.py
new file mode 100644
index 0000000000..7998dc82e5
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_v21_extensions.py
@@ -0,0 +1,196 @@
+# Copyright 2013 IBM Corp.
+# Copyright 2014 NEC Corporation. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.config import cfg
+import stevedore
+import webob.exc
+
+from nova.api import openstack
+from nova.api.openstack import compute
+from nova.api.openstack.compute import plugins
+from nova.api.openstack import extensions
+from nova import exception
+from nova import test
+
+CONF = cfg.CONF
+
+
+class fake_bad_extension(object):
+ name = "fake_bad_extension"
+ alias = "fake-bad"
+
+
+class fake_stevedore_enabled_extensions(object):
+ def __init__(self, namespace, check_func, invoke_on_load=False,
+ invoke_args=(), invoke_kwds=None):
+ self.extensions = []
+
+ def map(self, func, *args, **kwds):
+ pass
+
+ def __iter__(self):
+ return iter(self.extensions)
+
+
+class fake_loaded_extension_info(object):
+ def __init__(self):
+ self.extensions = {}
+
+ def register_extension(self, ext):
+ self.extensions[ext] = ext
+ return True
+
+ def get_extensions(self):
+ return {'core1': None, 'core2': None, 'noncore1': None}
+
+
+class ExtensionLoadingTestCase(test.NoDBTestCase):
+
+ def _set_v21_core(self, core_extensions):
+ openstack.API_V3_CORE_EXTENSIONS = core_extensions
+
+ def test_extensions_loaded(self):
+ app = compute.APIRouterV21()
+ self.assertIn('servers', app._loaded_extension_info.extensions)
+
+ def test_check_bad_extension(self):
+ extension_info = plugins.LoadedExtensionInfo()
+ self.assertFalse(extension_info._check_extension(fake_bad_extension))
+
+ def test_extensions_blacklist(self):
+ app = compute.APIRouterV21()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+ CONF.set_override('extensions_blacklist', ['os-hosts'], 'osapi_v3')
+ app = compute.APIRouterV21()
+ self.assertNotIn('os-hosts', app._loaded_extension_info.extensions)
+
+ def test_extensions_whitelist_accept(self):
+ # NOTE(maurosr): just to avoid to get an exception raised for not
+ # loading all core api.
+ v21_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['servers'])
+ self.addCleanup(self._set_v21_core, v21_core)
+
+ app = compute.APIRouterV21()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+ CONF.set_override('extensions_whitelist', ['servers', 'os-hosts'],
+ 'osapi_v3')
+ app = compute.APIRouterV21()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+
+ def test_extensions_whitelist_block(self):
+ # NOTE(maurosr): just to avoid to get an exception raised for not
+ # loading all core api.
+ v21_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['servers'])
+ self.addCleanup(self._set_v21_core, v21_core)
+
+ app = compute.APIRouterV21()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+ CONF.set_override('extensions_whitelist', ['servers'], 'osapi_v3')
+ app = compute.APIRouterV21()
+ self.assertNotIn('os-hosts', app._loaded_extension_info.extensions)
+
+ def test_blacklist_overrides_whitelist(self):
+ # NOTE(maurosr): just to avoid to get an exception raised for not
+ # loading all core api.
+ v21_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['servers'])
+ self.addCleanup(self._set_v21_core, v21_core)
+
+ app = compute.APIRouterV21()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+ CONF.set_override('extensions_whitelist', ['servers', 'os-hosts'],
+ 'osapi_v3')
+ CONF.set_override('extensions_blacklist', ['os-hosts'], 'osapi_v3')
+ app = compute.APIRouterV21()
+ self.assertNotIn('os-hosts', app._loaded_extension_info.extensions)
+ self.assertIn('servers', app._loaded_extension_info.extensions)
+ self.assertEqual(1, len(app._loaded_extension_info.extensions))
+
+ def test_get_missing_core_extensions(self):
+ v21_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['core1', 'core2'])
+ self.addCleanup(self._set_v21_core, v21_core)
+ self.assertEqual(0, len(
+ compute.APIRouterV21.get_missing_core_extensions(
+ ['core1', 'core2', 'noncore1'])))
+ missing_core = compute.APIRouterV21.get_missing_core_extensions(
+ ['core1'])
+ self.assertEqual(1, len(missing_core))
+ self.assertIn('core2', missing_core)
+ missing_core = compute.APIRouterV21.get_missing_core_extensions([])
+ self.assertEqual(2, len(missing_core))
+ self.assertIn('core1', missing_core)
+ self.assertIn('core2', missing_core)
+ missing_core = compute.APIRouterV21.get_missing_core_extensions(
+ ['noncore1'])
+ self.assertEqual(2, len(missing_core))
+ self.assertIn('core1', missing_core)
+ self.assertIn('core2', missing_core)
+
+ def test_core_extensions_present(self):
+ self.stubs.Set(stevedore.enabled, 'EnabledExtensionManager',
+ fake_stevedore_enabled_extensions)
+ self.stubs.Set(plugins, 'LoadedExtensionInfo',
+ fake_loaded_extension_info)
+ v21_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['core1', 'core2'])
+ self.addCleanup(self._set_v21_core, v21_core)
+ # if no core API extensions are missing then an exception will
+ # not be raised when creating an instance of compute.APIRouterV21
+ compute.APIRouterV21()
+
+ def test_core_extensions_missing(self):
+ self.stubs.Set(stevedore.enabled, 'EnabledExtensionManager',
+ fake_stevedore_enabled_extensions)
+ self.stubs.Set(plugins, 'LoadedExtensionInfo',
+ fake_loaded_extension_info)
+ self.assertRaises(exception.CoreAPIMissing, compute.APIRouterV21)
+
+ def test_extensions_expected_error(self):
+ @extensions.expected_errors(404)
+ def fake_func():
+ raise webob.exc.HTTPNotFound()
+
+ self.assertRaises(webob.exc.HTTPNotFound, fake_func)
+
+ def test_extensions_expected_error_from_list(self):
+ @extensions.expected_errors((404, 403))
+ def fake_func():
+ raise webob.exc.HTTPNotFound()
+
+ self.assertRaises(webob.exc.HTTPNotFound, fake_func)
+
+ def test_extensions_unexpected_error(self):
+ @extensions.expected_errors(404)
+ def fake_func():
+ raise webob.exc.HTTPConflict()
+
+ self.assertRaises(webob.exc.HTTPInternalServerError, fake_func)
+
+ def test_extensions_unexpected_error_from_list(self):
+ @extensions.expected_errors((404, 413))
+ def fake_func():
+ raise webob.exc.HTTPConflict()
+
+ self.assertRaises(webob.exc.HTTPInternalServerError, fake_func)
+
+ def test_extensions_unexpected_policy_not_authorized_error(self):
+ @extensions.expected_errors(404)
+ def fake_func():
+ raise exception.PolicyNotAuthorized(action="foo")
+
+ self.assertRaises(exception.PolicyNotAuthorized, fake_func)
diff --git a/nova/tests/unit/api/openstack/compute/test_v3_auth.py b/nova/tests/unit/api/openstack/compute/test_v3_auth.py
new file mode 100644
index 0000000000..e728fa89d6
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_v3_auth.py
@@ -0,0 +1,62 @@
+# Copyright 2013 IBM Corp.
+# Copyright 2010 OpenStack Foundation
+# 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
+import webob.dec
+
+from nova import context
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+class TestNoAuthMiddlewareV3(test.NoDBTestCase):
+
+ def setUp(self):
+ super(TestNoAuthMiddlewareV3, self).setUp()
+ self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext)
+ fakes.stub_out_rate_limiting(self.stubs)
+ fakes.stub_out_networking(self.stubs)
+
+ def test_authorize_user(self):
+ req = webob.Request.blank('/v2/fake')
+ req.headers['X-Auth-User'] = 'user1'
+ req.headers['X-Auth-Key'] = 'user1_key'
+ req.headers['X-Auth-Project-Id'] = 'user1_project'
+ result = req.get_response(fakes.wsgi_app_v21(use_no_auth=True))
+ self.assertEqual(result.status, '204 No Content')
+ self.assertEqual(result.headers['X-Server-Management-Url'],
+ "http://localhost/v2/fake")
+
+ def test_authorize_user_trailing_slash(self):
+ # make sure it works with trailing slash on the request
+ req = webob.Request.blank('/v2/fake/')
+ req.headers['X-Auth-User'] = 'user1'
+ req.headers['X-Auth-Key'] = 'user1_key'
+ req.headers['X-Auth-Project-Id'] = 'user1_project'
+ result = req.get_response(fakes.wsgi_app_v21(use_no_auth=True))
+ self.assertEqual(result.status, '204 No Content')
+ self.assertEqual(result.headers['X-Server-Management-Url'],
+ "http://localhost/v2/fake")
+
+ def test_auth_token_no_empty_headers(self):
+ req = webob.Request.blank('/v2/fake')
+ req.headers['X-Auth-User'] = 'user1'
+ req.headers['X-Auth-Key'] = 'user1_key'
+ req.headers['X-Auth-Project-Id'] = 'user1_project'
+ result = req.get_response(fakes.wsgi_app_v21(use_no_auth=True))
+ self.assertEqual(result.status, '204 No Content')
+ self.assertNotIn('X-CDN-Management-Url', result.headers)
+ self.assertNotIn('X-Storage-Url', result.headers)
diff --git a/nova/tests/unit/api/openstack/compute/test_v3_extensions.py b/nova/tests/unit/api/openstack/compute/test_v3_extensions.py
new file mode 100644
index 0000000000..da6aa43d7f
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_v3_extensions.py
@@ -0,0 +1,194 @@
+# Copyright 2013 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo.config import cfg
+import stevedore
+import webob.exc
+
+from nova.api import openstack
+from nova.api.openstack import compute
+from nova.api.openstack.compute import plugins
+from nova.api.openstack import extensions
+from nova import exception
+from nova import test
+
+CONF = cfg.CONF
+
+
+class fake_bad_extension(object):
+ name = "fake_bad_extension"
+ alias = "fake-bad"
+
+
+class fake_stevedore_enabled_extensions(object):
+ def __init__(self, namespace, check_func, invoke_on_load=False,
+ invoke_args=(), invoke_kwds=None):
+ self.extensions = []
+
+ def map(self, func, *args, **kwds):
+ pass
+
+ def __iter__(self):
+ return iter(self.extensions)
+
+
+class fake_loaded_extension_info(object):
+ def __init__(self):
+ self.extensions = {}
+
+ def register_extension(self, ext):
+ self.extensions[ext] = ext
+ return True
+
+ def get_extensions(self):
+ return {'core1': None, 'core2': None, 'noncore1': None}
+
+
+class ExtensionLoadingTestCase(test.NoDBTestCase):
+
+ def _set_v3_core(self, core_extensions):
+ openstack.API_V3_CORE_EXTENSIONS = core_extensions
+
+ def test_extensions_loaded(self):
+ app = compute.APIRouterV3()
+ self.assertIn('servers', app._loaded_extension_info.extensions)
+
+ def test_check_bad_extension(self):
+ extension_info = plugins.LoadedExtensionInfo()
+ self.assertFalse(extension_info._check_extension(fake_bad_extension))
+
+ def test_extensions_blacklist(self):
+ app = compute.APIRouterV3()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+ CONF.set_override('extensions_blacklist', ['os-hosts'], 'osapi_v3')
+ app = compute.APIRouterV3()
+ self.assertNotIn('os-hosts', app._loaded_extension_info.extensions)
+
+ def test_extensions_whitelist_accept(self):
+ # NOTE(maurosr): just to avoid to get an exception raised for not
+ # loading all core api.
+ v3_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['servers'])
+ self.addCleanup(self._set_v3_core, v3_core)
+
+ app = compute.APIRouterV3()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+ CONF.set_override('extensions_whitelist', ['servers', 'os-hosts'],
+ 'osapi_v3')
+ app = compute.APIRouterV3()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+
+ def test_extensions_whitelist_block(self):
+ # NOTE(maurosr): just to avoid to get an exception raised for not
+ # loading all core api.
+ v3_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['servers'])
+ self.addCleanup(self._set_v3_core, v3_core)
+
+ app = compute.APIRouterV3()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+ CONF.set_override('extensions_whitelist', ['servers'], 'osapi_v3')
+ app = compute.APIRouterV3()
+ self.assertNotIn('os-hosts', app._loaded_extension_info.extensions)
+
+ def test_blacklist_overrides_whitelist(self):
+ # NOTE(maurosr): just to avoid to get an exception raised for not
+ # loading all core api.
+ v3_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['servers'])
+ self.addCleanup(self._set_v3_core, v3_core)
+
+ app = compute.APIRouterV3()
+ self.assertIn('os-hosts', app._loaded_extension_info.extensions)
+ CONF.set_override('extensions_whitelist', ['servers', 'os-hosts'],
+ 'osapi_v3')
+ CONF.set_override('extensions_blacklist', ['os-hosts'], 'osapi_v3')
+ app = compute.APIRouterV3()
+ self.assertNotIn('os-hosts', app._loaded_extension_info.extensions)
+ self.assertIn('servers', app._loaded_extension_info.extensions)
+ self.assertEqual(len(app._loaded_extension_info.extensions), 1)
+
+ def test_get_missing_core_extensions(self):
+ v3_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['core1', 'core2'])
+ self.addCleanup(self._set_v3_core, v3_core)
+ self.assertEqual(len(compute.APIRouterV3.get_missing_core_extensions(
+ ['core1', 'core2', 'noncore1'])), 0)
+ missing_core = compute.APIRouterV3.get_missing_core_extensions(
+ ['core1'])
+ self.assertEqual(len(missing_core), 1)
+ self.assertIn('core2', missing_core)
+ missing_core = compute.APIRouterV3.get_missing_core_extensions([])
+ self.assertEqual(len(missing_core), 2)
+ self.assertIn('core1', missing_core)
+ self.assertIn('core2', missing_core)
+ missing_core = compute.APIRouterV3.get_missing_core_extensions(
+ ['noncore1'])
+ self.assertEqual(len(missing_core), 2)
+ self.assertIn('core1', missing_core)
+ self.assertIn('core2', missing_core)
+
+ def test_core_extensions_present(self):
+ self.stubs.Set(stevedore.enabled, 'EnabledExtensionManager',
+ fake_stevedore_enabled_extensions)
+ self.stubs.Set(plugins, 'LoadedExtensionInfo',
+ fake_loaded_extension_info)
+ v3_core = openstack.API_V3_CORE_EXTENSIONS
+ openstack.API_V3_CORE_EXTENSIONS = set(['core1', 'core2'])
+ self.addCleanup(self._set_v3_core, v3_core)
+ # if no core API extensions are missing then an exception will
+ # not be raised when creating an instance of compute.APIRouterV3
+ compute.APIRouterV3()
+
+ def test_core_extensions_missing(self):
+ self.stubs.Set(stevedore.enabled, 'EnabledExtensionManager',
+ fake_stevedore_enabled_extensions)
+ self.stubs.Set(plugins, 'LoadedExtensionInfo',
+ fake_loaded_extension_info)
+ self.assertRaises(exception.CoreAPIMissing, compute.APIRouterV3)
+
+ def test_extensions_expected_error(self):
+ @extensions.expected_errors(404)
+ def fake_func():
+ raise webob.exc.HTTPNotFound()
+
+ self.assertRaises(webob.exc.HTTPNotFound, fake_func)
+
+ def test_extensions_expected_error_from_list(self):
+ @extensions.expected_errors((404, 403))
+ def fake_func():
+ raise webob.exc.HTTPNotFound()
+
+ self.assertRaises(webob.exc.HTTPNotFound, fake_func)
+
+ def test_extensions_unexpected_error(self):
+ @extensions.expected_errors(404)
+ def fake_func():
+ raise webob.exc.HTTPConflict()
+
+ self.assertRaises(webob.exc.HTTPInternalServerError, fake_func)
+
+ def test_extensions_unexpected_error_from_list(self):
+ @extensions.expected_errors((404, 413))
+ def fake_func():
+ raise webob.exc.HTTPConflict()
+
+ self.assertRaises(webob.exc.HTTPInternalServerError, fake_func)
+
+ def test_extensions_unexpected_policy_not_authorized_error(self):
+ @extensions.expected_errors(404)
+ def fake_func():
+ raise exception.PolicyNotAuthorized(action="foo")
+
+ self.assertRaises(exception.PolicyNotAuthorized, fake_func)
diff --git a/nova/tests/unit/api/openstack/compute/test_versions.py b/nova/tests/unit/api/openstack/compute/test_versions.py
new file mode 100644
index 0000000000..fabd15e01c
--- /dev/null
+++ b/nova/tests/unit/api/openstack/compute/test_versions.py
@@ -0,0 +1,797 @@
+# Copyright 2010-2011 OpenStack Foundation
+# 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 copy
+import uuid as stdlib_uuid
+
+import feedparser
+from lxml import etree
+from oslo.serialization import jsonutils
+import webob
+
+from nova.api.openstack.compute import versions
+from nova.api.openstack.compute import views
+from nova.api.openstack import xmlutil
+from nova import test
+from nova.tests.unit.api.openstack import common
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import matchers
+
+
+NS = {
+ 'atom': 'http://www.w3.org/2005/Atom',
+ 'ns': 'http://docs.openstack.org/common/api/v1.0'
+}
+
+
+EXP_LINKS = {
+ 'v2.0': {
+ 'html': 'http://docs.openstack.org/',
+ },
+ 'v2.1': {
+ 'html': 'http://docs.openstack.org/'
+ },
+}
+
+
+EXP_VERSIONS = {
+ "v2.0": {
+ "id": "v2.0",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "describedby",
+ "type": "text/html",
+ "href": EXP_LINKS['v2.0']['html'],
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute+xml;version=2",
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute+json;version=2",
+ },
+ ],
+ },
+ "v2.1": {
+ "id": "v2.1",
+ "status": "EXPERIMENTAL",
+ "updated": "2013-07-23T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2.1/",
+ },
+ {
+ "rel": "describedby",
+ "type": "text/html",
+ "href": EXP_LINKS['v2.1']['html'],
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute+json;version=2.1",
+ }
+ ],
+ }
+}
+
+
+class VersionsTestV20(test.NoDBTestCase):
+
+ def test_get_version_list(self):
+ req = webob.Request.blank('/')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ versions = jsonutils.loads(res.body)["versions"]
+ expected = [
+ {
+ "id": "v2.0",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/",
+ }],
+ },
+ {
+ "id": "v2.1",
+ "status": "EXPERIMENTAL",
+ "updated": "2013-07-23T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/",
+ }],
+ },
+ ]
+ self.assertEqual(versions, expected)
+
+ def test_get_version_list_302(self):
+ req = webob.Request.blank('/v2')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 302)
+ redirect_req = webob.Request.blank('/v2/')
+ self.assertEqual(res.location, redirect_req.url)
+
+ def _test_get_version_2_detail(self, url, accept=None):
+ if accept is None:
+ accept = "application/json"
+ req = webob.Request.blank(url)
+ req.accept = accept
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ version = jsonutils.loads(res.body)
+ expected = {
+ "version": {
+ "id": "v2.0",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/",
+ },
+ {
+ "rel": "describedby",
+ "type": "text/html",
+ "href": EXP_LINKS['v2.0']['html'],
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/"
+ "vnd.openstack.compute+xml;version=2",
+ },
+ {
+ "base": "application/json",
+ "type": "application/"
+ "vnd.openstack.compute+json;version=2",
+ },
+ ],
+ },
+ }
+ self.assertEqual(expected, version)
+
+ def test_get_version_2_detail(self):
+ self._test_get_version_2_detail('/v2/')
+
+ def test_get_version_2_detail_content_type(self):
+ accept = "application/json;version=2"
+ self._test_get_version_2_detail('/', accept=accept)
+
+ def test_get_version_2_versions_invalid(self):
+ req = webob.Request.blank('/v2/versions/1234')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(404, res.status_int)
+
+ def test_get_version_2_detail_xml(self):
+ req = webob.Request.blank('/v2/')
+ req.accept = "application/xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/xml")
+
+ version = etree.XML(res.body)
+ xmlutil.validate_schema(version, 'version')
+
+ expected = EXP_VERSIONS['v2.0']
+ self.assertTrue(version.xpath('/ns:version', namespaces=NS))
+ media_types = version.xpath('ns:media-types/ns:media-type',
+ namespaces=NS)
+ self.assertTrue(common.compare_media_types(media_types,
+ expected['media-types']))
+ for key in ['id', 'status', 'updated']:
+ self.assertEqual(version.get(key), expected[key])
+ links = version.xpath('atom:link', namespaces=NS)
+ self.assertTrue(common.compare_links(links,
+ [{'rel': 'self', 'href': 'http://localhost/v2/'}]
+ + expected['links']))
+
+ def test_get_version_list_xml(self):
+ req = webob.Request.blank('/')
+ req.accept = "application/xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/xml")
+
+ root = etree.XML(res.body)
+ xmlutil.validate_schema(root, 'versions')
+
+ self.assertTrue(root.xpath('/ns:versions', namespaces=NS))
+ versions = root.xpath('ns:version', namespaces=NS)
+ self.assertEqual(len(versions), 2)
+
+ for i, v in enumerate(['v2.0', 'v2.1']):
+ version = versions[i]
+ expected = EXP_VERSIONS[v]
+ for key in ['id', 'status', 'updated']:
+ self.assertEqual(version.get(key), expected[key])
+ (link,) = version.xpath('atom:link', namespaces=NS)
+ self.assertTrue(common.compare_links(link,
+ [{'rel': 'self', 'href': 'http://localhost/%s/' % v}]))
+
+ def test_get_version_2_detail_atom(self):
+ req = webob.Request.blank('/v2/')
+ req.accept = "application/atom+xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual("application/atom+xml", res.content_type)
+
+ xmlutil.validate_schema(etree.XML(res.body), 'atom')
+
+ f = feedparser.parse(res.body)
+ self.assertEqual(f.feed.title, 'About This Version')
+ self.assertEqual(f.feed.updated, '2011-01-21T11:33:21Z')
+ self.assertEqual(f.feed.id, 'http://localhost/v2/')
+ self.assertEqual(f.feed.author, 'Rackspace')
+ self.assertEqual(f.feed.author_detail.href,
+ 'http://www.rackspace.com/')
+ self.assertEqual(f.feed.links[0]['href'], 'http://localhost/v2/')
+ self.assertEqual(f.feed.links[0]['rel'], 'self')
+
+ self.assertEqual(len(f.entries), 1)
+ entry = f.entries[0]
+ self.assertEqual(entry.id, 'http://localhost/v2/')
+ self.assertEqual(entry.title, 'Version v2.0')
+ self.assertEqual(entry.updated, '2011-01-21T11:33:21Z')
+ self.assertEqual(len(entry.content), 1)
+ self.assertEqual(entry.content[0].value,
+ 'Version v2.0 CURRENT (2011-01-21T11:33:21Z)')
+ self.assertEqual(len(entry.links), 2)
+ self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/')
+ self.assertEqual(entry.links[0]['rel'], 'self')
+ self.assertEqual(entry.links[1], {
+ 'href': EXP_LINKS['v2.0']['html'],
+ 'type': 'text/html',
+ 'rel': 'describedby'})
+
+ def test_get_version_list_atom(self):
+ req = webob.Request.blank('/')
+ req.accept = "application/atom+xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/atom+xml")
+
+ f = feedparser.parse(res.body)
+ self.assertEqual(f.feed.title, 'Available API Versions')
+ self.assertEqual(f.feed.updated, '2013-07-23T11:33:21Z')
+ self.assertEqual(f.feed.id, 'http://localhost/')
+ self.assertEqual(f.feed.author, 'Rackspace')
+ self.assertEqual(f.feed.author_detail.href,
+ 'http://www.rackspace.com/')
+ self.assertEqual(f.feed.links[0]['href'], 'http://localhost/')
+ self.assertEqual(f.feed.links[0]['rel'], 'self')
+
+ self.assertEqual(len(f.entries), 2)
+ entry = f.entries[0]
+ self.assertEqual(entry.id, 'http://localhost/v2/')
+ self.assertEqual(entry.title, 'Version v2.0')
+ self.assertEqual(entry.updated, '2011-01-21T11:33:21Z')
+ self.assertEqual(len(entry.content), 1)
+ self.assertEqual(entry.content[0].value,
+ 'Version v2.0 CURRENT (2011-01-21T11:33:21Z)')
+ self.assertEqual(len(entry.links), 1)
+ self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/')
+ self.assertEqual(entry.links[0]['rel'], 'self')
+
+ entry = f.entries[1]
+ self.assertEqual(entry.id, 'http://localhost/v2/')
+ self.assertEqual(entry.title, 'Version v2.1')
+ self.assertEqual(entry.updated, '2013-07-23T11:33:21Z')
+ self.assertEqual(len(entry.content), 1)
+ self.assertEqual(entry.content[0].value,
+ 'Version v2.1 EXPERIMENTAL (2013-07-23T11:33:21Z)')
+ self.assertEqual(len(entry.links), 1)
+ self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/')
+ self.assertEqual(entry.links[0]['rel'], 'self')
+
+ def test_multi_choice_image(self):
+ req = webob.Request.blank('/images/1')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 300)
+ self.assertEqual(res.content_type, "application/json")
+
+ expected = {
+ "choices": [
+ {
+ "id": "v2.0",
+ "status": "CURRENT",
+ "links": [
+ {
+ "href": "http://localhost/v2/images/1",
+ "rel": "self",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute+xml"
+ ";version=2"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute+json"
+ ";version=2"
+ },
+ ],
+ },
+ {
+ "id": "v2.1",
+ "status": "EXPERIMENTAL",
+ "links": [
+ {
+ "href": "http://localhost/v2/images/1",
+ "rel": "self",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/json",
+ "type":
+ "application/vnd.openstack.compute+json;version=2.1",
+ }
+ ],
+ },
+ ], }
+
+ self.assertThat(jsonutils.loads(res.body),
+ matchers.DictMatches(expected))
+
+ def test_multi_choice_image_xml(self):
+ req = webob.Request.blank('/images/1')
+ req.accept = "application/xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 300)
+ self.assertEqual(res.content_type, "application/xml")
+
+ root = etree.XML(res.body)
+ self.assertTrue(root.xpath('/ns:choices', namespaces=NS))
+ versions = root.xpath('ns:version', namespaces=NS)
+ self.assertEqual(len(versions), 2)
+
+ version = versions[0]
+ self.assertEqual(version.get('id'), 'v2.0')
+ self.assertEqual(version.get('status'), 'CURRENT')
+ media_types = version.xpath('ns:media-types/ns:media-type',
+ namespaces=NS)
+ self.assertTrue(common.
+ compare_media_types(media_types,
+ EXP_VERSIONS['v2.0']['media-types']
+ ))
+
+ links = version.xpath('atom:link', namespaces=NS)
+ self.assertTrue(common.compare_links(links,
+ [{'rel': 'self', 'href': 'http://localhost/v2/images/1'}]))
+
+ version = versions[1]
+ self.assertEqual(version.get('id'), 'v2.1')
+ self.assertEqual(version.get('status'), 'EXPERIMENTAL')
+ media_types = version.xpath('ns:media-types/ns:media-type',
+ namespaces=NS)
+ self.assertTrue(common.
+ compare_media_types(media_types,
+ EXP_VERSIONS['v2.1']['media-types']
+ ))
+
+ links = version.xpath('atom:link', namespaces=NS)
+ self.assertTrue(common.compare_links(links,
+ [{'rel': 'self', 'href': 'http://localhost/v2/images/1'}]))
+
+ def test_multi_choice_server_atom(self):
+ """Make sure multi choice responses do not have content-type
+ application/atom+xml (should use default of json)
+ """
+ req = webob.Request.blank('/servers')
+ req.accept = "application/atom+xml"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 300)
+ self.assertEqual(res.content_type, "application/json")
+
+ def test_multi_choice_server(self):
+ uuid = str(stdlib_uuid.uuid4())
+ req = webob.Request.blank('/servers/' + uuid)
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 300)
+ self.assertEqual(res.content_type, "application/json")
+
+ expected = {
+ "choices": [
+ {
+ "id": "v2.0",
+ "status": "CURRENT",
+ "links": [
+ {
+ "href": "http://localhost/v2/servers/" + uuid,
+ "rel": "self",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute+xml"
+ ";version=2"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute+json"
+ ";version=2"
+ },
+ ],
+ },
+ {
+ "id": "v2.1",
+ "status": "EXPERIMENTAL",
+ "links": [
+ {
+ "href": "http://localhost/v2/servers/" + uuid,
+ "rel": "self",
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/json",
+ "type":
+ "application/vnd.openstack.compute+json;version=2.1",
+ }
+ ],
+ },
+ ], }
+
+ self.assertThat(jsonutils.loads(res.body),
+ matchers.DictMatches(expected))
+
+
+class VersionsViewBuilderTests(test.NoDBTestCase):
+ def test_view_builder(self):
+ base_url = "http://example.org/"
+
+ version_data = {
+ "v3.2.1": {
+ "id": "3.2.1",
+ "status": "CURRENT",
+ "updated": "2011-07-18T11:30:00Z",
+ }
+ }
+
+ expected = {
+ "versions": [
+ {
+ "id": "3.2.1",
+ "status": "CURRENT",
+ "updated": "2011-07-18T11:30:00Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://example.org/v2/",
+ },
+ ],
+ }
+ ]
+ }
+
+ builder = views.versions.ViewBuilder(base_url)
+ output = builder.build_versions(version_data)
+
+ self.assertEqual(output, expected)
+
+ def test_generate_href(self):
+ base_url = "http://example.org/app/"
+
+ expected = "http://example.org/app/v2/"
+
+ builder = views.versions.ViewBuilder(base_url)
+ actual = builder.generate_href('v2')
+
+ self.assertEqual(actual, expected)
+
+ def test_generate_href_v21(self):
+ base_url = "http://example.org/app/"
+
+ expected = "http://example.org/app/v2/"
+
+ builder = views.versions.ViewBuilder(base_url)
+ actual = builder.generate_href('v2.1')
+
+ self.assertEqual(actual, expected)
+
+ def test_generate_href_unknown(self):
+ base_url = "http://example.org/app/"
+
+ expected = "http://example.org/app/v2/"
+
+ builder = views.versions.ViewBuilder(base_url)
+ actual = builder.generate_href('foo')
+
+ self.assertEqual(actual, expected)
+
+
+class VersionsSerializerTests(test.NoDBTestCase):
+ def test_versions_list_xml_serializer(self):
+ versions_data = {
+ 'versions': [
+ {
+ "id": "2.7",
+ "updated": "2011-07-18T11:30:00Z",
+ "status": "DEPRECATED",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://test/v2",
+ },
+ ],
+ },
+ ]
+ }
+
+ serializer = versions.VersionsTemplate()
+ response = serializer.serialize(versions_data)
+
+ root = etree.XML(response)
+ xmlutil.validate_schema(root, 'versions')
+
+ self.assertTrue(root.xpath('/ns:versions', namespaces=NS))
+ version_elems = root.xpath('ns:version', namespaces=NS)
+ self.assertEqual(len(version_elems), 1)
+ version = version_elems[0]
+ self.assertEqual(version.get('id'), versions_data['versions'][0]['id'])
+ self.assertEqual(version.get('status'),
+ versions_data['versions'][0]['status'])
+
+ (link,) = version.xpath('atom:link', namespaces=NS)
+ self.assertTrue(common.compare_links(link, [{
+ 'rel': 'self',
+ 'href': 'http://test/v2',
+ 'type': 'application/atom+xml'}]))
+
+ def test_versions_multi_xml_serializer(self):
+ versions_data = {
+ 'choices': [
+ {
+ "id": "2.7",
+ "updated": "2011-07-18T11:30:00Z",
+ "status": "DEPRECATED",
+ "media-types": EXP_VERSIONS['v2.0']['media-types'],
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://test/v2/images",
+ },
+ ],
+ },
+ ]
+ }
+
+ serializer = versions.ChoicesTemplate()
+ response = serializer.serialize(versions_data)
+
+ root = etree.XML(response)
+ self.assertTrue(root.xpath('/ns:choices', namespaces=NS))
+ (version,) = root.xpath('ns:version', namespaces=NS)
+ self.assertEqual(version.get('id'), versions_data['choices'][0]['id'])
+ self.assertEqual(version.get('status'),
+ versions_data['choices'][0]['status'])
+
+ media_types = list(version)[0]
+ self.assertEqual(media_types.tag.split('}')[1], "media-types")
+
+ media_types = version.xpath('ns:media-types/ns:media-type',
+ namespaces=NS)
+ self.assertTrue(common.compare_media_types(media_types,
+ versions_data['choices'][0]['media-types']))
+
+ (link,) = version.xpath('atom:link', namespaces=NS)
+ self.assertTrue(common.compare_links(link,
+ versions_data['choices'][0]['links']))
+
+ def test_versions_list_atom_serializer(self):
+ versions_data = {
+ 'versions': [
+ {
+ "id": "2.9.8",
+ "updated": "2011-07-20T11:40:00Z",
+ "status": "CURRENT",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://test/2.9.8",
+ },
+ ],
+ },
+ ]
+ }
+
+ serializer = versions.VersionsAtomSerializer()
+ response = serializer.serialize(versions_data)
+ f = feedparser.parse(response)
+
+ self.assertEqual(f.feed.title, 'Available API Versions')
+ self.assertEqual(f.feed.updated, '2011-07-20T11:40:00Z')
+ self.assertEqual(f.feed.id, 'http://test/')
+ self.assertEqual(f.feed.author, 'Rackspace')
+ self.assertEqual(f.feed.author_detail.href,
+ 'http://www.rackspace.com/')
+ self.assertEqual(f.feed.links[0]['href'], 'http://test/')
+ self.assertEqual(f.feed.links[0]['rel'], 'self')
+
+ self.assertEqual(len(f.entries), 1)
+ entry = f.entries[0]
+ self.assertEqual(entry.id, 'http://test/2.9.8')
+ self.assertEqual(entry.title, 'Version 2.9.8')
+ self.assertEqual(entry.updated, '2011-07-20T11:40:00Z')
+ self.assertEqual(len(entry.content), 1)
+ self.assertEqual(entry.content[0].value,
+ 'Version 2.9.8 CURRENT (2011-07-20T11:40:00Z)')
+ self.assertEqual(len(entry.links), 1)
+ self.assertEqual(entry.links[0]['href'], 'http://test/2.9.8')
+ self.assertEqual(entry.links[0]['rel'], 'self')
+
+ def test_version_detail_atom_serializer(self):
+ versions_data = {
+ "version": {
+ "id": "v2.0",
+ "status": "CURRENT",
+ "updated": "2011-01-21T11:33:21Z",
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://localhost/v2/",
+ },
+ {
+ "rel": "describedby",
+ "type": "text/html",
+ "href": EXP_LINKS['v2.0']['html'],
+ },
+ ],
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.compute+xml"
+ ";version=2",
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.compute+json"
+ ";version=2",
+ }
+ ],
+ },
+ }
+
+ serializer = versions.VersionAtomSerializer()
+ response = serializer.serialize(versions_data)
+ f = feedparser.parse(response)
+
+ self.assertEqual(f.feed.title, 'About This Version')
+ self.assertEqual(f.feed.updated, '2011-01-21T11:33:21Z')
+ self.assertEqual(f.feed.id, 'http://localhost/v2/')
+ self.assertEqual(f.feed.author, 'Rackspace')
+ self.assertEqual(f.feed.author_detail.href,
+ 'http://www.rackspace.com/')
+ self.assertEqual(f.feed.links[0]['href'], 'http://localhost/v2/')
+ self.assertEqual(f.feed.links[0]['rel'], 'self')
+
+ self.assertEqual(len(f.entries), 1)
+ entry = f.entries[0]
+ self.assertEqual(entry.id, 'http://localhost/v2/')
+ self.assertEqual(entry.title, 'Version v2.0')
+ self.assertEqual(entry.updated, '2011-01-21T11:33:21Z')
+ self.assertEqual(len(entry.content), 1)
+ self.assertEqual(entry.content[0].value,
+ 'Version v2.0 CURRENT (2011-01-21T11:33:21Z)')
+ self.assertEqual(len(entry.links), 2)
+ self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/')
+ self.assertEqual(entry.links[0]['rel'], 'self')
+ self.assertEqual(entry.links[1], {
+ 'rel': 'describedby',
+ 'type': 'text/html',
+ 'href': EXP_LINKS['v2.0']['html']})
+
+ def test_multi_choice_image_with_body(self):
+ req = webob.Request.blank('/images/1')
+ req.accept = "application/json"
+ req.method = 'POST'
+ req.content_type = "application/json"
+ req.body = "{\"foo\": \"bar\"}"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(300, res.status_int)
+ self.assertEqual("application/json", res.content_type)
+
+ def test_get_version_list_with_body(self):
+ req = webob.Request.blank('/')
+ req.accept = "application/json"
+ req.method = 'POST'
+ req.content_type = "application/json"
+ req.body = "{\"foo\": \"bar\"}"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(200, res.status_int)
+ self.assertEqual("application/json", res.content_type)
+
+
+# NOTE(oomichi): Now version API of v2.0 covers "/"(root).
+# So this class tests "/v2.1" only for v2.1 API.
+class VersionsTestV21(test.NoDBTestCase):
+ exp_versions = copy.deepcopy(EXP_VERSIONS)
+ exp_versions['v2.0']['links'].insert(0,
+ {'href': 'http://localhost/v2.1/', 'rel': 'self'},
+ )
+
+ def test_get_version_list_302(self):
+ req = webob.Request.blank('/v2.1')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app_v21())
+ self.assertEqual(res.status_int, 302)
+ redirect_req = webob.Request.blank('/v2.1/')
+ self.assertEqual(res.location, redirect_req.url)
+
+ def test_get_version_21_detail(self):
+ req = webob.Request.blank('/v2.1/')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app_v21())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ version = jsonutils.loads(res.body)
+ expected = {"version": self.exp_versions['v2.1']}
+ self.assertEqual(expected, version)
+
+ def test_get_version_21_versions_v21_detail(self):
+ req = webob.Request.blank('/v2.1/fake/versions/v2.1')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app_v21())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ version = jsonutils.loads(res.body)
+ expected = {"version": self.exp_versions['v2.1']}
+ self.assertEqual(expected, version)
+
+ def test_get_version_21_versions_v20_detail(self):
+ req = webob.Request.blank('/v2.1/fake/versions/v2.0')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app_v21())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ version = jsonutils.loads(res.body)
+ expected = {"version": self.exp_versions['v2.0']}
+ self.assertEqual(expected, version)
+
+ def test_get_version_21_versions_invalid(self):
+ req = webob.Request.blank('/v2.1/versions/1234')
+ req.accept = "application/json"
+ res = req.get_response(fakes.wsgi_app_v21())
+ self.assertEqual(res.status_int, 404)
+
+ def test_get_version_21_detail_content_type(self):
+ req = webob.Request.blank('/')
+ req.accept = "application/json;version=2.1"
+ res = req.get_response(fakes.wsgi_app_v21())
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res.content_type, "application/json")
+ version = jsonutils.loads(res.body)
+ expected = {"version": self.exp_versions['v2.1']}
+ self.assertEqual(expected, version)
diff --git a/nova/tests/unit/api/openstack/fakes.py b/nova/tests/unit/api/openstack/fakes.py
new file mode 100644
index 0000000000..34c072a634
--- /dev/null
+++ b/nova/tests/unit/api/openstack/fakes.py
@@ -0,0 +1,662 @@
+# Copyright 2010 OpenStack Foundation
+# 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 datetime
+import uuid
+
+from oslo.serialization import jsonutils
+from oslo.utils import timeutils
+import routes
+import six
+import webob
+import webob.dec
+import webob.request
+
+from nova.api import auth as api_auth
+from nova.api import openstack as openstack_api
+from nova.api.openstack import auth
+from nova.api.openstack import compute
+from nova.api.openstack.compute import limits
+from nova.api.openstack.compute import versions
+from nova.api.openstack import urlmap
+from nova.api.openstack import wsgi as os_wsgi
+from nova.compute import api as compute_api
+from nova.compute import flavors
+from nova.compute import vm_states
+from nova import context
+from nova.db.sqlalchemy import models
+from nova import exception as exc
+import nova.netconf
+from nova.network import api as network_api
+from nova import quota
+from nova.tests.unit import fake_block_device
+from nova.tests.unit import fake_network
+from nova.tests.unit.objects import test_keypair
+from nova import utils
+from nova import wsgi
+
+
+QUOTAS = quota.QUOTAS
+
+
+FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
+FAKE_UUIDS = {}
+
+
+class Context(object):
+ pass
+
+
+class FakeRouter(wsgi.Router):
+ def __init__(self, ext_mgr=None):
+ pass
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ res = webob.Response()
+ res.status = '200'
+ res.headers['X-Test-Success'] = 'True'
+ return res
+
+
+@webob.dec.wsgify
+def fake_wsgi(self, req):
+ return self.application
+
+
+def wsgi_app(inner_app_v2=None, fake_auth_context=None,
+ use_no_auth=False, ext_mgr=None, init_only=None):
+ if not inner_app_v2:
+ inner_app_v2 = compute.APIRouter(ext_mgr, init_only)
+
+ if use_no_auth:
+ api_v2 = openstack_api.FaultWrapper(auth.NoAuthMiddleware(
+ limits.RateLimitingMiddleware(inner_app_v2)))
+ else:
+ if fake_auth_context is not None:
+ ctxt = fake_auth_context
+ else:
+ ctxt = context.RequestContext('fake', 'fake', auth_token=True)
+ api_v2 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt,
+ limits.RateLimitingMiddleware(inner_app_v2)))
+
+ mapper = urlmap.URLMap()
+ mapper['/v2'] = api_v2
+ mapper['/v1.1'] = api_v2
+ mapper['/'] = openstack_api.FaultWrapper(versions.Versions())
+ return mapper
+
+
+def wsgi_app_v21(inner_app_v21=None, fake_auth_context=None,
+ use_no_auth=False, ext_mgr=None, init_only=None):
+ if not inner_app_v21:
+ inner_app_v21 = compute.APIRouterV21(init_only)
+
+ if use_no_auth:
+ api_v21 = openstack_api.FaultWrapper(auth.NoAuthMiddlewareV3(
+ limits.RateLimitingMiddleware(inner_app_v21)))
+ else:
+ if fake_auth_context is not None:
+ ctxt = fake_auth_context
+ else:
+ ctxt = context.RequestContext('fake', 'fake', auth_token=True)
+ api_v21 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt,
+ limits.RateLimitingMiddleware(inner_app_v21)))
+
+ mapper = urlmap.URLMap()
+ mapper['/v2'] = api_v21
+ mapper['/v2.1'] = api_v21
+ return mapper
+
+
+def stub_out_key_pair_funcs(stubs, have_key_pair=True):
+ def key_pair(context, user_id):
+ return [dict(test_keypair.fake_keypair,
+ name='key', public_key='public_key')]
+
+ def one_key_pair(context, user_id, name):
+ if name == 'key':
+ return dict(test_keypair.fake_keypair,
+ name='key', public_key='public_key')
+ else:
+ raise exc.KeypairNotFound(user_id=user_id, name=name)
+
+ def no_key_pair(context, user_id):
+ return []
+
+ if have_key_pair:
+ stubs.Set(nova.db, 'key_pair_get_all_by_user', key_pair)
+ stubs.Set(nova.db, 'key_pair_get', one_key_pair)
+ else:
+ stubs.Set(nova.db, 'key_pair_get_all_by_user', no_key_pair)
+
+
+def stub_out_rate_limiting(stubs):
+ def fake_rate_init(self, app):
+ super(limits.RateLimitingMiddleware, self).__init__(app)
+ self.application = app
+
+ stubs.Set(nova.api.openstack.compute.limits.RateLimitingMiddleware,
+ '__init__', fake_rate_init)
+
+ stubs.Set(nova.api.openstack.compute.limits.RateLimitingMiddleware,
+ '__call__', fake_wsgi)
+
+
+def stub_out_instance_quota(stubs, allowed, quota, resource='instances'):
+ def fake_reserve(context, **deltas):
+ requested = deltas.pop(resource, 0)
+ if requested > allowed:
+ quotas = dict(instances=1, cores=1, ram=1)
+ quotas[resource] = quota
+ usages = dict(instances=dict(in_use=0, reserved=0),
+ cores=dict(in_use=0, reserved=0),
+ ram=dict(in_use=0, reserved=0))
+ usages[resource]['in_use'] = (quotas[resource] * 0.9 -
+ allowed)
+ usages[resource]['reserved'] = quotas[resource] * 0.1
+ headroom = dict(
+ (res, value - (usages[res]['in_use'] + usages[res]['reserved']))
+ for res, value in quotas.iteritems()
+ )
+ raise exc.OverQuota(overs=[resource], quotas=quotas,
+ usages=usages, headroom=headroom)
+ stubs.Set(QUOTAS, 'reserve', fake_reserve)
+
+
+def stub_out_networking(stubs):
+ def get_my_ip():
+ return '127.0.0.1'
+ stubs.Set(nova.netconf, '_get_my_ip', get_my_ip)
+
+
+def stub_out_compute_api_snapshot(stubs):
+
+ def snapshot(self, context, instance, name, extra_properties=None):
+ # emulate glance rejecting image names which are too long
+ if len(name) > 256:
+ raise exc.Invalid
+ return dict(id='123', status='ACTIVE', name=name,
+ properties=extra_properties)
+
+ stubs.Set(compute_api.API, 'snapshot', snapshot)
+
+
+class stub_out_compute_api_backup(object):
+
+ def __init__(self, stubs):
+ self.stubs = stubs
+ self.extra_props_last_call = None
+ stubs.Set(compute_api.API, 'backup', self.backup)
+
+ def backup(self, context, instance, name, backup_type, rotation,
+ extra_properties=None):
+ self.extra_props_last_call = extra_properties
+ props = dict(backup_type=backup_type,
+ rotation=rotation)
+ props.update(extra_properties or {})
+ return dict(id='123', status='ACTIVE', name=name, properties=props)
+
+
+def stub_out_nw_api_get_instance_nw_info(stubs, num_networks=1, func=None):
+ fake_network.stub_out_nw_api_get_instance_nw_info(stubs)
+
+
+def stub_out_nw_api_get_floating_ips_by_fixed_address(stubs, func=None):
+ def get_floating_ips_by_fixed_address(self, context, fixed_ip):
+ return ['1.2.3.4']
+
+ if func is None:
+ func = get_floating_ips_by_fixed_address
+ stubs.Set(network_api.API, 'get_floating_ips_by_fixed_address', func)
+
+
+def stub_out_nw_api(stubs, cls=None, private=None, publics=None):
+ if not private:
+ private = '192.168.0.3'
+ if not publics:
+ publics = ['1.2.3.4']
+
+ class Fake:
+ def get_instance_nw_info(*args, **kwargs):
+ pass
+
+ def get_floating_ips_by_fixed_address(*args, **kwargs):
+ return publics
+
+ def validate_networks(self, context, networks, max_count):
+ return max_count
+
+ def create_pci_requests_for_sriov_ports(self, context,
+ system_metadata,
+ requested_networks):
+ pass
+
+ if cls is None:
+ cls = Fake
+ stubs.Set(network_api, 'API', cls)
+ fake_network.stub_out_nw_api_get_instance_nw_info(stubs)
+
+
+class FakeToken(object):
+ id_count = 0
+
+ def __getitem__(self, key):
+ return getattr(self, key)
+
+ def __init__(self, **kwargs):
+ FakeToken.id_count += 1
+ self.id = FakeToken.id_count
+ for k, v in kwargs.iteritems():
+ setattr(self, k, v)
+
+
+class FakeRequestContext(context.RequestContext):
+ def __init__(self, *args, **kwargs):
+ kwargs['auth_token'] = kwargs.get('auth_token', 'fake_auth_token')
+ return super(FakeRequestContext, self).__init__(*args, **kwargs)
+
+
+class HTTPRequest(os_wsgi.Request):
+
+ @staticmethod
+ def blank(*args, **kwargs):
+ kwargs['base_url'] = 'http://localhost/v2'
+ use_admin_context = kwargs.pop('use_admin_context', False)
+ out = os_wsgi.Request.blank(*args, **kwargs)
+ out.environ['nova.context'] = FakeRequestContext('fake_user', 'fake',
+ is_admin=use_admin_context)
+ return out
+
+
+class HTTPRequestV3(os_wsgi.Request):
+
+ @staticmethod
+ def blank(*args, **kwargs):
+ kwargs['base_url'] = 'http://localhost/v3'
+ use_admin_context = kwargs.pop('use_admin_context', False)
+ out = os_wsgi.Request.blank(*args, **kwargs)
+ out.environ['nova.context'] = FakeRequestContext('fake_user', 'fake',
+ is_admin=use_admin_context)
+ return out
+
+
+class TestRouter(wsgi.Router):
+ def __init__(self, controller, mapper=None):
+ if not mapper:
+ mapper = routes.Mapper()
+ mapper.resource("test", "tests",
+ controller=os_wsgi.Resource(controller))
+ super(TestRouter, self).__init__(mapper)
+
+
+class FakeAuthDatabase(object):
+ data = {}
+
+ @staticmethod
+ def auth_token_get(context, token_hash):
+ return FakeAuthDatabase.data.get(token_hash, None)
+
+ @staticmethod
+ def auth_token_create(context, token):
+ fake_token = FakeToken(created_at=timeutils.utcnow(), **token)
+ FakeAuthDatabase.data[fake_token.token_hash] = fake_token
+ FakeAuthDatabase.data['id_%i' % fake_token.id] = fake_token
+ return fake_token
+
+ @staticmethod
+ def auth_token_destroy(context, token_id):
+ token = FakeAuthDatabase.data.get('id_%i' % token_id)
+ if token and token.token_hash in FakeAuthDatabase.data:
+ del FakeAuthDatabase.data[token.token_hash]
+ del FakeAuthDatabase.data['id_%i' % token_id]
+
+
+class FakeRateLimiter(object):
+ def __init__(self, application):
+ self.application = application
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ return self.application
+
+
+def create_info_cache(nw_cache):
+ if nw_cache is None:
+ pub0 = ('192.168.1.100',)
+ pub1 = ('2001:db8:0:1::1',)
+
+ def _ip(ip):
+ return {'address': ip, 'type': 'fixed'}
+
+ nw_cache = [
+ {'address': 'aa:aa:aa:aa:aa:aa',
+ 'id': 1,
+ 'network': {'bridge': 'br0',
+ 'id': 1,
+ 'label': 'test1',
+ 'subnets': [{'cidr': '192.168.1.0/24',
+ 'ips': [_ip(ip) for ip in pub0]},
+ {'cidr': 'b33f::/64',
+ 'ips': [_ip(ip) for ip in pub1]}]}}]
+
+ if not isinstance(nw_cache, six.string_types):
+ nw_cache = jsonutils.dumps(nw_cache)
+
+ return {
+ "info_cache": {
+ "network_info": nw_cache,
+ "deleted": False,
+ "created_at": None,
+ "deleted_at": None,
+ "updated_at": None,
+ }
+ }
+
+
+def get_fake_uuid(token=0):
+ if token not in FAKE_UUIDS:
+ FAKE_UUIDS[token] = str(uuid.uuid4())
+ return FAKE_UUIDS[token]
+
+
+def fake_instance_get(**kwargs):
+ def _return_server(context, uuid, columns_to_join=None, use_slave=False):
+ return stub_instance(1, **kwargs)
+ return _return_server
+
+
+def fake_actions_to_locked_server(self, context, instance, *args, **kwargs):
+ raise exc.InstanceIsLocked(instance_uuid=instance['uuid'])
+
+
+def fake_instance_get_all_by_filters(num_servers=5, **kwargs):
+ def _return_servers(context, *args, **kwargs):
+ servers_list = []
+ marker = None
+ limit = None
+ found_marker = False
+ if "marker" in kwargs:
+ marker = kwargs["marker"]
+ if "limit" in kwargs:
+ limit = kwargs["limit"]
+
+ if 'columns_to_join' in kwargs:
+ kwargs.pop('columns_to_join')
+
+ if 'use_slave' in kwargs:
+ kwargs.pop('use_slave')
+
+ for i in xrange(num_servers):
+ uuid = get_fake_uuid(i)
+ server = stub_instance(id=i + 1, uuid=uuid,
+ **kwargs)
+ servers_list.append(server)
+ if marker is not None and uuid == marker:
+ found_marker = True
+ servers_list = []
+ if marker is not None and not found_marker:
+ raise exc.MarkerNotFound(marker=marker)
+ if limit is not None:
+ servers_list = servers_list[:limit]
+ return servers_list
+ return _return_servers
+
+
+def stub_instance(id, user_id=None, project_id=None, host=None,
+ node=None, vm_state=None, task_state=None,
+ reservation_id="", uuid=FAKE_UUID, image_ref="10",
+ flavor_id="1", name=None, key_name='',
+ access_ipv4=None, access_ipv6=None, progress=0,
+ auto_disk_config=False, display_name=None,
+ include_fake_metadata=True, config_drive=None,
+ power_state=None, nw_cache=None, metadata=None,
+ security_groups=None, root_device_name=None,
+ limit=None, marker=None,
+ launched_at=timeutils.utcnow(),
+ terminated_at=timeutils.utcnow(),
+ availability_zone='', locked_by=None, cleaned=False,
+ memory_mb=0, vcpus=0, root_gb=0, ephemeral_gb=0):
+ if user_id is None:
+ user_id = 'fake_user'
+ if project_id is None:
+ project_id = 'fake_project'
+
+ if metadata:
+ metadata = [{'key': k, 'value': v} for k, v in metadata.items()]
+ elif include_fake_metadata:
+ metadata = [models.InstanceMetadata(key='seq', value=str(id))]
+ else:
+ metadata = []
+
+ inst_type = flavors.get_flavor_by_flavor_id(int(flavor_id))
+ sys_meta = flavors.save_flavor_info({}, inst_type)
+
+ if host is not None:
+ host = str(host)
+
+ if key_name:
+ key_data = 'FAKE'
+ else:
+ key_data = ''
+
+ if security_groups is None:
+ security_groups = [{"id": 1, "name": "test", "description": "Foo:",
+ "project_id": "project", "user_id": "user",
+ "created_at": None, "updated_at": None,
+ "deleted_at": None, "deleted": False}]
+
+ # ReservationID isn't sent back, hack it in there.
+ server_name = name or "server%s" % id
+ if reservation_id != "":
+ server_name = "reservation_%s" % (reservation_id, )
+
+ info_cache = create_info_cache(nw_cache)
+
+ instance = {
+ "id": int(id),
+ "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
+ "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
+ "deleted_at": datetime.datetime(2010, 12, 12, 10, 0, 0),
+ "deleted": None,
+ "user_id": user_id,
+ "project_id": project_id,
+ "image_ref": image_ref,
+ "kernel_id": "",
+ "ramdisk_id": "",
+ "launch_index": 0,
+ "key_name": key_name,
+ "key_data": key_data,
+ "config_drive": config_drive,
+ "vm_state": vm_state or vm_states.BUILDING,
+ "task_state": task_state,
+ "power_state": power_state,
+ "memory_mb": memory_mb,
+ "vcpus": vcpus,
+ "root_gb": root_gb,
+ "ephemeral_gb": ephemeral_gb,
+ "ephemeral_key_uuid": None,
+ "hostname": display_name or server_name,
+ "host": host,
+ "node": node,
+ "instance_type_id": 1,
+ "instance_type": inst_type,
+ "user_data": "",
+ "reservation_id": reservation_id,
+ "mac_address": "",
+ "scheduled_at": timeutils.utcnow(),
+ "launched_at": launched_at,
+ "terminated_at": terminated_at,
+ "availability_zone": availability_zone,
+ "display_name": display_name or server_name,
+ "display_description": "",
+ "locked": locked_by is not None,
+ "locked_by": locked_by,
+ "metadata": metadata,
+ "access_ip_v4": access_ipv4,
+ "access_ip_v6": access_ipv6,
+ "uuid": uuid,
+ "progress": progress,
+ "auto_disk_config": auto_disk_config,
+ "name": "instance-%s" % id,
+ "shutdown_terminate": True,
+ "disable_terminate": False,
+ "security_groups": security_groups,
+ "root_device_name": root_device_name,
+ "system_metadata": utils.dict_to_metadata(sys_meta),
+ "pci_devices": [],
+ "vm_mode": "",
+ "default_swap_device": "",
+ "default_ephemeral_device": "",
+ "launched_on": "",
+ "cell_name": "",
+ "architecture": "",
+ "os_type": "",
+ "cleaned": cleaned}
+
+ instance.update(info_cache)
+ instance['info_cache']['instance_uuid'] = instance['uuid']
+
+ return instance
+
+
+def stub_volume(id, **kwargs):
+ volume = {
+ 'id': id,
+ 'user_id': 'fakeuser',
+ 'project_id': 'fakeproject',
+ 'host': 'fakehost',
+ 'size': 1,
+ 'availability_zone': 'fakeaz',
+ 'instance_uuid': 'fakeuuid',
+ 'mountpoint': '/',
+ 'status': 'fakestatus',
+ 'attach_status': 'attached',
+ 'name': 'vol name',
+ 'display_name': 'displayname',
+ 'display_description': 'displaydesc',
+ 'created_at': datetime.datetime(1999, 1, 1, 1, 1, 1),
+ 'snapshot_id': None,
+ 'volume_type_id': 'fakevoltype',
+ 'volume_metadata': [],
+ 'volume_type': {'name': 'vol_type_name'}}
+
+ volume.update(kwargs)
+ return volume
+
+
+def stub_volume_create(self, context, size, name, description, snapshot,
+ **param):
+ vol = stub_volume('1')
+ vol['size'] = size
+ vol['display_name'] = name
+ vol['display_description'] = description
+ try:
+ vol['snapshot_id'] = snapshot['id']
+ except (KeyError, TypeError):
+ vol['snapshot_id'] = None
+ vol['availability_zone'] = param.get('availability_zone', 'fakeaz')
+ return vol
+
+
+def stub_volume_update(self, context, *args, **param):
+ pass
+
+
+def stub_volume_delete(self, context, *args, **param):
+ pass
+
+
+def stub_volume_get(self, context, volume_id):
+ return stub_volume(volume_id)
+
+
+def stub_volume_notfound(self, context, volume_id):
+ raise exc.VolumeNotFound(volume_id=volume_id)
+
+
+def stub_volume_get_all(context, search_opts=None):
+ return [stub_volume(100, project_id='fake'),
+ stub_volume(101, project_id='superfake'),
+ stub_volume(102, project_id='superduperfake')]
+
+
+def stub_volume_check_attach(self, context, *args, **param):
+ pass
+
+
+def stub_snapshot(id, **kwargs):
+ snapshot = {
+ 'id': id,
+ 'volume_id': 12,
+ 'status': 'available',
+ 'volume_size': 100,
+ 'created_at': timeutils.utcnow(),
+ 'display_name': 'Default name',
+ 'display_description': 'Default description',
+ 'project_id': 'fake'
+ }
+
+ snapshot.update(kwargs)
+ return snapshot
+
+
+def stub_snapshot_create(self, context, volume_id, name, description):
+ return stub_snapshot(100, volume_id=volume_id, display_name=name,
+ display_description=description)
+
+
+def stub_compute_volume_snapshot_create(self, context, volume_id, create_info):
+ return {'snapshot': {'id': 100, 'volumeId': volume_id}}
+
+
+def stub_snapshot_delete(self, context, snapshot_id):
+ if snapshot_id == '-1':
+ raise exc.NotFound
+
+
+def stub_compute_volume_snapshot_delete(self, context, volume_id, snapshot_id,
+ delete_info):
+ pass
+
+
+def stub_snapshot_get(self, context, snapshot_id):
+ if snapshot_id == '-1':
+ raise exc.NotFound
+ return stub_snapshot(snapshot_id)
+
+
+def stub_snapshot_get_all(self, context):
+ return [stub_snapshot(100, project_id='fake'),
+ stub_snapshot(101, project_id='superfake'),
+ stub_snapshot(102, project_id='superduperfake')]
+
+
+def stub_bdm_get_all_by_instance(context, instance_uuid, use_slave=False):
+ return [fake_block_device.FakeDbBlockDeviceDict(
+ {'id': 1, 'source_type': 'volume', 'destination_type': 'volume',
+ 'volume_id': 'volume_id1', 'instance_uuid': instance_uuid}),
+ fake_block_device.FakeDbBlockDeviceDict(
+ {'id': 2, 'source_type': 'volume', 'destination_type': 'volume',
+ 'volume_id': 'volume_id2', 'instance_uuid': instance_uuid})]
+
+
+def fake_get_available_languages():
+ existing_translations = ['en_GB', 'en_AU', 'de', 'zh_CN', 'en_US']
+ return existing_translations
+
+
+def fake_not_implemented(*args, **kwargs):
+ raise NotImplementedError()
diff --git a/nova/tests/unit/api/openstack/test_common.py b/nova/tests/unit/api/openstack/test_common.py
new file mode 100644
index 0000000000..a61f70cf95
--- /dev/null
+++ b/nova/tests/unit/api/openstack/test_common.py
@@ -0,0 +1,764 @@
+# Copyright 2010 OpenStack Foundation
+# 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.
+
+"""
+Test suites for 'common' code used throughout the OpenStack HTTP API.
+"""
+
+import xml.dom.minidom as minidom
+
+from lxml import etree
+import mock
+import six
+from testtools import matchers
+import webob
+import webob.exc
+import webob.multidict
+
+from nova.api.openstack import common
+from nova.api.openstack import xmlutil
+from nova.compute import task_states
+from nova.compute import vm_states
+from nova import exception
+from nova import test
+from nova.tests.unit import utils
+
+
+NS = "{http://docs.openstack.org/compute/api/v1.1}"
+ATOMNS = "{http://www.w3.org/2005/Atom}"
+
+
+class LimiterTest(test.TestCase):
+ """Unit tests for the `nova.api.openstack.common.limited` method which
+ takes in a list of items and, depending on the 'offset' and 'limit' GET
+ params, returns a subset or complete set of the given items.
+ """
+
+ def setUp(self):
+ """Run before each test."""
+ super(LimiterTest, self).setUp()
+ self.tiny = range(1)
+ self.small = range(10)
+ self.medium = range(1000)
+ self.large = range(10000)
+
+ def test_limiter_offset_zero(self):
+ # Test offset key works with 0.
+ req = webob.Request.blank('/?offset=0')
+ self.assertEqual(common.limited(self.tiny, req), self.tiny)
+ self.assertEqual(common.limited(self.small, req), self.small)
+ self.assertEqual(common.limited(self.medium, req), self.medium)
+ self.assertEqual(common.limited(self.large, req), self.large[:1000])
+
+ def test_limiter_offset_medium(self):
+ # Test offset key works with a medium sized number.
+ req = webob.Request.blank('/?offset=10')
+ self.assertEqual(common.limited(self.tiny, req), [])
+ self.assertEqual(common.limited(self.small, req), self.small[10:])
+ self.assertEqual(common.limited(self.medium, req), self.medium[10:])
+ self.assertEqual(common.limited(self.large, req), self.large[10:1010])
+
+ def test_limiter_offset_over_max(self):
+ # Test offset key works with a number over 1000 (max_limit).
+ req = webob.Request.blank('/?offset=1001')
+ self.assertEqual(common.limited(self.tiny, req), [])
+ self.assertEqual(common.limited(self.small, req), [])
+ self.assertEqual(common.limited(self.medium, req), [])
+ self.assertEqual(
+ common.limited(self.large, req), self.large[1001:2001])
+
+ def test_limiter_offset_blank(self):
+ # Test offset key works with a blank offset.
+ req = webob.Request.blank('/?offset=')
+ self.assertRaises(
+ webob.exc.HTTPBadRequest, common.limited, self.tiny, req)
+
+ def test_limiter_offset_bad(self):
+ # Test offset key works with a BAD offset.
+ req = webob.Request.blank(u'/?offset=\u0020aa')
+ self.assertRaises(
+ webob.exc.HTTPBadRequest, common.limited, self.tiny, req)
+
+ def test_limiter_nothing(self):
+ # Test request with no offset or limit.
+ req = webob.Request.blank('/')
+ self.assertEqual(common.limited(self.tiny, req), self.tiny)
+ self.assertEqual(common.limited(self.small, req), self.small)
+ self.assertEqual(common.limited(self.medium, req), self.medium)
+ self.assertEqual(common.limited(self.large, req), self.large[:1000])
+
+ def test_limiter_limit_zero(self):
+ # Test limit of zero.
+ req = webob.Request.blank('/?limit=0')
+ self.assertEqual(common.limited(self.tiny, req), self.tiny)
+ self.assertEqual(common.limited(self.small, req), self.small)
+ self.assertEqual(common.limited(self.medium, req), self.medium)
+ self.assertEqual(common.limited(self.large, req), self.large[:1000])
+
+ def test_limiter_limit_medium(self):
+ # Test limit of 10.
+ req = webob.Request.blank('/?limit=10')
+ self.assertEqual(common.limited(self.tiny, req), self.tiny)
+ self.assertEqual(common.limited(self.small, req), self.small)
+ self.assertEqual(common.limited(self.medium, req), self.medium[:10])
+ self.assertEqual(common.limited(self.large, req), self.large[:10])
+
+ def test_limiter_limit_over_max(self):
+ # Test limit of 3000.
+ req = webob.Request.blank('/?limit=3000')
+ self.assertEqual(common.limited(self.tiny, req), self.tiny)
+ self.assertEqual(common.limited(self.small, req), self.small)
+ self.assertEqual(common.limited(self.medium, req), self.medium)
+ self.assertEqual(common.limited(self.large, req), self.large[:1000])
+
+ def test_limiter_limit_and_offset(self):
+ # Test request with both limit and offset.
+ items = range(2000)
+ req = webob.Request.blank('/?offset=1&limit=3')
+ self.assertEqual(common.limited(items, req), items[1:4])
+ req = webob.Request.blank('/?offset=3&limit=0')
+ self.assertEqual(common.limited(items, req), items[3:1003])
+ req = webob.Request.blank('/?offset=3&limit=1500')
+ self.assertEqual(common.limited(items, req), items[3:1003])
+ req = webob.Request.blank('/?offset=3000&limit=10')
+ self.assertEqual(common.limited(items, req), [])
+
+ def test_limiter_custom_max_limit(self):
+ # Test a max_limit other than 1000.
+ items = range(2000)
+ req = webob.Request.blank('/?offset=1&limit=3')
+ self.assertEqual(
+ common.limited(items, req, max_limit=2000), items[1:4])
+ req = webob.Request.blank('/?offset=3&limit=0')
+ self.assertEqual(
+ common.limited(items, req, max_limit=2000), items[3:])
+ req = webob.Request.blank('/?offset=3&limit=2500')
+ self.assertEqual(
+ common.limited(items, req, max_limit=2000), items[3:])
+ req = webob.Request.blank('/?offset=3000&limit=10')
+ self.assertEqual(common.limited(items, req, max_limit=2000), [])
+
+ def test_limiter_negative_limit(self):
+ # Test a negative limit.
+ req = webob.Request.blank('/?limit=-3000')
+ self.assertRaises(
+ webob.exc.HTTPBadRequest, common.limited, self.tiny, req)
+
+ def test_limiter_negative_offset(self):
+ # Test a negative offset.
+ req = webob.Request.blank('/?offset=-30')
+ self.assertRaises(
+ webob.exc.HTTPBadRequest, common.limited, self.tiny, req)
+
+
+class SortParamUtilsTest(test.TestCase):
+
+ def test_get_sort_params_defaults(self):
+ '''Verifies the default sort key and direction.'''
+ sort_keys, sort_dirs = common.get_sort_params({})
+ self.assertEqual(['created_at'], sort_keys)
+ self.assertEqual(['desc'], sort_dirs)
+
+ def test_get_sort_params_override_defaults(self):
+ '''Verifies that the defaults can be overriden.'''
+ sort_keys, sort_dirs = common.get_sort_params({}, default_key='key1',
+ default_dir='dir1')
+ self.assertEqual(['key1'], sort_keys)
+ self.assertEqual(['dir1'], sort_dirs)
+
+ sort_keys, sort_dirs = common.get_sort_params({}, default_key=None,
+ default_dir=None)
+ self.assertEqual([], sort_keys)
+ self.assertEqual([], sort_dirs)
+
+ def test_get_sort_params_single_value(self):
+ '''Verifies a single sort key and direction.'''
+ params = webob.multidict.MultiDict()
+ params.add('sort_key', 'key1')
+ params.add('sort_dir', 'dir1')
+ sort_keys, sort_dirs = common.get_sort_params(params)
+ self.assertEqual(['key1'], sort_keys)
+ self.assertEqual(['dir1'], sort_dirs)
+
+ def test_get_sort_params_single_with_default(self):
+ '''Verifies a single sort value with a default.'''
+ params = webob.multidict.MultiDict()
+ params.add('sort_key', 'key1')
+ sort_keys, sort_dirs = common.get_sort_params(params)
+ self.assertEqual(['key1'], sort_keys)
+ # sort_key was supplied, sort_dir should be defaulted
+ self.assertEqual(['desc'], sort_dirs)
+
+ params = webob.multidict.MultiDict()
+ params.add('sort_dir', 'dir1')
+ sort_keys, sort_dirs = common.get_sort_params(params)
+ self.assertEqual(['created_at'], sort_keys)
+ # sort_dir was supplied, sort_key should be defaulted
+ self.assertEqual(['dir1'], sort_dirs)
+
+ def test_get_sort_params_multiple_values(self):
+ '''Verifies multiple sort parameter values.'''
+ params = webob.multidict.MultiDict()
+ params.add('sort_key', 'key1')
+ params.add('sort_key', 'key2')
+ params.add('sort_key', 'key3')
+ params.add('sort_dir', 'dir1')
+ params.add('sort_dir', 'dir2')
+ params.add('sort_dir', 'dir3')
+ sort_keys, sort_dirs = common.get_sort_params(params)
+ self.assertEqual(['key1', 'key2', 'key3'], sort_keys)
+ self.assertEqual(['dir1', 'dir2', 'dir3'], sort_dirs)
+ # Also ensure that the input parameters are not modified
+ sort_key_vals = []
+ sort_dir_vals = []
+ while 'sort_key' in params:
+ sort_key_vals.append(params.pop('sort_key'))
+ while 'sort_dir' in params:
+ sort_dir_vals.append(params.pop('sort_dir'))
+ self.assertEqual(['key1', 'key2', 'key3'], sort_key_vals)
+ self.assertEqual(['dir1', 'dir2', 'dir3'], sort_dir_vals)
+ self.assertEqual(0, len(params))
+
+
+class PaginationParamsTest(test.TestCase):
+ """Unit tests for the `nova.api.openstack.common.get_pagination_params`
+ method which takes in a request object and returns 'marker' and 'limit'
+ GET params.
+ """
+
+ def test_no_params(self):
+ # Test no params.
+ req = webob.Request.blank('/')
+ self.assertEqual(common.get_pagination_params(req), {})
+
+ def test_valid_marker(self):
+ # Test valid marker param.
+ req = webob.Request.blank(
+ '/?marker=263abb28-1de6-412f-b00b-f0ee0c4333c2')
+ self.assertEqual(common.get_pagination_params(req),
+ {'marker': '263abb28-1de6-412f-b00b-f0ee0c4333c2'})
+
+ def test_valid_limit(self):
+ # Test valid limit param.
+ req = webob.Request.blank('/?limit=10')
+ self.assertEqual(common.get_pagination_params(req), {'limit': 10})
+
+ def test_invalid_limit(self):
+ # Test invalid limit param.
+ req = webob.Request.blank('/?limit=-2')
+ self.assertRaises(
+ webob.exc.HTTPBadRequest, common.get_pagination_params, req)
+
+ def test_valid_limit_and_marker(self):
+ # Test valid limit and marker parameters.
+ marker = '263abb28-1de6-412f-b00b-f0ee0c4333c2'
+ req = webob.Request.blank('/?limit=20&marker=%s' % marker)
+ self.assertEqual(common.get_pagination_params(req),
+ {'marker': marker, 'limit': 20})
+
+ def test_valid_page_size(self):
+ # Test valid page_size param.
+ req = webob.Request.blank('/?page_size=10')
+ self.assertEqual(common.get_pagination_params(req),
+ {'page_size': 10})
+
+ def test_invalid_page_size(self):
+ # Test invalid page_size param.
+ req = webob.Request.blank('/?page_size=-2')
+ self.assertRaises(
+ webob.exc.HTTPBadRequest, common.get_pagination_params, req)
+
+ def test_valid_limit_and_page_size(self):
+ # Test valid limit and page_size parameters.
+ req = webob.Request.blank('/?limit=20&page_size=5')
+ self.assertEqual(common.get_pagination_params(req),
+ {'page_size': 5, 'limit': 20})
+
+
+class MiscFunctionsTest(test.TestCase):
+
+ def test_remove_major_version_from_href(self):
+ fixture = 'http://www.testsite.com/v1/images'
+ expected = 'http://www.testsite.com/images'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href(self):
+ fixture = 'http://www.testsite.com/v1.1/images'
+ expected = 'http://www.testsite.com/images'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href_2(self):
+ fixture = 'http://www.testsite.com/v1.1/'
+ expected = 'http://www.testsite.com/'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href_3(self):
+ fixture = 'http://www.testsite.com/v10.10'
+ expected = 'http://www.testsite.com'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href_4(self):
+ fixture = 'http://www.testsite.com/v1.1/images/v10.5'
+ expected = 'http://www.testsite.com/images/v10.5'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href_bad_request(self):
+ fixture = 'http://www.testsite.com/1.1/images'
+ self.assertRaises(ValueError,
+ common.remove_version_from_href,
+ fixture)
+
+ def test_remove_version_from_href_bad_request_2(self):
+ fixture = 'http://www.testsite.com/v/images'
+ self.assertRaises(ValueError,
+ common.remove_version_from_href,
+ fixture)
+
+ def test_remove_version_from_href_bad_request_3(self):
+ fixture = 'http://www.testsite.com/v1.1images'
+ self.assertRaises(ValueError,
+ common.remove_version_from_href,
+ fixture)
+
+ def test_get_id_from_href_with_int_url(self):
+ fixture = 'http://www.testsite.com/dir/45'
+ actual = common.get_id_from_href(fixture)
+ expected = '45'
+ self.assertEqual(actual, expected)
+
+ def test_get_id_from_href_with_int(self):
+ fixture = '45'
+ actual = common.get_id_from_href(fixture)
+ expected = '45'
+ self.assertEqual(actual, expected)
+
+ def test_get_id_from_href_with_int_url_query(self):
+ fixture = 'http://www.testsite.com/dir/45?asdf=jkl'
+ actual = common.get_id_from_href(fixture)
+ expected = '45'
+ self.assertEqual(actual, expected)
+
+ def test_get_id_from_href_with_uuid_url(self):
+ fixture = 'http://www.testsite.com/dir/abc123'
+ actual = common.get_id_from_href(fixture)
+ expected = "abc123"
+ self.assertEqual(actual, expected)
+
+ def test_get_id_from_href_with_uuid_url_query(self):
+ fixture = 'http://www.testsite.com/dir/abc123?asdf=jkl'
+ actual = common.get_id_from_href(fixture)
+ expected = "abc123"
+ self.assertEqual(actual, expected)
+
+ def test_get_id_from_href_with_uuid(self):
+ fixture = 'abc123'
+ actual = common.get_id_from_href(fixture)
+ expected = 'abc123'
+ self.assertEqual(actual, expected)
+
+ def test_raise_http_conflict_for_instance_invalid_state(self):
+ exc = exception.InstanceInvalidState(attr='fake_attr',
+ state='fake_state', method='fake_method',
+ instance_uuid='fake')
+ try:
+ common.raise_http_conflict_for_instance_invalid_state(exc,
+ 'meow', 'fake_server_id')
+ except webob.exc.HTTPConflict as e:
+ self.assertEqual(six.text_type(e),
+ "Cannot 'meow' instance fake_server_id while it is in "
+ "fake_attr fake_state")
+ else:
+ self.fail("webob.exc.HTTPConflict was not raised")
+
+ def test_check_img_metadata_properties_quota_valid_metadata(self):
+ ctxt = utils.get_test_admin_context()
+ metadata1 = {"key": "value"}
+ actual = common.check_img_metadata_properties_quota(ctxt, metadata1)
+ self.assertIsNone(actual)
+
+ metadata2 = {"key": "v" * 260}
+ actual = common.check_img_metadata_properties_quota(ctxt, metadata2)
+ self.assertIsNone(actual)
+
+ metadata3 = {"key": ""}
+ actual = common.check_img_metadata_properties_quota(ctxt, metadata3)
+ self.assertIsNone(actual)
+
+ def test_check_img_metadata_properties_quota_inv_metadata(self):
+ ctxt = utils.get_test_admin_context()
+ metadata1 = {"a" * 260: "value"}
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ common.check_img_metadata_properties_quota, ctxt, metadata1)
+
+ metadata2 = {"": "value"}
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ common.check_img_metadata_properties_quota, ctxt, metadata2)
+
+ metadata3 = "invalid metadata"
+ self.assertRaises(webob.exc.HTTPBadRequest,
+ common.check_img_metadata_properties_quota, ctxt, metadata3)
+
+ metadata4 = None
+ self.assertIsNone(common.check_img_metadata_properties_quota(ctxt,
+ metadata4))
+ metadata5 = {}
+ self.assertIsNone(common.check_img_metadata_properties_quota(ctxt,
+ metadata5))
+
+ def test_status_from_state(self):
+ for vm_state in (vm_states.ACTIVE, vm_states.STOPPED):
+ for task_state in (task_states.RESIZE_PREP,
+ task_states.RESIZE_MIGRATING,
+ task_states.RESIZE_MIGRATED,
+ task_states.RESIZE_FINISH):
+ actual = common.status_from_state(vm_state, task_state)
+ expected = 'RESIZE'
+ self.assertEqual(expected, actual)
+
+ def test_status_rebuild_from_state(self):
+ for vm_state in (vm_states.ACTIVE, vm_states.STOPPED,
+ vm_states.ERROR):
+ for task_state in (task_states.REBUILDING,
+ task_states.REBUILD_BLOCK_DEVICE_MAPPING,
+ task_states.REBUILD_SPAWNING):
+ actual = common.status_from_state(vm_state, task_state)
+ expected = 'REBUILD'
+ self.assertEqual(expected, actual)
+
+ def test_task_and_vm_state_from_status(self):
+ fixture1 = ['reboot']
+ actual = common.task_and_vm_state_from_status(fixture1)
+ expected = [vm_states.ACTIVE], [task_states.REBOOT_PENDING,
+ task_states.REBOOT_STARTED,
+ task_states.REBOOTING]
+ self.assertEqual(expected, actual)
+
+ fixture2 = ['resize']
+ actual = common.task_and_vm_state_from_status(fixture2)
+ expected = ([vm_states.ACTIVE, vm_states.STOPPED],
+ [task_states.RESIZE_FINISH,
+ task_states.RESIZE_MIGRATED,
+ task_states.RESIZE_MIGRATING,
+ task_states.RESIZE_PREP])
+ self.assertEqual(expected, actual)
+
+ fixture3 = ['resize', 'reboot']
+ actual = common.task_and_vm_state_from_status(fixture3)
+ expected = ([vm_states.ACTIVE, vm_states.STOPPED],
+ [task_states.REBOOT_PENDING,
+ task_states.REBOOT_STARTED,
+ task_states.REBOOTING,
+ task_states.RESIZE_FINISH,
+ task_states.RESIZE_MIGRATED,
+ task_states.RESIZE_MIGRATING,
+ task_states.RESIZE_PREP])
+ self.assertEqual(expected, actual)
+
+
+class TestCollectionLinks(test.NoDBTestCase):
+ """Tests the _get_collection_links method."""
+
+ @mock.patch('nova.api.openstack.common.ViewBuilder._get_next_link')
+ def test_items_less_than_limit(self, href_link_mock):
+ items = [
+ {"uuid": "123"}
+ ]
+ req = mock.MagicMock()
+ params = mock.PropertyMock(return_value=dict(limit=10))
+ type(req).params = params
+
+ builder = common.ViewBuilder()
+ results = builder._get_collection_links(req, items, "ignored", "uuid")
+
+ self.assertFalse(href_link_mock.called)
+ self.assertThat(results, matchers.HasLength(0))
+
+ @mock.patch('nova.api.openstack.common.ViewBuilder._get_next_link')
+ def test_items_equals_given_limit(self, href_link_mock):
+ items = [
+ {"uuid": "123"}
+ ]
+ req = mock.MagicMock()
+ params = mock.PropertyMock(return_value=dict(limit=1))
+ type(req).params = params
+
+ builder = common.ViewBuilder()
+ results = builder._get_collection_links(req, items,
+ mock.sentinel.coll_key,
+ "uuid")
+
+ href_link_mock.assert_called_once_with(req, "123",
+ mock.sentinel.coll_key)
+ self.assertThat(results, matchers.HasLength(1))
+
+ @mock.patch('nova.api.openstack.common.ViewBuilder._get_next_link')
+ def test_items_equals_default_limit(self, href_link_mock):
+ items = [
+ {"uuid": "123"}
+ ]
+ req = mock.MagicMock()
+ params = mock.PropertyMock(return_value=dict())
+ type(req).params = params
+ self.flags(osapi_max_limit=1)
+
+ builder = common.ViewBuilder()
+ results = builder._get_collection_links(req, items,
+ mock.sentinel.coll_key,
+ "uuid")
+
+ href_link_mock.assert_called_once_with(req, "123",
+ mock.sentinel.coll_key)
+ self.assertThat(results, matchers.HasLength(1))
+
+ @mock.patch('nova.api.openstack.common.ViewBuilder._get_next_link')
+ def test_items_equals_default_limit_with_given(self, href_link_mock):
+ items = [
+ {"uuid": "123"}
+ ]
+ req = mock.MagicMock()
+ # Given limit is greater than default max, only return default max
+ params = mock.PropertyMock(return_value=dict(limit=2))
+ type(req).params = params
+ self.flags(osapi_max_limit=1)
+
+ builder = common.ViewBuilder()
+ results = builder._get_collection_links(req, items,
+ mock.sentinel.coll_key,
+ "uuid")
+
+ href_link_mock.assert_called_once_with(req, "123",
+ mock.sentinel.coll_key)
+ self.assertThat(results, matchers.HasLength(1))
+
+
+class MetadataXMLDeserializationTest(test.TestCase):
+
+ deserializer = common.MetadataXMLDeserializer()
+
+ def test_create(self):
+ request_body = """
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key='123'>asdf</meta>
+ <meta key='567'>jkl;</meta>
+ </metadata>"""
+ output = self.deserializer.deserialize(request_body, 'create')
+ expected = {"body": {"metadata": {"123": "asdf", "567": "jkl;"}}}
+ self.assertEqual(output, expected)
+
+ def test_create_empty(self):
+ request_body = """
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"/>"""
+ output = self.deserializer.deserialize(request_body, 'create')
+ expected = {"body": {"metadata": {}}}
+ self.assertEqual(output, expected)
+
+ def test_update_all(self):
+ request_body = """
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key='123'>asdf</meta>
+ <meta key='567'>jkl;</meta>
+ </metadata>"""
+ output = self.deserializer.deserialize(request_body, 'update_all')
+ expected = {"body": {"metadata": {"123": "asdf", "567": "jkl;"}}}
+ self.assertEqual(output, expected)
+
+ def test_update(self):
+ request_body = """
+ <meta xmlns="http://docs.openstack.org/compute/api/v1.1"
+ key='123'>asdf</meta>"""
+ output = self.deserializer.deserialize(request_body, 'update')
+ expected = {"body": {"meta": {"123": "asdf"}}}
+ self.assertEqual(output, expected)
+
+
+class MetadataXMLSerializationTest(test.TestCase):
+
+ def test_xml_declaration(self):
+ serializer = common.MetadataTemplate()
+ fixture = {
+ 'metadata': {
+ 'one': 'two',
+ 'three': 'four',
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
+ self.assertTrue(has_dec)
+
+ def test_index(self):
+ serializer = common.MetadataTemplate()
+ fixture = {
+ 'metadata': {
+ 'one': 'two',
+ 'three': 'four',
+ },
+ }
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'metadata')
+ metadata_dict = fixture['metadata']
+ metadata_elems = root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 2)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = metadata_dict.items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+
+ def test_index_null(self):
+ serializer = common.MetadataTemplate()
+ fixture = {
+ 'metadata': {
+ None: None,
+ },
+ }
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'metadata')
+ metadata_dict = fixture['metadata']
+ metadata_elems = root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 1)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = metadata_dict.items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+
+ def test_index_unicode(self):
+ serializer = common.MetadataTemplate()
+ fixture = {
+ 'metadata': {
+ u'three': u'Jos\xe9',
+ },
+ }
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'metadata')
+ metadata_dict = fixture['metadata']
+ metadata_elems = root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 1)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = metadata_dict.items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(metadata_elem.text.strip(), meta_value)
+
+ def test_show(self):
+ serializer = common.MetaItemTemplate()
+ fixture = {
+ 'meta': {
+ 'one': 'two',
+ },
+ }
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ meta_dict = fixture['meta']
+ (meta_key, meta_value) = meta_dict.items()[0]
+ self.assertEqual(str(root.get('key')), str(meta_key))
+ self.assertEqual(root.text.strip(), meta_value)
+
+ def test_update_all(self):
+ serializer = common.MetadataTemplate()
+ fixture = {
+ 'metadata': {
+ 'key6': 'value6',
+ 'key4': 'value4',
+ },
+ }
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'metadata')
+ metadata_dict = fixture['metadata']
+ metadata_elems = root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 2)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = metadata_dict.items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+
+ def test_update_item(self):
+ serializer = common.MetaItemTemplate()
+ fixture = {
+ 'meta': {
+ 'one': 'two',
+ },
+ }
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ meta_dict = fixture['meta']
+ (meta_key, meta_value) = meta_dict.items()[0]
+ self.assertEqual(str(root.get('key')), str(meta_key))
+ self.assertEqual(root.text.strip(), meta_value)
+
+ def test_create(self):
+ serializer = common.MetadataTemplate()
+ fixture = {
+ 'metadata': {
+ 'key9': 'value9',
+ 'key2': 'value2',
+ 'key1': 'value1',
+ },
+ }
+ output = serializer.serialize(fixture)
+ root = etree.XML(output)
+ xmlutil.validate_schema(root, 'metadata')
+ metadata_dict = fixture['metadata']
+ metadata_elems = root.findall('{0}meta'.format(NS))
+ self.assertEqual(len(metadata_elems), 3)
+ for i, metadata_elem in enumerate(metadata_elems):
+ (meta_key, meta_value) = metadata_dict.items()[i]
+ self.assertEqual(str(metadata_elem.get('key')), str(meta_key))
+ self.assertEqual(str(metadata_elem.text).strip(), str(meta_value))
+ actual = minidom.parseString(output.replace(" ", ""))
+
+ expected = minidom.parseString("""
+ <metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+ <meta key="key2">value2</meta>
+ <meta key="key9">value9</meta>
+ <meta key="key1">value1</meta>
+ </metadata>
+ """.replace(" ", "").replace("\n", ""))
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
+ def test_metadata_deserializer(self):
+ """Should throw a 400 error on corrupt xml."""
+ deserializer = common.MetadataXMLDeserializer()
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ deserializer.deserialize,
+ utils.killer_xml_body())
+
+
+class LinkPrefixTest(test.NoDBTestCase):
+
+ def test_update_link_prefix(self):
+ vb = common.ViewBuilder()
+ result = vb._update_link_prefix("http://192.168.0.243:24/",
+ "http://127.0.0.1/compute")
+ self.assertEqual("http://127.0.0.1/compute", result)
+
+ result = vb._update_link_prefix("http://foo.x.com/v1",
+ "http://new.prefix.com")
+ self.assertEqual("http://new.prefix.com/v1", result)
+
+ result = vb._update_link_prefix(
+ "http://foo.x.com/v1",
+ "http://new.prefix.com:20455/new_extra_prefix")
+ self.assertEqual("http://new.prefix.com:20455/new_extra_prefix/v1",
+ result)
diff --git a/nova/tests/unit/api/openstack/test_faults.py b/nova/tests/unit/api/openstack/test_faults.py
new file mode 100644
index 0000000000..b52a7e5896
--- /dev/null
+++ b/nova/tests/unit/api/openstack/test_faults.py
@@ -0,0 +1,315 @@
+# Copyright 2013 IBM Corp.
+# Copyright 2010 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from xml.dom import minidom
+
+import mock
+from oslo.serialization import jsonutils
+import webob
+import webob.dec
+import webob.exc
+
+import nova.api.openstack
+from nova.api.openstack import common
+from nova.api.openstack import wsgi
+from nova import exception
+from nova import i18n
+from nova.i18n import _
+from nova import test
+
+
+class TestFaultWrapper(test.NoDBTestCase):
+ """Tests covering `nova.api.openstack:FaultWrapper` class."""
+
+ @mock.patch('oslo.i18n.translate')
+ @mock.patch('nova.i18n.get_available_languages')
+ def test_safe_exception_translated(self, mock_languages, mock_translate):
+ def fake_translate(value, locale):
+ return "I've been translated!"
+
+ mock_translate.side_effect = fake_translate
+
+ # Create an exception, passing a translatable message with a
+ # known value we can test for later.
+ safe_exception = exception.NotFound(_('Should be translated.'))
+ safe_exception.safe = True
+ safe_exception.code = 404
+
+ req = webob.Request.blank('/')
+
+ def raiser(*args, **kwargs):
+ raise safe_exception
+
+ wrapper = nova.api.openstack.FaultWrapper(raiser)
+ response = req.get_response(wrapper)
+
+ # The text of the exception's message attribute (replaced
+ # above with a non-default value) should be passed to
+ # translate().
+ mock_translate.assert_any_call(u'Should be translated.', None)
+ # The return value from translate() should appear in the response.
+ self.assertIn("I've been translated!", unicode(response.body))
+
+
+class TestFaults(test.NoDBTestCase):
+ """Tests covering `nova.api.openstack.faults:Fault` class."""
+
+ def _prepare_xml(self, xml_string):
+ """Remove characters from string which hinder XML equality testing."""
+ xml_string = xml_string.replace(" ", "")
+ xml_string = xml_string.replace("\n", "")
+ xml_string = xml_string.replace("\t", "")
+ return xml_string
+
+ def test_400_fault_json(self):
+ # Test fault serialized to JSON via file-extension and/or header.
+ requests = [
+ webob.Request.blank('/.json'),
+ webob.Request.blank('/', headers={"Accept": "application/json"}),
+ ]
+
+ for request in requests:
+ fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='scram'))
+ response = request.get_response(fault)
+
+ expected = {
+ "badRequest": {
+ "message": "scram",
+ "code": 400,
+ },
+ }
+ actual = jsonutils.loads(response.body)
+
+ self.assertEqual(response.content_type, "application/json")
+ self.assertEqual(expected, actual)
+
+ def test_413_fault_json(self):
+ # Test fault serialized to JSON via file-extension and/or header.
+ requests = [
+ webob.Request.blank('/.json'),
+ webob.Request.blank('/', headers={"Accept": "application/json"}),
+ ]
+
+ for request in requests:
+ exc = webob.exc.HTTPRequestEntityTooLarge
+ # NOTE(aloga): we intentionally pass an integer for the
+ # 'Retry-After' header. It should be then converted to a str
+ fault = wsgi.Fault(exc(explanation='sorry',
+ headers={'Retry-After': 4}))
+ response = request.get_response(fault)
+
+ expected = {
+ "overLimit": {
+ "message": "sorry",
+ "code": 413,
+ "retryAfter": "4",
+ },
+ }
+ actual = jsonutils.loads(response.body)
+
+ self.assertEqual(response.content_type, "application/json")
+ self.assertEqual(expected, actual)
+
+ def test_429_fault_json(self):
+ # Test fault serialized to JSON via file-extension and/or header.
+ requests = [
+ webob.Request.blank('/.json'),
+ webob.Request.blank('/', headers={"Accept": "application/json"}),
+ ]
+
+ for request in requests:
+ exc = webob.exc.HTTPTooManyRequests
+ # NOTE(aloga): we intentionally pass an integer for the
+ # 'Retry-After' header. It should be then converted to a str
+ fault = wsgi.Fault(exc(explanation='sorry',
+ headers={'Retry-After': 4}))
+ response = request.get_response(fault)
+
+ expected = {
+ "overLimit": {
+ "message": "sorry",
+ "code": 429,
+ "retryAfter": "4",
+ },
+ }
+ actual = jsonutils.loads(response.body)
+
+ self.assertEqual(response.content_type, "application/json")
+ self.assertEqual(expected, actual)
+
+ def test_raise(self):
+ # Ensure the ability to raise :class:`Fault` in WSGI-ified methods.
+ @webob.dec.wsgify
+ def raiser(req):
+ raise wsgi.Fault(webob.exc.HTTPNotFound(explanation='whut?'))
+
+ req = webob.Request.blank('/.xml')
+ resp = req.get_response(raiser)
+ self.assertEqual(resp.content_type, "application/xml")
+ self.assertEqual(resp.status_int, 404)
+ self.assertIn('whut?', resp.body)
+
+ def test_raise_403(self):
+ # Ensure the ability to raise :class:`Fault` in WSGI-ified methods.
+ @webob.dec.wsgify
+ def raiser(req):
+ raise wsgi.Fault(webob.exc.HTTPForbidden(explanation='whut?'))
+
+ req = webob.Request.blank('/.xml')
+ resp = req.get_response(raiser)
+ self.assertEqual(resp.content_type, "application/xml")
+ self.assertEqual(resp.status_int, 403)
+ self.assertNotIn('resizeNotAllowed', resp.body)
+ self.assertIn('forbidden', resp.body)
+
+ def test_raise_localize_explanation(self):
+ msgid = "String with params: %s"
+ params = ('blah', )
+ lazy_gettext = i18n._
+ expl = lazy_gettext(msgid) % params
+
+ @webob.dec.wsgify
+ def raiser(req):
+ raise wsgi.Fault(webob.exc.HTTPNotFound(explanation=expl))
+
+ req = webob.Request.blank('/.xml')
+ resp = req.get_response(raiser)
+ self.assertEqual(resp.content_type, "application/xml")
+ self.assertEqual(resp.status_int, 404)
+ self.assertIn((msgid % params), resp.body)
+
+ def test_fault_has_status_int(self):
+ # Ensure the status_int is set correctly on faults.
+ fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='what?'))
+ self.assertEqual(fault.status_int, 400)
+
+ def test_xml_serializer(self):
+ # Ensure that a v1.1 request responds with a v1.1 xmlns.
+ request = webob.Request.blank('/v1.1',
+ headers={"Accept": "application/xml"})
+
+ fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='scram'))
+ response = request.get_response(fault)
+
+ self.assertIn(common.XML_NS_V11, response.body)
+ self.assertEqual(response.content_type, "application/xml")
+ self.assertEqual(response.status_int, 400)
+
+
+class FaultsXMLSerializationTestV11(test.NoDBTestCase):
+ """Tests covering `nova.api.openstack.faults:Fault` class."""
+
+ def _prepare_xml(self, xml_string):
+ xml_string = xml_string.replace(" ", "")
+ xml_string = xml_string.replace("\n", "")
+ xml_string = xml_string.replace("\t", "")
+ return xml_string
+
+ def test_400_fault(self):
+ metadata = {'attributes': {"badRequest": 'code'}}
+ serializer = wsgi.XMLDictSerializer(metadata=metadata,
+ xmlns=common.XML_NS_V11)
+
+ fixture = {
+ "badRequest": {
+ "message": "scram",
+ "code": 400,
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ actual = minidom.parseString(self._prepare_xml(output))
+
+ expected = minidom.parseString(self._prepare_xml("""
+ <badRequest code="400" xmlns="%s">
+ <message>scram</message>
+ </badRequest>
+ """) % common.XML_NS_V11)
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
+ def test_413_fault(self):
+ metadata = {'attributes': {"overLimit": 'code'}}
+ serializer = wsgi.XMLDictSerializer(metadata=metadata,
+ xmlns=common.XML_NS_V11)
+
+ fixture = {
+ "overLimit": {
+ "message": "sorry",
+ "code": 413,
+ "retryAfter": 4,
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ actual = minidom.parseString(self._prepare_xml(output))
+
+ expected = minidom.parseString(self._prepare_xml("""
+ <overLimit code="413" xmlns="%s">
+ <message>sorry</message>
+ <retryAfter>4</retryAfter>
+ </overLimit>
+ """) % common.XML_NS_V11)
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
+ def test_429_fault(self):
+ metadata = {'attributes': {"overLimit": 'code'}}
+ serializer = wsgi.XMLDictSerializer(metadata=metadata,
+ xmlns=common.XML_NS_V11)
+
+ fixture = {
+ "overLimit": {
+ "message": "sorry",
+ "code": 429,
+ "retryAfter": 4,
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ actual = minidom.parseString(self._prepare_xml(output))
+
+ expected = minidom.parseString(self._prepare_xml("""
+ <overLimit code="429" xmlns="%s">
+ <message>sorry</message>
+ <retryAfter>4</retryAfter>
+ </overLimit>
+ """) % common.XML_NS_V11)
+
+ self.assertEqual(expected.toxml(), actual.toxml())
+
+ def test_404_fault(self):
+ metadata = {'attributes': {"itemNotFound": 'code'}}
+ serializer = wsgi.XMLDictSerializer(metadata=metadata,
+ xmlns=common.XML_NS_V11)
+
+ fixture = {
+ "itemNotFound": {
+ "message": "sorry",
+ "code": 404,
+ },
+ }
+
+ output = serializer.serialize(fixture)
+ actual = minidom.parseString(self._prepare_xml(output))
+
+ expected = minidom.parseString(self._prepare_xml("""
+ <itemNotFound code="404" xmlns="%s">
+ <message>sorry</message>
+ </itemNotFound>
+ """) % common.XML_NS_V11)
+
+ self.assertEqual(expected.toxml(), actual.toxml())
diff --git a/nova/tests/unit/api/openstack/test_mapper.py b/nova/tests/unit/api/openstack/test_mapper.py
new file mode 100644
index 0000000000..b872be546f
--- /dev/null
+++ b/nova/tests/unit/api/openstack/test_mapper.py
@@ -0,0 +1,46 @@
+# Copyright 2013 OpenStack Foundation
+# 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
+
+from nova.api import openstack as openstack_api
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+
+
+class MapperTest(test.NoDBTestCase):
+ def test_resource_project_prefix(self):
+ class Controller(object):
+ def index(self, req):
+ return 'foo'
+
+ app = fakes.TestRouter(Controller(),
+ openstack_api.ProjectMapper())
+ req = webob.Request.blank('/1234/tests')
+ resp = req.get_response(app)
+ self.assertEqual(resp.body, 'foo')
+ self.assertEqual(resp.status_int, 200)
+
+ def test_resource_no_project_prefix(self):
+ class Controller(object):
+ def index(self, req):
+ return 'foo'
+
+ app = fakes.TestRouter(Controller(),
+ openstack_api.PlainMapper())
+ req = webob.Request.blank('/tests')
+ resp = req.get_response(app)
+ self.assertEqual(resp.body, 'foo')
+ self.assertEqual(resp.status_int, 200)
diff --git a/nova/tests/unit/api/openstack/test_wsgi.py b/nova/tests/unit/api/openstack/test_wsgi.py
new file mode 100644
index 0000000000..7607101628
--- /dev/null
+++ b/nova/tests/unit/api/openstack/test_wsgi.py
@@ -0,0 +1,1244 @@
+# 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 inspect
+
+import webob
+
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova import exception
+from nova import i18n
+from nova import test
+from nova.tests.unit.api.openstack import fakes
+from nova.tests.unit import utils
+
+
+class RequestTest(test.NoDBTestCase):
+ def test_content_type_missing(self):
+ request = wsgi.Request.blank('/tests/123', method='POST')
+ request.body = "<body />"
+ self.assertIsNone(request.get_content_type())
+
+ def test_content_type_unsupported(self):
+ request = wsgi.Request.blank('/tests/123', method='POST')
+ request.headers["Content-Type"] = "text/html"
+ request.body = "asdf<br />"
+ self.assertRaises(exception.InvalidContentType,
+ request.get_content_type)
+
+ def test_content_type_with_charset(self):
+ request = wsgi.Request.blank('/tests/123')
+ request.headers["Content-Type"] = "application/json; charset=UTF-8"
+ result = request.get_content_type()
+ self.assertEqual(result, "application/json")
+
+ def test_content_type_from_accept(self):
+ for content_type in ('application/xml',
+ 'application/vnd.openstack.compute+xml',
+ 'application/json',
+ 'application/vnd.openstack.compute+json'):
+ request = wsgi.Request.blank('/tests/123')
+ request.headers["Accept"] = content_type
+ result = request.best_match_content_type()
+ self.assertEqual(result, content_type)
+
+ def test_content_type_from_accept_best(self):
+ request = wsgi.Request.blank('/tests/123')
+ request.headers["Accept"] = "application/xml, application/json"
+ result = request.best_match_content_type()
+ self.assertEqual(result, "application/json")
+
+ request = wsgi.Request.blank('/tests/123')
+ request.headers["Accept"] = ("application/json; q=0.3, "
+ "application/xml; q=0.9")
+ result = request.best_match_content_type()
+ self.assertEqual(result, "application/xml")
+
+ def test_content_type_from_query_extension(self):
+ request = wsgi.Request.blank('/tests/123.xml')
+ result = request.best_match_content_type()
+ self.assertEqual(result, "application/xml")
+
+ request = wsgi.Request.blank('/tests/123.json')
+ result = request.best_match_content_type()
+ self.assertEqual(result, "application/json")
+
+ request = wsgi.Request.blank('/tests/123.invalid')
+ result = request.best_match_content_type()
+ self.assertEqual(result, "application/json")
+
+ def test_content_type_accept_and_query_extension(self):
+ request = wsgi.Request.blank('/tests/123.xml')
+ request.headers["Accept"] = "application/json"
+ result = request.best_match_content_type()
+ self.assertEqual(result, "application/xml")
+
+ def test_content_type_accept_default(self):
+ request = wsgi.Request.blank('/tests/123.unsupported')
+ request.headers["Accept"] = "application/unsupported1"
+ result = request.best_match_content_type()
+ self.assertEqual(result, "application/json")
+
+ def test_cache_and_retrieve_instances(self):
+ request = wsgi.Request.blank('/foo')
+ instances = []
+ for x in xrange(3):
+ instances.append({'uuid': 'uuid%s' % x})
+ # Store 2
+ request.cache_db_instances(instances[:2])
+ # Store 1
+ request.cache_db_instance(instances[2])
+ self.assertEqual(request.get_db_instance('uuid0'),
+ instances[0])
+ self.assertEqual(request.get_db_instance('uuid1'),
+ instances[1])
+ self.assertEqual(request.get_db_instance('uuid2'),
+ instances[2])
+ self.assertIsNone(request.get_db_instance('uuid3'))
+ self.assertEqual(request.get_db_instances(),
+ {'uuid0': instances[0],
+ 'uuid1': instances[1],
+ 'uuid2': instances[2]})
+
+ def test_cache_and_retrieve_compute_nodes(self):
+ request = wsgi.Request.blank('/foo')
+ compute_nodes = []
+ for x in xrange(3):
+ compute_nodes.append({'id': 'id%s' % x})
+ # Store 2
+ request.cache_db_compute_nodes(compute_nodes[:2])
+ # Store 1
+ request.cache_db_compute_node(compute_nodes[2])
+ self.assertEqual(request.get_db_compute_node('id0'),
+ compute_nodes[0])
+ self.assertEqual(request.get_db_compute_node('id1'),
+ compute_nodes[1])
+ self.assertEqual(request.get_db_compute_node('id2'),
+ compute_nodes[2])
+ self.assertIsNone(request.get_db_compute_node('id3'))
+ self.assertEqual(request.get_db_compute_nodes(),
+ {'id0': compute_nodes[0],
+ 'id1': compute_nodes[1],
+ 'id2': compute_nodes[2]})
+
+ def test_from_request(self):
+ self.stubs.Set(i18n, 'get_available_languages',
+ fakes.fake_get_available_languages)
+
+ request = wsgi.Request.blank('/')
+ accepted = 'bogus;q=1.1, en-gb;q=0.7,en-us,en;q=.5,*;q=.7'
+ request.headers = {'Accept-Language': accepted}
+ self.assertEqual(request.best_match_language(), 'en_US')
+
+ def test_asterisk(self):
+ # asterisk should match first available if there
+ # are not any other available matches
+ self.stubs.Set(i18n, 'get_available_languages',
+ fakes.fake_get_available_languages)
+
+ request = wsgi.Request.blank('/')
+ accepted = '*,es;q=.5'
+ request.headers = {'Accept-Language': accepted}
+ self.assertEqual(request.best_match_language(), 'en_GB')
+
+ def test_prefix(self):
+ self.stubs.Set(i18n, 'get_available_languages',
+ fakes.fake_get_available_languages)
+
+ request = wsgi.Request.blank('/')
+ accepted = 'zh'
+ request.headers = {'Accept-Language': accepted}
+ self.assertEqual(request.best_match_language(), 'zh_CN')
+
+ def test_secondary(self):
+ self.stubs.Set(i18n, 'get_available_languages',
+ fakes.fake_get_available_languages)
+
+ request = wsgi.Request.blank('/')
+ accepted = 'nn,en-gb;q=.5'
+ request.headers = {'Accept-Language': accepted}
+ self.assertEqual(request.best_match_language(), 'en_GB')
+
+ def test_none_found(self):
+ self.stubs.Set(i18n, 'get_available_languages',
+ fakes.fake_get_available_languages)
+
+ request = wsgi.Request.blank('/')
+ accepted = 'nb-no'
+ request.headers = {'Accept-Language': accepted}
+ self.assertIs(request.best_match_language(), None)
+
+ def test_no_lang_header(self):
+ self.stubs.Set(i18n, 'get_available_languages',
+ fakes.fake_get_available_languages)
+
+ request = wsgi.Request.blank('/')
+ accepted = ''
+ request.headers = {'Accept-Language': accepted}
+ self.assertIs(request.best_match_language(), None)
+
+
+class ActionDispatcherTest(test.NoDBTestCase):
+ def test_dispatch(self):
+ serializer = wsgi.ActionDispatcher()
+ serializer.create = lambda x: 'pants'
+ self.assertEqual(serializer.dispatch({}, action='create'), 'pants')
+
+ def test_dispatch_action_None(self):
+ serializer = wsgi.ActionDispatcher()
+ serializer.create = lambda x: 'pants'
+ serializer.default = lambda x: 'trousers'
+ self.assertEqual(serializer.dispatch({}, action=None), 'trousers')
+
+ def test_dispatch_default(self):
+ serializer = wsgi.ActionDispatcher()
+ serializer.create = lambda x: 'pants'
+ serializer.default = lambda x: 'trousers'
+ self.assertEqual(serializer.dispatch({}, action='update'), 'trousers')
+
+
+class DictSerializerTest(test.NoDBTestCase):
+ def test_dispatch_default(self):
+ serializer = wsgi.DictSerializer()
+ self.assertEqual(serializer.serialize({}, 'update'), '')
+
+
+class XMLDictSerializerTest(test.NoDBTestCase):
+ def test_xml(self):
+ input_dict = dict(servers=dict(a=(2, 3)))
+ expected_xml = '<serversxmlns="asdf"><a>(2,3)</a></servers>'
+ serializer = wsgi.XMLDictSerializer(xmlns="asdf")
+ result = serializer.serialize(input_dict)
+ result = result.replace('\n', '').replace(' ', '')
+ self.assertEqual(result, expected_xml)
+
+ def test_xml_contains_unicode(self):
+ input_dict = dict(test=u'\u89e3\u7801')
+ expected_xml = '<test>\xe8\xa7\xa3\xe7\xa0\x81</test>'
+ serializer = wsgi.XMLDictSerializer()
+ result = serializer.serialize(input_dict)
+ result = result.replace('\n', '').replace(' ', '')
+ self.assertEqual(expected_xml, result)
+
+
+class JSONDictSerializerTest(test.NoDBTestCase):
+ def test_json(self):
+ input_dict = dict(servers=dict(a=(2, 3)))
+ expected_json = '{"servers":{"a":[2,3]}}'
+ serializer = wsgi.JSONDictSerializer()
+ result = serializer.serialize(input_dict)
+ result = result.replace('\n', '').replace(' ', '')
+ self.assertEqual(result, expected_json)
+
+
+class TextDeserializerTest(test.NoDBTestCase):
+ def test_dispatch_default(self):
+ deserializer = wsgi.TextDeserializer()
+ self.assertEqual(deserializer.deserialize({}, 'update'), {})
+
+
+class JSONDeserializerTest(test.NoDBTestCase):
+ def test_json(self):
+ data = """{"a": {
+ "a1": "1",
+ "a2": "2",
+ "bs": ["1", "2", "3", {"c": {"c1": "1"}}],
+ "d": {"e": "1"},
+ "f": "1"}}"""
+ as_dict = {
+ 'body': {
+ 'a': {
+ 'a1': '1',
+ 'a2': '2',
+ 'bs': ['1', '2', '3', {'c': {'c1': '1'}}],
+ 'd': {'e': '1'},
+ 'f': '1',
+ },
+ },
+ }
+ deserializer = wsgi.JSONDeserializer()
+ self.assertEqual(deserializer.deserialize(data), as_dict)
+
+ def test_json_valid_utf8(self):
+ data = """{"server": {"min_count": 1, "flavorRef": "1",
+ "name": "\xe6\xa6\x82\xe5\xbf\xb5",
+ "imageRef": "10bab10c-1304-47d",
+ "max_count": 1}} """
+ as_dict = {
+ 'body': {
+ u'server': {
+ u'min_count': 1, u'flavorRef': u'1',
+ u'name': u'\u6982\u5ff5',
+ u'imageRef': u'10bab10c-1304-47d',
+ u'max_count': 1
+ }
+ }
+ }
+ deserializer = wsgi.JSONDeserializer()
+ self.assertEqual(deserializer.deserialize(data), as_dict)
+
+ def test_json_invalid_utf8(self):
+ """Send invalid utf-8 to JSONDeserializer."""
+ data = """{"server": {"min_count": 1, "flavorRef": "1",
+ "name": "\xf0\x28\x8c\x28",
+ "imageRef": "10bab10c-1304-47d",
+ "max_count": 1}} """
+
+ deserializer = wsgi.JSONDeserializer()
+ self.assertRaises(exception.MalformedRequestBody,
+ deserializer.deserialize, data)
+
+
+class XMLDeserializerTest(test.NoDBTestCase):
+ def test_xml(self):
+ xml = """
+ <a a1="1" a2="2">
+ <bs><b>1</b><b>2</b><b>3</b><b><c c1="1"/></b></bs>
+ <d><e>1</e></d>
+ <f>1</f>
+ </a>
+ """.strip()
+ as_dict = {
+ 'body': {
+ 'a': {
+ 'a1': '1',
+ 'a2': '2',
+ 'bs': ['1', '2', '3', {'c': {'c1': '1'}}],
+ 'd': {'e': '1'},
+ 'f': '1',
+ },
+ },
+ }
+ metadata = {'plurals': {'bs': 'b', 'ts': 't'}}
+ deserializer = wsgi.XMLDeserializer(metadata=metadata)
+ self.assertEqual(deserializer.deserialize(xml), as_dict)
+
+ def test_xml_empty(self):
+ xml = '<a></a>'
+ as_dict = {"body": {"a": {}}}
+ deserializer = wsgi.XMLDeserializer()
+ self.assertEqual(deserializer.deserialize(xml), as_dict)
+
+ def test_xml_valid_utf8(self):
+ xml = """ <a><name>\xe6\xa6\x82\xe5\xbf\xb5</name></a> """
+ deserializer = wsgi.XMLDeserializer()
+ as_dict = {'body': {u'a': {u'name': u'\u6982\u5ff5'}}}
+ self.assertEqual(deserializer.deserialize(xml), as_dict)
+
+ def test_xml_invalid_utf8(self):
+ """Send invalid utf-8 to XMLDeserializer."""
+ xml = """ <a><name>\xf0\x28\x8c\x28</name></a> """
+ deserializer = wsgi.XMLDeserializer()
+ self.assertRaises(exception.MalformedRequestBody,
+ deserializer.deserialize, xml)
+
+
+class ResourceTest(test.NoDBTestCase):
+
+ def get_req_id_header_name(self, request):
+ header_name = 'x-openstack-request-id'
+ if utils.get_api_version(request) < 3:
+ header_name = 'x-compute-request-id'
+
+ return header_name
+
+ def test_resource_call_with_method_get(self):
+ class Controller(object):
+ def index(self, req):
+ return 'success'
+
+ app = fakes.TestRouter(Controller())
+ # the default method is GET
+ req = webob.Request.blank('/tests')
+ response = req.get_response(app)
+ self.assertEqual(response.body, 'success')
+ self.assertEqual(response.status_int, 200)
+ req.body = '{"body": {"key": "value"}}'
+ response = req.get_response(app)
+ self.assertEqual(response.body, 'success')
+ self.assertEqual(response.status_int, 200)
+ req.content_type = 'application/json'
+ response = req.get_response(app)
+ self.assertEqual(response.body, 'success')
+ self.assertEqual(response.status_int, 200)
+
+ def test_resource_call_with_method_post(self):
+ class Controller(object):
+ @extensions.expected_errors(400)
+ def create(self, req, body):
+ if expected_body != body:
+ msg = "The request body invalid"
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ return "success"
+ # verify the method: POST
+ app = fakes.TestRouter(Controller())
+ req = webob.Request.blank('/tests', method="POST",
+ content_type='application/json')
+ req.body = '{"body": {"key": "value"}}'
+ expected_body = {'body': {
+ "key": "value"
+ }
+ }
+ response = req.get_response(app)
+ self.assertEqual(response.status_int, 200)
+ self.assertEqual(response.body, 'success')
+ # verify without body
+ expected_body = None
+ req.body = None
+ response = req.get_response(app)
+ self.assertEqual(response.status_int, 200)
+ self.assertEqual(response.body, 'success')
+ # the body is validated in the controller
+ expected_body = {'body': None}
+ response = req.get_response(app)
+ expected_unsupported_type_body = ('{"badRequest": '
+ '{"message": "The request body invalid", "code": 400}}')
+ self.assertEqual(response.status_int, 400)
+ self.assertEqual(expected_unsupported_type_body, response.body)
+
+ def test_resource_call_with_method_put(self):
+ class Controller(object):
+ def update(self, req, id, body):
+ if expected_body != body:
+ msg = "The request body invalid"
+ raise webob.exc.HTTPBadRequest(explanation=msg)
+ return "success"
+ # verify the method: PUT
+ app = fakes.TestRouter(Controller())
+ req = webob.Request.blank('/tests/test_id', method="PUT",
+ content_type='application/json')
+ req.body = '{"body": {"key": "value"}}'
+ expected_body = {'body': {
+ "key": "value"
+ }
+ }
+ response = req.get_response(app)
+ self.assertEqual(response.body, 'success')
+ self.assertEqual(response.status_int, 200)
+ req.body = None
+ expected_body = None
+ response = req.get_response(app)
+ self.assertEqual(response.status_int, 200)
+ # verify no content_type is contained in the request
+ req.content_type = None
+ req.body = '{"body": {"key": "value"}}'
+ response = req.get_response(app)
+ expected_unsupported_type_body = ('{"badRequest": '
+ '{"message": "Unsupported Content-Type", "code": 400}}')
+ self.assertEqual(response.status_int, 400)
+ self.assertEqual(expected_unsupported_type_body, response.body)
+
+ def test_resource_call_with_method_delete(self):
+ class Controller(object):
+ def delete(self, req, id):
+ return "success"
+
+ # verify the method: DELETE
+ app = fakes.TestRouter(Controller())
+ req = webob.Request.blank('/tests/test_id', method="DELETE")
+ response = req.get_response(app)
+ self.assertEqual(response.status_int, 200)
+ self.assertEqual(response.body, 'success')
+ # ignore the body
+ req.body = '{"body": {"key": "value"}}'
+ response = req.get_response(app)
+ self.assertEqual(response.status_int, 200)
+ self.assertEqual(response.body, 'success')
+
+ def test_resource_not_authorized(self):
+ class Controller(object):
+ def index(self, req):
+ raise exception.Forbidden()
+
+ req = webob.Request.blank('/tests')
+ app = fakes.TestRouter(Controller())
+ response = req.get_response(app)
+ self.assertEqual(response.status_int, 403)
+
+ def test_dispatch(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ method, extensions = resource.get_method(None, 'index', None, '')
+ actual = resource.dispatch(method, None, {'pants': 'off'})
+ expected = 'off'
+ self.assertEqual(actual, expected)
+
+ def test_get_method_unknown_controller_method(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ self.assertRaises(AttributeError, resource.get_method,
+ None, 'create', None, '')
+
+ def test_get_method_action_json(self):
+ class Controller(wsgi.Controller):
+ @wsgi.action('fooAction')
+ def _action_foo(self, req, id, body):
+ return body
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ method, extensions = resource.get_method(None, 'action',
+ 'application/json',
+ '{"fooAction": true}')
+ self.assertEqual(controller._action_foo, method)
+
+ def test_get_method_action_xml(self):
+ class Controller(wsgi.Controller):
+ @wsgi.action('fooAction')
+ def _action_foo(self, req, id, body):
+ return body
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ method, extensions = resource.get_method(None, 'action',
+ 'application/xml',
+ '<fooAction>true</fooAction>')
+ self.assertEqual(controller._action_foo, method)
+
+ def test_get_method_action_corrupt_xml(self):
+ class Controller(wsgi.Controller):
+ @wsgi.action('fooAction')
+ def _action_foo(self, req, id, body):
+ return body
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ self.assertRaises(
+ exception.MalformedRequestBody,
+ resource.get_method,
+ None, 'action',
+ 'application/xml',
+ utils.killer_xml_body())
+
+ def test_get_method_action_bad_body(self):
+ class Controller(wsgi.Controller):
+ @wsgi.action('fooAction')
+ def _action_foo(self, req, id, body):
+ return body
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ self.assertRaises(exception.MalformedRequestBody, resource.get_method,
+ None, 'action', 'application/json', '{}')
+
+ def test_get_method_unknown_controller_action(self):
+ class Controller(wsgi.Controller):
+ @wsgi.action('fooAction')
+ def _action_foo(self, req, id, body):
+ return body
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ self.assertRaises(KeyError, resource.get_method,
+ None, 'action', 'application/json',
+ '{"barAction": true}')
+
+ def test_get_method_action_method(self):
+ class Controller():
+ def action(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ method, extensions = resource.get_method(None, 'action',
+ 'application/xml',
+ '<fooAction>true</fooAction')
+ self.assertEqual(controller.action, method)
+
+ def test_get_action_args(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ env = {
+ 'wsgiorg.routing_args': [None, {
+ 'controller': None,
+ 'format': None,
+ 'action': 'update',
+ 'id': 12,
+ }],
+ }
+
+ expected = {'action': 'update', 'id': 12}
+
+ self.assertEqual(resource.get_action_args(env), expected)
+
+ def test_get_body_bad_content(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ request = wsgi.Request.blank('/', method='POST')
+ request.headers['Content-Type'] = 'application/none'
+ request.body = 'foo'
+
+ content_type, body = resource.get_body(request)
+ self.assertIsNone(content_type)
+ self.assertEqual(body, '')
+
+ def test_get_body_no_content_type(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ request = wsgi.Request.blank('/', method='POST')
+ request.body = 'foo'
+
+ content_type, body = resource.get_body(request)
+ self.assertIsNone(content_type)
+ self.assertEqual(body, 'foo')
+
+ def test_get_body_no_content_body(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ request = wsgi.Request.blank('/', method='POST')
+ request.headers['Content-Type'] = 'application/json'
+ request.body = ''
+
+ content_type, body = resource.get_body(request)
+ self.assertEqual('application/json', content_type)
+ self.assertEqual(body, '')
+
+ def test_get_body(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ request = wsgi.Request.blank('/', method='POST')
+ request.headers['Content-Type'] = 'application/json'
+ request.body = 'foo'
+
+ content_type, body = resource.get_body(request)
+ self.assertEqual(content_type, 'application/json')
+ self.assertEqual(body, 'foo')
+
+ def test_get_request_id_with_dict_response_body(self):
+ class Controller(wsgi.Controller):
+ def index(self, req):
+ return {'foo': 'bar'}
+
+ req = fakes.HTTPRequest.blank('/tests')
+ app = fakes.TestRouter(Controller())
+ response = req.get_response(app)
+ self.assertIn('nova.context', req.environ)
+ self.assertEqual(response.body, '{"foo": "bar"}')
+ self.assertEqual(response.status_int, 200)
+
+ def test_no_request_id_with_str_response_body(self):
+ class Controller(wsgi.Controller):
+ def index(self, req):
+ return 'foo'
+
+ req = fakes.HTTPRequest.blank('/tests')
+ app = fakes.TestRouter(Controller())
+ response = req.get_response(app)
+ # NOTE(alaski): This test is really to ensure that a str response
+ # doesn't error. Not having a request_id header is a side effect of
+ # our wsgi setup, ideally it would be there.
+ expected_header = self.get_req_id_header_name(req)
+ self.assertFalse(hasattr(response.headers, expected_header))
+ self.assertEqual(response.body, 'foo')
+ self.assertEqual(response.status_int, 200)
+
+ def test_get_request_id_no_response_body(self):
+ class Controller(object):
+ def index(self, req):
+ pass
+
+ req = fakes.HTTPRequest.blank('/tests')
+ app = fakes.TestRouter(Controller())
+ response = req.get_response(app)
+ self.assertIn('nova.context', req.environ)
+ self.assertEqual(response.body, '')
+ self.assertEqual(response.status_int, 200)
+
+ def test_deserialize_badtype(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ self.assertRaises(exception.InvalidContentType,
+ resource.deserialize,
+ controller.index, 'application/none', 'foo')
+
+ def test_deserialize_default(self):
+ class JSONDeserializer(object):
+ def deserialize(self, body):
+ return 'json'
+
+ class XMLDeserializer(object):
+ def deserialize(self, body):
+ return 'xml'
+
+ class Controller(object):
+ @wsgi.deserializers(xml=XMLDeserializer)
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller, json=JSONDeserializer)
+
+ obj = resource.deserialize(controller.index, 'application/json', 'foo')
+ self.assertEqual(obj, 'json')
+
+ def test_deserialize_decorator(self):
+ class JSONDeserializer(object):
+ def deserialize(self, body):
+ return 'json'
+
+ class XMLDeserializer(object):
+ def deserialize(self, body):
+ return 'xml'
+
+ class Controller(object):
+ @wsgi.deserializers(xml=XMLDeserializer)
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller, json=JSONDeserializer)
+
+ obj = resource.deserialize(controller.index, 'application/xml', 'foo')
+ self.assertEqual(obj, 'xml')
+
+ def test_register_actions(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ class ControllerExtended(wsgi.Controller):
+ @wsgi.action('fooAction')
+ def _action_foo(self, req, id, body):
+ return body
+
+ @wsgi.action('barAction')
+ def _action_bar(self, req, id, body):
+ return body
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ self.assertEqual({}, resource.wsgi_actions)
+
+ extended = ControllerExtended()
+ resource.register_actions(extended)
+ self.assertEqual({
+ 'fooAction': extended._action_foo,
+ 'barAction': extended._action_bar,
+ }, resource.wsgi_actions)
+
+ def test_register_extensions(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ class ControllerExtended(wsgi.Controller):
+ @wsgi.extends
+ def index(self, req, resp_obj, pants=None):
+ return None
+
+ @wsgi.extends(action='fooAction')
+ def _action_foo(self, req, resp, id, body):
+ return None
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+ self.assertEqual({}, resource.wsgi_extensions)
+ self.assertEqual({}, resource.wsgi_action_extensions)
+
+ extended = ControllerExtended()
+ resource.register_extensions(extended)
+ self.assertEqual({'index': [extended.index]}, resource.wsgi_extensions)
+ self.assertEqual({'fooAction': [extended._action_foo]},
+ resource.wsgi_action_extensions)
+
+ def test_get_method_extensions(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ class ControllerExtended(wsgi.Controller):
+ @wsgi.extends
+ def index(self, req, resp_obj, pants=None):
+ return None
+
+ controller = Controller()
+ extended = ControllerExtended()
+ resource = wsgi.Resource(controller)
+ resource.register_extensions(extended)
+ method, extensions = resource.get_method(None, 'index', None, '')
+ self.assertEqual(method, controller.index)
+ self.assertEqual(extensions, [extended.index])
+
+ def test_get_method_action_extensions(self):
+ class Controller(wsgi.Controller):
+ def index(self, req, pants=None):
+ return pants
+
+ @wsgi.action('fooAction')
+ def _action_foo(self, req, id, body):
+ return body
+
+ class ControllerExtended(wsgi.Controller):
+ @wsgi.extends(action='fooAction')
+ def _action_foo(self, req, resp_obj, id, body):
+ return None
+
+ controller = Controller()
+ extended = ControllerExtended()
+ resource = wsgi.Resource(controller)
+ resource.register_extensions(extended)
+ method, extensions = resource.get_method(None, 'action',
+ 'application/json',
+ '{"fooAction": true}')
+ self.assertEqual(method, controller._action_foo)
+ self.assertEqual(extensions, [extended._action_foo])
+
+ def test_get_method_action_whitelist_extensions(self):
+ class Controller(wsgi.Controller):
+ def index(self, req, pants=None):
+ return pants
+
+ class ControllerExtended(wsgi.Controller):
+ @wsgi.action('create')
+ def _create(self, req, body):
+ pass
+
+ @wsgi.action('delete')
+ def _delete(self, req, id):
+ pass
+
+ controller = Controller()
+ extended = ControllerExtended()
+ resource = wsgi.Resource(controller)
+ resource.register_actions(extended)
+
+ method, extensions = resource.get_method(None, 'create',
+ 'application/json',
+ '{"create": true}')
+ self.assertEqual(method, extended._create)
+ self.assertEqual(extensions, [])
+
+ method, extensions = resource.get_method(None, 'delete', None, None)
+ self.assertEqual(method, extended._delete)
+ self.assertEqual(extensions, [])
+
+ def test_pre_process_extensions_regular(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ called = []
+
+ def extension1(req, resp_obj):
+ called.append(1)
+ return None
+
+ def extension2(req, resp_obj):
+ called.append(2)
+ return None
+
+ extensions = [extension1, extension2]
+ response, post = resource.pre_process_extensions(extensions, None, {})
+ self.assertEqual(called, [])
+ self.assertIsNone(response)
+ self.assertEqual(list(post), [extension2, extension1])
+
+ def test_pre_process_extensions_generator(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ called = []
+
+ def extension1(req):
+ called.append('pre1')
+ yield
+ called.append('post1')
+
+ def extension2(req):
+ called.append('pre2')
+ yield
+ called.append('post2')
+
+ extensions = [extension1, extension2]
+ response, post = resource.pre_process_extensions(extensions, None, {})
+ post = list(post)
+ self.assertEqual(called, ['pre1', 'pre2'])
+ self.assertIsNone(response)
+ self.assertEqual(len(post), 2)
+ self.assertTrue(inspect.isgenerator(post[0]))
+ self.assertTrue(inspect.isgenerator(post[1]))
+
+ for gen in post:
+ try:
+ gen.send(None)
+ except StopIteration:
+ continue
+
+ self.assertEqual(called, ['pre1', 'pre2', 'post2', 'post1'])
+
+ def test_pre_process_extensions_generator_response(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ called = []
+
+ def extension1(req):
+ called.append('pre1')
+ yield 'foo'
+
+ def extension2(req):
+ called.append('pre2')
+
+ extensions = [extension1, extension2]
+ response, post = resource.pre_process_extensions(extensions, None, {})
+ self.assertEqual(called, ['pre1'])
+ self.assertEqual(response, 'foo')
+ self.assertEqual(post, [])
+
+ def test_post_process_extensions_regular(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ called = []
+
+ def extension1(req, resp_obj):
+ called.append(1)
+ return None
+
+ def extension2(req, resp_obj):
+ called.append(2)
+ return None
+
+ response = resource.post_process_extensions([extension2, extension1],
+ None, None, {})
+ self.assertEqual(called, [2, 1])
+ self.assertIsNone(response)
+
+ def test_post_process_extensions_regular_response(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ called = []
+
+ def extension1(req, resp_obj):
+ called.append(1)
+ return None
+
+ def extension2(req, resp_obj):
+ called.append(2)
+ return 'foo'
+
+ response = resource.post_process_extensions([extension2, extension1],
+ None, None, {})
+ self.assertEqual(called, [2])
+ self.assertEqual(response, 'foo')
+
+ def test_post_process_extensions_generator(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ called = []
+
+ def extension1(req):
+ yield
+ called.append(1)
+
+ def extension2(req):
+ yield
+ called.append(2)
+
+ ext1 = extension1(None)
+ ext1.next()
+ ext2 = extension2(None)
+ ext2.next()
+
+ response = resource.post_process_extensions([ext2, ext1],
+ None, None, {})
+
+ self.assertEqual(called, [2, 1])
+ self.assertIsNone(response)
+
+ def test_post_process_extensions_generator_response(self):
+ class Controller(object):
+ def index(self, req, pants=None):
+ return pants
+
+ controller = Controller()
+ resource = wsgi.Resource(controller)
+
+ called = []
+
+ def extension1(req):
+ yield
+ called.append(1)
+
+ def extension2(req):
+ yield
+ called.append(2)
+ yield 'foo'
+
+ ext1 = extension1(None)
+ ext1.next()
+ ext2 = extension2(None)
+ ext2.next()
+
+ response = resource.post_process_extensions([ext2, ext1],
+ None, None, {})
+
+ self.assertEqual(called, [2])
+ self.assertEqual(response, 'foo')
+
+ def test_resource_exception_handler_type_error(self):
+ # A TypeError should be translated to a Fault/HTTP 400.
+ def foo(a,):
+ return a
+
+ try:
+ with wsgi.ResourceExceptionHandler():
+ foo() # generate a TypeError
+ self.fail("Should have raised a Fault (HTTP 400)")
+ except wsgi.Fault as fault:
+ self.assertEqual(400, fault.status_int)
+
+ def test_resource_headers_are_utf8(self):
+ resp = webob.Response(status_int=202)
+ resp.headers['x-header1'] = 1
+ resp.headers['x-header2'] = u'header2'
+ resp.headers['x-header3'] = u'header3'
+
+ class Controller(object):
+ def index(self, req):
+ return resp
+
+ req = webob.Request.blank('/tests')
+ app = fakes.TestRouter(Controller())
+ response = req.get_response(app)
+
+ for hdr, val in response.headers.iteritems():
+ # All headers must be utf8
+ self.assertIsInstance(hdr, str)
+ self.assertIsInstance(val, str)
+ self.assertEqual(response.headers['x-header1'], '1')
+ self.assertEqual(response.headers['x-header2'], 'header2')
+ self.assertEqual(response.headers['x-header3'], 'header3')
+
+ def test_resource_valid_utf8_body(self):
+ class Controller(object):
+ def update(self, req, id, body):
+ return body
+
+ req = webob.Request.blank('/tests/test_id', method="PUT")
+ body = """ {"name": "\xe6\xa6\x82\xe5\xbf\xb5" } """
+ expected_body = '{"name": "\\u6982\\u5ff5"}'
+ req.body = body
+ req.headers['Content-Type'] = 'application/json'
+ app = fakes.TestRouter(Controller())
+ response = req.get_response(app)
+ self.assertEqual(response.body, expected_body)
+ self.assertEqual(response.status_int, 200)
+
+ def test_resource_invalid_utf8(self):
+ class Controller(object):
+ def update(self, req, id, body):
+ return body
+
+ req = webob.Request.blank('/tests/test_id', method="PUT")
+ body = """ {"name": "\xf0\x28\x8c\x28" } """
+ req.body = body
+ req.headers['Content-Type'] = 'application/json'
+ app = fakes.TestRouter(Controller())
+ self.assertRaises(UnicodeDecodeError, req.get_response, app)
+
+
+class ResponseObjectTest(test.NoDBTestCase):
+ def test_default_code(self):
+ robj = wsgi.ResponseObject({})
+ self.assertEqual(robj.code, 200)
+
+ def test_modified_code(self):
+ robj = wsgi.ResponseObject({})
+ robj._default_code = 202
+ self.assertEqual(robj.code, 202)
+
+ def test_override_default_code(self):
+ robj = wsgi.ResponseObject({}, code=404)
+ self.assertEqual(robj.code, 404)
+
+ def test_override_modified_code(self):
+ robj = wsgi.ResponseObject({}, code=404)
+ robj._default_code = 202
+ self.assertEqual(robj.code, 404)
+
+ def test_set_header(self):
+ robj = wsgi.ResponseObject({})
+ robj['Header'] = 'foo'
+ self.assertEqual(robj.headers, {'header': 'foo'})
+
+ def test_get_header(self):
+ robj = wsgi.ResponseObject({})
+ robj['Header'] = 'foo'
+ self.assertEqual(robj['hEADER'], 'foo')
+
+ def test_del_header(self):
+ robj = wsgi.ResponseObject({})
+ robj['Header'] = 'foo'
+ del robj['hEADER']
+ self.assertNotIn('header', robj.headers)
+
+ def test_header_isolation(self):
+ robj = wsgi.ResponseObject({})
+ robj['Header'] = 'foo'
+ hdrs = robj.headers
+ hdrs['hEADER'] = 'bar'
+ self.assertEqual(robj['hEADER'], 'foo')
+
+ def test_default_serializers(self):
+ robj = wsgi.ResponseObject({})
+ self.assertEqual(robj.serializers, {})
+
+ def test_bind_serializers(self):
+ robj = wsgi.ResponseObject({}, json='foo')
+ robj._bind_method_serializers(dict(xml='bar', json='baz'))
+ self.assertEqual(robj.serializers, dict(xml='bar', json='foo'))
+
+ def test_get_serializer(self):
+ robj = wsgi.ResponseObject({}, json='json', xml='xml', atom='atom')
+ for content_type, mtype in wsgi._MEDIA_TYPE_MAP.items():
+ _mtype, serializer = robj.get_serializer(content_type)
+ self.assertEqual(serializer, mtype)
+
+ def test_get_serializer_defaults(self):
+ robj = wsgi.ResponseObject({})
+ default_serializers = dict(json='json', xml='xml', atom='atom')
+ for content_type, mtype in wsgi._MEDIA_TYPE_MAP.items():
+ self.assertRaises(exception.InvalidContentType,
+ robj.get_serializer, content_type)
+ _mtype, serializer = robj.get_serializer(content_type,
+ default_serializers)
+ self.assertEqual(serializer, mtype)
+
+ def test_serialize(self):
+ class JSONSerializer(object):
+ def serialize(self, obj):
+ return 'json'
+
+ class XMLSerializer(object):
+ def serialize(self, obj):
+ return 'xml'
+
+ class AtomSerializer(object):
+ def serialize(self, obj):
+ return 'atom'
+
+ robj = wsgi.ResponseObject({}, code=202,
+ json=JSONSerializer,
+ xml=XMLSerializer,
+ atom=AtomSerializer)
+ robj['X-header1'] = 'header1'
+ robj['X-header2'] = 'header2'
+ robj['X-header3'] = 3
+ robj['X-header-unicode'] = u'header-unicode'
+
+ for content_type, mtype in wsgi._MEDIA_TYPE_MAP.items():
+ request = wsgi.Request.blank('/tests/123')
+ response = robj.serialize(request, content_type)
+
+ self.assertEqual(response.headers['Content-Type'], content_type)
+ for hdr, val in response.headers.iteritems():
+ # All headers must be utf8
+ self.assertIsInstance(hdr, str)
+ self.assertIsInstance(val, str)
+ self.assertEqual(response.headers['X-header1'], 'header1')
+ self.assertEqual(response.headers['X-header2'], 'header2')
+ self.assertEqual(response.headers['X-header3'], '3')
+ self.assertEqual(response.status_int, 202)
+ self.assertEqual(response.body, mtype)
+
+
+class ValidBodyTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(ValidBodyTest, self).setUp()
+ self.controller = wsgi.Controller()
+
+ def test_is_valid_body(self):
+ body = {'foo': {}}
+ self.assertTrue(self.controller.is_valid_body(body, 'foo'))
+
+ def test_is_valid_body_none(self):
+ wsgi.Resource(controller=None)
+ self.assertFalse(self.controller.is_valid_body(None, 'foo'))
+
+ def test_is_valid_body_empty(self):
+ wsgi.Resource(controller=None)
+ self.assertFalse(self.controller.is_valid_body({}, 'foo'))
+
+ def test_is_valid_body_no_entity(self):
+ wsgi.Resource(controller=None)
+ body = {'bar': {}}
+ self.assertFalse(self.controller.is_valid_body(body, 'foo'))
+
+ def test_is_valid_body_malformed_entity(self):
+ wsgi.Resource(controller=None)
+ body = {'foo': 'bar'}
+ self.assertFalse(self.controller.is_valid_body(body, 'foo'))
diff --git a/nova/tests/unit/api/openstack/test_xmlutil.py b/nova/tests/unit/api/openstack/test_xmlutil.py
new file mode 100644
index 0000000000..19186889bb
--- /dev/null
+++ b/nova/tests/unit/api/openstack/test_xmlutil.py
@@ -0,0 +1,948 @@
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from xml.dom import minidom
+
+from lxml import etree
+
+from nova.api.openstack import xmlutil
+from nova import exception
+from nova import test
+from nova.tests.unit import utils as tests_utils
+
+
+class SelectorTest(test.NoDBTestCase):
+ obj_for_test = {
+ 'test': {
+ 'name': 'test',
+ 'values': [1, 2, 3],
+ 'attrs': {
+ 'foo': 1,
+ 'bar': 2,
+ 'baz': 3,
+ },
+ },
+ }
+
+ def test_repr(self):
+ sel = xmlutil.Selector()
+ self.assertEqual(repr(sel), "Selector()")
+
+ def test_empty_selector(self):
+ sel = xmlutil.EmptyStringSelector()
+ self.assertEqual(len(sel.chain), 0)
+ self.assertEqual(sel(self.obj_for_test), self.obj_for_test)
+ self.assertEqual(
+ repr(self.obj_for_test),
+ "{'test': {'values': [1, 2, 3], 'name': 'test', 'attrs': "
+ "{'baz': 3, 'foo': 1, 'bar': 2}}}")
+
+ def test_dict_selector(self):
+ sel = xmlutil.Selector('test')
+ self.assertEqual(len(sel.chain), 1)
+ self.assertEqual(sel.chain[0], 'test')
+ self.assertEqual(sel(self.obj_for_test),
+ self.obj_for_test['test'])
+
+ def test_datum_selector(self):
+ sel = xmlutil.Selector('test', 'name')
+ self.assertEqual(len(sel.chain), 2)
+ self.assertEqual(sel.chain[0], 'test')
+ self.assertEqual(sel.chain[1], 'name')
+ self.assertEqual(sel(self.obj_for_test), 'test')
+
+ def test_list_selector(self):
+ sel = xmlutil.Selector('test', 'values', 0)
+ self.assertEqual(len(sel.chain), 3)
+ self.assertEqual(sel.chain[0], 'test')
+ self.assertEqual(sel.chain[1], 'values')
+ self.assertEqual(sel.chain[2], 0)
+ self.assertEqual(sel(self.obj_for_test), 1)
+
+ def test_items_selector(self):
+ sel = xmlutil.Selector('test', 'attrs', xmlutil.get_items)
+ self.assertEqual(len(sel.chain), 3)
+ self.assertEqual(sel.chain[2], xmlutil.get_items)
+ for key, val in sel(self.obj_for_test):
+ self.assertEqual(self.obj_for_test['test']['attrs'][key], val)
+
+ def test_missing_key_selector(self):
+ sel = xmlutil.Selector('test2', 'attrs')
+ self.assertIsNone(sel(self.obj_for_test))
+ self.assertRaises(KeyError, sel, self.obj_for_test, True)
+
+ def test_constant_selector(self):
+ sel = xmlutil.ConstantSelector('Foobar')
+ self.assertEqual(sel.value, 'Foobar')
+ self.assertEqual(sel(self.obj_for_test), 'Foobar')
+ self.assertEqual(repr(sel), "'Foobar'")
+
+
+class TemplateElementTest(test.NoDBTestCase):
+ def test_element_initial_attributes(self):
+ # Create a template element with some attributes
+ elem = xmlutil.TemplateElement('test', attrib=dict(a=1, b=2, c=3),
+ c=4, d=5, e=6)
+
+ # Verify all the attributes are as expected
+ expected = dict(a=1, b=2, c=4, d=5, e=6)
+ for k, v in expected.items():
+ self.assertEqual(elem.attrib[k].chain[0], v)
+ self.assertTrue(repr(elem))
+
+ def test_element_get_attributes(self):
+ expected = dict(a=1, b=2, c=3)
+
+ # Create a template element with some attributes
+ elem = xmlutil.TemplateElement('test', attrib=expected)
+
+ # Verify that get() retrieves the attributes
+ for k, v in expected.items():
+ self.assertEqual(elem.get(k).chain[0], v)
+
+ def test_element_set_attributes(self):
+ attrs = dict(a=None, b='foo', c=xmlutil.Selector('foo', 'bar'))
+
+ # Create a bare template element with no attributes
+ elem = xmlutil.TemplateElement('test')
+
+ # Set the attribute values
+ for k, v in attrs.items():
+ elem.set(k, v)
+
+ # Now verify what got set
+ self.assertEqual(len(elem.attrib['a'].chain), 1)
+ self.assertEqual(elem.attrib['a'].chain[0], 'a')
+ self.assertEqual(len(elem.attrib['b'].chain), 1)
+ self.assertEqual(elem.attrib['b'].chain[0], 'foo')
+ self.assertEqual(elem.attrib['c'], attrs['c'])
+
+ def test_element_attribute_keys(self):
+ attrs = dict(a=1, b=2, c=3, d=4)
+ expected = set(attrs.keys())
+
+ # Create a template element with some attributes
+ elem = xmlutil.TemplateElement('test', attrib=attrs)
+
+ # Now verify keys
+ self.assertEqual(set(elem.keys()), expected)
+
+ def test_element_attribute_items(self):
+ expected = dict(a=xmlutil.Selector(1),
+ b=xmlutil.Selector(2),
+ c=xmlutil.Selector(3))
+ keys = set(expected.keys())
+
+ # Create a template element with some attributes
+ elem = xmlutil.TemplateElement('test', attrib=expected)
+
+ # Now verify items
+ for k, v in elem.items():
+ self.assertEqual(expected[k], v)
+ keys.remove(k)
+
+ # Did we visit all keys?
+ self.assertEqual(len(keys), 0)
+
+ def test_element_selector_none(self):
+ # Create a template element with no selector
+ elem = xmlutil.TemplateElement('test')
+
+ self.assertEqual(len(elem.selector.chain), 0)
+
+ def test_element_selector_string(self):
+ # Create a template element with a string selector
+ elem = xmlutil.TemplateElement('test', selector='test')
+
+ self.assertEqual(len(elem.selector.chain), 1)
+ self.assertEqual(elem.selector.chain[0], 'test')
+
+ def test_element_selector(self):
+ sel = xmlutil.Selector('a', 'b')
+
+ # Create a template element with an explicit selector
+ elem = xmlutil.TemplateElement('test', selector=sel)
+
+ self.assertEqual(elem.selector, sel)
+
+ def test_element_subselector_none(self):
+ # Create a template element with no subselector
+ elem = xmlutil.TemplateElement('test')
+
+ self.assertIsNone(elem.subselector)
+
+ def test_element_subselector_string(self):
+ # Create a template element with a string subselector
+ elem = xmlutil.TemplateElement('test', subselector='test')
+
+ self.assertEqual(len(elem.subselector.chain), 1)
+ self.assertEqual(elem.subselector.chain[0], 'test')
+
+ def test_element_subselector(self):
+ sel = xmlutil.Selector('a', 'b')
+
+ # Create a template element with an explicit subselector
+ elem = xmlutil.TemplateElement('test', subselector=sel)
+
+ self.assertEqual(elem.subselector, sel)
+
+ def test_element_append_child(self):
+ # Create an element
+ elem = xmlutil.TemplateElement('test')
+
+ # Make sure the element starts off empty
+ self.assertEqual(len(elem), 0)
+
+ # Create a child element
+ child = xmlutil.TemplateElement('child')
+
+ # Append the child to the parent
+ elem.append(child)
+
+ # Verify that the child was added
+ self.assertEqual(len(elem), 1)
+ self.assertEqual(elem[0], child)
+ self.assertIn('child', elem)
+ self.assertEqual(elem['child'], child)
+
+ # Ensure that multiple children of the same name are rejected
+ child2 = xmlutil.TemplateElement('child')
+ self.assertRaises(KeyError, elem.append, child2)
+
+ def test_element_extend_children(self):
+ # Create an element
+ elem = xmlutil.TemplateElement('test')
+
+ # Make sure the element starts off empty
+ self.assertEqual(len(elem), 0)
+
+ # Create a few children
+ children = [
+ xmlutil.TemplateElement('child1'),
+ xmlutil.TemplateElement('child2'),
+ xmlutil.TemplateElement('child3'),
+ ]
+
+ # Extend the parent by those children
+ elem.extend(children)
+
+ # Verify that the children were added
+ self.assertEqual(len(elem), 3)
+ for idx in range(len(elem)):
+ self.assertEqual(children[idx], elem[idx])
+ self.assertIn(children[idx].tag, elem)
+ self.assertEqual(elem[children[idx].tag], children[idx])
+
+ # Ensure that multiple children of the same name are rejected
+ children2 = [
+ xmlutil.TemplateElement('child4'),
+ xmlutil.TemplateElement('child1'),
+ ]
+ self.assertRaises(KeyError, elem.extend, children2)
+
+ # Also ensure that child4 was not added
+ self.assertEqual(len(elem), 3)
+ self.assertEqual(elem[-1].tag, 'child3')
+
+ def test_element_insert_child(self):
+ # Create an element
+ elem = xmlutil.TemplateElement('test')
+
+ # Make sure the element starts off empty
+ self.assertEqual(len(elem), 0)
+
+ # Create a few children
+ children = [
+ xmlutil.TemplateElement('child1'),
+ xmlutil.TemplateElement('child2'),
+ xmlutil.TemplateElement('child3'),
+ ]
+
+ # Extend the parent by those children
+ elem.extend(children)
+
+ # Create a child to insert
+ child = xmlutil.TemplateElement('child4')
+
+ # Insert it
+ elem.insert(1, child)
+
+ # Ensure the child was inserted in the right place
+ self.assertEqual(len(elem), 4)
+ children.insert(1, child)
+ for idx in range(len(elem)):
+ self.assertEqual(children[idx], elem[idx])
+ self.assertIn(children[idx].tag, elem)
+ self.assertEqual(elem[children[idx].tag], children[idx])
+
+ # Ensure that multiple children of the same name are rejected
+ child2 = xmlutil.TemplateElement('child2')
+ self.assertRaises(KeyError, elem.insert, 2, child2)
+
+ def test_element_remove_child(self):
+ # Create an element
+ elem = xmlutil.TemplateElement('test')
+
+ # Make sure the element starts off empty
+ self.assertEqual(len(elem), 0)
+
+ # Create a few children
+ children = [
+ xmlutil.TemplateElement('child1'),
+ xmlutil.TemplateElement('child2'),
+ xmlutil.TemplateElement('child3'),
+ ]
+
+ # Extend the parent by those children
+ elem.extend(children)
+
+ # Create a test child to remove
+ child = xmlutil.TemplateElement('child2')
+
+ # Try to remove it
+ self.assertRaises(ValueError, elem.remove, child)
+
+ # Ensure that no child was removed
+ self.assertEqual(len(elem), 3)
+
+ # Now remove a legitimate child
+ elem.remove(children[1])
+
+ # Ensure that the child was removed
+ self.assertEqual(len(elem), 2)
+ self.assertEqual(elem[0], children[0])
+ self.assertEqual(elem[1], children[2])
+ self.assertEqual('child2' in elem, False)
+
+ # Ensure the child cannot be retrieved by name
+ def get_key(elem, key):
+ return elem[key]
+ self.assertRaises(KeyError, get_key, elem, 'child2')
+
+ def test_element_text(self):
+ # Create an element
+ elem = xmlutil.TemplateElement('test')
+
+ # Ensure that it has no text
+ self.assertIsNone(elem.text)
+
+ # Try setting it to a string and ensure it becomes a selector
+ elem.text = 'test'
+ self.assertEqual(hasattr(elem.text, 'chain'), True)
+ self.assertEqual(len(elem.text.chain), 1)
+ self.assertEqual(elem.text.chain[0], 'test')
+
+ # Try resetting the text to None
+ elem.text = None
+ self.assertIsNone(elem.text)
+
+ # Now make up a selector and try setting the text to that
+ sel = xmlutil.Selector()
+ elem.text = sel
+ self.assertEqual(elem.text, sel)
+
+ # Finally, try deleting the text and see what happens
+ del elem.text
+ self.assertIsNone(elem.text)
+
+ def test_apply_attrs(self):
+ # Create a template element
+ attrs = dict(attr1=xmlutil.ConstantSelector(1),
+ attr2=xmlutil.ConstantSelector(2))
+ tmpl_elem = xmlutil.TemplateElement('test', attrib=attrs)
+
+ # Create an etree element
+ elem = etree.Element('test')
+
+ # Apply the template to the element
+ tmpl_elem.apply(elem, None)
+
+ # Now, verify the correct attributes were set
+ for k, v in elem.items():
+ self.assertEqual(str(attrs[k].value), v)
+
+ def test_apply_text(self):
+ # Create a template element
+ tmpl_elem = xmlutil.TemplateElement('test')
+ tmpl_elem.text = xmlutil.ConstantSelector(1)
+
+ # Create an etree element
+ elem = etree.Element('test')
+
+ # Apply the template to the element
+ tmpl_elem.apply(elem, None)
+
+ # Now, verify the text was set
+ self.assertEqual(str(tmpl_elem.text.value), elem.text)
+
+ def test__render(self):
+ attrs = dict(attr1=xmlutil.ConstantSelector(1),
+ attr2=xmlutil.ConstantSelector(2),
+ attr3=xmlutil.ConstantSelector(3))
+
+ # Create a master template element
+ master_elem = xmlutil.TemplateElement('test', attr1=attrs['attr1'])
+
+ # Create a couple of slave template element
+ slave_elems = [
+ xmlutil.TemplateElement('test', attr2=attrs['attr2']),
+ xmlutil.TemplateElement('test', attr3=attrs['attr3']),
+ ]
+
+ # Try the render
+ elem = master_elem._render(None, None, slave_elems, None)
+
+ # Verify the particulars of the render
+ self.assertEqual(elem.tag, 'test')
+ self.assertEqual(len(elem.nsmap), 0)
+ for k, v in elem.items():
+ self.assertEqual(str(attrs[k].value), v)
+
+ # Create a parent for the element to be rendered
+ parent = etree.Element('parent')
+
+ # Try the render again...
+ elem = master_elem._render(parent, None, slave_elems, dict(a='foo'))
+
+ # Verify the particulars of the render
+ self.assertEqual(len(parent), 1)
+ self.assertEqual(parent[0], elem)
+ self.assertEqual(len(elem.nsmap), 1)
+ self.assertEqual(elem.nsmap['a'], 'foo')
+
+ def test_render(self):
+ # Create a template element
+ tmpl_elem = xmlutil.TemplateElement('test')
+ tmpl_elem.text = xmlutil.Selector()
+
+ # Create the object we're going to render
+ obj = ['elem1', 'elem2', 'elem3', 'elem4']
+
+ # Try a render with no object
+ elems = tmpl_elem.render(None, None)
+ self.assertEqual(len(elems), 0)
+
+ # Try a render with one object
+ elems = tmpl_elem.render(None, 'foo')
+ self.assertEqual(len(elems), 1)
+ self.assertEqual(elems[0][0].text, 'foo')
+ self.assertEqual(elems[0][1], 'foo')
+
+ # Now, try rendering an object with multiple entries
+ parent = etree.Element('parent')
+ elems = tmpl_elem.render(parent, obj)
+ self.assertEqual(len(elems), 4)
+
+ # Check the results
+ for idx in range(len(obj)):
+ self.assertEqual(elems[idx][0].text, obj[idx])
+ self.assertEqual(elems[idx][1], obj[idx])
+
+ # Check with a subselector
+ tmpl_elem = xmlutil.TemplateElement(
+ 'test',
+ subselector=xmlutil.ConstantSelector('foo'))
+ parent = etree.Element('parent')
+
+ # Try a render with no object
+ elems = tmpl_elem.render(parent, obj)
+ self.assertEqual(len(elems), 4)
+
+ def test_subelement(self):
+ # Try the SubTemplateElement constructor
+ parent = xmlutil.SubTemplateElement(None, 'parent')
+ self.assertEqual(parent.tag, 'parent')
+ self.assertEqual(len(parent), 0)
+
+ # Now try it with a parent element
+ child = xmlutil.SubTemplateElement(parent, 'child')
+ self.assertEqual(child.tag, 'child')
+ self.assertEqual(len(parent), 1)
+ self.assertEqual(parent[0], child)
+
+ def test_wrap(self):
+ # These are strange methods, but they make things easier
+ elem = xmlutil.TemplateElement('test')
+ self.assertEqual(elem.unwrap(), elem)
+ self.assertEqual(elem.wrap().root, elem)
+
+ def test_dyntag(self):
+ obj = ['a', 'b', 'c']
+
+ # Create a template element with a dynamic tag
+ tmpl_elem = xmlutil.TemplateElement(xmlutil.Selector())
+
+ # Try the render
+ parent = etree.Element('parent')
+ elems = tmpl_elem.render(parent, obj)
+
+ # Verify the particulars of the render
+ self.assertEqual(len(elems), len(obj))
+ for idx in range(len(obj)):
+ self.assertEqual(elems[idx][0].tag, obj[idx])
+
+ def test_tree(self):
+ # Create a template element
+ elem = xmlutil.TemplateElement('test', attr3='attr3')
+ elem.text = 'test'
+ self.assertEqual(elem.tree(),
+ "<test !selector=Selector() "
+ "!text=Selector('test',) "
+ "attr3=Selector('attr3',)"
+ "/>")
+
+ # Create a template element
+ elem = xmlutil.TemplateElement('test2')
+
+ # Create a child element
+ child = xmlutil.TemplateElement('child')
+
+ # Append the child to the parent
+ elem.append(child)
+
+ self.assertEqual(elem.tree(),
+ "<test2 !selector=Selector()>"
+ "<child !selector=Selector()/></test2>")
+
+
+class TemplateTest(test.NoDBTestCase):
+ def test_tree(self):
+ elem = xmlutil.TemplateElement('test')
+ tmpl = xmlutil.Template(elem)
+ self.assertTrue(tmpl.tree())
+
+ def test_wrap(self):
+ # These are strange methods, but they make things easier
+ elem = xmlutil.TemplateElement('test')
+ tmpl = xmlutil.Template(elem)
+ self.assertEqual(tmpl.unwrap(), elem)
+ self.assertEqual(tmpl.wrap(), tmpl)
+
+ def test__siblings(self):
+ # Set up a basic template
+ elem = xmlutil.TemplateElement('test')
+ tmpl = xmlutil.Template(elem)
+
+ # Check that we get the right siblings
+ siblings = tmpl._siblings()
+ self.assertEqual(len(siblings), 1)
+ self.assertEqual(siblings[0], elem)
+
+ def test__nsmap(self):
+ # Set up a basic template
+ elem = xmlutil.TemplateElement('test')
+ tmpl = xmlutil.Template(elem, nsmap=dict(a="foo"))
+
+ # Check out that we get the right namespace dictionary
+ nsmap = tmpl._nsmap()
+ self.assertNotEqual(id(nsmap), id(tmpl.nsmap))
+ self.assertEqual(len(nsmap), 1)
+ self.assertEqual(nsmap['a'], 'foo')
+
+ def test_master_attach(self):
+ # Set up a master template
+ elem = xmlutil.TemplateElement('test')
+ tmpl = xmlutil.MasterTemplate(elem, 1)
+
+ # Make sure it has a root but no slaves
+ self.assertEqual(tmpl.root, elem)
+ self.assertEqual(len(tmpl.slaves), 0)
+ self.assertTrue(repr(tmpl))
+
+ # Try to attach an invalid slave
+ bad_elem = xmlutil.TemplateElement('test2')
+ self.assertRaises(ValueError, tmpl.attach, bad_elem)
+ self.assertEqual(len(tmpl.slaves), 0)
+
+ # Try to attach an invalid and a valid slave
+ good_elem = xmlutil.TemplateElement('test')
+ self.assertRaises(ValueError, tmpl.attach, good_elem, bad_elem)
+ self.assertEqual(len(tmpl.slaves), 0)
+
+ # Try to attach an inapplicable template
+ class InapplicableTemplate(xmlutil.Template):
+ def apply(self, master):
+ return False
+ inapp_tmpl = InapplicableTemplate(good_elem)
+ tmpl.attach(inapp_tmpl)
+ self.assertEqual(len(tmpl.slaves), 0)
+
+ # Now try attaching an applicable template
+ tmpl.attach(good_elem)
+ self.assertEqual(len(tmpl.slaves), 1)
+ self.assertEqual(tmpl.slaves[0].root, good_elem)
+
+ def test_master_copy(self):
+ # Construct a master template
+ elem = xmlutil.TemplateElement('test')
+ tmpl = xmlutil.MasterTemplate(elem, 1, nsmap=dict(a='foo'))
+
+ # Give it a slave
+ slave = xmlutil.TemplateElement('test')
+ tmpl.attach(slave)
+
+ # Construct a copy
+ copy = tmpl.copy()
+
+ # Check to see if we actually managed a copy
+ self.assertNotEqual(tmpl, copy)
+ self.assertEqual(tmpl.root, copy.root)
+ self.assertEqual(tmpl.version, copy.version)
+ self.assertEqual(id(tmpl.nsmap), id(copy.nsmap))
+ self.assertNotEqual(id(tmpl.slaves), id(copy.slaves))
+ self.assertEqual(len(tmpl.slaves), len(copy.slaves))
+ self.assertEqual(tmpl.slaves[0], copy.slaves[0])
+
+ def test_slave_apply(self):
+ # Construct a master template
+ elem = xmlutil.TemplateElement('test')
+ master = xmlutil.MasterTemplate(elem, 3)
+
+ # Construct a slave template with applicable minimum version
+ slave = xmlutil.SlaveTemplate(elem, 2)
+ self.assertEqual(slave.apply(master), True)
+ self.assertTrue(repr(slave))
+
+ # Construct a slave template with equal minimum version
+ slave = xmlutil.SlaveTemplate(elem, 3)
+ self.assertEqual(slave.apply(master), True)
+
+ # Construct a slave template with inapplicable minimum version
+ slave = xmlutil.SlaveTemplate(elem, 4)
+ self.assertEqual(slave.apply(master), False)
+
+ # Construct a slave template with applicable version range
+ slave = xmlutil.SlaveTemplate(elem, 2, 4)
+ self.assertEqual(slave.apply(master), True)
+
+ # Construct a slave template with low version range
+ slave = xmlutil.SlaveTemplate(elem, 1, 2)
+ self.assertEqual(slave.apply(master), False)
+
+ # Construct a slave template with high version range
+ slave = xmlutil.SlaveTemplate(elem, 4, 5)
+ self.assertEqual(slave.apply(master), False)
+
+ # Construct a slave template with matching version range
+ slave = xmlutil.SlaveTemplate(elem, 3, 3)
+ self.assertEqual(slave.apply(master), True)
+
+ def test__serialize(self):
+ # Our test object to serialize
+ obj = {
+ 'test': {
+ 'name': 'foobar',
+ 'values': [1, 2, 3, 4],
+ 'attrs': {
+ 'a': 1,
+ 'b': 2,
+ 'c': 3,
+ 'd': 4,
+ },
+ 'image': {
+ 'name': 'image_foobar',
+ 'id': 42,
+ },
+ },
+ }
+
+ # Set up our master template
+ root = xmlutil.TemplateElement('test', selector='test',
+ name='name')
+ value = xmlutil.SubTemplateElement(root, 'value', selector='values')
+ value.text = xmlutil.Selector()
+ attrs = xmlutil.SubTemplateElement(root, 'attrs', selector='attrs')
+ xmlutil.SubTemplateElement(attrs, 'attr', selector=xmlutil.get_items,
+ key=0, value=1)
+ master = xmlutil.MasterTemplate(root, 1, nsmap=dict(f='foo'))
+
+ # Set up our slave template
+ root_slave = xmlutil.TemplateElement('test', selector='test')
+ image = xmlutil.SubTemplateElement(root_slave, 'image',
+ selector='image', id='id')
+ image.text = xmlutil.Selector('name')
+ slave = xmlutil.SlaveTemplate(root_slave, 1, nsmap=dict(b='bar'))
+
+ # Attach the slave to the master...
+ master.attach(slave)
+
+ # Try serializing our object
+ siblings = master._siblings()
+ nsmap = master._nsmap()
+ result = master._serialize(None, obj, siblings, nsmap)
+
+ # Now we get to manually walk the element tree...
+ self.assertEqual(result.tag, 'test')
+ self.assertEqual(len(result.nsmap), 2)
+ self.assertEqual(result.nsmap['f'], 'foo')
+ self.assertEqual(result.nsmap['b'], 'bar')
+ self.assertEqual(result.get('name'), obj['test']['name'])
+ for idx, val in enumerate(obj['test']['values']):
+ self.assertEqual(result[idx].tag, 'value')
+ self.assertEqual(result[idx].text, str(val))
+ idx += 1
+ self.assertEqual(result[idx].tag, 'attrs')
+ for attr in result[idx]:
+ self.assertEqual(attr.tag, 'attr')
+ self.assertEqual(attr.get('value'),
+ str(obj['test']['attrs'][attr.get('key')]))
+ idx += 1
+ self.assertEqual(result[idx].tag, 'image')
+ self.assertEqual(result[idx].get('id'),
+ str(obj['test']['image']['id']))
+ self.assertEqual(result[idx].text, obj['test']['image']['name'])
+
+ templ = xmlutil.Template(None)
+ self.assertEqual(templ.serialize(None), '')
+
+ def test_serialize_with_colon_tagname_support(self):
+ # Our test object to serialize
+ obj = {'extra_specs': {'foo:bar': '999'}}
+ expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<extra_specs><foo:bar xmlns:foo="foo">999</foo:bar>'
+ '</extra_specs>'))
+ # Set up our master template
+ root = xmlutil.TemplateElement('extra_specs', selector='extra_specs',
+ colon_ns=True)
+ value = xmlutil.SubTemplateElement(root, 'foo:bar', selector='foo:bar',
+ colon_ns=True)
+ value.text = xmlutil.Selector()
+ master = xmlutil.MasterTemplate(root, 1)
+ result = master.serialize(obj)
+ self.assertEqual(expected_xml, result)
+
+ def test__serialize_with_empty_datum_selector(self):
+ # Our test object to serialize
+ obj = {
+ 'test': {
+ 'name': 'foobar',
+ 'image': ''
+ },
+ }
+
+ root = xmlutil.TemplateElement('test', selector='test',
+ name='name')
+ master = xmlutil.MasterTemplate(root, 1)
+ root_slave = xmlutil.TemplateElement('test', selector='test')
+ image = xmlutil.SubTemplateElement(root_slave, 'image',
+ selector='image')
+ image.set('id')
+ xmlutil.make_links(image, 'links')
+ slave = xmlutil.SlaveTemplate(root_slave, 1)
+ master.attach(slave)
+
+ siblings = master._siblings()
+ result = master._serialize(None, obj, siblings)
+ self.assertEqual(result.tag, 'test')
+ self.assertEqual(result[0].tag, 'image')
+ self.assertEqual(result[0].get('id'), str(obj['test']['image']))
+
+
+class MasterTemplateBuilder(xmlutil.TemplateBuilder):
+ def construct(self):
+ elem = xmlutil.TemplateElement('test')
+ return xmlutil.MasterTemplate(elem, 1)
+
+
+class SlaveTemplateBuilder(xmlutil.TemplateBuilder):
+ def construct(self):
+ elem = xmlutil.TemplateElement('test')
+ return xmlutil.SlaveTemplate(elem, 1)
+
+
+class TemplateBuilderTest(test.NoDBTestCase):
+ def test_master_template_builder(self):
+ # Make sure the template hasn't been built yet
+ self.assertIsNone(MasterTemplateBuilder._tmpl)
+
+ # Now, construct the template
+ tmpl1 = MasterTemplateBuilder()
+
+ # Make sure that there is a template cached...
+ self.assertIsNotNone(MasterTemplateBuilder._tmpl)
+
+ # Make sure it wasn't what was returned...
+ self.assertNotEqual(MasterTemplateBuilder._tmpl, tmpl1)
+
+ # Make sure it doesn't get rebuilt
+ cached = MasterTemplateBuilder._tmpl
+ tmpl2 = MasterTemplateBuilder()
+ self.assertEqual(MasterTemplateBuilder._tmpl, cached)
+
+ # Make sure we're always getting fresh copies
+ self.assertNotEqual(tmpl1, tmpl2)
+
+ # Make sure we can override the copying behavior
+ tmpl3 = MasterTemplateBuilder(False)
+ self.assertEqual(MasterTemplateBuilder._tmpl, tmpl3)
+
+ def test_slave_template_builder(self):
+ # Make sure the template hasn't been built yet
+ self.assertIsNone(SlaveTemplateBuilder._tmpl)
+
+ # Now, construct the template
+ tmpl1 = SlaveTemplateBuilder()
+
+ # Make sure there is a template cached...
+ self.assertIsNotNone(SlaveTemplateBuilder._tmpl)
+
+ # Make sure it was what was returned...
+ self.assertEqual(SlaveTemplateBuilder._tmpl, tmpl1)
+
+ # Make sure it doesn't get rebuilt
+ tmpl2 = SlaveTemplateBuilder()
+ self.assertEqual(SlaveTemplateBuilder._tmpl, tmpl1)
+
+ # Make sure we're always getting the cached copy
+ self.assertEqual(tmpl1, tmpl2)
+
+
+class MiscellaneousXMLUtilTests(test.NoDBTestCase):
+ def test_validate_schema(self):
+ xml = '''<?xml version='1.0' encoding='UTF-8'?>
+<metadata xmlns="http://docs.openstack.org/compute/api/v1.1">
+<meta key="key6">value6</meta><meta key="key4">value4</meta>
+</metadata>
+'''
+ xmlutil.validate_schema(xml, 'metadata')
+ # No way to test the return value of validate_schema.
+ # It just raises an exception when something is wrong.
+ self.assertTrue(True)
+
+ def test_make_links(self):
+ elem = xmlutil.TemplateElement('image', selector='image')
+ self.assertTrue(repr(xmlutil.make_links(elem, 'links')))
+
+ def test_make_flat_dict(self):
+ expected_xml = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<wrapper><a>foo</a><b>bar</b></wrapper>')
+ root = xmlutil.make_flat_dict('wrapper')
+ tmpl = xmlutil.MasterTemplate(root, 1)
+ result = tmpl.serialize(dict(wrapper=dict(a='foo', b='bar')))
+ self.assertEqual(result, expected_xml)
+
+ expected_xml = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+'<ns0:wrapper xmlns:ns0="ns"><ns0:a>foo</ns0:a><ns0:b>bar</ns0:b>'
+"</ns0:wrapper>")
+ root = xmlutil.make_flat_dict('wrapper', ns='ns')
+ tmpl = xmlutil.MasterTemplate(root, 1)
+ result = tmpl.serialize(dict(wrapper=dict(a='foo', b='bar')))
+ self.assertEqual(result, expected_xml)
+
+ def test_make_flat_dict_with_colon_tagname_support(self):
+ # Our test object to serialize
+ obj = {'extra_specs': {'foo:bar': '999'}}
+ expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<extra_specs><foo:bar xmlns:foo="foo">999</foo:bar>'
+ '</extra_specs>'))
+ # Set up our master template
+ root = xmlutil.make_flat_dict('extra_specs', colon_ns=True)
+ master = xmlutil.MasterTemplate(root, 1)
+ result = master.serialize(obj)
+ self.assertEqual(expected_xml, result)
+
+ def test_make_flat_dict_with_parent(self):
+ # Our test object to serialize
+ obj = {"device": {"id": 1,
+ "extra_info": {"key1": "value1",
+ "key2": "value2"}}}
+
+ expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<device id="1"><extra_info><key2>value2</key2>'
+ '<key1>value1</key1></extra_info></device>'))
+
+ root = xmlutil.TemplateElement('device', selector='device')
+ root.set('id')
+ extra = xmlutil.make_flat_dict('extra_info', root=root)
+ root.append(extra)
+ master = xmlutil.MasterTemplate(root, 1)
+ result = master.serialize(obj)
+ self.assertEqual(expected_xml, result)
+
+ def test_make_flat_dict_with_dicts(self):
+ # Our test object to serialize
+ obj = {"device": {"id": 1,
+ "extra_info": {"key1": "value1",
+ "key2": "value2"}}}
+
+ expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<device><id>1</id><extra_info><key2>value2</key2>'
+ '<key1>value1</key1></extra_info></device>'))
+
+ root = xmlutil.make_flat_dict('device', selector='device',
+ ignore_sub_dicts=True)
+ extra = xmlutil.make_flat_dict('extra_info', selector='extra_info')
+ root.append(extra)
+ master = xmlutil.MasterTemplate(root, 1)
+ result = master.serialize(obj)
+ self.assertEqual(expected_xml, result)
+
+ def test_safe_parse_xml(self):
+
+ normal_body = ('<?xml version="1.0" ?>'
+ '<foo><bar><v1>hey</v1><v2>there</v2></bar></foo>')
+
+ dom = xmlutil.safe_minidom_parse_string(normal_body)
+ # Some versions of minidom inject extra newlines so we ignore them
+ result = str(dom.toxml()).replace('\n', '')
+ self.assertEqual(normal_body, result)
+
+ self.assertRaises(exception.MalformedRequestBody,
+ xmlutil.safe_minidom_parse_string,
+ tests_utils.killer_xml_body())
+
+
+class SafeParserTestCase(test.NoDBTestCase):
+ def test_external_dtd(self):
+ xml_string = ("""<?xml version="1.0" encoding="utf-8"?>
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+ <html>
+ <head/>
+ <body>html with dtd</body>
+ </html>""")
+
+ parser = xmlutil.ProtectedExpatParser(forbid_dtd=False,
+ forbid_entities=True)
+ self.assertRaises(ValueError,
+ minidom.parseString,
+ xml_string, parser)
+
+ def test_external_file(self):
+ xml_string = """<!DOCTYPE external [
+ <!ENTITY ee SYSTEM "file:///PATH/TO/root.xml">
+ ]>
+ <root>&ee;</root>"""
+
+ parser = xmlutil.ProtectedExpatParser(forbid_dtd=False,
+ forbid_entities=True)
+ self.assertRaises(ValueError,
+ minidom.parseString,
+ xml_string, parser)
+
+ def test_notation(self):
+ xml_string = """<?xml version="1.0" standalone="no"?>
+ <!-- comment data -->
+ <!DOCTYPE x [
+ <!NOTATION notation SYSTEM "notation.jpeg">
+ ]>
+ <root attr1="value1">
+ </root>"""
+
+ parser = xmlutil.ProtectedExpatParser(forbid_dtd=False,
+ forbid_entities=True)
+ self.assertRaises(ValueError,
+ minidom.parseString,
+ xml_string, parser)