From 60ff2e3b72b5a3c9200f8fc47aac01cdb610bdcf Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Mon, 9 Jan 2012 14:02:02 -0800 Subject: Implements blueprint separate-nova-volumeapi * Moves openstack/v2 directory to compute and fixes tests accordingly * Moves some code from api/openstack/compute to shared location, for use by volume api * Implements basic volume functionality for types, volumes, and snapshots * Changes service name from osapi to osapi_compute (and adds osapi_volume) * Renames nova-api-os to nova-api-os-compute, adds nove-api-os-volume * Separate extension mechanism for compute and volume ** Removes flag osapi_extension and replaces with osapi_compute_extension and osapi_volume_extension * Updates the paste config * Fixes setup.py to include nova-os-api-compute and nova-os-api-volume * Fix bug in volume version code that occurred as result of trunk merge * Update integrated/test_volumes.py to use new endpoint Change-Id: I4c2e57c3cafd4e1a9e2ff3ce201c8cf28326afcd --- bin/nova-api-os | 47 - bin/nova-api-os-compute | 47 + bin/nova-api-os-volume | 47 + etc/nova/api-paste.ini | 62 +- nova/api/mapper.py | 76 + nova/api/openstack/__init__.py | 69 + nova/api/openstack/auth.py | 257 ++ nova/api/openstack/compute/__init__.py | 146 + nova/api/openstack/compute/consoles.py | 131 + nova/api/openstack/compute/contrib/__init__.py | 32 + nova/api/openstack/compute/contrib/accounts.py | 107 + .../api/openstack/compute/contrib/admin_actions.py | 291 ++ nova/api/openstack/compute/contrib/cloudpipe.py | 172 + .../openstack/compute/contrib/console_output.py | 73 + .../openstack/compute/contrib/createserverext.py | 60 + .../openstack/compute/contrib/deferred_delete.py | 77 + nova/api/openstack/compute/contrib/disk_config.py | 200 + .../openstack/compute/contrib/extended_status.py | 116 + .../openstack/compute/contrib/flavorextradata.py | 37 + .../openstack/compute/contrib/flavorextraspecs.py | 127 + .../openstack/compute/contrib/floating_ip_dns.py | 227 ++ .../openstack/compute/contrib/floating_ip_pools.py | 104 + nova/api/openstack/compute/contrib/floating_ips.py | 237 ++ nova/api/openstack/compute/contrib/hosts.py | 187 + nova/api/openstack/compute/contrib/keypairs.py | 163 + nova/api/openstack/compute/contrib/multinic.py | 106 + nova/api/openstack/compute/contrib/networks.py | 117 + nova/api/openstack/compute/contrib/quotas.py | 102 + nova/api/openstack/compute/contrib/rescue.py | 80 + .../openstack/compute/contrib/security_groups.py | 592 +++ .../compute/contrib/server_action_list.py | 77 + .../compute/contrib/server_diagnostics.py | 69 + .../compute/contrib/simple_tenant_usage.py | 265 ++ nova/api/openstack/compute/contrib/users.py | 145 + .../compute/contrib/virtual_interfaces.py | 93 + .../compute/contrib/virtual_storage_arrays.py | 687 ++++ nova/api/openstack/compute/contrib/volumes.py | 550 +++ nova/api/openstack/compute/contrib/volumetypes.py | 237 ++ nova/api/openstack/compute/contrib/zones.py | 239 ++ nova/api/openstack/compute/extensions.py | 45 + nova/api/openstack/compute/flavors.py | 112 + nova/api/openstack/compute/image_metadata.py | 118 + nova/api/openstack/compute/images.py | 195 + nova/api/openstack/compute/ips.py | 105 + nova/api/openstack/compute/limits.py | 477 +++ .../api/openstack/compute/ratelimiting/__init__.py | 222 ++ nova/api/openstack/compute/schemas/atom-link.rng | 141 + nova/api/openstack/compute/schemas/atom.rng | 597 +++ .../openstack/compute/schemas/v1.1/addresses.rng | 14 + .../openstack/compute/schemas/v1.1/extension.rng | 11 + .../openstack/compute/schemas/v1.1/extensions.rng | 6 + nova/api/openstack/compute/schemas/v1.1/flavor.rng | 13 + .../api/openstack/compute/schemas/v1.1/flavors.rng | 6 + .../compute/schemas/v1.1/flavors_index.rng | 12 + nova/api/openstack/compute/schemas/v1.1/image.rng | 36 + nova/api/openstack/compute/schemas/v1.1/images.rng | 6 + .../compute/schemas/v1.1/images_index.rng | 15 + nova/api/openstack/compute/schemas/v1.1/limits.rng | 28 + .../openstack/compute/schemas/v1.1/metadata.rng | 9 + nova/api/openstack/compute/schemas/v1.1/server.rng | 59 + .../api/openstack/compute/schemas/v1.1/servers.rng | 6 + .../compute/schemas/v1.1/servers_index.rng | 15 + .../api/openstack/compute/schemas/v1.1/version.rng | 17 + .../openstack/compute/schemas/v1.1/versions.rng | 11 + nova/api/openstack/compute/server_metadata.py | 175 + nova/api/openstack/compute/servers.py | 1123 ++++++ nova/api/openstack/compute/versions.py | 236 ++ nova/api/openstack/compute/views/__init__.py | 0 nova/api/openstack/compute/views/addresses.py | 52 + nova/api/openstack/compute/views/flavors.py | 62 + nova/api/openstack/compute/views/images.py | 139 + nova/api/openstack/compute/views/limits.py | 96 + nova/api/openstack/compute/views/servers.py | 193 + nova/api/openstack/compute/views/versions.py | 94 + nova/api/openstack/extensions.py | 623 ++++ nova/api/openstack/urlmap.py | 297 ++ nova/api/openstack/v2/__init__.py | 182 - nova/api/openstack/v2/auth.py | 257 -- nova/api/openstack/v2/consoles.py | 131 - nova/api/openstack/v2/contrib/__init__.py | 90 - nova/api/openstack/v2/contrib/accounts.py | 107 - nova/api/openstack/v2/contrib/admin_actions.py | 291 -- nova/api/openstack/v2/contrib/cloudpipe.py | 172 - nova/api/openstack/v2/contrib/console_output.py | 73 - nova/api/openstack/v2/contrib/createserverext.py | 60 - nova/api/openstack/v2/contrib/deferred_delete.py | 77 - nova/api/openstack/v2/contrib/disk_config.py | 200 - nova/api/openstack/v2/contrib/extended_status.py | 116 - nova/api/openstack/v2/contrib/flavorextradata.py | 37 - nova/api/openstack/v2/contrib/flavorextraspecs.py | 127 - nova/api/openstack/v2/contrib/floating_ip_dns.py | 227 -- nova/api/openstack/v2/contrib/floating_ip_pools.py | 104 - nova/api/openstack/v2/contrib/floating_ips.py | 237 -- nova/api/openstack/v2/contrib/hosts.py | 187 - nova/api/openstack/v2/contrib/keypairs.py | 163 - nova/api/openstack/v2/contrib/multinic.py | 106 - nova/api/openstack/v2/contrib/networks.py | 117 - nova/api/openstack/v2/contrib/quotas.py | 102 - nova/api/openstack/v2/contrib/rescue.py | 80 - nova/api/openstack/v2/contrib/security_groups.py | 592 --- .../api/openstack/v2/contrib/server_action_list.py | 77 - .../api/openstack/v2/contrib/server_diagnostics.py | 69 - .../openstack/v2/contrib/simple_tenant_usage.py | 265 -- nova/api/openstack/v2/contrib/users.py | 145 - .../api/openstack/v2/contrib/virtual_interfaces.py | 92 - .../openstack/v2/contrib/virtual_storage_arrays.py | 687 ---- nova/api/openstack/v2/contrib/volumes.py | 550 --- nova/api/openstack/v2/contrib/volumetypes.py | 237 -- nova/api/openstack/v2/contrib/zones.py | 239 -- nova/api/openstack/v2/extensions.py | 575 --- nova/api/openstack/v2/flavors.py | 112 - nova/api/openstack/v2/image_metadata.py | 118 - nova/api/openstack/v2/images.py | 195 - nova/api/openstack/v2/ips.py | 105 - nova/api/openstack/v2/limits.py | 477 --- nova/api/openstack/v2/ratelimiting/__init__.py | 222 -- nova/api/openstack/v2/schemas/atom-link.rng | 141 - nova/api/openstack/v2/schemas/atom.rng | 597 --- nova/api/openstack/v2/schemas/v1.1/addresses.rng | 14 - nova/api/openstack/v2/schemas/v1.1/extension.rng | 11 - nova/api/openstack/v2/schemas/v1.1/extensions.rng | 6 - nova/api/openstack/v2/schemas/v1.1/flavor.rng | 13 - nova/api/openstack/v2/schemas/v1.1/flavors.rng | 6 - .../openstack/v2/schemas/v1.1/flavors_index.rng | 12 - nova/api/openstack/v2/schemas/v1.1/image.rng | 36 - nova/api/openstack/v2/schemas/v1.1/images.rng | 6 - .../api/openstack/v2/schemas/v1.1/images_index.rng | 15 - nova/api/openstack/v2/schemas/v1.1/limits.rng | 28 - nova/api/openstack/v2/schemas/v1.1/metadata.rng | 9 - nova/api/openstack/v2/schemas/v1.1/server.rng | 59 - nova/api/openstack/v2/schemas/v1.1/servers.rng | 6 - .../openstack/v2/schemas/v1.1/servers_index.rng | 15 - nova/api/openstack/v2/schemas/v1.1/version.rng | 17 - nova/api/openstack/v2/schemas/v1.1/versions.rng | 11 - nova/api/openstack/v2/server_metadata.py | 175 - nova/api/openstack/v2/servers.py | 1123 ------ nova/api/openstack/v2/urlmap.py | 297 -- nova/api/openstack/v2/versions.py | 236 -- nova/api/openstack/v2/views/__init__.py | 0 nova/api/openstack/v2/views/addresses.py | 52 - nova/api/openstack/v2/views/flavors.py | 62 - nova/api/openstack/v2/views/images.py | 139 - nova/api/openstack/v2/views/limits.py | 96 - nova/api/openstack/v2/views/servers.py | 193 - nova/api/openstack/v2/views/versions.py | 94 - nova/api/openstack/volume/__init__.py | 99 + nova/api/openstack/volume/contrib/__init__.py | 32 + nova/api/openstack/volume/extensions.py | 44 + nova/api/openstack/volume/snapshots.py | 183 + nova/api/openstack/volume/types.py | 89 + nova/api/openstack/volume/versions.py | 83 + nova/api/openstack/volume/views/__init__.py | 16 + nova/api/openstack/volume/views/versions.py | 37 + nova/api/openstack/volume/volumes.py | 254 ++ nova/api/openstack/xmlutil.py | 4 +- nova/auth/manager.py | 2 +- nova/common/cfg.py | 8 +- nova/flags.py | 14 +- nova/service.py | 9 +- nova/tests/api/openstack/compute/__init__.py | 16 + .../api/openstack/compute/contrib/__init__.py | 15 + .../api/openstack/compute/contrib/test_accounts.py | 162 + .../compute/contrib/test_admin_actions.py | 250 ++ .../openstack/compute/contrib/test_cloudpipe.py | 234 ++ .../compute/contrib/test_console_output.py | 97 + .../compute/contrib/test_createserverext.py | 430 +++ .../compute/contrib/test_deferred_delete.py | 120 + .../openstack/compute/contrib/test_disk_config.py | 252 ++ .../compute/contrib/test_extendedstatus.py | 76 + .../compute/contrib/test_flavors_extra_specs.py | 184 + .../compute/contrib/test_floating_ip_dns.py | 260 ++ .../compute/contrib/test_floating_ip_pools.py | 73 + .../openstack/compute/contrib/test_floating_ips.py | 329 ++ .../api/openstack/compute/contrib/test_hosts.py | 174 + .../api/openstack/compute/contrib/test_keypairs.py | 175 + .../openstack/compute/contrib/test_multinic_xs.py | 113 + .../api/openstack/compute/contrib/test_networks.py | 137 + .../api/openstack/compute/contrib/test_quotas.py | 193 + .../api/openstack/compute/contrib/test_rescue.py | 79 + .../compute/contrib/test_security_groups.py | 1013 +++++ .../compute/contrib/test_server_action_list.py | 103 + .../compute/contrib/test_server_diagnostics.py | 86 + .../compute/contrib/test_simple_tenant_usage.py | 339 ++ .../openstack/compute/contrib/test_snapshots.py | 299 ++ .../api/openstack/compute/contrib/test_users.py | 154 + .../compute/contrib/test_virtual_interfaces.py | 93 + .../openstack/compute/contrib/test_volume_types.py | 209 ++ .../contrib/test_volume_types_extra_specs.py | 198 + .../api/openstack/compute/contrib/test_volumes.py | 239 ++ .../api/openstack/compute/contrib/test_vsa.py | 715 ++++ .../api/openstack/compute/contrib/test_zones.py | 284 ++ .../api/openstack/compute/extensions/__init__.py | 15 + .../api/openstack/compute/extensions/foxinsocks.py | 93 + nova/tests/api/openstack/compute/test_api.py | 122 + nova/tests/api/openstack/compute/test_auth.py | 319 ++ nova/tests/api/openstack/compute/test_consoles.py | 297 ++ .../tests/api/openstack/compute/test_extensions.py | 562 +++ nova/tests/api/openstack/compute/test_flavors.py | 652 ++++ .../api/openstack/compute/test_image_metadata.py | 200 + nova/tests/api/openstack/compute/test_images.py | 1646 +++++++++ nova/tests/api/openstack/compute/test_limits.py | 939 +++++ .../api/openstack/compute/test_server_actions.py | 834 +++++ .../api/openstack/compute/test_server_metadata.py | 361 ++ nova/tests/api/openstack/compute/test_servers.py | 3850 ++++++++++++++++++++ nova/tests/api/openstack/compute/test_urlmap.py | 115 + nova/tests/api/openstack/compute/test_versions.py | 668 ++++ nova/tests/api/openstack/fakes.py | 32 +- nova/tests/api/openstack/v2/__init__.py | 16 - nova/tests/api/openstack/v2/contrib/__init__.py | 15 - .../api/openstack/v2/contrib/test_accounts.py | 162 - .../api/openstack/v2/contrib/test_admin_actions.py | 250 -- .../api/openstack/v2/contrib/test_cloudpipe.py | 234 -- .../openstack/v2/contrib/test_console_output.py | 97 - .../openstack/v2/contrib/test_createserverext.py | 430 --- .../openstack/v2/contrib/test_deferred_delete.py | 120 - .../api/openstack/v2/contrib/test_disk_config.py | 252 -- .../openstack/v2/contrib/test_extendedstatus.py | 76 - .../v2/contrib/test_flavors_extra_specs.py | 184 - .../openstack/v2/contrib/test_floating_ip_dns.py | 260 -- .../openstack/v2/contrib/test_floating_ip_pools.py | 73 - .../api/openstack/v2/contrib/test_floating_ips.py | 329 -- nova/tests/api/openstack/v2/contrib/test_hosts.py | 174 - .../api/openstack/v2/contrib/test_keypairs.py | 175 - .../api/openstack/v2/contrib/test_multinic_xs.py | 113 - .../api/openstack/v2/contrib/test_networks.py | 137 - nova/tests/api/openstack/v2/contrib/test_quotas.py | 193 - nova/tests/api/openstack/v2/contrib/test_rescue.py | 79 - .../openstack/v2/contrib/test_security_groups.py | 1013 ----- .../v2/contrib/test_server_action_list.py | 103 - .../v2/contrib/test_server_diagnostics.py | 86 - .../v2/contrib/test_simple_tenant_usage.py | 339 -- .../api/openstack/v2/contrib/test_snapshots.py | 299 -- nova/tests/api/openstack/v2/contrib/test_users.py | 154 - .../v2/contrib/test_virtual_interfaces.py | 93 - .../api/openstack/v2/contrib/test_volume_types.py | 209 -- .../v2/contrib/test_volume_types_extra_specs.py | 198 - .../tests/api/openstack/v2/contrib/test_volumes.py | 239 -- nova/tests/api/openstack/v2/contrib/test_vsa.py | 714 ---- nova/tests/api/openstack/v2/contrib/test_zones.py | 284 -- nova/tests/api/openstack/v2/extensions/__init__.py | 15 - .../api/openstack/v2/extensions/foxinsocks.py | 93 - nova/tests/api/openstack/v2/test_api.py | 121 - nova/tests/api/openstack/v2/test_auth.py | 314 -- nova/tests/api/openstack/v2/test_consoles.py | 297 -- nova/tests/api/openstack/v2/test_extensions.py | 557 --- nova/tests/api/openstack/v2/test_flavors.py | 652 ---- nova/tests/api/openstack/v2/test_image_metadata.py | 200 - nova/tests/api/openstack/v2/test_images.py | 1646 --------- nova/tests/api/openstack/v2/test_limits.py | 939 ----- nova/tests/api/openstack/v2/test_server_actions.py | 834 ----- .../tests/api/openstack/v2/test_server_metadata.py | 361 -- nova/tests/api/openstack/v2/test_servers.py | 3849 ------------------- nova/tests/api/openstack/v2/test_urlmap.py | 115 - nova/tests/api/openstack/v2/test_versions.py | 668 ---- nova/tests/api/openstack/volume/__init__.py | 16 + nova/tests/api/openstack/volume/test_snapshots.py | 299 ++ nova/tests/api/openstack/volume/test_types.py | 166 + nova/tests/api/openstack/volume/test_volumes.py | 179 + nova/tests/integrated/api/client.py | 8 +- nova/tests/integrated/integrated_helpers.py | 10 +- nova/tests/integrated/test_extensions.py | 9 +- nova/tests/integrated/test_volumes.py | 158 +- setup.py | 3 +- 263 files changed, 31083 insertions(+), 29492 deletions(-) delete mode 100755 bin/nova-api-os create mode 100755 bin/nova-api-os-compute create mode 100755 bin/nova-api-os-volume create mode 100644 nova/api/mapper.py create mode 100644 nova/api/openstack/auth.py create mode 100644 nova/api/openstack/compute/__init__.py create mode 100644 nova/api/openstack/compute/consoles.py create mode 100644 nova/api/openstack/compute/contrib/__init__.py create mode 100644 nova/api/openstack/compute/contrib/accounts.py create mode 100644 nova/api/openstack/compute/contrib/admin_actions.py create mode 100644 nova/api/openstack/compute/contrib/cloudpipe.py create mode 100644 nova/api/openstack/compute/contrib/console_output.py create mode 100644 nova/api/openstack/compute/contrib/createserverext.py create mode 100644 nova/api/openstack/compute/contrib/deferred_delete.py create mode 100644 nova/api/openstack/compute/contrib/disk_config.py create mode 100644 nova/api/openstack/compute/contrib/extended_status.py create mode 100644 nova/api/openstack/compute/contrib/flavorextradata.py create mode 100644 nova/api/openstack/compute/contrib/flavorextraspecs.py create mode 100644 nova/api/openstack/compute/contrib/floating_ip_dns.py create mode 100644 nova/api/openstack/compute/contrib/floating_ip_pools.py create mode 100644 nova/api/openstack/compute/contrib/floating_ips.py create mode 100644 nova/api/openstack/compute/contrib/hosts.py create mode 100644 nova/api/openstack/compute/contrib/keypairs.py create mode 100644 nova/api/openstack/compute/contrib/multinic.py create mode 100644 nova/api/openstack/compute/contrib/networks.py create mode 100644 nova/api/openstack/compute/contrib/quotas.py create mode 100644 nova/api/openstack/compute/contrib/rescue.py create mode 100644 nova/api/openstack/compute/contrib/security_groups.py create mode 100644 nova/api/openstack/compute/contrib/server_action_list.py create mode 100644 nova/api/openstack/compute/contrib/server_diagnostics.py create mode 100644 nova/api/openstack/compute/contrib/simple_tenant_usage.py create mode 100644 nova/api/openstack/compute/contrib/users.py create mode 100644 nova/api/openstack/compute/contrib/virtual_interfaces.py create mode 100644 nova/api/openstack/compute/contrib/virtual_storage_arrays.py create mode 100644 nova/api/openstack/compute/contrib/volumes.py create mode 100644 nova/api/openstack/compute/contrib/volumetypes.py create mode 100644 nova/api/openstack/compute/contrib/zones.py create mode 100644 nova/api/openstack/compute/extensions.py create mode 100644 nova/api/openstack/compute/flavors.py create mode 100644 nova/api/openstack/compute/image_metadata.py create mode 100644 nova/api/openstack/compute/images.py create mode 100644 nova/api/openstack/compute/ips.py create mode 100644 nova/api/openstack/compute/limits.py create mode 100644 nova/api/openstack/compute/ratelimiting/__init__.py create mode 100644 nova/api/openstack/compute/schemas/atom-link.rng create mode 100644 nova/api/openstack/compute/schemas/atom.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/addresses.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/extension.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/extensions.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/flavor.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/flavors.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/flavors_index.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/image.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/images.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/images_index.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/limits.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/metadata.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/server.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/servers.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/servers_index.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/version.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/versions.rng create mode 100644 nova/api/openstack/compute/server_metadata.py create mode 100644 nova/api/openstack/compute/servers.py create mode 100644 nova/api/openstack/compute/versions.py create mode 100644 nova/api/openstack/compute/views/__init__.py create mode 100644 nova/api/openstack/compute/views/addresses.py create mode 100644 nova/api/openstack/compute/views/flavors.py create mode 100644 nova/api/openstack/compute/views/images.py create mode 100644 nova/api/openstack/compute/views/limits.py create mode 100644 nova/api/openstack/compute/views/servers.py create mode 100644 nova/api/openstack/compute/views/versions.py create mode 100644 nova/api/openstack/extensions.py create mode 100644 nova/api/openstack/urlmap.py delete mode 100644 nova/api/openstack/v2/__init__.py delete mode 100644 nova/api/openstack/v2/auth.py delete mode 100644 nova/api/openstack/v2/consoles.py delete mode 100644 nova/api/openstack/v2/contrib/__init__.py delete mode 100644 nova/api/openstack/v2/contrib/accounts.py delete mode 100644 nova/api/openstack/v2/contrib/admin_actions.py delete mode 100644 nova/api/openstack/v2/contrib/cloudpipe.py delete mode 100644 nova/api/openstack/v2/contrib/console_output.py delete mode 100644 nova/api/openstack/v2/contrib/createserverext.py delete mode 100644 nova/api/openstack/v2/contrib/deferred_delete.py delete mode 100644 nova/api/openstack/v2/contrib/disk_config.py delete mode 100644 nova/api/openstack/v2/contrib/extended_status.py delete mode 100644 nova/api/openstack/v2/contrib/flavorextradata.py delete mode 100644 nova/api/openstack/v2/contrib/flavorextraspecs.py delete mode 100644 nova/api/openstack/v2/contrib/floating_ip_dns.py delete mode 100644 nova/api/openstack/v2/contrib/floating_ip_pools.py delete mode 100644 nova/api/openstack/v2/contrib/floating_ips.py delete mode 100644 nova/api/openstack/v2/contrib/hosts.py delete mode 100644 nova/api/openstack/v2/contrib/keypairs.py delete mode 100644 nova/api/openstack/v2/contrib/multinic.py delete mode 100644 nova/api/openstack/v2/contrib/networks.py delete mode 100644 nova/api/openstack/v2/contrib/quotas.py delete mode 100644 nova/api/openstack/v2/contrib/rescue.py delete mode 100644 nova/api/openstack/v2/contrib/security_groups.py delete mode 100644 nova/api/openstack/v2/contrib/server_action_list.py delete mode 100644 nova/api/openstack/v2/contrib/server_diagnostics.py delete mode 100644 nova/api/openstack/v2/contrib/simple_tenant_usage.py delete mode 100644 nova/api/openstack/v2/contrib/users.py delete mode 100644 nova/api/openstack/v2/contrib/virtual_interfaces.py delete mode 100644 nova/api/openstack/v2/contrib/virtual_storage_arrays.py delete mode 100644 nova/api/openstack/v2/contrib/volumes.py delete mode 100644 nova/api/openstack/v2/contrib/volumetypes.py delete mode 100644 nova/api/openstack/v2/contrib/zones.py delete mode 100644 nova/api/openstack/v2/extensions.py delete mode 100644 nova/api/openstack/v2/flavors.py delete mode 100644 nova/api/openstack/v2/image_metadata.py delete mode 100644 nova/api/openstack/v2/images.py delete mode 100644 nova/api/openstack/v2/ips.py delete mode 100644 nova/api/openstack/v2/limits.py delete mode 100644 nova/api/openstack/v2/ratelimiting/__init__.py delete mode 100644 nova/api/openstack/v2/schemas/atom-link.rng delete mode 100644 nova/api/openstack/v2/schemas/atom.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/addresses.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/extension.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/extensions.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/flavor.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/flavors.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/flavors_index.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/image.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/images.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/images_index.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/limits.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/metadata.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/server.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/servers.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/servers_index.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/version.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/versions.rng delete mode 100644 nova/api/openstack/v2/server_metadata.py delete mode 100644 nova/api/openstack/v2/servers.py delete mode 100644 nova/api/openstack/v2/urlmap.py delete mode 100644 nova/api/openstack/v2/versions.py delete mode 100644 nova/api/openstack/v2/views/__init__.py delete mode 100644 nova/api/openstack/v2/views/addresses.py delete mode 100644 nova/api/openstack/v2/views/flavors.py delete mode 100644 nova/api/openstack/v2/views/images.py delete mode 100644 nova/api/openstack/v2/views/limits.py delete mode 100644 nova/api/openstack/v2/views/servers.py delete mode 100644 nova/api/openstack/v2/views/versions.py create mode 100644 nova/api/openstack/volume/__init__.py create mode 100644 nova/api/openstack/volume/contrib/__init__.py create mode 100644 nova/api/openstack/volume/extensions.py create mode 100644 nova/api/openstack/volume/snapshots.py create mode 100644 nova/api/openstack/volume/types.py create mode 100644 nova/api/openstack/volume/versions.py create mode 100644 nova/api/openstack/volume/views/__init__.py create mode 100644 nova/api/openstack/volume/views/versions.py create mode 100644 nova/api/openstack/volume/volumes.py create mode 100644 nova/tests/api/openstack/compute/__init__.py create mode 100644 nova/tests/api/openstack/compute/contrib/__init__.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_accounts.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_admin_actions.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_cloudpipe.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_console_output.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_createserverext.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_deferred_delete.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_disk_config.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_extendedstatus.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_flavors_extra_specs.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_floating_ip_dns.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_floating_ip_pools.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_floating_ips.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_hosts.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_keypairs.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_multinic_xs.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_networks.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_quotas.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_rescue.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_security_groups.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_server_action_list.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_server_diagnostics.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_simple_tenant_usage.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_snapshots.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_users.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_virtual_interfaces.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_volume_types.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_volume_types_extra_specs.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_volumes.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_vsa.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_zones.py create mode 100644 nova/tests/api/openstack/compute/extensions/__init__.py create mode 100644 nova/tests/api/openstack/compute/extensions/foxinsocks.py create mode 100644 nova/tests/api/openstack/compute/test_api.py create mode 100644 nova/tests/api/openstack/compute/test_auth.py create mode 100644 nova/tests/api/openstack/compute/test_consoles.py create mode 100644 nova/tests/api/openstack/compute/test_extensions.py create mode 100644 nova/tests/api/openstack/compute/test_flavors.py create mode 100644 nova/tests/api/openstack/compute/test_image_metadata.py create mode 100644 nova/tests/api/openstack/compute/test_images.py create mode 100644 nova/tests/api/openstack/compute/test_limits.py create mode 100644 nova/tests/api/openstack/compute/test_server_actions.py create mode 100644 nova/tests/api/openstack/compute/test_server_metadata.py create mode 100644 nova/tests/api/openstack/compute/test_servers.py create mode 100644 nova/tests/api/openstack/compute/test_urlmap.py create mode 100644 nova/tests/api/openstack/compute/test_versions.py delete mode 100644 nova/tests/api/openstack/v2/__init__.py delete mode 100644 nova/tests/api/openstack/v2/contrib/__init__.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_accounts.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_admin_actions.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_cloudpipe.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_console_output.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_createserverext.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_deferred_delete.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_disk_config.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_extendedstatus.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_flavors_extra_specs.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_floating_ip_dns.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_floating_ip_pools.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_floating_ips.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_hosts.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_keypairs.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_multinic_xs.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_networks.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_quotas.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_rescue.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_security_groups.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_server_action_list.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_server_diagnostics.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_simple_tenant_usage.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_snapshots.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_users.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_virtual_interfaces.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_volume_types.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_volume_types_extra_specs.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_volumes.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_vsa.py delete mode 100644 nova/tests/api/openstack/v2/contrib/test_zones.py delete mode 100644 nova/tests/api/openstack/v2/extensions/__init__.py delete mode 100644 nova/tests/api/openstack/v2/extensions/foxinsocks.py delete mode 100644 nova/tests/api/openstack/v2/test_api.py delete mode 100644 nova/tests/api/openstack/v2/test_auth.py delete mode 100644 nova/tests/api/openstack/v2/test_consoles.py delete mode 100644 nova/tests/api/openstack/v2/test_extensions.py delete mode 100644 nova/tests/api/openstack/v2/test_flavors.py delete mode 100644 nova/tests/api/openstack/v2/test_image_metadata.py delete mode 100644 nova/tests/api/openstack/v2/test_images.py delete mode 100644 nova/tests/api/openstack/v2/test_limits.py delete mode 100644 nova/tests/api/openstack/v2/test_server_actions.py delete mode 100644 nova/tests/api/openstack/v2/test_server_metadata.py delete mode 100644 nova/tests/api/openstack/v2/test_servers.py delete mode 100644 nova/tests/api/openstack/v2/test_urlmap.py delete mode 100644 nova/tests/api/openstack/v2/test_versions.py create mode 100644 nova/tests/api/openstack/volume/__init__.py create mode 100644 nova/tests/api/openstack/volume/test_snapshots.py create mode 100644 nova/tests/api/openstack/volume/test_types.py create mode 100644 nova/tests/api/openstack/volume/test_volumes.py diff --git a/bin/nova-api-os b/bin/nova-api-os deleted file mode 100755 index 83a808987..000000000 --- a/bin/nova-api-os +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Starter script for Nova OS API.""" - -import eventlet -eventlet.monkey_patch() - -import os -import sys - - -possible_topdir = os.path.normpath(os.path.join(os.path.abspath( - sys.argv[0]), os.pardir, os.pardir)) -if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): - sys.path.insert(0, possible_topdir) - - -from nova import flags -from nova import log as logging -from nova import service -from nova import utils - -if __name__ == '__main__': - utils.default_flagfile() - flags.FLAGS(sys.argv) - logging.setup() - utils.monkey_patch() - server = service.WSGIService('osapi') - service.serve(server) - service.wait() diff --git a/bin/nova-api-os-compute b/bin/nova-api-os-compute new file mode 100755 index 000000000..7bfff2a42 --- /dev/null +++ b/bin/nova-api-os-compute @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Starter script for Nova OS API.""" + +import eventlet +eventlet.monkey_patch() + +import os +import sys + + +possible_topdir = os.path.normpath(os.path.join(os.path.abspath( + sys.argv[0]), os.pardir, os.pardir)) +if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): + sys.path.insert(0, possible_topdir) + + +from nova import flags +from nova import log as logging +from nova import service +from nova import utils + +if __name__ == '__main__': + utils.default_flagfile() + flags.FLAGS(sys.argv) + logging.setup() + utils.monkey_patch() + server = service.WSGIService('osapi_compute') + service.serve(server) + service.wait() diff --git a/bin/nova-api-os-volume b/bin/nova-api-os-volume new file mode 100755 index 000000000..33909c5b5 --- /dev/null +++ b/bin/nova-api-os-volume @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Starter script for Nova OS API.""" + +import eventlet +eventlet.monkey_patch() + +import os +import sys + + +possible_topdir = os.path.normpath(os.path.join(os.path.abspath( + sys.argv[0]), os.pardir, os.pardir)) +if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): + sys.path.insert(0, possible_topdir) + + +from nova import flags +from nova import log as logging +from nova import service +from nova import utils + +if __name__ == '__main__': + utils.default_flagfile() + flags.FLAGS(sys.argv) + logging.setup() + utils.monkey_patch() + server = service.WSGIService('osapi_volume') + service.serve(server) + service.wait() diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index 145044df7..c2de4b484 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -78,40 +78,60 @@ paste.app_factory = nova.api.ec2:Executor.factory # Openstack # ############# -[composite:osapi] -use = call:nova.api.openstack.v2.urlmap:urlmap_factory -/: osversions -/v1.1: openstack_api_v2 -/v2: openstack_api_v2 - -[pipeline:openstack_api_v2] -pipeline = faultwrap noauth ratelimit serialize extensions osapi_app_v2 +[composite:osapi_compute] +use = call:nova.api.openstack.urlmap:urlmap_factory +/: oscomputeversions +/v1.1: openstack_compute_api_v2 +/v2: openstack_compute_api_v2 + +[composite:osapi_volume] +use = call:nova.api.openstack.urlmap:urlmap_factory +/: osvolumeversions +/v1: openstack_volume_api_v1 + +[pipeline:openstack_compute_api_v2] +pipeline = faultwrap noauth ratelimit serialize compute_extensions osapi_compute_app_v2 # NOTE(vish): use the following pipeline for deprecated auth -# pipeline = faultwrap auth ratelimit serialize extensions osapi_app_v2 +# pipeline = faultwrap auth ratelimit serialize extensions osapi_compute_app_v2 + +[pipeline:openstack_volume_api_v1] +pipeline = faultwrap noauth ratelimit serialize volume_extensions osapi_volume_app_v1 [filter:faultwrap] -paste.filter_factory = nova.api.openstack.v2:FaultWrapper.factory +paste.filter_factory = nova.api.openstack:FaultWrapper.factory [filter:auth] -paste.filter_factory = nova.api.openstack.v2.auth:AuthMiddleware.factory +paste.filter_factory = nova.api.openstack.auth:AuthMiddleware.factory [filter:noauth] -paste.filter_factory = nova.api.openstack.v2.auth:NoAuthMiddleware.factory +paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory [filter:ratelimit] -paste.filter_factory = nova.api.openstack.v2.limits:RateLimitingMiddleware.factory +paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware.factory [filter:serialize] paste.filter_factory = nova.api.openstack.wsgi:LazySerializationMiddleware.factory -[filter:extensions] -paste.filter_factory = nova.api.openstack.v2.extensions:ExtensionMiddleware.factory +[filter:compute_extensions] +paste.filter_factory = nova.api.openstack.compute.extensions:ExtensionMiddleware.factory + +[filter:volume_extensions] +paste.filter_factory = nova.api.openstack.volume.extensions:ExtensionMiddleware.factory + +[app:osapi_compute_app_v2] +paste.app_factory = nova.api.openstack.compute:APIRouter.factory + +[pipeline:oscomputeversions] +pipeline = faultwrap oscomputeversionapp + +[app:osapi_volume_app_v1] +paste.app_factory = nova.api.openstack.volume:APIRouter.factory -[app:osapi_app_v2] -paste.app_factory = nova.api.openstack.v2:APIRouter.factory +[app:oscomputeversionapp] +paste.app_factory = nova.api.openstack.compute.versions:Versions.factory -[pipeline:osversions] -pipeline = faultwrap osversionapp +[pipeline:osvolumeversions] +pipeline = faultwrap osvolumeversionapp -[app:osversionapp] -paste.app_factory = nova.api.openstack.v2.versions:Versions.factory +[app:osvolumeversionapp] +paste.app_factory = nova.api.openstack.volume.versions:Versions.factory diff --git a/nova/api/mapper.py b/nova/api/mapper.py new file mode 100644 index 000000000..cd26e06ee --- /dev/null +++ b/nova/api/mapper.py @@ -0,0 +1,76 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +WSGI middleware for OpenStack API controllers. +""" + +import routes +import webob.dec +import webob.exc + +from nova.api.openstack import wsgi +from nova import flags +from nova import log as logging +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack.compute') +FLAGS = flags.FLAGS +flags.DEFINE_bool('allow_admin_api', + False, + 'When True, this API service will accept admin operations.') +flags.DEFINE_bool('allow_instance_snapshots', + True, + 'When True, this API service will permit instance snapshot operations.') + + +class FaultWrapper(base_wsgi.Middleware): + """Calls down the middleware stack, making exceptions into faults.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + try: + return req.get_response(self.application) + except Exception as ex: + LOG.exception(_("Caught error: %s"), unicode(ex)) + exc = webob.exc.HTTPInternalServerError() + return wsgi.Fault(exc) + + +class APIMapper(routes.Mapper): + def routematch(self, url=None, environ=None): + if url is "": + result = self._match("", environ) + return result[0], result[1] + return routes.Mapper.routematch(self, url, environ) + + +class ProjectMapper(APIMapper): + def resource(self, member_name, collection_name, **kwargs): + if not ('parent_resource' in kwargs): + kwargs['path_prefix'] = '{project_id}/' + else: + parent_resource = kwargs['parent_resource'] + p_collection = parent_resource['collection_name'] + p_member = parent_resource['member_name'] + kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection, + p_member) + routes.Mapper.resource(self, member_name, + collection_name, + **kwargs) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index e69de29bb..dfc174a58 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -0,0 +1,69 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +WSGI middleware for OpenStack API controllers. +""" + +import routes +import webob.dec +import webob.exc + +from nova.api.openstack import wsgi +from nova import flags +from nova import log as logging +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack') + + +class FaultWrapper(base_wsgi.Middleware): + """Calls down the middleware stack, making exceptions into faults.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + try: + return req.get_response(self.application) + except Exception as ex: + LOG.exception(_("Caught error: %s"), unicode(ex)) + exc = webob.exc.HTTPInternalServerError() + return wsgi.Fault(exc) + + +class APIMapper(routes.Mapper): + def routematch(self, url=None, environ=None): + if url is "": + result = self._match("", environ) + return result[0], result[1] + return routes.Mapper.routematch(self, url, environ) + + +class ProjectMapper(APIMapper): + def resource(self, member_name, collection_name, **kwargs): + if not ('parent_resource' in kwargs): + kwargs['path_prefix'] = '{project_id}/' + else: + parent_resource = kwargs['parent_resource'] + p_collection = parent_resource['collection_name'] + p_member = parent_resource['member_name'] + kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection, + p_member) + routes.Mapper.resource(self, member_name, + collection_name, + **kwargs) diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py new file mode 100644 index 000000000..9a0432d51 --- /dev/null +++ b/nova/api/openstack/auth.py @@ -0,0 +1,257 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import hashlib +import os +import time + +import webob.dec +import webob.exc + +from nova.api.openstack import common +from nova.api.openstack import wsgi +from nova import auth +from nova import context +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils +from nova import wsgi as base_wsgi + +LOG = logging.getLogger('nova.api.openstack.compute.auth') +FLAGS = flags.FLAGS +flags.DECLARE('use_forwarded_for', 'nova.api.auth') + + +class NoAuthMiddleware(base_wsgi.Middleware): + """Return a fake token if one isn't specified.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + if 'X-Auth-Token' not in req.headers: + user_id = req.headers.get('X-Auth-User', 'admin') + project_id = req.headers.get('X-Auth-Project-Id', 'admin') + os_url = os.path.join(req.url, project_id) + res = webob.Response() + # NOTE(vish): This is expecting and returning Auth(1.1), whereas + # keystone uses 2.0 auth. We should probably allow + # 2.0 auth here as well. + res.headers['X-Auth-Token'] = '%s:%s' % (user_id, project_id) + res.headers['X-Server-Management-Url'] = os_url + res.headers['X-Storage-Url'] = '' + res.headers['X-CDN-Management-Url'] = '' + res.content_type = 'text/plain' + res.status = '204' + return res + + token = req.headers['X-Auth-Token'] + user_id, _sep, project_id = token.partition(':') + project_id = project_id or user_id + remote_address = getattr(req, 'remote_address', '127.0.0.1') + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + ctx = context.RequestContext(user_id, + project_id, + is_admin=True, + remote_address=remote_address) + + req.environ['nova.context'] = ctx + return self.application + + +class AuthMiddleware(base_wsgi.Middleware): + """Authorize the openstack API request or return an HTTP Forbidden.""" + + def __init__(self, application, db_driver=None): + if not db_driver: + db_driver = FLAGS.db_driver + self.db = utils.import_object(db_driver) + self.auth = auth.manager.AuthManager() + super(AuthMiddleware, self).__init__(application) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + if not self.has_authentication(req): + return self.authenticate(req) + user_id = self.get_user_by_authentication(req) + if not user_id: + token = req.headers["X-Auth-Token"] + msg = _("%(user_id)s could not be found with token '%(token)s'") + LOG.warn(msg % locals()) + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + + # Get all valid projects for the user + projects = self.auth.get_projects(user_id) + if not projects: + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + + project_id = "" + path_parts = req.path.split('/') + # TODO(wwolf): this v1.1 check will be temporary as + # keystone should be taking this over at some point + if len(path_parts) > 1 and path_parts[1] in ('v1.1', 'v2'): + project_id = path_parts[2] + # Check that the project for project_id exists, and that user + # is authorized to use it + try: + self.auth.get_project(project_id) + except exception.ProjectNotFound: + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + if project_id not in [p.id for p in projects]: + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + else: + # As a fallback, set project_id from the headers, which is the v1.0 + # behavior. As a last resort, be forgiving to the user and set + # project_id based on a valid project of theirs. + try: + project_id = req.headers["X-Auth-Project-Id"] + except KeyError: + project_id = projects[0].id + + is_admin = self.auth.is_admin(user_id) + remote_address = getattr(req, 'remote_address', '127.0.0.1') + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + ctx = context.RequestContext(user_id, + project_id, + is_admin=is_admin, + remote_address=remote_address) + req.environ['nova.context'] = ctx + + if not is_admin and not self.auth.is_project_member(user_id, + project_id): + msg = _("%(user_id)s must be an admin or a " + "member of %(project_id)s") + LOG.warn(msg % locals()) + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + + return self.application + + def has_authentication(self, req): + return 'X-Auth-Token' in req.headers + + def get_user_by_authentication(self, req): + return self.authorize_token(req.headers["X-Auth-Token"]) + + def authenticate(self, req): + # Unless the request is explicitly made against // don't + # honor it + path_info = req.path_info + if len(path_info) > 1: + msg = _("Authentication requests must be made against a version " + "root (e.g. /v2).") + LOG.warn(msg) + return wsgi.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) + + def _get_auth_header(key): + """Ensures that the KeyError returned is meaningful.""" + try: + return req.headers[key] + except KeyError as ex: + raise KeyError(key) + try: + username = _get_auth_header('X-Auth-User') + key = _get_auth_header('X-Auth-Key') + except KeyError as ex: + msg = _("Could not find %s in request.") % ex + LOG.warn(msg) + return wsgi.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) + + token, user = self._authorize_user(username, key, req) + if user and token: + res = webob.Response() + res.headers['X-Auth-Token'] = token['token_hash'] + res.headers['X-Server-Management-Url'] = \ + token['server_management_url'] + res.headers['X-Storage-Url'] = token['storage_url'] + res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] + res.content_type = 'text/plain' + res.status = '204' + LOG.debug(_("Successfully authenticated '%s'") % username) + return res + else: + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + + def authorize_token(self, token_hash): + """ retrieves user information from the datastore given a token + + If the token has expired, returns None + If the token is not found, returns None + Otherwise returns dict(id=(the authorized user's id)) + + This method will also remove the token if the timestamp is older than + 2 days ago. + """ + ctxt = context.get_admin_context() + try: + token = self.db.auth_token_get(ctxt, token_hash) + except exception.NotFound: + return None + if token: + delta = utils.utcnow() - token['created_at'] + if delta.days >= 2: + self.db.auth_token_destroy(ctxt, token['token_hash']) + else: + return token['user_id'] + return None + + def _authorize_user(self, username, key, req): + """Generates a new token and assigns it to a user. + + username - string + key - string API key + req - wsgi.Request object + """ + ctxt = context.get_admin_context() + + project_id = req.headers.get('X-Auth-Project-Id') + if project_id is None: + # If the project_id is not provided in the headers, be forgiving to + # the user and set project_id based on a valid project of theirs. + user = self.auth.get_user_from_access_key(key) + projects = self.auth.get_projects(user.id) + if not projects: + raise webob.exc.HTTPUnauthorized() + project_id = projects[0].id + + try: + user = self.auth.get_user_from_access_key(key) + except exception.NotFound: + LOG.warn(_("User not found with provided API key.")) + user = None + + if user and user.name == username: + token_hash = hashlib.sha1('%s%s%f' % (username, key, + time.time())).hexdigest() + token_dict = {} + token_dict['token_hash'] = token_hash + token_dict['cdn_management_url'] = '' + os_url = req.url + token_dict['server_management_url'] = os_url.strip('/') + version = common.get_version_from_href(os_url) + if version in ('1.1', '2'): + token_dict['server_management_url'] += '/' + project_id + token_dict['storage_url'] = '' + token_dict['user_id'] = user.id + token = self.db.auth_token_create(ctxt, token_dict) + return token, user + elif user and user.name != username: + msg = _("Provided API key is valid, but not for user " + "'%(username)s'") % locals() + LOG.warn(msg) + + return None, None diff --git a/nova/api/openstack/compute/__init__.py b/nova/api/openstack/compute/__init__.py new file mode 100644 index 000000000..2f6e92a42 --- /dev/null +++ b/nova/api/openstack/compute/__init__.py @@ -0,0 +1,146 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +WSGI middleware for OpenStack API controllers. +""" + +import routes +import webob.dec +import webob.exc + +import nova.api.openstack +from nova.api.openstack.compute import consoles +from nova.api.openstack.compute import extensions +from nova.api.openstack.compute import flavors +from nova.api.openstack.compute import images +from nova.api.openstack.compute import image_metadata +from nova.api.openstack.compute import ips +from nova.api.openstack.compute import limits +from nova.api.openstack.compute import servers +from nova.api.openstack.compute import server_metadata +from nova.api.openstack.compute import versions +from nova.api.openstack import wsgi +from nova import flags +from nova import log as logging +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack.compute') +FLAGS = flags.FLAGS +flags.DEFINE_bool('allow_admin_api', + False, + 'When True, this API service will accept admin operations.') +flags.DEFINE_bool('allow_instance_snapshots', + True, + 'When True, this API service will permit instance snapshot operations.') + + +class APIRouter(base_wsgi.Router): + """ + Routes requests on the OpenStack API to the appropriate controller + and method. + """ + + @classmethod + def factory(cls, global_config, **local_config): + """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one""" + return cls() + + def __init__(self, ext_mgr=None): + if ext_mgr is None: + ext_mgr = extensions.ExtensionManager() + + mapper = nova.api.openstack.ProjectMapper() + self._setup_routes(mapper) + self._setup_ext_routes(mapper, ext_mgr) + super(APIRouter, self).__init__(mapper) + + def _setup_ext_routes(self, mapper, ext_mgr): + for resource in ext_mgr.get_resources(): + LOG.debug(_('Extended resource: %s'), + resource.collection) + + kargs = dict( + controller=wsgi.Resource( + resource.controller, resource.deserializer, + resource.serializer), + collection=resource.collection_actions, + member=resource.member_actions) + + if resource.parent: + kargs['parent_resource'] = resource.parent + + mapper.resource(resource.collection, resource.collection, **kargs) + + def _setup_routes(self, mapper): + mapper.connect("versions", "/", + controller=versions.create_resource(), + action='show') + + mapper.redirect("", "/") + + mapper.resource("console", "consoles", + controller=consoles.create_resource(), + parent_resource=dict(member_name='server', + collection_name='servers')) + + mapper.resource("server", "servers", + controller=servers.create_resource(), + collection={'detail': 'GET'}, + member={'action': 'POST'}) + + mapper.resource("ip", "ips", controller=ips.create_resource(), + parent_resource=dict(member_name='server', + collection_name='servers')) + + mapper.resource("image", "images", + controller=images.create_resource(), + collection={'detail': 'GET'}) + + mapper.resource("limit", "limits", + controller=limits.create_resource()) + + mapper.resource("flavor", "flavors", + controller=flavors.create_resource(), + collection={'detail': 'GET'}) + + image_metadata_controller = image_metadata.create_resource() + + mapper.resource("image_meta", "metadata", + controller=image_metadata_controller, + parent_resource=dict(member_name='image', + collection_name='images')) + + mapper.connect("metadata", "/{project_id}/images/{image_id}/metadata", + controller=image_metadata_controller, + action='update_all', + conditions={"method": ['PUT']}) + + server_metadata_controller = server_metadata.create_resource() + + mapper.resource("server_meta", "metadata", + controller=server_metadata_controller, + parent_resource=dict(member_name='server', + collection_name='servers')) + + mapper.connect("metadata", + "/{project_id}/servers/{server_id}/metadata", + controller=server_metadata_controller, + action='update_all', + conditions={"method": ['PUT']}) diff --git a/nova/api/openstack/compute/consoles.py b/nova/api/openstack/compute/consoles.py new file mode 100644 index 000000000..e9eee4c75 --- /dev/null +++ b/nova/api/openstack/compute/consoles.py @@ -0,0 +1,131 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob +from webob import exc + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import console +from nova import exception + + +def _translate_keys(cons): + """Coerces a console instance into proper dictionary format """ + pool = cons['pool'] + info = {'id': cons['id'], + 'console_type': pool['console_type']} + return dict(console=info) + + +def _translate_detail_keys(cons): + """Coerces a console instance into proper dictionary format with + correctly mapped attributes """ + pool = cons['pool'] + info = {'id': cons['id'], + 'console_type': pool['console_type'], + 'password': cons['password'], + 'instance_name': cons['instance_name'], + 'port': cons['port'], + 'host': pool['public_hostname']} + return dict(console=info) + + +class ConsoleTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('console', selector='console') + + id_elem = xmlutil.SubTemplateElement(root, 'id', selector='id') + id_elem.text = xmlutil.Selector() + + port_elem = xmlutil.SubTemplateElement(root, 'port', selector='port') + port_elem.text = xmlutil.Selector() + + host_elem = xmlutil.SubTemplateElement(root, 'host', selector='host') + host_elem.text = xmlutil.Selector() + + passwd_elem = xmlutil.SubTemplateElement(root, 'password', + selector='password') + passwd_elem.text = xmlutil.Selector() + + constype_elem = xmlutil.SubTemplateElement(root, 'console_type', + selector='console_type') + constype_elem.text = xmlutil.Selector() + + return xmlutil.MasterTemplate(root, 1) + + +class ConsolesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('consoles') + console = xmlutil.SubTemplateElement(root, 'console', + selector='consoles') + console.append(ConsoleTemplate()) + + return xmlutil.MasterTemplate(root, 1) + + +class Controller(object): + """The Consoles controller for the Openstack API""" + + def __init__(self): + self.console_api = console.API() + + @wsgi.serializers(xml=ConsolesTemplate) + def index(self, req, server_id): + """Returns a list of consoles for this instance""" + consoles = self.console_api.get_consoles( + req.environ['nova.context'], + server_id) + return dict(consoles=[_translate_keys(console) + for console in consoles]) + + def create(self, req, server_id): + """Creates a new console""" + self.console_api.create_console( + req.environ['nova.context'], + server_id) + + @wsgi.serializers(xml=ConsoleTemplate) + def show(self, req, server_id, id): + """Shows in-depth information on a specific console""" + try: + console = self.console_api.get_console( + req.environ['nova.context'], + server_id, + int(id)) + except exception.NotFound: + raise exc.HTTPNotFound() + return _translate_detail_keys(console) + + def update(self, req, server_id, id): + """You can't update a console""" + raise exc.HTTPNotImplemented() + + def delete(self, req, server_id, id): + """Deletes a console""" + try: + self.console_api.delete_console(req.environ['nova.context'], + server_id, + int(id)) + except exception.NotFound: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/contrib/__init__.py b/nova/api/openstack/compute/contrib/__init__.py new file mode 100644 index 000000000..2713a82f4 --- /dev/null +++ b/nova/api/openstack/compute/contrib/__init__.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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. + +"""Contrib contains extensions that are shipped with nova. + +It can't be called 'extensions' because that causes namespacing problems. + +""" + +from nova import log as logging +from nova.api.openstack import extensions + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib') + + +def standard_extensions(ext_mgr): + extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__) diff --git a/nova/api/openstack/compute/contrib/accounts.py b/nova/api/openstack/compute/contrib/accounts.py new file mode 100644 index 000000000..9253c037d --- /dev/null +++ b/nova/api/openstack/compute/contrib/accounts.py @@ -0,0 +1,107 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob.exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.auth import manager +from nova import exception +from nova import flags +from nova import log as logging + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack.compute.contrib.accounts') + + +class AccountTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('account', selector='account') + root.set('id', 'id') + root.set('name', 'name') + root.set('description', 'description') + root.set('manager', 'manager') + + return xmlutil.MasterTemplate(root, 1) + + +def _translate_keys(account): + return dict(id=account.id, + name=account.name, + description=account.description, + manager=account.project_manager_id) + + +class Controller(object): + + def __init__(self): + self.manager = manager.AuthManager() + + def _check_admin(self, context): + """We cannot depend on the db layer to check for admin access + for the auth manager, so we do it here""" + if not context.is_admin: + raise exception.AdminRequired() + + def index(self, req): + raise webob.exc.HTTPNotImplemented() + + @wsgi.serializers(xml=AccountTemplate) + def show(self, req, id): + """Return data about the given account id""" + account = self.manager.get_project(id) + return dict(account=_translate_keys(account)) + + def delete(self, req, id): + self._check_admin(req.environ['nova.context']) + self.manager.delete_project(id) + return {} + + def create(self, req, body): + """We use update with create-or-update semantics + because the id comes from an external source""" + raise webob.exc.HTTPNotImplemented() + + @wsgi.serializers(xml=AccountTemplate) + def update(self, req, id, body): + """This is really create or update.""" + self._check_admin(req.environ['nova.context']) + description = body['account'].get('description') + manager = body['account'].get('manager') + try: + account = self.manager.get_project(id) + self.manager.modify_project(id, manager, description) + except exception.NotFound: + account = self.manager.create_project(id, manager, description) + return dict(account=_translate_keys(account)) + + +class Accounts(extensions.ExtensionDescriptor): + """Admin-only access to accounts""" + + name = "Accounts" + alias = "os-accounts" + namespace = "http://docs.openstack.org/compute/ext/accounts/api/v1.1" + updated = "2011-12-23T00:00:00+00:00" + admin_only = True + + def get_resources(self): + #TODO(bcwaldon): This should be prefixed with 'os-' + res = extensions.ResourceExtension('accounts', + Controller()) + + return [res] diff --git a/nova/api/openstack/compute/contrib/admin_actions.py b/nova/api/openstack/compute/contrib/admin_actions.py new file mode 100644 index 000000000..dedef3061 --- /dev/null +++ b/nova/api/openstack/compute/contrib/admin_actions.py @@ -0,0 +1,291 @@ +# Copyright 2011 Openstack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os.path +import traceback + +import webob +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova.scheduler import api as scheduler_api + + +FLAGS = flags.FLAGS +LOG = logging.getLogger("nova.api.openstack.compute.contrib.admin_actions") + + +class Admin_actions(extensions.ExtensionDescriptor): + """Enable admin-only server actions + + Actions include: pause, unpause, suspend, resume, migrate, + resetNetwork, injectNetworkInfo, lock, unlock, createBackup + """ + + name = "AdminActions" + alias = "os-admin-actions" + namespace = "http://docs.openstack.org/compute/ext/admin-actions/api/v1.1" + updated = "2011-09-20T00:00:00+00:00" + admin_only = True + + def __init__(self, ext_mgr): + super(Admin_actions, self).__init__(ext_mgr) + self.compute_api = compute.API() + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _pause(self, input_dict, req, id): + """Permit Admins to pause the server""" + ctxt = req.environ['nova.context'] + try: + server = self.compute_api.get(ctxt, id) + self.compute_api.pause(ctxt, server) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'pause') + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::pause %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _unpause(self, input_dict, req, id): + """Permit Admins to unpause the server""" + ctxt = req.environ['nova.context'] + try: + server = self.compute_api.get(ctxt, id) + self.compute_api.unpause(ctxt, server) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'unpause') + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::unpause %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _suspend(self, input_dict, req, id): + """Permit admins to suspend the server""" + context = req.environ['nova.context'] + try: + server = self.compute_api.get(context, id) + self.compute_api.suspend(context, server) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'suspend') + except Exception: + readable = traceback.format_exc() + LOG.exception(_("compute.api::suspend %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _resume(self, input_dict, req, id): + """Permit admins to resume the server from suspend""" + context = req.environ['nova.context'] + try: + server = self.compute_api.get(context, id) + self.compute_api.resume(context, server) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'resume') + except Exception: + readable = traceback.format_exc() + LOG.exception(_("compute.api::resume %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _migrate(self, input_dict, req, id): + """Permit admins to migrate a server to a new host""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.resize(req.environ['nova.context'], instance) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'migrate') + except Exception, e: + LOG.exception(_("Error in migrate %s"), e) + raise exc.HTTPBadRequest() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _reset_network(self, input_dict, req, id): + """Permit admins to reset networking on an server""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.reset_network(context, instance) + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::reset_network %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _inject_network_info(self, input_dict, req, id): + """Permit admins to inject network info into a server""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.inject_network_info(context, instance) + except exception.InstanceNotFound: + raise exc.HTTPNotFound(_("Server not found")) + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::inject_network_info %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _lock(self, input_dict, req, id): + """Permit admins to lock a server""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.lock(context, instance) + except exception.InstanceNotFound: + raise exc.HTTPNotFound(_("Server not found")) + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::lock %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _unlock(self, input_dict, req, id): + """Permit admins to lock a server""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.unlock(context, instance) + except exception.InstanceNotFound: + raise exc.HTTPNotFound(_("Server not found")) + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::unlock %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + def _create_backup(self, input_dict, req, instance_id): + """Backup a server instance. + + Images now have an `image_type` associated with them, which can be + 'snapshot' or the backup type, like 'daily' or 'weekly'. + + If the image_type is backup-like, then the rotation factor can be + included and that will cause the oldest backups that exceed the + rotation factor to be deleted. + + """ + context = req.environ["nova.context"] + + try: + entity = input_dict["createBackup"] + except (KeyError, TypeError): + raise exc.HTTPBadRequest(_("Malformed request body")) + + try: + image_name = entity["name"] + backup_type = entity["backup_type"] + rotation = entity["rotation"] + + except KeyError as missing_key: + msg = _("createBackup entity requires %s attribute") % missing_key + raise exc.HTTPBadRequest(explanation=msg) + + except TypeError: + msg = _("Malformed createBackup entity") + raise exc.HTTPBadRequest(explanation=msg) + + try: + rotation = int(rotation) + except ValueError: + msg = _("createBackup attribute 'rotation' must be an integer") + raise exc.HTTPBadRequest(explanation=msg) + + # preserve link to server in image properties + server_ref = os.path.join(req.application_url, 'servers', instance_id) + props = {'instance_ref': server_ref} + + metadata = entity.get('metadata', {}) + common.check_img_metadata_quota_limit(context, metadata) + try: + props.update(metadata) + except ValueError: + msg = _("Invalid metadata") + raise exc.HTTPBadRequest(explanation=msg) + + try: + instance = self.compute_api.get(context, instance_id) + except exception.NotFound: + raise exc.HTTPNotFound(_("Instance not found")) + + try: + image = self.compute_api.backup(context, instance, image_name, + backup_type, rotation, extra_properties=props) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'createBackup') + + # build location of newly-created image entity + image_id = str(image['id']) + image_ref = os.path.join(req.application_url, 'images', image_id) + + resp = webob.Response(status_int=202) + resp.headers['Location'] = image_ref + return resp + + def get_actions(self): + actions = [ + #TODO(bcwaldon): These actions should be prefixed with 'os-' + extensions.ActionExtension("servers", "pause", self._pause), + extensions.ActionExtension("servers", "unpause", self._unpause), + extensions.ActionExtension("servers", "suspend", self._suspend), + extensions.ActionExtension("servers", "resume", self._resume), + extensions.ActionExtension("servers", "migrate", self._migrate), + + extensions.ActionExtension("servers", + "createBackup", + self._create_backup), + + extensions.ActionExtension("servers", + "resetNetwork", + self._reset_network), + + extensions.ActionExtension("servers", + "injectNetworkInfo", + self._inject_network_info), + + extensions.ActionExtension("servers", "lock", self._lock), + extensions.ActionExtension("servers", "unlock", self._unlock), + ] + + return actions diff --git a/nova/api/openstack/compute/contrib/cloudpipe.py b/nova/api/openstack/compute/contrib/cloudpipe.py new file mode 100644 index 000000000..17bfa810b --- /dev/null +++ b/nova/api/openstack/compute/contrib/cloudpipe.py @@ -0,0 +1,172 @@ +# Copyright 2011 Openstack, LLC. +# +# 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. + +"""Connect your vlan to the world.""" + +import os + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova.auth import manager +from nova.cloudpipe import pipelib +from nova import compute +from nova.compute import vm_states +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils + + +FLAGS = flags.FLAGS +LOG = logging.getLogger("nova.api.openstack.compute.contrib.cloudpipe") + + +class CloudpipeTemplate(xmlutil.TemplateBuilder): + def construct(self): + return xmlutil.MasterTemplate(xmlutil.make_flat_dict('cloudpipe'), 1) + + +class CloudpipesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('cloudpipes') + elem = xmlutil.make_flat_dict('cloudpipe', selector='cloudpipes', + subselector='cloudpipe') + root.append(elem) + return xmlutil.MasterTemplate(root, 1) + + +class CloudpipeController(object): + """Handle creating and listing cloudpipe instances.""" + + def __init__(self): + self.compute_api = compute.API() + self.auth_manager = manager.AuthManager() + self.cloudpipe = pipelib.CloudPipe() + self.setup() + + def setup(self): + """Ensure the keychains and folders exist.""" + # TODO(todd): this was copyed from api.ec2.cloud + # FIXME(ja): this should be moved to a nova-manage command, + # if not setup throw exceptions instead of running + # Create keys folder, if it doesn't exist + if not os.path.exists(FLAGS.keys_path): + os.makedirs(FLAGS.keys_path) + # Gen root CA, if we don't have one + root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) + if not os.path.exists(root_ca_path): + genrootca_sh_path = os.path.join(os.path.dirname(__file__), + os.path.pardir, + os.path.pardir, + 'CA', + 'genrootca.sh') + + start = os.getcwd() + if not os.path.exists(FLAGS.ca_path): + os.makedirs(FLAGS.ca_path) + os.chdir(FLAGS.ca_path) + # TODO(vish): Do this with M2Crypto instead + utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path) + os.chdir(start) + + def _get_cloudpipe_for_project(self, context, project_id): + """Get the cloudpipe instance for a project ID.""" + # NOTE(todd): this should probably change to compute_api.get_all + # or db.instance_get_project_vpn + for instance in db.instance_get_all_by_project(context, project_id): + if (instance['image_id'] == str(FLAGS.vpn_image_id) + and instance['vm_state'] != vm_states.DELETED): + return instance + + def _vpn_dict(self, project, vpn_instance): + rv = {'project_id': project.id, + 'public_ip': project.vpn_ip, + 'public_port': project.vpn_port} + if vpn_instance: + rv['instance_id'] = vpn_instance['uuid'] + rv['created_at'] = utils.isotime(vpn_instance['created_at']) + address = vpn_instance.get('fixed_ip', None) + if address: + rv['internal_ip'] = address['address'] + if project.vpn_ip and project.vpn_port: + if utils.vpn_ping(project.vpn_ip, project.vpn_port): + rv['state'] = 'running' + else: + rv['state'] = 'down' + else: + rv['state'] = 'invalid' + else: + rv['state'] = 'pending' + return rv + + @wsgi.serializers(xml=CloudpipeTemplate) + def create(self, req, body): + """Create a new cloudpipe instance, if none exists. + + Parameters: {cloudpipe: {project_id: XYZ}} + """ + + ctxt = req.environ['nova.context'] + params = body.get('cloudpipe', {}) + project_id = params.get('project_id', ctxt.project_id) + instance = self._get_cloudpipe_for_project(ctxt, project_id) + if not instance: + proj = self.auth_manager.get_project(project_id) + user_id = proj.project_manager_id + try: + self.cloudpipe.launch_vpn_instance(project_id, user_id) + except db.NoMoreNetworks: + msg = _("Unable to claim IP for VPN instances, ensure it " + "isn't running, and try again in a few minutes") + raise exception.ApiError(msg) + instance = self._get_cloudpipe_for_project(ctxt, proj) + return {'instance_id': instance['uuid']} + + @wsgi.serializers(xml=CloudpipesTemplate) + def index(self, req): + """Show admins the list of running cloudpipe instances.""" + context = req.environ['nova.context'] + vpns = [] + # TODO(todd): could use compute_api.get_all with admin context? + for project in self.auth_manager.get_projects(): + instance = self._get_cloudpipe_for_project(context, project.id) + vpns.append(self._vpn_dict(project, instance)) + return {'cloudpipes': vpns} + + +class Cloudpipe(extensions.ExtensionDescriptor): + """Adds actions to create cloudpipe instances. + + When running with the Vlan network mode, you need a mechanism to route + from the public Internet to your vlans. This mechanism is known as a + cloudpipe. + + At the time of creating this class, only OpenVPN is supported. Support for + a SSH Bastion host is forthcoming. + """ + + name = "Cloudpipe" + alias = "os-cloudpipe" + namespace = "http://docs.openstack.org/compute/ext/cloudpipe/api/v1.1" + updated = "2011-12-16T00:00:00+00:00" + admin_only = True + + def get_resources(self): + resources = [] + res = extensions.ResourceExtension('os-cloudpipe', + CloudpipeController()) + resources.append(res) + return resources diff --git a/nova/api/openstack/compute/contrib/console_output.py b/nova/api/openstack/compute/contrib/console_output.py new file mode 100644 index 000000000..a27d8663b --- /dev/null +++ b/nova/api/openstack/compute/contrib/console_output.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Grid Dynamics +# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev +# +# 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 import compute +from nova import exception +from nova import log as logging +from nova.api.openstack import extensions + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib.console_output') + + +class Console_output(extensions.ExtensionDescriptor): + """Console log output support, with tailing ability.""" + + name = "Console_output" + alias = "os-console-output" + namespace = "http://docs.openstack.org/compute/ext/" \ + "os-console-output/api/v2" + updated = "2011-12-08T00:00:00+00:00" + + def __init__(self, ext_mgr): + self.compute_api = compute.API() + super(Console_output, self).__init__(ext_mgr) + + def get_console_output(self, input_dict, req, server_id): + """Get text console output.""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.routing_get(context, server_id) + except exception.NotFound: + raise webob.exc.HTTPNotFound(_('Instance not found')) + + try: + length = input_dict['os-getConsoleOutput'].get('length') + except (TypeError, KeyError): + raise webob.exc.HTTPBadRequest(_('Malformed request body')) + + try: + output = self.compute_api.get_console_output(context, + instance, + length) + except exception.ApiError, e: + raise webob.exc.HTTPBadRequest(explanation=e.message) + except exception.NotAuthorized, e: + raise webob.exc.HTTPUnauthorized() + + return {'output': output} + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [extensions.ActionExtension("servers", "os-getConsoleOutput", + self.get_console_output)] + + return actions diff --git a/nova/api/openstack/compute/contrib/createserverext.py b/nova/api/openstack/compute/contrib/createserverext.py new file mode 100644 index 000000000..25b53a0d6 --- /dev/null +++ b/nova/api/openstack/compute/contrib/createserverext.py @@ -0,0 +1,60 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# +# 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 extensions +from nova.api.openstack.compute import servers +from nova.api.openstack.compute import views +from nova.api.openstack import wsgi + + +class ViewBuilder(views.servers.ViewBuilder): + """Adds security group output when viewing server details.""" + + def show(self, request, instance): + """Detailed view of a single instance.""" + server = super(ViewBuilder, self).show(request, instance) + server["server"]["security_groups"] = self._get_groups(instance) + return server + + def _get_groups(self, instance): + """Get a list of security groups for this instance.""" + groups = instance.get('security_groups') + if groups is not None: + return [{"name": group["name"]} for group in groups] + + +class Controller(servers.Controller): + _view_builder_class = ViewBuilder + + +class Createserverext(extensions.ExtensionDescriptor): + """Extended support to the Create Server v1.1 API""" + + name = "Createserverext" + alias = "os-create-server-ext" + namespace = "http://docs.openstack.org/compute/ext/" \ + "createserverext/api/v1.1" + updated = "2011-07-19T00:00:00+00:00" + + def get_resources(self): + resources = [] + controller = Controller() + + res = extensions.ResourceExtension('os-create-server-ext', + controller=controller) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/deferred_delete.py b/nova/api/openstack/compute/contrib/deferred_delete.py new file mode 100644 index 000000000..312c22c80 --- /dev/null +++ b/nova/api/openstack/compute/contrib/deferred_delete.py @@ -0,0 +1,77 @@ +# Copyright 2011 Openstack, LLC +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The deferred instance delete extension.""" + +import webob + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack.compute import servers +from nova import compute +from nova import exception +from nova import log as logging + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.deferred-delete") + + +class Deferred_delete(extensions.ExtensionDescriptor): + """Instance deferred delete""" + + name = "DeferredDelete" + alias = "os-deferred-delete" + namespace = "http://docs.openstack.org/compute/ext/" \ + "deferred-delete/api/v1.1" + updated = "2011-09-01T00:00:00+00:00" + + def __init__(self, ext_mgr): + super(Deferred_delete, self).__init__(ext_mgr) + self.compute_api = compute.API() + + def _restore(self, input_dict, req, instance_id): + """Restore a previously deleted instance.""" + + context = req.environ["nova.context"] + instance = self.compute_api.get(context, instance_id) + try: + self.compute_api.restore(context, instance) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'restore') + return webob.Response(status_int=202) + + def _force_delete(self, input_dict, req, instance_id): + """Force delete of instance before deferred cleanup.""" + + context = req.environ["nova.context"] + instance = self.compute_api.get(context, instance_id) + try: + self.compute_api.force_delete(context, instance) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'forceDelete') + return webob.Response(status_int=202) + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + extensions.ActionExtension("servers", "restore", + self._restore), + extensions.ActionExtension("servers", "forceDelete", + self._force_delete), + ] + + return actions diff --git a/nova/api/openstack/compute/contrib/disk_config.py b/nova/api/openstack/compute/contrib/disk_config.py new file mode 100644 index 000000000..392291652 --- /dev/null +++ b/nova/api/openstack/compute/contrib/disk_config.py @@ -0,0 +1,200 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# +# 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 + +"""Disk Config extension.""" + +from xml.dom import minidom + +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import xmlutil +from nova import compute +from nova import db +from nova import log as logging +from nova import utils + +LOG = logging.getLogger('nova.api.openstack.contrib.disk_config') + +ALIAS = 'RAX-DCF' +XMLNS_DCF = "http://docs.rackspacecloud.com/servers/api/ext/diskConfig/v1.0" + + +class ServerDiskConfigTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server') + root.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) + return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) + + +class ServersDiskConfigTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + elem.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) + return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) + + +class ImageDiskConfigTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('image') + root.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) + return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) + + +class ImagesDiskConfigTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + elem.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) + return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) + + +def disk_config_to_api(value): + return 'AUTO' if value else 'MANUAL' + + +def disk_config_from_api(value): + if value == 'AUTO': + return True + elif value == 'MANUAL': + return False + else: + msg = _("RAX-DCF:diskConfig must be either 'MANUAL' or 'AUTO'.") + raise exc.HTTPBadRequest(explanation=msg) + + +class Disk_config(extensions.ExtensionDescriptor): + """Disk Management Extension""" + + name = "DiskConfig" + alias = ALIAS + namespace = XMLNS_DCF + updated = "2011-09-27:00:00+00:00" + + API_DISK_CONFIG = "%s:diskConfig" % ALIAS + INTERNAL_DISK_CONFIG = "auto_disk_config" + + def __init__(self, ext_mgr): + super(Disk_config, self).__init__(ext_mgr) + self.compute_api = compute.API() + + def _extract_resource_from_body(self, res, body, + singular, singular_template, plural, plural_template): + """Returns a list of the given resources from the request body. + + The templates passed in are used for XML serialization. + """ + template = res.environ.get('nova.template') + if plural in body: + resources = body[plural] + if template: + template.attach(plural_template) + elif singular in body: + resources = [body[singular]] + if template: + template.attach(singular_template) + else: + resources = [] + + return resources + + def _GET_servers(self, req, res, body): + context = req.environ['nova.context'] + + servers = self._extract_resource_from_body(res, body, + singular='server', singular_template=ServerDiskConfigTemplate(), + plural='servers', plural_template=ServersDiskConfigTemplate()) + + # Filter out any servers that already have the key set (most likely + # from a remote zone) + servers = filter(lambda s: self.API_DISK_CONFIG not in s, servers) + + # Get DB information for servers + uuids = [server['id'] for server in servers] + db_servers = db.instance_get_all_by_filters(context, {'uuid': uuids}) + db_servers = dict([(s['uuid'], s) for s in db_servers]) + + for server in servers: + db_server = db_servers.get(server['id']) + if db_server: + value = db_server[self.INTERNAL_DISK_CONFIG] + server[self.API_DISK_CONFIG] = disk_config_to_api(value) + + return res + + def _GET_images(self, req, res, body): + images = self._extract_resource_from_body(res, body, + singular='image', singular_template=ImageDiskConfigTemplate(), + plural='images', plural_template=ImagesDiskConfigTemplate()) + + for image in images: + metadata = image['metadata'] + + if self.INTERNAL_DISK_CONFIG in metadata: + raw_value = metadata[self.INTERNAL_DISK_CONFIG] + value = utils.bool_from_str(raw_value) + image[self.API_DISK_CONFIG] = disk_config_to_api(value) + + return res + + def _POST_servers(self, req, res, body): + return self._GET_servers(req, res, body) + + def _pre_POST_servers(self, req): + # NOTE(sirp): deserialization currently occurs *after* pre-processing + # extensions are called. Until extensions are refactored so that + # deserialization occurs earlier, we have to perform the + # deserialization ourselves. + content_type = req.content_type + + if 'xml' in content_type: + node = minidom.parseString(req.body) + server = node.getElementsByTagName('server')[0] + api_value = server.getAttribute(self.API_DISK_CONFIG) + if api_value: + value = disk_config_from_api(api_value) + server.setAttribute(self.INTERNAL_DISK_CONFIG, str(value)) + req.body = str(node.toxml()) + else: + body = utils.loads(req.body) + server = body['server'] + api_value = server.get(self.API_DISK_CONFIG) + if api_value: + value = disk_config_from_api(api_value) + server[self.INTERNAL_DISK_CONFIG] = value + req.body = utils.dumps(body) + + def _pre_PUT_servers(self, req): + return self._pre_POST_servers(req) + + def get_request_extensions(self): + ReqExt = extensions.RequestExtension + return [ + ReqExt(method='GET', + url_route='/:(project_id)/servers/:(id)', + handler=self._GET_servers), + ReqExt(method='POST', + url_route='/:(project_id)/servers', + handler=self._POST_servers, + pre_handler=self._pre_POST_servers), + ReqExt(method='PUT', + url_route='/:(project_id)/servers/:(id)', + pre_handler=self._pre_PUT_servers), + ReqExt(method='GET', + url_route='/:(project_id)/images/:(id)', + handler=self._GET_images) + ] diff --git a/nova/api/openstack/compute/contrib/extended_status.py b/nova/api/openstack/compute/contrib/extended_status.py new file mode 100644 index 000000000..7f9301b93 --- /dev/null +++ b/nova/api/openstack/compute/contrib/extended_status.py @@ -0,0 +1,116 @@ +# Copyright 2011 Openstack, LLC. +# +# 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. + +"""The Extended Status Admin API extension.""" + +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging + + +FLAGS = flags.FLAGS +LOG = logging.getLogger("nova.api.openstack.compute.contrib.extendedstatus") + + +class Extended_status(extensions.ExtensionDescriptor): + """Extended Status support""" + + name = "ExtendedStatus" + alias = "OS-EXT-STS" + namespace = "http://docs.openstack.org/compute/ext/" \ + "extended_status/api/v1.1" + updated = "2011-11-03T00:00:00+00:00" + admin_only = True + + def get_request_extensions(self): + request_extensions = [] + + def _get_and_extend_one(context, server_id, body): + compute_api = compute.API() + try: + inst_ref = compute_api.routing_get(context, server_id) + except exception.NotFound: + LOG.warn("Instance %s not found (one)" % server_id) + explanation = _("Server not found.") + raise exc.HTTPNotFound(explanation=explanation) + + for state in ['task_state', 'vm_state', 'power_state']: + key = "%s:%s" % (Extended_status.alias, state) + body['server'][key] = inst_ref[state] + + def _get_and_extend_all(context, body): + # TODO(mdietz): This is a brilliant argument for this to *not* + # be an extension. The problem is we either have to 1) duplicate + # the logic from the servers controller or 2) do what we did + # and iterate over the list of potentially sorted, limited + # and whatever else elements and find each individual. + compute_api = compute.API() + + for server in list(body['servers']): + try: + inst_ref = compute_api.routing_get(context, server['id']) + except exception.NotFound: + # NOTE(dtroyer): A NotFound exception at this point + # happens because a delete was in progress and the + # server that was present in the original call to + # compute.api.get_all() is no longer present. + # Delete it from the response and move on. + LOG.warn("Instance %s not found (all)" % server['id']) + body['servers'].remove(server) + continue + + #TODO(bcwaldon): these attributes should be prefixed with + # something specific to this extension + for state in ['task_state', 'vm_state', 'power_state']: + key = "%s:%s" % (Extended_status.alias, state) + server[key] = inst_ref[state] + + def _extended_status_handler(req, res, body): + context = req.environ['nova.context'] + server_id = req.environ['wsgiorg.routing_args'][1].get('id') + + if 'nova.template' in req.environ: + tmpl = req.environ['nova.template'] + tmpl.attach(ExtendedStatusTemplate()) + + if server_id: + _get_and_extend_one(context, server_id, body) + else: + _get_and_extend_all(context, body) + return res + + req_ext = extensions.RequestExtension('GET', + '/:(project_id)/servers/:(id)', + _extended_status_handler) + request_extensions.append(req_ext) + + return request_extensions + + +class ExtendedStatusTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server') + root.set('{%s}task_state' % Extended_status.namespace, + '%s:task_state' % Extended_status.alias) + root.set('{%s}power_state' % Extended_status.namespace, + '%s:power_state' % Extended_status.alias) + root.set('{%s}vm_state' % Extended_status.namespace, + '%s:vm_state' % Extended_status.alias) + return xmlutil.SlaveTemplate(root, 1, nsmap={ + Extended_status.alias: Extended_status.namespace}) diff --git a/nova/api/openstack/compute/contrib/flavorextradata.py b/nova/api/openstack/compute/contrib/flavorextradata.py new file mode 100644 index 000000000..bf6fa8040 --- /dev/null +++ b/nova/api/openstack/compute/contrib/flavorextradata.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Canonical Ltd. +# 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. + +""" +The Flavor extra data extension +Openstack API version 1.1 lists "name", "ram", "disk", "vcpus" as flavor +attributes. This extension adds to that list: + rxtx_cap + rxtx_quota + swap +""" + +from nova.api.openstack import extensions + + +class Flavorextradata(extensions.ExtensionDescriptor): + """Provide additional data for flavors""" + + name = "FlavorExtraData" + alias = "os-flavor-extra-data" + namespace = "http://docs.openstack.org/compute/ext/" \ + "flavor_extra_data/api/v1.1" + updated = "2011-09-14T00:00:00+00:00" diff --git a/nova/api/openstack/compute/contrib/flavorextraspecs.py b/nova/api/openstack/compute/contrib/flavorextraspecs.py new file mode 100644 index 000000000..eafea5d1f --- /dev/null +++ b/nova/api/openstack/compute/contrib/flavorextraspecs.py @@ -0,0 +1,127 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +""" The instance type extra specs extension""" + +from webob import exc + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import db +from nova import exception + + +class ExtraSpecsTemplate(xmlutil.TemplateBuilder): + def construct(self): + return xmlutil.MasterTemplate(xmlutil.make_flat_dict('extra_specs'), 1) + + +class FlavorExtraSpecsController(object): + """ The flavor extra specs API controller for the Openstack API """ + + def _get_extra_specs(self, context, flavor_id): + extra_specs = db.instance_type_extra_specs_get(context, flavor_id) + specs_dict = {} + for key, value in extra_specs.iteritems(): + specs_dict[key] = value + return dict(extra_specs=specs_dict) + + def _check_body(self, body): + if body is None or body == "": + expl = _('No Request Body') + raise exc.HTTPBadRequest(explanation=expl) + + @wsgi.serializers(xml=ExtraSpecsTemplate) + def index(self, req, flavor_id): + """ Returns the list of extra specs for a givenflavor """ + context = req.environ['nova.context'] + return self._get_extra_specs(context, flavor_id) + + @wsgi.serializers(xml=ExtraSpecsTemplate) + def create(self, req, flavor_id, body): + self._check_body(body) + context = req.environ['nova.context'] + specs = body.get('extra_specs') + try: + db.instance_type_extra_specs_update_or_create(context, + flavor_id, + specs) + except exception.QuotaError as error: + self._handle_quota_error(error) + return body + + @wsgi.serializers(xml=ExtraSpecsTemplate) + def update(self, req, flavor_id, id, body): + self._check_body(body) + context = req.environ['nova.context'] + if not id in body: + expl = _('Request body and URI mismatch') + raise exc.HTTPBadRequest(explanation=expl) + if len(body) > 1: + expl = _('Request body contains too many items') + raise exc.HTTPBadRequest(explanation=expl) + try: + db.instance_type_extra_specs_update_or_create(context, + flavor_id, + body) + except exception.QuotaError as error: + self._handle_quota_error(error) + + return body + + @wsgi.serializers(xml=ExtraSpecsTemplate) + def show(self, req, flavor_id, id): + """ Return a single extra spec item """ + context = req.environ['nova.context'] + specs = self._get_extra_specs(context, flavor_id) + if id in specs['extra_specs']: + return {id: specs['extra_specs'][id]} + else: + raise exc.HTTPNotFound() + + def delete(self, req, flavor_id, id): + """ Deletes an existing extra spec """ + context = req.environ['nova.context'] + db.instance_type_extra_specs_delete(context, flavor_id, id) + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions.""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPBadRequest(explanation=error.message) + raise error + + +class Flavorextraspecs(extensions.ExtensionDescriptor): + """Instance type (flavor) extra specs""" + + name = "FlavorExtraSpecs" + alias = "os-flavor-extra-specs" + namespace = "http://docs.openstack.org/compute/ext/" \ + "flavor_extra_specs/api/v1.1" + updated = "2011-06-23T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-extra_specs', + FlavorExtraSpecsController(), + parent=dict(member_name='flavor', collection_name='flavors')) + + resources.append(res) + return resources diff --git a/nova/api/openstack/compute/contrib/floating_ip_dns.py b/nova/api/openstack/compute/contrib/floating_ip_dns.py new file mode 100644 index 000000000..032d5bd7e --- /dev/null +++ b/nova/api/openstack/compute/contrib/floating_ip_dns.py @@ -0,0 +1,227 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Andrew Bogott for the Wikimedia 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 urllib + +import webob + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import exception +from nova import log as logging +from nova import network + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib.floating_ip_dns') + + +def make_dns_entry(elem): + elem.set('id') + elem.set('ip') + elem.set('type') + elem.set('zone') + elem.set('name') + + +def make_zone_entry(elem): + elem.set('zone') + + +class FloatingIPDNSTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('dns_entry', + selector='dns_entry') + make_dns_entry(root) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPDNSsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('dns_entries') + elem = xmlutil.SubTemplateElement(root, 'dns_entry', + selector='dns_entries') + make_dns_entry(elem) + return xmlutil.MasterTemplate(root, 1) + + +class ZonesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zones') + elem = xmlutil.SubTemplateElement(root, 'zone', + selector='zones') + make_zone_entry(elem) + return xmlutil.MasterTemplate(root, 1) + + +def _translate_dns_entry_view(dns_entry): + result = {} + result['ip'] = dns_entry.get('ip') + result['id'] = dns_entry.get('id') + result['type'] = dns_entry.get('type') + result['zone'] = dns_entry.get('zone') + result['name'] = dns_entry.get('name') + return {'dns_entry': result} + + +def _translate_dns_entries_view(dns_entries): + return {'dns_entries': [_translate_dns_entry_view(entry)['dns_entry'] + for entry in dns_entries]} + + +def _translate_zone_entries_view(zonelist): + return {'zones': [{'zone': zone} for zone in zonelist]} + + +def _unquote_zone(zone): + """Unquoting function for receiving a zone name in a URL. + + Zone 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. + """ + return urllib.unquote(zone).replace('%2E', '.') + + +def _create_dns_entry(ip, name, zone): + return {'ip': ip, 'name': name, 'zone': zone} + + +class FloatingIPDNSController(object): + """DNS Entry controller for OpenStack API""" + + def __init__(self): + self.network_api = network.API() + super(FloatingIPDNSController, self).__init__() + + @wsgi.serializers(xml=FloatingIPDNSsTemplate) + def show(self, req, id): + """Return a list of dns entries. If ip is specified, query for + names. if name is specified, query for ips. + Quoted domain (aka 'zone') specified as id.""" + context = req.environ['nova.context'] + params = req.GET + floating_ip = params['ip'] if 'ip' in params else "" + name = params['name'] if 'name' in params else "" + zone = _unquote_zone(id) + + if floating_ip: + entries = self.network_api.get_dns_entries_by_address(context, + floating_ip, + zone) + entrylist = [_create_dns_entry(floating_ip, entry, zone) + for entry in entries] + elif name: + entries = self.network_api.get_dns_entries_by_name(context, + name, zone) + entrylist = [_create_dns_entry(entry, name, zone) + for entry in entries] + else: + entrylist = [] + + return _translate_dns_entries_view(entrylist) + + @wsgi.serializers(xml=ZonesTemplate) + def index(self, req): + """Return a list of available DNS zones.""" + + context = req.environ['nova.context'] + zones = self.network_api.get_dns_zones(context) + + return _translate_zone_entries_view(zones) + + @wsgi.serializers(xml=FloatingIPDNSTemplate) + def create(self, req, body): + """Add dns entry for name and address""" + context = req.environ['nova.context'] + + try: + entry = body['dns_entry'] + address = entry['ip'] + name = entry['name'] + dns_type = entry['dns_type'] + zone = entry['zone'] + except (TypeError, KeyError): + raise webob.exc.HTTPUnprocessableEntity() + + try: + self.network_api.add_dns_entry(context, address, name, + dns_type, zone) + except exception.FloatingIpDNSExists: + return webob.Response(status_int=409) + + return _translate_dns_entry_view({'ip': address, + 'name': name, + 'type': dns_type, + 'zone': zone}) + + def update(self, req, id, body): + """Modify a dns entry.""" + context = req.environ['nova.context'] + zone = _unquote_zone(id) + + try: + entry = body['dns_entry'] + name = entry['name'] + new_ip = entry['ip'] + except (TypeError, KeyError): + raise webob.exc.HTTPUnprocessableEntity() + + try: + self.network_api.modify_dns_entry(context, name, + new_ip, zone) + except exception.NotFound: + return webob.Response(status_int=404) + + return _translate_dns_entry_view({'ip': new_ip, + 'name': name, + 'zone': zone}) + + def delete(self, req, id): + """Delete the entry identified by req and id. """ + context = req.environ['nova.context'] + params = req.GET + name = params['name'] if 'name' in params else "" + zone = _unquote_zone(id) + + try: + self.network_api.delete_dns_entry(context, name, zone) + except exception.NotFound: + return webob.Response(status_int=404) + + return webob.Response(status_int=200) + + +class Floating_ip_dns(extensions.ExtensionDescriptor): + """Floating IP DNS support""" + + name = "Floating_ip_dns" + alias = "os-floating-ip-dns" + namespace = "http://docs.openstack.org/ext/floating_ip_dns/api/v1.1" + updated = "2011-12-23:00:00+00:00" + + def __init__(self, ext_mgr): + self.network_api = network.API() + super(Floating_ip_dns, self).__init__(ext_mgr) + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-floating-ip-dns', + FloatingIPDNSController()) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/floating_ip_pools.py b/nova/api/openstack/compute/contrib/floating_ip_pools.py new file mode 100644 index 000000000..01b9a3645 --- /dev/null +++ b/nova/api/openstack/compute/contrib/floating_ip_pools.py @@ -0,0 +1,104 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 X.commerce, a business unit of eBay 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 nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import log as logging +from nova import network + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib.floating_ip_pools') + + +def _translate_floating_ip_view(pool): + return { + 'name': pool['name'], + } + + +def _translate_floating_ip_pools_view(pools): + return { + 'floating_ip_pools': [_translate_floating_ip_view(pool) + for pool in pools] + } + + +class FloatingIPPoolsController(object): + """The Floating IP Pool API controller for the OpenStack API.""" + + def __init__(self): + self.network_api = network.API() + super(FloatingIPPoolsController, self).__init__() + + def index(self, req): + """Return a list of pools.""" + context = req.environ['nova.context'] + pools = self.network_api.get_floating_ip_pools(context) + return _translate_floating_ip_pools_view(pools) + + +def make_float_ip(elem): + elem.set('name') + + +class FloatingIPPoolTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ip_pool', + selector='floating_ip_pool') + make_float_ip(root) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPPoolsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ip_pools') + elem = xmlutil.SubTemplateElement(root, 'floating_ip_pool', + selector='floating_ip_pools') + make_float_ip(elem) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPPoolsSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return FloatingIPPoolsTemplate() + + +class Floating_ip_pools(extensions.ExtensionDescriptor): + """Floating IPs support""" + + name = "Floating_ip_pools" + alias = "os-floating-ip-pools" + namespace = \ + "http://docs.openstack.org/compute/ext/floating_ip_pools/api/v1.1" + updated = "2012-01-04T00:00:00+00:00" + + def get_resources(self): + resources = [] + + body_serializers = { + 'application/xml': FloatingIPPoolsSerializer(), + } + + serializer = wsgi.ResponseSerializer(body_serializers) + + res = extensions.ResourceExtension('os-floating-ip-pools', + FloatingIPPoolsController(), + serializer=serializer, + member_actions={}) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/floating_ips.py b/nova/api/openstack/compute/contrib/floating_ips.py new file mode 100644 index 000000000..2400f6c83 --- /dev/null +++ b/nova/api/openstack/compute/contrib/floating_ips.py @@ -0,0 +1,237 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 Grid Dynamics +# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev +# +# 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 import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import compute +from nova import exception +from nova import log as logging +from nova import network +from nova import rpc + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib.floating_ips') + + +def make_float_ip(elem): + elem.set('id') + elem.set('ip') + elem.set('pool') + elem.set('fixed_ip') + elem.set('instance_id') + + +class FloatingIPTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ip', + selector='floating_ip') + make_float_ip(root) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ips') + elem = xmlutil.SubTemplateElement(root, 'floating_ip', + selector='floating_ips') + make_float_ip(elem) + return xmlutil.MasterTemplate(root, 1) + + +def _translate_floating_ip_view(floating_ip): + result = { + 'id': floating_ip['id'], + 'ip': floating_ip['address'], + 'pool': floating_ip['pool'], + } + try: + result['fixed_ip'] = floating_ip['fixed_ip']['address'] + except (TypeError, KeyError): + result['fixed_ip'] = None + try: + result['instance_id'] = floating_ip['fixed_ip']['instance']['uuid'] + except (TypeError, KeyError): + result['instance_id'] = None + return {'floating_ip': result} + + +def _translate_floating_ips_view(floating_ips): + return {'floating_ips': [_translate_floating_ip_view(ip)['floating_ip'] + for ip in floating_ips]} + + +class FloatingIPController(object): + """The Floating IPs API controller for the OpenStack API.""" + + def __init__(self): + self.network_api = network.API() + super(FloatingIPController, self).__init__() + + @wsgi.serializers(xml=FloatingIPTemplate) + def show(self, req, id): + """Return data about the given floating ip.""" + context = req.environ['nova.context'] + + try: + floating_ip = self.network_api.get_floating_ip(context, id) + except exception.NotFound: + raise webob.exc.HTTPNotFound() + + return _translate_floating_ip_view(floating_ip) + + @wsgi.serializers(xml=FloatingIPsTemplate) + def index(self, req): + """Return a list of floating ips allocated to a project.""" + context = req.environ['nova.context'] + + floating_ips = self.network_api.get_floating_ips_by_project(context) + + return _translate_floating_ips_view(floating_ips) + + @wsgi.serializers(xml=FloatingIPTemplate) + def create(self, req, body=None): + context = req.environ['nova.context'] + + pool = None + if body and 'pool' in body: + pool = body['pool'] + try: + address = self.network_api.allocate_floating_ip(context, pool) + ip = self.network_api.get_floating_ip_by_address(context, address) + except rpc.RemoteError as ex: + # NOTE(tr3buchet) - why does this block exist? + if ex.exc_type == 'NoMoreFloatingIps': + if pool: + msg = _("No more floating ips in pool %s.") % pool + else: + msg = _("No more floating ips available.") + raise webob.exc.HTTPBadRequest(explanation=msg) + else: + raise + + return _translate_floating_ip_view(ip) + + def delete(self, req, id): + context = req.environ['nova.context'] + floating_ip = self.network_api.get_floating_ip(context, id) + + if floating_ip.get('fixed_ip'): + self.network_api.disassociate_floating_ip(context, + floating_ip['address']) + + self.network_api.release_floating_ip(context, + address=floating_ip['address']) + return webob.Response(status_int=202) + + def _get_ip_by_id(self, context, value): + """Checks that value is id and then returns its address.""" + return self.network_api.get_floating_ip(context, value)['address'] + + +class FloatingIPSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return FloatingIPsTemplate() + + def default(self): + return FloatingIPTemplate() + + +class Floating_ips(extensions.ExtensionDescriptor): + """Floating IPs support""" + + name = "Floating_ips" + alias = "os-floating-ips" + namespace = "http://docs.openstack.org/compute/ext/floating_ips/api/v1.1" + updated = "2011-06-16T00:00:00+00:00" + + def __init__(self, ext_mgr): + self.compute_api = compute.API() + self.network_api = network.API() + super(Floating_ips, self).__init__(ext_mgr) + + def _add_floating_ip(self, input_dict, req, instance_id): + """Associate floating_ip to an instance.""" + context = req.environ['nova.context'] + + try: + address = input_dict['addFloatingIp']['address'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Address not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + instance = self.compute_api.get(context, instance_id) + self.compute_api.associate_floating_ip(context, instance, + address) + except exception.ApiError, e: + raise webob.exc.HTTPBadRequest(explanation=e.message) + except exception.NotAuthorized, e: + raise webob.exc.HTTPUnauthorized() + + return webob.Response(status_int=202) + + def _remove_floating_ip(self, input_dict, req, instance_id): + """Dissociate floating_ip from an instance.""" + context = req.environ['nova.context'] + + try: + address = input_dict['removeFloatingIp']['address'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Address not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + floating_ip = self.network_api.get_floating_ip_by_address(context, + address) + if floating_ip.get('fixed_ip'): + try: + self.network_api.disassociate_floating_ip(context, address) + except exception.NotAuthorized, e: + raise webob.exc.HTTPUnauthorized() + + return webob.Response(status_int=202) + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-floating-ips', + FloatingIPController(), + member_actions={}) + resources.append(res) + + return resources + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + extensions.ActionExtension("servers", "addFloatingIp", + self._add_floating_ip), + extensions.ActionExtension("servers", "removeFloatingIp", + self._remove_floating_ip), + ] + + return actions diff --git a/nova/api/openstack/compute/contrib/hosts.py b/nova/api/openstack/compute/contrib/hosts.py new file mode 100644 index 000000000..66dd64def --- /dev/null +++ b/nova/api/openstack/compute/contrib/hosts.py @@ -0,0 +1,187 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The hosts admin extension.""" + +import webob.exc +from xml.dom import minidom +from xml.parsers import expat + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova.scheduler import api as scheduler_api + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.hosts") +FLAGS = flags.FLAGS + + +class HostIndexTemplate(xmlutil.TemplateBuilder): + def construct(self): + def shimmer(obj, do_raise=False): + # A bare list is passed in; we need to wrap it in a dict + return dict(hosts=obj) + + root = xmlutil.TemplateElement('hosts', selector=shimmer) + elem = xmlutil.SubTemplateElement(root, 'host', selector='hosts') + elem.set('host_name') + elem.set('service') + + return xmlutil.MasterTemplate(root, 1) + + +class HostUpdateTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('host') + root.set('host') + root.set('status') + + return xmlutil.MasterTemplate(root, 1) + + +class HostActionTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('host') + root.set('host') + root.set('power_action') + + return xmlutil.MasterTemplate(root, 1) + + +class HostDeserializer(wsgi.XMLDeserializer): + def default(self, string): + try: + node = minidom.parseString(string) + except expat.ExpatError: + msg = _("cannot understand XML") + raise exception.MalformedRequestBody(reason=msg) + + updates = {} + for child in node.childNodes[0].childNodes: + updates[child.tagName] = self.extract_text(child) + + return dict(body=updates) + + +def _list_hosts(req, service=None): + """Returns a summary list of hosts, optionally filtering + by service type. + """ + context = req.environ['nova.context'] + hosts = scheduler_api.get_host_list(context) + if service: + hosts = [host for host in hosts + if host["service"] == service] + return hosts + + +def check_host(fn): + """Makes sure that the host exists.""" + def wrapped(self, req, id, service=None, *args, **kwargs): + listed_hosts = _list_hosts(req, service) + hosts = [h["host_name"] for h in listed_hosts] + if id in hosts: + return fn(self, req, id, *args, **kwargs) + else: + raise exception.HostNotFound(host=id) + return wrapped + + +class HostController(object): + """The Hosts API controller for the OpenStack API.""" + def __init__(self): + self.compute_api = compute.API() + super(HostController, self).__init__() + + @wsgi.serializers(xml=HostIndexTemplate) + def index(self, req): + return {'hosts': _list_hosts(req)} + + @wsgi.serializers(xml=HostUpdateTemplate) + @wsgi.deserializers(xml=HostDeserializer) + @check_host + def update(self, req, id, body): + for raw_key, raw_val in body.iteritems(): + key = raw_key.lower().strip() + val = raw_val.lower().strip() + # NOTE: (dabo) Right now only 'status' can be set, but other + # settings may follow. + if key == "status": + if val[:6] in ("enable", "disabl"): + return self._set_enabled_status(req, id, + enabled=(val.startswith("enable"))) + else: + explanation = _("Invalid status: '%s'") % raw_val + raise webob.exc.HTTPBadRequest(explanation=explanation) + else: + explanation = _("Invalid update setting: '%s'") % raw_key + raise webob.exc.HTTPBadRequest(explanation=explanation) + + def _set_enabled_status(self, req, host, enabled): + """Sets the specified host's ability to accept new instances.""" + context = req.environ['nova.context'] + state = "enabled" if enabled else "disabled" + LOG.audit(_("Setting host %(host)s to %(state)s.") % locals()) + result = self.compute_api.set_host_enabled(context, host=host, + enabled=enabled) + if result not in ("enabled", "disabled"): + # An error message was returned + raise webob.exc.HTTPBadRequest(explanation=result) + return {"host": host, "status": result} + + def _host_power_action(self, req, host, action): + """Reboots, shuts down or powers up the host.""" + context = req.environ['nova.context'] + try: + result = self.compute_api.host_power_action(context, host=host, + action=action) + except NotImplementedError as e: + raise webob.exc.HTTPBadRequest(explanation=e.msg) + return {"host": host, "power_action": result} + + @wsgi.serializers(xml=HostActionTemplate) + def startup(self, req, id): + return self._host_power_action(req, host=id, action="startup") + + @wsgi.serializers(xml=HostActionTemplate) + def shutdown(self, req, id): + return self._host_power_action(req, host=id, action="shutdown") + + @wsgi.serializers(xml=HostActionTemplate) + def reboot(self, req, id): + return self._host_power_action(req, host=id, action="reboot") + + +class Hosts(extensions.ExtensionDescriptor): + """Admin-only host administration""" + + name = "Hosts" + alias = "os-hosts" + namespace = "http://docs.openstack.org/compute/ext/hosts/api/v1.1" + updated = "2011-06-29T00:00:00+00:00" + admin_only = True + + def get_resources(self): + resources = [extensions.ResourceExtension('os-hosts', + HostController(), + collection_actions={'update': 'PUT'}, + member_actions={"startup": "GET", "shutdown": "GET", + "reboot": "GET"})] + return resources diff --git a/nova/api/openstack/compute/contrib/keypairs.py b/nova/api/openstack/compute/contrib/keypairs.py new file mode 100644 index 000000000..5ac205df5 --- /dev/null +++ b/nova/api/openstack/compute/contrib/keypairs.py @@ -0,0 +1,163 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" Keypair management extension""" + +import os +import shutil +import tempfile + +import webob +from webob import exc + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import crypto +from nova import db +from nova import exception + + +class KeypairTemplate(xmlutil.TemplateBuilder): + def construct(self): + return xmlutil.MasterTemplate(xmlutil.make_flat_dict('keypair'), 1) + + +class KeypairsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('keypairs') + elem = xmlutil.make_flat_dict('keypair', selector='keypairs', + subselector='keypair') + root.append(elem) + + return xmlutil.MasterTemplate(root, 1) + + +class KeypairController(object): + """ Keypair API controller for the Openstack API """ + + # TODO(ja): both this file and nova.api.ec2.cloud.py have similar logic. + # move the common keypair logic to nova.compute.API? + + def _gen_key(self): + """ + Generate a key + """ + private_key, public_key, fingerprint = crypto.generate_key_pair() + return {'private_key': private_key, + 'public_key': public_key, + 'fingerprint': fingerprint} + + @wsgi.serializers(xml=KeypairTemplate) + def create(self, req, body): + """ + Create or import keypair. + + Sending name will generate a key and return private_key + and fingerprint. + + You can send a public_key to add an existing ssh key + + params: keypair object with: + name (required) - string + public_key (optional) - string + """ + + context = req.environ['nova.context'] + params = body['keypair'] + name = params['name'] + + # NOTE(ja): generation is slow, so shortcut invalid name exception + try: + db.key_pair_get(context, context.user_id, name) + raise exception.KeyPairExists(key_name=name) + except exception.NotFound: + pass + + keypair = {'user_id': context.user_id, + 'name': name} + + # import if public_key is sent + if 'public_key' in params: + tmpdir = tempfile.mkdtemp() + fn = os.path.join(tmpdir, 'import.pub') + with open(fn, 'w') as pub: + pub.write(params['public_key']) + fingerprint = crypto.generate_fingerprint(fn) + shutil.rmtree(tmpdir) + keypair['public_key'] = params['public_key'] + keypair['fingerprint'] = fingerprint + else: + generated_key = self._gen_key() + keypair['private_key'] = generated_key['private_key'] + keypair['public_key'] = generated_key['public_key'] + keypair['fingerprint'] = generated_key['fingerprint'] + + db.key_pair_create(context, keypair) + return {'keypair': keypair} + + def delete(self, req, id): + """ + Delete a keypair with a given name + """ + context = req.environ['nova.context'] + db.key_pair_destroy(context, context.user_id, id) + return webob.Response(status_int=202) + + @wsgi.serializers(xml=KeypairsTemplate) + def index(self, req): + """ + List of keypairs for a user + """ + context = req.environ['nova.context'] + key_pairs = db.key_pair_get_all_by_user(context, context.user_id) + rval = [] + for key_pair in key_pairs: + rval.append({'keypair': { + 'name': key_pair['name'], + 'public_key': key_pair['public_key'], + 'fingerprint': key_pair['fingerprint'], + }}) + + return {'keypairs': rval} + + +class KeypairsSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return KeypairsTemplate() + + def default(self): + return KeypairTemplate() + + +class Keypairs(extensions.ExtensionDescriptor): + """Keypair Support""" + + name = "Keypairs" + alias = "os-keypairs" + namespace = "http://docs.openstack.org/compute/ext/keypairs/api/v1.1" + updated = "2011-08-08T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-keypairs', + KeypairController()) + + resources.append(res) + return resources diff --git a/nova/api/openstack/compute/contrib/multinic.py b/nova/api/openstack/compute/contrib/multinic.py new file mode 100644 index 000000000..18b95e63d --- /dev/null +++ b/nova/api/openstack/compute/contrib/multinic.py @@ -0,0 +1,106 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The multinic extension.""" + +import webob +from webob import exc + +from nova.api.openstack import extensions +from nova import compute +from nova import exception +from nova import log as logging + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.multinic") + + +# Note: The class name is as it has to be for this to be loaded as an +# extension--only first character capitalized. +class Multinic(extensions.ExtensionDescriptor): + """Multiple network support""" + + name = "Multinic" + alias = "NMN" + namespace = "http://docs.openstack.org/compute/ext/multinic/api/v1.1" + updated = "2011-06-09T00:00:00+00:00" + + def __init__(self, ext_mgr): + """Initialize the extension. + + Gets a compute.API object so we can call the back-end + add_fixed_ip() and remove_fixed_ip() methods. + """ + + super(Multinic, self).__init__(ext_mgr) + self.compute_api = compute.API() + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + + actions = [] + + # Add the add_fixed_ip action + act = extensions.ActionExtension("servers", "addFixedIp", + self._add_fixed_ip) + actions.append(act) + + # Add the remove_fixed_ip action + act = extensions.ActionExtension("servers", "removeFixedIp", + self._remove_fixed_ip) + actions.append(act) + + return actions + + def _get_instance(self, context, instance_id): + try: + return self.compute_api.get(context, instance_id) + except exception.InstanceNotFound: + msg = _("Server not found") + raise exc.HTTPNotFound(msg) + + def _add_fixed_ip(self, input_dict, req, id): + """Adds an IP on a given network to an instance.""" + + # Validate the input entity + if 'networkId' not in input_dict['addFixedIp']: + msg = _("Missing 'networkId' argument for addFixedIp") + raise exc.HTTPUnprocessableEntity(explanation=msg) + + context = req.environ['nova.context'] + instance = self._get_instance(context, id) + network_id = input_dict['addFixedIp']['networkId'] + self.compute_api.add_fixed_ip(context, instance, network_id) + return webob.Response(status_int=202) + + def _remove_fixed_ip(self, input_dict, req, id): + """Removes an IP from an instance.""" + + # Validate the input entity + if 'address' not in input_dict['removeFixedIp']: + msg = _("Missing 'address' argument for removeFixedIp") + raise exc.HTTPUnprocessableEntity(explanation=msg) + + context = req.environ['nova.context'] + instance = self._get_instance(context, id) + address = input_dict['removeFixedIp']['address'] + + try: + self.compute_api.remove_fixed_ip(context, instance, address) + except exceptions.FixedIpNotFoundForSpecificInstance: + LOG.exception(_("Unable to find address %r") % address) + raise exc.HTTPBadRequest() + + return webob.Response(status_int=202) diff --git a/nova/api/openstack/compute/contrib/networks.py b/nova/api/openstack/compute/contrib/networks.py new file mode 100644 index 000000000..f2381a19d --- /dev/null +++ b/nova/api/openstack/compute/contrib/networks.py @@ -0,0 +1,117 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Grid Dynamics +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from webob import exc + +from nova.api.openstack import extensions +from nova import exception +from nova import flags +from nova import log as logging +import nova.network.api + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack.compute.contrib.networks') + + +def network_dict(network): + if network: + fields = ('bridge', 'vpn_public_port', 'dhcp_start', + 'bridge_interface', 'updated_at', 'id', 'cidr_v6', + 'deleted_at', 'gateway', 'label', 'project_id', + 'vpn_private_address', 'deleted', 'vlan', 'broadcast', + 'netmask', 'injected', 'cidr', 'vpn_public_address', + 'multi_host', 'dns1', 'host', 'gateway_v6', 'netmask_v6', + 'created_at') + return dict((field, network[field]) for field in fields) + else: + return {} + + +class NetworkController(object): + + def __init__(self, network_api=None): + self.network_api = network_api or nova.network.api.API() + + def action(self, req, id, body): + _actions = { + 'disassociate': self._disassociate, + } + + for action, data in body.iteritems(): + try: + return _actions[action](req, id, body) + except KeyError: + msg = _("Network does not have %s action") % action + raise exc.HTTPBadRequest(explanation=msg) + + raise exc.HTTPBadRequest(explanation=_("Invalid request body")) + + def _disassociate(self, request, network_id, body): + context = request.environ['nova.context'] + LOG.debug(_("Disassociating network with id %s" % network_id)) + try: + self.network_api.disassociate(context, network_id) + except exception.NetworkNotFound: + raise exc.HTTPNotFound(_("Network not found")) + return exc.HTTPAccepted() + + def index(self, req): + context = req.environ['nova.context'] + networks = self.network_api.get_all(context) + result = [network_dict(net_ref) for net_ref in networks] + return {'networks': result} + + def show(self, req, id): + context = req.environ['nova.context'] + LOG.debug(_("Showing network with id %s") % id) + try: + network = self.network_api.get(context, id) + except exception.NetworkNotFound: + raise exc.HTTPNotFound(_("Network not found")) + return {'network': network_dict(network)} + + def delete(self, req, id): + context = req.environ['nova.context'] + LOG.info(_("Deleting network with id %s") % id) + try: + self.network_api.delete(context, id) + except exception.NetworkNotFound: + raise exc.HTTPNotFound(_("Network not found")) + return exc.HTTPAccepted() + + def create(self, req, id, body=None): + raise exc.HTTPNotImplemented() + + +class Networks(extensions.ExtensionDescriptor): + """Admin-only Network Management Extension""" + + name = "Networks" + alias = "os-networks" + namespace = "http://docs.openstack.org/compute/ext/networks/api/v1.1" + updated = "2011-12-23 00:00:00" + admin_only = True + + def get_resources(self): + member_actions = {'action': 'POST'} + res = extensions.ResourceExtension('os-networks', + NetworkController(), + member_actions=member_actions) + return [res] diff --git a/nova/api/openstack/compute/contrib/quotas.py b/nova/api/openstack/compute/contrib/quotas.py new file mode 100644 index 000000000..5e4a20568 --- /dev/null +++ b/nova/api/openstack/compute/contrib/quotas.py @@ -0,0 +1,102 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import db +from nova import exception +from nova import quota + + +quota_resources = ['metadata_items', 'injected_file_content_bytes', + 'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances', + 'injected_files', 'cores'] + + +class QuotaTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('quota_set', selector='quota_set') + root.set('id') + + for resource in quota_resources: + elem = xmlutil.SubTemplateElement(root, resource) + elem.text = resource + + return xmlutil.MasterTemplate(root, 1) + + +class QuotaSetsController(object): + + def _format_quota_set(self, project_id, quota_set): + """Convert the quota object to a result dict""" + + result = dict(id=str(project_id)) + + for resource in quota_resources: + result[resource] = quota_set[resource] + + return dict(quota_set=result) + + @wsgi.serializers(xml=QuotaTemplate) + def show(self, req, id): + context = req.environ['nova.context'] + try: + db.sqlalchemy.api.authorize_project_context(context, id) + return self._format_quota_set(id, + quota.get_project_quotas(context, id)) + except exception.NotAuthorized: + raise webob.exc.HTTPForbidden() + + @wsgi.serializers(xml=QuotaTemplate) + def update(self, req, id, body): + context = req.environ['nova.context'] + project_id = id + for key in body['quota_set'].keys(): + if key in quota_resources: + value = int(body['quota_set'][key]) + try: + db.quota_update(context, project_id, key, value) + except exception.ProjectQuotaNotFound: + db.quota_create(context, project_id, key, value) + except exception.AdminRequired: + raise webob.exc.HTTPForbidden() + return {'quota_set': quota.get_project_quotas(context, project_id)} + + def defaults(self, req, id): + return self._format_quota_set(id, quota._get_default_quotas()) + + +class Quotas(extensions.ExtensionDescriptor): + """Quotas management support""" + + name = "Quotas" + alias = "os-quota-sets" + namespace = "http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1" + updated = "2011-08-08T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-quota-sets', + QuotaSetsController(), + member_actions={'defaults': 'GET'}) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/rescue.py b/nova/api/openstack/compute/contrib/rescue.py new file mode 100644 index 000000000..0ea77cc6a --- /dev/null +++ b/nova/api/openstack/compute/contrib/rescue.py @@ -0,0 +1,80 @@ +# Copyright 2011 Openstack, LLC. +# +# 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. + +"""The rescue mode extension.""" + +import webob +from webob import exc + +from nova.api.openstack import extensions as exts +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils + + +FLAGS = flags.FLAGS +LOG = logging.getLogger("nova.api.openstack.compute.contrib.rescue") + + +class Rescue(exts.ExtensionDescriptor): + """Instance rescue mode""" + + name = "Rescue" + alias = "os-rescue" + namespace = "http://docs.openstack.org/compute/ext/rescue/api/v1.1" + updated = "2011-08-18T00:00:00+00:00" + + def __init__(self, ext_mgr): + super(Rescue, self).__init__(ext_mgr) + self.compute_api = compute.API() + + def _get_instance(self, context, instance_id): + try: + return self.compute_api.get(context, instance_id) + except exception.InstanceNotFound: + msg = _("Server not found") + raise exc.HTTPNotFound(msg) + + @exts.wrap_errors + def _rescue(self, input_dict, req, instance_id): + """Rescue an instance.""" + context = req.environ["nova.context"] + + if input_dict['rescue'] and 'adminPass' in input_dict['rescue']: + password = input_dict['rescue']['adminPass'] + else: + password = utils.generate_password(FLAGS.password_length) + + instance = self._get_instance(context, instance_id) + self.compute_api.rescue(context, instance, rescue_password=password) + return {'adminPass': password} + + @exts.wrap_errors + def _unrescue(self, input_dict, req, instance_id): + """Unrescue an instance.""" + context = req.environ["nova.context"] + instance = self._get_instance(context, instance_id) + self.compute_api.unrescue(context, instance) + return webob.Response(status_int=202) + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + exts.ActionExtension("servers", "rescue", self._rescue), + exts.ActionExtension("servers", "unrescue", self._unrescue), + ] + + return actions diff --git a/nova/api/openstack/compute/contrib/security_groups.py b/nova/api/openstack/compute/contrib/security_groups.py new file mode 100644 index 000000000..f0d3dfe16 --- /dev/null +++ b/nova/api/openstack/compute/contrib/security_groups.py @@ -0,0 +1,592 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The security groups extension.""" + +import urllib +from xml.dom import minidom + +from webob import exc +import webob + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.security_groups") +FLAGS = flags.FLAGS + + +def make_rule(elem): + elem.set('id') + elem.set('parent_group_id') + + proto = xmlutil.SubTemplateElement(elem, 'ip_protocol') + proto.text = 'ip_protocol' + + from_port = xmlutil.SubTemplateElement(elem, 'from_port') + from_port.text = 'from_port' + + to_port = xmlutil.SubTemplateElement(elem, 'to_port') + to_port.text = 'to_port' + + group = xmlutil.SubTemplateElement(elem, 'group', selector='group') + name = xmlutil.SubTemplateElement(group, 'name') + name.text = 'name' + tenant_id = xmlutil.SubTemplateElement(group, 'tenant_id') + tenant_id.text = 'tenant_id' + + ip_range = xmlutil.SubTemplateElement(elem, 'ip_range', + selector='ip_range') + cidr = xmlutil.SubTemplateElement(ip_range, 'cidr') + cidr.text = 'cidr' + + +def make_sg(elem): + elem.set('id') + elem.set('tenant_id') + elem.set('name') + + desc = xmlutil.SubTemplateElement(elem, 'description') + desc.text = 'description' + + rules = xmlutil.SubTemplateElement(elem, 'rules') + rule = xmlutil.SubTemplateElement(rules, 'rule', selector='rules') + make_rule(rule) + + +sg_nsmap = {None: wsgi.XMLNS_V11} + + +class SecurityGroupRuleTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('security_group_rule', + selector='security_group_rule') + make_rule(root) + return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) + + +class SecurityGroupTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('security_group', + selector='security_group') + make_sg(root) + return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) + + +class SecurityGroupsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('security_groups') + elem = xmlutil.SubTemplateElement(root, 'security_group', + selector='security_groups') + make_sg(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) + + +class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): + """ + Deserializer to handle xml-formatted security group requests. + """ + def default(self, string): + """Deserialize an xml-formatted security group create request""" + dom = minidom.parseString(string) + security_group = {} + sg_node = self.find_first_child_named(dom, + 'security_group') + if sg_node is not None: + if sg_node.hasAttribute('name'): + security_group['name'] = sg_node.getAttribute('name') + desc_node = self.find_first_child_named(sg_node, + "description") + if desc_node: + security_group['description'] = self.extract_text(desc_node) + return {'body': {'security_group': security_group}} + + +class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer): + """ + Deserializer to handle xml-formatted security group requests. + """ + + def default(self, string): + """Deserialize an xml-formatted security group create request""" + dom = minidom.parseString(string) + security_group_rule = self._extract_security_group_rule(dom) + return {'body': {'security_group_rule': security_group_rule}} + + def _extract_security_group_rule(self, node): + """Marshal the security group rule attribute of a parsed request""" + sg_rule = {} + sg_rule_node = self.find_first_child_named(node, + 'security_group_rule') + if sg_rule_node is not None: + ip_protocol_node = self.find_first_child_named(sg_rule_node, + "ip_protocol") + if ip_protocol_node is not None: + sg_rule['ip_protocol'] = self.extract_text(ip_protocol_node) + + from_port_node = self.find_first_child_named(sg_rule_node, + "from_port") + if from_port_node is not None: + sg_rule['from_port'] = self.extract_text(from_port_node) + + to_port_node = self.find_first_child_named(sg_rule_node, "to_port") + if to_port_node is not None: + sg_rule['to_port'] = self.extract_text(to_port_node) + + parent_group_id_node = self.find_first_child_named(sg_rule_node, + "parent_group_id") + if parent_group_id_node is not None: + sg_rule['parent_group_id'] = self.extract_text( + parent_group_id_node) + + group_id_node = self.find_first_child_named(sg_rule_node, + "group_id") + if group_id_node is not None: + sg_rule['group_id'] = self.extract_text(group_id_node) + + cidr_node = self.find_first_child_named(sg_rule_node, "cidr") + if cidr_node is not None: + sg_rule['cidr'] = self.extract_text(cidr_node) + + return sg_rule + + +class SecurityGroupController(object): + """The Security group API controller for the OpenStack API.""" + + def __init__(self): + self.compute_api = compute.API() + super(SecurityGroupController, self).__init__() + + def _format_security_group_rule(self, context, rule): + sg_rule = {} + sg_rule['id'] = rule.id + sg_rule['parent_group_id'] = rule.parent_group_id + sg_rule['ip_protocol'] = rule.protocol + sg_rule['from_port'] = rule.from_port + sg_rule['to_port'] = rule.to_port + sg_rule['group'] = {} + sg_rule['ip_range'] = {} + if rule.group_id: + source_group = db.security_group_get(context, rule.group_id) + sg_rule['group'] = {'name': source_group.name, + 'tenant_id': source_group.project_id} + else: + sg_rule['ip_range'] = {'cidr': rule.cidr} + return sg_rule + + def _format_security_group(self, context, group): + security_group = {} + security_group['id'] = group.id + security_group['description'] = group.description + security_group['name'] = group.name + security_group['tenant_id'] = group.project_id + security_group['rules'] = [] + for rule in group.rules: + security_group['rules'] += [self._format_security_group_rule( + context, rule)] + return security_group + + def _get_security_group(self, context, id): + try: + id = int(id) + security_group = db.security_group_get(context, id) + except ValueError: + msg = _("Security group id should be integer") + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + return security_group + + @wsgi.serializers(xml=SecurityGroupTemplate) + def show(self, req, id): + """Return data about the given security group.""" + context = req.environ['nova.context'] + security_group = self._get_security_group(context, id) + return {'security_group': self._format_security_group(context, + security_group)} + + def delete(self, req, id): + """Delete a security group.""" + context = req.environ['nova.context'] + security_group = self._get_security_group(context, id) + LOG.audit(_("Delete security group %s"), id, context=context) + db.security_group_destroy(context, security_group.id) + + return webob.Response(status_int=202) + + @wsgi.serializers(xml=SecurityGroupsTemplate) + def index(self, req): + """Returns a list of security groups""" + context = req.environ['nova.context'] + + self.compute_api.ensure_default_security_group(context) + groups = db.security_group_get_by_project(context, + context.project_id) + limited_list = common.limited(groups, req) + result = [self._format_security_group(context, group) + for group in limited_list] + + return {'security_groups': + list(sorted(result, + key=lambda k: (k['tenant_id'], k['name'])))} + + @wsgi.serializers(xml=SecurityGroupTemplate) + @wsgi.deserializers(xml=SecurityGroupXMLDeserializer) + def create(self, req, body): + """Creates a new security group.""" + context = req.environ['nova.context'] + if not body: + raise exc.HTTPUnprocessableEntity() + + security_group = body.get('security_group', None) + + if security_group is None: + raise exc.HTTPUnprocessableEntity() + + group_name = security_group.get('name', None) + group_description = security_group.get('description', None) + + self._validate_security_group_property(group_name, "name") + self._validate_security_group_property(group_description, + "description") + group_name = group_name.strip() + group_description = group_description.strip() + + LOG.audit(_("Create Security Group %s"), group_name, context=context) + self.compute_api.ensure_default_security_group(context) + if db.security_group_exists(context, context.project_id, group_name): + msg = _('Security group %s already exists') % group_name + raise exc.HTTPBadRequest(explanation=msg) + + group = {'user_id': context.user_id, + 'project_id': context.project_id, + 'name': group_name, + 'description': group_description} + group_ref = db.security_group_create(context, group) + + return {'security_group': self._format_security_group(context, + group_ref)} + + def _validate_security_group_property(self, value, typ): + """ typ will be either 'name' or 'description', + depending on the caller + """ + try: + val = value.strip() + except AttributeError: + msg = _("Security group %s is not a string or unicode") % typ + raise exc.HTTPBadRequest(explanation=msg) + if not val: + msg = _("Security group %s cannot be empty.") % typ + raise exc.HTTPBadRequest(explanation=msg) + if len(val) > 255: + msg = _("Security group %s should not be greater " + "than 255 characters.") % typ + raise exc.HTTPBadRequest(explanation=msg) + + +class SecurityGroupRulesController(SecurityGroupController): + + @wsgi.serializers(xml=SecurityGroupRuleTemplate) + @wsgi.deserializers(xml=SecurityGroupRulesXMLDeserializer) + def create(self, req, body): + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + if not 'security_group_rule' in body: + raise exc.HTTPUnprocessableEntity() + + self.compute_api.ensure_default_security_group(context) + + sg_rule = body['security_group_rule'] + parent_group_id = sg_rule.get('parent_group_id', None) + try: + parent_group_id = int(parent_group_id) + security_group = db.security_group_get(context, parent_group_id) + except ValueError: + msg = _("Parent group id is not integer") + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as exp: + msg = _("Security group (%s) not found") % parent_group_id + raise exc.HTTPNotFound(explanation=msg) + + msg = _("Authorize security group ingress %s") + LOG.audit(msg, security_group['name'], context=context) + + try: + values = self._rule_args_to_dict(context, + to_port=sg_rule.get('to_port'), + from_port=sg_rule.get('from_port'), + parent_group_id=sg_rule.get('parent_group_id'), + ip_protocol=sg_rule.get('ip_protocol'), + cidr=sg_rule.get('cidr'), + group_id=sg_rule.get('group_id')) + except Exception as exp: + raise exc.HTTPBadRequest(explanation=unicode(exp)) + + if values is None: + msg = _("Not enough parameters to build a " + "valid rule.") + raise exc.HTTPBadRequest(explanation=msg) + + values['parent_group_id'] = security_group.id + + if self._security_group_rule_exists(security_group, values): + msg = _('This rule already exists in group %s') % parent_group_id + raise exc.HTTPBadRequest(explanation=msg) + + security_group_rule = db.security_group_rule_create(context, values) + + self.compute_api.trigger_security_group_rules_refresh(context, + security_group_id=security_group['id']) + + return {"security_group_rule": self._format_security_group_rule( + context, + security_group_rule)} + + def _security_group_rule_exists(self, security_group, values): + """Indicates whether the specified rule values are already + defined in the given security group. + """ + for rule in security_group.rules: + if 'group_id' in values: + if rule['group_id'] == values['group_id']: + return True + else: + is_duplicate = True + for key in ('cidr', 'from_port', 'to_port', 'protocol'): + if rule[key] != values[key]: + is_duplicate = False + break + if is_duplicate: + return True + return False + + def _rule_args_to_dict(self, context, to_port=None, from_port=None, + parent_group_id=None, ip_protocol=None, + cidr=None, group_id=None): + values = {} + + if group_id is not None: + try: + parent_group_id = int(parent_group_id) + group_id = int(group_id) + except ValueError: + msg = _("Parent or group id is not integer") + raise exception.InvalidInput(reason=msg) + + if parent_group_id == group_id: + msg = _("Parent group id and group id cannot be same") + raise exception.InvalidInput(reason=msg) + + values['group_id'] = group_id + #check if groupId exists + db.security_group_get(context, group_id) + elif cidr: + # If this fails, it throws an exception. This is what we want. + try: + cidr = urllib.unquote(cidr).decode() + except Exception: + raise exception.InvalidCidr(cidr=cidr) + + if not utils.is_valid_cidr(cidr): + # Raise exception for non-valid address + raise exception.InvalidCidr(cidr=cidr) + + values['cidr'] = cidr + else: + values['cidr'] = '0.0.0.0/0' + + if ip_protocol and from_port and to_port: + + ip_protocol = str(ip_protocol) + try: + from_port = int(from_port) + to_port = int(to_port) + except ValueError: + if ip_protocol.upper() == 'ICMP': + raise exception.InvalidInput(reason="Type and" + " Code must be integers for ICMP protocol type") + else: + raise exception.InvalidInput(reason="To and From ports " + "must be integers") + + if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: + raise exception.InvalidIpProtocol(protocol=ip_protocol) + + # Verify that from_port must always be less than + # or equal to to_port + if from_port > to_port: + raise exception.InvalidPortRange(from_port=from_port, + to_port=to_port, msg="Former value cannot" + " be greater than the later") + + # Verify valid TCP, UDP port ranges + if (ip_protocol.upper() in ['TCP', 'UDP'] and + (from_port < 1 or to_port > 65535)): + raise exception.InvalidPortRange(from_port=from_port, + to_port=to_port, msg="Valid TCP ports should" + " be between 1-65535") + + # Verify ICMP type and code + if (ip_protocol.upper() == "ICMP" and + (from_port < -1 or to_port > 255)): + raise exception.InvalidPortRange(from_port=from_port, + to_port=to_port, msg="For ICMP, the" + " type:code must be valid") + + values['protocol'] = ip_protocol + values['from_port'] = from_port + values['to_port'] = to_port + else: + # If cidr based filtering, protocol and ports are mandatory + if 'cidr' in values: + return None + + return values + + def delete(self, req, id): + context = req.environ['nova.context'] + + self.compute_api.ensure_default_security_group(context) + try: + id = int(id) + rule = db.security_group_rule_get(context, id) + except ValueError: + msg = _("Rule id is not integer") + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as exp: + msg = _("Rule (%s) not found") % id + raise exc.HTTPNotFound(explanation=msg) + + group_id = rule.parent_group_id + self.compute_api.ensure_default_security_group(context) + security_group = db.security_group_get(context, group_id) + + msg = _("Revoke security group ingress %s") + LOG.audit(msg, security_group['name'], context=context) + + db.security_group_rule_destroy(context, rule['id']) + self.compute_api.trigger_security_group_rules_refresh(context, + security_group_id=security_group['id']) + + return webob.Response(status_int=202) + + +class Security_groups(extensions.ExtensionDescriptor): + """Security group support""" + + name = "SecurityGroups" + alias = "security_groups" + namespace = "http://docs.openstack.org/compute/ext/securitygroups/api/v1.1" + updated = "2011-07-21T00:00:00+00:00" + + def __init__(self, ext_mgr): + self.compute_api = compute.API() + super(Security_groups, self).__init__(ext_mgr) + + def _addSecurityGroup(self, input_dict, req, instance_id): + context = req.environ['nova.context'] + + try: + body = input_dict['addSecurityGroup'] + group_name = body['name'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Security group not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + if not group_name or group_name.strip() == '': + msg = _("Security group name cannot be empty") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + instance = self.compute_api.get(context, instance_id) + self.compute_api.add_security_group(context, instance, group_name) + except exception.SecurityGroupNotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + except exception.InstanceNotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + except exception.Invalid as exp: + raise exc.HTTPBadRequest(explanation=unicode(exp)) + + return webob.Response(status_int=202) + + def _removeSecurityGroup(self, input_dict, req, instance_id): + context = req.environ['nova.context'] + + try: + body = input_dict['removeSecurityGroup'] + group_name = body['name'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Security group not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + if not group_name or group_name.strip() == '': + msg = _("Security group name cannot be empty") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + instance = self.compute_api.get(context, instance_id) + self.compute_api.remove_security_group(context, instance, + group_name) + except exception.SecurityGroupNotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + except exception.InstanceNotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + except exception.Invalid as exp: + raise exc.HTTPBadRequest(explanation=unicode(exp)) + + return webob.Response(status_int=202) + + def get_actions(self): + """Return the actions the extensions adds""" + actions = [ + extensions.ActionExtension("servers", "addSecurityGroup", + self._addSecurityGroup), + extensions.ActionExtension("servers", "removeSecurityGroup", + self._removeSecurityGroup) + ] + return actions + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-security-groups', + controller=SecurityGroupController()) + + resources.append(res) + + res = extensions.ResourceExtension('os-security-group-rules', + controller=SecurityGroupRulesController()) + resources.append(res) + return resources diff --git a/nova/api/openstack/compute/contrib/server_action_list.py b/nova/api/openstack/compute/contrib/server_action_list.py new file mode 100644 index 000000000..436758572 --- /dev/null +++ b/nova/api/openstack/compute/contrib/server_action_list.py @@ -0,0 +1,77 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob.exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception + + +sa_nsmap = {None: wsgi.XMLNS_V11} + + +class ServerActionsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('actions') + elem = xmlutil.SubTemplateElement(root, 'action', selector='actions') + elem.set('created_at') + elem.set('action') + elem.set('error') + return xmlutil.MasterTemplate(root, 1, nsmap=sa_nsmap) + + +class ServerActionListController(object): + @wsgi.serializers(xml=ServerActionsTemplate) + def index(self, req, server_id): + context = req.environ["nova.context"] + compute_api = compute.API() + + try: + instance = compute_api.get(context, server_id) + except exception.NotFound: + raise webob.exc.HTTPNotFound(_("Instance not found")) + + items = compute_api.get_actions(context, instance) + + def _format_item(item): + return { + 'created_at': str(item['created_at']), + 'action': item['action'], + 'error': item['error'], + } + + return {'actions': [_format_item(item) for item in items]} + + +class Server_action_list(extensions.ExtensionDescriptor): + """Allow Admins to view pending server actions""" + + name = "ServerActionList" + alias = "os-server-action-list" + namespace = "http://docs.openstack.org/compute/ext/" \ + "server-actions-list/api/v1.1" + updated = "2011-12-21T00:00:00+00:00" + admin_only = True + + def get_resources(self): + parent_def = {'member_name': 'server', 'collection_name': 'servers'} + #NOTE(bcwaldon): This should be prefixed with 'os-' + ext = extensions.ResourceExtension('actions', + ServerActionListController(), + parent=parent_def) + return [ext] diff --git a/nova/api/openstack/compute/contrib/server_diagnostics.py b/nova/api/openstack/compute/contrib/server_diagnostics.py new file mode 100644 index 000000000..11d1affaf --- /dev/null +++ b/nova/api/openstack/compute/contrib/server_diagnostics.py @@ -0,0 +1,69 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob.exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova.scheduler import api as scheduler_api + + +sd_nsmap = {None: wsgi.XMLNS_V11} + + +class ServerDiagnosticsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('diagnostics') + elem = xmlutil.SubTemplateElement(root, xmlutil.Selector(0), + selector=xmlutil.get_items) + elem.text = 1 + return xmlutil.MasterTemplate(root, 1, nsmap=sd_nsmap) + + +class ServerDiagnosticsController(object): + @wsgi.serializers(xml=ServerDiagnosticsTemplate) + @exception.novaclient_converter + @scheduler_api.redirect_handler + def index(self, req, server_id): + context = req.environ["nova.context"] + compute_api = compute.API() + try: + instance = compute_api.get(context, id) + except exception.NotFound(): + raise webob.exc.HTTPNotFound(_("Instance not found")) + + return compute_api.get_diagnostics(context, instance) + + +class Server_diagnostics(extensions.ExtensionDescriptor): + """Allow Admins to view server diagnostics through server action""" + + name = "ServerDiagnostics" + alias = "os-server-diagnostics" + namespace = "http://docs.openstack.org/compute/ext/" \ + "server-diagnostics/api/v1.1" + updated = "2011-12-21T00:00:00+00:00" + admin_only = True + + def get_resources(self): + parent_def = {'member_name': 'server', 'collection_name': 'servers'} + #NOTE(bcwaldon): This should be prefixed with 'os-' + ext = extensions.ResourceExtension('diagnostics', + ServerDiagnosticsController(), + parent=parent_def) + return [ext] diff --git a/nova/api/openstack/compute/contrib/simple_tenant_usage.py b/nova/api/openstack/compute/contrib/simple_tenant_usage.py new file mode 100644 index 000000000..f34581f6c --- /dev/null +++ b/nova/api/openstack/compute/contrib/simple_tenant_usage.py @@ -0,0 +1,265 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from datetime import datetime +import urlparse + +import webob + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.compute import api +from nova import exception +from nova import flags + + +FLAGS = flags.FLAGS + + +def make_usage(elem): + for subelem_tag in ('tenant_id', 'total_local_gb_usage', + 'total_vcpus_usage', 'total_memory_mb_usage', + 'total_hours', 'start', 'stop'): + subelem = xmlutil.SubTemplateElement(elem, subelem_tag) + subelem.text = subelem_tag + + server_usages = xmlutil.SubTemplateElement(elem, 'server_usages') + server_usage = xmlutil.SubTemplateElement(server_usages, 'server_usage', + selector='server_usages') + for subelem_tag in ('name', 'hours', 'memory_mb', 'local_gb', 'vcpus', + 'tenant_id', 'flavor', 'started_at', 'ended_at', + 'state', 'uptime'): + subelem = xmlutil.SubTemplateElement(server_usage, subelem_tag) + subelem.text = subelem_tag + + +class SimpleTenantUsageTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('tenant_usage', selector='tenant_usage') + make_usage(root) + return xmlutil.MasterTemplate(root, 1) + + +class SimpleTenantUsagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('tenant_usages') + elem = xmlutil.SubTemplateElement(root, 'tenant_usage', + selector='tenant_usages') + make_usage(elem) + return xmlutil.MasterTemplate(root, 1) + + +class SimpleTenantUsageController(object): + def _hours_for(self, instance, period_start, period_stop): + launched_at = instance['launched_at'] + terminated_at = instance['terminated_at'] + if terminated_at is not None: + if not isinstance(terminated_at, datetime): + terminated_at = datetime.strptime(terminated_at, + "%Y-%m-%d %H:%M:%S.%f") + + if launched_at is not None: + if not isinstance(launched_at, datetime): + launched_at = datetime.strptime(launched_at, + "%Y-%m-%d %H:%M:%S.%f") + + if terminated_at and terminated_at < period_start: + return 0 + # nothing if it started after the usage report ended + if launched_at and launched_at > period_stop: + return 0 + if launched_at: + # if instance launched after period_started, don't charge for first + start = max(launched_at, period_start) + if terminated_at: + # if instance stopped before period_stop, don't charge after + stop = min(period_stop, terminated_at) + else: + # instance is still running, so charge them up to current time + stop = period_stop + dt = stop - start + seconds = dt.days * 3600 * 24 + dt.seconds\ + + dt.microseconds / 100000.0 + + return seconds / 3600.0 + else: + # instance hasn't launched, so no charge + return 0 + + def _tenant_usages_for_period(self, context, period_start, + period_stop, tenant_id=None, detailed=True): + + compute_api = api.API() + instances = compute_api.get_active_by_window(context, + period_start, + period_stop, + tenant_id) + from nova import log as logging + logging.info(instances) + rval = {} + flavors = {} + + for instance in instances: + info = {} + info['hours'] = self._hours_for(instance, + period_start, + period_stop) + flavor_type = instance['instance_type_id'] + + if not flavors.get(flavor_type): + try: + it_ref = compute_api.get_instance_type(context, + flavor_type) + flavors[flavor_type] = it_ref + except exception.InstanceTypeNotFound: + # can't bill if there is no instance type + continue + + flavor = flavors[flavor_type] + + info['name'] = instance['display_name'] + + info['memory_mb'] = flavor['memory_mb'] + info['local_gb'] = flavor['local_gb'] + info['vcpus'] = flavor['vcpus'] + + info['tenant_id'] = instance['project_id'] + + info['flavor'] = flavor['name'] + + info['started_at'] = instance['launched_at'] + + info['ended_at'] = instance['terminated_at'] + + if info['ended_at']: + info['state'] = 'terminated' + else: + info['state'] = instance['vm_state'] + + now = datetime.utcnow() + + if info['state'] == 'terminated': + delta = info['ended_at'] - info['started_at'] + else: + delta = now - info['started_at'] + + info['uptime'] = delta.days * 24 * 60 + delta.seconds + + if not info['tenant_id'] in rval: + summary = {} + summary['tenant_id'] = info['tenant_id'] + if detailed: + summary['server_usages'] = [] + summary['total_local_gb_usage'] = 0 + summary['total_vcpus_usage'] = 0 + summary['total_memory_mb_usage'] = 0 + summary['total_hours'] = 0 + summary['start'] = period_start + summary['stop'] = period_stop + rval[info['tenant_id']] = summary + + summary = rval[info['tenant_id']] + summary['total_local_gb_usage'] += info['local_gb'] * info['hours'] + summary['total_vcpus_usage'] += info['vcpus'] * info['hours'] + summary['total_memory_mb_usage'] += info['memory_mb']\ + * info['hours'] + + summary['total_hours'] += info['hours'] + if detailed: + summary['server_usages'].append(info) + + return rval.values() + + def _parse_datetime(self, dtstr): + if isinstance(dtstr, datetime): + return dtstr + try: + return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S") + except Exception: + try: + return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S.%f") + except Exception: + return datetime.strptime(dtstr, "%Y-%m-%d %H:%M:%S.%f") + + def _get_datetime_range(self, req): + qs = req.environ.get('QUERY_STRING', '') + env = urlparse.parse_qs(qs) + period_start = self._parse_datetime(env.get('start', + [datetime.utcnow().isoformat()])[0]) + period_stop = self._parse_datetime(env.get('end', + [datetime.utcnow().isoformat()])[0]) + + detailed = bool(env.get('detailed', False)) + return (period_start, period_stop, detailed) + + @wsgi.serializers(xml=SimpleTenantUsagesTemplate) + def index(self, req): + """Retrive tenant_usage for all tenants""" + context = req.environ['nova.context'] + + if not context.is_admin: + return webob.Response(status_int=403) + + (period_start, period_stop, detailed) = self._get_datetime_range(req) + usages = self._tenant_usages_for_period(context, + period_start, + period_stop, + detailed=detailed) + return {'tenant_usages': usages} + + @wsgi.serializers(xml=SimpleTenantUsageTemplate) + def show(self, req, id): + """Retrive tenant_usage for a specified tenant""" + tenant_id = id + context = req.environ['nova.context'] + + if not context.is_admin: + if tenant_id != context.project_id: + return webob.Response(status_int=403) + + (period_start, period_stop, ignore) = self._get_datetime_range(req) + usage = self._tenant_usages_for_period(context, + period_start, + period_stop, + tenant_id=tenant_id, + detailed=True) + if len(usage): + usage = usage[0] + else: + usage = {} + return {'tenant_usage': usage} + + +class Simple_tenant_usage(extensions.ExtensionDescriptor): + """Simple tenant usage extension""" + + name = "SimpleTenantUsage" + alias = "os-simple-tenant-usage" + namespace = "http://docs.openstack.org/compute/ext/" \ + "os-simple-tenant-usage/api/v1.1" + updated = "2011-08-19T00:00:00+00:00" + admin_only = True + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-simple-tenant-usage', + SimpleTenantUsageController()) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/users.py b/nova/api/openstack/compute/contrib/users.py new file mode 100644 index 000000000..55dba02e4 --- /dev/null +++ b/nova/api/openstack/compute/contrib/users.py @@ -0,0 +1,145 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.auth import manager +from nova import exception +from nova import flags +from nova import log as logging + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack.users') + + +def make_user(elem): + elem.set('id') + elem.set('name') + elem.set('access') + elem.set('secret') + elem.set('admin') + + +class UserTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('user', selector='user') + make_user(root) + return xmlutil.MasterTemplate(root, 1) + + +class UsersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('users') + elem = xmlutil.SubTemplateElement(root, 'user', selector='users') + make_user(elem) + return xmlutil.MasterTemplate(root, 1) + + +def _translate_keys(user): + return dict(id=user.id, + name=user.name, + access=user.access, + secret=user.secret, + admin=user.admin) + + +class Controller(object): + + def __init__(self): + self.manager = manager.AuthManager() + + def _check_admin(self, context): + """We cannot depend on the db layer to check for admin access + for the auth manager, so we do it here""" + if not context.is_admin: + raise exception.AdminRequired() + + @wsgi.serializers(xml=UsersTemplate) + def index(self, req): + """Return all users in brief""" + users = self.manager.get_users() + users = common.limited(users, req) + users = [_translate_keys(user) for user in users] + return dict(users=users) + + @wsgi.serializers(xml=UsersTemplate) + def detail(self, req): + """Return all users in detail""" + return self.index(req) + + @wsgi.serializers(xml=UserTemplate) + def show(self, req, id): + """Return data about the given user id""" + + #NOTE(justinsb): The drivers are a little inconsistent in how they + # deal with "NotFound" - some throw, some return None. + try: + user = self.manager.get_user(id) + except exception.NotFound: + user = None + + if user is None: + raise exc.HTTPNotFound() + + return dict(user=_translate_keys(user)) + + def delete(self, req, id): + self._check_admin(req.environ['nova.context']) + self.manager.delete_user(id) + return {} + + @wsgi.serializers(xml=UserTemplate) + def create(self, req, body): + self._check_admin(req.environ['nova.context']) + is_admin = body['user'].get('admin') in ('T', 'True', True) + name = body['user'].get('name') + access = body['user'].get('access') + secret = body['user'].get('secret') + user = self.manager.create_user(name, access, secret, is_admin) + return dict(user=_translate_keys(user)) + + @wsgi.serializers(xml=UserTemplate) + def update(self, req, id, body): + self._check_admin(req.environ['nova.context']) + is_admin = body['user'].get('admin') + if is_admin is not None: + is_admin = is_admin in ('T', 'True', True) + access = body['user'].get('access') + secret = body['user'].get('secret') + self.manager.modify_user(id, access, secret, is_admin) + return dict(user=_translate_keys(self.manager.get_user(id))) + + +class Users(extensions.ExtensionDescriptor): + """Allow admins to acces user information""" + + name = "Users" + alias = "os-users" + namespace = "http://docs.openstack.org/compute/ext/users/api/v1.1" + updated = "2011-08-08T00:00:00+00:00" + admin_only = True + + def get_resources(self): + coll_actions = {'detail': 'GET'} + res = extensions.ResourceExtension('users', + Controller(), + collection_actions=coll_actions) + + return [res] diff --git a/nova/api/openstack/compute/contrib/virtual_interfaces.py b/nova/api/openstack/compute/contrib/virtual_interfaces.py new file mode 100644 index 000000000..ea37c4d97 --- /dev/null +++ b/nova/api/openstack/compute/contrib/virtual_interfaces.py @@ -0,0 +1,93 @@ +# 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. + +"""The virtual interfaces extension.""" + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import log as logging +from nova import network + + +LOG = logging.getLogger("nova.api.openstack.compute." + "contrib.virtual_interfaces") + + +vif_nsmap = {None: wsgi.XMLNS_V11} + + +class VirtualInterfaceTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('virtual_interfaces') + elem = xmlutil.SubTemplateElement(root, 'virtual_interface', + selector='virtual_interfaces') + elem.set('id') + elem.set('mac_address') + return xmlutil.MasterTemplate(root, 1, nsmap=vif_nsmap) + + +def _translate_vif_summary_view(_context, vif): + """Maps keys for VIF summary view.""" + d = {} + d['id'] = vif['uuid'] + d['mac_address'] = vif['address'] + return d + + +class ServerVirtualInterfaceController(object): + """The instance VIF API controller for the Openstack API. + """ + + def __init__(self): + self.network_api = network.API() + super(ServerVirtualInterfaceController, self).__init__() + + def _items(self, req, server_id, entity_maker): + """Returns a list of VIFs, transformed through entity_maker.""" + context = req.environ['nova.context'] + + vifs = self.network_api.get_vifs_by_instance(context, server_id) + limited_list = common.limited(vifs, req) + res = [entity_maker(context, vif) for vif in limited_list] + return {'virtual_interfaces': res} + + @wsgi.serializers(xml=VirtualInterfaceTemplate) + def index(self, req, server_id): + """Returns the list of VIFs for a given instance.""" + return self._items(req, server_id, + entity_maker=_translate_vif_summary_view) + + +class Virtual_interfaces(extensions.ExtensionDescriptor): + """Virtual interface support""" + + name = "VirtualInterfaces" + alias = "virtual_interfaces" + namespace = "http://docs.openstack.org/compute/ext/" \ + "virtual_interfaces/api/v1.1" + updated = "2011-08-17T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-virtual-interfaces', + controller=ServerVirtualInterfaceController(), + parent=dict(member_name='server', collection_name='servers')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/virtual_storage_arrays.py b/nova/api/openstack/compute/contrib/virtual_storage_arrays.py new file mode 100644 index 000000000..39edd155b --- /dev/null +++ b/nova/api/openstack/compute/contrib/virtual_storage_arrays.py @@ -0,0 +1,687 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# +# 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. + +""" The virtul storage array extension""" + + +import webob +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack.compute.contrib import volumes +from nova.api.openstack import extensions +from nova.api.openstack.compute import servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova.compute import instance_types +from nova import network +from nova import db +from nova import quota +from nova import exception +from nova import flags +from nova import log as logging +from nova import vsa +from nova import volume + +FLAGS = flags.FLAGS + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.vsa") + + +def _vsa_view(context, vsa, details=False, instances=None): + """Map keys for vsa summary/detailed view.""" + d = {} + + d['id'] = vsa.get('id') + d['name'] = vsa.get('name') + d['displayName'] = vsa.get('display_name') + d['displayDescription'] = vsa.get('display_description') + + d['createTime'] = vsa.get('created_at') + d['status'] = vsa.get('status') + + if 'vsa_instance_type' in vsa: + d['vcType'] = vsa['vsa_instance_type'].get('name', None) + else: + d['vcType'] = vsa['instance_type_id'] + + d['vcCount'] = vsa.get('vc_count') + d['driveCount'] = vsa.get('vol_count') + + d['ipAddress'] = None + for instance in instances: + fixed_addr = None + floating_addr = None + if instance['fixed_ips']: + fixed = instance['fixed_ips'][0] + fixed_addr = fixed['address'] + if fixed['floating_ips']: + floating_addr = fixed['floating_ips'][0]['address'] + + if floating_addr: + d['ipAddress'] = floating_addr + break + else: + d['ipAddress'] = d['ipAddress'] or fixed_addr + + return d + + +def make_vsa(elem): + elem.set('id') + elem.set('name') + elem.set('displayName') + elem.set('displayDescription') + elem.set('createTime') + elem.set('status') + elem.set('vcType') + elem.set('vcCount') + elem.set('driveCount') + elem.set('ipAddress') + + +class VsaTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('vsa', selector='vsa') + make_vsa(root) + return xmlutil.MasterTemplate(root, 1) + + +class VsaSetTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('vsaSet') + elem = xmlutil.SubTemplateElement(root, 'vsa', selector='vsaSet') + make_vsa(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VsaController(object): + """The Virtual Storage Array API controller for the OpenStack API.""" + + def __init__(self): + self.vsa_api = vsa.API() + self.compute_api = compute.API() + self.network_api = network.API() + super(VsaController, self).__init__() + + def _get_instances_by_vsa_id(self, context, id): + return self.compute_api.get_all(context, + search_opts={'metadata': dict(vsa_id=str(id))}) + + def _items(self, req, details): + """Return summary or detailed list of VSAs.""" + context = req.environ['nova.context'] + vsas = self.vsa_api.get_all(context) + limited_list = common.limited(vsas, req) + + vsa_list = [] + for vsa in limited_list: + instances = self._get_instances_by_vsa_id(context, vsa.get('id')) + vsa_list.append(_vsa_view(context, vsa, details, instances)) + return {'vsaSet': vsa_list} + + @wsgi.serializers(xml=VsaSetTemplate) + def index(self, req): + """Return a short list of VSAs.""" + return self._items(req, details=False) + + @wsgi.serializers(xml=VsaSetTemplate) + def detail(self, req): + """Return a detailed list of VSAs.""" + return self._items(req, details=True) + + @wsgi.serializers(xml=VsaTemplate) + def show(self, req, id): + """Return data about the given VSA.""" + context = req.environ['nova.context'] + + try: + vsa = self.vsa_api.get(context, vsa_id=id) + except exception.NotFound: + raise exc.HTTPNotFound() + + instances = self._get_instances_by_vsa_id(context, vsa.get('id')) + return {'vsa': _vsa_view(context, vsa, True, instances)} + + @wsgi.serializers(xml=VsaTemplate) + def create(self, req, body): + """Create a new VSA.""" + context = req.environ['nova.context'] + + if not body or 'vsa' not in body: + LOG.debug(_("No body provided"), context=context) + raise exc.HTTPUnprocessableEntity() + + vsa = body['vsa'] + + display_name = vsa.get('displayName') + vc_type = vsa.get('vcType', FLAGS.default_vsa_instance_type) + try: + instance_type = instance_types.get_instance_type_by_name(vc_type) + except exception.NotFound: + raise exc.HTTPNotFound() + + LOG.audit(_("Create VSA %(display_name)s of type %(vc_type)s"), + locals(), context=context) + + args = dict(display_name=display_name, + display_description=vsa.get('displayDescription'), + instance_type=instance_type, + storage=vsa.get('storage'), + shared=vsa.get('shared'), + availability_zone=vsa.get('placement', {}).\ + get('AvailabilityZone')) + + vsa = self.vsa_api.create(context, **args) + + instances = self._get_instances_by_vsa_id(context, vsa.get('id')) + return {'vsa': _vsa_view(context, vsa, True, instances)} + + def delete(self, req, id): + """Delete a VSA.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete VSA with id: %s"), id, context=context) + + try: + self.vsa_api.delete(context, vsa_id=id) + except exception.NotFound: + raise exc.HTTPNotFound() + + def associate_address(self, req, id, body): + """ /zadr-vsa/{vsa_id}/associate_address + auto or manually associate an IP to VSA + """ + context = req.environ['nova.context'] + + if body is None: + ip = 'auto' + else: + ip = body.get('ipAddress', 'auto') + + LOG.audit(_("Associate address %(ip)s to VSA %(id)s"), + locals(), context=context) + + try: + instances = self._get_instances_by_vsa_id(context, id) + if instances is None or len(instances) == 0: + raise exc.HTTPNotFound() + + for instance in instances: + self.network_api.allocate_for_instance(context, instance, + vpn=False) + # Placeholder + return + + except exception.NotFound: + raise exc.HTTPNotFound() + + def disassociate_address(self, req, id, body): + """ /zadr-vsa/{vsa_id}/disassociate_address + auto or manually associate an IP to VSA + """ + context = req.environ['nova.context'] + + if body is None: + ip = 'auto' + else: + ip = body.get('ipAddress', 'auto') + + LOG.audit(_("Disassociate address from VSA %(id)s"), + locals(), context=context) + # Placeholder + + +def make_volume(elem): + volumes.make_volume(elem) + elem.set('name') + elem.set('vsaId') + + +class VsaVolumeDriveController(volumes.VolumeController): + """The base class for VSA volumes & drives. + + A child resource of the VSA object. Allows operations with + volumes and drives created to/from particular VSA + + """ + + def __init__(self): + self.volume_api = volume.API() + self.vsa_api = vsa.API() + super(VsaVolumeDriveController, self).__init__() + + def _translation(self, context, vol, vsa_id, details): + if details: + translation = volumes._translate_volume_detail_view + else: + translation = volumes._translate_volume_summary_view + + d = translation(context, vol) + d['vsaId'] = vsa_id + d['name'] = vol['name'] + return d + + def _check_volume_ownership(self, context, vsa_id, id): + obj = self.object + try: + volume_ref = self.volume_api.get(context, volume_id=id) + except exception.NotFound: + LOG.error(_("%(obj)s with ID %(id)s not found"), locals()) + raise + + own_vsa_id = self.volume_api.get_volume_metadata_value(volume_ref, + self.direction) + if own_vsa_id != vsa_id: + LOG.error(_("%(obj)s with ID %(id)s belongs to VSA %(own_vsa_id)s"\ + " and not to VSA %(vsa_id)s."), locals()) + raise exception.Invalid() + + def _items(self, req, vsa_id, details): + """Return summary or detailed list of volumes for particular VSA.""" + context = req.environ['nova.context'] + + vols = self.volume_api.get_all(context, + search_opts={'metadata': {self.direction: str(vsa_id)}}) + limited_list = common.limited(vols, req) + + res = [self._translation(context, vol, vsa_id, details) \ + for vol in limited_list] + + return {self.objects: res} + + def index(self, req, vsa_id): + """Return a short list of volumes created from particular VSA.""" + LOG.audit(_("Index. vsa_id=%(vsa_id)s"), locals()) + return self._items(req, vsa_id, details=False) + + def detail(self, req, vsa_id): + """Return a detailed list of volumes created from particular VSA.""" + LOG.audit(_("Detail. vsa_id=%(vsa_id)s"), locals()) + return self._items(req, vsa_id, details=True) + + def create(self, req, vsa_id, body): + """Create a new volume from VSA.""" + LOG.audit(_("Create. vsa_id=%(vsa_id)s, body=%(body)s"), locals()) + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + vol = body[self.object] + size = vol['size'] + LOG.audit(_("Create volume of %(size)s GB from VSA ID %(vsa_id)s"), + locals(), context=context) + try: + # create is supported for volumes only (drives created through VSA) + volume_type = self.vsa_api.get_vsa_volume_type(context) + except exception.NotFound: + raise exc.HTTPNotFound() + + new_volume = self.volume_api.create(context, + size, + None, + vol.get('displayName'), + vol.get('displayDescription'), + volume_type=volume_type, + metadata=dict(from_vsa_id=str(vsa_id))) + + return {self.object: self._translation(context, new_volume, + vsa_id, True)} + + def update(self, req, vsa_id, id, body): + """Update a volume.""" + context = req.environ['nova.context'] + + try: + self._check_volume_ownership(context, vsa_id, id) + except exception.NotFound: + raise exc.HTTPNotFound() + except exception.Invalid: + raise exc.HTTPBadRequest() + + vol = body[self.object] + updatable_fields = [{'displayName': 'display_name'}, + {'displayDescription': 'display_description'}, + {'status': 'status'}, + {'providerLocation': 'provider_location'}, + {'providerAuth': 'provider_auth'}] + changes = {} + for field in updatable_fields: + key = field.keys()[0] + val = field[key] + if key in vol: + changes[val] = vol[key] + + obj = self.object + LOG.audit(_("Update %(obj)s with id: %(id)s, changes: %(changes)s"), + locals(), context=context) + + try: + self.volume_api.update(context, volume_id=id, fields=changes) + except exception.NotFound: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + + def delete(self, req, vsa_id, id): + """Delete a volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete. vsa_id=%(vsa_id)s, id=%(id)s"), locals()) + + try: + self._check_volume_ownership(context, vsa_id, id) + except exception.NotFound: + raise exc.HTTPNotFound() + except exception.Invalid: + raise exc.HTTPBadRequest() + + return super(VsaVolumeDriveController, self).delete(req, id) + + def show(self, req, vsa_id, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Show. vsa_id=%(vsa_id)s, id=%(id)s"), locals()) + + try: + self._check_volume_ownership(context, vsa_id, id) + except exception.NotFound: + raise exc.HTTPNotFound() + except exception.Invalid: + raise exc.HTTPBadRequest() + + return super(VsaVolumeDriveController, self).show(req, id) + + +class VsaVolumeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume', selector='volume') + make_volume(root) + return xmlutil.MasterTemplate(root, 1) + + +class VsaVolumesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumes') + elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') + make_volume(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VsaVolumeController(VsaVolumeDriveController): + """The VSA volume API controller for the Openstack API. + + A child resource of the VSA object. Allows operations with volumes created + by particular VSA + + """ + + def __init__(self): + self.direction = 'from_vsa_id' + self.objects = 'volumes' + self.object = 'volume' + super(VsaVolumeController, self).__init__() + + @wsgi.serializers(xml=VsaVolumesTemplate) + def index(self, req, vsa_id): + return super(VsaVolumeController, self).index(req, vsa_id) + + @wsgi.serializers(xml=VsaVolumesTemplate) + def detail(self, req, vsa_id): + return super(VsaVolumeController, self).detail(req, vsa_id) + + @wsgi.serializers(xml=VsaVolumeTemplate) + def create(self, req, vsa_id, body): + return super(VsaVolumeController, self).create(req, vsa_id, body) + + @wsgi.serializers(xml=VsaVolumeTemplate) + def update(self, req, vsa_id, id, body): + return super(VsaVolumeController, self).update(req, vsa_id, id, body) + + @wsgi.serializers(xml=VsaVolumeTemplate) + def show(self, req, vsa_id, id): + return super(VsaVolumeController, self).show(req, vsa_id, id) + + +class VsaDriveTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('drive', selector='drive') + make_volume(root) + return xmlutil.MasterTemplate(root, 1) + + +class VsaDrivesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('drives') + elem = xmlutil.SubTemplateElement(root, 'drive', selector='drives') + make_volume(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VsaDriveController(VsaVolumeDriveController): + """The VSA Drive API controller for the Openstack API. + + A child resource of the VSA object. Allows operations with drives created + for particular VSA + + """ + + def __init__(self): + self.direction = 'to_vsa_id' + self.objects = 'drives' + self.object = 'drive' + super(VsaDriveController, self).__init__() + + def create(self, req, vsa_id, body): + """Create a new drive for VSA. Should be done through VSA APIs""" + raise exc.HTTPBadRequest() + + def update(self, req, vsa_id, id, body): + """Update a drive. Should be done through VSA APIs""" + raise exc.HTTPBadRequest() + + def delete(self, req, vsa_id, id): + """Delete a volume. Should be done through VSA APIs""" + raise exc.HTTPBadRequest() + + @wsgi.serializers(xml=VsaDrivesTemplate) + def index(self, req, vsa_id): + return super(VsaDriveController, self).index(req, vsa_id) + + @wsgi.serializers(xml=VsaDrivesTemplate) + def detail(self, req, vsa_id): + return super(VsaDriveController, self).detail(req, vsa_id) + + @wsgi.serializers(xml=VsaDriveTemplate) + def show(self, req, vsa_id, id): + return super(VsaDriveController, self).show(req, vsa_id, id) + + +def make_vpool(elem): + elem.set('id') + elem.set('vsaId') + elem.set('name') + elem.set('displayName') + elem.set('displayDescription') + elem.set('driveCount') + elem.set('protection') + elem.set('stripeSize') + elem.set('stripeWidth') + elem.set('createTime') + elem.set('status') + + drive_ids = xmlutil.SubTemplateElement(elem, 'driveIds') + drive_id = xmlutil.SubTemplateElement(drive_ids, 'driveId', + selector='driveIds') + drive_id.text = xmlutil.Selector() + + +class VsaVPoolTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('vpool', selector='vpool') + make_vpool(root) + return xmlutil.MasterTemplate(root, 1) + + +class VsaVPoolsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('vpools') + elem = xmlutil.SubTemplateElement(root, 'vpool', selector='vpools') + make_vpool(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VsaVPoolController(object): + """The vPool VSA API controller for the OpenStack API.""" + + def __init__(self): + self.vsa_api = vsa.API() + super(VsaVPoolController, self).__init__() + + @wsgi.serializers(xml=VsaVPoolsTemplate) + def index(self, req, vsa_id): + """Return a short list of vpools created from particular VSA.""" + return {'vpools': []} + + def create(self, req, vsa_id, body): + """Create a new vPool for VSA.""" + raise exc.HTTPBadRequest() + + def update(self, req, vsa_id, id, body): + """Update vPool parameters.""" + raise exc.HTTPBadRequest() + + def delete(self, req, vsa_id, id): + """Delete a vPool.""" + raise exc.HTTPBadRequest() + + def show(self, req, vsa_id, id): + """Return data about the given vPool.""" + raise exc.HTTPBadRequest() + + +class VsaVCController(servers.Controller): + """The VSA Virtual Controller API controller for the OpenStack API.""" + + def __init__(self): + self.vsa_api = vsa.API() + self.compute_api = compute.API() + self.vsa_id = None # VP-TODO: temporary ugly hack + super(VsaVCController, self).__init__() + + def _get_servers(self, req, is_detail): + """Returns a list of servers, taking into account any search + options specified. + """ + + if self.vsa_id is None: + super(VsaVCController, self)._get_servers(req, is_detail) + + context = req.environ['nova.context'] + + search_opts = {'metadata': dict(vsa_id=str(self.vsa_id))} + instance_list = self.compute_api.get_all( + context, search_opts=search_opts) + + limited_list = self._limit_items(instance_list, req) + servers = [self._build_view(req, inst, is_detail)['server'] + for inst in limited_list] + return dict(servers=servers) + + @wsgi.serializers(xml=servers.MinimalServersTemplate) + def index(self, req, vsa_id): + """Return list of instances for particular VSA.""" + + LOG.audit(_("Index instances for VSA %s"), vsa_id) + + self.vsa_id = vsa_id # VP-TODO: temporary ugly hack + result = super(VsaVCController, self).detail(req) + self.vsa_id = None + return result + + def create(self, req, vsa_id, body): + """Create a new instance for VSA.""" + raise exc.HTTPBadRequest() + + def update(self, req, vsa_id, id, body): + """Update VSA instance.""" + raise exc.HTTPBadRequest() + + def delete(self, req, vsa_id, id): + """Delete VSA instance.""" + raise exc.HTTPBadRequest() + + @wsgi.serializers(xml=servers.ServerTemplate) + def show(self, req, vsa_id, id): + """Return data about the given instance.""" + return super(VsaVCController, self).show(req, id) + + +class Virtual_storage_arrays(extensions.ExtensionDescriptor): + """Virtual Storage Arrays support""" + + name = "VSAs" + alias = "zadr-vsa" + namespace = "http://docs.openstack.org/compute/ext/vsa/api/v1.1" + updated = "2011-08-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'zadr-vsa', + VsaController(), + collection_actions={'detail': 'GET'}, + member_actions={'add_capacity': 'POST', + 'remove_capacity': 'POST', + 'associate_address': 'POST', + 'disassociate_address': 'POST'}) + resources.append(res) + + res = extensions.ResourceExtension('volumes', + VsaVolumeController(), + collection_actions={'detail': 'GET'}, + parent=dict( + member_name='vsa', + collection_name='zadr-vsa')) + resources.append(res) + + res = extensions.ResourceExtension('drives', + VsaDriveController(), + collection_actions={'detail': 'GET'}, + parent=dict( + member_name='vsa', + collection_name='zadr-vsa')) + resources.append(res) + + res = extensions.ResourceExtension('vpools', + VsaVPoolController(), + parent=dict( + member_name='vsa', + collection_name='zadr-vsa')) + resources.append(res) + + res = extensions.ResourceExtension('instances', + VsaVCController(), + parent=dict( + member_name='vsa', + collection_name='zadr-vsa')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/volumes.py b/nova/api/openstack/compute/contrib/volumes.py new file mode 100644 index 000000000..972c000ef --- /dev/null +++ b/nova/api/openstack/compute/contrib/volumes.py @@ -0,0 +1,550 @@ +# Copyright 2011 Justin Santa Barbara +# 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. + +"""The volumes extension.""" + +from webob import exc +import webob + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack.compute import servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova.volume import volume_types + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.volumes") + + +FLAGS = flags.FLAGS + + +def _translate_volume_detail_view(context, vol): + """Maps keys for volumes details view.""" + + d = _translate_volume_summary_view(context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_volume_summary_view(context, vol): + """Maps keys for volumes summary view.""" + d = {} + + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] + + if vol['attach_status'] == 'attached': + d['attachments'] = [_translate_attachment_detail_view(context, vol)] + else: + d['attachments'] = [{}] + + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + + if vol['volume_type_id'] and vol.get('volume_type'): + d['volumeType'] = vol['volume_type']['name'] + else: + d['volumeType'] = vol['volume_type_id'] + + d['snapshotId'] = vol['snapshot_id'] + LOG.audit(_("vol=%s"), vol, context=context) + + if vol.get('volume_metadata'): + meta_dict = {} + for i in vol['volume_metadata']: + meta_dict[i['key']] = i['value'] + d['metadata'] = meta_dict + else: + d['metadata'] = {} + + return d + + +def make_volume(elem): + elem.set('id') + elem.set('status') + elem.set('size') + elem.set('availabilityZone') + elem.set('createdAt') + elem.set('displayName') + elem.set('displayDescription') + elem.set('volumeType') + elem.set('snapshotId') + + attachments = xmlutil.SubTemplateElement(elem, 'attachments') + attachment = xmlutil.SubTemplateElement(attachments, 'attachment', + selector='attachments') + make_attachment(attachment) + + metadata = xmlutil.make_flat_dict('metadata') + elem.append(metadata) + + +class VolumeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume', selector='volume') + make_volume(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumes') + elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') + make_volume(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeController(object): + """The Volumes API controller for the OpenStack API.""" + + def __init__(self): + self.volume_api = volume.API() + super(VolumeController, self).__init__() + + @wsgi.serializers(xml=VolumeTemplate) + def show(self, req, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + raise exc.HTTPNotFound() + + return {'volume': _translate_volume_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + + @wsgi.serializers(xml=VolumesTemplate) + def index(self, req): + """Returns a summary list of volumes.""" + return self._items(req, entity_maker=_translate_volume_summary_view) + + @wsgi.serializers(xml=VolumesTemplate) + def detail(self, req): + """Returns a detailed list of volumes.""" + return self._items(req, entity_maker=_translate_volume_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker.""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumes': res} + + @wsgi.serializers(xml=VolumeTemplate) + def create(self, req, body): + """Creates a new volume.""" + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + vol = body['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + + vol_type = vol.get('volume_type', None) + if vol_type: + try: + vol_type = volume_types.get_volume_type_by_name(context, + vol_type) + except exception.NotFound: + raise exc.HTTPNotFound() + + metadata = vol.get('metadata', None) + + new_volume = self.volume_api.create(context, size, + vol.get('snapshot_id'), + vol.get('display_name'), + vol.get('display_description'), + volume_type=vol_type, + metadata=metadata) + + # Work around problem that instance is lazy-loaded... + new_volume = self.volume_api.get(context, new_volume['id']) + + retval = _translate_volume_detail_view(context, new_volume) + + return {'volume': retval} + + +def _translate_attachment_detail_view(_context, vol): + """Maps keys for attachment details view.""" + + d = _translate_attachment_summary_view(_context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_attachment_summary_view(_context, vol): + """Maps keys for attachment summary view.""" + d = {} + + volume_id = vol['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + d['id'] = volume_id + + d['volumeId'] = volume_id + if vol.get('instance'): + d['serverId'] = vol['instance']['uuid'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d + + +def make_attachment(elem): + elem.set('id') + elem.set('serverId') + elem.set('volumeId') + elem.set('device') + + +class VolumeAttachmentTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumeAttachment', + selector='volumeAttachment') + make_attachment(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeAttachmentsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumeAttachments') + elem = xmlutil.SubTemplateElement(root, 'volumeAttachment', + selector='volumeAttachments') + make_attachment(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeAttachmentController(object): + """The volume attachment API controller for the Openstack API. + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally) + + """ + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(VolumeAttachmentController, self).__init__() + + @wsgi.serializers(xml=VolumeAttachmentsTemplate) + def index(self, req, server_id): + """Returns the list of volume attachments for a given instance.""" + return self._items(req, server_id, + entity_maker=_translate_attachment_summary_view) + + @wsgi.serializers(xml=VolumeAttachmentTemplate) + def show(self, req, server_id, id): + """Return data about the given volume attachment.""" + context = req.environ['nova.context'] + + volume_id = id + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + LOG.debug("volume_id not found") + raise exc.HTTPNotFound() + + instance = vol['instance'] + if instance is None or str(instance['uuid']) != server_id: + LOG.debug("instance_id != server_id") + raise exc.HTTPNotFound() + + return {'volumeAttachment': _translate_attachment_detail_view(context, + vol)} + + @wsgi.serializers(xml=VolumeAttachmentTemplate) + def create(self, req, server_id, body): + """Attach a volume to an instance.""" + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + volume_id = body['volumeAttachment']['volumeId'] + device = body['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + try: + instance = self.compute_api.get(context, server_id) + self.compute_api.attach_volume(context, instance, + volume_id, device) + except exception.NotFound: + raise exc.HTTPNotFound() + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id + + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart + + # TODO(justinsb): How do I return "accepted" here? + return {'volumeAttachment': attachment} + + def update(self, req, server_id, id, body): + """Update a volume attachment. We don't currently support this.""" + raise exc.HTTPBadRequest() + + def delete(self, req, server_id, id): + """Detach a volume from an instance.""" + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + raise exc.HTTPNotFound() + + instance = vol['instance'] + if instance is None or str(instance['uuid']) != server_id: + LOG.debug("instance_id != server_id") + raise exc.HTTPNotFound() + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return webob.Response(status_int=202) + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker.""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + raise exc.HTTPNotFound() + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} + + +class BootFromVolumeController(servers.Controller): + """The boot from volume API controller for the Openstack API.""" + + def _get_block_device_mapping(self, data): + return data.get('block_device_mapping') + + +def _translate_snapshot_detail_view(context, vol): + """Maps keys for snapshots details view.""" + + d = _translate_snapshot_summary_view(context, vol) + + # NOTE(gagupta): No additional data / lookups at the moment + return d + + +def _translate_snapshot_summary_view(context, vol): + """Maps keys for snapshots summary view.""" + d = {} + + d['id'] = vol['id'] + d['volumeId'] = vol['volume_id'] + d['status'] = vol['status'] + # NOTE(gagupta): We map volume_size as the snapshot size + d['size'] = vol['volume_size'] + d['createdAt'] = vol['created_at'] + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +def make_snapshot(elem): + elem.set('id') + elem.set('status') + elem.set('size') + elem.set('createdAt') + elem.set('displayName') + elem.set('displayDescription') + elem.set('volumeId') + + +class SnapshotTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('snapshot', selector='snapshot') + make_snapshot(root) + return xmlutil.MasterTemplate(root, 1) + + +class SnapshotsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('snapshots') + elem = xmlutil.SubTemplateElement(root, 'snapshot', + selector='snapshots') + make_snapshot(elem) + return xmlutil.MasterTemplate(root, 1) + + +class SnapshotController(object): + """The Volumes API controller for the OpenStack API.""" + + def __init__(self): + self.volume_api = volume.API() + super(SnapshotController, self).__init__() + + @wsgi.serializers(xml=SnapshotTemplate) + def show(self, req, id): + """Return data about the given snapshot.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get_snapshot(context, id) + except exception.NotFound: + return exc.HTTPNotFound() + + return {'snapshot': _translate_snapshot_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a snapshot.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete snapshot with id: %s"), id, context=context) + + try: + self.volume_api.delete_snapshot(context, snapshot_id=id) + except exception.NotFound: + return exc.HTTPNotFound() + return webob.Response(status_int=202) + + @wsgi.serializers(xml=SnapshotsTemplate) + def index(self, req): + """Returns a summary list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_summary_view) + + @wsgi.serializers(xml=SnapshotsTemplate) + def detail(self, req): + """Returns a detailed list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of snapshots, transformed through entity_maker.""" + context = req.environ['nova.context'] + + snapshots = self.volume_api.get_all_snapshots(context) + limited_list = common.limited(snapshots, req) + res = [entity_maker(context, snapshot) for snapshot in limited_list] + return {'snapshots': res} + + @wsgi.serializers(xml=SnapshotTemplate) + def create(self, req, body): + """Creates a new snapshot.""" + context = req.environ['nova.context'] + + if not body: + return exc.HTTPUnprocessableEntity() + + snapshot = body['snapshot'] + volume_id = snapshot['volume_id'] + force = snapshot.get('force', False) + LOG.audit(_("Create snapshot from volume %s"), volume_id, + context=context) + + if force: + new_snapshot = self.volume_api.create_snapshot_force(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + else: + new_snapshot = self.volume_api.create_snapshot(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + + retval = _translate_snapshot_detail_view(context, new_snapshot) + + return {'snapshot': retval} + + +class Volumes(extensions.ExtensionDescriptor): + """Volumes support""" + + name = "Volumes" + alias = "os-volumes" + namespace = "http://docs.openstack.org/compute/ext/volumes/api/v1.1" + updated = "2011-03-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + # NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? + res = extensions.ResourceExtension('os-volumes', + VolumeController(), + collection_actions={'detail': 'GET'}) + resources.append(res) + + res = extensions.ResourceExtension('os-volume_attachments', + VolumeAttachmentController(), + parent=dict( + member_name='server', + collection_name='servers')) + resources.append(res) + + res = extensions.ResourceExtension('os-volumes_boot', + BootFromVolumeController()) + resources.append(res) + + res = extensions.ResourceExtension('os-snapshots', + SnapshotController(), + collection_actions={'detail': 'GET'}) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/volumetypes.py b/nova/api/openstack/compute/contrib/volumetypes.py new file mode 100644 index 000000000..bf249f3f8 --- /dev/null +++ b/nova/api/openstack/compute/contrib/volumetypes.py @@ -0,0 +1,237 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# +# 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. + +""" The volume type & volume types extra specs extension""" + +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import db +from nova import exception +from nova.volume import volume_types + + +def make_voltype(elem): + elem.set('id') + elem.set('name') + extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') + elem.append(extra_specs) + + +class VolumeTypeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume_type', selector='volume_type') + make_voltype(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume_types') + sel = lambda obj, do_raise=False: obj.values() + elem = xmlutil.SubTemplateElement(root, 'volume_type', selector=sel) + make_voltype(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypesController(object): + """ The volume types API controller for the Openstack API """ + + @wsgi.serializers(xml=VolumeTypesTemplate) + def index(self, req): + """ Returns the list of volume types """ + context = req.environ['nova.context'] + return volume_types.get_all_types(context) + + @wsgi.serializers(xml=VolumeTypeTemplate) + def create(self, req, body): + """Creates a new volume type.""" + context = req.environ['nova.context'] + + if not body or body == "": + raise exc.HTTPUnprocessableEntity() + + vol_type = body.get('volume_type', None) + if vol_type is None or vol_type == "": + raise exc.HTTPUnprocessableEntity() + + name = vol_type.get('name', None) + specs = vol_type.get('extra_specs', {}) + + if name is None or name == "": + raise exc.HTTPUnprocessableEntity() + + try: + volume_types.create(context, name, specs) + vol_type = volume_types.get_volume_type_by_name(context, name) + except exception.QuotaError as error: + self._handle_quota_error(error) + except exception.NotFound: + raise exc.HTTPNotFound() + + return {'volume_type': vol_type} + + @wsgi.serializers(xml=VolumeTypeTemplate) + def show(self, req, id): + """ Return a single volume type item """ + context = req.environ['nova.context'] + + try: + vol_type = volume_types.get_volume_type(context, id) + except exception.NotFound or exception.ApiError: + raise exc.HTTPNotFound() + + return {'volume_type': vol_type} + + def delete(self, req, id): + """ Deletes an existing volume type """ + context = req.environ['nova.context'] + + try: + vol_type = volume_types.get_volume_type(context, id) + volume_types.destroy(context, vol_type['name']) + except exception.NotFound: + raise exc.HTTPNotFound() + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions.""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPBadRequest(explanation=error.message) + raise error + + +class VolumeTypeExtraSpecsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypeExtraSpecTemplate(xmlutil.TemplateBuilder): + def construct(self): + tagname = xmlutil.Selector('key') + + def extraspec_sel(obj, do_raise=False): + # Have to extract the key and value for later use... + key, value = obj.items()[0] + return dict(key=key, value=value) + + root = xmlutil.TemplateElement(tagname, selector=extraspec_sel) + root.text = 'value' + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypeExtraSpecsController(object): + """ The volume type extra specs API controller for the Openstack API """ + + def _get_extra_specs(self, context, vol_type_id): + extra_specs = db.volume_type_extra_specs_get(context, vol_type_id) + specs_dict = {} + for key, value in extra_specs.iteritems(): + specs_dict[key] = value + return dict(extra_specs=specs_dict) + + def _check_body(self, body): + if body is None or body == "": + expl = _('No Request Body') + raise exc.HTTPBadRequest(explanation=expl) + + @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) + def index(self, req, vol_type_id): + """ Returns the list of extra specs for a given volume type """ + context = req.environ['nova.context'] + return self._get_extra_specs(context, vol_type_id) + + @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) + def create(self, req, vol_type_id, body): + self._check_body(body) + context = req.environ['nova.context'] + specs = body.get('extra_specs') + try: + db.volume_type_extra_specs_update_or_create(context, + vol_type_id, + specs) + except exception.QuotaError as error: + self._handle_quota_error(error) + return body + + @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) + def update(self, req, vol_type_id, id, body): + self._check_body(body) + context = req.environ['nova.context'] + if not id in body: + expl = _('Request body and URI mismatch') + raise exc.HTTPBadRequest(explanation=expl) + if len(body) > 1: + expl = _('Request body contains too many items') + raise exc.HTTPBadRequest(explanation=expl) + try: + db.volume_type_extra_specs_update_or_create(context, + vol_type_id, + body) + except exception.QuotaError as error: + self._handle_quota_error(error) + + return body + + @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) + def show(self, req, vol_type_id, id): + """ Return a single extra spec item """ + context = req.environ['nova.context'] + specs = self._get_extra_specs(context, vol_type_id) + if id in specs['extra_specs']: + return {id: specs['extra_specs'][id]} + else: + raise exc.HTTPNotFound() + + def delete(self, req, vol_type_id, id): + """ Deletes an existing extra spec """ + context = req.environ['nova.context'] + db.volume_type_extra_specs_delete(context, vol_type_id, id) + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions.""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPBadRequest(explanation=error.message) + raise error + + +class Volumetypes(extensions.ExtensionDescriptor): + """Volume types support""" + + name = "VolumeTypes" + alias = "os-volume-types" + namespace = "http://docs.openstack.org/compute/ext/volume_types/api/v1.1" + updated = "2011-08-24T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-volume-types', + VolumeTypesController()) + resources.append(res) + + res = extensions.ResourceExtension('extra_specs', + VolumeTypeExtraSpecsController(), + parent=dict( + member_name='vol_type', + collection_name='os-volume-types')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/zones.py b/nova/api/openstack/compute/contrib/zones.py new file mode 100644 index 000000000..28e6f0772 --- /dev/null +++ b/nova/api/openstack/compute/contrib/zones.py @@ -0,0 +1,239 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The zones extension.""" + +import json + +from nova.api.openstack import common +from nova.api.openstack.compute import servers +from nova.api.openstack import extensions +from nova.api.openstack import xmlutil +from nova.api.openstack import wsgi +from nova.compute import api as compute +from nova import crypto +from nova import exception +from nova import flags +from nova import log as logging +import nova.scheduler.api + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.zones") +FLAGS = flags.FLAGS + + +class CapabilitySelector(object): + def __call__(self, obj, do_raise=False): + return [(k, v) for k, v in obj.items() + if k not in ('id', 'api_url', 'name', 'capabilities')] + + +def make_zone(elem): + elem.set('id') + elem.set('api_url') + elem.set('name') + elem.set('capabilities') + + cap = xmlutil.SubTemplateElement(elem, xmlutil.Selector(0), + selector=CapabilitySelector()) + cap.text = 1 + + +zone_nsmap = {None: wsgi.XMLNS_V10} + + +class ZoneTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zone', selector='zone') + make_zone(root) + return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) + + +class ZonesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zones') + elem = xmlutil.SubTemplateElement(root, 'zone', selector='zones') + make_zone(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) + + +class WeightsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('weights') + weight = xmlutil.SubTemplateElement(root, 'weight', selector='weights') + blob = xmlutil.SubTemplateElement(weight, 'blob') + blob.text = 'blob' + inner_weight = xmlutil.SubTemplateElement(weight, 'weight') + inner_weight.text = 'weight' + return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) + + +def _filter_keys(item, keys): + """ + Filters all model attributes except for keys + item is a dict + + """ + return dict((k, v) for k, v in item.iteritems() if k in keys) + + +def _exclude_keys(item, keys): + return dict((k, v) for k, v in item.iteritems() if k and (k not in keys)) + + +def _scrub_zone(zone): + return _exclude_keys(zone, ('username', 'password', 'created_at', + 'deleted', 'deleted_at', 'updated_at')) + + +def check_encryption_key(func): + def wrapped(*args, **kwargs): + if not FLAGS.build_plan_encryption_key: + raise exception.Error(_("--build_plan_encryption_key not set")) + return func(*args, **kwargs) + return wrapped + + +class Controller(object): + """Controller for Zone resources.""" + + def __init__(self): + self.compute_api = compute.API() + + @wsgi.serializers(xml=ZonesTemplate) + def index(self, req): + """Return all zones in brief""" + # Ask the ZoneManager in the Scheduler for most recent data, + # or fall-back to the database ... + items = nova.scheduler.api.get_zone_list(req.environ['nova.context']) + items = common.limited(items, req) + items = [_scrub_zone(item) for item in items] + return dict(zones=items) + + @wsgi.serializers(xml=ZonesTemplate) + def detail(self, req): + """Return all zones in detail""" + return self.index(req) + + @wsgi.serializers(xml=ZoneTemplate) + def info(self, req): + """Return name and capabilities for this zone.""" + context = req.environ['nova.context'] + items = nova.scheduler.api.get_zone_capabilities(context) + + zone = dict(name=FLAGS.zone_name) + caps = FLAGS.zone_capabilities + for cap in caps: + key, value = cap.split('=') + zone[key] = value + for item, (min_value, max_value) in items.iteritems(): + zone[item] = "%s,%s" % (min_value, max_value) + return dict(zone=zone) + + @wsgi.serializers(xml=ZoneTemplate) + def show(self, req, id): + """Return data about the given zone id""" + zone_id = int(id) + context = req.environ['nova.context'] + zone = nova.scheduler.api.zone_get(context, zone_id) + return dict(zone=_scrub_zone(zone)) + + def delete(self, req, id): + """Delete a child zone entry.""" + zone_id = int(id) + nova.scheduler.api.zone_delete(req.environ['nova.context'], zone_id) + return {} + + @wsgi.serializers(xml=ZoneTemplate) + @wsgi.deserializers(xml=servers.CreateDeserializer) + def create(self, req, body): + """Create a child zone entry.""" + context = req.environ['nova.context'] + zone = nova.scheduler.api.zone_create(context, body["zone"]) + return dict(zone=_scrub_zone(zone)) + + @wsgi.serializers(xml=ZoneTemplate) + def update(self, req, id, body): + """Update a child zone entry.""" + context = req.environ['nova.context'] + zone_id = int(id) + zone = nova.scheduler.api.zone_update(context, zone_id, body["zone"]) + return dict(zone=_scrub_zone(zone)) + + @wsgi.serializers(xml=WeightsTemplate) + @check_encryption_key + def select(self, req, body): + """Returns a weighted list of costs to create instances + of desired capabilities.""" + ctx = req.environ['nova.context'] + specs = json.loads(body) + build_plan = nova.scheduler.api.select(ctx, specs=specs) + cooked = self._scrub_build_plan(build_plan) + return {"weights": cooked} + + def _scrub_build_plan(self, build_plan): + """Remove all the confidential data and return a sanitized + version of the build plan. Include an encrypted full version + of the weighting entry so we can get back to it later.""" + encryptor = crypto.encryptor(FLAGS.build_plan_encryption_key) + cooked = [] + for entry in build_plan: + json_entry = json.dumps(entry) + cipher_text = encryptor(json_entry) + cooked.append(dict(weight=entry['weight'], + blob=cipher_text)) + return cooked + + +class ZonesXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return ZonesTemplate() + + def detail(self): + return ZonesTemplate() + + def select(self): + return WeightsTemplate() + + def default(self): + return ZoneTemplate() + + +class Zones(extensions.ExtensionDescriptor): + """Enables zones-related functionality such as adding child zones, + listing child zones, getting the capabilities of the local zone, + and returning build plans to parent zones' schedulers + """ + + name = "Zones" + alias = "os-zones" + namespace = "http://docs.openstack.org/compute/ext/zones/api/v1.1" + updated = "2011-09-21T00:00:00+00:00" + admin_only = True + + def get_resources(self): + #NOTE(bcwaldon): This resource should be prefixed with 'os-' + coll_actions = { + 'detail': 'GET', + 'info': 'GET', + 'select': 'POST', + } + + res = extensions.ResourceExtension('zones', + Controller(), + collection_actions=coll_actions) + return [res] diff --git a/nova/api/openstack/compute/extensions.py b/nova/api/openstack/compute/extensions.py new file mode 100644 index 000000000..39849e802 --- /dev/null +++ b/nova/api/openstack/compute/extensions.py @@ -0,0 +1,45 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.openstack import extensions as base_extensions +from nova import flags +from nova import log as logging + + +LOG = logging.getLogger('nova.api.openstack.compute.extensions') +FLAGS = flags.FLAGS + + +class ExtensionManager(base_extensions.ExtensionManager): + def __new__(cls): + if cls._ext_mgr is None: + LOG.audit(_('Initializing extension manager.')) + + cls._ext_mgr = super(ExtensionManager, cls).__new__(cls) + + cls.cls_list = FLAGS.osapi_compute_extension + cls._ext_mgr.extensions = {} + cls._ext_mgr._load_extensions() + + return cls._ext_mgr + + +class ExtensionMiddleware(base_extensions.ExtensionMiddleware): + def __init__(self, application, ext_mgr=None): + if not ext_mgr: + ext_mgr = ExtensionManager() + super(ExtensionMiddleware, self).__init__(application, ext_mgr) diff --git a/nova/api/openstack/compute/flavors.py b/nova/api/openstack/compute/flavors.py new file mode 100644 index 000000000..b5212e703 --- /dev/null +++ b/nova/api/openstack/compute/flavors.py @@ -0,0 +1,112 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob + +from nova.api.openstack.compute.views import flavors as flavors_view +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.compute import instance_types +from nova import exception + + +def make_flavor(elem, detailed=False): + elem.set('name') + elem.set('id') + if detailed: + elem.set('ram') + elem.set('disk') + + for attr in ("vcpus", "swap", "rxtx_factor"): + elem.set(attr, xmlutil.EmptyStringSelector(attr)) + + xmlutil.make_links(elem, 'links') + + +flavor_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class FlavorTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavor', selector='flavor') + make_flavor(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) + + +class MinimalFlavorsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavors') + elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') + make_flavor(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) + + +class FlavorsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavors') + elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') + make_flavor(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) + + +class Controller(wsgi.Controller): + """Flavor controller for the OpenStack API.""" + + _view_builder_class = flavors_view.ViewBuilder + + @wsgi.serializers(xml=MinimalFlavorsTemplate) + def index(self, req): + """Return all flavors in brief.""" + flavors = self._get_flavors(req) + return self._view_builder.index(req, flavors) + + @wsgi.serializers(xml=FlavorsTemplate) + def detail(self, req): + """Return all flavors in detail.""" + flavors = self._get_flavors(req) + return self._view_builder.detail(req, flavors) + + @wsgi.serializers(xml=FlavorTemplate) + def show(self, req, id): + """Return data about the given flavor id.""" + try: + flavor = instance_types.get_instance_type_by_flavor_id(id) + except exception.NotFound: + raise webob.exc.HTTPNotFound() + + return self._view_builder.show(req, flavor) + + def _get_flavors(self, req): + """Helper function that returns a list of flavor dicts.""" + filters = {} + if 'minRam' in req.params: + try: + filters['min_memory_mb'] = int(req.params['minRam']) + except ValueError: + pass # ignore bogus values per spec + + if 'minDisk' in req.params: + try: + filters['min_local_gb'] = int(req.params['minDisk']) + except ValueError: + pass # ignore bogus values per spec + + return instance_types.get_all_types(filters=filters) + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/image_metadata.py b/nova/api/openstack/compute/image_metadata.py new file mode 100644 index 000000000..1e29d23ce --- /dev/null +++ b/nova/api/openstack/compute/image_metadata.py @@ -0,0 +1,118 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import wsgi +from nova import exception +from nova import flags +from nova import image + + +FLAGS = flags.FLAGS + + +class Controller(object): + """The image metadata API controller for the Openstack API""" + + def __init__(self): + self.image_service = image.get_default_image_service() + + def _get_image(self, context, image_id): + try: + return self.image_service.show(context, image_id) + except exception.NotFound: + msg = _("Image not found.") + raise exc.HTTPNotFound(explanation=msg) + + @wsgi.serializers(xml=common.MetadataTemplate) + def index(self, req, image_id): + """Returns the list of metadata for a given instance""" + context = req.environ['nova.context'] + metadata = self._get_image(context, image_id)['properties'] + return dict(metadata=metadata) + + @wsgi.serializers(xml=common.MetaItemTemplate) + def show(self, req, image_id, id): + context = req.environ['nova.context'] + metadata = self._get_image(context, image_id)['properties'] + if id in metadata: + return {'meta': {id: metadata[id]}} + else: + raise exc.HTTPNotFound() + + @wsgi.serializers(xml=common.MetadataTemplate) + @wsgi.deserializers(xml=common.MetadataDeserializer) + def create(self, req, image_id, body): + context = req.environ['nova.context'] + image = self._get_image(context, image_id) + if 'metadata' in body: + for key, value in body['metadata'].iteritems(): + image['properties'][key] = value + common.check_img_metadata_quota_limit(context, image['properties']) + self.image_service.update(context, image_id, image, None) + return dict(metadata=image['properties']) + + @wsgi.serializers(xml=common.MetaItemTemplate) + @wsgi.deserializers(xml=common.MetaItemDeserializer) + def update(self, req, image_id, id, body): + context = req.environ['nova.context'] + + try: + meta = body['meta'] + except KeyError: + expl = _('Incorrect request body format') + raise exc.HTTPBadRequest(explanation=expl) + + if not id in meta: + expl = _('Request body and URI mismatch') + raise exc.HTTPBadRequest(explanation=expl) + if len(meta) > 1: + expl = _('Request body contains too many items') + raise exc.HTTPBadRequest(explanation=expl) + + image = self._get_image(context, image_id) + image['properties'][id] = meta[id] + common.check_img_metadata_quota_limit(context, image['properties']) + self.image_service.update(context, image_id, image, None) + return dict(meta=meta) + + @wsgi.serializers(xml=common.MetadataTemplate) + @wsgi.deserializers(xml=common.MetadataDeserializer) + def update_all(self, req, image_id, body): + context = req.environ['nova.context'] + image = self._get_image(context, image_id) + metadata = body.get('metadata', {}) + common.check_img_metadata_quota_limit(context, metadata) + image['properties'] = metadata + self.image_service.update(context, image_id, image, None) + return dict(metadata=metadata) + + @wsgi.response(204) + def delete(self, req, image_id, id): + context = req.environ['nova.context'] + image = self._get_image(context, image_id) + if not id in image['properties']: + msg = _("Invalid metadata key") + raise exc.HTTPNotFound(explanation=msg) + image['properties'].pop(id) + self.image_service.update(context, image_id, image, None) + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/images.py b/nova/api/openstack/compute/images.py new file mode 100644 index 000000000..194c5071e --- /dev/null +++ b/nova/api/openstack/compute/images.py @@ -0,0 +1,195 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob.exc + +from nova.api.openstack import common +from nova.api.openstack.compute.views import images as views_images +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova import flags +import nova.image +from nova import log + + +LOG = log.getLogger('nova.api.openstack.compute.images') +FLAGS = flags.FLAGS + +SUPPORTED_FILTERS = { + 'name': 'name', + 'status': 'status', + 'changes-since': 'changes-since', + 'server': 'property-instance_ref', + 'type': 'property-image_type', + 'minRam': 'min_ram', + 'minDisk': 'min_disk', +} + + +def make_image(elem, detailed=False): + elem.set('name') + elem.set('id') + + if detailed: + elem.set('updated') + elem.set('created') + elem.set('status') + elem.set('progress') + elem.set('minRam') + elem.set('minDisk') + + server = xmlutil.SubTemplateElement(elem, 'server', selector='server') + server.set('id') + xmlutil.make_links(server, 'links') + + elem.append(common.MetadataTemplate()) + + xmlutil.make_links(elem, 'links') + + +image_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class ImageTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('image', selector='image') + make_image(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) + + +class MinimalImagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + make_image(elem) + xmlutil.make_links(root, 'images_links') + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) + + +class ImagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + make_image(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) + + +class Controller(wsgi.Controller): + """Base controller for retrieving/displaying images.""" + + _view_builder_class = views_images.ViewBuilder + + def __init__(self, image_service=None, compute_service=None, **kwargs): + """Initialize new `ImageController`. + + :param compute_service: `nova.compute.api:API` + :param image_service: `nova.image.glance:GlancemageService` + + """ + super(Controller, self).__init__(**kwargs) + self._compute_service = compute_service or compute.API() + self._image_service = image_service or \ + nova.image.get_default_image_service() + + def _get_filters(self, req): + """ + Return a dictionary of query param filters from the request + + :param req: the Request object coming from the wsgi layer + :retval a dict of key/value filters + """ + filters = {} + for param in req.params: + if param in SUPPORTED_FILTERS or param.startswith('property-'): + # map filter name or carry through if property-* + filter_name = SUPPORTED_FILTERS.get(param, param) + filters[filter_name] = req.params.get(param) + return filters + + @wsgi.serializers(xml=ImageTemplate) + def show(self, req, id): + """Return detailed information about a specific image. + + :param req: `wsgi.Request` object + :param id: Image identifier + """ + context = req.environ['nova.context'] + + try: + image = self._image_service.show(context, id) + except (exception.NotFound, exception.InvalidImageRef): + explanation = _("Image not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + + return self._view_builder.show(req, image) + + def delete(self, req, id): + """Delete an image, if allowed. + + :param req: `wsgi.Request` object + :param id: Image identifier (integer) + """ + context = req.environ['nova.context'] + try: + self._image_service.delete(context, id) + except exception.ImageNotFound: + explanation = _("Image not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + return webob.exc.HTTPNoContent() + + @wsgi.serializers(xml=MinimalImagesTemplate) + def index(self, req): + """Return an index listing of images available to the request. + + :param req: `wsgi.Request` object + + """ + context = req.environ['nova.context'] + filters = self._get_filters(req) + params = req.GET.copy() + page_params = common.get_pagination_params(req) + for key, val in page_params.iteritems(): + params[key] = val + + images = self._image_service.index(context, filters=filters, + **page_params) + return self._view_builder.index(req, images) + + @wsgi.serializers(xml=ImagesTemplate) + def detail(self, req): + """Return a detailed index listing of images available to the request. + + :param req: `wsgi.Request` object. + + """ + context = req.environ['nova.context'] + filters = self._get_filters(req) + params = req.GET.copy() + page_params = common.get_pagination_params(req) + for key, val in page_params.iteritems(): + params[key] = val + images = self._image_service.detail(context, filters=filters, + **page_params) + + return self._view_builder.detail(req, images) + + def create(self, *args, **kwargs): + raise webob.exc.HTTPMethodNotAllowed() + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/ips.py b/nova/api/openstack/compute/ips.py new file mode 100644 index 000000000..ec107914a --- /dev/null +++ b/nova/api/openstack/compute/ips.py @@ -0,0 +1,105 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from webob import exc + +import nova +from nova.api.openstack import common +from nova.api.openstack.compute.views import addresses as view_addresses +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import log as logging +from nova import flags + + +LOG = logging.getLogger('nova.api.openstack.compute.ips') +FLAGS = flags.FLAGS + + +def make_network(elem): + elem.set('id', 0) + + ip = xmlutil.SubTemplateElement(elem, 'ip', selector=1) + ip.set('version') + ip.set('addr') + + +network_nsmap = {None: xmlutil.XMLNS_V11} + + +class NetworkTemplate(xmlutil.TemplateBuilder): + def construct(self): + sel = xmlutil.Selector(xmlutil.get_items, 0) + root = xmlutil.TemplateElement('network', selector=sel) + make_network(root) + return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) + + +class AddressesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('addresses', selector='addresses') + elem = xmlutil.SubTemplateElement(root, 'network', + selector=xmlutil.get_items) + make_network(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) + + +class Controller(wsgi.Controller): + """The servers addresses API controller for the Openstack API.""" + + _view_builder_class = view_addresses.ViewBuilder + + def __init__(self, **kwargs): + super(Controller, self).__init__(**kwargs) + self._compute_api = nova.compute.API() + + def _get_instance(self, context, server_id): + try: + instance = self._compute_api.get(context, server_id) + except nova.exception.NotFound: + msg = _("Instance does not exist") + raise exc.HTTPNotFound(explanation=msg) + return instance + + def create(self, req, server_id, body): + raise exc.HTTPNotImplemented() + + def delete(self, req, server_id, id): + raise exc.HTTPNotImplemented() + + @wsgi.serializers(xml=AddressesTemplate) + def index(self, req, server_id): + context = req.environ["nova.context"] + instance = self._get_instance(context, server_id) + networks = common.get_networks_for_instance(context, instance) + return self._view_builder.index(networks) + + @wsgi.serializers(xml=NetworkTemplate) + def show(self, req, server_id, id): + context = req.environ["nova.context"] + instance = self._get_instance(context, server_id) + networks = common.get_networks_for_instance(context, instance) + + if id not in networks: + msg = _("Instance is not a member of specified network") + raise exc.HTTPNotFound(explanation=msg) + + return self._view_builder.show(networks[id], id) + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/limits.py b/nova/api/openstack/compute/limits.py new file mode 100644 index 000000000..0a1b80057 --- /dev/null +++ b/nova/api/openstack/compute/limits.py @@ -0,0 +1,477 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Module dedicated functions/classes dealing with rate limiting requests. +""" + +from collections import defaultdict +import copy +import httplib +import json +import math +import re +import time + +from webob.dec import wsgify +import webob.exc + +from nova.api.openstack.compute.views import limits as limits_views +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import quota +from nova import utils +from nova import wsgi as base_wsgi + + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + + +limits_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class LimitsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('limits', selector='limits') + + rates = xmlutil.SubTemplateElement(root, 'rates') + rate = xmlutil.SubTemplateElement(rates, 'rate', selector='rate') + rate.set('uri', 'uri') + rate.set('regex', 'regex') + limit = xmlutil.SubTemplateElement(rate, 'limit', selector='limit') + limit.set('value', 'value') + limit.set('verb', 'verb') + limit.set('remaining', 'remaining') + limit.set('unit', 'unit') + limit.set('next-available', 'next-available') + + absolute = xmlutil.SubTemplateElement(root, 'absolute', + selector='absolute') + limit = xmlutil.SubTemplateElement(absolute, 'limit', + selector=xmlutil.get_items) + limit.set('name', 0) + limit.set('value', 1) + + return xmlutil.MasterTemplate(root, 1, nsmap=limits_nsmap) + + +class LimitsController(object): + """ + Controller for accessing limits in the OpenStack API. + """ + + @wsgi.serializers(xml=LimitsTemplate) + def index(self, req): + """ + Return all global and rate limit information. + """ + context = req.environ['nova.context'] + abs_limits = quota.get_project_quotas(context, context.project_id) + rate_limits = req.environ.get("nova.limits", []) + + builder = self._get_view_builder(req) + return builder.build(rate_limits, abs_limits) + + def _get_view_builder(self, req): + return limits_views.ViewBuilder() + + +def create_resource(): + return wsgi.Resource(LimitsController()) + + +class Limit(object): + """ + Stores information about a limit for HTTP requests. + """ + + UNITS = { + 1: "SECOND", + 60: "MINUTE", + 60 * 60: "HOUR", + 60 * 60 * 24: "DAY", + } + + UNIT_MAP = dict([(v, k) for k, v in UNITS.items()]) + + def __init__(self, verb, uri, regex, value, unit): + """ + Initialize a new `Limit`. + + @param verb: HTTP verb (POST, PUT, etc.) + @param uri: Human-readable URI + @param regex: Regular expression format for this limit + @param value: Integer number of requests which can be made + @param unit: Unit of measure for the value parameter + """ + self.verb = verb + self.uri = uri + self.regex = regex + self.value = int(value) + self.unit = unit + self.unit_string = self.display_unit().lower() + self.remaining = int(value) + + if value <= 0: + raise ValueError("Limit value must be > 0") + + self.last_request = None + self.next_request = None + + self.water_level = 0 + self.capacity = self.unit + self.request_value = float(self.capacity) / float(self.value) + self.error_message = _("Only %(value)s %(verb)s request(s) can be "\ + "made to %(uri)s every %(unit_string)s." % self.__dict__) + + def __call__(self, verb, url): + """ + Represents a call to this limit from a relevant request. + + @param verb: string http verb (POST, GET, etc.) + @param url: string URL + """ + if self.verb != verb or not re.match(self.regex, url): + return + + now = self._get_time() + + if self.last_request is None: + self.last_request = now + + leak_value = now - self.last_request + + self.water_level -= leak_value + self.water_level = max(self.water_level, 0) + self.water_level += self.request_value + + difference = self.water_level - self.capacity + + self.last_request = now + + if difference > 0: + self.water_level -= self.request_value + self.next_request = now + difference + return difference + + cap = self.capacity + water = self.water_level + val = self.value + + self.remaining = math.floor(((cap - water) / cap) * val) + self.next_request = now + + def _get_time(self): + """Retrieve the current time. Broken out for testability.""" + return time.time() + + def display_unit(self): + """Display the string name of the unit.""" + return self.UNITS.get(self.unit, "UNKNOWN") + + def display(self): + """Return a useful representation of this class.""" + return { + "verb": self.verb, + "URI": self.uri, + "regex": self.regex, + "value": self.value, + "remaining": int(self.remaining), + "unit": self.display_unit(), + "resetTime": int(self.next_request or self._get_time()), + } + +# "Limit" format is a dictionary with the HTTP verb, human-readable URI, +# a regular-expression to match, value and unit of measure (PER_DAY, etc.) + +DEFAULT_LIMITS = [ + Limit("POST", "*", ".*", 10, PER_MINUTE), + Limit("POST", "*/servers", "^/servers", 50, PER_DAY), + Limit("PUT", "*", ".*", 10, PER_MINUTE), + Limit("GET", "*changes-since*", ".*changes-since.*", 3, PER_MINUTE), + Limit("DELETE", "*", ".*", 100, PER_MINUTE), +] + + +class RateLimitingMiddleware(base_wsgi.Middleware): + """ + Rate-limits requests passing through this middleware. All limit information + is stored in memory for this implementation. + """ + + def __init__(self, application, limits=None, limiter=None, **kwargs): + """ + Initialize new `RateLimitingMiddleware`, which wraps the given WSGI + application and sets up the given limits. + + @param application: WSGI application to wrap + @param limits: String describing limits + @param limiter: String identifying class for representing limits + + Other parameters are passed to the constructor for the limiter. + """ + base_wsgi.Middleware.__init__(self, application) + + # Select the limiter class + if limiter is None: + limiter = Limiter + else: + limiter = utils.import_class(limiter) + + # Parse the limits, if any are provided + if limits is not None: + limits = limiter.parse_limits(limits) + + self._limiter = limiter(limits or DEFAULT_LIMITS, **kwargs) + + @wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """ + Represents a single call through this middleware. We should record the + request if we have a limit relevant to it. If no limit is relevant to + the request, ignore it. + + If the request should be rate limited, return a fault telling the user + they are over the limit and need to retry later. + """ + verb = req.method + url = req.url + context = req.environ.get("nova.context") + + if context: + username = context.user_id + else: + username = None + + delay, error = self._limiter.check_for_delay(verb, url, username) + + if delay: + msg = _("This request was rate-limited.") + retry = time.time() + delay + return wsgi.OverLimitFault(msg, error, retry) + + req.environ["nova.limits"] = self._limiter.get_limits(username) + + return self.application + + +class Limiter(object): + """ + Rate-limit checking class which handles limits in memory. + """ + + def __init__(self, limits, **kwargs): + """ + Initialize the new `Limiter`. + + @param limits: List of `Limit` objects + """ + self.limits = copy.deepcopy(limits) + self.levels = defaultdict(lambda: copy.deepcopy(limits)) + + # Pick up any per-user limit information + for key, value in kwargs.items(): + if key.startswith('user:'): + username = key[5:] + self.levels[username] = self.parse_limits(value) + + def get_limits(self, username=None): + """ + Return the limits for a given user. + """ + return [limit.display() for limit in self.levels[username]] + + def check_for_delay(self, verb, url, username=None): + """ + Check the given verb/user/user triplet for limit. + + @return: Tuple of delay (in seconds) and error message (or None, None) + """ + delays = [] + + for limit in self.levels[username]: + delay = limit(verb, url) + if delay: + delays.append((delay, limit.error_message)) + + if delays: + delays.sort() + return delays[0] + + return None, None + + # Note: This method gets called before the class is instantiated, + # so this must be either a static method or a class method. It is + # used to develop a list of limits to feed to the constructor. We + # put this in the class so that subclasses can override the + # default limit parsing. + @staticmethod + def parse_limits(limits): + """ + Convert a string into a list of Limit instances. This + implementation expects a semicolon-separated sequence of + parenthesized groups, where each group contains a + comma-separated sequence consisting of HTTP method, + user-readable URI, a URI reg-exp, an integer number of + requests which can be made, and a unit of measure. Valid + values for the latter are "SECOND", "MINUTE", "HOUR", and + "DAY". + + @return: List of Limit instances. + """ + + # Handle empty limit strings + limits = limits.strip() + if not limits: + return [] + + # Split up the limits by semicolon + result = [] + for group in limits.split(';'): + group = group.strip() + if group[:1] != '(' or group[-1:] != ')': + raise ValueError("Limit rules must be surrounded by " + "parentheses") + group = group[1:-1] + + # Extract the Limit arguments + args = [a.strip() for a in group.split(',')] + if len(args) != 5: + raise ValueError("Limit rules must contain the following " + "arguments: verb, uri, regex, value, unit") + + # Pull out the arguments + verb, uri, regex, value, unit = args + + # Upper-case the verb + verb = verb.upper() + + # Convert value--raises ValueError if it's not integer + value = int(value) + + # Convert unit + unit = unit.upper() + if unit not in Limit.UNIT_MAP: + raise ValueError("Invalid units specified") + unit = Limit.UNIT_MAP[unit] + + # Build a limit + result.append(Limit(verb, uri, regex, value, unit)) + + return result + + +class WsgiLimiter(object): + """ + Rate-limit checking from a WSGI application. Uses an in-memory `Limiter`. + + To use: + POST / with JSON data such as: + { + "verb" : GET, + "path" : "/servers" + } + + and receive a 204 No Content, or a 403 Forbidden with an X-Wait-Seconds + header containing the number of seconds to wait before the action would + succeed. + """ + + def __init__(self, limits=None): + """ + Initialize the new `WsgiLimiter`. + + @param limits: List of `Limit` objects + """ + self._limiter = Limiter(limits or DEFAULT_LIMITS) + + @wsgify(RequestClass=wsgi.Request) + def __call__(self, request): + """ + Handles a call to this application. Returns 204 if the request is + acceptable to the limiter, else a 403 is returned with a relevant + header indicating when the request *will* succeed. + """ + if request.method != "POST": + raise webob.exc.HTTPMethodNotAllowed() + + try: + info = dict(json.loads(request.body)) + except ValueError: + raise webob.exc.HTTPBadRequest() + + username = request.path_info_pop() + verb = info.get("verb") + path = info.get("path") + + delay, error = self._limiter.check_for_delay(verb, path, username) + + if delay: + headers = {"X-Wait-Seconds": "%.2f" % delay} + return webob.exc.HTTPForbidden(headers=headers, explanation=error) + else: + return webob.exc.HTTPNoContent() + + +class WsgiLimiterProxy(object): + """ + Rate-limit requests based on answers from a remote source. + """ + + def __init__(self, limiter_address): + """ + Initialize the new `WsgiLimiterProxy`. + + @param limiter_address: IP/port combination of where to request limit + """ + self.limiter_address = limiter_address + + def check_for_delay(self, verb, path, username=None): + body = json.dumps({"verb": verb, "path": path}) + headers = {"Content-Type": "application/json"} + + conn = httplib.HTTPConnection(self.limiter_address) + + if username: + conn.request("POST", "/%s" % (username), body, headers) + else: + conn.request("POST", "/", body, headers) + + resp = conn.getresponse() + + if 200 >= resp.status < 300: + return None, None + + return resp.getheader("X-Wait-Seconds"), resp.read() or None + + # Note: This method gets called before the class is instantiated, + # so this must be either a static method or a class method. It is + # used to develop a list of limits to feed to the constructor. + # This implementation returns an empty list, since all limit + # decisions are made by a remote server. + @staticmethod + def parse_limits(limits): + """ + Ignore a limits string--simply doesn't apply for the limit + proxy. + + @return: Empty list. + """ + + return [] diff --git a/nova/api/openstack/compute/ratelimiting/__init__.py b/nova/api/openstack/compute/ratelimiting/__init__.py new file mode 100644 index 000000000..78dc465a7 --- /dev/null +++ b/nova/api/openstack/compute/ratelimiting/__init__.py @@ -0,0 +1,222 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Rate limiting of arbitrary actions.""" + +import httplib +import time +import urllib + +import webob.dec +import webob.exc + +from nova import wsgi +from nova.api.openstack import wsgi as os_wsgi + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + + +class RateLimitingMiddleware(wsgi.Middleware): + """Rate limit incoming requests according to the OpenStack rate limits.""" + + def __init__(self, application, service_host=None): + """Create a rate limiting middleware that wraps the given application. + + By default, rate counters are stored in memory. If service_host is + specified, the middleware instead relies on the ratelimiting.WSGIApp + at the given host+port to keep rate counters. + """ + if not service_host: + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = Limiter(limits={ + 'DELETE': (100, PER_MINUTE), + 'PUT': (10, PER_MINUTE), + 'POST': (10, PER_MINUTE), + 'POST servers': (50, PER_DAY), + 'GET changes-since': (3, PER_MINUTE), + }) + else: + self.limiter = WSGIAppProxy(service_host) + super(RateLimitingMiddleware, self).__init__(application) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + return self.rate_limited_request(req, self.application) + + def rate_limited_request(self, req, application): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + action_name = self.get_action_name(req) + if not action_name: + # Not rate limited + return application + delay = self.get_delay(action_name, + req.environ['nova.context'].user_id) + if delay: + # TODO(gundlach): Get the retry-after format correct. + exc = webob.exc.HTTPRequestEntityTooLarge( + explanation=('Too many requests.'), + headers={'Retry-After': time.time() + delay}) + raise os_wsgi.Fault(exc) + return application + + def get_delay(self, action_name, username): + """Return the delay for the given action and username, or None if + the action would not be rate limited. + """ + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + # Attempt the "POST" first, lest we are rate limited by "POST" but + # use up a precious "POST servers" call. + delay = self.limiter.perform("POST", username=username) + if delay: + return delay + return self.limiter.perform(action_name, username=username) + + def get_action_name(self, req): + """Return the action name for this request.""" + if req.method == 'GET' and 'changes-since' in req.GET: + return 'GET changes-since' + if req.method == 'POST' and req.path_info.startswith('/servers'): + return 'POST servers' + if req.method in ['PUT', 'POST', 'DELETE']: + return req.method + return None + + +class Limiter(object): + + """Class providing rate limiting of arbitrary actions.""" + + def __init__(self, limits): + """Create a rate limiter. + + limits: a dict mapping from action name to a tuple. The tuple contains + the number of times the action may be performed, and the time period + (in seconds) during which the number must not be exceeded for this + action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would + allow 10 'reboot' actions per minute. + """ + self.limits = limits + self._levels = {} + + def perform(self, action_name, username='nobody'): + """Attempt to perform an action by the given username. + + action_name: the string name of the action to perform. This must + be a key in the limits dict passed to the ctor. + + username: an optional string name of the user performing the action. + Each user has her own set of rate limiting counters. Defaults to + 'nobody' (so that if you never specify a username when calling + perform(), a single set of counters will be used.) + + Return None if the action may proceed. If the action may not proceed + because it has been rate limited, return the float number of seconds + until the action would succeed. + """ + # Think of rate limiting as a bucket leaking water at 1cc/second. The + # bucket can hold as many ccs as there are seconds in the rate + # limiting period (e.g. 3600 for per-hour ratelimits), and if you can + # perform N actions in that time, each action fills the bucket by + # 1/Nth of its volume. You may only perform an action if the bucket + # would not overflow. + now = time.time() + key = '%s:%s' % (username, action_name) + last_time_performed, water_level = self._levels.get(key, (now, 0)) + # The bucket leaks 1cc/second. + water_level -= (now - last_time_performed) + if water_level < 0: + water_level = 0 + num_allowed_per_period, period_in_secs = self.limits[action_name] + # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. + capacity = period_in_secs + new_level = water_level + (capacity * 1.0 / num_allowed_per_period) + if new_level > capacity: + # Delay this many seconds. + return new_level - capacity + self._levels[key] = (now, new_level) + return None + +# If one instance of this WSGIApps is unable to handle your load, put a +# sharding app in front that shards by username to one of many backends. + + +class WSGIApp(object): + + """Application that tracks rate limits in memory. Send requests to it of + this form: + + POST /limiter// + + and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header + containing the number of seconds to wait before the action would succeed. + """ + + def __init__(self, limiter): + """Create the WSGI application using the given Limiter instance.""" + self.limiter = limiter + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + parts = req.path_info.split('/') + # format: /limiter// + if req.method != 'POST': + raise webob.exc.HTTPMethodNotAllowed() + if len(parts) != 4 or parts[1] != 'limiter': + raise webob.exc.HTTPNotFound() + username = parts[2] + action_name = urllib.unquote(parts[3]) + delay = self.limiter.perform(action_name, username) + if delay: + return webob.exc.HTTPForbidden( + headers={'X-Wait-Seconds': "%.2f" % delay}) + else: + # 200 OK + return '' + + +class WSGIAppProxy(object): + + """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" + + def __init__(self, service_host): + """Creates a proxy pointing to a ratelimiting.WSGIApp at the given + host.""" + self.service_host = service_host + + def perform(self, action, username='nobody'): + conn = httplib.HTTPConnection(self.service_host) + conn.request('POST', '/limiter/%s/%s' % (username, action)) + resp = conn.getresponse() + if resp.status == 200: + # No delay + return None + return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/openstack/compute/schemas/atom-link.rng b/nova/api/openstack/compute/schemas/atom-link.rng new file mode 100644 index 000000000..edba5eee6 --- /dev/null +++ b/nova/api/openstack/compute/schemas/atom-link.rng @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + [^:]* + + + + + + .+/.+ + + + + + + [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* + + + + + + + + + + + + xml:base + xml:lang + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/atom.rng b/nova/api/openstack/compute/schemas/atom.rng new file mode 100644 index 000000000..c2df4e410 --- /dev/null +++ b/nova/api/openstack/compute/schemas/atom.rng @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text + html + + + + + + + + + xhtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An atom:feed must have an atom:author unless all of its atom:entry children have an atom:author. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An atom:entry must have at least one atom:link element with a rel attribute of 'alternate' or an atom:content. + + + An atom:entry must have an atom:author if its feed does not. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text + html + + + + + + + + + + + + + xhtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + [^:]* + + + + + + .+/.+ + + + + + + [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* + + + + + + + + + + .+@.+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + xml:base + xml:lang + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/addresses.rng b/nova/api/openstack/compute/schemas/v1.1/addresses.rng new file mode 100644 index 000000000..b498e8a63 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/addresses.rng @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/extension.rng b/nova/api/openstack/compute/schemas/v1.1/extension.rng new file mode 100644 index 000000000..336659755 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/extension.rng @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/extensions.rng b/nova/api/openstack/compute/schemas/v1.1/extensions.rng new file mode 100644 index 000000000..4d8bff646 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/extensions.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/flavor.rng b/nova/api/openstack/compute/schemas/v1.1/flavor.rng new file mode 100644 index 000000000..08746ce3d --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/flavor.rng @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/flavors.rng b/nova/api/openstack/compute/schemas/v1.1/flavors.rng new file mode 100644 index 000000000..b7a3acc01 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/flavors.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/flavors_index.rng b/nova/api/openstack/compute/schemas/v1.1/flavors_index.rng new file mode 100644 index 000000000..d1a4fedb1 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/flavors_index.rng @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/image.rng b/nova/api/openstack/compute/schemas/v1.1/image.rng new file mode 100644 index 000000000..505081fba --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/image.rng @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/images.rng b/nova/api/openstack/compute/schemas/v1.1/images.rng new file mode 100644 index 000000000..064d4d9cc --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/images.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/images_index.rng b/nova/api/openstack/compute/schemas/v1.1/images_index.rng new file mode 100644 index 000000000..3db0b2672 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/images_index.rng @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/limits.rng b/nova/api/openstack/compute/schemas/v1.1/limits.rng new file mode 100644 index 000000000..1af8108ec --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/limits.rng @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/metadata.rng b/nova/api/openstack/compute/schemas/v1.1/metadata.rng new file mode 100644 index 000000000..b2f5d702a --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/metadata.rng @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/server.rng b/nova/api/openstack/compute/schemas/v1.1/server.rng new file mode 100644 index 000000000..07fa16daa --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/server.rng @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/servers.rng b/nova/api/openstack/compute/schemas/v1.1/servers.rng new file mode 100644 index 000000000..4e2bb8853 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/servers.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/servers_index.rng b/nova/api/openstack/compute/schemas/v1.1/servers_index.rng new file mode 100644 index 000000000..023e4b66a --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/servers_index.rng @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/version.rng b/nova/api/openstack/compute/schemas/v1.1/version.rng new file mode 100644 index 000000000..ae76270ba --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/version.rng @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/versions.rng b/nova/api/openstack/compute/schemas/v1.1/versions.rng new file mode 100644 index 000000000..8b2cc7f71 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/versions.rng @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/nova/api/openstack/compute/server_metadata.py b/nova/api/openstack/compute/server_metadata.py new file mode 100644 index 000000000..52a90f96e --- /dev/null +++ b/nova/api/openstack/compute/server_metadata.py @@ -0,0 +1,175 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import wsgi +from nova import compute +from nova import exception + + +class Controller(object): + """ The server metadata API controller for the Openstack API """ + + def __init__(self): + self.compute_api = compute.API() + super(Controller, self).__init__() + + def _get_metadata(self, context, server_id): + try: + server = self.compute_api.get(context, server_id) + meta = self.compute_api.get_instance_metadata(context, server) + except exception.InstanceNotFound: + msg = _('Server does not exist') + raise exc.HTTPNotFound(explanation=msg) + + meta_dict = {} + for key, value in meta.iteritems(): + meta_dict[key] = value + return meta_dict + + @wsgi.serializers(xml=common.MetadataTemplate) + def index(self, req, server_id): + """ Returns the list of metadata for a given instance """ + context = req.environ['nova.context'] + return {'metadata': self._get_metadata(context, server_id)} + + @wsgi.serializers(xml=common.MetadataTemplate) + @wsgi.deserializers(xml=common.MetadataDeserializer) + def create(self, req, server_id, body): + try: + metadata = body['metadata'] + except (KeyError, TypeError): + msg = _("Malformed request body") + raise exc.HTTPBadRequest(explanation=msg) + + context = req.environ['nova.context'] + + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + delete=False) + + return {'metadata': new_metadata} + + @wsgi.serializers(xml=common.MetaItemTemplate) + @wsgi.deserializers(xml=common.MetaItemDeserializer) + def update(self, req, server_id, id, body): + try: + meta_item = body['meta'] + except (TypeError, KeyError): + expl = _('Malformed request body') + raise exc.HTTPBadRequest(explanation=expl) + + try: + meta_value = meta_item[id] + except (AttributeError, KeyError): + expl = _('Request body and URI mismatch') + raise exc.HTTPBadRequest(explanation=expl) + + if len(meta_item) > 1: + expl = _('Request body contains too many items') + raise exc.HTTPBadRequest(explanation=expl) + + context = req.environ['nova.context'] + self._update_instance_metadata(context, + server_id, + meta_item, + delete=False) + + return {'meta': meta_item} + + @wsgi.serializers(xml=common.MetadataTemplate) + @wsgi.deserializers(xml=common.MetadataDeserializer) + def update_all(self, req, server_id, body): + try: + metadata = body['metadata'] + except (TypeError, KeyError): + expl = _('Malformed request body') + raise exc.HTTPBadRequest(explanation=expl) + + context = req.environ['nova.context'] + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + delete=True) + + return {'metadata': new_metadata} + + def _update_instance_metadata(self, context, server_id, metadata, + delete=False): + try: + server = self.compute_api.get(context, server_id) + return self.compute_api.update_instance_metadata(context, + server, + metadata, + delete) + + except exception.InstanceNotFound: + msg = _('Server does not exist') + raise exc.HTTPNotFound(explanation=msg) + + except (ValueError, AttributeError): + msg = _("Malformed request body") + raise exc.HTTPBadRequest(explanation=msg) + + except exception.QuotaError as error: + self._handle_quota_error(error) + + @wsgi.serializers(xml=common.MetaItemTemplate) + def show(self, req, server_id, id): + """ Return a single metadata item """ + context = req.environ['nova.context'] + data = self._get_metadata(context, server_id) + + try: + return {'meta': {id: data[id]}} + except KeyError: + msg = _("Metadata item was not found") + raise exc.HTTPNotFound(explanation=msg) + + @wsgi.response(204) + def delete(self, req, server_id, id): + """ Deletes an existing metadata """ + context = req.environ['nova.context'] + + metadata = self._get_metadata(context, server_id) + + try: + meta_value = metadata[id] + except KeyError: + msg = _("Metadata item was not found") + raise exc.HTTPNotFound(explanation=msg) + + try: + server = self.compute_api.get(context, server_id) + self.compute_api.delete_instance_metadata(context, server, id) + except exception.InstanceNotFound: + msg = _('Server does not exist') + raise exc.HTTPNotFound(explanation=msg) + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions.""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPRequestEntityTooLarge(explanation=error.message, + headers={'Retry-After': 0}) + raise error + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py new file mode 100644 index 000000000..60b85f591 --- /dev/null +++ b/nova/api/openstack/compute/servers.py @@ -0,0 +1,1123 @@ +# Copyright 2010 OpenStack LLC. +# 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 os +from xml.dom import minidom + +from webob import exc +import webob + +from nova.api.openstack import common +from nova.api.openstack.compute import ips +from nova.api.openstack.compute.views import servers as views_servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova.compute import instance_types +from nova import exception +from nova import flags +from nova import log as logging +from nova.rpc import common as rpc_common +from nova.scheduler import api as scheduler_api +from nova import utils + + +LOG = logging.getLogger('nova.api.openstack.compute.servers') +FLAGS = flags.FLAGS + + +class SecurityGroupsTemplateElement(xmlutil.TemplateElement): + def will_render(self, datum): + return 'security_groups' in datum + + +def make_fault(elem): + fault = xmlutil.SubTemplateElement(elem, 'fault', selector='fault') + fault.set('code') + fault.set('created') + msg = xmlutil.SubTemplateElement(fault, 'message') + msg.text = 'message' + det = xmlutil.SubTemplateElement(fault, 'details') + det.text = 'details' + + +def make_server(elem, detailed=False): + elem.set('name') + elem.set('id') + + if detailed: + elem.set('userId', 'user_id') + elem.set('tenantId', 'tenant_id') + elem.set('updated') + elem.set('created') + elem.set('hostId') + elem.set('accessIPv4') + elem.set('accessIPv6') + elem.set('status') + elem.set('progress') + + # Attach image node + image = xmlutil.SubTemplateElement(elem, 'image', selector='image') + image.set('id') + xmlutil.make_links(image, 'links') + + # Attach flavor node + flavor = xmlutil.SubTemplateElement(elem, 'flavor', selector='flavor') + flavor.set('id') + xmlutil.make_links(flavor, 'links') + + # Attach fault node + make_fault(elem) + + # Attach metadata node + elem.append(common.MetadataTemplate()) + + # Attach addresses node + elem.append(ips.AddressesTemplate()) + + # Attach security groups node + secgrps = SecurityGroupsTemplateElement('security_groups') + elem.append(secgrps) + secgrp = xmlutil.SubTemplateElement(secgrps, 'security_group', + selector='security_groups') + secgrp.set('name') + + xmlutil.make_links(elem, 'links') + + +server_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class ServerTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server', selector='server') + make_server(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) + + +class MinimalServersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + make_server(elem) + xmlutil.make_links(root, 'servers_links') + return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) + + +class ServersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + make_server(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) + + +class ServerAdminPassTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server') + root.set('adminPass') + return xmlutil.SlaveTemplate(root, 1, nsmap=server_nsmap) + + +def FullServerTemplate(): + master = ServerTemplate() + master.attach(ServerAdminPassTemplate()) + return master + + +class CommonDeserializer(wsgi.MetadataXMLDeserializer): + """ + Common deserializer to handle xml-formatted server create + requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ + + metadata_deserializer = common.MetadataXMLDeserializer() + + def _extract_personality(self, server_node): + """Marshal the personality attribute of a parsed request""" + node = self.find_first_child_named(server_node, "personality") + if node is not None: + personality = [] + for file_node in self.find_children_named(node, "file"): + item = {} + if file_node.hasAttribute("path"): + item["path"] = file_node.getAttribute("path") + item["contents"] = self.extract_text(file_node) + personality.append(item) + return personality + else: + return None + + def _extract_server(self, node): + """Marshal the server attribute of a parsed request""" + server = {} + server_node = self.find_first_child_named(node, 'server') + + attributes = ["name", "imageRef", "flavorRef", "adminPass", + "accessIPv4", "accessIPv6"] + for attr in attributes: + if server_node.getAttribute(attr): + server[attr] = server_node.getAttribute(attr) + + metadata_node = self.find_first_child_named(server_node, "metadata") + if metadata_node is not None: + server["metadata"] = self.extract_metadata(metadata_node) + + personality = self._extract_personality(server_node) + if personality is not None: + server["personality"] = personality + + networks = self._extract_networks(server_node) + if networks is not None: + server["networks"] = networks + + security_groups = self._extract_security_groups(server_node) + if security_groups is not None: + server["security_groups"] = security_groups + + auto_disk_config = server_node.getAttribute('auto_disk_config') + if auto_disk_config: + server['auto_disk_config'] = utils.bool_from_str(auto_disk_config) + + return server + + def _extract_networks(self, server_node): + """Marshal the networks attribute of a parsed request""" + node = self.find_first_child_named(server_node, "networks") + if node is not None: + networks = [] + for network_node in self.find_children_named(node, + "network"): + item = {} + if network_node.hasAttribute("uuid"): + item["uuid"] = network_node.getAttribute("uuid") + if network_node.hasAttribute("fixed_ip"): + item["fixed_ip"] = network_node.getAttribute("fixed_ip") + networks.append(item) + return networks + else: + return None + + def _extract_security_groups(self, server_node): + """Marshal the security_groups attribute of a parsed request""" + node = self.find_first_child_named(server_node, "security_groups") + if node is not None: + security_groups = [] + for sg_node in self.find_children_named(node, "security_group"): + item = {} + name_node = self.find_first_child_named(sg_node, "name") + if name_node: + item["name"] = self.extract_text(name_node) + security_groups.append(item) + return security_groups + else: + return None + + +class ActionDeserializer(CommonDeserializer): + """ + Deserializer to handle xml-formatted server action requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ + + def default(self, string): + dom = minidom.parseString(string) + action_node = dom.childNodes[0] + action_name = action_node.tagName + + action_deserializer = { + 'createImage': self._action_create_image, + 'changePassword': self._action_change_password, + 'reboot': self._action_reboot, + 'rebuild': self._action_rebuild, + 'resize': self._action_resize, + 'confirmResize': self._action_confirm_resize, + 'revertResize': self._action_revert_resize, + }.get(action_name, super(ActionDeserializer, self).default) + + action_data = action_deserializer(action_node) + + return {'body': {action_name: action_data}} + + def _action_create_image(self, node): + return self._deserialize_image_action(node, ('name',)) + + def _action_change_password(self, node): + if not node.hasAttribute("adminPass"): + raise AttributeError("No adminPass was specified in request") + return {"adminPass": node.getAttribute("adminPass")} + + def _action_reboot(self, node): + if not node.hasAttribute("type"): + raise AttributeError("No reboot type was specified in request") + return {"type": node.getAttribute("type")} + + def _action_rebuild(self, node): + rebuild = {} + if node.hasAttribute("name"): + rebuild['name'] = node.getAttribute("name") + + metadata_node = self.find_first_child_named(node, "metadata") + if metadata_node is not None: + rebuild["metadata"] = self.extract_metadata(metadata_node) + + personality = self._extract_personality(node) + if personality is not None: + rebuild["personality"] = personality + + if not node.hasAttribute("imageRef"): + raise AttributeError("No imageRef was specified in request") + rebuild["imageRef"] = node.getAttribute("imageRef") + + return rebuild + + def _action_resize(self, node): + if not node.hasAttribute("flavorRef"): + raise AttributeError("No flavorRef was specified in request") + return {"flavorRef": node.getAttribute("flavorRef")} + + def _action_confirm_resize(self, node): + return None + + def _action_revert_resize(self, node): + return None + + def _deserialize_image_action(self, node, allowed_attributes): + data = {} + for attribute in allowed_attributes: + value = node.getAttribute(attribute) + if value: + data[attribute] = value + metadata_node = self.find_first_child_named(node, 'metadata') + if metadata_node is not None: + metadata = self.metadata_deserializer.extract_metadata( + metadata_node) + data['metadata'] = metadata + return data + + +class CreateDeserializer(CommonDeserializer): + """ + Deserializer to handle xml-formatted server create requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ + + def default(self, string): + """Deserialize an xml-formatted server create request""" + dom = minidom.parseString(string) + server = self._extract_server(dom) + return {'body': {'server': server}} + + +class Controller(wsgi.Controller): + """ The Server API base controller class for the OpenStack API """ + + _view_builder_class = views_servers.ViewBuilder + + @staticmethod + def _add_location(robj): + # Just in case... + if 'server' not in robj.obj: + return robj + + link = filter(lambda l: l['rel'] == 'self', + robj.obj['server']['links']) + if link: + robj['Location'] = link[0]['href'] + + # Convenience return + return robj + + def __init__(self, **kwargs): + super(Controller, self).__init__(**kwargs) + self.compute_api = compute.API() + + @wsgi.serializers(xml=MinimalServersTemplate) + def index(self, req): + """ Returns a list of server names and ids for a given user """ + try: + servers = self._get_servers(req, is_detail=False) + except exception.Invalid as err: + raise exc.HTTPBadRequest(explanation=str(err)) + except exception.NotFound: + raise exc.HTTPNotFound() + return servers + + @wsgi.serializers(xml=ServersTemplate) + def detail(self, req): + """ Returns a list of server details for a given user """ + try: + servers = self._get_servers(req, is_detail=True) + except exception.Invalid as err: + raise exc.HTTPBadRequest(explanation=str(err)) + except exception.NotFound as err: + raise exc.HTTPNotFound() + return servers + + def _get_block_device_mapping(self, data): + """Get block_device_mapping from 'server' dictionary. + Overridden by volumes controller. + """ + return None + + def _add_instance_faults(self, ctxt, instances): + faults = self.compute_api.get_instance_faults(ctxt, instances) + if faults is not None: + for instance in instances: + faults_list = faults.get(instance['uuid'], []) + try: + instance['fault'] = faults_list[0] + except IndexError: + pass + + return instances + + def _get_servers(self, req, is_detail): + """Returns a list of servers, taking into account any search + options specified. + """ + + search_opts = {} + search_opts.update(req.str_GET) + + context = req.environ['nova.context'] + remove_invalid_options(context, search_opts, + self._get_server_search_options()) + + # Convert local_zone_only into a boolean + search_opts['local_zone_only'] = utils.bool_from_str( + search_opts.get('local_zone_only', False)) + + # If search by 'status', we need to convert it to 'vm_state' + # to pass on to child zones. + if 'status' in search_opts: + status = search_opts['status'] + state = common.vm_state_from_status(status) + if state is None: + reason = _('Invalid server status: %(status)s') % locals() + raise exception.InvalidInput(reason=reason) + search_opts['vm_state'] = state + + if 'changes-since' in search_opts: + try: + parsed = utils.parse_isotime(search_opts['changes-since']) + except ValueError: + msg = _('Invalid changes-since value') + raise exc.HTTPBadRequest(explanation=msg) + search_opts['changes-since'] = parsed + + # By default, compute's get_all() will return deleted instances. + # If an admin hasn't specified a 'deleted' search option, we need + # to filter out deleted instances by setting the filter ourselves. + # ... Unless 'changes-since' is specified, because 'changes-since' + # should return recently deleted images according to the API spec. + + if 'deleted' not in search_opts: + if 'changes-since' not in search_opts: + # No 'changes-since', so we only want non-deleted servers + search_opts['deleted'] = False + + instance_list = self.compute_api.get_all(context, + search_opts=search_opts) + + limited_list = self._limit_items(instance_list, req) + if is_detail: + self._add_instance_faults(context, limited_list) + return self._view_builder.detail(req, limited_list) + else: + return self._view_builder.index(req, limited_list) + + def _get_server(self, context, instance_uuid): + """Utility function for looking up an instance by uuid""" + try: + return self.compute_api.routing_get(context, instance_uuid) + except exception.NotFound: + raise exc.HTTPNotFound() + + def _handle_quota_error(self, error): + """ + Reraise quota errors as api-specific http exceptions + """ + + code_mappings = { + "OnsetFileLimitExceeded": + _("Personality file limit exceeded"), + "OnsetFilePathLimitExceeded": + _("Personality file path too long"), + "OnsetFileContentLimitExceeded": + _("Personality file content too long"), + + # NOTE(bcwaldon): expose the message generated below in order + # to better explain how the quota was exceeded + "InstanceLimitExceeded": error.message, + } + + expl = code_mappings.get(error.code) + if expl: + raise exc.HTTPRequestEntityTooLarge(explanation=expl, + headers={'Retry-After': 0}) + # if the original error is okay, just reraise it + raise error + + def _validate_server_name(self, value): + if not isinstance(value, basestring): + msg = _("Server name is not a string or unicode") + raise exc.HTTPBadRequest(explanation=msg) + + if value.strip() == '': + msg = _("Server name is an empty string") + raise exc.HTTPBadRequest(explanation=msg) + + def _get_injected_files(self, personality): + """ + Create a list of injected files from the personality attribute + + At this time, injected_files must be formatted as a list of + (file_path, file_content) pairs for compatibility with the + underlying compute service. + """ + injected_files = [] + + for item in personality: + try: + path = item['path'] + contents = item['contents'] + except KeyError as key: + expl = _('Bad personality format: missing %s') % key + raise exc.HTTPBadRequest(explanation=expl) + except TypeError: + expl = _('Bad personality format') + raise exc.HTTPBadRequest(explanation=expl) + try: + contents = base64.b64decode(contents) + except TypeError: + expl = _('Personality content for %s cannot be decoded') % path + raise exc.HTTPBadRequest(explanation=expl) + injected_files.append((path, contents)) + return injected_files + + def _get_requested_networks(self, requested_networks): + """ + Create a list of requested networks from the networks attribute + """ + networks = [] + for network in requested_networks: + try: + network_uuid = network['uuid'] + + if not utils.is_uuid_like(network_uuid): + msg = _("Bad networks format: network uuid is not in" + " proper format (%s)") % network_uuid + raise exc.HTTPBadRequest(explanation=msg) + + #fixed IP address is optional + #if the fixed IP address is not provided then + #it will use one of the available IP address from the network + address = network.get('fixed_ip', None) + if address is not None and not utils.is_valid_ipv4(address): + msg = _("Invalid fixed IP address (%s)") % address + raise exc.HTTPBadRequest(explanation=msg) + # check if the network id is already present in the list, + # we don't want duplicate networks to be passed + # at the boot time + for id, ip in networks: + if id == network_uuid: + expl = _("Duplicate networks (%s) are not allowed")\ + % network_uuid + raise exc.HTTPBadRequest(explanation=expl) + + networks.append((network_uuid, address)) + except KeyError as key: + expl = _('Bad network format: missing %s') % key + raise exc.HTTPBadRequest(explanation=expl) + except TypeError: + expl = _('Bad networks format') + raise exc.HTTPBadRequest(explanation=expl) + + return networks + + def _validate_user_data(self, user_data): + """Check if the user_data is encoded properly""" + if not user_data: + return + try: + user_data = base64.b64decode(user_data) + except TypeError: + expl = _('Userdata content cannot be decoded') + raise exc.HTTPBadRequest(explanation=expl) + + @wsgi.serializers(xml=ServerTemplate) + @exception.novaclient_converter + @scheduler_api.redirect_handler + def show(self, req, id): + """ Returns server details by server id """ + try: + context = req.environ['nova.context'] + instance = self.compute_api.routing_get(context, id) + self._add_instance_faults(context, [instance]) + return self._view_builder.show(req, instance) + except exception.NotFound: + raise exc.HTTPNotFound() + + @wsgi.response(202) + @wsgi.serializers(xml=FullServerTemplate) + @wsgi.deserializers(xml=CreateDeserializer) + def create(self, req, body): + """ Creates a new server for a given user """ + if not body: + raise exc.HTTPUnprocessableEntity() + + if not 'server' in body: + raise exc.HTTPUnprocessableEntity() + + body['server']['key_name'] = self._get_key_name(req, body) + + context = req.environ['nova.context'] + server_dict = body['server'] + password = self._get_server_admin_password(server_dict) + + if not 'name' in server_dict: + msg = _("Server name is not defined") + raise exc.HTTPBadRequest(explanation=msg) + + name = server_dict['name'] + self._validate_server_name(name) + name = name.strip() + + image_href = self._image_ref_from_req_data(body) + + # If the image href was generated by nova api, strip image_href + # down to an id and use the default glance connection params + if str(image_href).startswith(req.application_url): + image_href = image_href.split('/').pop() + + personality = server_dict.get('personality') + config_drive = server_dict.get('config_drive') + + injected_files = [] + if personality: + injected_files = self._get_injected_files(personality) + + sg_names = [] + security_groups = server_dict.get('security_groups') + if security_groups is not None: + sg_names = [sg['name'] for sg in security_groups if sg.get('name')] + if not sg_names: + sg_names.append('default') + + sg_names = list(set(sg_names)) + + requested_networks = server_dict.get('networks') + if requested_networks is not None: + requested_networks = self._get_requested_networks( + requested_networks) + + try: + flavor_id = self._flavor_id_from_req_data(body) + except ValueError as error: + msg = _("Invalid flavorRef provided.") + raise exc.HTTPBadRequest(explanation=msg) + + zone_blob = server_dict.get('blob') + + # optional openstack extensions: + key_name = server_dict.get('key_name') + user_data = server_dict.get('user_data') + self._validate_user_data(user_data) + + availability_zone = server_dict.get('availability_zone') + name = server_dict['name'] + self._validate_server_name(name) + name = name.strip() + + block_device_mapping = self._get_block_device_mapping(server_dict) + + # Only allow admins to specify their own reservation_ids + # This is really meant to allow zones to work. + reservation_id = server_dict.get('reservation_id') + if all([reservation_id is not None, + reservation_id != '', + not context.is_admin]): + reservation_id = None + + ret_resv_id = server_dict.get('return_reservation_id', False) + + min_count = server_dict.get('min_count') + max_count = server_dict.get('max_count') + # min_count and max_count are optional. If they exist, they come + # in as strings. We want to default 'min_count' to 1, and default + # 'max_count' to be 'min_count'. + min_count = int(min_count) if min_count else 1 + max_count = int(max_count) if max_count else min_count + if min_count > max_count: + min_count = max_count + + auto_disk_config = server_dict.get('auto_disk_config') + + try: + inst_type = \ + instance_types.get_instance_type_by_flavor_id(flavor_id) + + (instances, resv_id) = self.compute_api.create(context, + inst_type, + image_href, + display_name=name, + display_description=name, + key_name=key_name, + metadata=server_dict.get('metadata', {}), + access_ip_v4=server_dict.get('accessIPv4'), + access_ip_v6=server_dict.get('accessIPv6'), + injected_files=injected_files, + admin_password=password, + zone_blob=zone_blob, + reservation_id=reservation_id, + min_count=min_count, + max_count=max_count, + requested_networks=requested_networks, + security_group=sg_names, + user_data=user_data, + availability_zone=availability_zone, + config_drive=config_drive, + block_device_mapping=block_device_mapping, + auto_disk_config=auto_disk_config) + except exception.QuotaError as error: + self._handle_quota_error(error) + except exception.InstanceTypeMemoryTooSmall as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + except exception.InstanceTypeDiskTooSmall as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + except exception.ImageNotFound as error: + msg = _("Can not find requested image") + raise exc.HTTPBadRequest(explanation=msg) + except exception.FlavorNotFound as error: + msg = _("Invalid flavorRef provided.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.KeypairNotFound as error: + msg = _("Invalid key_name provided.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.SecurityGroupNotFound as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + except rpc_common.RemoteError as err: + msg = "%(err_type)s: %(err_msg)s" % \ + {'err_type': err.exc_type, 'err_msg': err.value} + raise exc.HTTPBadRequest(explanation=msg) + # Let the caller deal with unhandled exceptions. + + # If the caller wanted a reservation_id, return it + if ret_resv_id: + return {'reservation_id': resv_id} + + server = self._view_builder.create(req, instances[0]) + + if '_is_precooked' in server['server'].keys(): + del server['server']['_is_precooked'] + else: + server['server']['adminPass'] = password + + robj = wsgi.ResponseObject(server) + + return self._add_location(robj) + + def _delete(self, context, id): + instance = self._get_server(context, id) + if FLAGS.reclaim_instance_interval: + self.compute_api.soft_delete(context, instance) + else: + self.compute_api.delete(context, instance) + + @wsgi.serializers(xml=ServerTemplate) + @scheduler_api.redirect_handler + def update(self, req, id, body): + """Update server then pass on to version-specific controller""" + if len(req.body) == 0: + raise exc.HTTPUnprocessableEntity() + + if not body: + raise exc.HTTPUnprocessableEntity() + + ctxt = req.environ['nova.context'] + update_dict = {} + + if 'name' in body['server']: + name = body['server']['name'] + self._validate_server_name(name) + update_dict['display_name'] = name.strip() + + if 'accessIPv4' in body['server']: + access_ipv4 = body['server']['accessIPv4'] + update_dict['access_ip_v4'] = access_ipv4.strip() + + if 'accessIPv6' in body['server']: + access_ipv6 = body['server']['accessIPv6'] + update_dict['access_ip_v6'] = access_ipv6.strip() + + if 'auto_disk_config' in body['server']: + auto_disk_config = utils.bool_from_str( + body['server']['auto_disk_config']) + update_dict['auto_disk_config'] = auto_disk_config + + instance = self.compute_api.routing_get(ctxt, id) + + try: + self.compute_api.update(ctxt, instance, **update_dict) + except exception.NotFound: + raise exc.HTTPNotFound() + + instance.update(update_dict) + + self._add_instance_faults(ctxt, [instance]) + return self._view_builder.show(req, instance) + + @wsgi.response(202) + @wsgi.serializers(xml=FullServerTemplate) + @wsgi.deserializers(xml=ActionDeserializer) + @exception.novaclient_converter + @scheduler_api.redirect_handler + def action(self, req, id, body): + """Multi-purpose method used to take actions on a server""" + _actions = { + 'changePassword': self._action_change_password, + 'reboot': self._action_reboot, + 'resize': self._action_resize, + 'confirmResize': self._action_confirm_resize, + 'revertResize': self._action_revert_resize, + 'rebuild': self._action_rebuild, + 'createImage': self._action_create_image, + } + + for key in body: + if key in _actions: + return _actions[key](body, req, id) + else: + msg = _("There is no such server action: %s") % (key,) + raise exc.HTTPBadRequest(explanation=msg) + + msg = _("Invalid request body") + raise exc.HTTPBadRequest(explanation=msg) + + def _action_confirm_resize(self, input_dict, req, id): + context = req.environ['nova.context'] + instance = self._get_server(context, id) + try: + self.compute_api.confirm_resize(context, instance) + except exception.MigrationNotFound: + msg = _("Instance has not been resized.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'confirmResize') + except Exception, e: + LOG.exception(_("Error in confirm-resize %s"), e) + raise exc.HTTPBadRequest() + return exc.HTTPNoContent() + + def _action_revert_resize(self, input_dict, req, id): + context = req.environ['nova.context'] + instance = self._get_server(context, id) + try: + self.compute_api.revert_resize(context, instance) + except exception.MigrationNotFound: + msg = _("Instance has not been resized.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'revertResize') + except Exception, e: + LOG.exception(_("Error in revert-resize %s"), e) + raise exc.HTTPBadRequest() + return webob.Response(status_int=202) + + def _action_reboot(self, input_dict, req, id): + if 'reboot' in input_dict and 'type' in input_dict['reboot']: + valid_reboot_types = ['HARD', 'SOFT'] + reboot_type = input_dict['reboot']['type'].upper() + if not valid_reboot_types.count(reboot_type): + msg = _("Argument 'type' for reboot is not HARD or SOFT") + LOG.exception(msg) + raise exc.HTTPBadRequest(explanation=msg) + else: + msg = _("Missing argument 'type' for reboot") + LOG.exception(msg) + raise exc.HTTPBadRequest(explanation=msg) + + context = req.environ['nova.context'] + instance = self._get_server(context, id) + + try: + self.compute_api.reboot(context, instance, reboot_type) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'reboot') + except Exception, e: + LOG.exception(_("Error in reboot %s"), e) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + def _resize(self, req, instance_id, flavor_id): + """Begin the resize process with given instance/flavor.""" + context = req.environ["nova.context"] + instance = self._get_server(context, instance_id) + + try: + self.compute_api.resize(context, instance, flavor_id) + except exception.FlavorNotFound: + msg = _("Unable to locate requested flavor.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.CannotResizeToSameSize: + msg = _("Resize requires a change in size.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'resize') + + return webob.Response(status_int=202) + + @wsgi.response(204) + @exception.novaclient_converter + @scheduler_api.redirect_handler + def delete(self, req, id): + """ Destroys a server """ + try: + self._delete(req.environ['nova.context'], id) + except exception.NotFound: + raise exc.HTTPNotFound() + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'delete') + + def _get_key_name(self, req, body): + if 'server' in body: + try: + return body['server'].get('key_name') + except AttributeError: + msg = _("Malformed server entity") + raise exc.HTTPBadRequest(explanation=msg) + + def _image_ref_from_req_data(self, data): + try: + return data['server']['imageRef'] + except (TypeError, KeyError): + msg = _("Missing imageRef attribute") + raise exc.HTTPBadRequest(explanation=msg) + + def _flavor_id_from_req_data(self, data): + try: + flavor_ref = data['server']['flavorRef'] + except (TypeError, KeyError): + msg = _("Missing flavorRef attribute") + raise exc.HTTPBadRequest(explanation=msg) + + return common.get_id_from_href(flavor_ref) + + def _action_change_password(self, input_dict, req, id): + context = req.environ['nova.context'] + if (not 'changePassword' in input_dict + or not 'adminPass' in input_dict['changePassword']): + msg = _("No adminPass was specified") + raise exc.HTTPBadRequest(explanation=msg) + password = input_dict['changePassword']['adminPass'] + if not isinstance(password, basestring) or password == '': + msg = _("Invalid adminPass") + raise exc.HTTPBadRequest(explanation=msg) + server = self._get_server(context, id) + self.compute_api.set_admin_password(context, server, password) + return webob.Response(status_int=202) + + def _limit_items(self, items, req): + return common.limited_by_marker(items, req) + + def _validate_metadata(self, metadata): + """Ensure that we can work with the metadata given.""" + try: + metadata.iteritems() + except AttributeError as ex: + msg = _("Unable to parse metadata key/value pairs.") + LOG.debug(msg) + raise exc.HTTPBadRequest(explanation=msg) + + def _action_resize(self, input_dict, req, id): + """ Resizes a given instance to the flavor size requested """ + try: + flavor_ref = input_dict["resize"]["flavorRef"] + if not flavor_ref: + msg = _("Resize request has invalid 'flavorRef' attribute.") + raise exc.HTTPBadRequest(explanation=msg) + except (KeyError, TypeError): + msg = _("Resize requests require 'flavorRef' attribute.") + raise exc.HTTPBadRequest(explanation=msg) + + return self._resize(req, id, flavor_ref) + + def _action_rebuild(self, info, request, instance_id): + """Rebuild an instance with the given attributes""" + try: + body = info['rebuild'] + except (KeyError, TypeError): + raise exc.HTTPBadRequest(_("Invalid request body")) + + try: + image_href = body["imageRef"] + except (KeyError, TypeError): + msg = _("Could not parse imageRef from request.") + raise exc.HTTPBadRequest(explanation=msg) + + try: + password = body['adminPass'] + except (KeyError, TypeError): + password = utils.generate_password(FLAGS.password_length) + + context = request.environ['nova.context'] + instance = self._get_server(context, instance_id) + + attr_map = { + 'personality': 'files_to_inject', + 'name': 'display_name', + 'accessIPv4': 'access_ip_v4', + 'accessIPv6': 'access_ip_v6', + 'metadata': 'metadata', + } + + kwargs = {} + + for request_attribute, instance_attribute in attr_map.items(): + try: + kwargs[instance_attribute] = body[request_attribute] + except (KeyError, TypeError): + pass + + self._validate_metadata(kwargs.get('metadata', {})) + + if 'files_to_inject' in kwargs: + personality = kwargs['files_to_inject'] + kwargs['files_to_inject'] = self._get_injected_files(personality) + + try: + self.compute_api.rebuild(context, + instance, + image_href, + password, + **kwargs) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'rebuild') + except exception.InstanceNotFound: + msg = _("Instance could not be found") + raise exc.HTTPNotFound(explanation=msg) + + instance = self._get_server(context, instance_id) + + self._add_instance_faults(context, [instance]) + view = self._view_builder.show(request, instance) + + # Add on the adminPass attribute since the view doesn't do it + view['server']['adminPass'] = password + + robj = wsgi.ResponseObject(view) + return self._add_location(robj) + + @common.check_snapshots_enabled + def _action_create_image(self, input_dict, req, instance_id): + """Snapshot a server instance.""" + context = req.environ['nova.context'] + entity = input_dict.get("createImage", {}) + + try: + image_name = entity["name"] + + except KeyError: + msg = _("createImage entity requires name attribute") + raise exc.HTTPBadRequest(explanation=msg) + + except TypeError: + msg = _("Malformed createImage entity") + raise exc.HTTPBadRequest(explanation=msg) + + # preserve link to server in image properties + server_ref = os.path.join(req.application_url, 'servers', instance_id) + props = {'instance_ref': server_ref} + + metadata = entity.get('metadata', {}) + common.check_img_metadata_quota_limit(context, metadata) + try: + props.update(metadata) + except ValueError: + msg = _("Invalid metadata") + raise exc.HTTPBadRequest(explanation=msg) + + instance = self._get_server(context, instance_id) + + try: + image = self.compute_api.snapshot(context, + instance, + image_name, + extra_properties=props) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'createImage') + + # build location of newly-created image entity + image_id = str(image['id']) + image_ref = os.path.join(req.application_url, + context.project_id, + 'images', + image_id) + + resp = webob.Response(status_int=202) + resp.headers['Location'] = image_ref + return resp + + def _get_server_admin_password(self, server): + """ Determine the admin password for a server on creation """ + password = server.get('adminPass') + + if password is None: + return utils.generate_password(FLAGS.password_length) + if not isinstance(password, basestring) or password == '': + msg = _("Invalid adminPass") + raise exc.HTTPBadRequest(explanation=msg) + return password + + def _get_server_search_options(self): + """Return server search options allowed by non-admin""" + return ('reservation_id', 'name', 'local_zone_only', + 'status', 'image', 'flavor', 'changes-since') + + +def create_resource(): + return wsgi.Resource(Controller()) + + +def remove_invalid_options(context, search_options, allowed_search_options): + """Remove search options that are not valid for non-admin API/context""" + if FLAGS.allow_admin_api and context.is_admin: + # Allow all options + return + # Otherwise, strip out all unknown options + unknown_options = [opt for opt in search_options + if opt not in allowed_search_options] + unk_opt_str = ", ".join(unknown_options) + log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals() + LOG.debug(log_msg) + for opt in unknown_options: + search_options.pop(opt, None) diff --git a/nova/api/openstack/compute/versions.py b/nova/api/openstack/compute/versions.py new file mode 100644 index 000000000..ddc31e9f0 --- /dev/null +++ b/nova/api/openstack/compute/versions.py @@ -0,0 +1,236 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from datetime import datetime + +from lxml import etree + +from nova.api.openstack.compute.views import versions as views_versions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil + + +VERSIONS = { + "v2.0": { + "id": "v2.0", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf", + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml;version=2", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=2", + } + ], + } +} + + +class MediaTypesTemplateElement(xmlutil.TemplateElement): + def will_render(self, datum): + return 'media-types' in datum + + +def make_version(elem): + elem.set('id') + elem.set('status') + elem.set('updated') + + mts = MediaTypesTemplateElement('media-types') + elem.append(mts) + + mt = xmlutil.SubTemplateElement(mts, 'media-type', selector='media-types') + mt.set('base') + mt.set('type') + + xmlutil.make_links(elem, 'links') + + +version_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class VersionTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('version', selector='version') + make_version(root) + return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) + + +class VersionsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('versions') + elem = xmlutil.SubTemplateElement(root, 'version', selector='versions') + make_version(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) + + +class ChoicesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('choices') + elem = xmlutil.SubTemplateElement(root, 'version', selector='choices') + make_version(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) + + +class AtomSerializer(wsgi.XMLDictSerializer): + + NSMAP = {None: xmlutil.XMLNS_ATOM} + + def __init__(self, metadata=None, xmlns=None): + self.metadata = metadata or {} + if not xmlns: + self.xmlns = wsgi.XMLNS_ATOM + else: + self.xmlns = xmlns + + def _get_most_recent_update(self, versions): + recent = None + for version in versions: + updated = datetime.strptime(version['updated'], + '%Y-%m-%dT%H:%M:%SZ') + if not recent: + recent = updated + elif updated > recent: + recent = updated + + return recent.strftime('%Y-%m-%dT%H:%M:%SZ') + + def _get_base_url(self, link_href): + # Make sure no trailing / + link_href = link_href.rstrip('/') + return link_href.rsplit('/', 1)[0] + '/' + + def _create_feed(self, versions, feed_title, feed_id): + feed = etree.Element('feed', nsmap=self.NSMAP) + title = etree.SubElement(feed, 'title') + title.set('type', 'text') + title.text = feed_title + + # Set this updated to the most recently updated version + recent = self._get_most_recent_update(versions) + etree.SubElement(feed, 'updated').text = recent + + etree.SubElement(feed, 'id').text = feed_id + + link = etree.SubElement(feed, 'link') + link.set('rel', 'self') + link.set('href', feed_id) + + author = etree.SubElement(feed, 'author') + etree.SubElement(author, 'name').text = 'Rackspace' + etree.SubElement(author, 'uri').text = 'http://www.rackspace.com/' + + for version in versions: + feed.append(self._create_version_entry(version)) + + return feed + + def _create_version_entry(self, version): + entry = etree.Element('entry') + etree.SubElement(entry, 'id').text = version['links'][0]['href'] + title = etree.SubElement(entry, 'title') + title.set('type', 'text') + title.text = 'Version %s' % version['id'] + etree.SubElement(entry, 'updated').text = version['updated'] + + for link in version['links']: + link_elem = etree.SubElement(entry, 'link') + link_elem.set('rel', link['rel']) + link_elem.set('href', link['href']) + if 'type' in link: + link_elem.set('type', link['type']) + + content = etree.SubElement(entry, 'content') + content.set('type', 'text') + content.text = 'Version %s %s (%s)' % (version['id'], + version['status'], + version['updated']) + return entry + + +class VersionsAtomSerializer(AtomSerializer): + def default(self, data): + versions = data['versions'] + feed_id = self._get_base_url(versions[0]['links'][0]['href']) + feed = self._create_feed(versions, 'Available API Versions', feed_id) + return self._to_xml(feed) + + +class VersionAtomSerializer(AtomSerializer): + def default(self, data): + version = data['version'] + feed_id = version['links'][0]['href'] + feed = self._create_feed([version], 'About This Version', feed_id) + return self._to_xml(feed) + + +class Versions(wsgi.Resource): + def __init__(self): + super(Versions, self).__init__(None) + + @wsgi.serializers(xml=VersionsTemplate, + atom=VersionsAtomSerializer) + def index(self, req): + """Return all versions.""" + builder = views_versions.get_view_builder(req) + return builder.build_versions(VERSIONS) + + @wsgi.serializers(xml=ChoicesTemplate) + @wsgi.response(300) + def multi(self, req): + """Return multiple choices.""" + builder = views_versions.get_view_builder(req) + return builder.build_choices(VERSIONS, req) + + def get_action_args(self, request_environment): + """Parse dictionary created by routes library.""" + args = {} + if request_environment['PATH_INFO'] == '/': + args['action'] = 'index' + else: + args['action'] = 'multi' + + return args + + +class VersionV2(object): + @wsgi.serializers(xml=VersionTemplate, + atom=VersionAtomSerializer) + def show(self, req): + builder = views_versions.get_view_builder(req) + return builder.build_version(VERSIONS['v2.0']) + + +def create_resource(): + return wsgi.Resource(VersionV2()) diff --git a/nova/api/openstack/compute/views/__init__.py b/nova/api/openstack/compute/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/api/openstack/compute/views/addresses.py b/nova/api/openstack/compute/views/addresses.py new file mode 100644 index 000000000..776ba9e59 --- /dev/null +++ b/nova/api/openstack/compute/views/addresses.py @@ -0,0 +1,52 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import itertools + +from nova.api.openstack import common +from nova import flags +from nova import log as logging + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack.compute.views.addresses') + + +class ViewBuilder(common.ViewBuilder): + """Models server addresses as a dictionary.""" + + _collection_name = "addresses" + + def basic(self, ip): + """Return a dictionary describing an IP address.""" + return { + "version": ip["version"], + "addr": ip["addr"], + } + + def show(self, network, label): + """Returns a dictionary describing a network.""" + all_ips = itertools.chain(network["ips"], network["floating_ips"]) + return {label: [self.basic(ip) for ip in all_ips]} + + def index(self, networks): + """Return a dictionary describing a list of networks.""" + addresses = {} + for label, network in networks.items(): + network_dict = self.show(network, label) + addresses[label] = network_dict[label] + return dict(addresses=addresses) diff --git a/nova/api/openstack/compute/views/flavors.py b/nova/api/openstack/compute/views/flavors.py new file mode 100644 index 000000000..64284e406 --- /dev/null +++ b/nova/api/openstack/compute/views/flavors.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.openstack import common + + +class ViewBuilder(common.ViewBuilder): + + _collection_name = "flavors" + + def basic(self, request, flavor): + return { + "flavor": { + "id": flavor["flavorid"], + "name": flavor["name"], + "links": self._get_links(request, flavor["flavorid"]), + }, + } + + def show(self, request, flavor): + return { + "flavor": { + "id": flavor["flavorid"], + "name": flavor["name"], + "ram": flavor["memory_mb"], + "disk": flavor["local_gb"], + "vcpus": flavor.get("vcpus") or "", + "swap": flavor.get("swap") or "", + "rxtx_factor": flavor.get("rxtx_factor") or "", + "links": self._get_links(request, flavor["flavorid"]), + }, + } + + def index(self, request, flavors): + """Return the 'index' view of flavors.""" + def _get_flavors(request, flavors): + for _, flavor in flavors.iteritems(): + yield self.basic(request, flavor)["flavor"] + + return dict(flavors=list(_get_flavors(request, flavors))) + + def detail(self, request, flavors): + """Return the 'detail' view of flavors.""" + def _get_flavors(request, flavors): + for _, flavor in flavors.iteritems(): + yield self.show(request, flavor)["flavor"] + + return dict(flavors=list(_get_flavors(request, flavors))) diff --git a/nova/api/openstack/compute/views/images.py b/nova/api/openstack/compute/views/images.py new file mode 100644 index 000000000..c4cfe8031 --- /dev/null +++ b/nova/api/openstack/compute/views/images.py @@ -0,0 +1,139 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os.path + +from nova.api.openstack import common +from nova import utils + + +class ViewBuilder(common.ViewBuilder): + + _collection_name = "images" + + def basic(self, request, image): + """Return a dictionary with basic image attributes.""" + return { + "image": { + "id": image.get("id"), + "name": image.get("name"), + "links": self._get_links(request, image["id"]), + }, + } + + def show(self, request, image): + """Return a dictionary with image details.""" + image_dict = { + "id": image.get("id"), + "name": image.get("name"), + "minRam": int(image.get("min_ram") or 0), + "minDisk": int(image.get("min_disk") or 0), + "metadata": image.get("properties", {}), + "created": self._format_date(image.get("created_at")), + "updated": self._format_date(image.get("updated_at")), + "status": self._get_status(image), + "progress": self._get_progress(image), + "links": self._get_links(request, image["id"]), + } + + server_ref = image.get("properties", {}).get("instance_ref") + + if server_ref is not None: + image_dict["server"] = { + "id": common.get_id_from_href(server_ref), + "links": [{ + "rel": "self", + "href": server_ref, + }, + { + "rel": "bookmark", + "href": common.remove_version_from_href(server_ref), + }], + } + + return dict(image=image_dict) + + def detail(self, request, images): + """Show a list of images with details.""" + list_func = self.show + return self._list_view(list_func, request, images) + + def index(self, request, images): + """Show a list of images with basic attributes.""" + list_func = self.basic + return self._list_view(list_func, request, images) + + def _list_view(self, list_func, request, images): + """Provide a view for a list of images.""" + image_list = [list_func(request, image)["image"] for image in images] + images_links = self._get_collection_links(request, images) + images_dict = dict(images=image_list) + + if images_links: + images_dict["images_links"] = images_links + + return images_dict + + def _get_links(self, request, identifier): + """Return a list of links for this image.""" + return [{ + "rel": "self", + "href": self._get_href_link(request, identifier), + }, + { + "rel": "bookmark", + "href": self._get_bookmark_link(request, identifier), + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": self._get_alternate_link(request, identifier), + }] + + def _get_alternate_link(self, request, identifier): + """Create an alternate link for a specific flavor id.""" + glance_url = utils.generate_glance_url() + return os.path.join(glance_url, + request.environ["nova.context"].project_id, + self._collection_name, + str(identifier)) + + @staticmethod + def _format_date(date_string): + """Return standard format for given date.""" + if date_string is not None: + return date_string.strftime('%Y-%m-%dT%H:%M:%SZ') + + @staticmethod + def _get_status(image): + """Update the status field to standardize format.""" + return { + 'active': 'ACTIVE', + 'queued': 'SAVING', + 'saving': 'SAVING', + 'deleted': 'DELETED', + 'pending_delete': 'DELETED', + 'killed': 'ERROR', + }.get(image.get("status"), 'UNKNOWN') + + @staticmethod + def _get_progress(image): + return { + "queued": 25, + "saving": 50, + "active": 100, + }.get(image.get("status"), 0) diff --git a/nova/api/openstack/compute/views/limits.py b/nova/api/openstack/compute/views/limits.py new file mode 100644 index 000000000..cff6781be --- /dev/null +++ b/nova/api/openstack/compute/views/limits.py @@ -0,0 +1,96 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from nova import utils + + +class ViewBuilder(object): + """Openstack API base limits view builder.""" + + def build(self, rate_limits, absolute_limits): + rate_limits = self._build_rate_limits(rate_limits) + absolute_limits = self._build_absolute_limits(absolute_limits) + + output = { + "limits": { + "rate": rate_limits, + "absolute": absolute_limits, + }, + } + + return output + + def _build_absolute_limits(self, absolute_limits): + """Builder for absolute limits + + absolute_limits should be given as a dict of limits. + For example: {"ram": 512, "gigabytes": 1024}. + + """ + limit_names = { + "ram": ["maxTotalRAMSize"], + "instances": ["maxTotalInstances"], + "cores": ["maxTotalCores"], + "metadata_items": ["maxServerMeta", "maxImageMeta"], + "injected_files": ["maxPersonality"], + "injected_file_content_bytes": ["maxPersonalitySize"], + } + limits = {} + for name, value in absolute_limits.iteritems(): + if name in limit_names and value is not None: + for name in limit_names[name]: + limits[name] = value + return limits + + def _build_rate_limits(self, rate_limits): + limits = [] + for rate_limit in rate_limits: + _rate_limit_key = None + _rate_limit = self._build_rate_limit(rate_limit) + + # check for existing key + for limit in limits: + if limit["uri"] == rate_limit["URI"] and \ + limit["regex"] == rate_limit["regex"]: + _rate_limit_key = limit + break + + # ensure we have a key if we didn't find one + if not _rate_limit_key: + _rate_limit_key = { + "uri": rate_limit["URI"], + "regex": rate_limit["regex"], + "limit": [], + } + limits.append(_rate_limit_key) + + _rate_limit_key["limit"].append(_rate_limit) + + return limits + + def _build_rate_limit(self, rate_limit): + next_avail = \ + datetime.datetime.utcfromtimestamp(rate_limit["resetTime"]) + return { + "verb": rate_limit["verb"], + "value": rate_limit["value"], + "remaining": int(rate_limit["remaining"]), + "unit": rate_limit["unit"], + "next-available": utils.isotime(at=next_avail), + } diff --git a/nova/api/openstack/compute/views/servers.py b/nova/api/openstack/compute/views/servers.py new file mode 100644 index 000000000..6a1622a66 --- /dev/null +++ b/nova/api/openstack/compute/views/servers.py @@ -0,0 +1,193 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# 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 hashlib + +from nova.api.openstack import common +from nova.api.openstack.compute.views import addresses as views_addresses +from nova.api.openstack.compute.views import flavors as views_flavors +from nova.api.openstack.compute.views import images as views_images +from nova import log as logging +from nova import utils + + +LOG = logging.getLogger('nova.api.openstack.compute.views.servers') + + +class ViewBuilder(common.ViewBuilder): + """Model a server API response as a python dictionary.""" + + _collection_name = "servers" + + _progress_statuses = ( + "ACTIVE", + "BUILD", + "REBUILD", + "RESIZE", + "VERIFY_RESIZE", + ) + + _fault_statuses = ( + "ERROR", + ) + + def __init__(self): + """Initialize view builder.""" + super(ViewBuilder, self).__init__() + self._address_builder = views_addresses.ViewBuilder() + self._flavor_builder = views_flavors.ViewBuilder() + self._image_builder = views_images.ViewBuilder() + + def _skip_precooked(func): + def wrapped(self, request, instance): + if instance.get("_is_precooked"): + return dict(server=instance) + else: + return func(self, request, instance) + return wrapped + + def create(self, request, instance): + """View that should be returned when an instance is created.""" + return { + "server": { + "id": instance["uuid"], + "links": self._get_links(request, instance["uuid"]), + }, + } + + @_skip_precooked + def basic(self, request, instance): + """Generic, non-detailed view of an instance.""" + return { + "server": { + "id": instance["uuid"], + "name": instance["display_name"], + "links": self._get_links(request, instance["uuid"]), + }, + } + + @_skip_precooked + def show(self, request, instance): + """Detailed view of a single instance.""" + server = { + "server": { + "id": instance["uuid"], + "name": instance["display_name"], + "status": self._get_vm_state(instance), + "tenant_id": instance.get("project_id") or "", + "user_id": instance.get("user_id") or "", + "metadata": self._get_metadata(instance), + "hostId": self._get_host_id(instance) or "", + "image": self._get_image(request, instance), + "flavor": self._get_flavor(request, instance), + "created": utils.isotime(instance["created_at"]), + "updated": utils.isotime(instance["updated_at"]), + "addresses": self._get_addresses(request, instance), + "accessIPv4": instance.get("access_ip_v4") or "", + "accessIPv6": instance.get("access_ip_v6") or "", + "key_name": instance.get("key_name") or "", + "config_drive": instance.get("config_drive"), + "links": self._get_links(request, instance["uuid"]), + }, + } + _inst_fault = self._get_fault(request, instance) + if server["server"]["status"] in self._fault_statuses and _inst_fault: + server['server']['fault'] = _inst_fault + + if server["server"]["status"] in self._progress_statuses: + server["server"]["progress"] = instance.get("progress", 0) + + return server + + def index(self, request, instances): + """Show a list of servers without many details.""" + return self._list_view(self.basic, request, instances) + + def detail(self, request, instances): + """Detailed view of a list of instance.""" + return self._list_view(self.show, request, instances) + + def _list_view(self, func, request, servers): + """Provide a view for a list of servers.""" + server_list = [func(request, server)["server"] for server in servers] + servers_links = self._get_collection_links(request, servers) + servers_dict = dict(servers=server_list) + + if servers_links: + servers_dict["servers_links"] = servers_links + + return servers_dict + + @staticmethod + def _get_metadata(instance): + metadata = instance.get("metadata", []) + return dict((item['key'], str(item['value'])) for item in metadata) + + @staticmethod + def _get_vm_state(instance): + return common.status_from_state(instance.get("vm_state"), + instance.get("task_state")) + + @staticmethod + def _get_host_id(instance): + host = instance.get("host") + if host: + return hashlib.sha224(host).hexdigest() # pylint: disable=E1101 + + def _get_addresses(self, request, instance): + context = request.environ["nova.context"] + networks = common.get_networks_for_instance(context, instance) + return self._address_builder.index(networks)["addresses"] + + def _get_image(self, request, instance): + image_ref = instance["image_ref"] + image_id = str(common.get_id_from_href(image_ref)) + bookmark = self._image_builder._get_bookmark_link(request, image_id) + return { + "id": image_id, + "links": [{ + "rel": "bookmark", + "href": bookmark, + }], + } + + def _get_flavor(self, request, instance): + flavor_id = instance["instance_type"]["flavorid"] + flavor_ref = self._flavor_builder._get_href_link(request, flavor_id) + flavor_bookmark = self._flavor_builder._get_bookmark_link(request, + flavor_id) + return { + "id": str(common.get_id_from_href(flavor_ref)), + "links": [{ + "rel": "bookmark", + "href": flavor_bookmark, + }], + } + + def _get_fault(self, request, instance): + fault = instance.get("fault", None) + + if not fault: + return None + + return { + "code": fault["code"], + "created": utils.isotime(fault["created_at"]), + "message": fault["message"], + "details": fault["details"], + } diff --git a/nova/api/openstack/compute/views/versions.py b/nova/api/openstack/compute/views/versions.py new file mode 100644 index 000000000..cb2fd9f4a --- /dev/null +++ b/nova/api/openstack/compute/views/versions.py @@ -0,0 +1,94 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import os + + +def get_view_builder(req): + base_url = req.application_url + return ViewBuilder(base_url) + + +class ViewBuilder(object): + + def __init__(self, base_url): + """ + :param base_url: url of the root wsgi application + """ + self.base_url = base_url + + def build_choices(self, VERSIONS, req): + version_objs = [] + for version in VERSIONS: + version = VERSIONS[version] + version_objs.append({ + "id": version['id'], + "status": version['status'], + "links": [ + { + "rel": "self", + "href": self.generate_href(req.path), + }, + ], + "media-types": version['media-types'], + }) + + return dict(choices=version_objs) + + def build_versions(self, versions): + version_objs = [] + for version in sorted(versions.keys()): + version = versions[version] + version_objs.append({ + "id": version['id'], + "status": version['status'], + "updated": version['updated'], + "links": self._build_links(version), + }) + + return dict(versions=version_objs) + + def build_version(self, version): + reval = copy.deepcopy(version) + reval['links'].insert(0, { + "rel": "self", + "href": self.base_url.rstrip('/') + '/', + }) + return dict(version=reval) + + def _build_links(self, version_data): + """Generate a container of links that refer to the provided version.""" + href = self.generate_href() + + links = [ + { + "rel": "self", + "href": href, + }, + ] + + return links + + def generate_href(self, path=None): + """Create an url that refers to a specific version_number.""" + version_number = 'v2' + if path: + path = path.strip('/') + return os.path.join(self.base_url, version_number, path) + else: + return os.path.join(self.base_url, version_number) + '/' diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py new file mode 100644 index 000000000..6c49e8ace --- /dev/null +++ b/nova/api/openstack/extensions.py @@ -0,0 +1,623 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import routes +import webob.dec +import webob.exc + +import nova.api.openstack +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack.extensions') + + +FLAGS = flags.FLAGS + + +class ExtensionDescriptor(object): + """Base class that defines the contract for extensions. + + Note that you don't have to derive from this class to have a valid + extension; it is purely a convenience. + + """ + + # The name of the extension, e.g., 'Fox In Socks' + name = None + + # The alias for the extension, e.g., 'FOXNSOX' + alias = None + + # Description comes from the docstring for the class + + # The XML namespace for the extension, e.g., + # 'http://www.fox.in.socks/api/ext/pie/v1.0' + namespace = None + + # The timestamp when the extension was last updated, e.g., + # '2011-01-22T13:25:27-06:00' + updated = None + + # This attribute causes the extension to load only when + # the admin api is enabled + admin_only = False + + def __init__(self, ext_mgr): + """Register extension with the extension manager.""" + + ext_mgr.register(self) + + def get_resources(self): + """List of extensions.ResourceExtension extension objects. + + Resources define new nouns, and are accessible through URLs. + + """ + resources = [] + return resources + + def get_actions(self): + """List of extensions.ActionExtension extension objects. + + Actions are verbs callable from the API. + + """ + actions = [] + return actions + + def get_request_extensions(self): + """List of extensions.RequestExtension extension objects. + + Request extensions are used to handle custom request data. + + """ + request_exts = [] + return request_exts + + @classmethod + def nsmap(cls): + """Synthesize a namespace map from extension.""" + + # Start with a base nsmap + nsmap = ext_nsmap.copy() + + # Add the namespace for the extension + nsmap[cls.alias] = cls.namespace + + return nsmap + + @classmethod + def xmlname(cls, name): + """Synthesize element and attribute names.""" + + return '{%s}%s' % (cls.namespace, name) + + +class ActionExtensionController(object): + def __init__(self, application): + self.application = application + self.action_handlers = {} + + def add_action(self, action_name, handler): + self.action_handlers[action_name] = handler + + def action(self, req, id, body): + for action_name, handler in self.action_handlers.iteritems(): + if action_name in body: + return handler(body, req, id) + # no action handler found (bump to downstream application) + res = self.application + return res + + +class ActionExtensionResource(wsgi.Resource): + + def __init__(self, application): + controller = ActionExtensionController(application) + wsgi.Resource.__init__(self, controller, + serializer=wsgi.ResponseSerializer(), + deserializer=wsgi.RequestDeserializer()) + + def add_action(self, action_name, handler): + self.controller.add_action(action_name, handler) + + +class RequestExtensionController(object): + + def __init__(self, application): + self.application = application + self.handlers = [] + self.pre_handlers = [] + + def add_handler(self, handler): + self.handlers.append(handler) + + def add_pre_handler(self, pre_handler): + self.pre_handlers.append(pre_handler) + + def process(self, req, *args, **kwargs): + for pre_handler in self.pre_handlers: + pre_handler(req) + + res = req.get_response(self.application) + res.environ = req.environ + + # Don't call extensions if the main application returned an + # unsuccessful status + successful = 200 <= res.status_int < 400 + if not successful: + return res + + # Deserialize the response body, if any + body = None + if res.body: + body = utils.loads(res.body) + + # currently request handlers are un-ordered + for handler in self.handlers: + res = handler(req, res, body) + + # Reserialize the response body + if body is not None: + res.body = utils.dumps(body) + + return res + + +class RequestExtensionResource(wsgi.Resource): + + def __init__(self, application): + controller = RequestExtensionController(application) + wsgi.Resource.__init__(self, controller, + serializer=wsgi.ResponseSerializer(), + deserializer=wsgi.RequestDeserializer()) + + def add_handler(self, handler): + self.controller.add_handler(handler) + + def add_pre_handler(self, pre_handler): + self.controller.add_pre_handler(pre_handler) + + +def make_ext(elem): + elem.set('name') + elem.set('namespace') + elem.set('alias') + elem.set('updated') + + desc = xmlutil.SubTemplateElement(elem, 'description') + desc.text = 'description' + + xmlutil.make_links(elem, 'links') + + +ext_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class ExtensionTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('extension', selector='extension') + make_ext(root) + return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap) + + +class ExtensionsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('extensions') + elem = xmlutil.SubTemplateElement(root, 'extension', + selector='extensions') + make_ext(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap) + + +class ExtensionsResource(wsgi.Resource): + + def __init__(self, extension_manager): + self.extension_manager = extension_manager + super(ExtensionsResource, self).__init__(None) + + def _translate(self, ext): + ext_data = {} + ext_data['name'] = ext.name + ext_data['alias'] = ext.alias + ext_data['description'] = ext.__doc__ + ext_data['namespace'] = ext.namespace + ext_data['updated'] = ext.updated + ext_data['links'] = [] # TODO(dprince): implement extension links + return ext_data + + @wsgi.serializers(xml=ExtensionsTemplate) + def index(self, req): + extensions = [] + for _alias, ext in self.extension_manager.extensions.iteritems(): + extensions.append(self._translate(ext)) + return dict(extensions=extensions) + + @wsgi.serializers(xml=ExtensionTemplate) + def show(self, req, id): + try: + # NOTE(dprince): the extensions alias is used as the 'id' for show + ext = self.extension_manager.extensions[id] + except KeyError: + raise webob.exc.HTTPNotFound() + + return dict(extension=self._translate(ext)) + + def delete(self, req, id): + raise webob.exc.HTTPNotFound() + + def create(self, req): + raise webob.exc.HTTPNotFound() + + +class ExtensionMiddleware(base_wsgi.Middleware): + """Extensions middleware for WSGI.""" + @classmethod + def factory(cls, global_config, **local_config): + """Paste factory.""" + def _factory(app): + return cls(app, **local_config) + return _factory + + def _action_ext_resources(self, application, ext_mgr, mapper): + """Return a dict of ActionExtensionResource-s by collection.""" + action_resources = {} + for action in ext_mgr.get_actions(): + if not action.collection in action_resources.keys(): + resource = ActionExtensionResource(application) + mapper.connect("/:(project_id)/%s/:(id)/action.:(format)" % + action.collection, + action='action', + controller=resource, + conditions=dict(method=['POST'])) + mapper.connect("/:(project_id)/%s/:(id)/action" % + action.collection, + action='action', + controller=resource, + conditions=dict(method=['POST'])) + action_resources[action.collection] = resource + + return action_resources + + def _request_ext_resources(self, application, ext_mgr, mapper): + """Returns a dict of RequestExtensionResource-s by collection.""" + request_ext_resources = {} + for req_ext in ext_mgr.get_request_extensions(): + if not req_ext.key in request_ext_resources.keys(): + resource = RequestExtensionResource(application) + mapper.connect(req_ext.url_route + '.:(format)', + action='process', + controller=resource, + conditions=req_ext.conditions) + + mapper.connect(req_ext.url_route, + action='process', + controller=resource, + conditions=req_ext.conditions) + request_ext_resources[req_ext.key] = resource + + return request_ext_resources + + def __init__(self, application, ext_mgr=None): + + if ext_mgr is None: + ext_mgr = ExtensionManager() + self.ext_mgr = ext_mgr + + mapper = nova.api.openstack.ProjectMapper() + + # extended actions + action_resources = self._action_ext_resources(application, ext_mgr, + mapper) + for action in ext_mgr.get_actions(): + LOG.debug(_('Extended action: %s'), action.action_name) + resource = action_resources[action.collection] + resource.add_action(action.action_name, action.handler) + + # extended requests + req_controllers = self._request_ext_resources(application, ext_mgr, + mapper) + for request_ext in ext_mgr.get_request_extensions(): + LOG.debug(_('Extended request: %s'), request_ext.key) + controller = req_controllers[request_ext.key] + if request_ext.handler: + controller.add_handler(request_ext.handler) + if request_ext.pre_handler: + controller.add_pre_handler(request_ext.pre_handler) + + self._router = routes.middleware.RoutesMiddleware(self._dispatch, + mapper) + + super(ExtensionMiddleware, self).__init__(application) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """Route the incoming request with router.""" + req.environ['extended.app'] = self.application + return self._router + + @staticmethod + @webob.dec.wsgify(RequestClass=wsgi.Request) + def _dispatch(req): + """Dispatch the request. + + Returns the routed WSGI app's response or defers to the extended + application. + + """ + match = req.environ['wsgiorg.routing_args'][1] + if not match: + return req.environ['extended.app'] + app = match['controller'] + return app + + +class ExtensionManager(object): + """Load extensions from the configured extension path. + + See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an + example extension implementation. + + """ + + _ext_mgr = None + + @classmethod + def reset(cls): + cls._ext_mgr = None + + def register(self, ext): + # Do nothing if the extension doesn't check out + if not self._check_extension(ext): + return + + alias = ext.alias + LOG.audit(_('Loaded extension: %s'), alias) + + if alias in self.extensions: + raise exception.Error("Found duplicate extension: %s" % alias) + self.extensions[alias] = ext + + def get_resources(self): + """Returns a list of ResourceExtension objects.""" + + resources = [] + resources.append(ResourceExtension('extensions', + ExtensionsResource(self))) + + for ext in self.extensions.values(): + try: + resources.extend(ext.get_resources()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have resource + # extensions + pass + return resources + + def get_actions(self): + """Returns a list of ActionExtension objects.""" + actions = [] + for ext in self.extensions.values(): + try: + actions.extend(ext.get_actions()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have action + # extensions + pass + return actions + + def get_request_extensions(self): + """Returns a list of RequestExtension objects.""" + request_exts = [] + for ext in self.extensions.values(): + try: + request_exts.extend(ext.get_request_extensions()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have request + # extensions + pass + return request_exts + + def _check_extension(self, extension): + """Checks for required methods in extension objects.""" + try: + LOG.debug(_('Ext name: %s'), extension.name) + LOG.debug(_('Ext alias: %s'), extension.alias) + LOG.debug(_('Ext description: %s'), + ' '.join(extension.__doc__.strip().split())) + LOG.debug(_('Ext namespace: %s'), extension.namespace) + LOG.debug(_('Ext updated: %s'), extension.updated) + LOG.debug(_('Ext admin_only: %s'), extension.admin_only) + except AttributeError as ex: + LOG.exception(_("Exception loading extension: %s"), unicode(ex)) + return False + + # Don't load admin api extensions if the admin api isn't enabled + if not FLAGS.allow_admin_api and extension.admin_only: + return False + + return True + + def load_extension(self, ext_factory): + """Execute an extension factory. + + Loads an extension. The 'ext_factory' is the name of a + callable that will be imported and called with one + argument--the extension manager. The factory callable is + expected to call the register() method at least once. + """ + + LOG.debug(_("Loading extension %s"), ext_factory) + + # Load the factory + factory = utils.import_class(ext_factory) + + # Call it + LOG.debug(_("Calling extension factory %s"), ext_factory) + factory(self) + + def _load_extensions(self): + """Load extensions specified on the command line.""" + + extensions = list(self.cls_list) + + for ext_factory in extensions: + try: + self.load_extension(ext_factory) + except Exception as exc: + LOG.warn(_('Failed to load extension %(ext_factory)s: ' + '%(exc)s') % locals()) + + +class RequestExtension(object): + """Extend requests and responses of core nova OpenStack API resources. + + Provide a way to add data to responses and handle custom request data + that is sent to core nova OpenStack API controllers. + + """ + def __init__(self, method, url_route, handler=None, pre_handler=None): + self.url_route = url_route + self.handler = handler + self.conditions = dict(method=[method]) + self.key = "%s-%s" % (method, url_route) + self.pre_handler = pre_handler + + +class ActionExtension(object): + """Add custom actions to core nova OpenStack API resources.""" + + def __init__(self, collection, action_name, handler): + self.collection = collection + self.action_name = action_name + self.handler = handler + + +class ResourceExtension(object): + """Add top level resources to the OpenStack API in nova.""" + + def __init__(self, collection, controller, parent=None, + collection_actions=None, member_actions=None, + deserializer=None, serializer=None): + if not collection_actions: + collection_actions = {} + if not member_actions: + member_actions = {} + self.collection = collection + self.controller = controller + self.parent = parent + self.collection_actions = collection_actions + self.member_actions = member_actions + self.deserializer = deserializer + self.serializer = serializer + + +class ExtensionsXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return ExtensionsTemplate() + + def show(self): + return ExtensionTemplate() + + +def require_admin(f): + @functools.wraps(f) + def wraps(self, req, *args, **kwargs): + if 'nova.context' in req.environ and\ + req.environ['nova.context'].is_admin: + return f(self, req, *args, **kwargs) + else: + raise exception.AdminRequired() + return wraps + + +def wrap_errors(fn): + """Ensure errors are not passed along.""" + def wrapped(*args): + try: + return fn(*args) + except Exception, e: + raise webob.exc.HTTPInternalServerError() + return wrapped + + +def load_standard_extensions(ext_mgr, logger, path, package): + """Registers all standard API extensions.""" + + # Walk through all the modules in our directory... + our_dir = path[0] + for dirpath, dirnames, filenames in os.walk(our_dir): + # Compute the relative package name from the dirpath + relpath = os.path.relpath(dirpath, our_dir) + if relpath == '.': + relpkg = '' + else: + relpkg = '.%s' % '.'.join(relpath.split(os.sep)) + + # Now, consider each file in turn, only considering .py files + for fname in filenames: + root, ext = os.path.splitext(fname) + + # Skip __init__ and anything that's not .py + if ext != '.py' or root == '__init__': + continue + + # Try loading it + classname = ("%s%s.%s.%s%s" % + (package, relpkg, root, + root[0].upper(), root[1:])) + try: + ext_mgr.load_extension(classname) + except Exception as exc: + logger.warn(_('Failed to load extension %(classname)s: ' + '%(exc)s') % locals()) + + # Now, let's consider any subdirectories we may have... + subdirs = [] + for dname in dirnames: + # Skip it if it does not have __init__.py + if not os.path.exists(os.path.join(dirpath, dname, + '__init__.py')): + continue + + # If it has extension(), delegate... + ext_name = ("%s%s.%s.extension" % + (package, relpkg, dname)) + try: + ext = utils.import_class(ext_name) + except exception.ClassNotFound: + # extension() doesn't exist on it, so we'll explore + # the directory for ourselves + subdirs.append(dname) + else: + try: + ext(ext_mgr) + except Exception as exc: + logger.warn(_('Failed to load extension %(ext_name)s: ' + '%(exc)s') % locals()) + + # Update the list of directories we'll explore... + dirnames[:] = subdirs diff --git a/nova/api/openstack/urlmap.py b/nova/api/openstack/urlmap.py new file mode 100644 index 000000000..825a12af2 --- /dev/null +++ b/nova/api/openstack/urlmap.py @@ -0,0 +1,297 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import paste.urlmap +import re +import urllib2 + +from nova import log as logging +from nova.api.openstack import wsgi + + +_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"' +_option_header_piece_re = re.compile(r';\s*([^\s;=]+|%s)\s*' + r'(?:=\s*([^;]+|%s))?\s*' % + (_quoted_string_re, _quoted_string_re)) + +LOG = logging.getLogger('nova.api.openstack.compute.map') + + +def unquote_header_value(value): + """Unquotes a header value. + This does not use the real unquoting but what browsers are actually + using for quoting. + + :param value: the header value to unquote. + """ + if value and value[0] == value[-1] == '"': + # this is not the real unquoting, but fixing this so that the + # RFC is met will result in bugs with internet explorer and + # probably some other browsers as well. IE for example is + # uploading files with "C:\foo\bar.txt" as filename + value = value[1:-1] + return value + + +def parse_list_header(value): + """Parse lists as described by RFC 2068 Section 2. + + In particular, parse comma-separated lists where the elements of + the list may include quoted-strings. A quoted-string could + contain a comma. A non-quoted string could have quotes in the + middle. Quotes are removed automatically after parsing. + + The return value is a standard :class:`list`: + + >>> parse_list_header('token, "quoted value"') + ['token', 'quoted value'] + + :param value: a string with a list header. + :return: :class:`list` + """ + result = [] + for item in urllib2.parse_http_list(value): + if item[:1] == item[-1:] == '"': + item = unquote_header_value(item[1:-1]) + result.append(item) + return result + + +def parse_options_header(value): + """Parse a ``Content-Type`` like header into a tuple with the content + type and the options: + + >>> parse_options_header('Content-Type: text/html; mimetype=text/html') + ('Content-Type:', {'mimetype': 'text/html'}) + + :param value: the header to parse. + :return: (str, options) + """ + def _tokenize(string): + for match in _option_header_piece_re.finditer(string): + key, value = match.groups() + key = unquote_header_value(key) + if value is not None: + value = unquote_header_value(value) + yield key, value + + if not value: + return '', {} + + parts = _tokenize(';' + value) + name = parts.next()[0] + extra = dict(parts) + return name, extra + + +class Accept(object): + def __init__(self, value): + self._content_types = [parse_options_header(v) for v in + parse_list_header(value)] + + def best_match(self, supported_content_types): + # FIXME: Should we have a more sophisticated matching algorithm that + # takes into account the version as well? + best_quality = -1 + best_content_type = None + best_params = {} + best_match = '*/*' + + for content_type in supported_content_types: + for content_mask, params in self._content_types: + try: + quality = float(params.get('q', 1)) + except ValueError: + continue + + if quality < best_quality: + continue + elif best_quality == quality: + if best_match.count('*') <= content_mask.count('*'): + continue + + if self._match_mask(content_mask, content_type): + best_quality = quality + best_content_type = content_type + best_params = params + best_match = content_mask + + return best_content_type, best_params + + def content_type_params(self, best_content_type): + """Find parameters in Accept header for given content type.""" + for content_type, params in self._content_types: + if best_content_type == content_type: + return params + + return {} + + def _match_mask(self, mask, content_type): + if '*' not in mask: + return content_type == mask + if mask == '*/*': + return True + mask_major = mask[:-2] + content_type_major = content_type.split('/', 1)[0] + return content_type_major == mask_major + + +def urlmap_factory(loader, global_conf, **local_conf): + if 'not_found_app' in local_conf: + not_found_app = local_conf.pop('not_found_app') + else: + not_found_app = global_conf.get('not_found_app') + if not_found_app: + not_found_app = loader.get_app(not_found_app, global_conf=global_conf) + urlmap = URLMap(not_found_app=not_found_app) + for path, app_name in local_conf.items(): + path = paste.urlmap.parse_path_expression(path) + app = loader.get_app(app_name, global_conf=global_conf) + urlmap[path] = app + return urlmap + + +class URLMap(paste.urlmap.URLMap): + def _match(self, host, port, path_info): + """Find longest match for a given URL path.""" + for (domain, app_url), app in self.applications: + if domain and domain != host and domain != host + ':' + port: + continue + if (path_info == app_url + or path_info.startswith(app_url + '/')): + return app, app_url + + return None, None + + def _set_script_name(self, app, app_url): + def wrap(environ, start_response): + environ['SCRIPT_NAME'] += app_url + return app(environ, start_response) + + return wrap + + def _munge_path(self, app, path_info, app_url): + def wrap(environ, start_response): + environ['SCRIPT_NAME'] += app_url + environ['PATH_INFO'] = path_info[len(app_url):] + return app(environ, start_response) + + return wrap + + def _path_strategy(self, host, port, path_info): + """Check path suffix for MIME type and path prefix for API version.""" + mime_type = app = app_url = None + + parts = path_info.rsplit('.', 1) + if len(parts) > 1: + possible_type = 'application/' + parts[1] + if possible_type in wsgi.SUPPORTED_CONTENT_TYPES: + mime_type = possible_type + + parts = path_info.split('/') + if len(parts) > 1: + possible_app, possible_app_url = self._match(host, port, path_info) + # Don't use prefix if it ends up matching default + if possible_app and possible_app_url: + app_url = possible_app_url + app = self._munge_path(possible_app, path_info, app_url) + + return mime_type, app, app_url + + def _content_type_strategy(self, host, port, environ): + """Check Content-Type header for API version.""" + app = None + params = parse_options_header(environ.get('CONTENT_TYPE', ''))[1] + if 'version' in params: + app, app_url = self._match(host, port, '/v' + params['version']) + if app: + app = self._set_script_name(app, app_url) + + return app + + def _accept_strategy(self, host, port, environ, supported_content_types): + """Check Accept header for best matching MIME type and API version.""" + accept = Accept(environ.get('HTTP_ACCEPT', '')) + + app = None + + # Find the best match in the Accept header + mime_type, params = accept.best_match(supported_content_types) + if 'version' in params: + app, app_url = self._match(host, port, '/v' + params['version']) + if app: + app = self._set_script_name(app, app_url) + + return mime_type, app + + def __call__(self, environ, start_response): + host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower() + if ':' in host: + host, port = host.split(':', 1) + else: + if environ['wsgi.url_scheme'] == 'http': + port = '80' + else: + port = '443' + + path_info = environ['PATH_INFO'] + path_info = self.normalize_url(path_info, False)[1] + + # The MIME type for the response is determined in one of two ways: + # 1) URL path suffix (eg /servers/detail.json) + # 2) Accept header (eg application/json;q=0.8, application/xml;q=0.2) + + # The API version is determined in one of three ways: + # 1) URL path prefix (eg /v1.1/tenant/servers/detail) + # 2) Content-Type header (eg application/json;version=1.1) + # 3) Accept header (eg application/json;q=0.8;version=1.1) + + supported_content_types = list(wsgi.SUPPORTED_CONTENT_TYPES) + + mime_type, app, app_url = self._path_strategy(host, port, path_info) + + # Accept application/atom+xml for the index query of each API + # version mount point as well as the root index + if (app_url and app_url + '/' == path_info) or path_info == '/': + supported_content_types.append('application/atom+xml') + + if not app: + app = self._content_type_strategy(host, port, environ) + + if not mime_type or not app: + possible_mime_type, possible_app = self._accept_strategy( + host, port, environ, supported_content_types) + if possible_mime_type and not mime_type: + mime_type = possible_mime_type + if possible_app and not app: + app = possible_app + + if not mime_type: + mime_type = 'application/json' + + if not app: + # Didn't match a particular version, probably matches default + app, app_url = self._match(host, port, path_info) + if app: + app = self._munge_path(app, path_info, app_url) + + if app: + environ['nova.best_content_type'] = mime_type + return app(environ, start_response) + + environ['paste.urlmap_object'] = self + return self.not_found_application(environ, start_response) diff --git a/nova/api/openstack/v2/__init__.py b/nova/api/openstack/v2/__init__.py deleted file mode 100644 index c211cd2f9..000000000 --- a/nova/api/openstack/v2/__init__.py +++ /dev/null @@ -1,182 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -WSGI middleware for OpenStack API controllers. -""" - -import routes -import webob.dec -import webob.exc - -from nova.api.openstack.v2 import consoles -from nova.api.openstack.v2 import extensions -from nova.api.openstack.v2 import flavors -from nova.api.openstack.v2 import images -from nova.api.openstack.v2 import image_metadata -from nova.api.openstack.v2 import ips -from nova.api.openstack.v2 import limits -from nova.api.openstack.v2 import servers -from nova.api.openstack.v2 import server_metadata -from nova.api.openstack.v2 import versions -from nova.api.openstack import wsgi -from nova import flags -from nova import log as logging -from nova import wsgi as base_wsgi - - -LOG = logging.getLogger('nova.api.openstack.v2') -FLAGS = flags.FLAGS -flags.DEFINE_bool('allow_admin_api', - False, - 'When True, this API service will accept admin operations.') -flags.DEFINE_bool('allow_instance_snapshots', - True, - 'When True, this API service will permit instance snapshot operations.') - - -class FaultWrapper(base_wsgi.Middleware): - """Calls down the middleware stack, making exceptions into faults.""" - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - try: - return req.get_response(self.application) - except Exception as ex: - LOG.exception(_("Caught error: %s"), unicode(ex)) - exc = webob.exc.HTTPInternalServerError() - return wsgi.Fault(exc) - - -class APIMapper(routes.Mapper): - def routematch(self, url=None, environ=None): - if url is "": - result = self._match("", environ) - return result[0], result[1] - return routes.Mapper.routematch(self, url, environ) - - -class ProjectMapper(APIMapper): - - def resource(self, member_name, collection_name, **kwargs): - if not ('parent_resource' in kwargs): - kwargs['path_prefix'] = '{project_id}/' - else: - parent_resource = kwargs['parent_resource'] - p_collection = parent_resource['collection_name'] - p_member = parent_resource['member_name'] - kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection, - p_member) - routes.Mapper.resource(self, member_name, - collection_name, - **kwargs) - - -class APIRouter(base_wsgi.Router): - """ - Routes requests on the OpenStack API to the appropriate controller - and method. - """ - - @classmethod - def factory(cls, global_config, **local_config): - """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one""" - return cls() - - def __init__(self, ext_mgr=None): - if ext_mgr is None: - ext_mgr = extensions.ExtensionManager() - - mapper = ProjectMapper() - self._setup_routes(mapper) - self._setup_ext_routes(mapper, ext_mgr) - super(APIRouter, self).__init__(mapper) - - def _setup_ext_routes(self, mapper, ext_mgr): - for resource in ext_mgr.get_resources(): - LOG.debug(_('Extended resource: %s'), - resource.collection) - - kargs = dict( - controller=wsgi.Resource( - resource.controller, resource.deserializer, - resource.serializer), - collection=resource.collection_actions, - member=resource.member_actions) - - if resource.parent: - kargs['parent_resource'] = resource.parent - - mapper.resource(resource.collection, resource.collection, **kargs) - - def _setup_routes(self, mapper): - mapper.connect("versions", "/", - controller=versions.create_resource(), - action='show') - - mapper.redirect("", "/") - - mapper.resource("console", "consoles", - controller=consoles.create_resource(), - parent_resource=dict(member_name='server', - collection_name='servers')) - - mapper.resource("server", "servers", - controller=servers.create_resource(), - collection={'detail': 'GET'}, - member={'action': 'POST'}) - - mapper.resource("ip", "ips", controller=ips.create_resource(), - parent_resource=dict(member_name='server', - collection_name='servers')) - - mapper.resource("image", "images", - controller=images.create_resource(), - collection={'detail': 'GET'}) - - mapper.resource("limit", "limits", - controller=limits.create_resource()) - - mapper.resource("flavor", "flavors", - controller=flavors.create_resource(), - collection={'detail': 'GET'}) - - image_metadata_controller = image_metadata.create_resource() - - mapper.resource("image_meta", "metadata", - controller=image_metadata_controller, - parent_resource=dict(member_name='image', - collection_name='images')) - - mapper.connect("metadata", "/{project_id}/images/{image_id}/metadata", - controller=image_metadata_controller, - action='update_all', - conditions={"method": ['PUT']}) - - server_metadata_controller = server_metadata.create_resource() - - mapper.resource("server_meta", "metadata", - controller=server_metadata_controller, - parent_resource=dict(member_name='server', - collection_name='servers')) - - mapper.connect("metadata", - "/{project_id}/servers/{server_id}/metadata", - controller=server_metadata_controller, - action='update_all', - conditions={"method": ['PUT']}) diff --git a/nova/api/openstack/v2/auth.py b/nova/api/openstack/v2/auth.py deleted file mode 100644 index ba5fb603f..000000000 --- a/nova/api/openstack/v2/auth.py +++ /dev/null @@ -1,257 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import hashlib -import os -import time - -import webob.dec -import webob.exc - -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova import auth -from nova import context -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils -from nova import wsgi as base_wsgi - -LOG = logging.getLogger('nova.api.openstack.v2.auth') -FLAGS = flags.FLAGS -flags.DECLARE('use_forwarded_for', 'nova.api.auth') - - -class NoAuthMiddleware(base_wsgi.Middleware): - """Return a fake token if one isn't specified.""" - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - if 'X-Auth-Token' not in req.headers: - user_id = req.headers.get('X-Auth-User', 'admin') - project_id = req.headers.get('X-Auth-Project-Id', 'admin') - os_url = os.path.join(req.url, project_id) - res = webob.Response() - # NOTE(vish): This is expecting and returning Auth(1.1), whereas - # keystone uses 2.0 auth. We should probably allow - # 2.0 auth here as well. - res.headers['X-Auth-Token'] = '%s:%s' % (user_id, project_id) - res.headers['X-Server-Management-Url'] = os_url - res.headers['X-Storage-Url'] = '' - res.headers['X-CDN-Management-Url'] = '' - res.content_type = 'text/plain' - res.status = '204' - return res - - token = req.headers['X-Auth-Token'] - user_id, _sep, project_id = token.partition(':') - project_id = project_id or user_id - remote_address = getattr(req, 'remote_address', '127.0.0.1') - if FLAGS.use_forwarded_for: - remote_address = req.headers.get('X-Forwarded-For', remote_address) - ctx = context.RequestContext(user_id, - project_id, - is_admin=True, - remote_address=remote_address) - - req.environ['nova.context'] = ctx - return self.application - - -class AuthMiddleware(base_wsgi.Middleware): - """Authorize the openstack API request or return an HTTP Forbidden.""" - - def __init__(self, application, db_driver=None): - if not db_driver: - db_driver = FLAGS.db_driver - self.db = utils.import_object(db_driver) - self.auth = auth.manager.AuthManager() - super(AuthMiddleware, self).__init__(application) - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - if not self.has_authentication(req): - return self.authenticate(req) - user_id = self.get_user_by_authentication(req) - if not user_id: - token = req.headers["X-Auth-Token"] - msg = _("%(user_id)s could not be found with token '%(token)s'") - LOG.warn(msg % locals()) - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - - # Get all valid projects for the user - projects = self.auth.get_projects(user_id) - if not projects: - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - - project_id = "" - path_parts = req.path.split('/') - # TODO(wwolf): this v1.1 check will be temporary as - # keystone should be taking this over at some point - if len(path_parts) > 1 and path_parts[1] in ('v1.1', 'v2'): - project_id = path_parts[2] - # Check that the project for project_id exists, and that user - # is authorized to use it - try: - self.auth.get_project(project_id) - except exception.ProjectNotFound: - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - if project_id not in [p.id for p in projects]: - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - else: - # As a fallback, set project_id from the headers, which is the v1.0 - # behavior. As a last resort, be forgiving to the user and set - # project_id based on a valid project of theirs. - try: - project_id = req.headers["X-Auth-Project-Id"] - except KeyError: - project_id = projects[0].id - - is_admin = self.auth.is_admin(user_id) - remote_address = getattr(req, 'remote_address', '127.0.0.1') - if FLAGS.use_forwarded_for: - remote_address = req.headers.get('X-Forwarded-For', remote_address) - ctx = context.RequestContext(user_id, - project_id, - is_admin=is_admin, - remote_address=remote_address) - req.environ['nova.context'] = ctx - - if not is_admin and not self.auth.is_project_member(user_id, - project_id): - msg = _("%(user_id)s must be an admin or a " - "member of %(project_id)s") - LOG.warn(msg % locals()) - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - - return self.application - - def has_authentication(self, req): - return 'X-Auth-Token' in req.headers - - def get_user_by_authentication(self, req): - return self.authorize_token(req.headers["X-Auth-Token"]) - - def authenticate(self, req): - # Unless the request is explicitly made against // don't - # honor it - path_info = req.path_info - if len(path_info) > 1: - msg = _("Authentication requests must be made against a version " - "root (e.g. /v2).") - LOG.warn(msg) - return wsgi.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) - - def _get_auth_header(key): - """Ensures that the KeyError returned is meaningful.""" - try: - return req.headers[key] - except KeyError as ex: - raise KeyError(key) - try: - username = _get_auth_header('X-Auth-User') - key = _get_auth_header('X-Auth-Key') - except KeyError as ex: - msg = _("Could not find %s in request.") % ex - LOG.warn(msg) - return wsgi.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) - - token, user = self._authorize_user(username, key, req) - if user and token: - res = webob.Response() - res.headers['X-Auth-Token'] = token['token_hash'] - res.headers['X-Server-Management-Url'] = \ - token['server_management_url'] - res.headers['X-Storage-Url'] = token['storage_url'] - res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] - res.content_type = 'text/plain' - res.status = '204' - LOG.debug(_("Successfully authenticated '%s'") % username) - return res - else: - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - - def authorize_token(self, token_hash): - """ retrieves user information from the datastore given a token - - If the token has expired, returns None - If the token is not found, returns None - Otherwise returns dict(id=(the authorized user's id)) - - This method will also remove the token if the timestamp is older than - 2 days ago. - """ - ctxt = context.get_admin_context() - try: - token = self.db.auth_token_get(ctxt, token_hash) - except exception.NotFound: - return None - if token: - delta = utils.utcnow() - token['created_at'] - if delta.days >= 2: - self.db.auth_token_destroy(ctxt, token['token_hash']) - else: - return token['user_id'] - return None - - def _authorize_user(self, username, key, req): - """Generates a new token and assigns it to a user. - - username - string - key - string API key - req - wsgi.Request object - """ - ctxt = context.get_admin_context() - - project_id = req.headers.get('X-Auth-Project-Id') - if project_id is None: - # If the project_id is not provided in the headers, be forgiving to - # the user and set project_id based on a valid project of theirs. - user = self.auth.get_user_from_access_key(key) - projects = self.auth.get_projects(user.id) - if not projects: - raise webob.exc.HTTPUnauthorized() - project_id = projects[0].id - - try: - user = self.auth.get_user_from_access_key(key) - except exception.NotFound: - LOG.warn(_("User not found with provided API key.")) - user = None - - if user and user.name == username: - token_hash = hashlib.sha1('%s%s%f' % (username, key, - time.time())).hexdigest() - token_dict = {} - token_dict['token_hash'] = token_hash - token_dict['cdn_management_url'] = '' - os_url = req.url - token_dict['server_management_url'] = os_url.strip('/') - version = common.get_version_from_href(os_url) - if version in ('1.1', '2'): - token_dict['server_management_url'] += '/' + project_id - token_dict['storage_url'] = '' - token_dict['user_id'] = user.id - token = self.db.auth_token_create(ctxt, token_dict) - return token, user - elif user and user.name != username: - msg = _("Provided API key is valid, but not for user " - "'%(username)s'") % locals() - LOG.warn(msg) - - return None, None diff --git a/nova/api/openstack/v2/consoles.py b/nova/api/openstack/v2/consoles.py deleted file mode 100644 index e9eee4c75..000000000 --- a/nova/api/openstack/v2/consoles.py +++ /dev/null @@ -1,131 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob -from webob import exc - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import console -from nova import exception - - -def _translate_keys(cons): - """Coerces a console instance into proper dictionary format """ - pool = cons['pool'] - info = {'id': cons['id'], - 'console_type': pool['console_type']} - return dict(console=info) - - -def _translate_detail_keys(cons): - """Coerces a console instance into proper dictionary format with - correctly mapped attributes """ - pool = cons['pool'] - info = {'id': cons['id'], - 'console_type': pool['console_type'], - 'password': cons['password'], - 'instance_name': cons['instance_name'], - 'port': cons['port'], - 'host': pool['public_hostname']} - return dict(console=info) - - -class ConsoleTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('console', selector='console') - - id_elem = xmlutil.SubTemplateElement(root, 'id', selector='id') - id_elem.text = xmlutil.Selector() - - port_elem = xmlutil.SubTemplateElement(root, 'port', selector='port') - port_elem.text = xmlutil.Selector() - - host_elem = xmlutil.SubTemplateElement(root, 'host', selector='host') - host_elem.text = xmlutil.Selector() - - passwd_elem = xmlutil.SubTemplateElement(root, 'password', - selector='password') - passwd_elem.text = xmlutil.Selector() - - constype_elem = xmlutil.SubTemplateElement(root, 'console_type', - selector='console_type') - constype_elem.text = xmlutil.Selector() - - return xmlutil.MasterTemplate(root, 1) - - -class ConsolesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('consoles') - console = xmlutil.SubTemplateElement(root, 'console', - selector='consoles') - console.append(ConsoleTemplate()) - - return xmlutil.MasterTemplate(root, 1) - - -class Controller(object): - """The Consoles controller for the Openstack API""" - - def __init__(self): - self.console_api = console.API() - - @wsgi.serializers(xml=ConsolesTemplate) - def index(self, req, server_id): - """Returns a list of consoles for this instance""" - consoles = self.console_api.get_consoles( - req.environ['nova.context'], - server_id) - return dict(consoles=[_translate_keys(console) - for console in consoles]) - - def create(self, req, server_id): - """Creates a new console""" - self.console_api.create_console( - req.environ['nova.context'], - server_id) - - @wsgi.serializers(xml=ConsoleTemplate) - def show(self, req, server_id, id): - """Shows in-depth information on a specific console""" - try: - console = self.console_api.get_console( - req.environ['nova.context'], - server_id, - int(id)) - except exception.NotFound: - raise exc.HTTPNotFound() - return _translate_detail_keys(console) - - def update(self, req, server_id, id): - """You can't update a console""" - raise exc.HTTPNotImplemented() - - def delete(self, req, server_id, id): - """Deletes a console""" - try: - self.console_api.delete_console(req.environ['nova.context'], - server_id, - int(id)) - except exception.NotFound: - raise exc.HTTPNotFound() - return webob.Response(status_int=202) - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/contrib/__init__.py b/nova/api/openstack/v2/contrib/__init__.py deleted file mode 100644 index d361dac9c..000000000 --- a/nova/api/openstack/v2/contrib/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# 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. - -"""Contrib contains extensions that are shipped with nova. - -It can't be called 'extensions' because that causes namespacing problems. - -""" - -import os - -from nova import exception -from nova import log as logging -from nova import utils - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib') - - -def standard_extensions(ext_mgr): - """Registers all standard API extensions.""" - - # Walk through all the modules in our directory... - our_dir = __path__[0] - for dirpath, dirnames, filenames in os.walk(our_dir): - # Compute the relative package name from the dirpath - relpath = os.path.relpath(dirpath, our_dir) - if relpath == '.': - relpkg = '' - else: - relpkg = '.%s' % '.'.join(relpath.split(os.sep)) - - # Now, consider each file in turn, only considering .py files - for fname in filenames: - root, ext = os.path.splitext(fname) - - # Skip __init__ and anything that's not .py - if ext != '.py' or root == '__init__': - continue - - # Try loading it - classname = ("%s%s.%s.%s%s" % - (__package__, relpkg, root, - root[0].upper(), root[1:])) - try: - ext_mgr.load_extension(classname) - except Exception as exc: - LOG.warn(_('Failed to load extension %(classname)s: ' - '%(exc)s') % locals()) - - # Now, let's consider any subdirectories we may have... - subdirs = [] - for dname in dirnames: - # Skip it if it does not have __init__.py - if not os.path.exists(os.path.join(dirpath, dname, - '__init__.py')): - continue - - # If it has extension(), delegate... - ext_name = ("%s%s.%s.extension" % - (__package__, relpkg, dname)) - try: - ext = utils.import_class(ext_name) - except exception.ClassNotFound: - # extension() doesn't exist on it, so we'll explore - # the directory for ourselves - subdirs.append(dname) - else: - try: - ext(ext_mgr) - except Exception as exc: - LOG.warn(_('Failed to load extension %(ext_name)s: ' - '%(exc)s') % locals()) - - # Update the list of directories we'll explore... - dirnames[:] = subdirs diff --git a/nova/api/openstack/v2/contrib/accounts.py b/nova/api/openstack/v2/contrib/accounts.py deleted file mode 100644 index 9bfff31d7..000000000 --- a/nova/api/openstack/v2/contrib/accounts.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob.exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.auth import manager -from nova import exception -from nova import flags -from nova import log as logging - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack.v2.contrib.accounts') - - -class AccountTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('account', selector='account') - root.set('id', 'id') - root.set('name', 'name') - root.set('description', 'description') - root.set('manager', 'manager') - - return xmlutil.MasterTemplate(root, 1) - - -def _translate_keys(account): - return dict(id=account.id, - name=account.name, - description=account.description, - manager=account.project_manager_id) - - -class Controller(object): - - def __init__(self): - self.manager = manager.AuthManager() - - def _check_admin(self, context): - """We cannot depend on the db layer to check for admin access - for the auth manager, so we do it here""" - if not context.is_admin: - raise exception.AdminRequired() - - def index(self, req): - raise webob.exc.HTTPNotImplemented() - - @wsgi.serializers(xml=AccountTemplate) - def show(self, req, id): - """Return data about the given account id""" - account = self.manager.get_project(id) - return dict(account=_translate_keys(account)) - - def delete(self, req, id): - self._check_admin(req.environ['nova.context']) - self.manager.delete_project(id) - return {} - - def create(self, req, body): - """We use update with create-or-update semantics - because the id comes from an external source""" - raise webob.exc.HTTPNotImplemented() - - @wsgi.serializers(xml=AccountTemplate) - def update(self, req, id, body): - """This is really create or update.""" - self._check_admin(req.environ['nova.context']) - description = body['account'].get('description') - manager = body['account'].get('manager') - try: - account = self.manager.get_project(id) - self.manager.modify_project(id, manager, description) - except exception.NotFound: - account = self.manager.create_project(id, manager, description) - return dict(account=_translate_keys(account)) - - -class Accounts(extensions.ExtensionDescriptor): - """Admin-only access to accounts""" - - name = "Accounts" - alias = "os-accounts" - namespace = "http://docs.openstack.org/compute/ext/accounts/api/v1.1" - updated = "2011-12-23T00:00:00+00:00" - admin_only = True - - def get_resources(self): - #TODO(bcwaldon): This should be prefixed with 'os-' - res = extensions.ResourceExtension('accounts', - Controller()) - - return [res] diff --git a/nova/api/openstack/v2/contrib/admin_actions.py b/nova/api/openstack/v2/contrib/admin_actions.py deleted file mode 100644 index 5479f88c4..000000000 --- a/nova/api/openstack/v2/contrib/admin_actions.py +++ /dev/null @@ -1,291 +0,0 @@ -# Copyright 2011 Openstack, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os.path -import traceback - -import webob -from webob import exc - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova.scheduler import api as scheduler_api - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.v2.contrib.admin_actions") - - -class Admin_actions(extensions.ExtensionDescriptor): - """Enable admin-only server actions - - Actions include: pause, unpause, suspend, resume, migrate, - resetNetwork, injectNetworkInfo, lock, unlock, createBackup - """ - - name = "AdminActions" - alias = "os-admin-actions" - namespace = "http://docs.openstack.org/compute/ext/admin-actions/api/v1.1" - updated = "2011-09-20T00:00:00+00:00" - admin_only = True - - def __init__(self, ext_mgr): - super(Admin_actions, self).__init__(ext_mgr) - self.compute_api = compute.API() - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _pause(self, input_dict, req, id): - """Permit Admins to pause the server""" - ctxt = req.environ['nova.context'] - try: - server = self.compute_api.get(ctxt, id) - self.compute_api.pause(ctxt, server) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'pause') - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::pause %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _unpause(self, input_dict, req, id): - """Permit Admins to unpause the server""" - ctxt = req.environ['nova.context'] - try: - server = self.compute_api.get(ctxt, id) - self.compute_api.unpause(ctxt, server) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'unpause') - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::unpause %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _suspend(self, input_dict, req, id): - """Permit admins to suspend the server""" - context = req.environ['nova.context'] - try: - server = self.compute_api.get(context, id) - self.compute_api.suspend(context, server) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'suspend') - except Exception: - readable = traceback.format_exc() - LOG.exception(_("compute.api::suspend %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _resume(self, input_dict, req, id): - """Permit admins to resume the server from suspend""" - context = req.environ['nova.context'] - try: - server = self.compute_api.get(context, id) - self.compute_api.resume(context, server) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'resume') - except Exception: - readable = traceback.format_exc() - LOG.exception(_("compute.api::resume %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _migrate(self, input_dict, req, id): - """Permit admins to migrate a server to a new host""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.resize(req.environ['nova.context'], instance) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'migrate') - except Exception, e: - LOG.exception(_("Error in migrate %s"), e) - raise exc.HTTPBadRequest() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _reset_network(self, input_dict, req, id): - """Permit admins to reset networking on an server""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.reset_network(context, instance) - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::reset_network %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _inject_network_info(self, input_dict, req, id): - """Permit admins to inject network info into a server""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.inject_network_info(context, instance) - except exception.InstanceNotFound: - raise exc.HTTPNotFound(_("Server not found")) - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::inject_network_info %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _lock(self, input_dict, req, id): - """Permit admins to lock a server""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.lock(context, instance) - except exception.InstanceNotFound: - raise exc.HTTPNotFound(_("Server not found")) - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::lock %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _unlock(self, input_dict, req, id): - """Permit admins to lock a server""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.unlock(context, instance) - except exception.InstanceNotFound: - raise exc.HTTPNotFound(_("Server not found")) - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::unlock %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - def _create_backup(self, input_dict, req, instance_id): - """Backup a server instance. - - Images now have an `image_type` associated with them, which can be - 'snapshot' or the backup type, like 'daily' or 'weekly'. - - If the image_type is backup-like, then the rotation factor can be - included and that will cause the oldest backups that exceed the - rotation factor to be deleted. - - """ - context = req.environ["nova.context"] - - try: - entity = input_dict["createBackup"] - except (KeyError, TypeError): - raise exc.HTTPBadRequest(_("Malformed request body")) - - try: - image_name = entity["name"] - backup_type = entity["backup_type"] - rotation = entity["rotation"] - - except KeyError as missing_key: - msg = _("createBackup entity requires %s attribute") % missing_key - raise exc.HTTPBadRequest(explanation=msg) - - except TypeError: - msg = _("Malformed createBackup entity") - raise exc.HTTPBadRequest(explanation=msg) - - try: - rotation = int(rotation) - except ValueError: - msg = _("createBackup attribute 'rotation' must be an integer") - raise exc.HTTPBadRequest(explanation=msg) - - # preserve link to server in image properties - server_ref = os.path.join(req.application_url, 'servers', instance_id) - props = {'instance_ref': server_ref} - - metadata = entity.get('metadata', {}) - common.check_img_metadata_quota_limit(context, metadata) - try: - props.update(metadata) - except ValueError: - msg = _("Invalid metadata") - raise exc.HTTPBadRequest(explanation=msg) - - try: - instance = self.compute_api.get(context, instance_id) - except exception.NotFound: - raise exc.HTTPNotFound(_("Instance not found")) - - try: - image = self.compute_api.backup(context, instance, image_name, - backup_type, rotation, extra_properties=props) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'createBackup') - - # build location of newly-created image entity - image_id = str(image['id']) - image_ref = os.path.join(req.application_url, 'images', image_id) - - resp = webob.Response(status_int=202) - resp.headers['Location'] = image_ref - return resp - - def get_actions(self): - actions = [ - #TODO(bcwaldon): These actions should be prefixed with 'os-' - extensions.ActionExtension("servers", "pause", self._pause), - extensions.ActionExtension("servers", "unpause", self._unpause), - extensions.ActionExtension("servers", "suspend", self._suspend), - extensions.ActionExtension("servers", "resume", self._resume), - extensions.ActionExtension("servers", "migrate", self._migrate), - - extensions.ActionExtension("servers", - "createBackup", - self._create_backup), - - extensions.ActionExtension("servers", - "resetNetwork", - self._reset_network), - - extensions.ActionExtension("servers", - "injectNetworkInfo", - self._inject_network_info), - - extensions.ActionExtension("servers", "lock", self._lock), - extensions.ActionExtension("servers", "unlock", self._unlock), - ] - - return actions diff --git a/nova/api/openstack/v2/contrib/cloudpipe.py b/nova/api/openstack/v2/contrib/cloudpipe.py deleted file mode 100644 index 9327e3987..000000000 --- a/nova/api/openstack/v2/contrib/cloudpipe.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2011 Openstack, LLC. -# -# 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. - -"""Connect your vlan to the world.""" - -import os - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova.auth import manager -from nova.cloudpipe import pipelib -from nova import compute -from nova.compute import vm_states -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.v2.contrib.cloudpipe") - - -class CloudpipeTemplate(xmlutil.TemplateBuilder): - def construct(self): - return xmlutil.MasterTemplate(xmlutil.make_flat_dict('cloudpipe'), 1) - - -class CloudpipesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('cloudpipes') - elem = xmlutil.make_flat_dict('cloudpipe', selector='cloudpipes', - subselector='cloudpipe') - root.append(elem) - return xmlutil.MasterTemplate(root, 1) - - -class CloudpipeController(object): - """Handle creating and listing cloudpipe instances.""" - - def __init__(self): - self.compute_api = compute.API() - self.auth_manager = manager.AuthManager() - self.cloudpipe = pipelib.CloudPipe() - self.setup() - - def setup(self): - """Ensure the keychains and folders exist.""" - # TODO(todd): this was copyed from api.ec2.cloud - # FIXME(ja): this should be moved to a nova-manage command, - # if not setup throw exceptions instead of running - # Create keys folder, if it doesn't exist - if not os.path.exists(FLAGS.keys_path): - os.makedirs(FLAGS.keys_path) - # Gen root CA, if we don't have one - root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) - if not os.path.exists(root_ca_path): - genrootca_sh_path = os.path.join(os.path.dirname(__file__), - os.path.pardir, - os.path.pardir, - 'CA', - 'genrootca.sh') - - start = os.getcwd() - if not os.path.exists(FLAGS.ca_path): - os.makedirs(FLAGS.ca_path) - os.chdir(FLAGS.ca_path) - # TODO(vish): Do this with M2Crypto instead - utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path) - os.chdir(start) - - def _get_cloudpipe_for_project(self, context, project_id): - """Get the cloudpipe instance for a project ID.""" - # NOTE(todd): this should probably change to compute_api.get_all - # or db.instance_get_project_vpn - for instance in db.instance_get_all_by_project(context, project_id): - if (instance['image_id'] == str(FLAGS.vpn_image_id) - and instance['vm_state'] != vm_states.DELETED): - return instance - - def _vpn_dict(self, project, vpn_instance): - rv = {'project_id': project.id, - 'public_ip': project.vpn_ip, - 'public_port': project.vpn_port} - if vpn_instance: - rv['instance_id'] = vpn_instance['uuid'] - rv['created_at'] = utils.isotime(vpn_instance['created_at']) - address = vpn_instance.get('fixed_ip', None) - if address: - rv['internal_ip'] = address['address'] - if project.vpn_ip and project.vpn_port: - if utils.vpn_ping(project.vpn_ip, project.vpn_port): - rv['state'] = 'running' - else: - rv['state'] = 'down' - else: - rv['state'] = 'invalid' - else: - rv['state'] = 'pending' - return rv - - @wsgi.serializers(xml=CloudpipeTemplate) - def create(self, req, body): - """Create a new cloudpipe instance, if none exists. - - Parameters: {cloudpipe: {project_id: XYZ}} - """ - - ctxt = req.environ['nova.context'] - params = body.get('cloudpipe', {}) - project_id = params.get('project_id', ctxt.project_id) - instance = self._get_cloudpipe_for_project(ctxt, project_id) - if not instance: - proj = self.auth_manager.get_project(project_id) - user_id = proj.project_manager_id - try: - self.cloudpipe.launch_vpn_instance(project_id, user_id) - except db.NoMoreNetworks: - msg = _("Unable to claim IP for VPN instances, ensure it " - "isn't running, and try again in a few minutes") - raise exception.ApiError(msg) - instance = self._get_cloudpipe_for_project(ctxt, proj) - return {'instance_id': instance['uuid']} - - @wsgi.serializers(xml=CloudpipesTemplate) - def index(self, req): - """Show admins the list of running cloudpipe instances.""" - context = req.environ['nova.context'] - vpns = [] - # TODO(todd): could use compute_api.get_all with admin context? - for project in self.auth_manager.get_projects(): - instance = self._get_cloudpipe_for_project(context, project.id) - vpns.append(self._vpn_dict(project, instance)) - return {'cloudpipes': vpns} - - -class Cloudpipe(extensions.ExtensionDescriptor): - """Adds actions to create cloudpipe instances. - - When running with the Vlan network mode, you need a mechanism to route - from the public Internet to your vlans. This mechanism is known as a - cloudpipe. - - At the time of creating this class, only OpenVPN is supported. Support for - a SSH Bastion host is forthcoming. - """ - - name = "Cloudpipe" - alias = "os-cloudpipe" - namespace = "http://docs.openstack.org/compute/ext/cloudpipe/api/v1.1" - updated = "2011-12-16T00:00:00+00:00" - admin_only = True - - def get_resources(self): - resources = [] - res = extensions.ResourceExtension('os-cloudpipe', - CloudpipeController()) - resources.append(res) - return resources diff --git a/nova/api/openstack/v2/contrib/console_output.py b/nova/api/openstack/v2/contrib/console_output.py deleted file mode 100644 index 0c9196398..000000000 --- a/nova/api/openstack/v2/contrib/console_output.py +++ /dev/null @@ -1,73 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# Copyright 2011 Grid Dynamics -# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev -# -# 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 import compute -from nova import exception -from nova import log as logging -from nova.api.openstack.v2 import extensions - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib.console_output') - - -class Console_output(extensions.ExtensionDescriptor): - """Console log output support, with tailing ability.""" - - name = "Console_output" - alias = "os-console-output" - namespace = "http://docs.openstack.org/compute/ext/" \ - "os-console-output/api/v2" - updated = "2011-12-08T00:00:00+00:00" - - def __init__(self, ext_mgr): - self.compute_api = compute.API() - super(Console_output, self).__init__(ext_mgr) - - def get_console_output(self, input_dict, req, server_id): - """Get text console output.""" - context = req.environ['nova.context'] - - try: - instance = self.compute_api.routing_get(context, server_id) - except exception.NotFound: - raise webob.exc.HTTPNotFound(_('Instance not found')) - - try: - length = input_dict['os-getConsoleOutput'].get('length') - except (TypeError, KeyError): - raise webob.exc.HTTPBadRequest(_('Malformed request body')) - - try: - output = self.compute_api.get_console_output(context, - instance, - length) - except exception.ApiError, e: - raise webob.exc.HTTPBadRequest(explanation=e.message) - except exception.NotAuthorized, e: - raise webob.exc.HTTPUnauthorized() - - return {'output': output} - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - actions = [extensions.ActionExtension("servers", "os-getConsoleOutput", - self.get_console_output)] - - return actions diff --git a/nova/api/openstack/v2/contrib/createserverext.py b/nova/api/openstack/v2/contrib/createserverext.py deleted file mode 100644 index 70fe2796f..000000000 --- a/nova/api/openstack/v2/contrib/createserverext.py +++ /dev/null @@ -1,60 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# -# 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.v2 import extensions -from nova.api.openstack.v2 import servers -from nova.api.openstack.v2 import views -from nova.api.openstack import wsgi - - -class ViewBuilder(views.servers.ViewBuilder): - """Adds security group output when viewing server details.""" - - def show(self, request, instance): - """Detailed view of a single instance.""" - server = super(ViewBuilder, self).show(request, instance) - server["server"]["security_groups"] = self._get_groups(instance) - return server - - def _get_groups(self, instance): - """Get a list of security groups for this instance.""" - groups = instance.get('security_groups') - if groups is not None: - return [{"name": group["name"]} for group in groups] - - -class Controller(servers.Controller): - _view_builder_class = ViewBuilder - - -class Createserverext(extensions.ExtensionDescriptor): - """Extended support to the Create Server v1.1 API""" - - name = "Createserverext" - alias = "os-create-server-ext" - namespace = "http://docs.openstack.org/compute/ext/" \ - "createserverext/api/v1.1" - updated = "2011-07-19T00:00:00+00:00" - - def get_resources(self): - resources = [] - controller = Controller() - - res = extensions.ResourceExtension('os-create-server-ext', - controller=controller) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/deferred_delete.py b/nova/api/openstack/v2/contrib/deferred_delete.py deleted file mode 100644 index 0b7c60073..000000000 --- a/nova/api/openstack/v2/contrib/deferred_delete.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2011 Openstack, LLC -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""The deferred instance delete extension.""" - -import webob - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack.v2 import servers -from nova import compute -from nova import exception -from nova import log as logging - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.deferred-delete") - - -class Deferred_delete(extensions.ExtensionDescriptor): - """Instance deferred delete""" - - name = "DeferredDelete" - alias = "os-deferred-delete" - namespace = "http://docs.openstack.org/compute/ext/" \ - "deferred-delete/api/v1.1" - updated = "2011-09-01T00:00:00+00:00" - - def __init__(self, ext_mgr): - super(Deferred_delete, self).__init__(ext_mgr) - self.compute_api = compute.API() - - def _restore(self, input_dict, req, instance_id): - """Restore a previously deleted instance.""" - - context = req.environ["nova.context"] - instance = self.compute_api.get(context, instance_id) - try: - self.compute_api.restore(context, instance) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'restore') - return webob.Response(status_int=202) - - def _force_delete(self, input_dict, req, instance_id): - """Force delete of instance before deferred cleanup.""" - - context = req.environ["nova.context"] - instance = self.compute_api.get(context, instance_id) - try: - self.compute_api.force_delete(context, instance) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'forceDelete') - return webob.Response(status_int=202) - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - actions = [ - extensions.ActionExtension("servers", "restore", - self._restore), - extensions.ActionExtension("servers", "forceDelete", - self._force_delete), - ] - - return actions diff --git a/nova/api/openstack/v2/contrib/disk_config.py b/nova/api/openstack/v2/contrib/disk_config.py deleted file mode 100644 index 7ce24a3fd..000000000 --- a/nova/api/openstack/v2/contrib/disk_config.py +++ /dev/null @@ -1,200 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# -# 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 - -"""Disk Config extension.""" - -from xml.dom import minidom - -from webob import exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import xmlutil -from nova import compute -from nova import db -from nova import log as logging -from nova import utils - -LOG = logging.getLogger('nova.api.openstack.contrib.disk_config') - -ALIAS = 'RAX-DCF' -XMLNS_DCF = "http://docs.rackspacecloud.com/servers/api/ext/diskConfig/v1.0" - - -class ServerDiskConfigTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('server') - root.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) - return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) - - -class ServersDiskConfigTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('servers') - elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') - elem.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) - return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) - - -class ImageDiskConfigTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('image') - root.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) - return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) - - -class ImagesDiskConfigTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('images') - elem = xmlutil.SubTemplateElement(root, 'image', selector='images') - elem.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) - return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) - - -def disk_config_to_api(value): - return 'AUTO' if value else 'MANUAL' - - -def disk_config_from_api(value): - if value == 'AUTO': - return True - elif value == 'MANUAL': - return False - else: - msg = _("RAX-DCF:diskConfig must be either 'MANUAL' or 'AUTO'.") - raise exc.HTTPBadRequest(explanation=msg) - - -class Disk_config(extensions.ExtensionDescriptor): - """Disk Management Extension""" - - name = "DiskConfig" - alias = ALIAS - namespace = XMLNS_DCF - updated = "2011-09-27:00:00+00:00" - - API_DISK_CONFIG = "%s:diskConfig" % ALIAS - INTERNAL_DISK_CONFIG = "auto_disk_config" - - def __init__(self, ext_mgr): - super(Disk_config, self).__init__(ext_mgr) - self.compute_api = compute.API() - - def _extract_resource_from_body(self, res, body, - singular, singular_template, plural, plural_template): - """Returns a list of the given resources from the request body. - - The templates passed in are used for XML serialization. - """ - template = res.environ.get('nova.template') - if plural in body: - resources = body[plural] - if template: - template.attach(plural_template) - elif singular in body: - resources = [body[singular]] - if template: - template.attach(singular_template) - else: - resources = [] - - return resources - - def _GET_servers(self, req, res, body): - context = req.environ['nova.context'] - - servers = self._extract_resource_from_body(res, body, - singular='server', singular_template=ServerDiskConfigTemplate(), - plural='servers', plural_template=ServersDiskConfigTemplate()) - - # Filter out any servers that already have the key set (most likely - # from a remote zone) - servers = filter(lambda s: self.API_DISK_CONFIG not in s, servers) - - # Get DB information for servers - uuids = [server['id'] for server in servers] - db_servers = db.instance_get_all_by_filters(context, {'uuid': uuids}) - db_servers = dict([(s['uuid'], s) for s in db_servers]) - - for server in servers: - db_server = db_servers.get(server['id']) - if db_server: - value = db_server[self.INTERNAL_DISK_CONFIG] - server[self.API_DISK_CONFIG] = disk_config_to_api(value) - - return res - - def _GET_images(self, req, res, body): - images = self._extract_resource_from_body(res, body, - singular='image', singular_template=ImageDiskConfigTemplate(), - plural='images', plural_template=ImagesDiskConfigTemplate()) - - for image in images: - metadata = image['metadata'] - - if self.INTERNAL_DISK_CONFIG in metadata: - raw_value = metadata[self.INTERNAL_DISK_CONFIG] - value = utils.bool_from_str(raw_value) - image[self.API_DISK_CONFIG] = disk_config_to_api(value) - - return res - - def _POST_servers(self, req, res, body): - return self._GET_servers(req, res, body) - - def _pre_POST_servers(self, req): - # NOTE(sirp): deserialization currently occurs *after* pre-processing - # extensions are called. Until extensions are refactored so that - # deserialization occurs earlier, we have to perform the - # deserialization ourselves. - content_type = req.content_type - - if 'xml' in content_type: - node = minidom.parseString(req.body) - server = node.getElementsByTagName('server')[0] - api_value = server.getAttribute(self.API_DISK_CONFIG) - if api_value: - value = disk_config_from_api(api_value) - server.setAttribute(self.INTERNAL_DISK_CONFIG, str(value)) - req.body = str(node.toxml()) - else: - body = utils.loads(req.body) - server = body['server'] - api_value = server.get(self.API_DISK_CONFIG) - if api_value: - value = disk_config_from_api(api_value) - server[self.INTERNAL_DISK_CONFIG] = value - req.body = utils.dumps(body) - - def _pre_PUT_servers(self, req): - return self._pre_POST_servers(req) - - def get_request_extensions(self): - ReqExt = extensions.RequestExtension - return [ - ReqExt(method='GET', - url_route='/:(project_id)/servers/:(id)', - handler=self._GET_servers), - ReqExt(method='POST', - url_route='/:(project_id)/servers', - handler=self._POST_servers, - pre_handler=self._pre_POST_servers), - ReqExt(method='PUT', - url_route='/:(project_id)/servers/:(id)', - pre_handler=self._pre_PUT_servers), - ReqExt(method='GET', - url_route='/:(project_id)/images/:(id)', - handler=self._GET_images) - ] diff --git a/nova/api/openstack/v2/contrib/extended_status.py b/nova/api/openstack/v2/contrib/extended_status.py deleted file mode 100644 index a3b0410f6..000000000 --- a/nova/api/openstack/v2/contrib/extended_status.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2011 Openstack, LLC. -# -# 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. - -"""The Extended Status Admin API extension.""" - -from webob import exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.v2.contrib.extendedstatus") - - -class Extended_status(extensions.ExtensionDescriptor): - """Extended Status support""" - - name = "ExtendedStatus" - alias = "OS-EXT-STS" - namespace = "http://docs.openstack.org/compute/ext/" \ - "extended_status/api/v1.1" - updated = "2011-11-03T00:00:00+00:00" - admin_only = True - - def get_request_extensions(self): - request_extensions = [] - - def _get_and_extend_one(context, server_id, body): - compute_api = compute.API() - try: - inst_ref = compute_api.routing_get(context, server_id) - except exception.NotFound: - LOG.warn("Instance %s not found (one)" % server_id) - explanation = _("Server not found.") - raise exc.HTTPNotFound(explanation=explanation) - - for state in ['task_state', 'vm_state', 'power_state']: - key = "%s:%s" % (Extended_status.alias, state) - body['server'][key] = inst_ref[state] - - def _get_and_extend_all(context, body): - # TODO(mdietz): This is a brilliant argument for this to *not* - # be an extension. The problem is we either have to 1) duplicate - # the logic from the servers controller or 2) do what we did - # and iterate over the list of potentially sorted, limited - # and whatever else elements and find each individual. - compute_api = compute.API() - - for server in list(body['servers']): - try: - inst_ref = compute_api.routing_get(context, server['id']) - except exception.NotFound: - # NOTE(dtroyer): A NotFound exception at this point - # happens because a delete was in progress and the - # server that was present in the original call to - # compute.api.get_all() is no longer present. - # Delete it from the response and move on. - LOG.warn("Instance %s not found (all)" % server['id']) - body['servers'].remove(server) - continue - - #TODO(bcwaldon): these attributes should be prefixed with - # something specific to this extension - for state in ['task_state', 'vm_state', 'power_state']: - key = "%s:%s" % (Extended_status.alias, state) - server[key] = inst_ref[state] - - def _extended_status_handler(req, res, body): - context = req.environ['nova.context'] - server_id = req.environ['wsgiorg.routing_args'][1].get('id') - - if 'nova.template' in req.environ: - tmpl = req.environ['nova.template'] - tmpl.attach(ExtendedStatusTemplate()) - - if server_id: - _get_and_extend_one(context, server_id, body) - else: - _get_and_extend_all(context, body) - return res - - req_ext = extensions.RequestExtension('GET', - '/:(project_id)/servers/:(id)', - _extended_status_handler) - request_extensions.append(req_ext) - - return request_extensions - - -class ExtendedStatusTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('server') - root.set('{%s}task_state' % Extended_status.namespace, - '%s:task_state' % Extended_status.alias) - root.set('{%s}power_state' % Extended_status.namespace, - '%s:power_state' % Extended_status.alias) - root.set('{%s}vm_state' % Extended_status.namespace, - '%s:vm_state' % Extended_status.alias) - return xmlutil.SlaveTemplate(root, 1, nsmap={ - Extended_status.alias: Extended_status.namespace}) diff --git a/nova/api/openstack/v2/contrib/flavorextradata.py b/nova/api/openstack/v2/contrib/flavorextradata.py deleted file mode 100644 index 462ad1aba..000000000 --- a/nova/api/openstack/v2/contrib/flavorextradata.py +++ /dev/null @@ -1,37 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Canonical Ltd. -# 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. - -""" -The Flavor extra data extension -Openstack API version 1.1 lists "name", "ram", "disk", "vcpus" as flavor -attributes. This extension adds to that list: - rxtx_cap - rxtx_quota - swap -""" - -from nova.api.openstack.v2 import extensions - - -class Flavorextradata(extensions.ExtensionDescriptor): - """Provide additional data for flavors""" - - name = "FlavorExtraData" - alias = "os-flavor-extra-data" - namespace = "http://docs.openstack.org/compute/ext/" \ - "flavor_extra_data/api/v1.1" - updated = "2011-09-14T00:00:00+00:00" diff --git a/nova/api/openstack/v2/contrib/flavorextraspecs.py b/nova/api/openstack/v2/contrib/flavorextraspecs.py deleted file mode 100644 index cb5b572fa..000000000 --- a/nova/api/openstack/v2/contrib/flavorextraspecs.py +++ /dev/null @@ -1,127 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# 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. - -""" The instance type extra specs extension""" - -from webob import exc - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import db -from nova import exception - - -class ExtraSpecsTemplate(xmlutil.TemplateBuilder): - def construct(self): - return xmlutil.MasterTemplate(xmlutil.make_flat_dict('extra_specs'), 1) - - -class FlavorExtraSpecsController(object): - """ The flavor extra specs API controller for the Openstack API """ - - def _get_extra_specs(self, context, flavor_id): - extra_specs = db.instance_type_extra_specs_get(context, flavor_id) - specs_dict = {} - for key, value in extra_specs.iteritems(): - specs_dict[key] = value - return dict(extra_specs=specs_dict) - - def _check_body(self, body): - if body is None or body == "": - expl = _('No Request Body') - raise exc.HTTPBadRequest(explanation=expl) - - @wsgi.serializers(xml=ExtraSpecsTemplate) - def index(self, req, flavor_id): - """ Returns the list of extra specs for a givenflavor """ - context = req.environ['nova.context'] - return self._get_extra_specs(context, flavor_id) - - @wsgi.serializers(xml=ExtraSpecsTemplate) - def create(self, req, flavor_id, body): - self._check_body(body) - context = req.environ['nova.context'] - specs = body.get('extra_specs') - try: - db.instance_type_extra_specs_update_or_create(context, - flavor_id, - specs) - except exception.QuotaError as error: - self._handle_quota_error(error) - return body - - @wsgi.serializers(xml=ExtraSpecsTemplate) - def update(self, req, flavor_id, id, body): - self._check_body(body) - context = req.environ['nova.context'] - if not id in body: - expl = _('Request body and URI mismatch') - raise exc.HTTPBadRequest(explanation=expl) - if len(body) > 1: - expl = _('Request body contains too many items') - raise exc.HTTPBadRequest(explanation=expl) - try: - db.instance_type_extra_specs_update_or_create(context, - flavor_id, - body) - except exception.QuotaError as error: - self._handle_quota_error(error) - - return body - - @wsgi.serializers(xml=ExtraSpecsTemplate) - def show(self, req, flavor_id, id): - """ Return a single extra spec item """ - context = req.environ['nova.context'] - specs = self._get_extra_specs(context, flavor_id) - if id in specs['extra_specs']: - return {id: specs['extra_specs'][id]} - else: - raise exc.HTTPNotFound() - - def delete(self, req, flavor_id, id): - """ Deletes an existing extra spec """ - context = req.environ['nova.context'] - db.instance_type_extra_specs_delete(context, flavor_id, id) - - def _handle_quota_error(self, error): - """Reraise quota errors as api-specific http exceptions.""" - if error.code == "MetadataLimitExceeded": - raise exc.HTTPBadRequest(explanation=error.message) - raise error - - -class Flavorextraspecs(extensions.ExtensionDescriptor): - """Instance type (flavor) extra specs""" - - name = "FlavorExtraSpecs" - alias = "os-flavor-extra-specs" - namespace = "http://docs.openstack.org/compute/ext/" \ - "flavor_extra_specs/api/v1.1" - updated = "2011-06-23T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'os-extra_specs', - FlavorExtraSpecsController(), - parent=dict(member_name='flavor', collection_name='flavors')) - - resources.append(res) - return resources diff --git a/nova/api/openstack/v2/contrib/floating_ip_dns.py b/nova/api/openstack/v2/contrib/floating_ip_dns.py deleted file mode 100644 index de1a0a27e..000000000 --- a/nova/api/openstack/v2/contrib/floating_ip_dns.py +++ /dev/null @@ -1,227 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Andrew Bogott for the Wikimedia 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 urllib - -import webob - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import exception -from nova import log as logging -from nova import network - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib.floating_ip_dns') - - -def make_dns_entry(elem): - elem.set('id') - elem.set('ip') - elem.set('type') - elem.set('zone') - elem.set('name') - - -def make_zone_entry(elem): - elem.set('zone') - - -class FloatingIPDNSTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('dns_entry', - selector='dns_entry') - make_dns_entry(root) - return xmlutil.MasterTemplate(root, 1) - - -class FloatingIPDNSsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('dns_entries') - elem = xmlutil.SubTemplateElement(root, 'dns_entry', - selector='dns_entries') - make_dns_entry(elem) - return xmlutil.MasterTemplate(root, 1) - - -class ZonesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('zones') - elem = xmlutil.SubTemplateElement(root, 'zone', - selector='zones') - make_zone_entry(elem) - return xmlutil.MasterTemplate(root, 1) - - -def _translate_dns_entry_view(dns_entry): - result = {} - result['ip'] = dns_entry.get('ip') - result['id'] = dns_entry.get('id') - result['type'] = dns_entry.get('type') - result['zone'] = dns_entry.get('zone') - result['name'] = dns_entry.get('name') - return {'dns_entry': result} - - -def _translate_dns_entries_view(dns_entries): - return {'dns_entries': [_translate_dns_entry_view(entry)['dns_entry'] - for entry in dns_entries]} - - -def _translate_zone_entries_view(zonelist): - return {'zones': [{'zone': zone} for zone in zonelist]} - - -def _unquote_zone(zone): - """Unquoting function for receiving a zone name in a URL. - - Zone 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. - """ - return urllib.unquote(zone).replace('%2E', '.') - - -def _create_dns_entry(ip, name, zone): - return {'ip': ip, 'name': name, 'zone': zone} - - -class FloatingIPDNSController(object): - """DNS Entry controller for OpenStack API""" - - def __init__(self): - self.network_api = network.API() - super(FloatingIPDNSController, self).__init__() - - @wsgi.serializers(xml=FloatingIPDNSsTemplate) - def show(self, req, id): - """Return a list of dns entries. If ip is specified, query for - names. if name is specified, query for ips. - Quoted domain (aka 'zone') specified as id.""" - context = req.environ['nova.context'] - params = req.GET - floating_ip = params['ip'] if 'ip' in params else "" - name = params['name'] if 'name' in params else "" - zone = _unquote_zone(id) - - if floating_ip: - entries = self.network_api.get_dns_entries_by_address(context, - floating_ip, - zone) - entrylist = [_create_dns_entry(floating_ip, entry, zone) - for entry in entries] - elif name: - entries = self.network_api.get_dns_entries_by_name(context, - name, zone) - entrylist = [_create_dns_entry(entry, name, zone) - for entry in entries] - else: - entrylist = [] - - return _translate_dns_entries_view(entrylist) - - @wsgi.serializers(xml=ZonesTemplate) - def index(self, req): - """Return a list of available DNS zones.""" - - context = req.environ['nova.context'] - zones = self.network_api.get_dns_zones(context) - - return _translate_zone_entries_view(zones) - - @wsgi.serializers(xml=FloatingIPDNSTemplate) - def create(self, req, body): - """Add dns entry for name and address""" - context = req.environ['nova.context'] - - try: - entry = body['dns_entry'] - address = entry['ip'] - name = entry['name'] - dns_type = entry['dns_type'] - zone = entry['zone'] - except (TypeError, KeyError): - raise webob.exc.HTTPUnprocessableEntity() - - try: - self.network_api.add_dns_entry(context, address, name, - dns_type, zone) - except exception.FloatingIpDNSExists: - return webob.Response(status_int=409) - - return _translate_dns_entry_view({'ip': address, - 'name': name, - 'type': dns_type, - 'zone': zone}) - - def update(self, req, id, body): - """Modify a dns entry.""" - context = req.environ['nova.context'] - zone = _unquote_zone(id) - - try: - entry = body['dns_entry'] - name = entry['name'] - new_ip = entry['ip'] - except (TypeError, KeyError): - raise webob.exc.HTTPUnprocessableEntity() - - try: - self.network_api.modify_dns_entry(context, name, - new_ip, zone) - except exception.NotFound: - return webob.Response(status_int=404) - - return _translate_dns_entry_view({'ip': new_ip, - 'name': name, - 'zone': zone}) - - def delete(self, req, id): - """Delete the entry identified by req and id. """ - context = req.environ['nova.context'] - params = req.GET - name = params['name'] if 'name' in params else "" - zone = _unquote_zone(id) - - try: - self.network_api.delete_dns_entry(context, name, zone) - except exception.NotFound: - return webob.Response(status_int=404) - - return webob.Response(status_int=200) - - -class Floating_ip_dns(extensions.ExtensionDescriptor): - """Floating IP DNS support""" - - name = "Floating_ip_dns" - alias = "os-floating-ip-dns" - namespace = "http://docs.openstack.org/ext/floating_ip_dns/api/v1.1" - updated = "2011-12-23:00:00+00:00" - - def __init__(self, ext_mgr): - self.network_api = network.API() - super(Floating_ip_dns, self).__init__(ext_mgr) - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-floating-ip-dns', - FloatingIPDNSController()) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/floating_ip_pools.py b/nova/api/openstack/v2/contrib/floating_ip_pools.py deleted file mode 100644 index 9d6386f25..000000000 --- a/nova/api/openstack/v2/contrib/floating_ip_pools.py +++ /dev/null @@ -1,104 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay 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 nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import log as logging -from nova import network - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib.floating_ip_poolss') - - -def _translate_floating_ip_view(pool): - return { - 'name': pool['name'], - } - - -def _translate_floating_ip_pools_view(pools): - return { - 'floating_ip_pools': [_translate_floating_ip_view(pool) - for pool in pools] - } - - -class FloatingIPPoolsController(object): - """The Floating IP Pool API controller for the OpenStack API.""" - - def __init__(self): - self.network_api = network.API() - super(FloatingIPPoolsController, self).__init__() - - def index(self, req): - """Return a list of pools.""" - context = req.environ['nova.context'] - pools = self.network_api.get_floating_ip_pools(context) - return _translate_floating_ip_pools_view(pools) - - -def make_float_ip(elem): - elem.set('name') - - -class FloatingIPPoolTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('floating_ip_pool', - selector='floating_ip_pool') - make_float_ip(root) - return xmlutil.MasterTemplate(root, 1) - - -class FloatingIPPoolsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('floating_ip_pools') - elem = xmlutil.SubTemplateElement(root, 'floating_ip_pool', - selector='floating_ip_pools') - make_float_ip(elem) - return xmlutil.MasterTemplate(root, 1) - - -class FloatingIPPoolsSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return FloatingIPPoolsTemplate() - - -class Floating_ip_pools(extensions.ExtensionDescriptor): - """Floating IPs support""" - - name = "Floating_ip_pools" - alias = "os-floating-ip-pools" - namespace = \ - "http://docs.openstack.org/compute/ext/floating_ip_pools/api/v1.1" - updated = "2012-01-04T00:00:00+00:00" - - def get_resources(self): - resources = [] - - body_serializers = { - 'application/xml': FloatingIPPoolsSerializer(), - } - - serializer = wsgi.ResponseSerializer(body_serializers) - - res = extensions.ResourceExtension('os-floating-ip-pools', - FloatingIPPoolsController(), - serializer=serializer, - member_actions={}) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/floating_ips.py b/nova/api/openstack/v2/contrib/floating_ips.py deleted file mode 100644 index 3a6f4ee34..000000000 --- a/nova/api/openstack/v2/contrib/floating_ips.py +++ /dev/null @@ -1,237 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 Grid Dynamics -# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev -# -# 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 import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import compute -from nova import exception -from nova import log as logging -from nova import network -from nova import rpc - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib.floating_ips') - - -def make_float_ip(elem): - elem.set('id') - elem.set('ip') - elem.set('pool') - elem.set('fixed_ip') - elem.set('instance_id') - - -class FloatingIPTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('floating_ip', - selector='floating_ip') - make_float_ip(root) - return xmlutil.MasterTemplate(root, 1) - - -class FloatingIPsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('floating_ips') - elem = xmlutil.SubTemplateElement(root, 'floating_ip', - selector='floating_ips') - make_float_ip(elem) - return xmlutil.MasterTemplate(root, 1) - - -def _translate_floating_ip_view(floating_ip): - result = { - 'id': floating_ip['id'], - 'ip': floating_ip['address'], - 'pool': floating_ip['pool'], - } - try: - result['fixed_ip'] = floating_ip['fixed_ip']['address'] - except (TypeError, KeyError): - result['fixed_ip'] = None - try: - result['instance_id'] = floating_ip['fixed_ip']['instance']['uuid'] - except (TypeError, KeyError): - result['instance_id'] = None - return {'floating_ip': result} - - -def _translate_floating_ips_view(floating_ips): - return {'floating_ips': [_translate_floating_ip_view(ip)['floating_ip'] - for ip in floating_ips]} - - -class FloatingIPController(object): - """The Floating IPs API controller for the OpenStack API.""" - - def __init__(self): - self.network_api = network.API() - super(FloatingIPController, self).__init__() - - @wsgi.serializers(xml=FloatingIPTemplate) - def show(self, req, id): - """Return data about the given floating ip.""" - context = req.environ['nova.context'] - - try: - floating_ip = self.network_api.get_floating_ip(context, id) - except exception.NotFound: - raise webob.exc.HTTPNotFound() - - return _translate_floating_ip_view(floating_ip) - - @wsgi.serializers(xml=FloatingIPsTemplate) - def index(self, req): - """Return a list of floating ips allocated to a project.""" - context = req.environ['nova.context'] - - floating_ips = self.network_api.get_floating_ips_by_project(context) - - return _translate_floating_ips_view(floating_ips) - - @wsgi.serializers(xml=FloatingIPTemplate) - def create(self, req, body=None): - context = req.environ['nova.context'] - - pool = None - if body and 'pool' in body: - pool = body['pool'] - try: - address = self.network_api.allocate_floating_ip(context, pool) - ip = self.network_api.get_floating_ip_by_address(context, address) - except rpc.RemoteError as ex: - # NOTE(tr3buchet) - why does this block exist? - if ex.exc_type == 'NoMoreFloatingIps': - if pool: - msg = _("No more floating ips in pool %s.") % pool - else: - msg = _("No more floating ips available.") - raise webob.exc.HTTPBadRequest(explanation=msg) - else: - raise - - return _translate_floating_ip_view(ip) - - def delete(self, req, id): - context = req.environ['nova.context'] - floating_ip = self.network_api.get_floating_ip(context, id) - - if floating_ip.get('fixed_ip'): - self.network_api.disassociate_floating_ip(context, - floating_ip['address']) - - self.network_api.release_floating_ip(context, - address=floating_ip['address']) - return webob.Response(status_int=202) - - def _get_ip_by_id(self, context, value): - """Checks that value is id and then returns its address.""" - return self.network_api.get_floating_ip(context, value)['address'] - - -class FloatingIPSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return FloatingIPsTemplate() - - def default(self): - return FloatingIPTemplate() - - -class Floating_ips(extensions.ExtensionDescriptor): - """Floating IPs support""" - - name = "Floating_ips" - alias = "os-floating-ips" - namespace = "http://docs.openstack.org/compute/ext/floating_ips/api/v1.1" - updated = "2011-06-16T00:00:00+00:00" - - def __init__(self, ext_mgr): - self.compute_api = compute.API() - self.network_api = network.API() - super(Floating_ips, self).__init__(ext_mgr) - - def _add_floating_ip(self, input_dict, req, instance_id): - """Associate floating_ip to an instance.""" - context = req.environ['nova.context'] - - try: - address = input_dict['addFloatingIp']['address'] - except TypeError: - msg = _("Missing parameter dict") - raise webob.exc.HTTPBadRequest(explanation=msg) - except KeyError: - msg = _("Address not specified") - raise webob.exc.HTTPBadRequest(explanation=msg) - - try: - instance = self.compute_api.get(context, instance_id) - self.compute_api.associate_floating_ip(context, instance, - address) - except exception.ApiError, e: - raise webob.exc.HTTPBadRequest(explanation=e.message) - except exception.NotAuthorized, e: - raise webob.exc.HTTPUnauthorized() - - return webob.Response(status_int=202) - - def _remove_floating_ip(self, input_dict, req, instance_id): - """Dissociate floating_ip from an instance.""" - context = req.environ['nova.context'] - - try: - address = input_dict['removeFloatingIp']['address'] - except TypeError: - msg = _("Missing parameter dict") - raise webob.exc.HTTPBadRequest(explanation=msg) - except KeyError: - msg = _("Address not specified") - raise webob.exc.HTTPBadRequest(explanation=msg) - - floating_ip = self.network_api.get_floating_ip_by_address(context, - address) - if floating_ip.get('fixed_ip'): - try: - self.network_api.disassociate_floating_ip(context, address) - except exception.NotAuthorized, e: - raise webob.exc.HTTPUnauthorized() - - return webob.Response(status_int=202) - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-floating-ips', - FloatingIPController(), - member_actions={}) - resources.append(res) - - return resources - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - actions = [ - extensions.ActionExtension("servers", "addFloatingIp", - self._add_floating_ip), - extensions.ActionExtension("servers", "removeFloatingIp", - self._remove_floating_ip), - ] - - return actions diff --git a/nova/api/openstack/v2/contrib/hosts.py b/nova/api/openstack/v2/contrib/hosts.py deleted file mode 100644 index 2bb61696e..000000000 --- a/nova/api/openstack/v2/contrib/hosts.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright (c) 2011 Openstack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""The hosts admin extension.""" - -import webob.exc -from xml.dom import minidom -from xml.parsers import expat - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova.scheduler import api as scheduler_api - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.hosts") -FLAGS = flags.FLAGS - - -class HostIndexTemplate(xmlutil.TemplateBuilder): - def construct(self): - def shimmer(obj, do_raise=False): - # A bare list is passed in; we need to wrap it in a dict - return dict(hosts=obj) - - root = xmlutil.TemplateElement('hosts', selector=shimmer) - elem = xmlutil.SubTemplateElement(root, 'host', selector='hosts') - elem.set('host_name') - elem.set('service') - - return xmlutil.MasterTemplate(root, 1) - - -class HostUpdateTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('host') - root.set('host') - root.set('status') - - return xmlutil.MasterTemplate(root, 1) - - -class HostActionTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('host') - root.set('host') - root.set('power_action') - - return xmlutil.MasterTemplate(root, 1) - - -class HostDeserializer(wsgi.XMLDeserializer): - def default(self, string): - try: - node = minidom.parseString(string) - except expat.ExpatError: - msg = _("cannot understand XML") - raise exception.MalformedRequestBody(reason=msg) - - updates = {} - for child in node.childNodes[0].childNodes: - updates[child.tagName] = self.extract_text(child) - - return dict(body=updates) - - -def _list_hosts(req, service=None): - """Returns a summary list of hosts, optionally filtering - by service type. - """ - context = req.environ['nova.context'] - hosts = scheduler_api.get_host_list(context) - if service: - hosts = [host for host in hosts - if host["service"] == service] - return hosts - - -def check_host(fn): - """Makes sure that the host exists.""" - def wrapped(self, req, id, service=None, *args, **kwargs): - listed_hosts = _list_hosts(req, service) - hosts = [h["host_name"] for h in listed_hosts] - if id in hosts: - return fn(self, req, id, *args, **kwargs) - else: - raise exception.HostNotFound(host=id) - return wrapped - - -class HostController(object): - """The Hosts API controller for the OpenStack API.""" - def __init__(self): - self.compute_api = compute.API() - super(HostController, self).__init__() - - @wsgi.serializers(xml=HostIndexTemplate) - def index(self, req): - return {'hosts': _list_hosts(req)} - - @wsgi.serializers(xml=HostUpdateTemplate) - @wsgi.deserializers(xml=HostDeserializer) - @check_host - def update(self, req, id, body): - for raw_key, raw_val in body.iteritems(): - key = raw_key.lower().strip() - val = raw_val.lower().strip() - # NOTE: (dabo) Right now only 'status' can be set, but other - # settings may follow. - if key == "status": - if val[:6] in ("enable", "disabl"): - return self._set_enabled_status(req, id, - enabled=(val.startswith("enable"))) - else: - explanation = _("Invalid status: '%s'") % raw_val - raise webob.exc.HTTPBadRequest(explanation=explanation) - else: - explanation = _("Invalid update setting: '%s'") % raw_key - raise webob.exc.HTTPBadRequest(explanation=explanation) - - def _set_enabled_status(self, req, host, enabled): - """Sets the specified host's ability to accept new instances.""" - context = req.environ['nova.context'] - state = "enabled" if enabled else "disabled" - LOG.audit(_("Setting host %(host)s to %(state)s.") % locals()) - result = self.compute_api.set_host_enabled(context, host=host, - enabled=enabled) - if result not in ("enabled", "disabled"): - # An error message was returned - raise webob.exc.HTTPBadRequest(explanation=result) - return {"host": host, "status": result} - - def _host_power_action(self, req, host, action): - """Reboots, shuts down or powers up the host.""" - context = req.environ['nova.context'] - try: - result = self.compute_api.host_power_action(context, host=host, - action=action) - except NotImplementedError as e: - raise webob.exc.HTTPBadRequest(explanation=e.msg) - return {"host": host, "power_action": result} - - @wsgi.serializers(xml=HostActionTemplate) - def startup(self, req, id): - return self._host_power_action(req, host=id, action="startup") - - @wsgi.serializers(xml=HostActionTemplate) - def shutdown(self, req, id): - return self._host_power_action(req, host=id, action="shutdown") - - @wsgi.serializers(xml=HostActionTemplate) - def reboot(self, req, id): - return self._host_power_action(req, host=id, action="reboot") - - -class Hosts(extensions.ExtensionDescriptor): - """Admin-only host administration""" - - name = "Hosts" - alias = "os-hosts" - namespace = "http://docs.openstack.org/compute/ext/hosts/api/v1.1" - updated = "2011-06-29T00:00:00+00:00" - admin_only = True - - def get_resources(self): - resources = [extensions.ResourceExtension('os-hosts', - HostController(), - collection_actions={'update': 'PUT'}, - member_actions={"startup": "GET", "shutdown": "GET", - "reboot": "GET"})] - return resources diff --git a/nova/api/openstack/v2/contrib/keypairs.py b/nova/api/openstack/v2/contrib/keypairs.py deleted file mode 100644 index 2dc9b063d..000000000 --- a/nova/api/openstack/v2/contrib/keypairs.py +++ /dev/null @@ -1,163 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" Keypair management extension""" - -import os -import shutil -import tempfile - -import webob -from webob import exc - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import crypto -from nova import db -from nova import exception - - -class KeypairTemplate(xmlutil.TemplateBuilder): - def construct(self): - return xmlutil.MasterTemplate(xmlutil.make_flat_dict('keypair'), 1) - - -class KeypairsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('keypairs') - elem = xmlutil.make_flat_dict('keypair', selector='keypairs', - subselector='keypair') - root.append(elem) - - return xmlutil.MasterTemplate(root, 1) - - -class KeypairController(object): - """ Keypair API controller for the Openstack API """ - - # TODO(ja): both this file and nova.api.ec2.cloud.py have similar logic. - # move the common keypair logic to nova.compute.API? - - def _gen_key(self): - """ - Generate a key - """ - private_key, public_key, fingerprint = crypto.generate_key_pair() - return {'private_key': private_key, - 'public_key': public_key, - 'fingerprint': fingerprint} - - @wsgi.serializers(xml=KeypairTemplate) - def create(self, req, body): - """ - Create or import keypair. - - Sending name will generate a key and return private_key - and fingerprint. - - You can send a public_key to add an existing ssh key - - params: keypair object with: - name (required) - string - public_key (optional) - string - """ - - context = req.environ['nova.context'] - params = body['keypair'] - name = params['name'] - - # NOTE(ja): generation is slow, so shortcut invalid name exception - try: - db.key_pair_get(context, context.user_id, name) - raise exception.KeyPairExists(key_name=name) - except exception.NotFound: - pass - - keypair = {'user_id': context.user_id, - 'name': name} - - # import if public_key is sent - if 'public_key' in params: - tmpdir = tempfile.mkdtemp() - fn = os.path.join(tmpdir, 'import.pub') - with open(fn, 'w') as pub: - pub.write(params['public_key']) - fingerprint = crypto.generate_fingerprint(fn) - shutil.rmtree(tmpdir) - keypair['public_key'] = params['public_key'] - keypair['fingerprint'] = fingerprint - else: - generated_key = self._gen_key() - keypair['private_key'] = generated_key['private_key'] - keypair['public_key'] = generated_key['public_key'] - keypair['fingerprint'] = generated_key['fingerprint'] - - db.key_pair_create(context, keypair) - return {'keypair': keypair} - - def delete(self, req, id): - """ - Delete a keypair with a given name - """ - context = req.environ['nova.context'] - db.key_pair_destroy(context, context.user_id, id) - return webob.Response(status_int=202) - - @wsgi.serializers(xml=KeypairsTemplate) - def index(self, req): - """ - List of keypairs for a user - """ - context = req.environ['nova.context'] - key_pairs = db.key_pair_get_all_by_user(context, context.user_id) - rval = [] - for key_pair in key_pairs: - rval.append({'keypair': { - 'name': key_pair['name'], - 'public_key': key_pair['public_key'], - 'fingerprint': key_pair['fingerprint'], - }}) - - return {'keypairs': rval} - - -class KeypairsSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return KeypairsTemplate() - - def default(self): - return KeypairTemplate() - - -class Keypairs(extensions.ExtensionDescriptor): - """Keypair Support""" - - name = "Keypairs" - alias = "os-keypairs" - namespace = "http://docs.openstack.org/compute/ext/keypairs/api/v1.1" - updated = "2011-08-08T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'os-keypairs', - KeypairController()) - - resources.append(res) - return resources diff --git a/nova/api/openstack/v2/contrib/multinic.py b/nova/api/openstack/v2/contrib/multinic.py deleted file mode 100644 index 6719fb18a..000000000 --- a/nova/api/openstack/v2/contrib/multinic.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""The multinic extension.""" - -import webob -from webob import exc - -from nova.api.openstack.v2 import extensions -from nova import compute -from nova import exception -from nova import log as logging - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.multinic") - - -# Note: The class name is as it has to be for this to be loaded as an -# extension--only first character capitalized. -class Multinic(extensions.ExtensionDescriptor): - """Multiple network support""" - - name = "Multinic" - alias = "NMN" - namespace = "http://docs.openstack.org/compute/ext/multinic/api/v1.1" - updated = "2011-06-09T00:00:00+00:00" - - def __init__(self, ext_mgr): - """Initialize the extension. - - Gets a compute.API object so we can call the back-end - add_fixed_ip() and remove_fixed_ip() methods. - """ - - super(Multinic, self).__init__(ext_mgr) - self.compute_api = compute.API() - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - - actions = [] - - # Add the add_fixed_ip action - act = extensions.ActionExtension("servers", "addFixedIp", - self._add_fixed_ip) - actions.append(act) - - # Add the remove_fixed_ip action - act = extensions.ActionExtension("servers", "removeFixedIp", - self._remove_fixed_ip) - actions.append(act) - - return actions - - def _get_instance(self, context, instance_id): - try: - return self.compute_api.get(context, instance_id) - except exception.InstanceNotFound: - msg = _("Server not found") - raise exc.HTTPNotFound(msg) - - def _add_fixed_ip(self, input_dict, req, id): - """Adds an IP on a given network to an instance.""" - - # Validate the input entity - if 'networkId' not in input_dict['addFixedIp']: - msg = _("Missing 'networkId' argument for addFixedIp") - raise exc.HTTPUnprocessableEntity(explanation=msg) - - context = req.environ['nova.context'] - instance = self._get_instance(context, id) - network_id = input_dict['addFixedIp']['networkId'] - self.compute_api.add_fixed_ip(context, instance, network_id) - return webob.Response(status_int=202) - - def _remove_fixed_ip(self, input_dict, req, id): - """Removes an IP from an instance.""" - - # Validate the input entity - if 'address' not in input_dict['removeFixedIp']: - msg = _("Missing 'address' argument for removeFixedIp") - raise exc.HTTPUnprocessableEntity(explanation=msg) - - context = req.environ['nova.context'] - instance = self._get_instance(context, id) - address = input_dict['removeFixedIp']['address'] - - try: - self.compute_api.remove_fixed_ip(context, instance, address) - except exceptions.FixedIpNotFoundForSpecificInstance: - LOG.exception(_("Unable to find address %r") % address) - raise exc.HTTPBadRequest() - - return webob.Response(status_int=202) diff --git a/nova/api/openstack/v2/contrib/networks.py b/nova/api/openstack/v2/contrib/networks.py deleted file mode 100644 index 4a96e534f..000000000 --- a/nova/api/openstack/v2/contrib/networks.py +++ /dev/null @@ -1,117 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Grid Dynamics -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -from webob import exc - -from nova.api.openstack.v2 import extensions -from nova import exception -from nova import flags -from nova import log as logging -import nova.network.api - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack.v2.contrib.networks') - - -def network_dict(network): - if network: - fields = ('bridge', 'vpn_public_port', 'dhcp_start', - 'bridge_interface', 'updated_at', 'id', 'cidr_v6', - 'deleted_at', 'gateway', 'label', 'project_id', - 'vpn_private_address', 'deleted', 'vlan', 'broadcast', - 'netmask', 'injected', 'cidr', 'vpn_public_address', - 'multi_host', 'dns1', 'host', 'gateway_v6', 'netmask_v6', - 'created_at') - return dict((field, network[field]) for field in fields) - else: - return {} - - -class NetworkController(object): - - def __init__(self, network_api=None): - self.network_api = network_api or nova.network.api.API() - - def action(self, req, id, body): - _actions = { - 'disassociate': self._disassociate, - } - - for action, data in body.iteritems(): - try: - return _actions[action](req, id, body) - except KeyError: - msg = _("Network does not have %s action") % action - raise exc.HTTPBadRequest(explanation=msg) - - raise exc.HTTPBadRequest(explanation=_("Invalid request body")) - - def _disassociate(self, request, network_id, body): - context = request.environ['nova.context'] - LOG.debug(_("Disassociating network with id %s" % network_id)) - try: - self.network_api.disassociate(context, network_id) - except exception.NetworkNotFound: - raise exc.HTTPNotFound(_("Network not found")) - return exc.HTTPAccepted() - - def index(self, req): - context = req.environ['nova.context'] - networks = self.network_api.get_all(context) - result = [network_dict(net_ref) for net_ref in networks] - return {'networks': result} - - def show(self, req, id): - context = req.environ['nova.context'] - LOG.debug(_("Showing network with id %s") % id) - try: - network = self.network_api.get(context, id) - except exception.NetworkNotFound: - raise exc.HTTPNotFound(_("Network not found")) - return {'network': network_dict(network)} - - def delete(self, req, id): - context = req.environ['nova.context'] - LOG.info(_("Deleting network with id %s") % id) - try: - self.network_api.delete(context, id) - except exception.NetworkNotFound: - raise exc.HTTPNotFound(_("Network not found")) - return exc.HTTPAccepted() - - def create(self, req, id, body=None): - raise exc.HTTPNotImplemented() - - -class Networks(extensions.ExtensionDescriptor): - """Admin-only Network Management Extension""" - - name = "Networks" - alias = "os-networks" - namespace = "http://docs.openstack.org/compute/ext/networks/api/v1.1" - updated = "2011-12-23 00:00:00" - admin_only = True - - def get_resources(self): - member_actions = {'action': 'POST'} - res = extensions.ResourceExtension('os-networks', - NetworkController(), - member_actions=member_actions) - return [res] diff --git a/nova/api/openstack/v2/contrib/quotas.py b/nova/api/openstack/v2/contrib/quotas.py deleted file mode 100644 index 191553a02..000000000 --- a/nova/api/openstack/v2/contrib/quotas.py +++ /dev/null @@ -1,102 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import db -from nova import exception -from nova import quota - - -quota_resources = ['metadata_items', 'injected_file_content_bytes', - 'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances', - 'injected_files', 'cores'] - - -class QuotaTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('quota_set', selector='quota_set') - root.set('id') - - for resource in quota_resources: - elem = xmlutil.SubTemplateElement(root, resource) - elem.text = resource - - return xmlutil.MasterTemplate(root, 1) - - -class QuotaSetsController(object): - - def _format_quota_set(self, project_id, quota_set): - """Convert the quota object to a result dict""" - - result = dict(id=str(project_id)) - - for resource in quota_resources: - result[resource] = quota_set[resource] - - return dict(quota_set=result) - - @wsgi.serializers(xml=QuotaTemplate) - def show(self, req, id): - context = req.environ['nova.context'] - try: - db.sqlalchemy.api.authorize_project_context(context, id) - return self._format_quota_set(id, - quota.get_project_quotas(context, id)) - except exception.NotAuthorized: - raise webob.exc.HTTPForbidden() - - @wsgi.serializers(xml=QuotaTemplate) - def update(self, req, id, body): - context = req.environ['nova.context'] - project_id = id - for key in body['quota_set'].keys(): - if key in quota_resources: - value = int(body['quota_set'][key]) - try: - db.quota_update(context, project_id, key, value) - except exception.ProjectQuotaNotFound: - db.quota_create(context, project_id, key, value) - except exception.AdminRequired: - raise webob.exc.HTTPForbidden() - return {'quota_set': quota.get_project_quotas(context, project_id)} - - def defaults(self, req, id): - return self._format_quota_set(id, quota._get_default_quotas()) - - -class Quotas(extensions.ExtensionDescriptor): - """Quotas management support""" - - name = "Quotas" - alias = "os-quota-sets" - namespace = "http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1" - updated = "2011-08-08T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-quota-sets', - QuotaSetsController(), - member_actions={'defaults': 'GET'}) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/rescue.py b/nova/api/openstack/v2/contrib/rescue.py deleted file mode 100644 index 20c18028d..000000000 --- a/nova/api/openstack/v2/contrib/rescue.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2011 Openstack, LLC. -# -# 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. - -"""The rescue mode extension.""" - -import webob -from webob import exc - -from nova.api.openstack.v2 import extensions as exts -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.v2.contrib.rescue") - - -class Rescue(exts.ExtensionDescriptor): - """Instance rescue mode""" - - name = "Rescue" - alias = "os-rescue" - namespace = "http://docs.openstack.org/compute/ext/rescue/api/v1.1" - updated = "2011-08-18T00:00:00+00:00" - - def __init__(self, ext_mgr): - super(Rescue, self).__init__(ext_mgr) - self.compute_api = compute.API() - - def _get_instance(self, context, instance_id): - try: - return self.compute_api.get(context, instance_id) - except exception.InstanceNotFound: - msg = _("Server not found") - raise exc.HTTPNotFound(msg) - - @exts.wrap_errors - def _rescue(self, input_dict, req, instance_id): - """Rescue an instance.""" - context = req.environ["nova.context"] - - if input_dict['rescue'] and 'adminPass' in input_dict['rescue']: - password = input_dict['rescue']['adminPass'] - else: - password = utils.generate_password(FLAGS.password_length) - - instance = self._get_instance(context, instance_id) - self.compute_api.rescue(context, instance, rescue_password=password) - return {'adminPass': password} - - @exts.wrap_errors - def _unrescue(self, input_dict, req, instance_id): - """Unrescue an instance.""" - context = req.environ["nova.context"] - instance = self._get_instance(context, instance_id) - self.compute_api.unrescue(context, instance) - return webob.Response(status_int=202) - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - actions = [ - exts.ActionExtension("servers", "rescue", self._rescue), - exts.ActionExtension("servers", "unrescue", self._unrescue), - ] - - return actions diff --git a/nova/api/openstack/v2/contrib/security_groups.py b/nova/api/openstack/v2/contrib/security_groups.py deleted file mode 100644 index f4abbcd51..000000000 --- a/nova/api/openstack/v2/contrib/security_groups.py +++ /dev/null @@ -1,592 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""The security groups extension.""" - -import urllib -from xml.dom import minidom - -from webob import exc -import webob - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.security_groups") -FLAGS = flags.FLAGS - - -def make_rule(elem): - elem.set('id') - elem.set('parent_group_id') - - proto = xmlutil.SubTemplateElement(elem, 'ip_protocol') - proto.text = 'ip_protocol' - - from_port = xmlutil.SubTemplateElement(elem, 'from_port') - from_port.text = 'from_port' - - to_port = xmlutil.SubTemplateElement(elem, 'to_port') - to_port.text = 'to_port' - - group = xmlutil.SubTemplateElement(elem, 'group', selector='group') - name = xmlutil.SubTemplateElement(group, 'name') - name.text = 'name' - tenant_id = xmlutil.SubTemplateElement(group, 'tenant_id') - tenant_id.text = 'tenant_id' - - ip_range = xmlutil.SubTemplateElement(elem, 'ip_range', - selector='ip_range') - cidr = xmlutil.SubTemplateElement(ip_range, 'cidr') - cidr.text = 'cidr' - - -def make_sg(elem): - elem.set('id') - elem.set('tenant_id') - elem.set('name') - - desc = xmlutil.SubTemplateElement(elem, 'description') - desc.text = 'description' - - rules = xmlutil.SubTemplateElement(elem, 'rules') - rule = xmlutil.SubTemplateElement(rules, 'rule', selector='rules') - make_rule(rule) - - -sg_nsmap = {None: wsgi.XMLNS_V11} - - -class SecurityGroupRuleTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('security_group_rule', - selector='security_group_rule') - make_rule(root) - return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) - - -class SecurityGroupTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('security_group', - selector='security_group') - make_sg(root) - return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) - - -class SecurityGroupsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('security_groups') - elem = xmlutil.SubTemplateElement(root, 'security_group', - selector='security_groups') - make_sg(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) - - -class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): - """ - Deserializer to handle xml-formatted security group requests. - """ - def default(self, string): - """Deserialize an xml-formatted security group create request""" - dom = minidom.parseString(string) - security_group = {} - sg_node = self.find_first_child_named(dom, - 'security_group') - if sg_node is not None: - if sg_node.hasAttribute('name'): - security_group['name'] = sg_node.getAttribute('name') - desc_node = self.find_first_child_named(sg_node, - "description") - if desc_node: - security_group['description'] = self.extract_text(desc_node) - return {'body': {'security_group': security_group}} - - -class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer): - """ - Deserializer to handle xml-formatted security group requests. - """ - - def default(self, string): - """Deserialize an xml-formatted security group create request""" - dom = minidom.parseString(string) - security_group_rule = self._extract_security_group_rule(dom) - return {'body': {'security_group_rule': security_group_rule}} - - def _extract_security_group_rule(self, node): - """Marshal the security group rule attribute of a parsed request""" - sg_rule = {} - sg_rule_node = self.find_first_child_named(node, - 'security_group_rule') - if sg_rule_node is not None: - ip_protocol_node = self.find_first_child_named(sg_rule_node, - "ip_protocol") - if ip_protocol_node is not None: - sg_rule['ip_protocol'] = self.extract_text(ip_protocol_node) - - from_port_node = self.find_first_child_named(sg_rule_node, - "from_port") - if from_port_node is not None: - sg_rule['from_port'] = self.extract_text(from_port_node) - - to_port_node = self.find_first_child_named(sg_rule_node, "to_port") - if to_port_node is not None: - sg_rule['to_port'] = self.extract_text(to_port_node) - - parent_group_id_node = self.find_first_child_named(sg_rule_node, - "parent_group_id") - if parent_group_id_node is not None: - sg_rule['parent_group_id'] = self.extract_text( - parent_group_id_node) - - group_id_node = self.find_first_child_named(sg_rule_node, - "group_id") - if group_id_node is not None: - sg_rule['group_id'] = self.extract_text(group_id_node) - - cidr_node = self.find_first_child_named(sg_rule_node, "cidr") - if cidr_node is not None: - sg_rule['cidr'] = self.extract_text(cidr_node) - - return sg_rule - - -class SecurityGroupController(object): - """The Security group API controller for the OpenStack API.""" - - def __init__(self): - self.compute_api = compute.API() - super(SecurityGroupController, self).__init__() - - def _format_security_group_rule(self, context, rule): - sg_rule = {} - sg_rule['id'] = rule.id - sg_rule['parent_group_id'] = rule.parent_group_id - sg_rule['ip_protocol'] = rule.protocol - sg_rule['from_port'] = rule.from_port - sg_rule['to_port'] = rule.to_port - sg_rule['group'] = {} - sg_rule['ip_range'] = {} - if rule.group_id: - source_group = db.security_group_get(context, rule.group_id) - sg_rule['group'] = {'name': source_group.name, - 'tenant_id': source_group.project_id} - else: - sg_rule['ip_range'] = {'cidr': rule.cidr} - return sg_rule - - def _format_security_group(self, context, group): - security_group = {} - security_group['id'] = group.id - security_group['description'] = group.description - security_group['name'] = group.name - security_group['tenant_id'] = group.project_id - security_group['rules'] = [] - for rule in group.rules: - security_group['rules'] += [self._format_security_group_rule( - context, rule)] - return security_group - - def _get_security_group(self, context, id): - try: - id = int(id) - security_group = db.security_group_get(context, id) - except ValueError: - msg = _("Security group id should be integer") - raise exc.HTTPBadRequest(explanation=msg) - except exception.NotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - return security_group - - @wsgi.serializers(xml=SecurityGroupTemplate) - def show(self, req, id): - """Return data about the given security group.""" - context = req.environ['nova.context'] - security_group = self._get_security_group(context, id) - return {'security_group': self._format_security_group(context, - security_group)} - - def delete(self, req, id): - """Delete a security group.""" - context = req.environ['nova.context'] - security_group = self._get_security_group(context, id) - LOG.audit(_("Delete security group %s"), id, context=context) - db.security_group_destroy(context, security_group.id) - - return webob.Response(status_int=202) - - @wsgi.serializers(xml=SecurityGroupsTemplate) - def index(self, req): - """Returns a list of security groups""" - context = req.environ['nova.context'] - - self.compute_api.ensure_default_security_group(context) - groups = db.security_group_get_by_project(context, - context.project_id) - limited_list = common.limited(groups, req) - result = [self._format_security_group(context, group) - for group in limited_list] - - return {'security_groups': - list(sorted(result, - key=lambda k: (k['tenant_id'], k['name'])))} - - @wsgi.serializers(xml=SecurityGroupTemplate) - @wsgi.deserializers(xml=SecurityGroupXMLDeserializer) - def create(self, req, body): - """Creates a new security group.""" - context = req.environ['nova.context'] - if not body: - raise exc.HTTPUnprocessableEntity() - - security_group = body.get('security_group', None) - - if security_group is None: - raise exc.HTTPUnprocessableEntity() - - group_name = security_group.get('name', None) - group_description = security_group.get('description', None) - - self._validate_security_group_property(group_name, "name") - self._validate_security_group_property(group_description, - "description") - group_name = group_name.strip() - group_description = group_description.strip() - - LOG.audit(_("Create Security Group %s"), group_name, context=context) - self.compute_api.ensure_default_security_group(context) - if db.security_group_exists(context, context.project_id, group_name): - msg = _('Security group %s already exists') % group_name - raise exc.HTTPBadRequest(explanation=msg) - - group = {'user_id': context.user_id, - 'project_id': context.project_id, - 'name': group_name, - 'description': group_description} - group_ref = db.security_group_create(context, group) - - return {'security_group': self._format_security_group(context, - group_ref)} - - def _validate_security_group_property(self, value, typ): - """ typ will be either 'name' or 'description', - depending on the caller - """ - try: - val = value.strip() - except AttributeError: - msg = _("Security group %s is not a string or unicode") % typ - raise exc.HTTPBadRequest(explanation=msg) - if not val: - msg = _("Security group %s cannot be empty.") % typ - raise exc.HTTPBadRequest(explanation=msg) - if len(val) > 255: - msg = _("Security group %s should not be greater " - "than 255 characters.") % typ - raise exc.HTTPBadRequest(explanation=msg) - - -class SecurityGroupRulesController(SecurityGroupController): - - @wsgi.serializers(xml=SecurityGroupRuleTemplate) - @wsgi.deserializers(xml=SecurityGroupRulesXMLDeserializer) - def create(self, req, body): - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - if not 'security_group_rule' in body: - raise exc.HTTPUnprocessableEntity() - - self.compute_api.ensure_default_security_group(context) - - sg_rule = body['security_group_rule'] - parent_group_id = sg_rule.get('parent_group_id', None) - try: - parent_group_id = int(parent_group_id) - security_group = db.security_group_get(context, parent_group_id) - except ValueError: - msg = _("Parent group id is not integer") - raise exc.HTTPBadRequest(explanation=msg) - except exception.NotFound as exp: - msg = _("Security group (%s) not found") % parent_group_id - raise exc.HTTPNotFound(explanation=msg) - - msg = _("Authorize security group ingress %s") - LOG.audit(msg, security_group['name'], context=context) - - try: - values = self._rule_args_to_dict(context, - to_port=sg_rule.get('to_port'), - from_port=sg_rule.get('from_port'), - parent_group_id=sg_rule.get('parent_group_id'), - ip_protocol=sg_rule.get('ip_protocol'), - cidr=sg_rule.get('cidr'), - group_id=sg_rule.get('group_id')) - except Exception as exp: - raise exc.HTTPBadRequest(explanation=unicode(exp)) - - if values is None: - msg = _("Not enough parameters to build a " - "valid rule.") - raise exc.HTTPBadRequest(explanation=msg) - - values['parent_group_id'] = security_group.id - - if self._security_group_rule_exists(security_group, values): - msg = _('This rule already exists in group %s') % parent_group_id - raise exc.HTTPBadRequest(explanation=msg) - - security_group_rule = db.security_group_rule_create(context, values) - - self.compute_api.trigger_security_group_rules_refresh(context, - security_group_id=security_group['id']) - - return {"security_group_rule": self._format_security_group_rule( - context, - security_group_rule)} - - def _security_group_rule_exists(self, security_group, values): - """Indicates whether the specified rule values are already - defined in the given security group. - """ - for rule in security_group.rules: - if 'group_id' in values: - if rule['group_id'] == values['group_id']: - return True - else: - is_duplicate = True - for key in ('cidr', 'from_port', 'to_port', 'protocol'): - if rule[key] != values[key]: - is_duplicate = False - break - if is_duplicate: - return True - return False - - def _rule_args_to_dict(self, context, to_port=None, from_port=None, - parent_group_id=None, ip_protocol=None, - cidr=None, group_id=None): - values = {} - - if group_id is not None: - try: - parent_group_id = int(parent_group_id) - group_id = int(group_id) - except ValueError: - msg = _("Parent or group id is not integer") - raise exception.InvalidInput(reason=msg) - - if parent_group_id == group_id: - msg = _("Parent group id and group id cannot be same") - raise exception.InvalidInput(reason=msg) - - values['group_id'] = group_id - #check if groupId exists - db.security_group_get(context, group_id) - elif cidr: - # If this fails, it throws an exception. This is what we want. - try: - cidr = urllib.unquote(cidr).decode() - except Exception: - raise exception.InvalidCidr(cidr=cidr) - - if not utils.is_valid_cidr(cidr): - # Raise exception for non-valid address - raise exception.InvalidCidr(cidr=cidr) - - values['cidr'] = cidr - else: - values['cidr'] = '0.0.0.0/0' - - if ip_protocol and from_port and to_port: - - ip_protocol = str(ip_protocol) - try: - from_port = int(from_port) - to_port = int(to_port) - except ValueError: - if ip_protocol.upper() == 'ICMP': - raise exception.InvalidInput(reason="Type and" - " Code must be integers for ICMP protocol type") - else: - raise exception.InvalidInput(reason="To and From ports " - "must be integers") - - if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: - raise exception.InvalidIpProtocol(protocol=ip_protocol) - - # Verify that from_port must always be less than - # or equal to to_port - if from_port > to_port: - raise exception.InvalidPortRange(from_port=from_port, - to_port=to_port, msg="Former value cannot" - " be greater than the later") - - # Verify valid TCP, UDP port ranges - if (ip_protocol.upper() in ['TCP', 'UDP'] and - (from_port < 1 or to_port > 65535)): - raise exception.InvalidPortRange(from_port=from_port, - to_port=to_port, msg="Valid TCP ports should" - " be between 1-65535") - - # Verify ICMP type and code - if (ip_protocol.upper() == "ICMP" and - (from_port < -1 or to_port > 255)): - raise exception.InvalidPortRange(from_port=from_port, - to_port=to_port, msg="For ICMP, the" - " type:code must be valid") - - values['protocol'] = ip_protocol - values['from_port'] = from_port - values['to_port'] = to_port - else: - # If cidr based filtering, protocol and ports are mandatory - if 'cidr' in values: - return None - - return values - - def delete(self, req, id): - context = req.environ['nova.context'] - - self.compute_api.ensure_default_security_group(context) - try: - id = int(id) - rule = db.security_group_rule_get(context, id) - except ValueError: - msg = _("Rule id is not integer") - raise exc.HTTPBadRequest(explanation=msg) - except exception.NotFound as exp: - msg = _("Rule (%s) not found") % id - raise exc.HTTPNotFound(explanation=msg) - - group_id = rule.parent_group_id - self.compute_api.ensure_default_security_group(context) - security_group = db.security_group_get(context, group_id) - - msg = _("Revoke security group ingress %s") - LOG.audit(msg, security_group['name'], context=context) - - db.security_group_rule_destroy(context, rule['id']) - self.compute_api.trigger_security_group_rules_refresh(context, - security_group_id=security_group['id']) - - return webob.Response(status_int=202) - - -class Security_groups(extensions.ExtensionDescriptor): - """Security group support""" - - name = "SecurityGroups" - alias = "security_groups" - namespace = "http://docs.openstack.org/compute/ext/securitygroups/api/v1.1" - updated = "2011-07-21T00:00:00+00:00" - - def __init__(self, ext_mgr): - self.compute_api = compute.API() - super(Security_groups, self).__init__(ext_mgr) - - def _addSecurityGroup(self, input_dict, req, instance_id): - context = req.environ['nova.context'] - - try: - body = input_dict['addSecurityGroup'] - group_name = body['name'] - except TypeError: - msg = _("Missing parameter dict") - raise webob.exc.HTTPBadRequest(explanation=msg) - except KeyError: - msg = _("Security group not specified") - raise webob.exc.HTTPBadRequest(explanation=msg) - - if not group_name or group_name.strip() == '': - msg = _("Security group name cannot be empty") - raise webob.exc.HTTPBadRequest(explanation=msg) - - try: - instance = self.compute_api.get(context, instance_id) - self.compute_api.add_security_group(context, instance, group_name) - except exception.SecurityGroupNotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - except exception.InstanceNotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - except exception.Invalid as exp: - raise exc.HTTPBadRequest(explanation=unicode(exp)) - - return webob.Response(status_int=202) - - def _removeSecurityGroup(self, input_dict, req, instance_id): - context = req.environ['nova.context'] - - try: - body = input_dict['removeSecurityGroup'] - group_name = body['name'] - except TypeError: - msg = _("Missing parameter dict") - raise webob.exc.HTTPBadRequest(explanation=msg) - except KeyError: - msg = _("Security group not specified") - raise webob.exc.HTTPBadRequest(explanation=msg) - - if not group_name or group_name.strip() == '': - msg = _("Security group name cannot be empty") - raise webob.exc.HTTPBadRequest(explanation=msg) - - try: - instance = self.compute_api.get(context, instance_id) - self.compute_api.remove_security_group(context, instance, - group_name) - except exception.SecurityGroupNotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - except exception.InstanceNotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - except exception.Invalid as exp: - raise exc.HTTPBadRequest(explanation=unicode(exp)) - - return webob.Response(status_int=202) - - def get_actions(self): - """Return the actions the extensions adds""" - actions = [ - extensions.ActionExtension("servers", "addSecurityGroup", - self._addSecurityGroup), - extensions.ActionExtension("servers", "removeSecurityGroup", - self._removeSecurityGroup) - ] - return actions - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-security-groups', - controller=SecurityGroupController()) - - resources.append(res) - - res = extensions.ResourceExtension('os-security-group-rules', - controller=SecurityGroupRulesController()) - resources.append(res) - return resources diff --git a/nova/api/openstack/v2/contrib/server_action_list.py b/nova/api/openstack/v2/contrib/server_action_list.py deleted file mode 100644 index 90573043e..000000000 --- a/nova/api/openstack/v2/contrib/server_action_list.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob.exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception - - -sa_nsmap = {None: wsgi.XMLNS_V11} - - -class ServerActionsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('actions') - elem = xmlutil.SubTemplateElement(root, 'action', selector='actions') - elem.set('created_at') - elem.set('action') - elem.set('error') - return xmlutil.MasterTemplate(root, 1, nsmap=sa_nsmap) - - -class ServerActionListController(object): - @wsgi.serializers(xml=ServerActionsTemplate) - def index(self, req, server_id): - context = req.environ["nova.context"] - compute_api = compute.API() - - try: - instance = compute_api.get(context, server_id) - except exception.NotFound: - raise webob.exc.HTTPNotFound(_("Instance not found")) - - items = compute_api.get_actions(context, instance) - - def _format_item(item): - return { - 'created_at': str(item['created_at']), - 'action': item['action'], - 'error': item['error'], - } - - return {'actions': [_format_item(item) for item in items]} - - -class Server_action_list(extensions.ExtensionDescriptor): - """Allow Admins to view pending server actions""" - - name = "ServerActionList" - alias = "os-server-action-list" - namespace = "http://docs.openstack.org/compute/ext/" \ - "server-actions-list/api/v1.1" - updated = "2011-12-21T00:00:00+00:00" - admin_only = True - - def get_resources(self): - parent_def = {'member_name': 'server', 'collection_name': 'servers'} - #NOTE(bcwaldon): This should be prefixed with 'os-' - ext = extensions.ResourceExtension('actions', - ServerActionListController(), - parent=parent_def) - return [ext] diff --git a/nova/api/openstack/v2/contrib/server_diagnostics.py b/nova/api/openstack/v2/contrib/server_diagnostics.py deleted file mode 100644 index daeda5081..000000000 --- a/nova/api/openstack/v2/contrib/server_diagnostics.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob.exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception -from nova.scheduler import api as scheduler_api - - -sd_nsmap = {None: wsgi.XMLNS_V11} - - -class ServerDiagnosticsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('diagnostics') - elem = xmlutil.SubTemplateElement(root, xmlutil.Selector(0), - selector=xmlutil.get_items) - elem.text = 1 - return xmlutil.MasterTemplate(root, 1, nsmap=sd_nsmap) - - -class ServerDiagnosticsController(object): - @wsgi.serializers(xml=ServerDiagnosticsTemplate) - @exception.novaclient_converter - @scheduler_api.redirect_handler - def index(self, req, server_id): - context = req.environ["nova.context"] - compute_api = compute.API() - try: - instance = compute_api.get(context, id) - except exception.NotFound(): - raise webob.exc.HTTPNotFound(_("Instance not found")) - - return compute_api.get_diagnostics(context, instance) - - -class Server_diagnostics(extensions.ExtensionDescriptor): - """Allow Admins to view server diagnostics through server action""" - - name = "ServerDiagnostics" - alias = "os-server-diagnostics" - namespace = "http://docs.openstack.org/compute/ext/" \ - "server-diagnostics/api/v1.1" - updated = "2011-12-21T00:00:00+00:00" - admin_only = True - - def get_resources(self): - parent_def = {'member_name': 'server', 'collection_name': 'servers'} - #NOTE(bcwaldon): This should be prefixed with 'os-' - ext = extensions.ResourceExtension('diagnostics', - ServerDiagnosticsController(), - parent=parent_def) - return [ext] diff --git a/nova/api/openstack/v2/contrib/simple_tenant_usage.py b/nova/api/openstack/v2/contrib/simple_tenant_usage.py deleted file mode 100644 index 7b07dfae1..000000000 --- a/nova/api/openstack/v2/contrib/simple_tenant_usage.py +++ /dev/null @@ -1,265 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from datetime import datetime -import urlparse - -import webob - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.compute import api -from nova import exception -from nova import flags - - -FLAGS = flags.FLAGS - - -def make_usage(elem): - for subelem_tag in ('tenant_id', 'total_local_gb_usage', - 'total_vcpus_usage', 'total_memory_mb_usage', - 'total_hours', 'start', 'stop'): - subelem = xmlutil.SubTemplateElement(elem, subelem_tag) - subelem.text = subelem_tag - - server_usages = xmlutil.SubTemplateElement(elem, 'server_usages') - server_usage = xmlutil.SubTemplateElement(server_usages, 'server_usage', - selector='server_usages') - for subelem_tag in ('name', 'hours', 'memory_mb', 'local_gb', 'vcpus', - 'tenant_id', 'flavor', 'started_at', 'ended_at', - 'state', 'uptime'): - subelem = xmlutil.SubTemplateElement(server_usage, subelem_tag) - subelem.text = subelem_tag - - -class SimpleTenantUsageTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('tenant_usage', selector='tenant_usage') - make_usage(root) - return xmlutil.MasterTemplate(root, 1) - - -class SimpleTenantUsagesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('tenant_usages') - elem = xmlutil.SubTemplateElement(root, 'tenant_usage', - selector='tenant_usages') - make_usage(elem) - return xmlutil.MasterTemplate(root, 1) - - -class SimpleTenantUsageController(object): - def _hours_for(self, instance, period_start, period_stop): - launched_at = instance['launched_at'] - terminated_at = instance['terminated_at'] - if terminated_at is not None: - if not isinstance(terminated_at, datetime): - terminated_at = datetime.strptime(terminated_at, - "%Y-%m-%d %H:%M:%S.%f") - - if launched_at is not None: - if not isinstance(launched_at, datetime): - launched_at = datetime.strptime(launched_at, - "%Y-%m-%d %H:%M:%S.%f") - - if terminated_at and terminated_at < period_start: - return 0 - # nothing if it started after the usage report ended - if launched_at and launched_at > period_stop: - return 0 - if launched_at: - # if instance launched after period_started, don't charge for first - start = max(launched_at, period_start) - if terminated_at: - # if instance stopped before period_stop, don't charge after - stop = min(period_stop, terminated_at) - else: - # instance is still running, so charge them up to current time - stop = period_stop - dt = stop - start - seconds = dt.days * 3600 * 24 + dt.seconds\ - + dt.microseconds / 100000.0 - - return seconds / 3600.0 - else: - # instance hasn't launched, so no charge - return 0 - - def _tenant_usages_for_period(self, context, period_start, - period_stop, tenant_id=None, detailed=True): - - compute_api = api.API() - instances = compute_api.get_active_by_window(context, - period_start, - period_stop, - tenant_id) - from nova import log as logging - logging.info(instances) - rval = {} - flavors = {} - - for instance in instances: - info = {} - info['hours'] = self._hours_for(instance, - period_start, - period_stop) - flavor_type = instance['instance_type_id'] - - if not flavors.get(flavor_type): - try: - it_ref = compute_api.get_instance_type(context, - flavor_type) - flavors[flavor_type] = it_ref - except exception.InstanceTypeNotFound: - # can't bill if there is no instance type - continue - - flavor = flavors[flavor_type] - - info['name'] = instance['display_name'] - - info['memory_mb'] = flavor['memory_mb'] - info['local_gb'] = flavor['local_gb'] - info['vcpus'] = flavor['vcpus'] - - info['tenant_id'] = instance['project_id'] - - info['flavor'] = flavor['name'] - - info['started_at'] = instance['launched_at'] - - info['ended_at'] = instance['terminated_at'] - - if info['ended_at']: - info['state'] = 'terminated' - else: - info['state'] = instance['vm_state'] - - now = datetime.utcnow() - - if info['state'] == 'terminated': - delta = info['ended_at'] - info['started_at'] - else: - delta = now - info['started_at'] - - info['uptime'] = delta.days * 24 * 60 + delta.seconds - - if not info['tenant_id'] in rval: - summary = {} - summary['tenant_id'] = info['tenant_id'] - if detailed: - summary['server_usages'] = [] - summary['total_local_gb_usage'] = 0 - summary['total_vcpus_usage'] = 0 - summary['total_memory_mb_usage'] = 0 - summary['total_hours'] = 0 - summary['start'] = period_start - summary['stop'] = period_stop - rval[info['tenant_id']] = summary - - summary = rval[info['tenant_id']] - summary['total_local_gb_usage'] += info['local_gb'] * info['hours'] - summary['total_vcpus_usage'] += info['vcpus'] * info['hours'] - summary['total_memory_mb_usage'] += info['memory_mb']\ - * info['hours'] - - summary['total_hours'] += info['hours'] - if detailed: - summary['server_usages'].append(info) - - return rval.values() - - def _parse_datetime(self, dtstr): - if isinstance(dtstr, datetime): - return dtstr - try: - return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S") - except Exception: - try: - return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S.%f") - except Exception: - return datetime.strptime(dtstr, "%Y-%m-%d %H:%M:%S.%f") - - def _get_datetime_range(self, req): - qs = req.environ.get('QUERY_STRING', '') - env = urlparse.parse_qs(qs) - period_start = self._parse_datetime(env.get('start', - [datetime.utcnow().isoformat()])[0]) - period_stop = self._parse_datetime(env.get('end', - [datetime.utcnow().isoformat()])[0]) - - detailed = bool(env.get('detailed', False)) - return (period_start, period_stop, detailed) - - @wsgi.serializers(xml=SimpleTenantUsagesTemplate) - def index(self, req): - """Retrive tenant_usage for all tenants""" - context = req.environ['nova.context'] - - if not context.is_admin: - return webob.Response(status_int=403) - - (period_start, period_stop, detailed) = self._get_datetime_range(req) - usages = self._tenant_usages_for_period(context, - period_start, - period_stop, - detailed=detailed) - return {'tenant_usages': usages} - - @wsgi.serializers(xml=SimpleTenantUsageTemplate) - def show(self, req, id): - """Retrive tenant_usage for a specified tenant""" - tenant_id = id - context = req.environ['nova.context'] - - if not context.is_admin: - if tenant_id != context.project_id: - return webob.Response(status_int=403) - - (period_start, period_stop, ignore) = self._get_datetime_range(req) - usage = self._tenant_usages_for_period(context, - period_start, - period_stop, - tenant_id=tenant_id, - detailed=True) - if len(usage): - usage = usage[0] - else: - usage = {} - return {'tenant_usage': usage} - - -class Simple_tenant_usage(extensions.ExtensionDescriptor): - """Simple tenant usage extension""" - - name = "SimpleTenantUsage" - alias = "os-simple-tenant-usage" - namespace = "http://docs.openstack.org/compute/ext/" \ - "os-simple-tenant-usage/api/v1.1" - updated = "2011-08-19T00:00:00+00:00" - admin_only = True - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-simple-tenant-usage', - SimpleTenantUsageController()) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/users.py b/nova/api/openstack/v2/contrib/users.py deleted file mode 100644 index e24c7c068..000000000 --- a/nova/api/openstack/v2/contrib/users.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from webob import exc - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.auth import manager -from nova import exception -from nova import flags -from nova import log as logging - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack.users') - - -def make_user(elem): - elem.set('id') - elem.set('name') - elem.set('access') - elem.set('secret') - elem.set('admin') - - -class UserTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('user', selector='user') - make_user(root) - return xmlutil.MasterTemplate(root, 1) - - -class UsersTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('users') - elem = xmlutil.SubTemplateElement(root, 'user', selector='users') - make_user(elem) - return xmlutil.MasterTemplate(root, 1) - - -def _translate_keys(user): - return dict(id=user.id, - name=user.name, - access=user.access, - secret=user.secret, - admin=user.admin) - - -class Controller(object): - - def __init__(self): - self.manager = manager.AuthManager() - - def _check_admin(self, context): - """We cannot depend on the db layer to check for admin access - for the auth manager, so we do it here""" - if not context.is_admin: - raise exception.AdminRequired() - - @wsgi.serializers(xml=UsersTemplate) - def index(self, req): - """Return all users in brief""" - users = self.manager.get_users() - users = common.limited(users, req) - users = [_translate_keys(user) for user in users] - return dict(users=users) - - @wsgi.serializers(xml=UsersTemplate) - def detail(self, req): - """Return all users in detail""" - return self.index(req) - - @wsgi.serializers(xml=UserTemplate) - def show(self, req, id): - """Return data about the given user id""" - - #NOTE(justinsb): The drivers are a little inconsistent in how they - # deal with "NotFound" - some throw, some return None. - try: - user = self.manager.get_user(id) - except exception.NotFound: - user = None - - if user is None: - raise exc.HTTPNotFound() - - return dict(user=_translate_keys(user)) - - def delete(self, req, id): - self._check_admin(req.environ['nova.context']) - self.manager.delete_user(id) - return {} - - @wsgi.serializers(xml=UserTemplate) - def create(self, req, body): - self._check_admin(req.environ['nova.context']) - is_admin = body['user'].get('admin') in ('T', 'True', True) - name = body['user'].get('name') - access = body['user'].get('access') - secret = body['user'].get('secret') - user = self.manager.create_user(name, access, secret, is_admin) - return dict(user=_translate_keys(user)) - - @wsgi.serializers(xml=UserTemplate) - def update(self, req, id, body): - self._check_admin(req.environ['nova.context']) - is_admin = body['user'].get('admin') - if is_admin is not None: - is_admin = is_admin in ('T', 'True', True) - access = body['user'].get('access') - secret = body['user'].get('secret') - self.manager.modify_user(id, access, secret, is_admin) - return dict(user=_translate_keys(self.manager.get_user(id))) - - -class Users(extensions.ExtensionDescriptor): - """Allow admins to acces user information""" - - name = "Users" - alias = "os-users" - namespace = "http://docs.openstack.org/compute/ext/users/api/v1.1" - updated = "2011-08-08T00:00:00+00:00" - admin_only = True - - def get_resources(self): - coll_actions = {'detail': 'GET'} - res = extensions.ResourceExtension('users', - Controller(), - collection_actions=coll_actions) - - return [res] diff --git a/nova/api/openstack/v2/contrib/virtual_interfaces.py b/nova/api/openstack/v2/contrib/virtual_interfaces.py deleted file mode 100644 index 401c7133e..000000000 --- a/nova/api/openstack/v2/contrib/virtual_interfaces.py +++ /dev/null @@ -1,92 +0,0 @@ -# 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. - -"""The virtual interfaces extension.""" - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import log as logging -from nova import network - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.virtual_interfaces") - - -vif_nsmap = {None: wsgi.XMLNS_V11} - - -class VirtualInterfaceTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('virtual_interfaces') - elem = xmlutil.SubTemplateElement(root, 'virtual_interface', - selector='virtual_interfaces') - elem.set('id') - elem.set('mac_address') - return xmlutil.MasterTemplate(root, 1, nsmap=vif_nsmap) - - -def _translate_vif_summary_view(_context, vif): - """Maps keys for VIF summary view.""" - d = {} - d['id'] = vif['uuid'] - d['mac_address'] = vif['address'] - return d - - -class ServerVirtualInterfaceController(object): - """The instance VIF API controller for the Openstack API. - """ - - def __init__(self): - self.network_api = network.API() - super(ServerVirtualInterfaceController, self).__init__() - - def _items(self, req, server_id, entity_maker): - """Returns a list of VIFs, transformed through entity_maker.""" - context = req.environ['nova.context'] - - vifs = self.network_api.get_vifs_by_instance(context, server_id) - limited_list = common.limited(vifs, req) - res = [entity_maker(context, vif) for vif in limited_list] - return {'virtual_interfaces': res} - - @wsgi.serializers(xml=VirtualInterfaceTemplate) - def index(self, req, server_id): - """Returns the list of VIFs for a given instance.""" - return self._items(req, server_id, - entity_maker=_translate_vif_summary_view) - - -class Virtual_interfaces(extensions.ExtensionDescriptor): - """Virtual interface support""" - - name = "VirtualInterfaces" - alias = "virtual_interfaces" - namespace = "http://docs.openstack.org/compute/ext/" \ - "virtual_interfaces/api/v1.1" - updated = "2011-08-17T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'os-virtual-interfaces', - controller=ServerVirtualInterfaceController(), - parent=dict(member_name='server', collection_name='servers')) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/virtual_storage_arrays.py b/nova/api/openstack/v2/contrib/virtual_storage_arrays.py deleted file mode 100644 index 0dee3f1b4..000000000 --- a/nova/api/openstack/v2/contrib/virtual_storage_arrays.py +++ /dev/null @@ -1,687 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Zadara Storage Inc. -# Copyright (c) 2011 OpenStack LLC. -# -# 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. - -""" The virtul storage array extension""" - - -import webob -from webob import exc - -from nova.api.openstack import common -from nova.api.openstack.v2.contrib import volumes -from nova.api.openstack.v2 import extensions -from nova.api.openstack.v2 import servers -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova.compute import instance_types -from nova import network -from nova import db -from nova import quota -from nova import exception -from nova import flags -from nova import log as logging -from nova import vsa -from nova import volume - -FLAGS = flags.FLAGS - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.vsa") - - -def _vsa_view(context, vsa, details=False, instances=None): - """Map keys for vsa summary/detailed view.""" - d = {} - - d['id'] = vsa.get('id') - d['name'] = vsa.get('name') - d['displayName'] = vsa.get('display_name') - d['displayDescription'] = vsa.get('display_description') - - d['createTime'] = vsa.get('created_at') - d['status'] = vsa.get('status') - - if 'vsa_instance_type' in vsa: - d['vcType'] = vsa['vsa_instance_type'].get('name', None) - else: - d['vcType'] = vsa['instance_type_id'] - - d['vcCount'] = vsa.get('vc_count') - d['driveCount'] = vsa.get('vol_count') - - d['ipAddress'] = None - for instance in instances: - fixed_addr = None - floating_addr = None - if instance['fixed_ips']: - fixed = instance['fixed_ips'][0] - fixed_addr = fixed['address'] - if fixed['floating_ips']: - floating_addr = fixed['floating_ips'][0]['address'] - - if floating_addr: - d['ipAddress'] = floating_addr - break - else: - d['ipAddress'] = d['ipAddress'] or fixed_addr - - return d - - -def make_vsa(elem): - elem.set('id') - elem.set('name') - elem.set('displayName') - elem.set('displayDescription') - elem.set('createTime') - elem.set('status') - elem.set('vcType') - elem.set('vcCount') - elem.set('driveCount') - elem.set('ipAddress') - - -class VsaTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('vsa', selector='vsa') - make_vsa(root) - return xmlutil.MasterTemplate(root, 1) - - -class VsaSetTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('vsaSet') - elem = xmlutil.SubTemplateElement(root, 'vsa', selector='vsaSet') - make_vsa(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VsaController(object): - """The Virtual Storage Array API controller for the OpenStack API.""" - - def __init__(self): - self.vsa_api = vsa.API() - self.compute_api = compute.API() - self.network_api = network.API() - super(VsaController, self).__init__() - - def _get_instances_by_vsa_id(self, context, id): - return self.compute_api.get_all(context, - search_opts={'metadata': dict(vsa_id=str(id))}) - - def _items(self, req, details): - """Return summary or detailed list of VSAs.""" - context = req.environ['nova.context'] - vsas = self.vsa_api.get_all(context) - limited_list = common.limited(vsas, req) - - vsa_list = [] - for vsa in limited_list: - instances = self._get_instances_by_vsa_id(context, vsa.get('id')) - vsa_list.append(_vsa_view(context, vsa, details, instances)) - return {'vsaSet': vsa_list} - - @wsgi.serializers(xml=VsaSetTemplate) - def index(self, req): - """Return a short list of VSAs.""" - return self._items(req, details=False) - - @wsgi.serializers(xml=VsaSetTemplate) - def detail(self, req): - """Return a detailed list of VSAs.""" - return self._items(req, details=True) - - @wsgi.serializers(xml=VsaTemplate) - def show(self, req, id): - """Return data about the given VSA.""" - context = req.environ['nova.context'] - - try: - vsa = self.vsa_api.get(context, vsa_id=id) - except exception.NotFound: - raise exc.HTTPNotFound() - - instances = self._get_instances_by_vsa_id(context, vsa.get('id')) - return {'vsa': _vsa_view(context, vsa, True, instances)} - - @wsgi.serializers(xml=VsaTemplate) - def create(self, req, body): - """Create a new VSA.""" - context = req.environ['nova.context'] - - if not body or 'vsa' not in body: - LOG.debug(_("No body provided"), context=context) - raise exc.HTTPUnprocessableEntity() - - vsa = body['vsa'] - - display_name = vsa.get('displayName') - vc_type = vsa.get('vcType', FLAGS.default_vsa_instance_type) - try: - instance_type = instance_types.get_instance_type_by_name(vc_type) - except exception.NotFound: - raise exc.HTTPNotFound() - - LOG.audit(_("Create VSA %(display_name)s of type %(vc_type)s"), - locals(), context=context) - - args = dict(display_name=display_name, - display_description=vsa.get('displayDescription'), - instance_type=instance_type, - storage=vsa.get('storage'), - shared=vsa.get('shared'), - availability_zone=vsa.get('placement', {}).\ - get('AvailabilityZone')) - - vsa = self.vsa_api.create(context, **args) - - instances = self._get_instances_by_vsa_id(context, vsa.get('id')) - return {'vsa': _vsa_view(context, vsa, True, instances)} - - def delete(self, req, id): - """Delete a VSA.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete VSA with id: %s"), id, context=context) - - try: - self.vsa_api.delete(context, vsa_id=id) - except exception.NotFound: - raise exc.HTTPNotFound() - - def associate_address(self, req, id, body): - """ /zadr-vsa/{vsa_id}/associate_address - auto or manually associate an IP to VSA - """ - context = req.environ['nova.context'] - - if body is None: - ip = 'auto' - else: - ip = body.get('ipAddress', 'auto') - - LOG.audit(_("Associate address %(ip)s to VSA %(id)s"), - locals(), context=context) - - try: - instances = self._get_instances_by_vsa_id(context, id) - if instances is None or len(instances) == 0: - raise exc.HTTPNotFound() - - for instance in instances: - self.network_api.allocate_for_instance(context, instance, - vpn=False) - # Placeholder - return - - except exception.NotFound: - raise exc.HTTPNotFound() - - def disassociate_address(self, req, id, body): - """ /zadr-vsa/{vsa_id}/disassociate_address - auto or manually associate an IP to VSA - """ - context = req.environ['nova.context'] - - if body is None: - ip = 'auto' - else: - ip = body.get('ipAddress', 'auto') - - LOG.audit(_("Disassociate address from VSA %(id)s"), - locals(), context=context) - # Placeholder - - -def make_volume(elem): - volumes.make_volume(elem) - elem.set('name') - elem.set('vsaId') - - -class VsaVolumeDriveController(volumes.VolumeController): - """The base class for VSA volumes & drives. - - A child resource of the VSA object. Allows operations with - volumes and drives created to/from particular VSA - - """ - - def __init__(self): - self.volume_api = volume.API() - self.vsa_api = vsa.API() - super(VsaVolumeDriveController, self).__init__() - - def _translation(self, context, vol, vsa_id, details): - if details: - translation = volumes._translate_volume_detail_view - else: - translation = volumes._translate_volume_summary_view - - d = translation(context, vol) - d['vsaId'] = vsa_id - d['name'] = vol['name'] - return d - - def _check_volume_ownership(self, context, vsa_id, id): - obj = self.object - try: - volume_ref = self.volume_api.get(context, volume_id=id) - except exception.NotFound: - LOG.error(_("%(obj)s with ID %(id)s not found"), locals()) - raise - - own_vsa_id = self.volume_api.get_volume_metadata_value(volume_ref, - self.direction) - if own_vsa_id != vsa_id: - LOG.error(_("%(obj)s with ID %(id)s belongs to VSA %(own_vsa_id)s"\ - " and not to VSA %(vsa_id)s."), locals()) - raise exception.Invalid() - - def _items(self, req, vsa_id, details): - """Return summary or detailed list of volumes for particular VSA.""" - context = req.environ['nova.context'] - - vols = self.volume_api.get_all(context, - search_opts={'metadata': {self.direction: str(vsa_id)}}) - limited_list = common.limited(vols, req) - - res = [self._translation(context, vol, vsa_id, details) \ - for vol in limited_list] - - return {self.objects: res} - - def index(self, req, vsa_id): - """Return a short list of volumes created from particular VSA.""" - LOG.audit(_("Index. vsa_id=%(vsa_id)s"), locals()) - return self._items(req, vsa_id, details=False) - - def detail(self, req, vsa_id): - """Return a detailed list of volumes created from particular VSA.""" - LOG.audit(_("Detail. vsa_id=%(vsa_id)s"), locals()) - return self._items(req, vsa_id, details=True) - - def create(self, req, vsa_id, body): - """Create a new volume from VSA.""" - LOG.audit(_("Create. vsa_id=%(vsa_id)s, body=%(body)s"), locals()) - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - vol = body[self.object] - size = vol['size'] - LOG.audit(_("Create volume of %(size)s GB from VSA ID %(vsa_id)s"), - locals(), context=context) - try: - # create is supported for volumes only (drives created through VSA) - volume_type = self.vsa_api.get_vsa_volume_type(context) - except exception.NotFound: - raise exc.HTTPNotFound() - - new_volume = self.volume_api.create(context, - size, - None, - vol.get('displayName'), - vol.get('displayDescription'), - volume_type=volume_type, - metadata=dict(from_vsa_id=str(vsa_id))) - - return {self.object: self._translation(context, new_volume, - vsa_id, True)} - - def update(self, req, vsa_id, id, body): - """Update a volume.""" - context = req.environ['nova.context'] - - try: - self._check_volume_ownership(context, vsa_id, id) - except exception.NotFound: - raise exc.HTTPNotFound() - except exception.Invalid: - raise exc.HTTPBadRequest() - - vol = body[self.object] - updatable_fields = [{'displayName': 'display_name'}, - {'displayDescription': 'display_description'}, - {'status': 'status'}, - {'providerLocation': 'provider_location'}, - {'providerAuth': 'provider_auth'}] - changes = {} - for field in updatable_fields: - key = field.keys()[0] - val = field[key] - if key in vol: - changes[val] = vol[key] - - obj = self.object - LOG.audit(_("Update %(obj)s with id: %(id)s, changes: %(changes)s"), - locals(), context=context) - - try: - self.volume_api.update(context, volume_id=id, fields=changes) - except exception.NotFound: - raise exc.HTTPNotFound() - return webob.Response(status_int=202) - - def delete(self, req, vsa_id, id): - """Delete a volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete. vsa_id=%(vsa_id)s, id=%(id)s"), locals()) - - try: - self._check_volume_ownership(context, vsa_id, id) - except exception.NotFound: - raise exc.HTTPNotFound() - except exception.Invalid: - raise exc.HTTPBadRequest() - - return super(VsaVolumeDriveController, self).delete(req, id) - - def show(self, req, vsa_id, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Show. vsa_id=%(vsa_id)s, id=%(id)s"), locals()) - - try: - self._check_volume_ownership(context, vsa_id, id) - except exception.NotFound: - raise exc.HTTPNotFound() - except exception.Invalid: - raise exc.HTTPBadRequest() - - return super(VsaVolumeDriveController, self).show(req, id) - - -class VsaVolumeTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume', selector='volume') - make_volume(root) - return xmlutil.MasterTemplate(root, 1) - - -class VsaVolumesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volumes') - elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') - make_volume(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VsaVolumeController(VsaVolumeDriveController): - """The VSA volume API controller for the Openstack API. - - A child resource of the VSA object. Allows operations with volumes created - by particular VSA - - """ - - def __init__(self): - self.direction = 'from_vsa_id' - self.objects = 'volumes' - self.object = 'volume' - super(VsaVolumeController, self).__init__() - - @wsgi.serializers(xml=VsaVolumesTemplate) - def index(self, req, vsa_id): - return super(VsaVolumeController, self).index(req, vsa_id) - - @wsgi.serializers(xml=VsaVolumesTemplate) - def detail(self, req, vsa_id): - return super(VsaVolumeController, self).detail(req, vsa_id) - - @wsgi.serializers(xml=VsaVolumeTemplate) - def create(self, req, vsa_id, body): - return super(VsaVolumeController, self).create(req, vsa_id, body) - - @wsgi.serializers(xml=VsaVolumeTemplate) - def update(self, req, vsa_id, id, body): - return super(VsaVolumeController, self).update(req, vsa_id, id, body) - - @wsgi.serializers(xml=VsaVolumeTemplate) - def show(self, req, vsa_id, id): - return super(VsaVolumeController, self).show(req, vsa_id, id) - - -class VsaDriveTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('drive', selector='drive') - make_volume(root) - return xmlutil.MasterTemplate(root, 1) - - -class VsaDrivesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('drives') - elem = xmlutil.SubTemplateElement(root, 'drive', selector='drives') - make_volume(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VsaDriveController(VsaVolumeDriveController): - """The VSA Drive API controller for the Openstack API. - - A child resource of the VSA object. Allows operations with drives created - for particular VSA - - """ - - def __init__(self): - self.direction = 'to_vsa_id' - self.objects = 'drives' - self.object = 'drive' - super(VsaDriveController, self).__init__() - - def create(self, req, vsa_id, body): - """Create a new drive for VSA. Should be done through VSA APIs""" - raise exc.HTTPBadRequest() - - def update(self, req, vsa_id, id, body): - """Update a drive. Should be done through VSA APIs""" - raise exc.HTTPBadRequest() - - def delete(self, req, vsa_id, id): - """Delete a volume. Should be done through VSA APIs""" - raise exc.HTTPBadRequest() - - @wsgi.serializers(xml=VsaDrivesTemplate) - def index(self, req, vsa_id): - return super(VsaDriveController, self).index(req, vsa_id) - - @wsgi.serializers(xml=VsaDrivesTemplate) - def detail(self, req, vsa_id): - return super(VsaDriveController, self).detail(req, vsa_id) - - @wsgi.serializers(xml=VsaDriveTemplate) - def show(self, req, vsa_id, id): - return super(VsaDriveController, self).show(req, vsa_id, id) - - -def make_vpool(elem): - elem.set('id') - elem.set('vsaId') - elem.set('name') - elem.set('displayName') - elem.set('displayDescription') - elem.set('driveCount') - elem.set('protection') - elem.set('stripeSize') - elem.set('stripeWidth') - elem.set('createTime') - elem.set('status') - - drive_ids = xmlutil.SubTemplateElement(elem, 'driveIds') - drive_id = xmlutil.SubTemplateElement(drive_ids, 'driveId', - selector='driveIds') - drive_id.text = xmlutil.Selector() - - -class VsaVPoolTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('vpool', selector='vpool') - make_vpool(root) - return xmlutil.MasterTemplate(root, 1) - - -class VsaVPoolsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('vpools') - elem = xmlutil.SubTemplateElement(root, 'vpool', selector='vpools') - make_vpool(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VsaVPoolController(object): - """The vPool VSA API controller for the OpenStack API.""" - - def __init__(self): - self.vsa_api = vsa.API() - super(VsaVPoolController, self).__init__() - - @wsgi.serializers(xml=VsaVPoolsTemplate) - def index(self, req, vsa_id): - """Return a short list of vpools created from particular VSA.""" - return {'vpools': []} - - def create(self, req, vsa_id, body): - """Create a new vPool for VSA.""" - raise exc.HTTPBadRequest() - - def update(self, req, vsa_id, id, body): - """Update vPool parameters.""" - raise exc.HTTPBadRequest() - - def delete(self, req, vsa_id, id): - """Delete a vPool.""" - raise exc.HTTPBadRequest() - - def show(self, req, vsa_id, id): - """Return data about the given vPool.""" - raise exc.HTTPBadRequest() - - -class VsaVCController(servers.Controller): - """The VSA Virtual Controller API controller for the OpenStack API.""" - - def __init__(self): - self.vsa_api = vsa.API() - self.compute_api = compute.API() - self.vsa_id = None # VP-TODO: temporary ugly hack - super(VsaVCController, self).__init__() - - def _get_servers(self, req, is_detail): - """Returns a list of servers, taking into account any search - options specified. - """ - - if self.vsa_id is None: - super(VsaVCController, self)._get_servers(req, is_detail) - - context = req.environ['nova.context'] - - search_opts = {'metadata': dict(vsa_id=str(self.vsa_id))} - instance_list = self.compute_api.get_all( - context, search_opts=search_opts) - - limited_list = self._limit_items(instance_list, req) - servers = [self._build_view(req, inst, is_detail)['server'] - for inst in limited_list] - return dict(servers=servers) - - @wsgi.serializers(xml=servers.MinimalServersTemplate) - def index(self, req, vsa_id): - """Return list of instances for particular VSA.""" - - LOG.audit(_("Index instances for VSA %s"), vsa_id) - - self.vsa_id = vsa_id # VP-TODO: temporary ugly hack - result = super(VsaVCController, self).detail(req) - self.vsa_id = None - return result - - def create(self, req, vsa_id, body): - """Create a new instance for VSA.""" - raise exc.HTTPBadRequest() - - def update(self, req, vsa_id, id, body): - """Update VSA instance.""" - raise exc.HTTPBadRequest() - - def delete(self, req, vsa_id, id): - """Delete VSA instance.""" - raise exc.HTTPBadRequest() - - @wsgi.serializers(xml=servers.ServerTemplate) - def show(self, req, vsa_id, id): - """Return data about the given instance.""" - return super(VsaVCController, self).show(req, id) - - -class Virtual_storage_arrays(extensions.ExtensionDescriptor): - """Virtual Storage Arrays support""" - - name = "VSAs" - alias = "zadr-vsa" - namespace = "http://docs.openstack.org/compute/ext/vsa/api/v1.1" - updated = "2011-08-25T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'zadr-vsa', - VsaController(), - collection_actions={'detail': 'GET'}, - member_actions={'add_capacity': 'POST', - 'remove_capacity': 'POST', - 'associate_address': 'POST', - 'disassociate_address': 'POST'}) - resources.append(res) - - res = extensions.ResourceExtension('volumes', - VsaVolumeController(), - collection_actions={'detail': 'GET'}, - parent=dict( - member_name='vsa', - collection_name='zadr-vsa')) - resources.append(res) - - res = extensions.ResourceExtension('drives', - VsaDriveController(), - collection_actions={'detail': 'GET'}, - parent=dict( - member_name='vsa', - collection_name='zadr-vsa')) - resources.append(res) - - res = extensions.ResourceExtension('vpools', - VsaVPoolController(), - parent=dict( - member_name='vsa', - collection_name='zadr-vsa')) - resources.append(res) - - res = extensions.ResourceExtension('instances', - VsaVCController(), - parent=dict( - member_name='vsa', - collection_name='zadr-vsa')) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/volumes.py b/nova/api/openstack/v2/contrib/volumes.py deleted file mode 100644 index 0ca50b288..000000000 --- a/nova/api/openstack/v2/contrib/volumes.py +++ /dev/null @@ -1,550 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# 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. - -"""The volumes extension.""" - -from webob import exc -import webob - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack.v2 import servers -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova.volume import volume_types - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.volumes") - - -FLAGS = flags.FLAGS - - -def _translate_volume_detail_view(context, vol): - """Maps keys for volumes details view.""" - - d = _translate_volume_summary_view(context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_volume_summary_view(context, vol): - """Maps keys for volumes summary view.""" - d = {} - - d['id'] = vol['id'] - d['status'] = vol['status'] - d['size'] = vol['size'] - d['availabilityZone'] = vol['availability_zone'] - d['createdAt'] = vol['created_at'] - - if vol['attach_status'] == 'attached': - d['attachments'] = [_translate_attachment_detail_view(context, vol)] - else: - d['attachments'] = [{}] - - d['displayName'] = vol['display_name'] - d['displayDescription'] = vol['display_description'] - - if vol['volume_type_id'] and vol.get('volume_type'): - d['volumeType'] = vol['volume_type']['name'] - else: - d['volumeType'] = vol['volume_type_id'] - - d['snapshotId'] = vol['snapshot_id'] - LOG.audit(_("vol=%s"), vol, context=context) - - if vol.get('volume_metadata'): - meta_dict = {} - for i in vol['volume_metadata']: - meta_dict[i['key']] = i['value'] - d['metadata'] = meta_dict - else: - d['metadata'] = {} - - return d - - -def make_volume(elem): - elem.set('id') - elem.set('status') - elem.set('size') - elem.set('availabilityZone') - elem.set('createdAt') - elem.set('displayName') - elem.set('displayDescription') - elem.set('volumeType') - elem.set('snapshotId') - - attachments = xmlutil.SubTemplateElement(elem, 'attachments') - attachment = xmlutil.SubTemplateElement(attachments, 'attachment', - selector='attachments') - make_attachment(attachment) - - metadata = xmlutil.make_flat_dict('metadata') - elem.append(metadata) - - -class VolumeTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume', selector='volume') - make_volume(root) - return xmlutil.MasterTemplate(root, 1) - - -class VolumesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volumes') - elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') - make_volume(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeController(object): - """The Volumes API controller for the OpenStack API.""" - - def __init__(self): - self.volume_api = volume.API() - super(VolumeController, self).__init__() - - @wsgi.serializers(xml=VolumeTemplate) - def show(self, req, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get(context, id) - except exception.NotFound: - raise exc.HTTPNotFound() - - return {'volume': _translate_volume_detail_view(context, vol)} - - def delete(self, req, id): - """Delete a volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete volume with id: %s"), id, context=context) - - try: - self.volume_api.delete(context, volume_id=id) - except exception.NotFound: - raise exc.HTTPNotFound() - return webob.Response(status_int=202) - - @wsgi.serializers(xml=VolumesTemplate) - def index(self, req): - """Returns a summary list of volumes.""" - return self._items(req, entity_maker=_translate_volume_summary_view) - - @wsgi.serializers(xml=VolumesTemplate) - def detail(self, req): - """Returns a detailed list of volumes.""" - return self._items(req, entity_maker=_translate_volume_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker.""" - context = req.environ['nova.context'] - - volumes = self.volume_api.get_all(context) - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumes': res} - - @wsgi.serializers(xml=VolumeTemplate) - def create(self, req, body): - """Creates a new volume.""" - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - vol = body['volume'] - size = vol['size'] - LOG.audit(_("Create volume of %s GB"), size, context=context) - - vol_type = vol.get('volume_type', None) - if vol_type: - try: - vol_type = volume_types.get_volume_type_by_name(context, - vol_type) - except exception.NotFound: - raise exc.HTTPNotFound() - - metadata = vol.get('metadata', None) - - new_volume = self.volume_api.create(context, size, - vol.get('snapshot_id'), - vol.get('display_name'), - vol.get('display_description'), - volume_type=vol_type, - metadata=metadata) - - # Work around problem that instance is lazy-loaded... - new_volume = self.volume_api.get(context, new_volume['id']) - - retval = _translate_volume_detail_view(context, new_volume) - - return {'volume': retval} - - -def _translate_attachment_detail_view(_context, vol): - """Maps keys for attachment details view.""" - - d = _translate_attachment_summary_view(_context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_attachment_summary_view(_context, vol): - """Maps keys for attachment summary view.""" - d = {} - - volume_id = vol['id'] - - # NOTE(justinsb): We use the volume id as the id of the attachment object - d['id'] = volume_id - - d['volumeId'] = volume_id - if vol.get('instance'): - d['serverId'] = vol['instance']['uuid'] - if vol.get('mountpoint'): - d['device'] = vol['mountpoint'] - - return d - - -def make_attachment(elem): - elem.set('id') - elem.set('serverId') - elem.set('volumeId') - elem.set('device') - - -class VolumeAttachmentTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volumeAttachment', - selector='volumeAttachment') - make_attachment(root) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeAttachmentsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volumeAttachments') - elem = xmlutil.SubTemplateElement(root, 'volumeAttachment', - selector='volumeAttachments') - make_attachment(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeAttachmentController(object): - """The volume attachment API controller for the Openstack API. - - A child resource of the server. Note that we use the volume id - as the ID of the attachment (though this is not guaranteed externally) - - """ - - def __init__(self): - self.compute_api = compute.API() - self.volume_api = volume.API() - super(VolumeAttachmentController, self).__init__() - - @wsgi.serializers(xml=VolumeAttachmentsTemplate) - def index(self, req, server_id): - """Returns the list of volume attachments for a given instance.""" - return self._items(req, server_id, - entity_maker=_translate_attachment_summary_view) - - @wsgi.serializers(xml=VolumeAttachmentTemplate) - def show(self, req, server_id, id): - """Return data about the given volume attachment.""" - context = req.environ['nova.context'] - - volume_id = id - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - LOG.debug("volume_id not found") - raise exc.HTTPNotFound() - - instance = vol['instance'] - if instance is None or str(instance['uuid']) != server_id: - LOG.debug("instance_id != server_id") - raise exc.HTTPNotFound() - - return {'volumeAttachment': _translate_attachment_detail_view(context, - vol)} - - @wsgi.serializers(xml=VolumeAttachmentTemplate) - def create(self, req, server_id, body): - """Attach a volume to an instance.""" - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - volume_id = body['volumeAttachment']['volumeId'] - device = body['volumeAttachment']['device'] - - msg = _("Attach volume %(volume_id)s to instance %(server_id)s" - " at %(device)s") % locals() - LOG.audit(msg, context=context) - - try: - instance = self.compute_api.get(context, server_id) - self.compute_api.attach_volume(context, instance, - volume_id, device) - except exception.NotFound: - raise exc.HTTPNotFound() - - # The attach is async - attachment = {} - attachment['id'] = volume_id - attachment['volumeId'] = volume_id - - # NOTE(justinsb): And now, we have a problem... - # The attach is async, so there's a window in which we don't see - # the attachment (until the attachment completes). We could also - # get problems with concurrent requests. I think we need an - # attachment state, and to write to the DB here, but that's a bigger - # change. - # For now, we'll probably have to rely on libraries being smart - - # TODO(justinsb): How do I return "accepted" here? - return {'volumeAttachment': attachment} - - def update(self, req, server_id, id, body): - """Update a volume attachment. We don't currently support this.""" - raise exc.HTTPBadRequest() - - def delete(self, req, server_id, id): - """Detach a volume from an instance.""" - context = req.environ['nova.context'] - - volume_id = id - LOG.audit(_("Detach volume %s"), volume_id, context=context) - - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - raise exc.HTTPNotFound() - - instance = vol['instance'] - if instance is None or str(instance['uuid']) != server_id: - LOG.debug("instance_id != server_id") - raise exc.HTTPNotFound() - - self.compute_api.detach_volume(context, - volume_id=volume_id) - - return webob.Response(status_int=202) - - def _items(self, req, server_id, entity_maker): - """Returns a list of attachments, transformed through entity_maker.""" - context = req.environ['nova.context'] - - try: - instance = self.compute_api.get(context, server_id) - except exception.NotFound: - raise exc.HTTPNotFound() - - volumes = instance['volumes'] - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumeAttachments': res} - - -class BootFromVolumeController(servers.Controller): - """The boot from volume API controller for the Openstack API.""" - - def _get_block_device_mapping(self, data): - return data.get('block_device_mapping') - - -def _translate_snapshot_detail_view(context, vol): - """Maps keys for snapshots details view.""" - - d = _translate_snapshot_summary_view(context, vol) - - # NOTE(gagupta): No additional data / lookups at the moment - return d - - -def _translate_snapshot_summary_view(context, vol): - """Maps keys for snapshots summary view.""" - d = {} - - d['id'] = vol['id'] - d['volumeId'] = vol['volume_id'] - d['status'] = vol['status'] - # NOTE(gagupta): We map volume_size as the snapshot size - d['size'] = vol['volume_size'] - d['createdAt'] = vol['created_at'] - d['displayName'] = vol['display_name'] - d['displayDescription'] = vol['display_description'] - return d - - -def make_snapshot(elem): - elem.set('id') - elem.set('status') - elem.set('size') - elem.set('createdAt') - elem.set('displayName') - elem.set('displayDescription') - elem.set('volumeId') - - -class SnapshotTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('snapshot', selector='snapshot') - make_snapshot(root) - return xmlutil.MasterTemplate(root, 1) - - -class SnapshotsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('snapshots') - elem = xmlutil.SubTemplateElement(root, 'snapshot', - selector='snapshots') - make_snapshot(elem) - return xmlutil.MasterTemplate(root, 1) - - -class SnapshotController(object): - """The Volumes API controller for the OpenStack API.""" - - def __init__(self): - self.volume_api = volume.API() - super(SnapshotController, self).__init__() - - @wsgi.serializers(xml=SnapshotTemplate) - def show(self, req, id): - """Return data about the given snapshot.""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get_snapshot(context, id) - except exception.NotFound: - return exc.HTTPNotFound() - - return {'snapshot': _translate_snapshot_detail_view(context, vol)} - - def delete(self, req, id): - """Delete a snapshot.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete snapshot with id: %s"), id, context=context) - - try: - self.volume_api.delete_snapshot(context, snapshot_id=id) - except exception.NotFound: - return exc.HTTPNotFound() - return webob.Response(status_int=202) - - @wsgi.serializers(xml=SnapshotsTemplate) - def index(self, req): - """Returns a summary list of snapshots.""" - return self._items(req, entity_maker=_translate_snapshot_summary_view) - - @wsgi.serializers(xml=SnapshotsTemplate) - def detail(self, req): - """Returns a detailed list of snapshots.""" - return self._items(req, entity_maker=_translate_snapshot_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of snapshots, transformed through entity_maker.""" - context = req.environ['nova.context'] - - snapshots = self.volume_api.get_all_snapshots(context) - limited_list = common.limited(snapshots, req) - res = [entity_maker(context, snapshot) for snapshot in limited_list] - return {'snapshots': res} - - @wsgi.serializers(xml=SnapshotTemplate) - def create(self, req, body): - """Creates a new snapshot.""" - context = req.environ['nova.context'] - - if not body: - return exc.HTTPUnprocessableEntity() - - snapshot = body['snapshot'] - volume_id = snapshot['volume_id'] - force = snapshot.get('force', False) - LOG.audit(_("Create snapshot from volume %s"), volume_id, - context=context) - - if force: - new_snapshot = self.volume_api.create_snapshot_force(context, - volume_id, - snapshot.get('display_name'), - snapshot.get('display_description')) - else: - new_snapshot = self.volume_api.create_snapshot(context, - volume_id, - snapshot.get('display_name'), - snapshot.get('display_description')) - - retval = _translate_snapshot_detail_view(context, new_snapshot) - - return {'snapshot': retval} - - -class Volumes(extensions.ExtensionDescriptor): - """Volumes support""" - - name = "Volumes" - alias = "os-volumes" - namespace = "http://docs.openstack.org/compute/ext/volumes/api/v1.1" - updated = "2011-03-25T00:00:00+00:00" - - def get_resources(self): - resources = [] - - # NOTE(justinsb): No way to provide singular name ('volume') - # Does this matter? - res = extensions.ResourceExtension('os-volumes', - VolumeController(), - collection_actions={'detail': 'GET'}) - resources.append(res) - - res = extensions.ResourceExtension('os-volume_attachments', - VolumeAttachmentController(), - parent=dict( - member_name='server', - collection_name='servers')) - resources.append(res) - - res = extensions.ResourceExtension('os-volumes_boot', - BootFromVolumeController()) - resources.append(res) - - res = extensions.ResourceExtension('os-snapshots', - SnapshotController(), - collection_actions={'detail': 'GET'}) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/volumetypes.py b/nova/api/openstack/v2/contrib/volumetypes.py deleted file mode 100644 index 231c86b1b..000000000 --- a/nova/api/openstack/v2/contrib/volumetypes.py +++ /dev/null @@ -1,237 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Zadara Storage Inc. -# Copyright (c) 2011 OpenStack LLC. -# -# 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. - -""" The volume type & volume types extra specs extension""" - -from webob import exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import db -from nova import exception -from nova.volume import volume_types - - -def make_voltype(elem): - elem.set('id') - elem.set('name') - extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') - elem.append(extra_specs) - - -class VolumeTypeTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume_type', selector='volume_type') - make_voltype(root) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume_types') - sel = lambda obj, do_raise=False: obj.values() - elem = xmlutil.SubTemplateElement(root, 'volume_type', selector=sel) - make_voltype(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypesController(object): - """ The volume types API controller for the Openstack API """ - - @wsgi.serializers(xml=VolumeTypesTemplate) - def index(self, req): - """ Returns the list of volume types """ - context = req.environ['nova.context'] - return volume_types.get_all_types(context) - - @wsgi.serializers(xml=VolumeTypeTemplate) - def create(self, req, body): - """Creates a new volume type.""" - context = req.environ['nova.context'] - - if not body or body == "": - raise exc.HTTPUnprocessableEntity() - - vol_type = body.get('volume_type', None) - if vol_type is None or vol_type == "": - raise exc.HTTPUnprocessableEntity() - - name = vol_type.get('name', None) - specs = vol_type.get('extra_specs', {}) - - if name is None or name == "": - raise exc.HTTPUnprocessableEntity() - - try: - volume_types.create(context, name, specs) - vol_type = volume_types.get_volume_type_by_name(context, name) - except exception.QuotaError as error: - self._handle_quota_error(error) - except exception.NotFound: - raise exc.HTTPNotFound() - - return {'volume_type': vol_type} - - @wsgi.serializers(xml=VolumeTypeTemplate) - def show(self, req, id): - """ Return a single volume type item """ - context = req.environ['nova.context'] - - try: - vol_type = volume_types.get_volume_type(context, id) - except exception.NotFound or exception.ApiError: - raise exc.HTTPNotFound() - - return {'volume_type': vol_type} - - def delete(self, req, id): - """ Deletes an existing volume type """ - context = req.environ['nova.context'] - - try: - vol_type = volume_types.get_volume_type(context, id) - volume_types.destroy(context, vol_type['name']) - except exception.NotFound: - raise exc.HTTPNotFound() - - def _handle_quota_error(self, error): - """Reraise quota errors as api-specific http exceptions.""" - if error.code == "MetadataLimitExceeded": - raise exc.HTTPBadRequest(explanation=error.message) - raise error - - -class VolumeTypeExtraSpecsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypeExtraSpecTemplate(xmlutil.TemplateBuilder): - def construct(self): - tagname = xmlutil.Selector('key') - - def extraspec_sel(obj, do_raise=False): - # Have to extract the key and value for later use... - key, value = obj.items()[0] - return dict(key=key, value=value) - - root = xmlutil.TemplateElement(tagname, selector=extraspec_sel) - root.text = 'value' - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypeExtraSpecsController(object): - """ The volume type extra specs API controller for the Openstack API """ - - def _get_extra_specs(self, context, vol_type_id): - extra_specs = db.volume_type_extra_specs_get(context, vol_type_id) - specs_dict = {} - for key, value in extra_specs.iteritems(): - specs_dict[key] = value - return dict(extra_specs=specs_dict) - - def _check_body(self, body): - if body is None or body == "": - expl = _('No Request Body') - raise exc.HTTPBadRequest(explanation=expl) - - @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) - def index(self, req, vol_type_id): - """ Returns the list of extra specs for a given volume type """ - context = req.environ['nova.context'] - return self._get_extra_specs(context, vol_type_id) - - @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) - def create(self, req, vol_type_id, body): - self._check_body(body) - context = req.environ['nova.context'] - specs = body.get('extra_specs') - try: - db.volume_type_extra_specs_update_or_create(context, - vol_type_id, - specs) - except exception.QuotaError as error: - self._handle_quota_error(error) - return body - - @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) - def update(self, req, vol_type_id, id, body): - self._check_body(body) - context = req.environ['nova.context'] - if not id in body: - expl = _('Request body and URI mismatch') - raise exc.HTTPBadRequest(explanation=expl) - if len(body) > 1: - expl = _('Request body contains too many items') - raise exc.HTTPBadRequest(explanation=expl) - try: - db.volume_type_extra_specs_update_or_create(context, - vol_type_id, - body) - except exception.QuotaError as error: - self._handle_quota_error(error) - - return body - - @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) - def show(self, req, vol_type_id, id): - """ Return a single extra spec item """ - context = req.environ['nova.context'] - specs = self._get_extra_specs(context, vol_type_id) - if id in specs['extra_specs']: - return {id: specs['extra_specs'][id]} - else: - raise exc.HTTPNotFound() - - def delete(self, req, vol_type_id, id): - """ Deletes an existing extra spec """ - context = req.environ['nova.context'] - db.volume_type_extra_specs_delete(context, vol_type_id, id) - - def _handle_quota_error(self, error): - """Reraise quota errors as api-specific http exceptions.""" - if error.code == "MetadataLimitExceeded": - raise exc.HTTPBadRequest(explanation=error.message) - raise error - - -class Volumetypes(extensions.ExtensionDescriptor): - """Volume types support""" - - name = "VolumeTypes" - alias = "os-volume-types" - namespace = "http://docs.openstack.org/compute/ext/volume_types/api/v1.1" - updated = "2011-08-24T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'os-volume-types', - VolumeTypesController()) - resources.append(res) - - res = extensions.ResourceExtension('extra_specs', - VolumeTypeExtraSpecsController(), - parent=dict( - member_name='vol_type', - collection_name='os-volume-types')) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/zones.py b/nova/api/openstack/v2/contrib/zones.py deleted file mode 100644 index adbc6580f..000000000 --- a/nova/api/openstack/v2/contrib/zones.py +++ /dev/null @@ -1,239 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""The zones extension.""" - -import json - -from nova.api.openstack import common -from nova.api.openstack.v2 import servers -from nova.api.openstack.v2 import extensions -from nova.api.openstack import xmlutil -from nova.api.openstack import wsgi -from nova.compute import api as compute -from nova import crypto -from nova import exception -from nova import flags -from nova import log as logging -import nova.scheduler.api - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.zones") -FLAGS = flags.FLAGS - - -class CapabilitySelector(object): - def __call__(self, obj, do_raise=False): - return [(k, v) for k, v in obj.items() - if k not in ('id', 'api_url', 'name', 'capabilities')] - - -def make_zone(elem): - elem.set('id') - elem.set('api_url') - elem.set('name') - elem.set('capabilities') - - cap = xmlutil.SubTemplateElement(elem, xmlutil.Selector(0), - selector=CapabilitySelector()) - cap.text = 1 - - -zone_nsmap = {None: wsgi.XMLNS_V10} - - -class ZoneTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('zone', selector='zone') - make_zone(root) - return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) - - -class ZonesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('zones') - elem = xmlutil.SubTemplateElement(root, 'zone', selector='zones') - make_zone(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) - - -class WeightsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('weights') - weight = xmlutil.SubTemplateElement(root, 'weight', selector='weights') - blob = xmlutil.SubTemplateElement(weight, 'blob') - blob.text = 'blob' - inner_weight = xmlutil.SubTemplateElement(weight, 'weight') - inner_weight.text = 'weight' - return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) - - -def _filter_keys(item, keys): - """ - Filters all model attributes except for keys - item is a dict - - """ - return dict((k, v) for k, v in item.iteritems() if k in keys) - - -def _exclude_keys(item, keys): - return dict((k, v) for k, v in item.iteritems() if k and (k not in keys)) - - -def _scrub_zone(zone): - return _exclude_keys(zone, ('username', 'password', 'created_at', - 'deleted', 'deleted_at', 'updated_at')) - - -def check_encryption_key(func): - def wrapped(*args, **kwargs): - if not FLAGS.build_plan_encryption_key: - raise exception.Error(_("--build_plan_encryption_key not set")) - return func(*args, **kwargs) - return wrapped - - -class Controller(object): - """Controller for Zone resources.""" - - def __init__(self): - self.compute_api = compute.API() - - @wsgi.serializers(xml=ZonesTemplate) - def index(self, req): - """Return all zones in brief""" - # Ask the ZoneManager in the Scheduler for most recent data, - # or fall-back to the database ... - items = nova.scheduler.api.get_zone_list(req.environ['nova.context']) - items = common.limited(items, req) - items = [_scrub_zone(item) for item in items] - return dict(zones=items) - - @wsgi.serializers(xml=ZonesTemplate) - def detail(self, req): - """Return all zones in detail""" - return self.index(req) - - @wsgi.serializers(xml=ZoneTemplate) - def info(self, req): - """Return name and capabilities for this zone.""" - context = req.environ['nova.context'] - items = nova.scheduler.api.get_zone_capabilities(context) - - zone = dict(name=FLAGS.zone_name) - caps = FLAGS.zone_capabilities - for cap in caps: - key, value = cap.split('=') - zone[key] = value - for item, (min_value, max_value) in items.iteritems(): - zone[item] = "%s,%s" % (min_value, max_value) - return dict(zone=zone) - - @wsgi.serializers(xml=ZoneTemplate) - def show(self, req, id): - """Return data about the given zone id""" - zone_id = int(id) - context = req.environ['nova.context'] - zone = nova.scheduler.api.zone_get(context, zone_id) - return dict(zone=_scrub_zone(zone)) - - def delete(self, req, id): - """Delete a child zone entry.""" - zone_id = int(id) - nova.scheduler.api.zone_delete(req.environ['nova.context'], zone_id) - return {} - - @wsgi.serializers(xml=ZoneTemplate) - @wsgi.deserializers(xml=servers.CreateDeserializer) - def create(self, req, body): - """Create a child zone entry.""" - context = req.environ['nova.context'] - zone = nova.scheduler.api.zone_create(context, body["zone"]) - return dict(zone=_scrub_zone(zone)) - - @wsgi.serializers(xml=ZoneTemplate) - def update(self, req, id, body): - """Update a child zone entry.""" - context = req.environ['nova.context'] - zone_id = int(id) - zone = nova.scheduler.api.zone_update(context, zone_id, body["zone"]) - return dict(zone=_scrub_zone(zone)) - - @wsgi.serializers(xml=WeightsTemplate) - @check_encryption_key - def select(self, req, body): - """Returns a weighted list of costs to create instances - of desired capabilities.""" - ctx = req.environ['nova.context'] - specs = json.loads(body) - build_plan = nova.scheduler.api.select(ctx, specs=specs) - cooked = self._scrub_build_plan(build_plan) - return {"weights": cooked} - - def _scrub_build_plan(self, build_plan): - """Remove all the confidential data and return a sanitized - version of the build plan. Include an encrypted full version - of the weighting entry so we can get back to it later.""" - encryptor = crypto.encryptor(FLAGS.build_plan_encryption_key) - cooked = [] - for entry in build_plan: - json_entry = json.dumps(entry) - cipher_text = encryptor(json_entry) - cooked.append(dict(weight=entry['weight'], - blob=cipher_text)) - return cooked - - -class ZonesXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return ZonesTemplate() - - def detail(self): - return ZonesTemplate() - - def select(self): - return WeightsTemplate() - - def default(self): - return ZoneTemplate() - - -class Zones(extensions.ExtensionDescriptor): - """Enables zones-related functionality such as adding child zones, - listing child zones, getting the capabilities of the local zone, - and returning build plans to parent zones' schedulers - """ - - name = "Zones" - alias = "os-zones" - namespace = "http://docs.openstack.org/compute/ext/zones/api/v1.1" - updated = "2011-09-21T00:00:00+00:00" - admin_only = True - - def get_resources(self): - #NOTE(bcwaldon): This resource should be prefixed with 'os-' - coll_actions = { - 'detail': 'GET', - 'info': 'GET', - 'select': 'POST', - } - - res = extensions.ResourceExtension('zones', - Controller(), - collection_actions=coll_actions) - return [res] diff --git a/nova/api/openstack/v2/extensions.py b/nova/api/openstack/v2/extensions.py deleted file mode 100644 index 0aa3146a9..000000000 --- a/nova/api/openstack/v2/extensions.py +++ /dev/null @@ -1,575 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# Copyright 2011 Justin Santa Barbara -# 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 routes -import webob.dec -import webob.exc - -import nova.api.openstack.v2 -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils -from nova import wsgi as base_wsgi - - -LOG = logging.getLogger('nova.api.openstack.v2.extensions') - - -FLAGS = flags.FLAGS - - -class ExtensionDescriptor(object): - """Base class that defines the contract for extensions. - - Note that you don't have to derive from this class to have a valid - extension; it is purely a convenience. - - """ - - # The name of the extension, e.g., 'Fox In Socks' - name = None - - # The alias for the extension, e.g., 'FOXNSOX' - alias = None - - # Description comes from the docstring for the class - - # The XML namespace for the extension, e.g., - # 'http://www.fox.in.socks/api/ext/pie/v1.0' - namespace = None - - # The timestamp when the extension was last updated, e.g., - # '2011-01-22T13:25:27-06:00' - updated = None - - # This attribute causes the extension to load only when - # the admin api is enabled - admin_only = False - - def __init__(self, ext_mgr): - """Register extension with the extension manager.""" - - ext_mgr.register(self) - - def get_resources(self): - """List of extensions.ResourceExtension extension objects. - - Resources define new nouns, and are accessible through URLs. - - """ - resources = [] - return resources - - def get_actions(self): - """List of extensions.ActionExtension extension objects. - - Actions are verbs callable from the API. - - """ - actions = [] - return actions - - def get_request_extensions(self): - """List of extensions.RequestExtension extension objects. - - Request extensions are used to handle custom request data. - - """ - request_exts = [] - return request_exts - - @classmethod - def nsmap(cls): - """Synthesize a namespace map from extension.""" - - # Start with a base nsmap - nsmap = ext_nsmap.copy() - - # Add the namespace for the extension - nsmap[cls.alias] = cls.namespace - - return nsmap - - @classmethod - def xmlname(cls, name): - """Synthesize element and attribute names.""" - - return '{%s}%s' % (cls.namespace, name) - - -class ActionExtensionController(object): - def __init__(self, application): - self.application = application - self.action_handlers = {} - - def add_action(self, action_name, handler): - self.action_handlers[action_name] = handler - - def action(self, req, id, body): - for action_name, handler in self.action_handlers.iteritems(): - if action_name in body: - return handler(body, req, id) - # no action handler found (bump to downstream application) - res = self.application - return res - - -class ActionExtensionResource(wsgi.Resource): - - def __init__(self, application): - controller = ActionExtensionController(application) - wsgi.Resource.__init__(self, controller, - serializer=wsgi.ResponseSerializer(), - deserializer=wsgi.RequestDeserializer()) - - def add_action(self, action_name, handler): - self.controller.add_action(action_name, handler) - - -class RequestExtensionController(object): - - def __init__(self, application): - self.application = application - self.handlers = [] - self.pre_handlers = [] - - def add_handler(self, handler): - self.handlers.append(handler) - - def add_pre_handler(self, pre_handler): - self.pre_handlers.append(pre_handler) - - def process(self, req, *args, **kwargs): - for pre_handler in self.pre_handlers: - pre_handler(req) - - res = req.get_response(self.application) - res.environ = req.environ - - # Don't call extensions if the main application returned an - # unsuccessful status - successful = 200 <= res.status_int < 400 - if not successful: - return res - - # Deserialize the response body, if any - body = None - if res.body: - body = utils.loads(res.body) - - # currently request handlers are un-ordered - for handler in self.handlers: - res = handler(req, res, body) - - # Reserialize the response body - if body is not None: - res.body = utils.dumps(body) - - return res - - -class RequestExtensionResource(wsgi.Resource): - - def __init__(self, application): - controller = RequestExtensionController(application) - wsgi.Resource.__init__(self, controller, - serializer=wsgi.ResponseSerializer(), - deserializer=wsgi.RequestDeserializer()) - - def add_handler(self, handler): - self.controller.add_handler(handler) - - def add_pre_handler(self, pre_handler): - self.controller.add_pre_handler(pre_handler) - - -def make_ext(elem): - elem.set('name') - elem.set('namespace') - elem.set('alias') - elem.set('updated') - - desc = xmlutil.SubTemplateElement(elem, 'description') - desc.text = 'description' - - xmlutil.make_links(elem, 'links') - - -ext_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class ExtensionTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('extension', selector='extension') - make_ext(root) - return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap) - - -class ExtensionsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('extensions') - elem = xmlutil.SubTemplateElement(root, 'extension', - selector='extensions') - make_ext(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap) - - -class ExtensionsResource(wsgi.Resource): - - def __init__(self, extension_manager): - self.extension_manager = extension_manager - super(ExtensionsResource, self).__init__(None) - - def _translate(self, ext): - ext_data = {} - ext_data['name'] = ext.name - ext_data['alias'] = ext.alias - ext_data['description'] = ext.__doc__ - ext_data['namespace'] = ext.namespace - ext_data['updated'] = ext.updated - ext_data['links'] = [] # TODO(dprince): implement extension links - return ext_data - - @wsgi.serializers(xml=ExtensionsTemplate) - def index(self, req): - extensions = [] - for _alias, ext in self.extension_manager.extensions.iteritems(): - extensions.append(self._translate(ext)) - return dict(extensions=extensions) - - @wsgi.serializers(xml=ExtensionTemplate) - def show(self, req, id): - try: - # NOTE(dprince): the extensions alias is used as the 'id' for show - ext = self.extension_manager.extensions[id] - except KeyError: - raise webob.exc.HTTPNotFound() - - return dict(extension=self._translate(ext)) - - def delete(self, req, id): - raise webob.exc.HTTPNotFound() - - def create(self, req): - raise webob.exc.HTTPNotFound() - - -class ExtensionMiddleware(base_wsgi.Middleware): - """Extensions middleware for WSGI.""" - @classmethod - def factory(cls, global_config, **local_config): - """Paste factory.""" - def _factory(app): - return cls(app, **local_config) - return _factory - - def _action_ext_resources(self, application, ext_mgr, mapper): - """Return a dict of ActionExtensionResource-s by collection.""" - action_resources = {} - for action in ext_mgr.get_actions(): - if not action.collection in action_resources.keys(): - resource = ActionExtensionResource(application) - mapper.connect("/:(project_id)/%s/:(id)/action.:(format)" % - action.collection, - action='action', - controller=resource, - conditions=dict(method=['POST'])) - mapper.connect("/:(project_id)/%s/:(id)/action" % - action.collection, - action='action', - controller=resource, - conditions=dict(method=['POST'])) - action_resources[action.collection] = resource - - return action_resources - - def _request_ext_resources(self, application, ext_mgr, mapper): - """Returns a dict of RequestExtensionResource-s by collection.""" - request_ext_resources = {} - for req_ext in ext_mgr.get_request_extensions(): - if not req_ext.key in request_ext_resources.keys(): - resource = RequestExtensionResource(application) - mapper.connect(req_ext.url_route + '.:(format)', - action='process', - controller=resource, - conditions=req_ext.conditions) - - mapper.connect(req_ext.url_route, - action='process', - controller=resource, - conditions=req_ext.conditions) - request_ext_resources[req_ext.key] = resource - - return request_ext_resources - - def __init__(self, application, ext_mgr=None): - - if ext_mgr is None: - ext_mgr = ExtensionManager() - self.ext_mgr = ext_mgr - - mapper = nova.api.openstack.v2.ProjectMapper() - - # extended actions - action_resources = self._action_ext_resources(application, ext_mgr, - mapper) - for action in ext_mgr.get_actions(): - LOG.debug(_('Extended action: %s'), action.action_name) - resource = action_resources[action.collection] - resource.add_action(action.action_name, action.handler) - - # extended requests - req_controllers = self._request_ext_resources(application, ext_mgr, - mapper) - for request_ext in ext_mgr.get_request_extensions(): - LOG.debug(_('Extended request: %s'), request_ext.key) - controller = req_controllers[request_ext.key] - if request_ext.handler: - controller.add_handler(request_ext.handler) - if request_ext.pre_handler: - controller.add_pre_handler(request_ext.pre_handler) - - self._router = routes.middleware.RoutesMiddleware(self._dispatch, - mapper) - - super(ExtensionMiddleware, self).__init__(application) - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - """Route the incoming request with router.""" - req.environ['extended.app'] = self.application - return self._router - - @staticmethod - @webob.dec.wsgify(RequestClass=wsgi.Request) - def _dispatch(req): - """Dispatch the request. - - Returns the routed WSGI app's response or defers to the extended - application. - - """ - match = req.environ['wsgiorg.routing_args'][1] - if not match: - return req.environ['extended.app'] - app = match['controller'] - return app - - -class ExtensionManager(object): - """Load extensions from the configured extension path. - - See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an - example extension implementation. - - """ - - _ext_mgr = None - - @classmethod - def reset(cls): - cls._ext_mgr = None - - def __new__(cls): - if cls._ext_mgr is None: - LOG.audit(_('Initializing extension manager.')) - - cls._ext_mgr = super(ExtensionManager, cls).__new__(cls) - - cls._ext_mgr.extensions = {} - cls._ext_mgr._load_extensions() - - return cls._ext_mgr - - def register(self, ext): - # Do nothing if the extension doesn't check out - if not self._check_extension(ext): - return - - alias = ext.alias - LOG.audit(_('Loaded extension: %s'), alias) - - if alias in self.extensions: - raise exception.Error("Found duplicate extension: %s" % alias) - self.extensions[alias] = ext - - def get_resources(self): - """Returns a list of ResourceExtension objects.""" - - resources = [] - resources.append(ResourceExtension('extensions', - ExtensionsResource(self))) - - for ext in self.extensions.values(): - try: - resources.extend(ext.get_resources()) - except AttributeError: - # NOTE(dprince): Extension aren't required to have resource - # extensions - pass - return resources - - def get_actions(self): - """Returns a list of ActionExtension objects.""" - actions = [] - for ext in self.extensions.values(): - try: - actions.extend(ext.get_actions()) - except AttributeError: - # NOTE(dprince): Extension aren't required to have action - # extensions - pass - return actions - - def get_request_extensions(self): - """Returns a list of RequestExtension objects.""" - request_exts = [] - for ext in self.extensions.values(): - try: - request_exts.extend(ext.get_request_extensions()) - except AttributeError: - # NOTE(dprince): Extension aren't required to have request - # extensions - pass - return request_exts - - def _check_extension(self, extension): - """Checks for required methods in extension objects.""" - try: - LOG.debug(_('Ext name: %s'), extension.name) - LOG.debug(_('Ext alias: %s'), extension.alias) - LOG.debug(_('Ext description: %s'), - ' '.join(extension.__doc__.strip().split())) - LOG.debug(_('Ext namespace: %s'), extension.namespace) - LOG.debug(_('Ext updated: %s'), extension.updated) - LOG.debug(_('Ext admin_only: %s'), extension.admin_only) - except AttributeError as ex: - LOG.exception(_("Exception loading extension: %s"), unicode(ex)) - return False - - # Don't load admin api extensions if the admin api isn't enabled - if not FLAGS.allow_admin_api and extension.admin_only: - return False - - return True - - def load_extension(self, ext_factory): - """Execute an extension factory. - - Loads an extension. The 'ext_factory' is the name of a - callable that will be imported and called with one - argument--the extension manager. The factory callable is - expected to call the register() method at least once. - """ - - LOG.debug(_("Loading extension %s"), ext_factory) - - # Load the factory - - factory = utils.import_class(ext_factory) - - # Call it - LOG.debug(_("Calling extension factory %s"), ext_factory) - factory(self) - - def _load_extensions(self): - """Load extensions specified on the command line.""" - - extensions = list(FLAGS.osapi_extension) - - for ext_factory in extensions: - try: - self.load_extension(ext_factory) - except Exception as exc: - LOG.warn(_('Failed to load extension %(ext_factory)s: ' - '%(exc)s') % locals()) - - -class RequestExtension(object): - """Extend requests and responses of core nova OpenStack API resources. - - Provide a way to add data to responses and handle custom request data - that is sent to core nova OpenStack API controllers. - - """ - def __init__(self, method, url_route, handler=None, pre_handler=None): - self.url_route = url_route - self.handler = handler - self.conditions = dict(method=[method]) - self.key = "%s-%s" % (method, url_route) - self.pre_handler = pre_handler - - -class ActionExtension(object): - """Add custom actions to core nova OpenStack API resources.""" - - def __init__(self, collection, action_name, handler): - self.collection = collection - self.action_name = action_name - self.handler = handler - - -class ResourceExtension(object): - """Add top level resources to the OpenStack API in nova.""" - - def __init__(self, collection, controller, parent=None, - collection_actions=None, member_actions=None, - deserializer=None, serializer=None): - if not collection_actions: - collection_actions = {} - if not member_actions: - member_actions = {} - self.collection = collection - self.controller = controller - self.parent = parent - self.collection_actions = collection_actions - self.member_actions = member_actions - self.deserializer = deserializer - self.serializer = serializer - - -class ExtensionsXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return ExtensionsTemplate() - - def show(self): - return ExtensionTemplate() - - -def require_admin(f): - @functools.wraps(f) - def wraps(self, req, *args, **kwargs): - if 'nova.context' in req.environ and\ - req.environ['nova.context'].is_admin: - return f(self, req, *args, **kwargs) - else: - raise exception.AdminRequired() - return wraps - - -def wrap_errors(fn): - """Ensure errors are not passed along.""" - def wrapped(*args): - try: - return fn(*args) - except Exception, e: - raise webob.exc.HTTPInternalServerError() - return wrapped diff --git a/nova/api/openstack/v2/flavors.py b/nova/api/openstack/v2/flavors.py deleted file mode 100644 index 21c2a8120..000000000 --- a/nova/api/openstack/v2/flavors.py +++ /dev/null @@ -1,112 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob - -from nova.api.openstack.v2.views import flavors as flavors_view -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.compute import instance_types -from nova import exception - - -def make_flavor(elem, detailed=False): - elem.set('name') - elem.set('id') - if detailed: - elem.set('ram') - elem.set('disk') - - for attr in ("vcpus", "swap", "rxtx_factor"): - elem.set(attr, xmlutil.EmptyStringSelector(attr)) - - xmlutil.make_links(elem, 'links') - - -flavor_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class FlavorTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('flavor', selector='flavor') - make_flavor(root, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) - - -class MinimalFlavorsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('flavors') - elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') - make_flavor(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) - - -class FlavorsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('flavors') - elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') - make_flavor(elem, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) - - -class Controller(wsgi.Controller): - """Flavor controller for the OpenStack API.""" - - _view_builder_class = flavors_view.ViewBuilder - - @wsgi.serializers(xml=MinimalFlavorsTemplate) - def index(self, req): - """Return all flavors in brief.""" - flavors = self._get_flavors(req) - return self._view_builder.index(req, flavors) - - @wsgi.serializers(xml=FlavorsTemplate) - def detail(self, req): - """Return all flavors in detail.""" - flavors = self._get_flavors(req) - return self._view_builder.detail(req, flavors) - - @wsgi.serializers(xml=FlavorTemplate) - def show(self, req, id): - """Return data about the given flavor id.""" - try: - flavor = instance_types.get_instance_type_by_flavor_id(id) - except exception.NotFound: - raise webob.exc.HTTPNotFound() - - return self._view_builder.show(req, flavor) - - def _get_flavors(self, req): - """Helper function that returns a list of flavor dicts.""" - filters = {} - if 'minRam' in req.params: - try: - filters['min_memory_mb'] = int(req.params['minRam']) - except ValueError: - pass # ignore bogus values per spec - - if 'minDisk' in req.params: - try: - filters['min_local_gb'] = int(req.params['minDisk']) - except ValueError: - pass # ignore bogus values per spec - - return instance_types.get_all_types(filters=filters) - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/image_metadata.py b/nova/api/openstack/v2/image_metadata.py deleted file mode 100644 index 1e29d23ce..000000000 --- a/nova/api/openstack/v2/image_metadata.py +++ /dev/null @@ -1,118 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from webob import exc - -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova import exception -from nova import flags -from nova import image - - -FLAGS = flags.FLAGS - - -class Controller(object): - """The image metadata API controller for the Openstack API""" - - def __init__(self): - self.image_service = image.get_default_image_service() - - def _get_image(self, context, image_id): - try: - return self.image_service.show(context, image_id) - except exception.NotFound: - msg = _("Image not found.") - raise exc.HTTPNotFound(explanation=msg) - - @wsgi.serializers(xml=common.MetadataTemplate) - def index(self, req, image_id): - """Returns the list of metadata for a given instance""" - context = req.environ['nova.context'] - metadata = self._get_image(context, image_id)['properties'] - return dict(metadata=metadata) - - @wsgi.serializers(xml=common.MetaItemTemplate) - def show(self, req, image_id, id): - context = req.environ['nova.context'] - metadata = self._get_image(context, image_id)['properties'] - if id in metadata: - return {'meta': {id: metadata[id]}} - else: - raise exc.HTTPNotFound() - - @wsgi.serializers(xml=common.MetadataTemplate) - @wsgi.deserializers(xml=common.MetadataDeserializer) - def create(self, req, image_id, body): - context = req.environ['nova.context'] - image = self._get_image(context, image_id) - if 'metadata' in body: - for key, value in body['metadata'].iteritems(): - image['properties'][key] = value - common.check_img_metadata_quota_limit(context, image['properties']) - self.image_service.update(context, image_id, image, None) - return dict(metadata=image['properties']) - - @wsgi.serializers(xml=common.MetaItemTemplate) - @wsgi.deserializers(xml=common.MetaItemDeserializer) - def update(self, req, image_id, id, body): - context = req.environ['nova.context'] - - try: - meta = body['meta'] - except KeyError: - expl = _('Incorrect request body format') - raise exc.HTTPBadRequest(explanation=expl) - - if not id in meta: - expl = _('Request body and URI mismatch') - raise exc.HTTPBadRequest(explanation=expl) - if len(meta) > 1: - expl = _('Request body contains too many items') - raise exc.HTTPBadRequest(explanation=expl) - - image = self._get_image(context, image_id) - image['properties'][id] = meta[id] - common.check_img_metadata_quota_limit(context, image['properties']) - self.image_service.update(context, image_id, image, None) - return dict(meta=meta) - - @wsgi.serializers(xml=common.MetadataTemplate) - @wsgi.deserializers(xml=common.MetadataDeserializer) - def update_all(self, req, image_id, body): - context = req.environ['nova.context'] - image = self._get_image(context, image_id) - metadata = body.get('metadata', {}) - common.check_img_metadata_quota_limit(context, metadata) - image['properties'] = metadata - self.image_service.update(context, image_id, image, None) - return dict(metadata=metadata) - - @wsgi.response(204) - def delete(self, req, image_id, id): - context = req.environ['nova.context'] - image = self._get_image(context, image_id) - if not id in image['properties']: - msg = _("Invalid metadata key") - raise exc.HTTPNotFound(explanation=msg) - image['properties'].pop(id) - self.image_service.update(context, image_id, image, None) - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/images.py b/nova/api/openstack/v2/images.py deleted file mode 100644 index 96a2275e6..000000000 --- a/nova/api/openstack/v2/images.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob.exc - -from nova.api.openstack import common -from nova.api.openstack.v2.views import images as views_images -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception -from nova import flags -import nova.image -from nova import log - - -LOG = log.getLogger('nova.api.openstack.v2.images') -FLAGS = flags.FLAGS - -SUPPORTED_FILTERS = { - 'name': 'name', - 'status': 'status', - 'changes-since': 'changes-since', - 'server': 'property-instance_ref', - 'type': 'property-image_type', - 'minRam': 'min_ram', - 'minDisk': 'min_disk', -} - - -def make_image(elem, detailed=False): - elem.set('name') - elem.set('id') - - if detailed: - elem.set('updated') - elem.set('created') - elem.set('status') - elem.set('progress') - elem.set('minRam') - elem.set('minDisk') - - server = xmlutil.SubTemplateElement(elem, 'server', selector='server') - server.set('id') - xmlutil.make_links(server, 'links') - - elem.append(common.MetadataTemplate()) - - xmlutil.make_links(elem, 'links') - - -image_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class ImageTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('image', selector='image') - make_image(root, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) - - -class MinimalImagesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('images') - elem = xmlutil.SubTemplateElement(root, 'image', selector='images') - make_image(elem) - xmlutil.make_links(root, 'images_links') - return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) - - -class ImagesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('images') - elem = xmlutil.SubTemplateElement(root, 'image', selector='images') - make_image(elem, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) - - -class Controller(wsgi.Controller): - """Base controller for retrieving/displaying images.""" - - _view_builder_class = views_images.ViewBuilder - - def __init__(self, image_service=None, compute_service=None, **kwargs): - """Initialize new `ImageController`. - - :param compute_service: `nova.compute.api:API` - :param image_service: `nova.image.glance:GlancemageService` - - """ - super(Controller, self).__init__(**kwargs) - self._compute_service = compute_service or compute.API() - self._image_service = image_service or \ - nova.image.get_default_image_service() - - def _get_filters(self, req): - """ - Return a dictionary of query param filters from the request - - :param req: the Request object coming from the wsgi layer - :retval a dict of key/value filters - """ - filters = {} - for param in req.params: - if param in SUPPORTED_FILTERS or param.startswith('property-'): - # map filter name or carry through if property-* - filter_name = SUPPORTED_FILTERS.get(param, param) - filters[filter_name] = req.params.get(param) - return filters - - @wsgi.serializers(xml=ImageTemplate) - def show(self, req, id): - """Return detailed information about a specific image. - - :param req: `wsgi.Request` object - :param id: Image identifier - """ - context = req.environ['nova.context'] - - try: - image = self._image_service.show(context, id) - except (exception.NotFound, exception.InvalidImageRef): - explanation = _("Image not found.") - raise webob.exc.HTTPNotFound(explanation=explanation) - - return self._view_builder.show(req, image) - - def delete(self, req, id): - """Delete an image, if allowed. - - :param req: `wsgi.Request` object - :param id: Image identifier (integer) - """ - context = req.environ['nova.context'] - try: - self._image_service.delete(context, id) - except exception.ImageNotFound: - explanation = _("Image not found.") - raise webob.exc.HTTPNotFound(explanation=explanation) - return webob.exc.HTTPNoContent() - - @wsgi.serializers(xml=MinimalImagesTemplate) - def index(self, req): - """Return an index listing of images available to the request. - - :param req: `wsgi.Request` object - - """ - context = req.environ['nova.context'] - filters = self._get_filters(req) - params = req.GET.copy() - page_params = common.get_pagination_params(req) - for key, val in page_params.iteritems(): - params[key] = val - - images = self._image_service.index(context, filters=filters, - **page_params) - return self._view_builder.index(req, images) - - @wsgi.serializers(xml=ImagesTemplate) - def detail(self, req): - """Return a detailed index listing of images available to the request. - - :param req: `wsgi.Request` object. - - """ - context = req.environ['nova.context'] - filters = self._get_filters(req) - params = req.GET.copy() - page_params = common.get_pagination_params(req) - for key, val in page_params.iteritems(): - params[key] = val - images = self._image_service.detail(context, filters=filters, - **page_params) - - return self._view_builder.detail(req, images) - - def create(self, *args, **kwargs): - raise webob.exc.HTTPMethodNotAllowed() - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/ips.py b/nova/api/openstack/v2/ips.py deleted file mode 100644 index 3dc9cf928..000000000 --- a/nova/api/openstack/v2/ips.py +++ /dev/null @@ -1,105 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from webob import exc - -import nova -from nova.api.openstack import common -from nova.api.openstack.v2.views import addresses as view_addresses -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import log as logging -from nova import flags - - -LOG = logging.getLogger('nova.api.openstack.v2.ips') -FLAGS = flags.FLAGS - - -def make_network(elem): - elem.set('id', 0) - - ip = xmlutil.SubTemplateElement(elem, 'ip', selector=1) - ip.set('version') - ip.set('addr') - - -network_nsmap = {None: xmlutil.XMLNS_V11} - - -class NetworkTemplate(xmlutil.TemplateBuilder): - def construct(self): - sel = xmlutil.Selector(xmlutil.get_items, 0) - root = xmlutil.TemplateElement('network', selector=sel) - make_network(root) - return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) - - -class AddressesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('addresses', selector='addresses') - elem = xmlutil.SubTemplateElement(root, 'network', - selector=xmlutil.get_items) - make_network(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) - - -class Controller(wsgi.Controller): - """The servers addresses API controller for the Openstack API.""" - - _view_builder_class = view_addresses.ViewBuilder - - def __init__(self, **kwargs): - super(Controller, self).__init__(**kwargs) - self._compute_api = nova.compute.API() - - def _get_instance(self, context, server_id): - try: - instance = self._compute_api.get(context, server_id) - except nova.exception.NotFound: - msg = _("Instance does not exist") - raise exc.HTTPNotFound(explanation=msg) - return instance - - def create(self, req, server_id, body): - raise exc.HTTPNotImplemented() - - def delete(self, req, server_id, id): - raise exc.HTTPNotImplemented() - - @wsgi.serializers(xml=AddressesTemplate) - def index(self, req, server_id): - context = req.environ["nova.context"] - instance = self._get_instance(context, server_id) - networks = common.get_networks_for_instance(context, instance) - return self._view_builder.index(networks) - - @wsgi.serializers(xml=NetworkTemplate) - def show(self, req, server_id, id): - context = req.environ["nova.context"] - instance = self._get_instance(context, server_id) - networks = common.get_networks_for_instance(context, instance) - - if id not in networks: - msg = _("Instance is not a member of specified network") - raise exc.HTTPNotFound(explanation=msg) - - return self._view_builder.show(networks[id], id) - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/limits.py b/nova/api/openstack/v2/limits.py deleted file mode 100644 index e1f5ff836..000000000 --- a/nova/api/openstack/v2/limits.py +++ /dev/null @@ -1,477 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Module dedicated functions/classes dealing with rate limiting requests. -""" - -from collections import defaultdict -import copy -import httplib -import json -import math -import re -import time - -from webob.dec import wsgify -import webob.exc - -from nova.api.openstack.v2.views import limits as limits_views -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import quota -from nova import utils -from nova import wsgi as base_wsgi - - -# Convenience constants for the limits dictionary passed to Limiter(). -PER_SECOND = 1 -PER_MINUTE = 60 -PER_HOUR = 60 * 60 -PER_DAY = 60 * 60 * 24 - - -limits_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class LimitsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('limits', selector='limits') - - rates = xmlutil.SubTemplateElement(root, 'rates') - rate = xmlutil.SubTemplateElement(rates, 'rate', selector='rate') - rate.set('uri', 'uri') - rate.set('regex', 'regex') - limit = xmlutil.SubTemplateElement(rate, 'limit', selector='limit') - limit.set('value', 'value') - limit.set('verb', 'verb') - limit.set('remaining', 'remaining') - limit.set('unit', 'unit') - limit.set('next-available', 'next-available') - - absolute = xmlutil.SubTemplateElement(root, 'absolute', - selector='absolute') - limit = xmlutil.SubTemplateElement(absolute, 'limit', - selector=xmlutil.get_items) - limit.set('name', 0) - limit.set('value', 1) - - return xmlutil.MasterTemplate(root, 1, nsmap=limits_nsmap) - - -class LimitsController(object): - """ - Controller for accessing limits in the OpenStack API. - """ - - @wsgi.serializers(xml=LimitsTemplate) - def index(self, req): - """ - Return all global and rate limit information. - """ - context = req.environ['nova.context'] - abs_limits = quota.get_project_quotas(context, context.project_id) - rate_limits = req.environ.get("nova.limits", []) - - builder = self._get_view_builder(req) - return builder.build(rate_limits, abs_limits) - - def _get_view_builder(self, req): - return limits_views.ViewBuilder() - - -def create_resource(): - return wsgi.Resource(LimitsController()) - - -class Limit(object): - """ - Stores information about a limit for HTTP requests. - """ - - UNITS = { - 1: "SECOND", - 60: "MINUTE", - 60 * 60: "HOUR", - 60 * 60 * 24: "DAY", - } - - UNIT_MAP = dict([(v, k) for k, v in UNITS.items()]) - - def __init__(self, verb, uri, regex, value, unit): - """ - Initialize a new `Limit`. - - @param verb: HTTP verb (POST, PUT, etc.) - @param uri: Human-readable URI - @param regex: Regular expression format for this limit - @param value: Integer number of requests which can be made - @param unit: Unit of measure for the value parameter - """ - self.verb = verb - self.uri = uri - self.regex = regex - self.value = int(value) - self.unit = unit - self.unit_string = self.display_unit().lower() - self.remaining = int(value) - - if value <= 0: - raise ValueError("Limit value must be > 0") - - self.last_request = None - self.next_request = None - - self.water_level = 0 - self.capacity = self.unit - self.request_value = float(self.capacity) / float(self.value) - self.error_message = _("Only %(value)s %(verb)s request(s) can be "\ - "made to %(uri)s every %(unit_string)s." % self.__dict__) - - def __call__(self, verb, url): - """ - Represents a call to this limit from a relevant request. - - @param verb: string http verb (POST, GET, etc.) - @param url: string URL - """ - if self.verb != verb or not re.match(self.regex, url): - return - - now = self._get_time() - - if self.last_request is None: - self.last_request = now - - leak_value = now - self.last_request - - self.water_level -= leak_value - self.water_level = max(self.water_level, 0) - self.water_level += self.request_value - - difference = self.water_level - self.capacity - - self.last_request = now - - if difference > 0: - self.water_level -= self.request_value - self.next_request = now + difference - return difference - - cap = self.capacity - water = self.water_level - val = self.value - - self.remaining = math.floor(((cap - water) / cap) * val) - self.next_request = now - - def _get_time(self): - """Retrieve the current time. Broken out for testability.""" - return time.time() - - def display_unit(self): - """Display the string name of the unit.""" - return self.UNITS.get(self.unit, "UNKNOWN") - - def display(self): - """Return a useful representation of this class.""" - return { - "verb": self.verb, - "URI": self.uri, - "regex": self.regex, - "value": self.value, - "remaining": int(self.remaining), - "unit": self.display_unit(), - "resetTime": int(self.next_request or self._get_time()), - } - -# "Limit" format is a dictionary with the HTTP verb, human-readable URI, -# a regular-expression to match, value and unit of measure (PER_DAY, etc.) - -DEFAULT_LIMITS = [ - Limit("POST", "*", ".*", 10, PER_MINUTE), - Limit("POST", "*/servers", "^/servers", 50, PER_DAY), - Limit("PUT", "*", ".*", 10, PER_MINUTE), - Limit("GET", "*changes-since*", ".*changes-since.*", 3, PER_MINUTE), - Limit("DELETE", "*", ".*", 100, PER_MINUTE), -] - - -class RateLimitingMiddleware(base_wsgi.Middleware): - """ - Rate-limits requests passing through this middleware. All limit information - is stored in memory for this implementation. - """ - - def __init__(self, application, limits=None, limiter=None, **kwargs): - """ - Initialize new `RateLimitingMiddleware`, which wraps the given WSGI - application and sets up the given limits. - - @param application: WSGI application to wrap - @param limits: String describing limits - @param limiter: String identifying class for representing limits - - Other parameters are passed to the constructor for the limiter. - """ - base_wsgi.Middleware.__init__(self, application) - - # Select the limiter class - if limiter is None: - limiter = Limiter - else: - limiter = utils.import_class(limiter) - - # Parse the limits, if any are provided - if limits is not None: - limits = limiter.parse_limits(limits) - - self._limiter = limiter(limits or DEFAULT_LIMITS, **kwargs) - - @wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - """ - Represents a single call through this middleware. We should record the - request if we have a limit relevant to it. If no limit is relevant to - the request, ignore it. - - If the request should be rate limited, return a fault telling the user - they are over the limit and need to retry later. - """ - verb = req.method - url = req.url - context = req.environ.get("nova.context") - - if context: - username = context.user_id - else: - username = None - - delay, error = self._limiter.check_for_delay(verb, url, username) - - if delay: - msg = _("This request was rate-limited.") - retry = time.time() + delay - return wsgi.OverLimitFault(msg, error, retry) - - req.environ["nova.limits"] = self._limiter.get_limits(username) - - return self.application - - -class Limiter(object): - """ - Rate-limit checking class which handles limits in memory. - """ - - def __init__(self, limits, **kwargs): - """ - Initialize the new `Limiter`. - - @param limits: List of `Limit` objects - """ - self.limits = copy.deepcopy(limits) - self.levels = defaultdict(lambda: copy.deepcopy(limits)) - - # Pick up any per-user limit information - for key, value in kwargs.items(): - if key.startswith('user:'): - username = key[5:] - self.levels[username] = self.parse_limits(value) - - def get_limits(self, username=None): - """ - Return the limits for a given user. - """ - return [limit.display() for limit in self.levels[username]] - - def check_for_delay(self, verb, url, username=None): - """ - Check the given verb/user/user triplet for limit. - - @return: Tuple of delay (in seconds) and error message (or None, None) - """ - delays = [] - - for limit in self.levels[username]: - delay = limit(verb, url) - if delay: - delays.append((delay, limit.error_message)) - - if delays: - delays.sort() - return delays[0] - - return None, None - - # Note: This method gets called before the class is instantiated, - # so this must be either a static method or a class method. It is - # used to develop a list of limits to feed to the constructor. We - # put this in the class so that subclasses can override the - # default limit parsing. - @staticmethod - def parse_limits(limits): - """ - Convert a string into a list of Limit instances. This - implementation expects a semicolon-separated sequence of - parenthesized groups, where each group contains a - comma-separated sequence consisting of HTTP method, - user-readable URI, a URI reg-exp, an integer number of - requests which can be made, and a unit of measure. Valid - values for the latter are "SECOND", "MINUTE", "HOUR", and - "DAY". - - @return: List of Limit instances. - """ - - # Handle empty limit strings - limits = limits.strip() - if not limits: - return [] - - # Split up the limits by semicolon - result = [] - for group in limits.split(';'): - group = group.strip() - if group[:1] != '(' or group[-1:] != ')': - raise ValueError("Limit rules must be surrounded by " - "parentheses") - group = group[1:-1] - - # Extract the Limit arguments - args = [a.strip() for a in group.split(',')] - if len(args) != 5: - raise ValueError("Limit rules must contain the following " - "arguments: verb, uri, regex, value, unit") - - # Pull out the arguments - verb, uri, regex, value, unit = args - - # Upper-case the verb - verb = verb.upper() - - # Convert value--raises ValueError if it's not integer - value = int(value) - - # Convert unit - unit = unit.upper() - if unit not in Limit.UNIT_MAP: - raise ValueError("Invalid units specified") - unit = Limit.UNIT_MAP[unit] - - # Build a limit - result.append(Limit(verb, uri, regex, value, unit)) - - return result - - -class WsgiLimiter(object): - """ - Rate-limit checking from a WSGI application. Uses an in-memory `Limiter`. - - To use: - POST / with JSON data such as: - { - "verb" : GET, - "path" : "/servers" - } - - and receive a 204 No Content, or a 403 Forbidden with an X-Wait-Seconds - header containing the number of seconds to wait before the action would - succeed. - """ - - def __init__(self, limits=None): - """ - Initialize the new `WsgiLimiter`. - - @param limits: List of `Limit` objects - """ - self._limiter = Limiter(limits or DEFAULT_LIMITS) - - @wsgify(RequestClass=wsgi.Request) - def __call__(self, request): - """ - Handles a call to this application. Returns 204 if the request is - acceptable to the limiter, else a 403 is returned with a relevant - header indicating when the request *will* succeed. - """ - if request.method != "POST": - raise webob.exc.HTTPMethodNotAllowed() - - try: - info = dict(json.loads(request.body)) - except ValueError: - raise webob.exc.HTTPBadRequest() - - username = request.path_info_pop() - verb = info.get("verb") - path = info.get("path") - - delay, error = self._limiter.check_for_delay(verb, path, username) - - if delay: - headers = {"X-Wait-Seconds": "%.2f" % delay} - return webob.exc.HTTPForbidden(headers=headers, explanation=error) - else: - return webob.exc.HTTPNoContent() - - -class WsgiLimiterProxy(object): - """ - Rate-limit requests based on answers from a remote source. - """ - - def __init__(self, limiter_address): - """ - Initialize the new `WsgiLimiterProxy`. - - @param limiter_address: IP/port combination of where to request limit - """ - self.limiter_address = limiter_address - - def check_for_delay(self, verb, path, username=None): - body = json.dumps({"verb": verb, "path": path}) - headers = {"Content-Type": "application/json"} - - conn = httplib.HTTPConnection(self.limiter_address) - - if username: - conn.request("POST", "/%s" % (username), body, headers) - else: - conn.request("POST", "/", body, headers) - - resp = conn.getresponse() - - if 200 >= resp.status < 300: - return None, None - - return resp.getheader("X-Wait-Seconds"), resp.read() or None - - # Note: This method gets called before the class is instantiated, - # so this must be either a static method or a class method. It is - # used to develop a list of limits to feed to the constructor. - # This implementation returns an empty list, since all limit - # decisions are made by a remote server. - @staticmethod - def parse_limits(limits): - """ - Ignore a limits string--simply doesn't apply for the limit - proxy. - - @return: Empty list. - """ - - return [] diff --git a/nova/api/openstack/v2/ratelimiting/__init__.py b/nova/api/openstack/v2/ratelimiting/__init__.py deleted file mode 100644 index 78dc465a7..000000000 --- a/nova/api/openstack/v2/ratelimiting/__init__.py +++ /dev/null @@ -1,222 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Rate limiting of arbitrary actions.""" - -import httplib -import time -import urllib - -import webob.dec -import webob.exc - -from nova import wsgi -from nova.api.openstack import wsgi as os_wsgi - -# Convenience constants for the limits dictionary passed to Limiter(). -PER_SECOND = 1 -PER_MINUTE = 60 -PER_HOUR = 60 * 60 -PER_DAY = 60 * 60 * 24 - - -class RateLimitingMiddleware(wsgi.Middleware): - """Rate limit incoming requests according to the OpenStack rate limits.""" - - def __init__(self, application, service_host=None): - """Create a rate limiting middleware that wraps the given application. - - By default, rate counters are stored in memory. If service_host is - specified, the middleware instead relies on the ratelimiting.WSGIApp - at the given host+port to keep rate counters. - """ - if not service_host: - #TODO(gundlach): These limits were based on limitations of Cloud - #Servers. We should revisit them in Nova. - self.limiter = Limiter(limits={ - 'DELETE': (100, PER_MINUTE), - 'PUT': (10, PER_MINUTE), - 'POST': (10, PER_MINUTE), - 'POST servers': (50, PER_DAY), - 'GET changes-since': (3, PER_MINUTE), - }) - else: - self.limiter = WSGIAppProxy(service_host) - super(RateLimitingMiddleware, self).__init__(application) - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - """Rate limit the request. - - If the request should be rate limited, return a 413 status with a - Retry-After header giving the time when the request would succeed. - """ - return self.rate_limited_request(req, self.application) - - def rate_limited_request(self, req, application): - """Rate limit the request. - - If the request should be rate limited, return a 413 status with a - Retry-After header giving the time when the request would succeed. - """ - action_name = self.get_action_name(req) - if not action_name: - # Not rate limited - return application - delay = self.get_delay(action_name, - req.environ['nova.context'].user_id) - if delay: - # TODO(gundlach): Get the retry-after format correct. - exc = webob.exc.HTTPRequestEntityTooLarge( - explanation=('Too many requests.'), - headers={'Retry-After': time.time() + delay}) - raise os_wsgi.Fault(exc) - return application - - def get_delay(self, action_name, username): - """Return the delay for the given action and username, or None if - the action would not be rate limited. - """ - if action_name == 'POST servers': - # "POST servers" is a POST, so it counts against "POST" too. - # Attempt the "POST" first, lest we are rate limited by "POST" but - # use up a precious "POST servers" call. - delay = self.limiter.perform("POST", username=username) - if delay: - return delay - return self.limiter.perform(action_name, username=username) - - def get_action_name(self, req): - """Return the action name for this request.""" - if req.method == 'GET' and 'changes-since' in req.GET: - return 'GET changes-since' - if req.method == 'POST' and req.path_info.startswith('/servers'): - return 'POST servers' - if req.method in ['PUT', 'POST', 'DELETE']: - return req.method - return None - - -class Limiter(object): - - """Class providing rate limiting of arbitrary actions.""" - - def __init__(self, limits): - """Create a rate limiter. - - limits: a dict mapping from action name to a tuple. The tuple contains - the number of times the action may be performed, and the time period - (in seconds) during which the number must not be exceeded for this - action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would - allow 10 'reboot' actions per minute. - """ - self.limits = limits - self._levels = {} - - def perform(self, action_name, username='nobody'): - """Attempt to perform an action by the given username. - - action_name: the string name of the action to perform. This must - be a key in the limits dict passed to the ctor. - - username: an optional string name of the user performing the action. - Each user has her own set of rate limiting counters. Defaults to - 'nobody' (so that if you never specify a username when calling - perform(), a single set of counters will be used.) - - Return None if the action may proceed. If the action may not proceed - because it has been rate limited, return the float number of seconds - until the action would succeed. - """ - # Think of rate limiting as a bucket leaking water at 1cc/second. The - # bucket can hold as many ccs as there are seconds in the rate - # limiting period (e.g. 3600 for per-hour ratelimits), and if you can - # perform N actions in that time, each action fills the bucket by - # 1/Nth of its volume. You may only perform an action if the bucket - # would not overflow. - now = time.time() - key = '%s:%s' % (username, action_name) - last_time_performed, water_level = self._levels.get(key, (now, 0)) - # The bucket leaks 1cc/second. - water_level -= (now - last_time_performed) - if water_level < 0: - water_level = 0 - num_allowed_per_period, period_in_secs = self.limits[action_name] - # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. - capacity = period_in_secs - new_level = water_level + (capacity * 1.0 / num_allowed_per_period) - if new_level > capacity: - # Delay this many seconds. - return new_level - capacity - self._levels[key] = (now, new_level) - return None - -# If one instance of this WSGIApps is unable to handle your load, put a -# sharding app in front that shards by username to one of many backends. - - -class WSGIApp(object): - - """Application that tracks rate limits in memory. Send requests to it of - this form: - - POST /limiter// - - and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header - containing the number of seconds to wait before the action would succeed. - """ - - def __init__(self, limiter): - """Create the WSGI application using the given Limiter instance.""" - self.limiter = limiter - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - parts = req.path_info.split('/') - # format: /limiter// - if req.method != 'POST': - raise webob.exc.HTTPMethodNotAllowed() - if len(parts) != 4 or parts[1] != 'limiter': - raise webob.exc.HTTPNotFound() - username = parts[2] - action_name = urllib.unquote(parts[3]) - delay = self.limiter.perform(action_name, username) - if delay: - return webob.exc.HTTPForbidden( - headers={'X-Wait-Seconds': "%.2f" % delay}) - else: - # 200 OK - return '' - - -class WSGIAppProxy(object): - - """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" - - def __init__(self, service_host): - """Creates a proxy pointing to a ratelimiting.WSGIApp at the given - host.""" - self.service_host = service_host - - def perform(self, action, username='nobody'): - conn = httplib.HTTPConnection(self.service_host) - conn.request('POST', '/limiter/%s/%s' % (username, action)) - resp = conn.getresponse() - if resp.status == 200: - # No delay - return None - return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/openstack/v2/schemas/atom-link.rng b/nova/api/openstack/v2/schemas/atom-link.rng deleted file mode 100644 index edba5eee6..000000000 --- a/nova/api/openstack/v2/schemas/atom-link.rng +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - [^:]* - - - - - - .+/.+ - - - - - - [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* - - - - - - - - - - - - xml:base - xml:lang - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/atom.rng b/nova/api/openstack/v2/schemas/atom.rng deleted file mode 100644 index c2df4e410..000000000 --- a/nova/api/openstack/v2/schemas/atom.rng +++ /dev/null @@ -1,597 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text - html - - - - - - - - - xhtml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An atom:feed must have an atom:author unless all of its atom:entry children have an atom:author. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An atom:entry must have at least one atom:link element with a rel attribute of 'alternate' or an atom:content. - - - An atom:entry must have an atom:author if its feed does not. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text - html - - - - - - - - - - - - - xhtml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - [^:]* - - - - - - .+/.+ - - - - - - [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* - - - - - - - - - - .+@.+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - xml:base - xml:lang - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/addresses.rng b/nova/api/openstack/v2/schemas/v1.1/addresses.rng deleted file mode 100644 index b498e8a63..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/addresses.rng +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/extension.rng b/nova/api/openstack/v2/schemas/v1.1/extension.rng deleted file mode 100644 index 336659755..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/extension.rng +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/extensions.rng b/nova/api/openstack/v2/schemas/v1.1/extensions.rng deleted file mode 100644 index 4d8bff646..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/extensions.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/flavor.rng b/nova/api/openstack/v2/schemas/v1.1/flavor.rng deleted file mode 100644 index 08746ce3d..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/flavor.rng +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/flavors.rng b/nova/api/openstack/v2/schemas/v1.1/flavors.rng deleted file mode 100644 index b7a3acc01..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/flavors.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/flavors_index.rng b/nova/api/openstack/v2/schemas/v1.1/flavors_index.rng deleted file mode 100644 index d1a4fedb1..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/flavors_index.rng +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/image.rng b/nova/api/openstack/v2/schemas/v1.1/image.rng deleted file mode 100644 index 505081fba..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/image.rng +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/images.rng b/nova/api/openstack/v2/schemas/v1.1/images.rng deleted file mode 100644 index 064d4d9cc..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/images.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/images_index.rng b/nova/api/openstack/v2/schemas/v1.1/images_index.rng deleted file mode 100644 index 3db0b2672..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/images_index.rng +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/limits.rng b/nova/api/openstack/v2/schemas/v1.1/limits.rng deleted file mode 100644 index 1af8108ec..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/limits.rng +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/metadata.rng b/nova/api/openstack/v2/schemas/v1.1/metadata.rng deleted file mode 100644 index b2f5d702a..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/metadata.rng +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/server.rng b/nova/api/openstack/v2/schemas/v1.1/server.rng deleted file mode 100644 index 07fa16daa..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/server.rng +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/servers.rng b/nova/api/openstack/v2/schemas/v1.1/servers.rng deleted file mode 100644 index 4e2bb8853..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/servers.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/servers_index.rng b/nova/api/openstack/v2/schemas/v1.1/servers_index.rng deleted file mode 100644 index 023e4b66a..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/servers_index.rng +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/version.rng b/nova/api/openstack/v2/schemas/v1.1/version.rng deleted file mode 100644 index ae76270ba..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/version.rng +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/versions.rng b/nova/api/openstack/v2/schemas/v1.1/versions.rng deleted file mode 100644 index 8b2cc7f71..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/versions.rng +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/nova/api/openstack/v2/server_metadata.py b/nova/api/openstack/v2/server_metadata.py deleted file mode 100644 index 52a90f96e..000000000 --- a/nova/api/openstack/v2/server_metadata.py +++ /dev/null @@ -1,175 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from webob import exc - -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova import compute -from nova import exception - - -class Controller(object): - """ The server metadata API controller for the Openstack API """ - - def __init__(self): - self.compute_api = compute.API() - super(Controller, self).__init__() - - def _get_metadata(self, context, server_id): - try: - server = self.compute_api.get(context, server_id) - meta = self.compute_api.get_instance_metadata(context, server) - except exception.InstanceNotFound: - msg = _('Server does not exist') - raise exc.HTTPNotFound(explanation=msg) - - meta_dict = {} - for key, value in meta.iteritems(): - meta_dict[key] = value - return meta_dict - - @wsgi.serializers(xml=common.MetadataTemplate) - def index(self, req, server_id): - """ Returns the list of metadata for a given instance """ - context = req.environ['nova.context'] - return {'metadata': self._get_metadata(context, server_id)} - - @wsgi.serializers(xml=common.MetadataTemplate) - @wsgi.deserializers(xml=common.MetadataDeserializer) - def create(self, req, server_id, body): - try: - metadata = body['metadata'] - except (KeyError, TypeError): - msg = _("Malformed request body") - raise exc.HTTPBadRequest(explanation=msg) - - context = req.environ['nova.context'] - - new_metadata = self._update_instance_metadata(context, - server_id, - metadata, - delete=False) - - return {'metadata': new_metadata} - - @wsgi.serializers(xml=common.MetaItemTemplate) - @wsgi.deserializers(xml=common.MetaItemDeserializer) - def update(self, req, server_id, id, body): - try: - meta_item = body['meta'] - except (TypeError, KeyError): - expl = _('Malformed request body') - raise exc.HTTPBadRequest(explanation=expl) - - try: - meta_value = meta_item[id] - except (AttributeError, KeyError): - expl = _('Request body and URI mismatch') - raise exc.HTTPBadRequest(explanation=expl) - - if len(meta_item) > 1: - expl = _('Request body contains too many items') - raise exc.HTTPBadRequest(explanation=expl) - - context = req.environ['nova.context'] - self._update_instance_metadata(context, - server_id, - meta_item, - delete=False) - - return {'meta': meta_item} - - @wsgi.serializers(xml=common.MetadataTemplate) - @wsgi.deserializers(xml=common.MetadataDeserializer) - def update_all(self, req, server_id, body): - try: - metadata = body['metadata'] - except (TypeError, KeyError): - expl = _('Malformed request body') - raise exc.HTTPBadRequest(explanation=expl) - - context = req.environ['nova.context'] - new_metadata = self._update_instance_metadata(context, - server_id, - metadata, - delete=True) - - return {'metadata': new_metadata} - - def _update_instance_metadata(self, context, server_id, metadata, - delete=False): - try: - server = self.compute_api.get(context, server_id) - return self.compute_api.update_instance_metadata(context, - server, - metadata, - delete) - - except exception.InstanceNotFound: - msg = _('Server does not exist') - raise exc.HTTPNotFound(explanation=msg) - - except (ValueError, AttributeError): - msg = _("Malformed request body") - raise exc.HTTPBadRequest(explanation=msg) - - except exception.QuotaError as error: - self._handle_quota_error(error) - - @wsgi.serializers(xml=common.MetaItemTemplate) - def show(self, req, server_id, id): - """ Return a single metadata item """ - context = req.environ['nova.context'] - data = self._get_metadata(context, server_id) - - try: - return {'meta': {id: data[id]}} - except KeyError: - msg = _("Metadata item was not found") - raise exc.HTTPNotFound(explanation=msg) - - @wsgi.response(204) - def delete(self, req, server_id, id): - """ Deletes an existing metadata """ - context = req.environ['nova.context'] - - metadata = self._get_metadata(context, server_id) - - try: - meta_value = metadata[id] - except KeyError: - msg = _("Metadata item was not found") - raise exc.HTTPNotFound(explanation=msg) - - try: - server = self.compute_api.get(context, server_id) - self.compute_api.delete_instance_metadata(context, server, id) - except exception.InstanceNotFound: - msg = _('Server does not exist') - raise exc.HTTPNotFound(explanation=msg) - - def _handle_quota_error(self, error): - """Reraise quota errors as api-specific http exceptions.""" - if error.code == "MetadataLimitExceeded": - raise exc.HTTPRequestEntityTooLarge(explanation=error.message, - headers={'Retry-After': 0}) - raise error - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/servers.py b/nova/api/openstack/v2/servers.py deleted file mode 100644 index c2655a73e..000000000 --- a/nova/api/openstack/v2/servers.py +++ /dev/null @@ -1,1123 +0,0 @@ -# Copyright 2010 OpenStack LLC. -# 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 os -from xml.dom import minidom - -from webob import exc -import webob - -from nova.api.openstack import common -from nova.api.openstack.v2 import ips -from nova.api.openstack.v2.views import servers as views_servers -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova.compute import instance_types -from nova import exception -from nova import flags -from nova import log as logging -from nova.rpc import common as rpc_common -from nova.scheduler import api as scheduler_api -from nova import utils - - -LOG = logging.getLogger('nova.api.openstack.v2.servers') -FLAGS = flags.FLAGS - - -class SecurityGroupsTemplateElement(xmlutil.TemplateElement): - def will_render(self, datum): - return 'security_groups' in datum - - -def make_fault(elem): - fault = xmlutil.SubTemplateElement(elem, 'fault', selector='fault') - fault.set('code') - fault.set('created') - msg = xmlutil.SubTemplateElement(fault, 'message') - msg.text = 'message' - det = xmlutil.SubTemplateElement(fault, 'details') - det.text = 'details' - - -def make_server(elem, detailed=False): - elem.set('name') - elem.set('id') - - if detailed: - elem.set('userId', 'user_id') - elem.set('tenantId', 'tenant_id') - elem.set('updated') - elem.set('created') - elem.set('hostId') - elem.set('accessIPv4') - elem.set('accessIPv6') - elem.set('status') - elem.set('progress') - - # Attach image node - image = xmlutil.SubTemplateElement(elem, 'image', selector='image') - image.set('id') - xmlutil.make_links(image, 'links') - - # Attach flavor node - flavor = xmlutil.SubTemplateElement(elem, 'flavor', selector='flavor') - flavor.set('id') - xmlutil.make_links(flavor, 'links') - - # Attach fault node - make_fault(elem) - - # Attach metadata node - elem.append(common.MetadataTemplate()) - - # Attach addresses node - elem.append(ips.AddressesTemplate()) - - # Attach security groups node - secgrps = SecurityGroupsTemplateElement('security_groups') - elem.append(secgrps) - secgrp = xmlutil.SubTemplateElement(secgrps, 'security_group', - selector='security_groups') - secgrp.set('name') - - xmlutil.make_links(elem, 'links') - - -server_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class ServerTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('server', selector='server') - make_server(root, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) - - -class MinimalServersTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('servers') - elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') - make_server(elem) - xmlutil.make_links(root, 'servers_links') - return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) - - -class ServersTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('servers') - elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') - make_server(elem, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) - - -class ServerAdminPassTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('server') - root.set('adminPass') - return xmlutil.SlaveTemplate(root, 1, nsmap=server_nsmap) - - -def FullServerTemplate(): - master = ServerTemplate() - master.attach(ServerAdminPassTemplate()) - return master - - -class CommonDeserializer(wsgi.MetadataXMLDeserializer): - """ - Common deserializer to handle xml-formatted server create - requests. - - Handles standard server attributes as well as optional metadata - and personality attributes - """ - - metadata_deserializer = common.MetadataXMLDeserializer() - - def _extract_personality(self, server_node): - """Marshal the personality attribute of a parsed request""" - node = self.find_first_child_named(server_node, "personality") - if node is not None: - personality = [] - for file_node in self.find_children_named(node, "file"): - item = {} - if file_node.hasAttribute("path"): - item["path"] = file_node.getAttribute("path") - item["contents"] = self.extract_text(file_node) - personality.append(item) - return personality - else: - return None - - def _extract_server(self, node): - """Marshal the server attribute of a parsed request""" - server = {} - server_node = self.find_first_child_named(node, 'server') - - attributes = ["name", "imageRef", "flavorRef", "adminPass", - "accessIPv4", "accessIPv6"] - for attr in attributes: - if server_node.getAttribute(attr): - server[attr] = server_node.getAttribute(attr) - - metadata_node = self.find_first_child_named(server_node, "metadata") - if metadata_node is not None: - server["metadata"] = self.extract_metadata(metadata_node) - - personality = self._extract_personality(server_node) - if personality is not None: - server["personality"] = personality - - networks = self._extract_networks(server_node) - if networks is not None: - server["networks"] = networks - - security_groups = self._extract_security_groups(server_node) - if security_groups is not None: - server["security_groups"] = security_groups - - auto_disk_config = server_node.getAttribute('auto_disk_config') - if auto_disk_config: - server['auto_disk_config'] = utils.bool_from_str(auto_disk_config) - - return server - - def _extract_networks(self, server_node): - """Marshal the networks attribute of a parsed request""" - node = self.find_first_child_named(server_node, "networks") - if node is not None: - networks = [] - for network_node in self.find_children_named(node, - "network"): - item = {} - if network_node.hasAttribute("uuid"): - item["uuid"] = network_node.getAttribute("uuid") - if network_node.hasAttribute("fixed_ip"): - item["fixed_ip"] = network_node.getAttribute("fixed_ip") - networks.append(item) - return networks - else: - return None - - def _extract_security_groups(self, server_node): - """Marshal the security_groups attribute of a parsed request""" - node = self.find_first_child_named(server_node, "security_groups") - if node is not None: - security_groups = [] - for sg_node in self.find_children_named(node, "security_group"): - item = {} - name_node = self.find_first_child_named(sg_node, "name") - if name_node: - item["name"] = self.extract_text(name_node) - security_groups.append(item) - return security_groups - else: - return None - - -class ActionDeserializer(CommonDeserializer): - """ - Deserializer to handle xml-formatted server action requests. - - Handles standard server attributes as well as optional metadata - and personality attributes - """ - - def default(self, string): - dom = minidom.parseString(string) - action_node = dom.childNodes[0] - action_name = action_node.tagName - - action_deserializer = { - 'createImage': self._action_create_image, - 'changePassword': self._action_change_password, - 'reboot': self._action_reboot, - 'rebuild': self._action_rebuild, - 'resize': self._action_resize, - 'confirmResize': self._action_confirm_resize, - 'revertResize': self._action_revert_resize, - }.get(action_name, super(ActionDeserializer, self).default) - - action_data = action_deserializer(action_node) - - return {'body': {action_name: action_data}} - - def _action_create_image(self, node): - return self._deserialize_image_action(node, ('name',)) - - def _action_change_password(self, node): - if not node.hasAttribute("adminPass"): - raise AttributeError("No adminPass was specified in request") - return {"adminPass": node.getAttribute("adminPass")} - - def _action_reboot(self, node): - if not node.hasAttribute("type"): - raise AttributeError("No reboot type was specified in request") - return {"type": node.getAttribute("type")} - - def _action_rebuild(self, node): - rebuild = {} - if node.hasAttribute("name"): - rebuild['name'] = node.getAttribute("name") - - metadata_node = self.find_first_child_named(node, "metadata") - if metadata_node is not None: - rebuild["metadata"] = self.extract_metadata(metadata_node) - - personality = self._extract_personality(node) - if personality is not None: - rebuild["personality"] = personality - - if not node.hasAttribute("imageRef"): - raise AttributeError("No imageRef was specified in request") - rebuild["imageRef"] = node.getAttribute("imageRef") - - return rebuild - - def _action_resize(self, node): - if not node.hasAttribute("flavorRef"): - raise AttributeError("No flavorRef was specified in request") - return {"flavorRef": node.getAttribute("flavorRef")} - - def _action_confirm_resize(self, node): - return None - - def _action_revert_resize(self, node): - return None - - def _deserialize_image_action(self, node, allowed_attributes): - data = {} - for attribute in allowed_attributes: - value = node.getAttribute(attribute) - if value: - data[attribute] = value - metadata_node = self.find_first_child_named(node, 'metadata') - if metadata_node is not None: - metadata = self.metadata_deserializer.extract_metadata( - metadata_node) - data['metadata'] = metadata - return data - - -class CreateDeserializer(CommonDeserializer): - """ - Deserializer to handle xml-formatted server create requests. - - Handles standard server attributes as well as optional metadata - and personality attributes - """ - - def default(self, string): - """Deserialize an xml-formatted server create request""" - dom = minidom.parseString(string) - server = self._extract_server(dom) - return {'body': {'server': server}} - - -class Controller(wsgi.Controller): - """ The Server API base controller class for the OpenStack API """ - - _view_builder_class = views_servers.ViewBuilder - - @staticmethod - def _add_location(robj): - # Just in case... - if 'server' not in robj.obj: - return robj - - link = filter(lambda l: l['rel'] == 'self', - robj.obj['server']['links']) - if link: - robj['Location'] = link[0]['href'] - - # Convenience return - return robj - - def __init__(self, **kwargs): - super(Controller, self).__init__(**kwargs) - self.compute_api = compute.API() - - @wsgi.serializers(xml=MinimalServersTemplate) - def index(self, req): - """ Returns a list of server names and ids for a given user """ - try: - servers = self._get_servers(req, is_detail=False) - except exception.Invalid as err: - raise exc.HTTPBadRequest(explanation=str(err)) - except exception.NotFound: - raise exc.HTTPNotFound() - return servers - - @wsgi.serializers(xml=ServersTemplate) - def detail(self, req): - """ Returns a list of server details for a given user """ - try: - servers = self._get_servers(req, is_detail=True) - except exception.Invalid as err: - raise exc.HTTPBadRequest(explanation=str(err)) - except exception.NotFound as err: - raise exc.HTTPNotFound() - return servers - - def _get_block_device_mapping(self, data): - """Get block_device_mapping from 'server' dictionary. - Overridden by volumes controller. - """ - return None - - def _add_instance_faults(self, ctxt, instances): - faults = self.compute_api.get_instance_faults(ctxt, instances) - if faults is not None: - for instance in instances: - faults_list = faults.get(instance['uuid'], []) - try: - instance['fault'] = faults_list[0] - except IndexError: - pass - - return instances - - def _get_servers(self, req, is_detail): - """Returns a list of servers, taking into account any search - options specified. - """ - - search_opts = {} - search_opts.update(req.str_GET) - - context = req.environ['nova.context'] - remove_invalid_options(context, search_opts, - self._get_server_search_options()) - - # Convert local_zone_only into a boolean - search_opts['local_zone_only'] = utils.bool_from_str( - search_opts.get('local_zone_only', False)) - - # If search by 'status', we need to convert it to 'vm_state' - # to pass on to child zones. - if 'status' in search_opts: - status = search_opts['status'] - state = common.vm_state_from_status(status) - if state is None: - reason = _('Invalid server status: %(status)s') % locals() - raise exception.InvalidInput(reason=reason) - search_opts['vm_state'] = state - - if 'changes-since' in search_opts: - try: - parsed = utils.parse_isotime(search_opts['changes-since']) - except ValueError: - msg = _('Invalid changes-since value') - raise exc.HTTPBadRequest(explanation=msg) - search_opts['changes-since'] = parsed - - # By default, compute's get_all() will return deleted instances. - # If an admin hasn't specified a 'deleted' search option, we need - # to filter out deleted instances by setting the filter ourselves. - # ... Unless 'changes-since' is specified, because 'changes-since' - # should return recently deleted images according to the API spec. - - if 'deleted' not in search_opts: - if 'changes-since' not in search_opts: - # No 'changes-since', so we only want non-deleted servers - search_opts['deleted'] = False - - instance_list = self.compute_api.get_all(context, - search_opts=search_opts) - - limited_list = self._limit_items(instance_list, req) - if is_detail: - self._add_instance_faults(context, limited_list) - return self._view_builder.detail(req, limited_list) - else: - return self._view_builder.index(req, limited_list) - - def _get_server(self, context, instance_uuid): - """Utility function for looking up an instance by uuid""" - try: - return self.compute_api.routing_get(context, instance_uuid) - except exception.NotFound: - raise exc.HTTPNotFound() - - def _handle_quota_error(self, error): - """ - Reraise quota errors as api-specific http exceptions - """ - - code_mappings = { - "OnsetFileLimitExceeded": - _("Personality file limit exceeded"), - "OnsetFilePathLimitExceeded": - _("Personality file path too long"), - "OnsetFileContentLimitExceeded": - _("Personality file content too long"), - - # NOTE(bcwaldon): expose the message generated below in order - # to better explain how the quota was exceeded - "InstanceLimitExceeded": error.message, - } - - expl = code_mappings.get(error.code) - if expl: - raise exc.HTTPRequestEntityTooLarge(explanation=expl, - headers={'Retry-After': 0}) - # if the original error is okay, just reraise it - raise error - - def _validate_server_name(self, value): - if not isinstance(value, basestring): - msg = _("Server name is not a string or unicode") - raise exc.HTTPBadRequest(explanation=msg) - - if value.strip() == '': - msg = _("Server name is an empty string") - raise exc.HTTPBadRequest(explanation=msg) - - def _get_injected_files(self, personality): - """ - Create a list of injected files from the personality attribute - - At this time, injected_files must be formatted as a list of - (file_path, file_content) pairs for compatibility with the - underlying compute service. - """ - injected_files = [] - - for item in personality: - try: - path = item['path'] - contents = item['contents'] - except KeyError as key: - expl = _('Bad personality format: missing %s') % key - raise exc.HTTPBadRequest(explanation=expl) - except TypeError: - expl = _('Bad personality format') - raise exc.HTTPBadRequest(explanation=expl) - try: - contents = base64.b64decode(contents) - except TypeError: - expl = _('Personality content for %s cannot be decoded') % path - raise exc.HTTPBadRequest(explanation=expl) - injected_files.append((path, contents)) - return injected_files - - def _get_requested_networks(self, requested_networks): - """ - Create a list of requested networks from the networks attribute - """ - networks = [] - for network in requested_networks: - try: - network_uuid = network['uuid'] - - if not utils.is_uuid_like(network_uuid): - msg = _("Bad networks format: network uuid is not in" - " proper format (%s)") % network_uuid - raise exc.HTTPBadRequest(explanation=msg) - - #fixed IP address is optional - #if the fixed IP address is not provided then - #it will use one of the available IP address from the network - address = network.get('fixed_ip', None) - if address is not None and not utils.is_valid_ipv4(address): - msg = _("Invalid fixed IP address (%s)") % address - raise exc.HTTPBadRequest(explanation=msg) - # check if the network id is already present in the list, - # we don't want duplicate networks to be passed - # at the boot time - for id, ip in networks: - if id == network_uuid: - expl = _("Duplicate networks (%s) are not allowed")\ - % network_uuid - raise exc.HTTPBadRequest(explanation=expl) - - networks.append((network_uuid, address)) - except KeyError as key: - expl = _('Bad network format: missing %s') % key - raise exc.HTTPBadRequest(explanation=expl) - except TypeError: - expl = _('Bad networks format') - raise exc.HTTPBadRequest(explanation=expl) - - return networks - - def _validate_user_data(self, user_data): - """Check if the user_data is encoded properly""" - if not user_data: - return - try: - user_data = base64.b64decode(user_data) - except TypeError: - expl = _('Userdata content cannot be decoded') - raise exc.HTTPBadRequest(explanation=expl) - - @wsgi.serializers(xml=ServerTemplate) - @exception.novaclient_converter - @scheduler_api.redirect_handler - def show(self, req, id): - """ Returns server details by server id """ - try: - context = req.environ['nova.context'] - instance = self.compute_api.routing_get(context, id) - self._add_instance_faults(context, [instance]) - return self._view_builder.show(req, instance) - except exception.NotFound: - raise exc.HTTPNotFound() - - @wsgi.response(202) - @wsgi.serializers(xml=FullServerTemplate) - @wsgi.deserializers(xml=CreateDeserializer) - def create(self, req, body): - """ Creates a new server for a given user """ - if not body: - raise exc.HTTPUnprocessableEntity() - - if not 'server' in body: - raise exc.HTTPUnprocessableEntity() - - body['server']['key_name'] = self._get_key_name(req, body) - - context = req.environ['nova.context'] - server_dict = body['server'] - password = self._get_server_admin_password(server_dict) - - if not 'name' in server_dict: - msg = _("Server name is not defined") - raise exc.HTTPBadRequest(explanation=msg) - - name = server_dict['name'] - self._validate_server_name(name) - name = name.strip() - - image_href = self._image_ref_from_req_data(body) - - # If the image href was generated by nova api, strip image_href - # down to an id and use the default glance connection params - if str(image_href).startswith(req.application_url): - image_href = image_href.split('/').pop() - - personality = server_dict.get('personality') - config_drive = server_dict.get('config_drive') - - injected_files = [] - if personality: - injected_files = self._get_injected_files(personality) - - sg_names = [] - security_groups = server_dict.get('security_groups') - if security_groups is not None: - sg_names = [sg['name'] for sg in security_groups if sg.get('name')] - if not sg_names: - sg_names.append('default') - - sg_names = list(set(sg_names)) - - requested_networks = server_dict.get('networks') - if requested_networks is not None: - requested_networks = self._get_requested_networks( - requested_networks) - - try: - flavor_id = self._flavor_id_from_req_data(body) - except ValueError as error: - msg = _("Invalid flavorRef provided.") - raise exc.HTTPBadRequest(explanation=msg) - - zone_blob = server_dict.get('blob') - - # optional openstack extensions: - key_name = server_dict.get('key_name') - user_data = server_dict.get('user_data') - self._validate_user_data(user_data) - - availability_zone = server_dict.get('availability_zone') - name = server_dict['name'] - self._validate_server_name(name) - name = name.strip() - - block_device_mapping = self._get_block_device_mapping(server_dict) - - # Only allow admins to specify their own reservation_ids - # This is really meant to allow zones to work. - reservation_id = server_dict.get('reservation_id') - if all([reservation_id is not None, - reservation_id != '', - not context.is_admin]): - reservation_id = None - - ret_resv_id = server_dict.get('return_reservation_id', False) - - min_count = server_dict.get('min_count') - max_count = server_dict.get('max_count') - # min_count and max_count are optional. If they exist, they come - # in as strings. We want to default 'min_count' to 1, and default - # 'max_count' to be 'min_count'. - min_count = int(min_count) if min_count else 1 - max_count = int(max_count) if max_count else min_count - if min_count > max_count: - min_count = max_count - - auto_disk_config = server_dict.get('auto_disk_config') - - try: - inst_type = \ - instance_types.get_instance_type_by_flavor_id(flavor_id) - - (instances, resv_id) = self.compute_api.create(context, - inst_type, - image_href, - display_name=name, - display_description=name, - key_name=key_name, - metadata=server_dict.get('metadata', {}), - access_ip_v4=server_dict.get('accessIPv4'), - access_ip_v6=server_dict.get('accessIPv6'), - injected_files=injected_files, - admin_password=password, - zone_blob=zone_blob, - reservation_id=reservation_id, - min_count=min_count, - max_count=max_count, - requested_networks=requested_networks, - security_group=sg_names, - user_data=user_data, - availability_zone=availability_zone, - config_drive=config_drive, - block_device_mapping=block_device_mapping, - auto_disk_config=auto_disk_config) - except exception.QuotaError as error: - self._handle_quota_error(error) - except exception.InstanceTypeMemoryTooSmall as error: - raise exc.HTTPBadRequest(explanation=unicode(error)) - except exception.InstanceTypeDiskTooSmall as error: - raise exc.HTTPBadRequest(explanation=unicode(error)) - except exception.ImageNotFound as error: - msg = _("Can not find requested image") - raise exc.HTTPBadRequest(explanation=msg) - except exception.FlavorNotFound as error: - msg = _("Invalid flavorRef provided.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.KeypairNotFound as error: - msg = _("Invalid key_name provided.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.SecurityGroupNotFound as error: - raise exc.HTTPBadRequest(explanation=unicode(error)) - except rpc_common.RemoteError as err: - msg = "%(err_type)s: %(err_msg)s" % \ - {'err_type': err.exc_type, 'err_msg': err.value} - raise exc.HTTPBadRequest(explanation=msg) - # Let the caller deal with unhandled exceptions. - - # If the caller wanted a reservation_id, return it - if ret_resv_id: - return {'reservation_id': resv_id} - - server = self._view_builder.create(req, instances[0]) - - if '_is_precooked' in server['server'].keys(): - del server['server']['_is_precooked'] - else: - server['server']['adminPass'] = password - - robj = wsgi.ResponseObject(server) - - return self._add_location(robj) - - def _delete(self, context, id): - instance = self._get_server(context, id) - if FLAGS.reclaim_instance_interval: - self.compute_api.soft_delete(context, instance) - else: - self.compute_api.delete(context, instance) - - @wsgi.serializers(xml=ServerTemplate) - @scheduler_api.redirect_handler - def update(self, req, id, body): - """Update server then pass on to version-specific controller""" - if len(req.body) == 0: - raise exc.HTTPUnprocessableEntity() - - if not body: - raise exc.HTTPUnprocessableEntity() - - ctxt = req.environ['nova.context'] - update_dict = {} - - if 'name' in body['server']: - name = body['server']['name'] - self._validate_server_name(name) - update_dict['display_name'] = name.strip() - - if 'accessIPv4' in body['server']: - access_ipv4 = body['server']['accessIPv4'] - update_dict['access_ip_v4'] = access_ipv4.strip() - - if 'accessIPv6' in body['server']: - access_ipv6 = body['server']['accessIPv6'] - update_dict['access_ip_v6'] = access_ipv6.strip() - - if 'auto_disk_config' in body['server']: - auto_disk_config = utils.bool_from_str( - body['server']['auto_disk_config']) - update_dict['auto_disk_config'] = auto_disk_config - - instance = self.compute_api.routing_get(ctxt, id) - - try: - self.compute_api.update(ctxt, instance, **update_dict) - except exception.NotFound: - raise exc.HTTPNotFound() - - instance.update(update_dict) - - self._add_instance_faults(ctxt, [instance]) - return self._view_builder.show(req, instance) - - @wsgi.response(202) - @wsgi.serializers(xml=FullServerTemplate) - @wsgi.deserializers(xml=ActionDeserializer) - @exception.novaclient_converter - @scheduler_api.redirect_handler - def action(self, req, id, body): - """Multi-purpose method used to take actions on a server""" - _actions = { - 'changePassword': self._action_change_password, - 'reboot': self._action_reboot, - 'resize': self._action_resize, - 'confirmResize': self._action_confirm_resize, - 'revertResize': self._action_revert_resize, - 'rebuild': self._action_rebuild, - 'createImage': self._action_create_image, - } - - for key in body: - if key in _actions: - return _actions[key](body, req, id) - else: - msg = _("There is no such server action: %s") % (key,) - raise exc.HTTPBadRequest(explanation=msg) - - msg = _("Invalid request body") - raise exc.HTTPBadRequest(explanation=msg) - - def _action_confirm_resize(self, input_dict, req, id): - context = req.environ['nova.context'] - instance = self._get_server(context, id) - try: - self.compute_api.confirm_resize(context, instance) - except exception.MigrationNotFound: - msg = _("Instance has not been resized.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'confirmResize') - except Exception, e: - LOG.exception(_("Error in confirm-resize %s"), e) - raise exc.HTTPBadRequest() - return exc.HTTPNoContent() - - def _action_revert_resize(self, input_dict, req, id): - context = req.environ['nova.context'] - instance = self._get_server(context, id) - try: - self.compute_api.revert_resize(context, instance) - except exception.MigrationNotFound: - msg = _("Instance has not been resized.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'revertResize') - except Exception, e: - LOG.exception(_("Error in revert-resize %s"), e) - raise exc.HTTPBadRequest() - return webob.Response(status_int=202) - - def _action_reboot(self, input_dict, req, id): - if 'reboot' in input_dict and 'type' in input_dict['reboot']: - valid_reboot_types = ['HARD', 'SOFT'] - reboot_type = input_dict['reboot']['type'].upper() - if not valid_reboot_types.count(reboot_type): - msg = _("Argument 'type' for reboot is not HARD or SOFT") - LOG.exception(msg) - raise exc.HTTPBadRequest(explanation=msg) - else: - msg = _("Missing argument 'type' for reboot") - LOG.exception(msg) - raise exc.HTTPBadRequest(explanation=msg) - - context = req.environ['nova.context'] - instance = self._get_server(context, id) - - try: - self.compute_api.reboot(context, instance, reboot_type) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'reboot') - except Exception, e: - LOG.exception(_("Error in reboot %s"), e) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - def _resize(self, req, instance_id, flavor_id): - """Begin the resize process with given instance/flavor.""" - context = req.environ["nova.context"] - instance = self._get_server(context, instance_id) - - try: - self.compute_api.resize(context, instance, flavor_id) - except exception.FlavorNotFound: - msg = _("Unable to locate requested flavor.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.CannotResizeToSameSize: - msg = _("Resize requires a change in size.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'resize') - - return webob.Response(status_int=202) - - @wsgi.response(204) - @exception.novaclient_converter - @scheduler_api.redirect_handler - def delete(self, req, id): - """ Destroys a server """ - try: - self._delete(req.environ['nova.context'], id) - except exception.NotFound: - raise exc.HTTPNotFound() - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'delete') - - def _get_key_name(self, req, body): - if 'server' in body: - try: - return body['server'].get('key_name') - except AttributeError: - msg = _("Malformed server entity") - raise exc.HTTPBadRequest(explanation=msg) - - def _image_ref_from_req_data(self, data): - try: - return data['server']['imageRef'] - except (TypeError, KeyError): - msg = _("Missing imageRef attribute") - raise exc.HTTPBadRequest(explanation=msg) - - def _flavor_id_from_req_data(self, data): - try: - flavor_ref = data['server']['flavorRef'] - except (TypeError, KeyError): - msg = _("Missing flavorRef attribute") - raise exc.HTTPBadRequest(explanation=msg) - - return common.get_id_from_href(flavor_ref) - - def _action_change_password(self, input_dict, req, id): - context = req.environ['nova.context'] - if (not 'changePassword' in input_dict - or not 'adminPass' in input_dict['changePassword']): - msg = _("No adminPass was specified") - raise exc.HTTPBadRequest(explanation=msg) - password = input_dict['changePassword']['adminPass'] - if not isinstance(password, basestring) or password == '': - msg = _("Invalid adminPass") - raise exc.HTTPBadRequest(explanation=msg) - server = self._get_server(context, id) - self.compute_api.set_admin_password(context, server, password) - return webob.Response(status_int=202) - - def _limit_items(self, items, req): - return common.limited_by_marker(items, req) - - def _validate_metadata(self, metadata): - """Ensure that we can work with the metadata given.""" - try: - metadata.iteritems() - except AttributeError as ex: - msg = _("Unable to parse metadata key/value pairs.") - LOG.debug(msg) - raise exc.HTTPBadRequest(explanation=msg) - - def _action_resize(self, input_dict, req, id): - """ Resizes a given instance to the flavor size requested """ - try: - flavor_ref = input_dict["resize"]["flavorRef"] - if not flavor_ref: - msg = _("Resize request has invalid 'flavorRef' attribute.") - raise exc.HTTPBadRequest(explanation=msg) - except (KeyError, TypeError): - msg = _("Resize requests require 'flavorRef' attribute.") - raise exc.HTTPBadRequest(explanation=msg) - - return self._resize(req, id, flavor_ref) - - def _action_rebuild(self, info, request, instance_id): - """Rebuild an instance with the given attributes""" - try: - body = info['rebuild'] - except (KeyError, TypeError): - raise exc.HTTPBadRequest(_("Invalid request body")) - - try: - image_href = body["imageRef"] - except (KeyError, TypeError): - msg = _("Could not parse imageRef from request.") - raise exc.HTTPBadRequest(explanation=msg) - - try: - password = body['adminPass'] - except (KeyError, TypeError): - password = utils.generate_password(FLAGS.password_length) - - context = request.environ['nova.context'] - instance = self._get_server(context, instance_id) - - attr_map = { - 'personality': 'files_to_inject', - 'name': 'display_name', - 'accessIPv4': 'access_ip_v4', - 'accessIPv6': 'access_ip_v6', - 'metadata': 'metadata', - } - - kwargs = {} - - for request_attribute, instance_attribute in attr_map.items(): - try: - kwargs[instance_attribute] = body[request_attribute] - except (KeyError, TypeError): - pass - - self._validate_metadata(kwargs.get('metadata', {})) - - if 'files_to_inject' in kwargs: - personality = kwargs['files_to_inject'] - kwargs['files_to_inject'] = self._get_injected_files(personality) - - try: - self.compute_api.rebuild(context, - instance, - image_href, - password, - **kwargs) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'rebuild') - except exception.InstanceNotFound: - msg = _("Instance could not be found") - raise exc.HTTPNotFound(explanation=msg) - - instance = self._get_server(context, instance_id) - - self._add_instance_faults(context, [instance]) - view = self._view_builder.show(request, instance) - - # Add on the adminPass attribute since the view doesn't do it - view['server']['adminPass'] = password - - robj = wsgi.ResponseObject(view) - return self._add_location(robj) - - @common.check_snapshots_enabled - def _action_create_image(self, input_dict, req, instance_id): - """Snapshot a server instance.""" - context = req.environ['nova.context'] - entity = input_dict.get("createImage", {}) - - try: - image_name = entity["name"] - - except KeyError: - msg = _("createImage entity requires name attribute") - raise exc.HTTPBadRequest(explanation=msg) - - except TypeError: - msg = _("Malformed createImage entity") - raise exc.HTTPBadRequest(explanation=msg) - - # preserve link to server in image properties - server_ref = os.path.join(req.application_url, 'servers', instance_id) - props = {'instance_ref': server_ref} - - metadata = entity.get('metadata', {}) - common.check_img_metadata_quota_limit(context, metadata) - try: - props.update(metadata) - except ValueError: - msg = _("Invalid metadata") - raise exc.HTTPBadRequest(explanation=msg) - - instance = self._get_server(context, instance_id) - - try: - image = self.compute_api.snapshot(context, - instance, - image_name, - extra_properties=props) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'createImage') - - # build location of newly-created image entity - image_id = str(image['id']) - image_ref = os.path.join(req.application_url, - context.project_id, - 'images', - image_id) - - resp = webob.Response(status_int=202) - resp.headers['Location'] = image_ref - return resp - - def _get_server_admin_password(self, server): - """ Determine the admin password for a server on creation """ - password = server.get('adminPass') - - if password is None: - return utils.generate_password(FLAGS.password_length) - if not isinstance(password, basestring) or password == '': - msg = _("Invalid adminPass") - raise exc.HTTPBadRequest(explanation=msg) - return password - - def _get_server_search_options(self): - """Return server search options allowed by non-admin""" - return ('reservation_id', 'name', 'local_zone_only', - 'status', 'image', 'flavor', 'changes-since') - - -def create_resource(): - return wsgi.Resource(Controller()) - - -def remove_invalid_options(context, search_options, allowed_search_options): - """Remove search options that are not valid for non-admin API/context""" - if FLAGS.allow_admin_api and context.is_admin: - # Allow all options - return - # Otherwise, strip out all unknown options - unknown_options = [opt for opt in search_options - if opt not in allowed_search_options] - unk_opt_str = ", ".join(unknown_options) - log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals() - LOG.debug(log_msg) - for opt in unknown_options: - search_options.pop(opt, None) diff --git a/nova/api/openstack/v2/urlmap.py b/nova/api/openstack/v2/urlmap.py deleted file mode 100644 index bae69198e..000000000 --- a/nova/api/openstack/v2/urlmap.py +++ /dev/null @@ -1,297 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import paste.urlmap -import re -import urllib2 - -from nova import log as logging -from nova.api.openstack import wsgi - - -_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"' -_option_header_piece_re = re.compile(r';\s*([^\s;=]+|%s)\s*' - r'(?:=\s*([^;]+|%s))?\s*' % - (_quoted_string_re, _quoted_string_re)) - -LOG = logging.getLogger('nova.api.openstack.v2.map') - - -def unquote_header_value(value): - """Unquotes a header value. - This does not use the real unquoting but what browsers are actually - using for quoting. - - :param value: the header value to unquote. - """ - if value and value[0] == value[-1] == '"': - # this is not the real unquoting, but fixing this so that the - # RFC is met will result in bugs with internet explorer and - # probably some other browsers as well. IE for example is - # uploading files with "C:\foo\bar.txt" as filename - value = value[1:-1] - return value - - -def parse_list_header(value): - """Parse lists as described by RFC 2068 Section 2. - - In particular, parse comma-separated lists where the elements of - the list may include quoted-strings. A quoted-string could - contain a comma. A non-quoted string could have quotes in the - middle. Quotes are removed automatically after parsing. - - The return value is a standard :class:`list`: - - >>> parse_list_header('token, "quoted value"') - ['token', 'quoted value'] - - :param value: a string with a list header. - :return: :class:`list` - """ - result = [] - for item in urllib2.parse_http_list(value): - if item[:1] == item[-1:] == '"': - item = unquote_header_value(item[1:-1]) - result.append(item) - return result - - -def parse_options_header(value): - """Parse a ``Content-Type`` like header into a tuple with the content - type and the options: - - >>> parse_options_header('Content-Type: text/html; mimetype=text/html') - ('Content-Type:', {'mimetype': 'text/html'}) - - :param value: the header to parse. - :return: (str, options) - """ - def _tokenize(string): - for match in _option_header_piece_re.finditer(string): - key, value = match.groups() - key = unquote_header_value(key) - if value is not None: - value = unquote_header_value(value) - yield key, value - - if not value: - return '', {} - - parts = _tokenize(';' + value) - name = parts.next()[0] - extra = dict(parts) - return name, extra - - -class Accept(object): - def __init__(self, value): - self._content_types = [parse_options_header(v) for v in - parse_list_header(value)] - - def best_match(self, supported_content_types): - # FIXME: Should we have a more sophisticated matching algorithm that - # takes into account the version as well? - best_quality = -1 - best_content_type = None - best_params = {} - best_match = '*/*' - - for content_type in supported_content_types: - for content_mask, params in self._content_types: - try: - quality = float(params.get('q', 1)) - except ValueError: - continue - - if quality < best_quality: - continue - elif best_quality == quality: - if best_match.count('*') <= content_mask.count('*'): - continue - - if self._match_mask(content_mask, content_type): - best_quality = quality - best_content_type = content_type - best_params = params - best_match = content_mask - - return best_content_type, best_params - - def content_type_params(self, best_content_type): - """Find parameters in Accept header for given content type.""" - for content_type, params in self._content_types: - if best_content_type == content_type: - return params - - return {} - - def _match_mask(self, mask, content_type): - if '*' not in mask: - return content_type == mask - if mask == '*/*': - return True - mask_major = mask[:-2] - content_type_major = content_type.split('/', 1)[0] - return content_type_major == mask_major - - -def urlmap_factory(loader, global_conf, **local_conf): - if 'not_found_app' in local_conf: - not_found_app = local_conf.pop('not_found_app') - else: - not_found_app = global_conf.get('not_found_app') - if not_found_app: - not_found_app = loader.get_app(not_found_app, global_conf=global_conf) - urlmap = URLMap(not_found_app=not_found_app) - for path, app_name in local_conf.items(): - path = paste.urlmap.parse_path_expression(path) - app = loader.get_app(app_name, global_conf=global_conf) - urlmap[path] = app - return urlmap - - -class URLMap(paste.urlmap.URLMap): - def _match(self, host, port, path_info): - """Find longest match for a given URL path.""" - for (domain, app_url), app in self.applications: - if domain and domain != host and domain != host + ':' + port: - continue - if (path_info == app_url - or path_info.startswith(app_url + '/')): - return app, app_url - - return None, None - - def _set_script_name(self, app, app_url): - def wrap(environ, start_response): - environ['SCRIPT_NAME'] += app_url - return app(environ, start_response) - - return wrap - - def _munge_path(self, app, path_info, app_url): - def wrap(environ, start_response): - environ['SCRIPT_NAME'] += app_url - environ['PATH_INFO'] = path_info[len(app_url):] - return app(environ, start_response) - - return wrap - - def _path_strategy(self, host, port, path_info): - """Check path suffix for MIME type and path prefix for API version.""" - mime_type = app = app_url = None - - parts = path_info.rsplit('.', 1) - if len(parts) > 1: - possible_type = 'application/' + parts[1] - if possible_type in wsgi.SUPPORTED_CONTENT_TYPES: - mime_type = possible_type - - parts = path_info.split('/') - if len(parts) > 1: - possible_app, possible_app_url = self._match(host, port, path_info) - # Don't use prefix if it ends up matching default - if possible_app and possible_app_url: - app_url = possible_app_url - app = self._munge_path(possible_app, path_info, app_url) - - return mime_type, app, app_url - - def _content_type_strategy(self, host, port, environ): - """Check Content-Type header for API version.""" - app = None - params = parse_options_header(environ.get('CONTENT_TYPE', ''))[1] - if 'version' in params: - app, app_url = self._match(host, port, '/v' + params['version']) - if app: - app = self._set_script_name(app, app_url) - - return app - - def _accept_strategy(self, host, port, environ, supported_content_types): - """Check Accept header for best matching MIME type and API version.""" - accept = Accept(environ.get('HTTP_ACCEPT', '')) - - app = None - - # Find the best match in the Accept header - mime_type, params = accept.best_match(supported_content_types) - if 'version' in params: - app, app_url = self._match(host, port, '/v' + params['version']) - if app: - app = self._set_script_name(app, app_url) - - return mime_type, app - - def __call__(self, environ, start_response): - host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower() - if ':' in host: - host, port = host.split(':', 1) - else: - if environ['wsgi.url_scheme'] == 'http': - port = '80' - else: - port = '443' - - path_info = environ['PATH_INFO'] - path_info = self.normalize_url(path_info, False)[1] - - # The MIME type for the response is determined in one of two ways: - # 1) URL path suffix (eg /servers/detail.json) - # 2) Accept header (eg application/json;q=0.8, application/xml;q=0.2) - - # The API version is determined in one of three ways: - # 1) URL path prefix (eg /v1.1/tenant/servers/detail) - # 2) Content-Type header (eg application/json;version=1.1) - # 3) Accept header (eg application/json;q=0.8;version=1.1) - - supported_content_types = list(wsgi.SUPPORTED_CONTENT_TYPES) - - mime_type, app, app_url = self._path_strategy(host, port, path_info) - - # Accept application/atom+xml for the index query of each API - # version mount point as well as the root index - if (app_url and app_url + '/' == path_info) or path_info == '/': - supported_content_types.append('application/atom+xml') - - if not app: - app = self._content_type_strategy(host, port, environ) - - if not mime_type or not app: - possible_mime_type, possible_app = self._accept_strategy( - host, port, environ, supported_content_types) - if possible_mime_type and not mime_type: - mime_type = possible_mime_type - if possible_app and not app: - app = possible_app - - if not mime_type: - mime_type = 'application/json' - - if not app: - # Didn't match a particular version, probably matches default - app, app_url = self._match(host, port, path_info) - if app: - app = self._munge_path(app, path_info, app_url) - - if app: - environ['nova.best_content_type'] = mime_type - return app(environ, start_response) - - environ['paste.urlmap_object'] = self - return self.not_found_application(environ, start_response) diff --git a/nova/api/openstack/v2/versions.py b/nova/api/openstack/v2/versions.py deleted file mode 100644 index 45b84f7c2..000000000 --- a/nova/api/openstack/v2/versions.py +++ /dev/null @@ -1,236 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from datetime import datetime - -from lxml import etree - -from nova.api.openstack.v2.views import versions as views_versions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil - - -VERSIONS = { - "v2.0": { - "id": "v2.0", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf", - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl", - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.compute+xml;version=2", - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute+json;version=2", - } - ], - } -} - - -class MediaTypesTemplateElement(xmlutil.TemplateElement): - def will_render(self, datum): - return 'media-types' in datum - - -def make_version(elem): - elem.set('id') - elem.set('status') - elem.set('updated') - - mts = MediaTypesTemplateElement('media-types') - elem.append(mts) - - mt = xmlutil.SubTemplateElement(mts, 'media-type', selector='media-types') - mt.set('base') - mt.set('type') - - xmlutil.make_links(elem, 'links') - - -version_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class VersionTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('version', selector='version') - make_version(root) - return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) - - -class VersionsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('versions') - elem = xmlutil.SubTemplateElement(root, 'version', selector='versions') - make_version(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) - - -class ChoicesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('choices') - elem = xmlutil.SubTemplateElement(root, 'version', selector='choices') - make_version(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) - - -class AtomSerializer(wsgi.XMLDictSerializer): - - NSMAP = {None: xmlutil.XMLNS_ATOM} - - def __init__(self, metadata=None, xmlns=None): - self.metadata = metadata or {} - if not xmlns: - self.xmlns = wsgi.XMLNS_ATOM - else: - self.xmlns = xmlns - - def _get_most_recent_update(self, versions): - recent = None - for version in versions: - updated = datetime.strptime(version['updated'], - '%Y-%m-%dT%H:%M:%SZ') - if not recent: - recent = updated - elif updated > recent: - recent = updated - - return recent.strftime('%Y-%m-%dT%H:%M:%SZ') - - def _get_base_url(self, link_href): - # Make sure no trailing / - link_href = link_href.rstrip('/') - return link_href.rsplit('/', 1)[0] + '/' - - def _create_feed(self, versions, feed_title, feed_id): - feed = etree.Element('feed', nsmap=self.NSMAP) - title = etree.SubElement(feed, 'title') - title.set('type', 'text') - title.text = feed_title - - # Set this updated to the most recently updated version - recent = self._get_most_recent_update(versions) - etree.SubElement(feed, 'updated').text = recent - - etree.SubElement(feed, 'id').text = feed_id - - link = etree.SubElement(feed, 'link') - link.set('rel', 'self') - link.set('href', feed_id) - - author = etree.SubElement(feed, 'author') - etree.SubElement(author, 'name').text = 'Rackspace' - etree.SubElement(author, 'uri').text = 'http://www.rackspace.com/' - - for version in versions: - feed.append(self._create_version_entry(version)) - - return feed - - def _create_version_entry(self, version): - entry = etree.Element('entry') - etree.SubElement(entry, 'id').text = version['links'][0]['href'] - title = etree.SubElement(entry, 'title') - title.set('type', 'text') - title.text = 'Version %s' % version['id'] - etree.SubElement(entry, 'updated').text = version['updated'] - - for link in version['links']: - link_elem = etree.SubElement(entry, 'link') - link_elem.set('rel', link['rel']) - link_elem.set('href', link['href']) - if 'type' in link: - link_elem.set('type', link['type']) - - content = etree.SubElement(entry, 'content') - content.set('type', 'text') - content.text = 'Version %s %s (%s)' % (version['id'], - version['status'], - version['updated']) - return entry - - -class VersionsAtomSerializer(AtomSerializer): - def default(self, data): - versions = data['versions'] - feed_id = self._get_base_url(versions[0]['links'][0]['href']) - feed = self._create_feed(versions, 'Available API Versions', feed_id) - return self._to_xml(feed) - - -class VersionAtomSerializer(AtomSerializer): - def default(self, data): - version = data['version'] - feed_id = version['links'][0]['href'] - feed = self._create_feed([version], 'About This Version', feed_id) - return self._to_xml(feed) - - -class Versions(wsgi.Resource): - def __init__(self): - super(Versions, self).__init__(None) - - @wsgi.serializers(xml=VersionsTemplate, - atom=VersionsAtomSerializer) - def index(self, req): - """Return all versions.""" - builder = views_versions.get_view_builder(req) - return builder.build_versions(VERSIONS) - - @wsgi.serializers(xml=ChoicesTemplate) - @wsgi.response(300) - def multi(self, req): - """Return multiple choices.""" - builder = views_versions.get_view_builder(req) - return builder.build_choices(VERSIONS, req) - - def get_action_args(self, request_environment): - """Parse dictionary created by routes library.""" - args = {} - if request_environment['PATH_INFO'] == '/': - args['action'] = 'index' - else: - args['action'] = 'multi' - - return args - - -class VersionV2(object): - @wsgi.serializers(xml=VersionTemplate, - atom=VersionAtomSerializer) - def show(self, req): - builder = views_versions.get_view_builder(req) - return builder.build_version(VERSIONS['v2.0']) - - -def create_resource(): - return wsgi.Resource(VersionV2()) diff --git a/nova/api/openstack/v2/views/__init__.py b/nova/api/openstack/v2/views/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/api/openstack/v2/views/addresses.py b/nova/api/openstack/v2/views/addresses.py deleted file mode 100644 index 6f518b11a..000000000 --- a/nova/api/openstack/v2/views/addresses.py +++ /dev/null @@ -1,52 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import itertools - -from nova.api.openstack import common -from nova import flags -from nova import log as logging - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack.v2.views.addresses') - - -class ViewBuilder(common.ViewBuilder): - """Models server addresses as a dictionary.""" - - _collection_name = "addresses" - - def basic(self, ip): - """Return a dictionary describing an IP address.""" - return { - "version": ip["version"], - "addr": ip["addr"], - } - - def show(self, network, label): - """Returns a dictionary describing a network.""" - all_ips = itertools.chain(network["ips"], network["floating_ips"]) - return {label: [self.basic(ip) for ip in all_ips]} - - def index(self, networks): - """Return a dictionary describing a list of networks.""" - addresses = {} - for label, network in networks.items(): - network_dict = self.show(network, label) - addresses[label] = network_dict[label] - return dict(addresses=addresses) diff --git a/nova/api/openstack/v2/views/flavors.py b/nova/api/openstack/v2/views/flavors.py deleted file mode 100644 index 64284e406..000000000 --- a/nova/api/openstack/v2/views/flavors.py +++ /dev/null @@ -1,62 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from nova.api.openstack import common - - -class ViewBuilder(common.ViewBuilder): - - _collection_name = "flavors" - - def basic(self, request, flavor): - return { - "flavor": { - "id": flavor["flavorid"], - "name": flavor["name"], - "links": self._get_links(request, flavor["flavorid"]), - }, - } - - def show(self, request, flavor): - return { - "flavor": { - "id": flavor["flavorid"], - "name": flavor["name"], - "ram": flavor["memory_mb"], - "disk": flavor["local_gb"], - "vcpus": flavor.get("vcpus") or "", - "swap": flavor.get("swap") or "", - "rxtx_factor": flavor.get("rxtx_factor") or "", - "links": self._get_links(request, flavor["flavorid"]), - }, - } - - def index(self, request, flavors): - """Return the 'index' view of flavors.""" - def _get_flavors(request, flavors): - for _, flavor in flavors.iteritems(): - yield self.basic(request, flavor)["flavor"] - - return dict(flavors=list(_get_flavors(request, flavors))) - - def detail(self, request, flavors): - """Return the 'detail' view of flavors.""" - def _get_flavors(request, flavors): - for _, flavor in flavors.iteritems(): - yield self.show(request, flavor)["flavor"] - - return dict(flavors=list(_get_flavors(request, flavors))) diff --git a/nova/api/openstack/v2/views/images.py b/nova/api/openstack/v2/views/images.py deleted file mode 100644 index c4cfe8031..000000000 --- a/nova/api/openstack/v2/views/images.py +++ /dev/null @@ -1,139 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os.path - -from nova.api.openstack import common -from nova import utils - - -class ViewBuilder(common.ViewBuilder): - - _collection_name = "images" - - def basic(self, request, image): - """Return a dictionary with basic image attributes.""" - return { - "image": { - "id": image.get("id"), - "name": image.get("name"), - "links": self._get_links(request, image["id"]), - }, - } - - def show(self, request, image): - """Return a dictionary with image details.""" - image_dict = { - "id": image.get("id"), - "name": image.get("name"), - "minRam": int(image.get("min_ram") or 0), - "minDisk": int(image.get("min_disk") or 0), - "metadata": image.get("properties", {}), - "created": self._format_date(image.get("created_at")), - "updated": self._format_date(image.get("updated_at")), - "status": self._get_status(image), - "progress": self._get_progress(image), - "links": self._get_links(request, image["id"]), - } - - server_ref = image.get("properties", {}).get("instance_ref") - - if server_ref is not None: - image_dict["server"] = { - "id": common.get_id_from_href(server_ref), - "links": [{ - "rel": "self", - "href": server_ref, - }, - { - "rel": "bookmark", - "href": common.remove_version_from_href(server_ref), - }], - } - - return dict(image=image_dict) - - def detail(self, request, images): - """Show a list of images with details.""" - list_func = self.show - return self._list_view(list_func, request, images) - - def index(self, request, images): - """Show a list of images with basic attributes.""" - list_func = self.basic - return self._list_view(list_func, request, images) - - def _list_view(self, list_func, request, images): - """Provide a view for a list of images.""" - image_list = [list_func(request, image)["image"] for image in images] - images_links = self._get_collection_links(request, images) - images_dict = dict(images=image_list) - - if images_links: - images_dict["images_links"] = images_links - - return images_dict - - def _get_links(self, request, identifier): - """Return a list of links for this image.""" - return [{ - "rel": "self", - "href": self._get_href_link(request, identifier), - }, - { - "rel": "bookmark", - "href": self._get_bookmark_link(request, identifier), - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": self._get_alternate_link(request, identifier), - }] - - def _get_alternate_link(self, request, identifier): - """Create an alternate link for a specific flavor id.""" - glance_url = utils.generate_glance_url() - return os.path.join(glance_url, - request.environ["nova.context"].project_id, - self._collection_name, - str(identifier)) - - @staticmethod - def _format_date(date_string): - """Return standard format for given date.""" - if date_string is not None: - return date_string.strftime('%Y-%m-%dT%H:%M:%SZ') - - @staticmethod - def _get_status(image): - """Update the status field to standardize format.""" - return { - 'active': 'ACTIVE', - 'queued': 'SAVING', - 'saving': 'SAVING', - 'deleted': 'DELETED', - 'pending_delete': 'DELETED', - 'killed': 'ERROR', - }.get(image.get("status"), 'UNKNOWN') - - @staticmethod - def _get_progress(image): - return { - "queued": 25, - "saving": 50, - "active": 100, - }.get(image.get("status"), 0) diff --git a/nova/api/openstack/v2/views/limits.py b/nova/api/openstack/v2/views/limits.py deleted file mode 100644 index cff6781be..000000000 --- a/nova/api/openstack/v2/views/limits.py +++ /dev/null @@ -1,96 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime - -from nova import utils - - -class ViewBuilder(object): - """Openstack API base limits view builder.""" - - def build(self, rate_limits, absolute_limits): - rate_limits = self._build_rate_limits(rate_limits) - absolute_limits = self._build_absolute_limits(absolute_limits) - - output = { - "limits": { - "rate": rate_limits, - "absolute": absolute_limits, - }, - } - - return output - - def _build_absolute_limits(self, absolute_limits): - """Builder for absolute limits - - absolute_limits should be given as a dict of limits. - For example: {"ram": 512, "gigabytes": 1024}. - - """ - limit_names = { - "ram": ["maxTotalRAMSize"], - "instances": ["maxTotalInstances"], - "cores": ["maxTotalCores"], - "metadata_items": ["maxServerMeta", "maxImageMeta"], - "injected_files": ["maxPersonality"], - "injected_file_content_bytes": ["maxPersonalitySize"], - } - limits = {} - for name, value in absolute_limits.iteritems(): - if name in limit_names and value is not None: - for name in limit_names[name]: - limits[name] = value - return limits - - def _build_rate_limits(self, rate_limits): - limits = [] - for rate_limit in rate_limits: - _rate_limit_key = None - _rate_limit = self._build_rate_limit(rate_limit) - - # check for existing key - for limit in limits: - if limit["uri"] == rate_limit["URI"] and \ - limit["regex"] == rate_limit["regex"]: - _rate_limit_key = limit - break - - # ensure we have a key if we didn't find one - if not _rate_limit_key: - _rate_limit_key = { - "uri": rate_limit["URI"], - "regex": rate_limit["regex"], - "limit": [], - } - limits.append(_rate_limit_key) - - _rate_limit_key["limit"].append(_rate_limit) - - return limits - - def _build_rate_limit(self, rate_limit): - next_avail = \ - datetime.datetime.utcfromtimestamp(rate_limit["resetTime"]) - return { - "verb": rate_limit["verb"], - "value": rate_limit["value"], - "remaining": int(rate_limit["remaining"]), - "unit": rate_limit["unit"], - "next-available": utils.isotime(at=next_avail), - } diff --git a/nova/api/openstack/v2/views/servers.py b/nova/api/openstack/v2/views/servers.py deleted file mode 100644 index 859bd48ab..000000000 --- a/nova/api/openstack/v2/views/servers.py +++ /dev/null @@ -1,193 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# 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 hashlib - -from nova.api.openstack import common -from nova.api.openstack.v2.views import addresses as views_addresses -from nova.api.openstack.v2.views import flavors as views_flavors -from nova.api.openstack.v2.views import images as views_images -from nova import log as logging -from nova import utils - - -LOG = logging.getLogger('nova.api.openstack.v2.views.servers') - - -class ViewBuilder(common.ViewBuilder): - """Model a server API response as a python dictionary.""" - - _collection_name = "servers" - - _progress_statuses = ( - "ACTIVE", - "BUILD", - "REBUILD", - "RESIZE", - "VERIFY_RESIZE", - ) - - _fault_statuses = ( - "ERROR", - ) - - def __init__(self): - """Initialize view builder.""" - super(ViewBuilder, self).__init__() - self._address_builder = views_addresses.ViewBuilder() - self._flavor_builder = views_flavors.ViewBuilder() - self._image_builder = views_images.ViewBuilder() - - def _skip_precooked(func): - def wrapped(self, request, instance): - if instance.get("_is_precooked"): - return dict(server=instance) - else: - return func(self, request, instance) - return wrapped - - def create(self, request, instance): - """View that should be returned when an instance is created.""" - return { - "server": { - "id": instance["uuid"], - "links": self._get_links(request, instance["uuid"]), - }, - } - - @_skip_precooked - def basic(self, request, instance): - """Generic, non-detailed view of an instance.""" - return { - "server": { - "id": instance["uuid"], - "name": instance["display_name"], - "links": self._get_links(request, instance["uuid"]), - }, - } - - @_skip_precooked - def show(self, request, instance): - """Detailed view of a single instance.""" - server = { - "server": { - "id": instance["uuid"], - "name": instance["display_name"], - "status": self._get_vm_state(instance), - "tenant_id": instance.get("project_id") or "", - "user_id": instance.get("user_id") or "", - "metadata": self._get_metadata(instance), - "hostId": self._get_host_id(instance) or "", - "image": self._get_image(request, instance), - "flavor": self._get_flavor(request, instance), - "created": utils.isotime(instance["created_at"]), - "updated": utils.isotime(instance["updated_at"]), - "addresses": self._get_addresses(request, instance), - "accessIPv4": instance.get("access_ip_v4") or "", - "accessIPv6": instance.get("access_ip_v6") or "", - "key_name": instance.get("key_name") or "", - "config_drive": instance.get("config_drive"), - "links": self._get_links(request, instance["uuid"]), - }, - } - _inst_fault = self._get_fault(request, instance) - if server["server"]["status"] in self._fault_statuses and _inst_fault: - server['server']['fault'] = _inst_fault - - if server["server"]["status"] in self._progress_statuses: - server["server"]["progress"] = instance.get("progress", 0) - - return server - - def index(self, request, instances): - """Show a list of servers without many details.""" - return self._list_view(self.basic, request, instances) - - def detail(self, request, instances): - """Detailed view of a list of instance.""" - return self._list_view(self.show, request, instances) - - def _list_view(self, func, request, servers): - """Provide a view for a list of servers.""" - server_list = [func(request, server)["server"] for server in servers] - servers_links = self._get_collection_links(request, servers) - servers_dict = dict(servers=server_list) - - if servers_links: - servers_dict["servers_links"] = servers_links - - return servers_dict - - @staticmethod - def _get_metadata(instance): - metadata = instance.get("metadata", []) - return dict((item['key'], str(item['value'])) for item in metadata) - - @staticmethod - def _get_vm_state(instance): - return common.status_from_state(instance.get("vm_state"), - instance.get("task_state")) - - @staticmethod - def _get_host_id(instance): - host = instance.get("host") - if host: - return hashlib.sha224(host).hexdigest() # pylint: disable=E1101 - - def _get_addresses(self, request, instance): - context = request.environ["nova.context"] - networks = common.get_networks_for_instance(context, instance) - return self._address_builder.index(networks)["addresses"] - - def _get_image(self, request, instance): - image_ref = instance["image_ref"] - image_id = str(common.get_id_from_href(image_ref)) - bookmark = self._image_builder._get_bookmark_link(request, image_id) - return { - "id": image_id, - "links": [{ - "rel": "bookmark", - "href": bookmark, - }], - } - - def _get_flavor(self, request, instance): - flavor_id = instance["instance_type"]["flavorid"] - flavor_ref = self._flavor_builder._get_href_link(request, flavor_id) - flavor_bookmark = self._flavor_builder._get_bookmark_link(request, - flavor_id) - return { - "id": str(common.get_id_from_href(flavor_ref)), - "links": [{ - "rel": "bookmark", - "href": flavor_bookmark, - }], - } - - def _get_fault(self, request, instance): - fault = instance.get("fault", None) - - if not fault: - return None - - return { - "code": fault["code"], - "created": utils.isotime(fault["created_at"]), - "message": fault["message"], - "details": fault["details"], - } diff --git a/nova/api/openstack/v2/views/versions.py b/nova/api/openstack/v2/views/versions.py deleted file mode 100644 index cb2fd9f4a..000000000 --- a/nova/api/openstack/v2/views/versions.py +++ /dev/null @@ -1,94 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import copy -import os - - -def get_view_builder(req): - base_url = req.application_url - return ViewBuilder(base_url) - - -class ViewBuilder(object): - - def __init__(self, base_url): - """ - :param base_url: url of the root wsgi application - """ - self.base_url = base_url - - def build_choices(self, VERSIONS, req): - version_objs = [] - for version in VERSIONS: - version = VERSIONS[version] - version_objs.append({ - "id": version['id'], - "status": version['status'], - "links": [ - { - "rel": "self", - "href": self.generate_href(req.path), - }, - ], - "media-types": version['media-types'], - }) - - return dict(choices=version_objs) - - def build_versions(self, versions): - version_objs = [] - for version in sorted(versions.keys()): - version = versions[version] - version_objs.append({ - "id": version['id'], - "status": version['status'], - "updated": version['updated'], - "links": self._build_links(version), - }) - - return dict(versions=version_objs) - - def build_version(self, version): - reval = copy.deepcopy(version) - reval['links'].insert(0, { - "rel": "self", - "href": self.base_url.rstrip('/') + '/', - }) - return dict(version=reval) - - def _build_links(self, version_data): - """Generate a container of links that refer to the provided version.""" - href = self.generate_href() - - links = [ - { - "rel": "self", - "href": href, - }, - ] - - return links - - def generate_href(self, path=None): - """Create an url that refers to a specific version_number.""" - version_number = 'v2' - if path: - path = path.strip('/') - return os.path.join(self.base_url, version_number, path) - else: - return os.path.join(self.base_url, version_number) + '/' diff --git a/nova/api/openstack/volume/__init__.py b/nova/api/openstack/volume/__init__.py new file mode 100644 index 000000000..075b53c29 --- /dev/null +++ b/nova/api/openstack/volume/__init__.py @@ -0,0 +1,99 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +WSGI middleware for OpenStack Volume API. +""" + +import routes +import webob.dec +import webob.exc + +import nova.api.openstack +from nova.api.openstack.volume import extensions +from nova.api.openstack.volume import snapshots +from nova.api.openstack.volume import types +from nova.api.openstack.volume import volumes +from nova.api.openstack.volume import versions +from nova.api.openstack import wsgi +from nova import flags +from nova import log as logging +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack.volume') +FLAGS = flags.FLAGS + + +class APIRouter(base_wsgi.Router): + """ + Routes requests on the OpenStack API to the appropriate controller + and method. + """ + + @classmethod + def factory(cls, global_config, **local_config): + """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one""" + return cls() + + def __init__(self, ext_mgr=None): + if ext_mgr is None: + ext_mgr = extensions.ExtensionManager() + + mapper = nova.api.openstack.ProjectMapper() + self._setup_routes(mapper) + self._setup_ext_routes(mapper, ext_mgr) + super(APIRouter, self).__init__(mapper) + + def _setup_ext_routes(self, mapper, ext_mgr): + serializer = wsgi.ResponseSerializer( + {'application/xml': wsgi.XMLDictSerializer()}) + for resource in ext_mgr.get_resources(): + LOG.debug(_('Extended resource: %s'), + resource.collection) + if resource.serializer is None: + resource.serializer = serializer + + kargs = dict( + controller=wsgi.Resource( + resource.controller, resource.deserializer, + resource.serializer), + collection=resource.collection_actions, + member=resource.member_actions) + + if resource.parent: + kargs['parent_resource'] = resource.parent + + mapper.resource(resource.collection, resource.collection, **kargs) + + def _setup_routes(self, mapper): + mapper.connect("versions", "/", + controller=versions.create_resource(), + action='show') + + mapper.redirect("", "/") + + mapper.resource("volume", "volumes", + controller=volumes.create_resource(), + collection={'detail': 'GET'}) + + mapper.resource("type", "types", + controller=types.create_resource()) + + mapper.resource("snapshot", "snapshots", + controller=snapshots.create_resource()) diff --git a/nova/api/openstack/volume/contrib/__init__.py b/nova/api/openstack/volume/contrib/__init__.py new file mode 100644 index 000000000..58c0413ab --- /dev/null +++ b/nova/api/openstack/volume/contrib/__init__.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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. + +"""Contrib contains extensions that are shipped with nova. + +It can't be called 'extensions' because that causes namespacing problems. + +""" + +from nova import log as logging +from nova.api.openstack import extensions + + +LOG = logging.getLogger('nova.api.openstack.volume.contrib') + + +def standard_extensions(ext_mgr): + extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__) diff --git a/nova/api/openstack/volume/extensions.py b/nova/api/openstack/volume/extensions.py new file mode 100644 index 000000000..d1007629e --- /dev/null +++ b/nova/api/openstack/volume/extensions.py @@ -0,0 +1,44 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.openstack import extensions as base_extensions +from nova import flags +from nova import log as logging + + +LOG = logging.getLogger('nova.api.openstack.volume.extensions') +FLAGS = flags.FLAGS + + +class ExtensionManager(base_extensions.ExtensionManager): + def __new__(cls): + if cls._ext_mgr is None: + LOG.audit(_('Initializing extension manager.')) + + cls._ext_mgr = super(ExtensionManager, cls).__new__(cls) + + cls.cls_list = FLAGS.osapi_volume_extension + cls._ext_mgr.extensions = {} + cls._ext_mgr._load_extensions() + + return cls._ext_mgr + + +class ExtensionMiddleware(base_extensions.ExtensionMiddleware): + def __init__(self, application, ext_mgr=None): + ext_mgr = ExtensionManager() + super(ExtensionMiddleware, self).__init__(application, ext_mgr) diff --git a/nova/api/openstack/volume/snapshots.py b/nova/api/openstack/volume/snapshots.py new file mode 100644 index 000000000..f6ec3dc5f --- /dev/null +++ b/nova/api/openstack/volume/snapshots.py @@ -0,0 +1,183 @@ +# Copyright 2011 Justin Santa Barbara +# 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. + +"""The volumes snapshots api.""" + +from webob import exc +import webob + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack.compute import servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova.volume import volume_types + + +LOG = logging.getLogger("nova.api.openstack.volume.snapshots") + + +FLAGS = flags.FLAGS + + +def _translate_snapshot_detail_view(context, vol): + """Maps keys for snapshots details view.""" + + d = _translate_snapshot_summary_view(context, vol) + + # NOTE(gagupta): No additional data / lookups at the moment + return d + + +def _translate_snapshot_summary_view(context, vol): + """Maps keys for snapshots summary view.""" + d = {} + + d['id'] = vol['id'] + d['volumeId'] = vol['volume_id'] + d['status'] = vol['status'] + # NOTE(gagupta): We map volume_size as the snapshot size + d['size'] = vol['volume_size'] + d['createdAt'] = vol['created_at'] + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class SnapshotsController(object): + """The Volumes API controller for the OpenStack API.""" + + def __init__(self): + self.volume_api = volume.API() + super(SnapshotsController, self).__init__() + + def show(self, req, id): + """Return data about the given snapshot.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get_snapshot(context, id) + except exception.NotFound: + return exc.HTTPNotFound() + + return {'snapshot': _translate_snapshot_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a snapshot.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete snapshot with id: %s"), id, context=context) + + try: + self.volume_api.delete_snapshot(context, snapshot_id=id) + except exception.NotFound: + return exc.HTTPNotFound() + return webob.Response(status_int=202) + + def index(self, req): + """Returns a summary list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_summary_view) + + def detail(self, req): + """Returns a detailed list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of snapshots, transformed through entity_maker.""" + context = req.environ['nova.context'] + + snapshots = self.volume_api.get_all_snapshots(context) + limited_list = common.limited(snapshots, req) + res = [entity_maker(context, snapshot) for snapshot in limited_list] + return {'snapshots': res} + + def create(self, req, body): + """Creates a new snapshot.""" + context = req.environ['nova.context'] + + if not body: + return exc.HTTPUnprocessableEntity() + + snapshot = body['snapshot'] + volume_id = snapshot['volume_id'] + force = snapshot.get('force', False) + LOG.audit(_("Create snapshot from volume %s"), volume_id, + context=context) + + if force: + new_snapshot = self.volume_api.create_snapshot_force(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + else: + new_snapshot = self.volume_api.create_snapshot(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + + retval = _translate_snapshot_detail_view(context, new_snapshot) + + return {'snapshot': retval} + + +def make_snapshot(elem): + elem.set('id') + elem.set('status') + elem.set('size') + elem.set('createdAt') + elem.set('displayName') + elem.set('displayDescription') + elem.set('volumeId') + + +class SnapshotTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('snapshot', selector='snapshot') + make_snapshot(root) + return xmlutil.MasterTemplate(root, 1) + + +class SnapshotsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('snapshots') + elem = xmlutil.SubTemplateElement(root, 'snapshot', + selector='snapshots') + make_snapshot(elem) + return xmlutil.MasterTemplate(root, 1) + + +class SnapshotSerializer(xmlutil.XMLTemplateSerializer): + def default(self): + return SnapshotTemplate() + + def index(self): + return SnapshotsTemplate() + + def detail(self): + return SnapshotsTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': SnapshotSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + + return wsgi.Resource(SnapshotsController(), serializer=serializer) diff --git a/nova/api/openstack/volume/types.py b/nova/api/openstack/volume/types.py new file mode 100644 index 000000000..1c6a68b58 --- /dev/null +++ b/nova/api/openstack/volume/types.py @@ -0,0 +1,89 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# +# 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. + +""" The volume type & volume types extra specs extension""" + +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import db +from nova import exception +from nova.volume import volume_types + + +class VolumeTypesController(object): + """ The volume types API controller for the Openstack API """ + + def index(self, req): + """ Returns the list of volume types """ + context = req.environ['nova.context'] + return volume_types.get_all_types(context) + + def show(self, req, id): + """ Return a single volume type item """ + context = req.environ['nova.context'] + + try: + vol_type = volume_types.get_volume_type(context, id) + except exception.NotFound or exception.ApiError: + raise exc.HTTPNotFound() + + return {'volume_type': vol_type} + + +def make_voltype(elem): + elem.set('id') + elem.set('name') + extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') + elem.append(extra_specs) + + +class VolumeTypeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume_type', selector='volume_type') + make_voltype(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume_types') + sel = lambda obj, do_raise=False: obj.values() + elem = xmlutil.SubTemplateElement(root, 'volume_type', selector=sel) + make_voltype(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypesSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return VolumeTypesTemplate() + + def default(self): + return VolumeTypeTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': VolumeTypesSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + + deserializer = wsgi.RequestDeserializer() + + return wsgi.Resource(VolumeTypesController(), serializer=serializer) diff --git a/nova/api/openstack/volume/versions.py b/nova/api/openstack/volume/versions.py new file mode 100644 index 000000000..9a29e4adf --- /dev/null +++ b/nova/api/openstack/volume/versions.py @@ -0,0 +1,83 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from datetime import datetime + +from lxml import etree + +from nova.api.openstack.compute import versions +from nova.api.openstack.volume.views import versions as views_versions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil + + +VERSIONS = { + "v1": { + "id": "v1", + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://jorgew.github.com/block-storage-api/" + "content/os-block-storage-1.0.pdf", + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + #(anthony) FIXME + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.volume+xml;version=1", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.volume+json;version=1", + } + ], + } +} + + +class Versions(versions.Versions): + def dispatch(self, request, *args): + """Respond to a request for all OpenStack API versions.""" + builder = views_versions.get_view_builder(request) + if request.path == '/': + # List Versions + return builder.build_versions(VERSIONS) + else: + # Versions Multiple Choice + return builder.build_choices(VERSIONS, request) + + +class VolumeVersionV1(object): + @wsgi.serializers(xml=versions.VersionTemplate, + atom=versions.VersionAtomSerializer) + def show(self, req): + builder = views_versions.get_view_builder(req) + return builder.build_version(VERSIONS['v2.0']) + + +def create_resource(): + return wsgi.Resource(VolumeVersionV1()) diff --git a/nova/api/openstack/volume/views/__init__.py b/nova/api/openstack/volume/views/__init__.py new file mode 100644 index 000000000..d65c689a8 --- /dev/null +++ b/nova/api/openstack/volume/views/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/nova/api/openstack/volume/views/versions.py b/nova/api/openstack/volume/views/versions.py new file mode 100644 index 000000000..e446a4b64 --- /dev/null +++ b/nova/api/openstack/volume/views/versions.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import os + +from nova.api.openstack.compute.views import versions as compute_views + + +def get_view_builder(req): + base_url = req.application_url + return ViewBuilder(base_url) + + +class ViewBuilder(compute_views.ViewBuilder): + def generate_href(self, path=None): + """Create an url that refers to a specific version_number.""" + version_number = 'v1' + if path: + path = path.strip('/') + return os.path.join(self.base_url, version_number, path) + else: + return os.path.join(self.base_url, version_number) + '/' diff --git a/nova/api/openstack/volume/volumes.py b/nova/api/openstack/volume/volumes.py new file mode 100644 index 000000000..5e16f4fde --- /dev/null +++ b/nova/api/openstack/volume/volumes.py @@ -0,0 +1,254 @@ +# Copyright 2011 Justin Santa Barbara +# 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. + +"""The volumes api.""" + +from webob import exc +import webob + +from nova.api.openstack import common +from nova.api.openstack.compute import servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova.volume import volume_types + + +LOG = logging.getLogger("nova.api.openstack.volume.volumes") + + +FLAGS = flags.FLAGS + + +def _translate_volume_detail_view(context, vol): + """Maps keys for volumes details view.""" + + d = _translate_volume_summary_view(context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_volume_summary_view(context, vol): + """Maps keys for volumes summary view.""" + d = {} + + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] + + if vol['attach_status'] == 'attached': + d['attachments'] = [_translate_attachment_detail_view(context, vol)] + else: + d['attachments'] = [{}] + + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + + if vol['volume_type_id'] and vol.get('volume_type'): + d['volumeType'] = vol['volume_type']['name'] + else: + d['volumeType'] = vol['volume_type_id'] + + d['snapshotId'] = vol['snapshot_id'] + LOG.audit(_("vol=%s"), vol, context=context) + + if vol.get('volume_metadata'): + meta_dict = {} + for i in vol['volume_metadata']: + meta_dict[i['key']] = i['value'] + d['metadata'] = meta_dict + else: + d['metadata'] = {} + + return d + + +class VolumeController(object): + """The Volumes API controller for the OpenStack API.""" + + def __init__(self): + self.volume_api = volume.API() + super(VolumeController, self).__init__() + + def show(self, req, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + raise exc.HTTPNotFound() + + return {'volume': _translate_volume_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + + def index(self, req): + """Returns a summary list of volumes.""" + return self._items(req, entity_maker=_translate_volume_summary_view) + + def detail(self, req): + """Returns a detailed list of volumes.""" + return self._items(req, entity_maker=_translate_volume_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker.""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumes': res} + + def create(self, req, body): + """Creates a new volume.""" + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + vol = body['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + + vol_type = vol.get('volume_type', None) + if vol_type: + try: + vol_type = volume_types.get_volume_type_by_name(context, + vol_type) + except exception.NotFound: + raise exc.HTTPNotFound() + + metadata = vol.get('metadata', None) + + new_volume = self.volume_api.create(context, size, + vol.get('snapshot_id'), + vol.get('display_name'), + vol.get('display_description'), + volume_type=vol_type, + metadata=metadata) + + # Work around problem that instance is lazy-loaded... + new_volume = self.volume_api.get(context, new_volume['id']) + + retval = _translate_volume_detail_view(context, new_volume) + + return {'volume': retval} + + +class VolumeAttachmentTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumeAttachment', + selector='volumeAttachment') + make_attachment(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeAttachmentsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumeAttachments') + elem = xmlutil.SubTemplateElement(root, 'volumeAttachment', + selector='volumeAttachments') + make_attachment(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeAttachmentSerializer(xmlutil.XMLTemplateSerializer): + def default(self): + return VolumeAttachmentTemplate() + + def index(self): + return VolumeAttachmentsTemplate() + + +def make_attachment(elem): + elem.set('id') + elem.set('serverId') + elem.set('volumeId') + elem.set('device') + + +def make_volume(elem): + elem.set('id') + elem.set('status') + elem.set('size') + elem.set('availabilityZone') + elem.set('createdAt') + elem.set('displayName') + elem.set('displayDescription') + elem.set('volumeType') + elem.set('snapshotId') + + attachments = xmlutil.SubTemplateElement(elem, 'attachments') + attachment = xmlutil.SubTemplateElement(attachments, 'attachment', + selector='attachments') + make_attachment(attachment) + + metadata = xmlutil.make_flat_dict('metadata') + elem.append(metadata) + + +class VolumeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume', selector='volume') + make_volume(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumes') + elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') + make_volume(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeSerializer(xmlutil.XMLTemplateSerializer): + def default(self): + return VolumeTemplate() + + def index(self): + return VolumesTemplate() + + def detail(self): + return VolumesTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': VolumeSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + + deserializer = wsgi.RequestDeserializer() + + return wsgi.Resource(VolumeController(), serializer=serializer) diff --git a/nova/api/openstack/xmlutil.py b/nova/api/openstack/xmlutil.py index 90861ad4f..4b64d4a8f 100644 --- a/nova/api/openstack/xmlutil.py +++ b/nova/api/openstack/xmlutil.py @@ -31,9 +31,9 @@ XMLNS_ATOM = 'http://www.w3.org/2005/Atom' def validate_schema(xml, schema_name): if isinstance(xml, str): xml = etree.fromstring(xml) - base_path = 'nova/api/openstack/v2/schemas/v1.1/' + base_path = 'nova/api/openstack/compute/schemas/v1.1/' if schema_name in ('atom', 'atom-link'): - base_path = 'nova/api/openstack/v2/schemas/' + base_path = 'nova/api/openstack/compute/schemas/' schema_path = os.path.join(utils.novadir(), '%s%s.rng' % (base_path, schema_name)) schema_doc = etree.parse(schema_path) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 3d984f1bf..f6c88e082 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -827,7 +827,7 @@ class AuthManager(object): 's3': 'http://%s:%s' % (s3_host, FLAGS.s3_port), 'os': '%s://%s:%s%s' % (FLAGS.osapi_scheme, ec2_host, - FLAGS.osapi_port, + FLAGS.osapi_compute_listen_port, FLAGS.osapi_path), 'user': user.name, 'nova': FLAGS.ca_file, diff --git a/nova/common/cfg.py b/nova/common/cfg.py index 58d17d99f..bfb7b8a19 100644 --- a/nova/common/cfg.py +++ b/nova/common/cfg.py @@ -32,14 +32,14 @@ Options can be strings, integers, floats, booleans, lists or 'multi strings': enabled_apis_opt = \ cfg.ListOpt('enabled_apis', - default=['ec2', 'osapi'], + default=['ec2', 'osapi_compute'], help='List of APIs to enable by default') DEFAULT_EXTENSIONS = [ 'nova.api.openstack.contrib.standard_extensions' ] - osapi_extension_opt = \ - cfg.MultiStrOpt('osapi_extension', + osapi_compute_extension_opt = \ + cfg.MultiStrOpt('osapi_compute_extension', default=DEFAULT_EXTENSIONS) Option schemas are registered with with the config manager at runtime, but @@ -55,7 +55,7 @@ before the option is referenced: ... def _load_extensions(self): - for ext_factory in self.conf.osapi_extension: + for ext_factory in self.conf.osapi_compute_extension: .... A common usage pattern is for each option schema to be defined in the module or diff --git a/nova/flags.py b/nova/flags.py index a51508c97..b566f303a 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -309,19 +309,21 @@ DEFINE_integer('rabbit_max_retries', 0, 'maximum rabbit connection attempts (0=try forever)') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') DEFINE_boolean('rabbit_durable_queues', False, 'use durable queues') -DEFINE_list('enabled_apis', ['ec2', 'osapi', 'metadata'], +DEFINE_list('enabled_apis', + ['ec2', 'osapi_compute', 'osapi_volume', 'metadata'], 'list of APIs to enable by default') DEFINE_string('ec2_host', '$my_ip', 'ip of api server') DEFINE_string('ec2_dmz_host', '$my_ip', 'internal ip of api server') DEFINE_integer('ec2_port', 8773, 'cloud controller port') DEFINE_string('ec2_scheme', 'http', 'prefix for ec2') DEFINE_string('ec2_path', '/services/Cloud', 'suffix for ec2') -DEFINE_multistring('osapi_extension', - ['nova.api.openstack.v2.contrib.standard_extensions'], - 'osapi extension to load') -DEFINE_string('osapi_host', '$my_ip', 'ip of api server') +DEFINE_multistring('osapi_compute_extension', + ['nova.api.openstack.compute.contrib.standard_extensions'], + 'osapi compute extension to load') +DEFINE_multistring('osapi_volume_extension', + ['nova.api.openstack.volume.contrib.standard_extensions'], + 'osapi volume extension to load') DEFINE_string('osapi_scheme', 'http', 'prefix for openstack') -DEFINE_integer('osapi_port', 8774, 'OpenStack API port') DEFINE_string('osapi_path', '/v1.1/', 'suffix for openstack') DEFINE_integer('osapi_max_limit', 1000, 'max number of items returned in a collection response') diff --git a/nova/service.py b/nova/service.py index 95399f650..bfe6d1d54 100644 --- a/nova/service.py +++ b/nova/service.py @@ -48,9 +48,10 @@ flags.DEFINE_integer('periodic_interval', 60, flags.DEFINE_string('ec2_listen', "0.0.0.0", 'IP address for EC2 API to listen') flags.DEFINE_integer('ec2_listen_port', 8773, 'port for ec2 api to listen') -flags.DEFINE_string('osapi_listen', "0.0.0.0", +flags.DEFINE_string('osapi_compute_listen', "0.0.0.0", 'IP address for OpenStack API to listen') -flags.DEFINE_integer('osapi_listen_port', 8774, 'port for os api to listen') +flags.DEFINE_integer('osapi_compute_listen_port', 8774, + 'list port for osapi compute') flags.DEFINE_string('metadata_manager', 'nova.api.manager.MetadataManager', 'OpenStack metadata service manager') flags.DEFINE_string('metadata_listen', "0.0.0.0", @@ -59,6 +60,10 @@ flags.DEFINE_integer('metadata_listen_port', 8775, 'port for metadata api to listen') flags.DEFINE_string('api_paste_config', "api-paste.ini", 'File name for the paste.deploy config for nova-api') +flags.DEFINE_string('osapi_volume_listen', "0.0.0.0", + 'IP address for OpenStack Volume API to listen') +flags.DEFINE_integer('osapi_volume_listen_port', 8776, + 'port for os volume api to listen') class Launcher(object): diff --git a/nova/tests/api/openstack/compute/__init__.py b/nova/tests/api/openstack/compute/__init__.py new file mode 100644 index 000000000..00fcfbb00 --- /dev/null +++ b/nova/tests/api/openstack/compute/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/nova/tests/api/openstack/compute/contrib/__init__.py b/nova/tests/api/openstack/compute/contrib/__init__.py new file mode 100644 index 000000000..848908a95 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# +# 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. diff --git a/nova/tests/api/openstack/compute/contrib/test_accounts.py b/nova/tests/api/openstack/compute/contrib/test_accounts.py new file mode 100644 index 000000000..dbf0e2600 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_accounts.py @@ -0,0 +1,162 @@ +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import json + +from lxml import etree +import webob + +from nova import test +from nova.api.openstack.compute.contrib import accounts +from nova.auth.manager import User +from nova.tests.api.openstack import fakes + + +def fake_init(self): + self.manager = fakes.FakeAuthManager() + + +def fake_admin_check(self, req): + return True + + +class AccountsTest(test.TestCase): + def setUp(self): + super(AccountsTest, self).setUp() + self.flags(verbose=True, allow_admin_api=True) + self.stubs.Set(accounts.Controller, '__init__', + fake_init) + self.stubs.Set(accounts.Controller, '_check_admin', + fake_admin_check) + fakes.FakeAuthManager.clear_fakes() + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + + fakemgr = fakes.FakeAuthManager() + joeuser = User('id1', 'guy1', 'acc1', 'secret1', False) + superuser = User('id2', 'guy2', 'acc2', 'secret2', True) + fakemgr.add_user(joeuser) + fakemgr.add_user(superuser) + fakemgr.create_project('test1', joeuser) + fakemgr.create_project('test2', superuser) + + def test_get_account(self): + req = webob.Request.blank('/v2/fake/accounts/test1') + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_dict['account']['id'], 'test1') + self.assertEqual(res_dict['account']['name'], 'test1') + self.assertEqual(res_dict['account']['manager'], 'id1') + + def test_get_account_xml(self): + req = webob.Request.blank('/v2/fake/accounts/test1.xml') + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual('account', res_tree.tag) + self.assertEqual('test1', res_tree.get('id')) + self.assertEqual('test1', res_tree.get('name')) + self.assertEqual('id1', res_tree.get('manager')) + + def test_account_delete(self): + req = webob.Request.blank('/v2/fake/accounts/test1') + req.method = 'DELETE' + res = req.get_response(fakes.wsgi_app()) + self.assertTrue('test1' not in fakes.FakeAuthManager.projects) + self.assertEqual(res.status_int, 200) + + def test_account_create(self): + body = dict(account=dict(description='test account', + manager='id1')) + req = webob.Request.blank('/v2/fake/accounts/newacct') + req.headers["Content-Type"] = "application/json" + req.method = 'PUT' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_dict['account']['id'], 'newacct') + self.assertEqual(res_dict['account']['name'], 'newacct') + self.assertEqual(res_dict['account']['description'], 'test account') + self.assertEqual(res_dict['account']['manager'], 'id1') + self.assertTrue('newacct' in + fakes.FakeAuthManager.projects) + self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 3) + + def test_account_create_xml(self): + body = dict(account=dict(description='test account', + manager='id1')) + req = webob.Request.blank('/v2/fake/accounts/newacct.xml') + req.headers["Content-Type"] = "application/json" + req.method = 'PUT' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, 'account') + self.assertEqual(res_tree.get('id'), 'newacct') + self.assertEqual(res_tree.get('name'), 'newacct') + self.assertEqual(res_tree.get('description'), 'test account') + self.assertEqual(res_tree.get('manager'), 'id1') + self.assertTrue('newacct' in + fakes.FakeAuthManager.projects) + self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 3) + + def test_account_update(self): + body = dict(account=dict(description='test account', + manager='id2')) + req = webob.Request.blank('/v2/fake/accounts/test1') + req.headers["Content-Type"] = "application/json" + req.method = 'PUT' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_dict['account']['id'], 'test1') + self.assertEqual(res_dict['account']['name'], 'test1') + self.assertEqual(res_dict['account']['description'], 'test account') + self.assertEqual(res_dict['account']['manager'], 'id2') + self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 2) + + def test_account_update_xml(self): + body = dict(account=dict(description='test account', + manager='id2')) + req = webob.Request.blank('/v2/fake/accounts/test1.xml') + req.headers["Content-Type"] = "application/json" + req.method = 'PUT' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + res_tree = etree.fromstring(res.body) + + self.assertEqual(res.status_int, 200) + self.assertEqual(res_tree.tag, 'account') + self.assertEqual(res_tree.get('id'), 'test1') + self.assertEqual(res_tree.get('name'), 'test1') + self.assertEqual(res_tree.get('description'), 'test account') + self.assertEqual(res_tree.get('manager'), 'id2') + self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 2) diff --git a/nova/tests/api/openstack/compute/contrib/test_admin_actions.py b/nova/tests/api/openstack/compute/contrib/test_admin_actions.py new file mode 100644 index 000000000..f8a5d1103 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_admin_actions.py @@ -0,0 +1,250 @@ +# Copyright 2011 OpenStack LLC. +# +# 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 json + +import webob + +from nova.api.openstack import compute as compute_api +from nova.api.openstack.compute import extensions +from nova.api.openstack import wsgi +from nova import compute +from nova import exception +from nova import flags +from nova import test +from nova import utils +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS + +INSTANCE = { + "id": 1, + "name": "fake", + "display_name": "test_server", + "uuid": "abcd", + "user_id": 'fake_user_id', + "tenant_id": 'fake_tenant_id', + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "security_groups": [{"id": 1, "name": "test"}], + "progress": 0, + "image_ref": 'http://foo.com/123', + "fixed_ips": [], + "instance_type": {"flavorid": '124'}, + } + + +def fake_compute_api(*args, **kwargs): + return True + + +def fake_compute_api_raises_invalid_state(*args, **kwargs): + raise exception.InstanceInvalidState + + +def fake_compute_api_get(self, context, instance_id): + return {'id': 1, 'uuid': instance_id} + + +class AdminActionsTest(test.TestCase): + + _actions = ('pause', 'unpause', 'suspend', 'resume', 'migrate', + 'resetNetwork', 'injectNetworkInfo', 'lock', 'unlock') + + _methods = ('pause', 'unpause', 'suspend', 'resume', 'resize', + 'reset_network', 'inject_network_info', 'lock', 'unlock') + + _actions_that_check_state = ( + # action, method + ('pause', 'pause'), + ('unpause', 'unpause'), + ('suspend', 'suspend'), + ('resume', 'resume'), + ('migrate', 'resize')) + + def setUp(self): + super(AdminActionsTest, self).setUp() + self.stubs.Set(compute.API, 'get', fake_compute_api_get) + self.UUID = utils.gen_uuid() + self.flags(allow_admin_api=True) + for _method in self._methods: + self.stubs.Set(compute.API, _method, fake_compute_api) + + def test_admin_api_actions(self): + self.maxDiff = None + app = fakes.wsgi_app() + for _action in self._actions: + req = webob.Request.blank('/v2/fake/servers/%s/action' % + self.UUID) + req.method = 'POST' + req.body = json.dumps({_action: None}) + req.content_type = 'application/json' + res = req.get_response(app) + self.assertEqual(res.status_int, 202) + + def test_admin_api_actions_raise_conflict_on_invalid_state(self): + self.maxDiff = None + app = fakes.wsgi_app() + + for _action, _method in self._actions_that_check_state: + self.stubs.Set(compute.API, _method, + fake_compute_api_raises_invalid_state) + + req = webob.Request.blank('/v2/fake/servers/%s/action' % + self.UUID) + req.method = 'POST' + req.body = json.dumps({_action: None}) + req.content_type = 'application/json' + res = req.get_response(app) + self.assertEqual(res.status_int, 409) + self.assertIn("invalid state for '%(_action)s'" % locals(), + res.body) + + +class CreateBackupTests(test.TestCase): + + def setUp(self): + super(CreateBackupTests, self).setUp() + + self.stubs.Set(compute.API, 'get', fake_compute_api_get) + self.backup_stubs = fakes.stub_out_compute_api_backup(self.stubs) + + self.flags(allow_admin_api=True) + router = compute_api.APIRouter() + ext_middleware = extensions.ExtensionMiddleware(router) + self.app = wsgi.LazySerializationMiddleware(ext_middleware) + + self.uuid = utils.gen_uuid() + + def _get_request(self, body): + url = '/fake/servers/%s/action' % self.uuid + req = fakes.HTTPRequest.blank(url) + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + return req + + def test_create_backup_with_metadata(self): + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + 'metadata': {'123': 'asdf'}, + }, + } + + request = self._get_request(body) + response = request.get_response(self.app) + + self.assertEqual(response.status_int, 202) + self.assertTrue(response.headers['Location']) + + def test_create_backup_with_too_much_metadata(self): + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + 'metadata': {'123': 'asdf'}, + }, + } + for num in range(FLAGS.quota_metadata_items + 1): + body['createBackup']['metadata']['foo%i' % num] = "bar" + + request = self._get_request(body) + response = request.get_response(self.app) + self.assertEqual(response.status_int, 413) + + def test_create_backup_no_name(self): + """Name is required for backups""" + body = { + 'createBackup': { + 'backup_type': 'daily', + 'rotation': 1, + }, + } + + request = self._get_request(body) + response = request.get_response(self.app) + self.assertEqual(response.status_int, 400) + + def test_create_backup_no_rotation(self): + """Rotation is required for backup requests""" + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + }, + } + + request = self._get_request(body) + response = request.get_response(self.app) + self.assertEqual(response.status_int, 400) + + 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, + }, + } + + request = self._get_request(body) + response = request.get_response(self.app) + self.assertEqual(response.status_int, 400) + + def test_create_backup_bad_entity(self): + body = {'createBackup': 'go'} + + request = self._get_request(body) + response = request.get_response(self.app) + self.assertEqual(response.status_int, 400) + + def test_create_backup(self): + """The happy path for creating backups""" + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + + request = self._get_request(body) + response = request.get_response(self.app) + + self.assertTrue(response.headers['Location']) + instance_ref = self.backup_stubs.extra_props_last_call['instance_ref'] + expected_server_location = 'http://localhost/v2/servers/%s' % self.uuid + self.assertEqual(expected_server_location, instance_ref) + + def test_create_backup_raises_conflict_on_invalid_state(self): + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + + self.stubs.Set(compute.API, 'backup', + fake_compute_api_raises_invalid_state) + + request = self._get_request(body) + response = request.get_response(self.app) + self.assertEqual(response.status_int, 409) diff --git a/nova/tests/api/openstack/compute/contrib/test_cloudpipe.py b/nova/tests/api/openstack/compute/contrib/test_cloudpipe.py new file mode 100644 index 000000000..7c8a8b09f --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_cloudpipe.py @@ -0,0 +1,234 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import json + +import webob +from lxml import etree + +from nova.api import auth +from nova.api.openstack import compute +from nova.api.openstack.compute import wsgi +from nova.api.openstack.compute.contrib import cloudpipe +from nova.auth import manager +from nova.cloudpipe import pipelib +from nova import context +from nova import crypto +from nova import db +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes +from nova import utils + + +EMPTY_INSTANCE_LIST = True +FLAGS = flags.FLAGS + + +class FakeProject(object): + def __init__(self, id, name, manager, desc, members, ip, port): + self.id = id + self.name = name + self.project_manager_id = manager + self.description = desc + self.member_ids = members + self.vpn_ip = ip + self.vpn_port = port + + +def fake_vpn_instance(): + return {'id': 7, 'image_id': FLAGS.vpn_image_id, 'vm_state': 'active', + 'created_at': utils.parse_strtime('1981-10-20T00:00:00.000000'), + 'uuid': 7777} + + +def fake_vpn_instance_low_id(): + return {'id': 4, 'image_id': FLAGS.vpn_image_id, 'vm_state': 'active', + 'created_at': utils.parse_strtime('1981-10-20T00:00:00.000000')} + + +def fake_project(): + proj = FakeProject(1, '1', 'fakeuser', '', [1], '127.0.0.1', 22) + return proj + + +def db_instance_get_all_by_project(self, project_id): + if EMPTY_INSTANCE_LIST: + return [] + else: + return [fake_vpn_instance()] + + +def db_security_group_exists(context, project_id, group_name): + # used in pipelib + return True + + +def pipelib_launch_vpn_instance(self, project_id, user_id): + global EMPTY_INSTANCE_LIST + EMPTY_INSTANCE_LIST = False + + +def auth_manager_get_project(self, project_id): + return fake_project() + + +def auth_manager_get_projects(self): + return [fake_project()] + + +def utils_vpn_ping(addr, port, timoeout=0.05, session_id=None): + return True + + +def better_not_call_this(*args, **kwargs): + raise Exception("You should not have done that") + + +class FakeAuthManager(object): + def get_projects(self): + return [fake_project()] + + def get_project(self, project_id): + return fake_project() + + +class CloudpipeTest(test.TestCase): + + def setUp(self): + super(CloudpipeTest, self).setUp() + self.flags(allow_admin_api=True) + self.app = fakes.wsgi_app() + inner_app = compute.APIRouter() + self.context = context.RequestContext('fake', 'fake', is_admin=True) + self.app = auth.InjectContext(self.context, inner_app) + route = inner_app.map.match('/1234/os-cloudpipe') + self.controller = route['controller'].controller + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + self.stubs.Set(db, "instance_get_all_by_project", + db_instance_get_all_by_project) + self.stubs.Set(db, "security_group_exists", + db_security_group_exists) + self.stubs.SmartSet(self.controller.cloudpipe, "launch_vpn_instance", + pipelib_launch_vpn_instance) + #self.stubs.SmartSet(self.controller.auth_manager, "get_project", + # auth_manager_get_project) + #self.stubs.SmartSet(self.controller.auth_manager, "get_projects", + # auth_manager_get_projects) + # NOTE(todd): The above code (just setting the stub, not invoking it) + # causes failures in AuthManagerLdapTestCase. So use a fake object. + self.controller.auth_manager = FakeAuthManager() + self.stubs.Set(utils, 'vpn_ping', utils_vpn_ping) + global EMPTY_INSTANCE_LIST + EMPTY_INSTANCE_LIST = True + + def test_cloudpipe_list_none_running(self): + """Should still get an entry per-project, just less descriptive.""" + req = webob.Request.blank('/fake/os-cloudpipe') + res = req.get_response(self.app) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + response = {'cloudpipes': [{'project_id': 1, 'public_ip': '127.0.0.1', + 'public_port': 22, 'state': 'pending'}]} + self.assertEqual(res_dict, response) + + def test_cloudpipe_list(self): + global EMPTY_INSTANCE_LIST + EMPTY_INSTANCE_LIST = False + req = webob.Request.blank('/fake/os-cloudpipe') + res = req.get_response(self.app) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + response = {'cloudpipes': [{'project_id': 1, 'public_ip': '127.0.0.1', + 'public_port': 22, 'state': 'running', + 'instance_id': 7777, + 'created_at': '1981-10-20T00:00:00Z'}]} + self.assertEqual(res_dict, response) + + def test_cloudpipe_create(self): + body = {'cloudpipe': {'project_id': 1}} + req = webob.Request.blank('/fake/os-cloudpipe') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + response = {'instance_id': 7777} + self.assertEqual(res_dict, response) + + def test_cloudpipe_create_already_running(self): + global EMPTY_INSTANCE_LIST + EMPTY_INSTANCE_LIST = False + self.stubs.SmartSet(self.controller.cloudpipe, 'launch_vpn_instance', + better_not_call_this) + body = {'cloudpipe': {'project_id': 1}} + req = webob.Request.blank('/fake/os-cloudpipe') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + response = {'instance_id': 7777} + self.assertEqual(res_dict, response) + + +class CloudpipesXMLSerializerTest(test.TestCase): + def test_default_serializer(self): + serializer = cloudpipe.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.assertTrue(child.tag in exemplar['cloudpipe']) + self.assertEqual(child.text, exemplar['cloudpipe'][child.tag]) + + def test_index_serializer(self): + serializer = cloudpipe.CloudpipesTemplate() + exemplar = dict(cloudpipes=[ + dict(cloudpipe=dict( + project_id='1234', + public_ip='1.2.3.4', + public_port='321', + instance_id='1234-1234-1234-1234', + created_at=utils.isotime(datetime.datetime.utcnow()), + state='running')), + dict(cloudpipe=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): + self.assertEqual('cloudpipe', cl_pipe.tag) + kp_data = exemplar['cloudpipes'][idx]['cloudpipe'] + for child in cl_pipe: + self.assertTrue(child.tag in 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 = ("\n" + '4321') + result = deserializer.deserialize(intext)['body'] + self.assertEqual(result, exemplar) diff --git a/nova/tests/api/openstack/compute/contrib/test_console_output.py b/nova/tests/api/openstack/compute/contrib/test_console_output.py new file mode 100644 index 000000000..ad22ff4cf --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_console_output.py @@ -0,0 +1,97 @@ +# 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 json + +import webob + +from nova import compute +from nova import exception +from nova import test +from nova.tests.api.openstack import fakes + + +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(self, context, instance_uuid): + return {'uuid': instance_uuid} + + +def fake_get_not_found(self, context, instance_uuid): + raise exception.NotFound() + + +class ConsoleOutputExtensionTest(test.TestCase): + + def setUp(self): + super(ConsoleOutputExtensionTest, self).setUp() + self.stubs.Set(compute.API, 'get_console_output', + fake_get_console_output) + self.stubs.Set(compute.API, 'get', fake_get) + + def test_get_text_console_instance_action(self): + body = {'os-getConsoleOutput': {}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + output = json.loads(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual(output, {'output': '0\n1\n2\n3\n4'}) + + def test_get_console_output_with_tail(self): + body = {'os-getConsoleOutput': {'length': 3}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + output = json.loads(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual(output, {'output': '2\n3\n4'}) + + def test_get_text_console_no_instance(self): + self.stubs.Set(compute.API, 'get', fake_get_not_found) + body = {'os-getConsoleOutput': {}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 404) + + def test_get_text_console_bad_body(self): + body = {} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) diff --git a/nova/tests/api/openstack/compute/contrib/test_createserverext.py b/nova/tests/api/openstack/compute/contrib/test_createserverext.py new file mode 100644 index 000000000..2393780a2 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_createserverext.py @@ -0,0 +1,430 @@ +# vim: tabstop=5 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +import datetime +import json +from xml.dom import minidom + +import webob + +import nova +from nova import db +from nova import exception +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS +FLAGS.verbose = True + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + +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')] + +INSTANCE = { + "id": 1, + "name": "fake", + "display_name": "test_server", + "uuid": FAKE_UUID, + "user_id": 'fake_user_id', + "tenant_id": 'fake_tenant_id', + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "security_groups": [{"id": 1, "name": "test"}], + "progress": 0, + "image_ref": 'http://foo.com/123', + "fixed_ips": [], + "instance_type": {"flavorid": '124'}, + } + + +def return_server_by_id(context, id, session=None): + INSTANCE['id'] = id + return INSTANCE + + +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() + + def tearDown(self): + super(CreateserverextTest, self).tearDown() + + def _make_stub_method(self, canned_return): + def stub_method(*args, **kwargs): + return canned_return + return stub_method + + def _setup_mock_compute_api(self): + + class MockComputeAPI(nova.compute.API): + + def __init__(self): + self.injected_files = None + self.networks = None + self.user_data = None + self.db = db + + def create(self, *args, **kwargs): + 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) + + def set_admin_password(self, *args, **kwargs): + pass + + compute_api = MockComputeAPI() + self.stubs.Set(nova.compute, 'API', + self._make_stub_method(compute_api)) + return compute_api + + def _setup_mock_network_api(self): + fakes.stub_out_nw_api(self.stubs) + + 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 = json.dumps(body_dict) + return req + + def _run_create_instance_with_mock_compute_api(self, request): + compute_api = self._setup_mock_compute_api() + self._setup_mock_network_api() + response = request.get_response(fakes.wsgi_app()) + return compute_api, response + + def _format_xml_request_body(self, body_dict): + server = body_dict['server'] + body_parts = [] + body_parts.extend([ + '', + '' % ( + server['name'], server['imageRef'], server['flavorRef'])]) + if 'metadata' in server: + metadata = server['metadata'] + body_parts.append('') + for item in metadata.iteritems(): + body_parts.append('%s' % item) + body_parts.append('') + if 'personality' in server: + personalities = server['personality'] + body_parts.append('') + for file in personalities: + item = (file['path'], file['contents']) + body_parts.append('%s' % item) + body_parts.append('') + if 'networks' in server: + networks = server['networks'] + body_parts.append('') + for network in networks: + item = (network['uuid'], network['fixed_ip']) + body_parts.append('' + % item) + body_parts.append('') + body_parts.append('') + 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) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + return request, response, compute_api.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) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + return request, response, compute_api.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) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + return request, response, compute_api.networks + + def test_create_instance_with_no_networks(self): + request, response, networks = \ + self._create_instance_with_networks_json(networks=None) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, None) + + def test_create_instance_with_no_networks_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml(networks=None) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, None) + + def test_create_instance_with_one_network(self): + request, response, networks = \ + self._create_instance_with_networks_json([FAKE_NETWORKS[0]]) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, [FAKE_NETWORKS[0]]) + + def test_create_instance_with_one_network_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml([FAKE_NETWORKS[0]]) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, [FAKE_NETWORKS[0]]) + + def test_create_instance_with_two_networks(self): + request, response, networks = \ + self._create_instance_with_networks_json(FAKE_NETWORKS) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, FAKE_NETWORKS) + + def test_create_instance_with_two_networks_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml(FAKE_NETWORKS) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, FAKE_NETWORKS) + + def test_create_instance_with_duplicate_networks(self): + request, response, networks = \ + self._create_instance_with_networks_json(DUPLICATE_NETWORKS) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_duplicate_networks_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml(DUPLICATE_NETWORKS) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + 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) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + self.assertEquals(response.status_int, 400) + self.assertEquals(compute_api.networks, None) + + 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, '') + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + self.assertEquals(response.status_int, 400) + self.assertEquals(compute_api.networks, None) + + def test_create_instance_with_network_invalid_id(self): + request, response, networks = \ + self._create_instance_with_networks_json(INVALID_NETWORKS) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_invalid_id_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml(INVALID_NETWORKS) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_empty_fixed_ip(self): + networks = [('1', '')] + request, response, networks = \ + self._create_instance_with_networks_json(networks) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_non_string_fixed_ip(self): + networks = [('1', 12345)] + request, response, networks = \ + self._create_instance_with_networks_json(networks) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_empty_fixed_ip_xml(self): + networks = [('1', '')] + request, response, networks = \ + self._create_instance_with_networks_xml(networks) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + 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) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + self.assertEquals(response.status_int, 202) + self.assertEquals(compute_api.networks, + [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)]) + + 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"', '') + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + self.assertEquals(response.status_int, 202) + self.assertEquals(compute_api.networks, + [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)]) + + def test_create_instance_with_userdata(self): + user_data_contents = '#!/bin/bash\necho "Oh no!"\n' + user_data_contents = base64.b64encode(user_data_contents) + request, response, user_data = \ + self._create_instance_with_user_data_json(user_data_contents) + self.assertEquals(response.status_int, 202) + self.assertEquals(user_data, user_data_contents) + + def test_create_instance_with_userdata_none(self): + user_data_contents = None + request, response, user_data = \ + self._create_instance_with_user_data_json(user_data_contents) + self.assertEquals(response.status_int, 202) + self.assertEquals(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' + request, response, user_data = \ + self._create_instance_with_user_data_json(user_data_contents) + self.assertEquals(response.status_int, 400) + self.assertEquals(user_data, None) + + def test_create_instance_with_security_group_json(self): + security_groups = ['test', 'test1'] + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_get_by_name) + self.stubs.Set(nova.db, 'instance_add_security_group', + return_instance_add_security_group) + self._setup_mock_network_api() + body_dict = self._create_security_group_request_dict(security_groups) + request = self._get_create_request_json(body_dict) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + self.assertEquals(response.status_int, 202) + + def test_get_server_by_id_verify_security_groups_json(self): + self.stubs.Set(nova.db, 'instance_get', return_server_by_id) + self._setup_mock_network_api() + req = webob.Request.blank('/v2/fake/os-create-server-ext/1') + req.headers['Content-Type'] = 'application/json' + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 200) + res_dict = json.loads(response.body) + expected_security_group = [{"name": "test"}] + self.assertEquals(res_dict['server']['security_groups'], + expected_security_group) + + def test_get_server_by_id_verify_security_groups_xml(self): + self.stubs.Set(nova.db, 'instance_get', return_server_by_id) + self._setup_mock_network_api() + req = webob.Request.blank('/v2/fake/os-create-server-ext/1') + req.headers['Accept'] = 'application/xml' + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(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(INSTANCE['security_groups'][0]['name'], + sec_group.getAttribute("name")) diff --git a/nova/tests/api/openstack/compute/contrib/test_deferred_delete.py b/nova/tests/api/openstack/compute/contrib/test_deferred_delete.py new file mode 100644 index 000000000..4addcf5c8 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_deferred_delete.py @@ -0,0 +1,120 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mox +import webob + +from nova.api.openstack.compute.contrib import deferred_delete +from nova import compute +from nova import exception +from nova import test + + +class FakeExtensionDescriptor(object): + def register(*args, **kwargs): + pass + + +class FakeRequest(object): + def __init__(self, context): + self.environ = {'nova.context': context} + + +class DeferredDeleteExtensionTest(test.TestCase): + def setUp(self): + super(DeferredDeleteExtensionTest, self).setUp() + self.extension = deferred_delete.Deferred_delete( + FakeExtensionDescriptor()) + self.fake_input_dict = {} + self.fake_uuid = 'fake_uuid' + self.fake_context = 'fake_context' + self.fake_req = FakeRequest(self.fake_context) + + def test_force_delete(self): + self.mox.StubOutWithMock(compute.API, 'get') + self.mox.StubOutWithMock(compute.API, 'force_delete') + + fake_instance = 'fake_instance' + + compute.API.get(self.fake_context, self.fake_uuid).AndReturn( + fake_instance) + compute.API.force_delete(self.fake_context, fake_instance) + + self.mox.ReplayAll() + res = self.extension._force_delete(self.fake_input_dict, + self.fake_req, self.fake_uuid) + self.mox.VerifyAll() + self.assertEqual(res.status_int, 202) + + def test_force_delete_raises_conflict_on_invalid_state(self): + self.mox.StubOutWithMock(compute.API, 'get') + self.mox.StubOutWithMock(compute.API, 'force_delete') + + fake_instance = 'fake_instance' + + compute.API.get(self.fake_context, self.fake_uuid).AndReturn( + fake_instance) + compute.API.force_delete(self.fake_context, fake_instance).AndRaise( + exception.InstanceInvalidState) + + self.mox.ReplayAll() + self.assertRaises(webob.exc.HTTPConflict, + self.extension._force_delete, self.fake_input_dict, + self.fake_req, self.fake_uuid) + self.mox.VerifyAll() + + def test_restore(self): + self.mox.StubOutWithMock(compute.API, 'get') + self.mox.StubOutWithMock(compute.API, 'restore') + + fake_instance = 'fake_instance' + + compute.API.get(self.fake_context, self.fake_uuid).AndReturn( + fake_instance) + compute.API.restore(self.fake_context, fake_instance) + + self.mox.ReplayAll() + res = self.extension._restore(self.fake_input_dict, + self.fake_req, self.fake_uuid) + self.mox.VerifyAll() + self.assertEqual(res.status_int, 202) + + def test_restore_raises_conflict_on_invalid_state(self): + self.mox.StubOutWithMock(compute.API, 'get') + self.mox.StubOutWithMock(compute.API, 'restore') + + fake_instance = 'fake_instance' + + compute.API.get(self.fake_context, self.fake_uuid).AndReturn( + fake_instance) + compute.API.restore(self.fake_context, fake_instance).AndRaise( + exception.InstanceInvalidState) + + self.mox.ReplayAll() + self.assertRaises(webob.exc.HTTPConflict, self.extension._restore, + self.fake_input_dict, self.fake_req, self.fake_uuid) + self.mox.VerifyAll() + + def test_get_actions(self): + result = self.extension.get_actions() + self.assertEqual(len(result), 2) + + action_and_methods = [(x.action_name, x.handler) for x in result] + self.assertIn(('restore', self.extension._restore), + action_and_methods) + self.assertIn(('forceDelete', self.extension._force_delete), + action_and_methods) diff --git a/nova/tests/api/openstack/compute/contrib/test_disk_config.py b/nova/tests/api/openstack/compute/contrib/test_disk_config.py new file mode 100644 index 000000000..bd995fd3d --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_disk_config.py @@ -0,0 +1,252 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from nova.api.openstack import compute +from nova.api.openstack.compute import extensions +from nova.api.openstack import wsgi +import nova.db.api +from nova import flags +import nova.rpc +from nova import test +from nova.tests.api.openstack import fakes +from nova import utils + + +MANUAL_INSTANCE_UUID = fakes.FAKE_UUID +AUTO_INSTANCE_UUID = fakes.FAKE_UUID.replace('a', 'b') + +stub_instance = fakes.stub_instance +FLAGS = flags.FLAGS + + +def instance_addresses(context, instance_id): + return None + + +class DiskConfigTestCase(test.TestCase): + + def setUp(self): + super(DiskConfigTestCase, self).setUp() + self.flags(verbose=True) + 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(nova.db, 'instance_get', fake_instance_get) + + def fake_instance_get_by_uuid(context, uuid): + for instance in FAKE_INSTANCES: + if uuid == instance['uuid']: + return instance + + self.stubs.Set(nova.db, 'instance_get_by_uuid', + fake_instance_get_by_uuid) + + def fake_instance_get_all(context, *args, **kwargs): + return FAKE_INSTANCES + + self.stubs.Set(nova.db, 'instance_get_all', fake_instance_get_all) + self.stubs.Set(nova.db, 'instance_get_all_by_filters', + fake_instance_get_all) + + def fake_instance_create(context, inst_, session=None): + class FakeModel(dict): + def save(self, session=None): + pass + + inst = FakeModel(**inst_) + inst['id'] = 1 + inst['uuid'] = AUTO_INSTANCE_UUID + inst['created_at'] = datetime.datetime(2010, 10, 10, 12, 0, 0) + inst['updated_at'] = datetime.datetime(2010, 10, 10, 12, 0, 0) + inst['progress'] = 0 + inst['name'] = 'instance-1' # this is a property + + def fake_instance_get_for_create(context, id_, *args, **kwargs): + return inst + + self.stubs.Set(nova.db, 'instance_get', + fake_instance_get_for_create) + self.stubs.Set(nova.db, 'instance_update', + fake_instance_get_for_create) + + def fake_instance_get_all_for_create(context, *args, **kwargs): + return [inst] + self.stubs.Set(nova.db, 'instance_get_all', + fake_instance_get_all_for_create) + self.stubs.Set(nova.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(nova.db, + 'instance_add_security_group', + fake_instance_add_security_group) + + return inst + + self.stubs.Set(nova.db, 'instance_create', fake_instance_create) + + app = compute.APIRouter() + app = extensions.ExtensionMiddleware(app) + app = wsgi.LazySerializationMiddleware(app) + self.app = app + + def assertDiskConfig(self, dict_, value): + self.assert_('RAX-DCF:diskConfig' in dict_) + self.assertEqual(dict_['RAX-DCF:diskConfig'], value) + + def test_show_server(self): + req = fakes.HTTPRequest.blank( + '/fake/servers/%s' % MANUAL_INSTANCE_UUID) + res = req.get_response(self.app) + server_dict = utils.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 = utils.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 = utils.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 = utils.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 = utils.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 = utils.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', + 'RAX-DCF:diskConfig': 'AUTO' + }} + + req.body = utils.dumps(body) + res = req.get_response(self.app) + server_dict = utils.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', + 'RAX-DCF:diskConfig': 'MANUAL' + }} + + req.body = utils.dumps(body) + res = req.get_response(self.app) + server_dict = utils.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 = utils.dumps(body) + res = req.get_response(self.app) + server_dict = utils.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 = utils.dumps(body) + res = req.get_response(self.app) + server_dict = utils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'AUTO') + + 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': {'RAX-DCF:diskConfig': 'server_test'}} + req.body = utils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + expected_msg = '{"badRequest": {"message": "RAX-DCF:diskConfig must'\ + ' be either \'MANUAL\' or \'AUTO\'.", "code": 400}}' + self.assertEqual(res.body, expected_msg) diff --git a/nova/tests/api/openstack/compute/contrib/test_extendedstatus.py b/nova/tests/api/openstack/compute/contrib/test_extendedstatus.py new file mode 100644 index 000000000..dc7f0cefa --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_extendedstatus.py @@ -0,0 +1,76 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import webob + +from nova import compute +from nova import exception +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS +FLAGS.verbose = True + + +def fake_compute_get(*args, **kwargs): + return fakes.stub_instance(1, task_state="kayaking", + vm_state="slightly crunchy", + power_state="empowered") + + +class ExtendedStatusTest(test.TestCase): + + def setUp(self): + super(ExtendedStatusTest, self).setUp() + self.uuid = '70f6db34-de8d-4fbd-aafb-4065bdfa6114' + self.url = '/v2/fake/servers/%s' % self.uuid + fakes.stub_out_nw_api(self.stubs) + self.flags(allow_admin_api=True) + self.stubs.Set(compute.api.API, 'routing_get', fake_compute_get) + + def _make_request(self): + req = webob.Request.blank(self.url) + req.headers['Accept'] = 'application/json' + res = req.get_response(fakes.wsgi_app()) + return res + + def assertServerStates(self, server, vm_state, power_state, task_state): + self.assertEqual(server.get('OS-EXT-STS:vm_state'), vm_state) + self.assertEqual(server.get('OS-EXT-STS:power_state'), power_state) + self.assertEqual(server.get('OS-EXT-STS:task_state'), task_state) + + def test_extended_status(self): + res = self._make_request() + body = json.loads(res.body) + + self.assertEqual(res.status_int, 200) + self.assertServerStates(body['server'], + vm_state='slightly crunchy', + power_state='empowered', + task_state='kayaking') + + def test_extended_status_no_instance_fails(self): + + def fake_compute_get(*args, **kwargs): + raise exception.InstanceNotFound() + + self.stubs.Set(compute.api.API, 'routing_get', fake_compute_get) + res = self._make_request() + + self.assertEqual(res.status_int, 404) diff --git a/nova/tests/api/openstack/compute/contrib/test_flavors_extra_specs.py b/nova/tests/api/openstack/compute/contrib/test_flavors_extra_specs.py new file mode 100644 index 000000000..94aac9621 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_flavors_extra_specs.py @@ -0,0 +1,184 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 webob + +from nova.api.openstack import wsgi +from nova.api.openstack.compute.contrib import flavorextraspecs +from nova import test +from nova.tests.api.openstack import fakes +import nova.wsgi + + +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_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 FlavorsExtraSpecsTest(test.TestCase): + + def setUp(self): + super(FlavorsExtraSpecsTest, self).setUp() + fakes.stub_out_key_pair_funcs(self.stubs) + self.controller = flavorextraspecs.FlavorExtraSpecsController() + + def test_index(self): + self.stubs.Set(nova.db, 'instance_type_extra_specs_get', + return_flavor_extra_specs) + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs') + 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, 'instance_type_extra_specs_get', + return_empty_flavor_extra_specs) + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs') + res_dict = self.controller.index(req, 1) + + self.assertEqual(0, len(res_dict['extra_specs'])) + + def test_show(self): + self.stubs.Set(nova.db, 'instance_type_extra_specs_get', + return_flavor_extra_specs) + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + + '/key5') + 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, 'instance_type_extra_specs_get', + return_empty_flavor_extra_specs) + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + + '/key6') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, 1, 'key6') + + def test_delete(self): + self.stubs.Set(nova.db, 'instance_type_extra_specs_delete', + delete_flavor_extra_specs) + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + + '/key5') + self.controller.delete(req, 1, 'key5') + + def test_create(self): + self.stubs.Set(nova.db, + 'instance_type_extra_specs_update_or_create', + return_create_flavor_extra_specs) + body = {"extra_specs": {"key1": "value1"}} + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs') + res_dict = self.controller.create(req, 1, body) + + self.assertEqual('value1', res_dict['extra_specs']['key1']) + + def test_create_empty_body(self): + self.stubs.Set(nova.db, + 'instance_type_extra_specs_update_or_create', + return_create_flavor_extra_specs) + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, 1, '') + + def test_update_item(self): + self.stubs.Set(nova.db, + 'instance_type_extra_specs_update_or_create', + return_create_flavor_extra_specs) + body = {"key1": "value1"} + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + + '/key1') + res_dict = self.controller.update(req, 1, 'key1', body) + + self.assertEqual('value1', res_dict['key1']) + + def test_update_item_empty_body(self): + self.stubs.Set(nova.db, + 'instance_type_extra_specs_update_or_create', + return_create_flavor_extra_specs) + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + + '/key1') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'key1', '') + + def test_update_item_too_many_keys(self): + self.stubs.Set(nova.db, + 'instance_type_extra_specs_update_or_create', + return_create_flavor_extra_specs) + body = {"key1": "value1", "key2": "value2"} + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + + '/key1') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'key1', body) + + def test_update_item_body_uri_mismatch(self): + self.stubs.Set(nova.db, + 'instance_type_extra_specs_update_or_create', + return_create_flavor_extra_specs) + body = {"key1": "value1"} + + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs/bad') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'bad', body) + + +class FlavorsExtraSpecsXMLSerializerTest(test.TestCase): + def test_serializer(self): + serializer = flavorextraspecs.ExtraSpecsTemplate() + expected = ("\n" + 'value1') + text = serializer.serialize(dict(extra_specs={"key1": "value1"})) + print text + self.assertEqual(text, expected) + + def test_deserializer(self): + deserializer = wsgi.XMLDeserializer() + expected = dict(extra_specs={"key1": "value1"}) + intext = ("\n" + 'value1') + result = deserializer.deserialize(intext)['body'] + self.assertEqual(result, expected) diff --git a/nova/tests/api/openstack/compute/contrib/test_floating_ip_dns.py b/nova/tests/api/openstack/compute/contrib/test_floating_ip_dns.py new file mode 100644 index 000000000..3107e50c4 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_floating_ip_dns.py @@ -0,0 +1,260 @@ +# 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 sys + +from lxml import etree +import webob +import urllib + +from nova.api.openstack.compute.contrib import floating_ips +from nova.api.openstack.compute.contrib import floating_ip_dns +from nova import context +from nova import db +from nova import network +from nova import rpc +from nova import test +from nova.tests.api.openstack import fakes +from nova import utils + + +name = "arbitraryname" +name2 = "anotherarbitraryname" + +testaddress = '10.0.0.66' +testaddress2 = '10.0.0.67' + +zone = "example.org" +zone2 = "example.net" +floating_ip_id = '1' + + +def _quote_zone(zone): + """ + Zone 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(zone.replace('.', '%2E')) + + +def network_api_get_floating_ip(self, context, id): + return {'id': floating_ip_id, 'address': testaddress, + 'fixed_ip': None} + + +def network_get_dns_zones(self, context): + return ['foo', 'bar', 'baz', 'quux'] + + +def network_get_dns_entries_by_address(self, context, address, zone): + return [name, name2] + + +def network_get_dns_entries_by_name(self, context, address, zone): + return [testaddress, testaddress2] + + +def network_add_dns_entry(self, context, address, name, dns_type, zone): + return {'dns_entry': {'ip': testaddress, + 'name': name, + 'type': dns_type, + 'zone': zone}} + + +def network_modify_dns_entry(self, context, address, name, zone): + return {'dns_entry': {'name': name, + 'ip': address, + 'zone': zone}} + + +class FloatingIpDNSTest(test.TestCase): + def _create_floating_ip(self): + """Create a floating ip object.""" + host = "fake_host" + return db.floating_ip_create(self.context, + {'address': testaddress, + 'host': host}) + + def _delete_floating_ip(self): + db.floating_ip_destroy(self.context, testaddress) + + def setUp(self): + super(FloatingIpDNSTest, self).setUp() + self.stubs.Set(network.api.API, "get_dns_zones", + network_get_dns_zones) + 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.context = context.get_admin_context() + + self._create_floating_ip() + self.dns_controller = floating_ip_dns.FloatingIPDNSController() + + def tearDown(self): + self._delete_floating_ip() + super(FloatingIpDNSTest, self).tearDown() + + def test_dns_zones_list(self): + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns') + res_dict = self.dns_controller.index(req) + entries = res_dict['zones'] + self.assertTrue(entries) + self.assertEqual(entries[0]['zone'], "foo") + self.assertEqual(entries[1]['zone'], "bar") + self.assertEqual(entries[2]['zone'], "baz") + self.assertEqual(entries[3]['zone'], "quux") + + def test_get_dns_entries_by_address(self): + qparams = {'ip': testaddress} + params = "?%s" % urllib.urlencode(qparams) if qparams else "" + + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s%s' % + (_quote_zone(zone), params)) + entries = self.dns_controller.show(req, _quote_zone(zone)) + + 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]['zone'], + zone) + + def test_get_dns_entries_by_name(self): + qparams = {'name': name} + params = "?%s" % urllib.urlencode(qparams) if qparams else "" + + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s%s' % + (_quote_zone(zone), params)) + entries = self.dns_controller.show(req, _quote_zone(zone)) + + self.assertEqual(len(entries['dns_entries']), 2) + self.assertEqual(entries['dns_entries'][0]['ip'], + testaddress) + self.assertEqual(entries['dns_entries'][1]['ip'], + testaddress2) + self.assertEqual(entries['dns_entries'][0]['zone'], + zone) + + def test_create(self): + body = {'dns_entry': + {'name': name, + 'ip': testaddress, + 'dns_type': 'A', + 'zone': zone}} + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns') + entry = self.dns_controller.create(req, body) + + self.assertEqual(entry['dns_entry']['ip'], testaddress) + + def test_delete(self): + self.called = False + self.deleted_zone = "" + self.deleted_name = "" + + def network_delete_dns_entry(fakeself, context, req, id): + self.called = True + self.deleted_zone = id + + self.stubs.Set(network.api.API, "delete_dns_entry", + network_delete_dns_entry) + + qparams = {'name': name} + params = "?%s" % urllib.urlencode(qparams) if qparams else "" + + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s%s' % + (_quote_zone(zone), params)) + entries = self.dns_controller.delete(req, _quote_zone(zone)) + + self.assertTrue(self.called) + self.assertEquals(self.deleted_zone, zone) + + def test_modify(self): + body = {'dns_entry': + {'name': name, + 'ip': testaddress2}} + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' % + zone) + entry = self.dns_controller.update(req, zone, body) + + self.assertEqual(entry['dns_entry']['ip'], testaddress2) + + +class FloatingIpDNSSerializerTest(test.TestCase): + def test_default_serializer(self): + serializer = floating_ip_dns.FloatingIPDNSTemplate() + text = serializer.serialize(dict( + dns_entry=dict( + ip=testaddress, + type='A', + zone=zone, + name=name))) + + tree = etree.fromstring(text) + + self.assertEqual('dns_entry', tree.tag) + self.assertEqual(testaddress, tree.get('ip')) + self.assertEqual(zone, tree.get('zone')) + self.assertEqual(name, tree.get('name')) + + def test_index_serializer(self): + serializer = floating_ip_dns.ZonesTemplate() + text = serializer.serialize(dict( + zones=[ + dict(zone=zone), + dict(zone=zone2)])) + + tree = etree.fromstring(text) + self.assertEqual('zones', tree.tag) + self.assertEqual(2, len(tree)) + self.assertEqual(zone, tree[0].get('zone')) + self.assertEqual(zone2, tree[1].get('zone')) + + def test_show_serializer(self): + serializer = floating_ip_dns.FloatingIPDNSsTemplate() + text = serializer.serialize(dict( + dns_entries=[ + dict(ip=testaddress, + type='A', + zone=zone, + name=name), + dict(ip=testaddress2, + type='C', + zone=zone, + 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(testaddress, tree[0].get('ip')) + self.assertEqual('A', tree[0].get('type')) + self.assertEqual(zone, tree[0].get('zone')) + self.assertEqual(name, tree[0].get('name')) + self.assertEqual(testaddress2, tree[1].get('ip')) + self.assertEqual('C', tree[1].get('type')) + self.assertEqual(zone, tree[1].get('zone')) + self.assertEqual(name2, tree[1].get('name')) diff --git a/nova/tests/api/openstack/compute/contrib/test_floating_ip_pools.py b/nova/tests/api/openstack/compute/contrib/test_floating_ip_pools.py new file mode 100644 index 000000000..ffe84a1ba --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_floating_ip_pools.py @@ -0,0 +1,73 @@ +# 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 +from nova import context +from nova import network +from nova import test +from nova.tests.api.openstack import fakes + + +def fake_get_floating_ip_pools(self, context): + return [{'name': 'nova'}, + {'name': 'other'}] + + +class FloatingIpPoolTest(test.TestCase): + def setUp(self): + super(FloatingIpPoolTest, 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 = floating_ip_pools.FloatingIPPoolsController() + + def test_translate_floating_ip_pools_view(self): + pools = fake_get_floating_ip_pools(None, self.context) + view = floating_ip_pools._translate_floating_ip_pools_view(pools) + self.assertTrue('floating_ip_pools' in view) + self.assertEqual(view['floating_ip_pools'][0]['name'], + pools[0]['name']) + self.assertEqual(view['floating_ip_pools'][1]['name'], + pools[1]['name']) + + def test_floating_ips_pools_list(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ip-pools') + res_dict = self.controller.index(req) + + pools = fake_get_floating_ip_pools(None, self.context) + response = {'floating_ip_pools': pools} + self.assertEqual(res_dict, response) + + +class FloatingIpPoolSerializerTest(test.TestCase): + def test_index_serializer(self): + serializer = floating_ip_pools.FloatingIPPoolsSerializer() + text = serializer.serialize(dict( + floating_ip_pools=[ + dict(name='nova'), + dict(name='other') + ]), 'index') + + 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/api/openstack/compute/contrib/test_floating_ips.py b/nova/tests/api/openstack/compute/contrib/test_floating_ips.py new file mode 100644 index 000000000..fe5444419 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_floating_ips.py @@ -0,0 +1,329 @@ +# 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. + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import floating_ips +from nova import context +from nova import db +from nova import network +from nova import rpc +from nova import test +from nova.tests.api.openstack import fakes +from nova import utils + +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': None} + + +def network_api_get_floating_ip_by_address(self, context, address): + return {'id': 1, 'address': '10.10.10.10', + 'pool': 'nova', + 'fixed_ip': {'address': '10.0.0.1', + 'instance': {'uuid': FAKE_UUID}}} + + +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}}}, + {'id': 2, + 'pool': 'nova', 'interface': 'eth0', + 'address': '10.10.10.11'}] + + +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, floating_address): + pass + + +def network_get_instance_nw_info(self, context, instance): + info = { + 'label': 'fake', + 'gateway': 'fake', + 'dhcp_server': 'fake', + 'broadcast': 'fake', + 'mac': 'fake', + 'vif_uuid': 'fake', + 'rxtx_cap': 'fake', + 'dns': [], + 'ips': [{'ip': '10.0.0.1'}], + 'should_create_bridge': False, + 'should_create_vlan': False} + + return [['ignore', info]] + + +def fake_instance_get(context, instance_id): + return { + "id": 1, + "uuid": utils.gen_uuid(), + "name": 'fake', + "user_id": 'fakeuser', + "project_id": '123'} + + +class StubExtensionManager(object): + def register(self, *args): + pass + + +class FloatingIpTest(test.TestCase): + floating_ip = "10.10.10.10" + + def _create_floating_ip(self): + """Create a floating ip object.""" + host = "fake_host" + return db.floating_ip_create(self.context, + {'address': self.floating_ip, + 'pool': 'nova', + 'host': host}) + + def _delete_floating_ip(self): + db.floating_ip_destroy(self.context, self.floating_ip) + + def setUp(self): + super(FloatingIpTest, self).setUp() + 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_nw_info", + network_get_instance_nw_info) + self.stubs.Set(db, 'instance_get', + fake_instance_get) + + self.context = context.get_admin_context() + self._create_floating_ip() + + self.controller = floating_ips.FloatingIPController() + self.manager = floating_ips.Floating_ips(StubExtensionManager()) + + def tearDown(self): + self._delete_floating_ip() + super(FloatingIpTest, self).tearDown() + + def test_translate_floating_ip_view(self): + floating_ip_address = self._create_floating_ip() + floating_ip = db.floating_ip_get_by_address(self.context, + floating_ip_address) + view = floating_ips._translate_floating_ip_view(floating_ip) + self.assertTrue('floating_ip' in view) + self.assertTrue(view['floating_ip']['id']) + self.assertEqual(view['floating_ip']['ip'], self.floating_ip) + self.assertEqual(view['floating_ip']['fixed_ip'], None) + self.assertEqual(view['floating_ip']['instance_id'], None) + + 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.assertTrue('floating_ip' in view) + + def test_floating_ips_list(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips') + 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_show(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/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']['instance_id'], None) + + 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}}} + self.stubs.Set(network.api.API, "get_floating_ip", get_floating_ip) + + req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/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']['instance_id'], FAKE_UUID) + +# test floating ip allocate/release(deallocate) + def test_floating_ip_allocate_no_free_ips(self): + def fake_call(*args, **kwargs): + raise(rpc.RemoteError('NoMoreFloatingIps', '', '')) + + self.stubs.Set(rpc, "call", fake_call) + + req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req) + + 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 = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips') + 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 = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/1') + self.controller.delete(req, 1) + +# test floating ip add/remove -> associate/disassociate + + def test_floating_ip_associate(self): + body = dict(addFloatingIp=dict(address=self.floating_ip)) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + self.manager._add_floating_ip(body, req, 'test_inst') + + def test_floating_ip_disassociate(self): + body = dict(removeFloatingIp=dict(address='10.10.10.10')) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + self.manager._remove_floating_ip(body, req, 'test_inst') + +# 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 = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._add_floating_ip, body, req, + 'test_inst') + + def test_missing_dict_param_in_remove_floating_ip(self): + body = dict(removeFloatingIp='11.0.0.1') + + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._remove_floating_ip, body, req, + 'test_inst') + + def test_missing_dict_param_in_add_floating_ip(self): + body = dict(addFloatingIp='11.0.0.1') + + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._add_floating_ip, body, req, + 'test_inst') + + +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/api/openstack/compute/contrib/test_hosts.py b/nova/tests/api/openstack/compute/contrib/test_hosts.py new file mode 100644 index 000000000..e6a91477e --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_hosts.py @@ -0,0 +1,174 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from lxml import etree +import webob.exc + +from nova import context +from nova import exception +from nova import flags +from nova import log as logging +from nova import test +from nova.api.openstack.compute.contrib import hosts as os_hosts +from nova.scheduler import api as scheduler_api + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.tests.hosts') +# Simulate the hosts returned by the zone manager. +HOST_LIST = [ + {"host_name": "host_c1", "service": "compute"}, + {"host_name": "host_c2", "service": "compute"}, + {"host_name": "host_v1", "service": "volume"}, + {"host_name": "host_v2", "service": "volume"}] + + +def stub_get_host_list(req): + return HOST_LIST + + +def stub_set_host_enabled(context, host, enabled): + # We'll simulate success and failure by assuming + # that 'host_c1' always succeeds, and 'host_c2' + # always fails + fail = (host == "host_c2") + status = "enabled" if (enabled ^ fail) else "disabled" + return status + + +def stub_host_power_action(context, host, action): + return action + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + + +class HostTestCase(test.TestCase): + """Test Case for hosts.""" + + def setUp(self): + super(HostTestCase, self).setUp() + self.controller = os_hosts.HostController() + self.req = FakeRequest() + self.stubs.Set(scheduler_api, 'get_host_list', stub_get_host_list) + self.stubs.Set(self.controller.compute_api, 'set_host_enabled', + stub_set_host_enabled) + self.stubs.Set(self.controller.compute_api, 'host_power_action', + stub_host_power_action) + + def test_list_hosts(self): + """Verify that the compute hosts are returned.""" + hosts = os_hosts._list_hosts(self.req) + self.assertEqual(hosts, HOST_LIST) + + compute_hosts = os_hosts._list_hosts(self.req, "compute") + expected = [host for host in HOST_LIST + if host["service"] == "compute"] + self.assertEqual(compute_hosts, expected) + + def test_disable_host(self): + dis_body = {"status": "disable"} + result_c1 = self.controller.update(self.req, "host_c1", body=dis_body) + self.assertEqual(result_c1["status"], "disabled") + result_c2 = self.controller.update(self.req, "host_c2", body=dis_body) + self.assertEqual(result_c2["status"], "enabled") + + def test_enable_host(self): + en_body = {"status": "enable"} + result_c1 = self.controller.update(self.req, "host_c1", body=en_body) + self.assertEqual(result_c1["status"], "enabled") + result_c2 = self.controller.update(self.req, "host_c2", body=en_body) + self.assertEqual(result_c2["status"], "disabled") + + def test_host_startup(self): + self.flags(allow_admin_api=True) + result = self.controller.startup(self.req, "host_c1") + self.assertEqual(result["power_action"], "startup") + + def test_host_shutdown(self): + self.flags(allow_admin_api=True) + result = self.controller.shutdown(self.req, "host_c1") + self.assertEqual(result["power_action"], "shutdown") + + def test_host_reboot(self): + self.flags(allow_admin_api=True) + result = self.controller.reboot(self.req, "host_c1") + self.assertEqual(result["power_action"], "reboot") + + def test_bad_status_value(self): + bad_body = {"status": "bad"} + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + self.req, "host_c1", body=bad_body) + + def test_bad_update_key(self): + bad_body = {"crazy": "bad"} + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + self.req, "host_c1", body=bad_body) + + def test_bad_host(self): + self.assertRaises(exception.HostNotFound, self.controller.update, + self.req, "bogus_host_name", body={"status": "disable"}) + + +class HostSerializerTest(test.TestCase): + def setUp(self): + super(HostSerializerTest, self).setUp() + self.deserializer = os_hosts.HostDeserializer() + + def test_index_serializer(self): + serializer = os_hosts.HostIndexTemplate() + text = serializer.serialize(HOST_LIST) + + tree = etree.fromstring(text) + + self.assertEqual('hosts', tree.tag) + self.assertEqual(len(HOST_LIST), len(tree)) + for i in range(len(HOST_LIST)): + self.assertEqual('host', tree[i].tag) + self.assertEqual(HOST_LIST[i]['host_name'], + tree[i].get('host_name')) + self.assertEqual(HOST_LIST[i]['service'], + tree[i].get('service')) + + def test_update_serializer(self): + exemplar = dict(host='host_c1', status='enabled') + serializer = os_hosts.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.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', foo='bar') + intext = ("\n" + 'enabledbar') + result = self.deserializer.deserialize(intext) + + self.assertEqual(dict(body=exemplar), result) diff --git a/nova/tests/api/openstack/compute/contrib/test_keypairs.py b/nova/tests/api/openstack/compute/contrib/test_keypairs.py new file mode 100644 index 000000000..4c252e176 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_keypairs.py @@ -0,0 +1,175 @@ +# 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 json + +import webob +from lxml import etree + +from nova.api.openstack import wsgi +from nova.api.openstack.compute.contrib import keypairs +from nova import context +from nova import db +from nova import test +from nova.tests.api.openstack import fakes + + +def fake_keypair(name): + return {'public_key': 'FAKE_KEY', + 'fingerprint': 'FAKE_FINGERPRINT', + 'name': name} + + +def db_key_pair_get_all_by_user(self, user_id): + return [fake_keypair('FAKE')] + + +def db_key_pair_create(self, keypair): + pass + + +def db_key_pair_destroy(context, user_id, name): + if not (user_id and name): + raise Exception() + + +class KeypairsTest(test.TestCase): + + def setUp(self): + super(KeypairsTest, self).setUp() + self.controller = keypairs.KeypairController() + 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.context = context.get_admin_context() + + def test_keypair_list(self): + req = webob.Request.blank('/v2/fake/os-keypairs') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + response = {'keypairs': [{'keypair': fake_keypair('FAKE')}]} + self.assertEqual(res_dict, response) + + def test_keypair_create(self): + body = {'keypair': {'name': 'create_test'}} + req = webob.Request.blank('/v2/fake/os-keypairs') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) + self.assertTrue(len(res_dict['keypair']['private_key']) > 0) + + 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('/v2/fake/os-keypairs') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + # FIXME(ja): sholud we check that public_key was sent to create? + res_dict = json.loads(res.body) + self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) + self.assertFalse('private_key' in res_dict['keypair']) + + def test_keypair_delete(self): + req = webob.Request.blank('/v2/fake/os-keypairs/FAKE') + req.method = 'DELETE' + req.headers['Content-Type'] = 'application/json' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + + +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.KeypairTemplate() + text = serializer.serialize(exemplar) + + print text + tree = etree.fromstring(text) + + self.assertEqual('keypair', tree.tag) + for child in tree: + self.assertTrue(child.tag in 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.KeypairsTemplate() + text = serializer.serialize(exemplar) + + print text + 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.assertTrue(child.tag in 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 = ("\n" + 'key_name' + 'public_key') + + result = self.deserializer.deserialize(intext)['body'] + self.assertEqual(result, exemplar) diff --git a/nova/tests/api/openstack/compute/contrib/test_multinic_xs.py b/nova/tests/api/openstack/compute/contrib/test_multinic_xs.py new file mode 100644 index 000000000..0d9b6e3ce --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_multinic_xs.py @@ -0,0 +1,113 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import webob + +from nova import compute +from nova import context +from nova import test +from nova.tests.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): + return {'id': 1, 'uuid': instance_id} + + +class FixedIpTest(test.TestCase): + def setUp(self): + super(FixedIpTest, 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.context = context.get_admin_context() + + 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('/v2/fake/servers/%s/action' % UUID) + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) + self.assertEqual(last_add_fixed_ip, (UUID, 'test_net')) + + 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('/v2/fake/servers/%s/action' % UUID) + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 422) + self.assertEqual(last_add_fixed_ip, (None, None)) + + 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('/v2/fake/servers/%s/action' % UUID) + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_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('/v2/fake/servers/%s/action' % UUID) + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 422) + self.assertEqual(last_remove_fixed_ip, (None, None)) diff --git a/nova/tests/api/openstack/compute/contrib/test_networks.py b/nova/tests/api/openstack/compute/contrib/test_networks.py new file mode 100644 index 000000000..0eefca652 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_networks.py @@ -0,0 +1,137 @@ +# Copyright 2011 Grid Dynamics +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import webob + +from nova.api.openstack.compute.contrib import networks +from nova import context +from nova import exception +from nova import test +from nova.tests.api.openstack import fakes + + +FAKE_NETWORKS = [ + { + 'bridge': 'br100', 'vpn_public_port': 1000, + 'dhcp_start': '10.0.0.3', 'bridge_interface': 'eth0', + 'updated_at': '2011-08-16 09:26:13.048257', 'id': 1, + 'cidr_v6': None, 'deleted_at': None, + 'gateway': '10.0.0.1', 'label': 'mynet_0', + 'project_id': '1234', + '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, 'host': 'nsokolov-desktop', + 'gateway_v6': None, 'netmask_v6': None, + 'created_at': '2011-08-15 06:19:19.387525', + }, + { + 'bridge': 'br101', 'vpn_public_port': 1001, + 'dhcp_start': '10.0.0.11', 'bridge_interface': 'eth0', + 'updated_at': None, 'id': 2, 'cidr_v6': None, + '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', + 'netmask': '255.255.255.248', 'injected': False, + 'cidr': '10.0.0.10/29', 'vpn_public_address': None, + 'multi_host': False, 'dns1': None, 'host': None, + 'gateway_v6': None, 'netmask_v6': None, + 'created_at': '2011-08-15 06:19:19.885495', + }, +] + + +class FakeNetworkAPI(object): + + def __init__(self): + self.networks = copy.deepcopy(FAKE_NETWORKS) + + def delete(self, context, network_id): + for i, network in enumerate(self.networks): + if network['id'] == network_id: + del self.networks[0] + return True + raise exception.NetworkNotFound() + + #NOTE(bcwaldon): this does nothing other than check for existance + def disassociate(self, context, network_id): + for i, network in enumerate(self.networks): + if network['id'] == network_id: + return True + raise exception.NetworkNotFound() + + def get_all(self, context): + return self.networks + + def get(self, context, network_id): + for network in self.networks: + if network['id'] == network_id: + return network + raise exception.NetworkNotFound() + + +class NetworksTest(test.TestCase): + + def setUp(self): + super(NetworksTest, self).setUp() + self.flags(allow_admin_api=True) + self.fake_network_api = FakeNetworkAPI() + self.controller = networks.NetworkController(self.fake_network_api) + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + self.context = context.RequestContext('user', '1234', is_admin=True) + + def test_network_list_all(self): + req = fakes.HTTPRequest.blank('/v2/1234/os-networks') + res_dict = self.controller.index(req) + self.assertEquals(res_dict, {'networks': FAKE_NETWORKS}) + + def test_network_disassociate(self): + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/1/action') + res = self.controller.action(req, 1, {'disassociate': None}) + self.assertEqual(res.status_int, 202) + + def test_network_disassociate_not_found(self): + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/100/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.action, + req, 100, {'disassociate': None}) + + def test_network_get(self): + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/1') + res_dict = self.controller.show(req, 1) + expected = {'network': FAKE_NETWORKS[0]} + self.assertEqual(res_dict, expected) + + def test_network_get_not_found(self): + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/100') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, 100) + + def test_network_delete(self): + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/1') + res = self.controller.delete(req, 1) + self.assertEqual(res.status_int, 202) + + def test_network_delete_not_found(self): + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/100') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, 100) diff --git a/nova/tests/api/openstack/compute/contrib/test_quotas.py b/nova/tests/api/openstack/compute/contrib/test_quotas.py new file mode 100644 index 000000000..29b01e117 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_quotas.py @@ -0,0 +1,193 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob +from lxml import etree + +from nova.api.openstack import wsgi +from nova.api.openstack.compute.contrib import quotas +from nova import context +from nova import test +from nova.tests.api.openstack import fakes + + +def quota_set(id): + return {'quota_set': {'id': id, 'metadata_items': 128, 'volumes': 10, + 'gigabytes': 1000, 'ram': 51200, 'floating_ips': 10, + 'instances': 10, 'injected_files': 5, 'cores': 20, + 'injected_file_content_bytes': 10240}} + + +def quota_set_list(): + return {'quota_set_list': [quota_set('1234'), quota_set('5678'), + quota_set('update_me')]} + + +class QuotaSetsTest(test.TestCase): + + def setUp(self): + super(QuotaSetsTest, self).setUp() + self.controller = quotas.QuotaSetsController() + self.user_id = 'fake' + self.project_id = 'fake' + self.user_context = context.RequestContext(self.user_id, + self.project_id) + self.admin_context = context.RequestContext(self.user_id, + self.project_id, + is_admin=True) + + def test_format_quota_set(self): + raw_quota_set = { + 'instances': 10, + 'cores': 20, + 'ram': 51200, + 'volumes': 10, + 'floating_ips': 10, + 'metadata_items': 128, + 'gigabytes': 1000, + 'injected_files': 5, + 'injected_file_content_bytes': 10240} + + quota_set = quotas.QuotaSetsController()._format_quota_set('1234', + raw_quota_set) + 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['volumes'], 10) + self.assertEqual(qs['gigabytes'], 1000) + self.assertEqual(qs['floating_ips'], 10) + self.assertEqual(qs['metadata_items'], 128) + self.assertEqual(qs['injected_files'], 5) + self.assertEqual(qs['injected_file_content_bytes'], 10240) + + 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') + + expected = {'quota_set': { + 'id': 'fake_tenant', + 'instances': 10, + 'cores': 20, + 'ram': 51200, + 'volumes': 10, + 'gigabytes': 1000, + 'floating_ips': 10, + 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_content_bytes': 10240}} + + self.assertEqual(res_dict, expected) + + def test_quotas_show_as_admin(self): + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234', + use_admin_context=True) + res_dict = self.controller.show(req, 1234) + + self.assertEqual(res_dict, quota_set('1234')) + + def test_quotas_show_as_unauthorized_user(self): + 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): + body = {'quota_set': {'instances': 50, 'cores': 50, + 'ram': 51200, 'volumes': 10, + 'gigabytes': 1000, 'floating_ips': 10, + 'metadata_items': 128, 'injected_files': 5, + 'injected_file_content_bytes': 10240}} + + 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, body) + + def test_quotas_update_as_user(self): + body = {'quota_set': {'instances': 50, 'cores': 50, + 'ram': 51200, 'volumes': 10, + 'gigabytes': 1000, 'floating_ips': 10, + 'metadata_items': 128, 'injected_files': 5, + 'injected_file_content_bytes': 10240}} + + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, + req, 'update_me', body) + + +class QuotaXMLSerializerTest(test.TestCase): + def setUp(self): + super(QuotaXMLSerializerTest, self).setUp() + self.serializer = quotas.QuotaTemplate() + self.deserializer = wsgi.XMLDeserializer() + + def test_serializer(self): + exemplar = dict(quota_set=dict( + id='project_id', + metadata_items=10, + injected_file_content_bytes=20, + volumes=30, + gigabytes=40, + ram=50, + floating_ips=60, + instances=70, + injected_files=80, + cores=90)) + text = self.serializer.serialize(exemplar) + + print text + 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.assertTrue(child.tag in 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', + volumes='30', + gigabytes='40', + ram='50', + floating_ips='60', + instances='70', + injected_files='80', + cores='90')) + intext = ("\n" + '' + '10' + '20' + '' + '30' + '40' + '50' + '60' + '70' + '80' + '90' + '') + + result = self.deserializer.deserialize(intext)['body'] + self.assertEqual(result, exemplar) diff --git a/nova/tests/api/openstack/compute/contrib/test_rescue.py b/nova/tests/api/openstack/compute/contrib/test_rescue.py new file mode 100644 index 000000000..9a3706173 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_rescue.py @@ -0,0 +1,79 @@ +# Copyright 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import webob + +from nova import compute +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes + +FLAGS = flags.FLAGS + + +def rescue(self, context, instance, rescue_password=None): + pass + + +def unrescue(self, context, instance): + pass + + +class RescueTest(test.TestCase): + def setUp(self): + super(RescueTest, self).setUp() + + def fake_compute_get(*args, **kwargs): + uuid = '70f6db34-de8d-4fbd-aafb-4065bdfa6114' + return {'id': 1, 'uuid': uuid} + + 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) + + def test_rescue_with_preset_password(self): + body = {"rescue": {"adminPass": "AABBCC112233"}} + req = webob.Request.blank('/v2/fake/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + resp_json = json.loads(resp.body) + self.assertEqual("AABBCC112233", resp_json['adminPass']) + + def test_rescue_generates_password(self): + body = dict(rescue=None) + req = webob.Request.blank('/v2/fake/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + resp_json = json.loads(resp.body) + self.assertEqual(FLAGS.password_length, len(resp_json['adminPass'])) + + def test_unrescue(self): + body = dict(unrescue=None) + req = webob.Request.blank('/v2/fake/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) diff --git a/nova/tests/api/openstack/compute/contrib/test_security_groups.py b/nova/tests/api/openstack/compute/contrib/test_security_groups.py new file mode 100644 index 000000000..e4ea99f40 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_security_groups.py @@ -0,0 +1,1013 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# +# 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 unittest + +from lxml import etree +import mox +import webob + +from nova.api.openstack.compute.contrib import security_groups +from nova.api.openstack import wsgi +import nova.db +from nova import exception +from nova import utils +from nova import test +from nova.tests.api.openstack import fakes + + +FAKE_UUID = 'a47ae74e-ab08-447f-8eee-ffd43fc46c16' + + +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): + return {'id': int(server_id), + 'power_state': 0x01, + 'host': "localhost", + 'uuid': FAKE_UUID, + 'name': 'asdf'} + + +def return_server_by_uuid(context, server_uuid): + return {'id': 1, + 'power_state': 0x01, + 'host': "localhost", + 'uuid': server_uuid, + 'name': 'asdf'} + + +def return_non_running_server(context, server_id): + return {'id': server_id, 'power_state': 0x02, 'uuid': FAKE_UUID, + '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_UUID}]} + + +def return_security_group_without_instances(context, project_id, group_name): + return {'id': 1, 'name': group_name} + + +def return_server_nonexistent(context, server_id): + raise exception.InstanceNotFound(instance_id=server_id) + + +class StubExtensionManager(object): + def register(self, *args): + pass + + +class TestSecurityGroups(test.TestCase): + def setUp(self): + super(TestSecurityGroups, self).setUp() + + self.controller = security_groups.SecurityGroupController() + self.manager = security_groups.Security_groups(StubExtensionManager()) + + def tearDown(self): + super(TestSecurityGroups, self).tearDown() + + 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.HTTPUnprocessableEntity, + self.controller.create, req, sg) + + 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}) + + 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}) + + 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}) + + 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}) + + 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}) + + 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}) + + def test_create_security_group_with_no_body(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.create, req, None) + + 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.HTTPUnprocessableEntity, + self.controller.create, req, body) + + 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}) + + 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}) + + 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}) + + 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}) + + 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.assertEquals(res_dict, expected) + + def test_get_security_group_by_id(self): + sg = security_group_template(id=2, rules=[]) + + def return_security_group(context, group_id): + self.assertEquals(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.assertEquals(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/111111111') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, '111111111') + + def test_delete_security_group_by_id(self): + sg = security_group_template(id=1, rules=[]) + + self.called = False + + def security_group_destroy(context, id): + self.called = True + + def return_security_group(context, group_id): + self.assertEquals(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_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/11111111') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, '11111111') + + def test_associate_by_non_existing_security_group_name(self): + body = dict(addSecurityGroup=dict(name='non-existing')) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._addSecurityGroup, body, req, '1') + + 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, body, req, 'invalid') + + 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, body, req, '1') + + 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, body, req, '1') + + 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, body, req, '1') + + 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, body, req, '1') + + 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.assertRaises(webob.exc.HTTPBadRequest, + self.manager._addSecurityGroup, body, req, '1') + + 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, body, req, '1') + + 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(body, req, '1') + + def test_disassociate_by_non_existing_security_group_name(self): + body = dict(removeSecurityGroup=dict(name='non-existing')) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._removeSecurityGroup, body, req, '1') + + 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, body, req, + 'invalid') + + 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, body, req, '1') + + 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, body, req, '1') + + 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, body, req, '1') + + 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, body, req, '1') + + 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.assertRaises(webob.exc.HTTPBadRequest, + self.manager._removeSecurityGroup, body, req, '1') + + 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, body, req, '1') + + 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(body, req, '1') + + +class TestSecurityGroupRules(test.TestCase): + def setUp(self): + super(TestSecurityGroupRules, self).setUp() + + sg1 = security_group_template(id=1) + sg2 = security_group_template(id=2, + name='authorize_revoke', + description='authorize-revoke testing') + db1 = security_group_db(sg1) + db2 = security_group_db(sg2) + + def return_security_group(context, group_id): + if group_id == db1['id']: + return db1 + if group_id == db2['id']: + return db2 + raise exception.NotFound() + + self.stubs.Set(nova.db, 'security_group_get', + return_security_group) + + self.parent_security_group = db2 + + self.controller = security_groups.SecurityGroupRulesController() + + def tearDown(self): + super(TestSecurityGroupRules, self).tearDown() + + def test_create_by_cidr(self): + rule = security_group_rule_template(cidr='10.2.3.124/24') + + 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.assertNotEquals(security_group_rule['id'], 0) + self.assertEquals(security_group_rule['parent_group_id'], 2) + self.assertEquals(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='1') + + 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.assertNotEquals(security_group_rule['id'], 0) + self.assertEquals(security_group_rule['parent_group_id'], 2) + + def test_create_by_invalid_cidr_json(self): + rules = { + "security_group_rule": { + "ip_protocol": "tcp", + "from_port": "22", + "to_port": "22", + "parent_group_id": 2, + "cidr": "10.2.3.124/2433"}} + rule = security_group_rule_template( + ip_protocol="tcp", + from_port=22, + to_port=22, + parent_group_id=2, + 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=2, + 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=2, + 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(self): + rule = security_group_rule_template(cidr='10.0.0.0/24') + + 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.HTTPUnprocessableEntity, + 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.HTTPUnprocessableEntity, + 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='invalid', + parent_group_id='1111111111111') + + 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_invalid_protocol(self): + rule = security_group_rule_template(ip_protocol='invalid-protocol', + cidr='10.2.2.0/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_with_no_protocol(self): + rule = security_group_rule_template(cidr='10.2.2.0/24') + 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') + + 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') + + 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') + + 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') + + 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') + 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') + 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') + + 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() + + 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.assertNotEquals(security_group_rule['id'], 0) + self.assertEquals(security_group_rule['parent_group_id'], + self.parent_security_group['id']) + self.assertEquals(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') + + 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='') + + 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='222222') + + 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_rule_with_same_group_parent_id(self): + rule = security_group_rule_template(group_id=2) + + 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_delete(self): + rule = security_group_rule_template(id=10) + + 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/10') + self.controller.delete(req, '10') + + 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' + + '/22222222222222') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, '22222222222222') + + +class TestSecurityGroupRulesXMLDeserializer(unittest.TestCase): + + def setUp(self): + self.deserializer = security_groups.SecurityGroupRulesXMLDeserializer() + + def test_create_request(self): + serial_request = """ + + 12 + 22 + 22 + + tcp + 10.0.0.0/24 +""" + 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.assertEquals(request['body'], expected) + + def test_create_no_protocol_request(self): + serial_request = """ + + 12 + 22 + 22 + + 10.0.0.0/24 +""" + 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.assertEquals(request['body'], expected) + + +class TestSecurityGroupXMLDeserializer(unittest.TestCase): + + def setUp(self): + self.deserializer = security_groups.SecurityGroupXMLDeserializer() + + def test_create_request(self): + serial_request = """ + + test +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group": { + "name": "test", + "description": "test", + }, + } + self.assertEquals(request['body'], expected) + + def test_create_no_description_request(self): + serial_request = """ + +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group": { + "name": "test", + }, + } + self.assertEquals(request['body'], expected) + + def test_create_no_name_request(self): + serial_request = """ + +test +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group": { + "description": "test", + }, + } + self.assertEquals(request['body'], expected) + + +class TestSecurityGroupXMLSerializer(unittest.TestCase): + def setUp(self): + self.namespace = wsgi.XMLNS_V11 + self.rule_serializer = security_groups.SecurityGroupRuleTemplate() + self.index_serializer = security_groups.SecurityGroupsTemplate() + self.default_serializer = security_groups.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.assertTrue(child_tag in 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.assertTrue(gr_child_tag in 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) + + print text + 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) + + print text + 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) + + print text + 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) diff --git a/nova/tests/api/openstack/compute/contrib/test_server_action_list.py b/nova/tests/api/openstack/compute/contrib/test_server_action_list.py new file mode 100644 index 000000000..aa5a7275c --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_server_action_list.py @@ -0,0 +1,103 @@ +# 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 datetime +import json +import unittest + +from lxml import etree + +from nova.api.openstack import compute +from nova.api.openstack.compute import extensions +from nova.api.openstack.compute.contrib import server_action_list +from nova.api.openstack import wsgi +import nova.compute +from nova import test +from nova.tests.api.openstack import fakes +import nova.utils + + +dt = datetime.datetime.utcnow() + + +def fake_get_actions(self, _context, instance_uuid): + return [ + {'action': 'rebuild', 'error': None, 'created_at': dt}, + {'action': 'reboot', 'error': 'Failed!', 'created_at': dt}, + ] + + +def fake_instance_get(self, _context, instance_uuid): + return {'uuid': instance_uuid} + + +class ServerActionsTest(test.TestCase): + + def setUp(self): + super(ServerActionsTest, self).setUp() + self.flags(allow_admin_api=True) + self.flags(verbose=True) + self.stubs.Set(nova.compute.API, 'get_actions', fake_get_actions) + self.stubs.Set(nova.compute.API, 'get', fake_instance_get) + self.compute_api = nova.compute.API() + + self.router = compute.APIRouter() + ext_middleware = extensions.ExtensionMiddleware(self.router) + self.app = wsgi.LazySerializationMiddleware(ext_middleware) + + def test_get_actions(self): + uuid = nova.utils.gen_uuid() + req = fakes.HTTPRequest.blank('/fake/servers/%s/actions' % uuid) + res = req.get_response(self.app) + output = json.loads(res.body) + expected = {'actions': [ + {'action': 'rebuild', 'error': None, 'created_at': str(dt)}, + {'action': 'reboot', 'error': 'Failed!', 'created_at': str(dt)}, + ]} + self.assertEqual(output, expected) + + +class TestServerActionsXMLSerializer(unittest.TestCase): + 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_action_list.ServerActionsTemplate() + exemplar = [dict( + created_at=datetime.datetime.now(), + action='foo', + error='quxx'), + dict( + created_at=datetime.datetime.now(), + action='bar', + error='xxuq')] + text = serializer.serialize(dict(actions=exemplar)) + + print text + tree = etree.fromstring(text) + + self.assertEqual('actions', self._tag(tree)) + self.assertEqual(len(tree), len(exemplar)) + for idx, child in enumerate(tree): + self.assertEqual('action', self._tag(child)) + for field in ('created_at', 'action', 'error'): + self.assertEqual(str(exemplar[idx][field]), child.get(field)) diff --git a/nova/tests/api/openstack/compute/contrib/test_server_diagnostics.py b/nova/tests/api/openstack/compute/contrib/test_server_diagnostics.py new file mode 100644 index 000000000..69aabdc26 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_server_diagnostics.py @@ -0,0 +1,86 @@ +# 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 json +import unittest + +from lxml import etree + +from nova.api.openstack import compute +from nova.api.openstack.compute import extensions +from nova.api.openstack.compute.contrib import server_diagnostics +from nova.api.openstack import wsgi +import nova.compute +from nova import test +from nova.tests.api.openstack import fakes +import nova.utils + + +def fake_get_diagnostics(self, _context, instance_uuid): + return {'data': 'Some diagnostic info'} + + +def fake_instance_get(self, _context, instance_uuid): + return {'uuid': instance_uuid} + + +class ServerDiagnosticsTest(test.TestCase): + + def setUp(self): + super(ServerDiagnosticsTest, self).setUp() + self.flags(allow_admin_api=True) + self.flags(verbose=True) + self.stubs.Set(nova.compute.API, 'get_diagnostics', + fake_get_diagnostics) + self.stubs.Set(nova.compute.API, 'get', fake_instance_get) + self.compute_api = nova.compute.API() + + self.router = compute.APIRouter() + ext_middleware = extensions.ExtensionMiddleware(self.router) + self.app = wsgi.LazySerializationMiddleware(ext_middleware) + + def test_get_diagnostics(self): + uuid = nova.utils.gen_uuid() + req = fakes.HTTPRequest.blank('/fake/servers/%s/diagnostics' % uuid) + res = req.get_response(self.app) + output = json.loads(res.body) + self.assertEqual(output, {'data': 'Some diagnostic info'}) + + +class TestServerDiagnosticsXMLSerializer(unittest.TestCase): + 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) + + print text + 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.assertTrue(tag in exemplar) + self.assertEqual(child.text, exemplar[tag]) diff --git a/nova/tests/api/openstack/compute/contrib/test_simple_tenant_usage.py b/nova/tests/api/openstack/compute/contrib/test_simple_tenant_usage.py new file mode 100644 index 000000000..3ff12bf3a --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_simple_tenant_usage.py @@ -0,0 +1,339 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import json + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import simple_tenant_usage +from nova.compute import api +from nova import context +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS + +SERVERS = 5 +TENANTS = 2 +HOURS = 24 +LOCAL_GB = 10 +MEMORY_MB = 1024 +VCPUS = 2 +STOP = datetime.datetime.utcnow() +START = STOP - datetime.timedelta(hours=HOURS) + + +def fake_instance_type_get(self, context, instance_type_id): + return {'id': 1, + 'vcpus': VCPUS, + 'local_gb': LOCAL_GB, + 'memory_mb': MEMORY_MB, + 'name': + 'fakeflavor'} + + +def get_fake_db_instance(start, end, instance_id, tenant_id): + return {'id': instance_id, + 'image_ref': '1', + 'project_id': tenant_id, + 'user_id': 'fakeuser', + 'display_name': 'name', + 'state_description': 'state', + 'instance_type_id': 1, + 'launched_at': start, + 'terminated_at': end} + + +def fake_instance_get_active_by_window(self, context, begin, end, project_id): + return [get_fake_db_instance(START, + STOP, + x, + "faketenant_%s" % (x / SERVERS)) + for x in xrange(TENANTS * SERVERS)] + + +class SimpleTenantUsageTest(test.TestCase): + def setUp(self): + super(SimpleTenantUsageTest, self).setUp() + self.stubs.Set(api.API, "get_instance_type", + fake_instance_type_get) + self.stubs.Set(api.API, "get_active_by_window", + fake_instance_get_active_by_window) + 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) + FLAGS.allow_admin_api = True + + def test_verify_index(self): + req = webob.Request.blank( + '/v2/faketenant_0/os-simple-tenant-usage?start=%s&end=%s' % + (START.isoformat(), STOP.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app( + fake_auth_context=self.admin_context)) + + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + usages = res_dict['tenant_usages'] + from nova import log as logging + logging.warn(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 * LOCAL_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_detailed_index(self): + req = webob.Request.blank( + '/v2/faketenant_0/os-simple-tenant-usage?' + 'detailed=1&start=%s&end=%s' % + (START.isoformat(), STOP.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app( + fake_auth_context=self.admin_context)) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + usages = res_dict['tenant_usages'] + 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_index_fails_for_nonadmin(self): + req = webob.Request.blank( + '/v2/faketenant_0/os-simple-tenant-usage?' + 'detailed=1&start=%s&end=%s' % + (START.isoformat(), STOP.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app( + fake_auth_context=self.user_context)) + self.assertEqual(res.status_int, 403) + + def test_verify_show(self): + req = webob.Request.blank( + '/v2/faketenant_0/os-simple-tenant-usage/' + 'faketenant_0?start=%s&end=%s' % + (START.isoformat(), STOP.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app( + fake_auth_context=self.user_context)) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + + usage = res_dict['tenant_usage'] + servers = usage['server_usages'] + self.assertEqual(len(usage['server_usages']), SERVERS) + for j in xrange(SERVERS): + self.assertEqual(int(servers[j]['hours']), HOURS) + + def test_verify_show_cant_view_other_tenant(self): + req = webob.Request.blank( + '/v2/faketenant_1/os-simple-tenant-usage/' + 'faketenant_0?start=%s&end=%s' % + (START.isoformat(), STOP.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app( + fake_auth_context=self.alt_user_context)) + self.assertEqual(res.status_int, 403) + + +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.assertTrue(child.tag in 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.assertTrue(child.tag in 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.SimpleTenantUsageTemplate() + today = datetime.datetime.now() + 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( + 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( + 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) + + print text + tree = etree.fromstring(text) + + self._verify_tenant_usage(raw_usage, tree) + + def test_serializer_index(self): + serializer = simple_tenant_usage.SimpleTenantUsagesTemplate() + today = datetime.datetime.now() + 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( + 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( + 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( + 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( + 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) + + print text + 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) diff --git a/nova/tests/api/openstack/compute/contrib/test_snapshots.py b/nova/tests/api/openstack/compute/contrib/test_snapshots.py new file mode 100644 index 000000000..69784e516 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_snapshots.py @@ -0,0 +1,299 @@ +# 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. + +import datetime +import json +import stubout + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import volumes +from nova import context +from nova import exception +from nova import flags +from nova import log as logging +from nova import test +from nova import volume +from nova.tests.api.openstack import fakes + +FLAGS = flags.FLAGS + +LOG = logging.getLogger('nova.tests.api.openstack.snapshot') + +_last_param = {} + + +def _get_default_snapshot_param(): + return { + 'id': 123, + 'volume_id': 12, + 'status': 'available', + 'volume_size': 100, + 'created_at': None, + 'display_name': 'Default name', + 'display_description': 'Default description', + } + + +def stub_snapshot_create(self, context, volume_id, name, description): + global _last_param + snapshot = _get_default_snapshot_param() + snapshot['volume_id'] = volume_id + snapshot['display_name'] = name + snapshot['display_description'] = description + + LOG.debug(_("_create: %s"), snapshot) + _last_param = snapshot + return snapshot + + +def stub_snapshot_delete(self, context, snapshot_id): + global _last_param + _last_param = dict(snapshot_id=snapshot_id) + + LOG.debug(_("_delete: %s"), locals()) + if snapshot_id != '123': + raise exception.NotFound + + +def stub_snapshot_get(self, context, snapshot_id): + global _last_param + _last_param = dict(snapshot_id=snapshot_id) + + LOG.debug(_("_get: %s"), locals()) + if snapshot_id != '123': + raise exception.NotFound + + param = _get_default_snapshot_param() + param['id'] = snapshot_id + return param + + +def stub_snapshot_get_all(self, context): + LOG.debug(_("_get_all: %s"), locals()) + param = _get_default_snapshot_param() + param['id'] = 123 + return [param] + + +class SnapshotApiTest(test.TestCase): + def setUp(self): + super(SnapshotApiTest, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.reset_fake_data() + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + self.stubs.Set(volume.api.API, "create_snapshot", stub_snapshot_create) + self.stubs.Set(volume.api.API, "create_snapshot_force", + stub_snapshot_create) + self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete) + self.stubs.Set(volume.api.API, "get_snapshot", stub_snapshot_get) + self.stubs.Set(volume.api.API, "get_all_snapshots", + stub_snapshot_get_all) + + self.context = context.get_admin_context() + + def tearDown(self): + self.stubs.UnsetAll() + super(SnapshotApiTest, self).tearDown() + + def test_snapshot_create(self): + global _last_param + _last_param = {} + + 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 = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + LOG.debug(_("test_snapshot_create: param=%s"), _last_param) + self.assertEqual(resp.status_int, 200) + + # Compare if parameters were correctly passed to stub + self.assertEqual(_last_param['display_name'], "Snapshot Test Name") + self.assertEqual(_last_param['display_description'], + "Snapshot Test Desc") + + resp_dict = json.loads(resp.body) + LOG.debug(_("test_snapshot_create: resp_dict=%s"), resp_dict) + self.assertTrue('snapshot' in resp_dict) + self.assertEqual(resp_dict['snapshot']['displayName'], + snapshot['display_name']) + self.assertEqual(resp_dict['snapshot']['displayDescription'], + snapshot['display_description']) + + def test_snapshot_create_force(self): + global _last_param + _last_param = {} + + 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 = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + LOG.debug(_("test_snapshot_create_force: param=%s"), _last_param) + self.assertEqual(resp.status_int, 200) + + # Compare if parameters were correctly passed to stub + self.assertEqual(_last_param['display_name'], "Snapshot Test Name") + self.assertEqual(_last_param['display_description'], + "Snapshot Test Desc") + + resp_dict = json.loads(resp.body) + LOG.debug(_("test_snapshot_create_force: resp_dict=%s"), resp_dict) + self.assertTrue('snapshot' in resp_dict) + self.assertEqual(resp_dict['snapshot']['displayName'], + snapshot['display_name']) + self.assertEqual(resp_dict['snapshot']['displayDescription'], + snapshot['display_description']) + + def test_snapshot_delete(self): + global _last_param + _last_param = {} + + snapshot_id = 123 + req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) + req.method = 'DELETE' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + def test_snapshot_delete_invalid_id(self): + global _last_param + _last_param = {} + + snapshot_id = 234 + req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) + req.method = 'DELETE' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 404) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + def test_snapshot_show(self): + global _last_param + _last_param = {} + + snapshot_id = 123 + req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + + LOG.debug(_("test_snapshot_show: resp=%s"), resp) + self.assertEqual(resp.status_int, 200) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + resp_dict = json.loads(resp.body) + self.assertTrue('snapshot' in resp_dict) + self.assertEqual(resp_dict['snapshot']['id'], str(snapshot_id)) + + def test_snapshot_show_invalid_id(self): + global _last_param + _last_param = {} + + snapshot_id = 234 + req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 404) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + def test_snapshot_detail(self): + req = webob.Request.blank('/v2/fake/os-snapshots/detail') + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + resp_dict = json.loads(resp.body) + LOG.debug(_("test_snapshot_detail: resp_dict=%s"), resp_dict) + self.assertTrue('snapshots' in resp_dict) + resp_snapshots = resp_dict['snapshots'] + self.assertEqual(len(resp_snapshots), 1) + + resp_snapshot = resp_snapshots.pop() + self.assertEqual(resp_snapshot['id'], 123) + + +class SnapshotSerializerTest(test.TestCase): + 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=datetime.datetime.now(), + displayName='snap_name', + displayDescription='snap_desc', + volumeId='vol_id', + ) + text = serializer.serialize(dict(snapshot=raw_snapshot)) + + print text + 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=datetime.datetime.now(), + displayName='snap1_name', + displayDescription='snap1_desc', + volumeId='vol1_id', + ), + dict( + id='snap2_id', + status='snap2_status', + size=1024, + createdAt=datetime.datetime.now(), + displayName='snap2_name', + displayDescription='snap2_desc', + volumeId='vol2_id', + )] + text = serializer.serialize(dict(snapshots=raw_snapshots)) + + print text + 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/api/openstack/compute/contrib/test_users.py b/nova/tests/api/openstack/compute/contrib/test_users.py new file mode 100644 index 000000000..5895f4f66 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_users.py @@ -0,0 +1,154 @@ +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from lxml import etree + +from nova.api.openstack.compute.contrib import users +from nova.auth.manager import User, Project +from nova import test +from nova.tests.api.openstack import fakes +from nova import utils + + +def fake_init(self): + self.manager = fakes.FakeAuthManager() + + +def fake_admin_check(self, req): + return True + + +class UsersTest(test.TestCase): + def setUp(self): + super(UsersTest, self).setUp() + self.flags(verbose=True, allow_admin_api=True) + self.stubs.Set(users.Controller, '__init__', + fake_init) + self.stubs.Set(users.Controller, '_check_admin', + fake_admin_check) + fakes.FakeAuthManager.clear_fakes() + fakes.FakeAuthManager.projects = dict(testacct=Project('testacct', + 'testacct', + 'id1', + 'test', + [])) + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + + fakemgr = fakes.FakeAuthManager() + fakemgr.add_user(User('id1', 'guy1', 'acc1', 'secret1', False)) + fakemgr.add_user(User('id2', 'guy2', 'acc2', 'secret2', True)) + + self.controller = users.Controller() + + def test_get_user_list(self): + req = fakes.HTTPRequest.blank('/v2/fake/users') + res_dict = self.controller.index(req) + + self.assertEqual(len(res_dict['users']), 2) + + def test_get_user_by_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/users/id2') + res_dict = self.controller.show(req, 'id2') + + self.assertEqual(res_dict['user']['id'], 'id2') + self.assertEqual(res_dict['user']['name'], 'guy2') + self.assertEqual(res_dict['user']['secret'], 'secret2') + self.assertEqual(res_dict['user']['admin'], True) + + def test_user_delete(self): + req = fakes.HTTPRequest.blank('/v2/fake/users/id1') + self.controller.delete(req, 'id1') + + self.assertTrue('id1' not in [u.id for u in + fakes.FakeAuthManager.auth_data]) + + def test_user_create(self): + secret = utils.generate_password() + body = dict(user=dict(name='test_guy', + access='acc3', + secret=secret, + admin=True)) + req = fakes.HTTPRequest.blank('/v2/fake/users') + res_dict = self.controller.create(req, body) + + # NOTE(justinsb): This is a questionable assertion in general + # fake sets id=name, but others might not... + self.assertEqual(res_dict['user']['id'], 'test_guy') + + self.assertEqual(res_dict['user']['name'], 'test_guy') + self.assertEqual(res_dict['user']['access'], 'acc3') + self.assertEqual(res_dict['user']['secret'], secret) + self.assertEqual(res_dict['user']['admin'], True) + self.assertTrue('test_guy' in [u.id for u in + fakes.FakeAuthManager.auth_data]) + self.assertEqual(len(fakes.FakeAuthManager.auth_data), 3) + + def test_user_update(self): + new_secret = utils.generate_password() + body = dict(user=dict(name='guy2', + access='acc2', + secret=new_secret)) + + req = fakes.HTTPRequest.blank('/v2/fake/users/id2') + res_dict = self.controller.update(req, 'id2', body) + + self.assertEqual(res_dict['user']['id'], 'id2') + self.assertEqual(res_dict['user']['name'], 'guy2') + self.assertEqual(res_dict['user']['access'], 'acc2') + self.assertEqual(res_dict['user']['secret'], new_secret) + self.assertEqual(res_dict['user']['admin'], True) + + +class TestUsersXMLSerializer(test.TestCase): + + def test_index(self): + serializer = users.UsersTemplate() + fixture = {'users': [{'id': 'id1', + 'name': 'guy1', + 'secret': 'secret1', + 'admin': False}, + {'id': 'id2', + 'name': 'guy2', + 'secret': 'secret2', + 'admin': True}]} + + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, 'users') + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, 'user') + self.assertEqual(res_tree[0].get('id'), 'id1') + self.assertEqual(res_tree[1].tag, 'user') + self.assertEqual(res_tree[1].get('id'), 'id2') + + def test_show(self): + serializer = users.UserTemplate() + fixture = {'user': {'id': 'id2', + 'name': 'guy2', + 'secret': 'secret2', + 'admin': True}} + + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, 'user') + self.assertEqual(res_tree.get('id'), 'id2') + self.assertEqual(res_tree.get('name'), 'guy2') + self.assertEqual(res_tree.get('secret'), 'secret2') + self.assertEqual(res_tree.get('admin'), 'True') diff --git a/nova/tests/api/openstack/compute/contrib/test_virtual_interfaces.py b/nova/tests/api/openstack/compute/contrib/test_virtual_interfaces.py new file mode 100644 index 000000000..d9f41f07b --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_virtual_interfaces.py @@ -0,0 +1,93 @@ +# 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. + +import json + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import virtual_interfaces +from nova.api.openstack import wsgi +from nova import network +from nova import test +from nova.tests.api.openstack import fakes + + +def get_vifs_by_instance(self, context, server_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 ServerVirtualInterfaceTest(test.TestCase): + + def setUp(self): + super(ServerVirtualInterfaceTest, self).setUp() + self.controller = virtual_interfaces.ServerVirtualInterfaceController() + self.stubs.Set(network.api.API, "get_vifs_by_instance", + get_vifs_by_instance) + + def tearDown(self): + super(ServerVirtualInterfaceTest, self).tearDown() + + 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()) + self.assertEqual(res.status_int, 200) + res_dict = json.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) + + +class ServerVirtualInterfaceSerializerTest(test.TestCase): + 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) + + print text + 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/api/openstack/compute/contrib/test_volume_types.py b/nova/tests/api/openstack/compute/contrib/test_volume_types.py new file mode 100644 index 000000000..fdd2214e3 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_volume_types.py @@ -0,0 +1,209 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import volumetypes +from nova import exception +from nova import test +from nova import log as logging +from nova.volume import volume_types +from nova.tests.api.openstack import fakes + + +LOG = logging.getLogger('nova.tests.api.openstack.compute.contrib.' + 'test_volume_types') + +last_param = {} + + +def stub_volume_type(id): + specs = { + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5"} + return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs) + + +def return_volume_types_get_all_types(context): + return dict(vol_type_1=stub_volume_type(1), + vol_type_2=stub_volume_type(2), + vol_type_3=stub_volume_type(3)) + + +def return_empty_volume_types_get_all_types(context): + return {} + + +def return_volume_types_get_volume_type(context, id): + if id == "777": + raise exception.VolumeTypeNotFound(volume_type_id=id) + return stub_volume_type(int(id)) + + +def return_volume_types_destroy(context, name): + if name == "777": + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) + pass + + +def return_volume_types_create(context, name, specs): + pass + + +def return_volume_types_get_by_name(context, name): + if name == "777": + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) + return stub_volume_type(int(name.split("_")[2])) + + +class VolumeTypesApiTest(test.TestCase): + def setUp(self): + super(VolumeTypesApiTest, self).setUp() + fakes.stub_out_key_pair_funcs(self.stubs) + self.controller = volumetypes.VolumeTypesController() + + def tearDown(self): + self.stubs.UnsetAll() + super(VolumeTypesApiTest, self).tearDown() + + def test_volume_types_index(self): + self.stubs.Set(volume_types, 'get_all_types', + return_volume_types_get_all_types) + + req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') + res_dict = self.controller.index(req) + + self.assertEqual(3, len(res_dict)) + for name in ['vol_type_1', 'vol_type_2', 'vol_type_3']: + self.assertEqual(name, res_dict[name]['name']) + self.assertEqual('value1', res_dict[name]['extra_specs']['key1']) + + def test_volume_types_index_no_data(self): + self.stubs.Set(volume_types, 'get_all_types', + return_empty_volume_types_get_all_types) + + req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') + res_dict = self.controller.index(req) + + self.assertEqual(0, len(res_dict)) + + def test_volume_types_show(self): + self.stubs.Set(volume_types, 'get_volume_type', + return_volume_types_get_volume_type) + + req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/1') + res_dict = self.controller.show(req, 1) + + self.assertEqual(1, len(res_dict)) + self.assertEqual('vol_type_1', res_dict['volume_type']['name']) + + def test_volume_types_show_not_found(self): + self.stubs.Set(volume_types, 'get_volume_type', + return_volume_types_get_volume_type) + + req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/777') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, '777') + + def test_volume_types_delete(self): + self.stubs.Set(volume_types, 'get_volume_type', + return_volume_types_get_volume_type) + self.stubs.Set(volume_types, 'destroy', + return_volume_types_destroy) + + req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/1') + self.controller.delete(req, 1) + + def test_volume_types_delete_not_found(self): + self.stubs.Set(volume_types, 'get_volume_type', + return_volume_types_get_volume_type) + self.stubs.Set(volume_types, 'destroy', + return_volume_types_destroy) + + req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/777') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, '777') + + def test_create(self): + self.stubs.Set(volume_types, 'create', + return_volume_types_create) + self.stubs.Set(volume_types, 'get_volume_type_by_name', + return_volume_types_get_by_name) + + body = {"volume_type": {"name": "vol_type_1", + "extra_specs": {"key1": "value1"}}} + req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') + res_dict = self.controller.create(req, body) + + self.assertEqual(1, len(res_dict)) + self.assertEqual('vol_type_1', res_dict['volume_type']['name']) + + def test_create_empty_body(self): + self.stubs.Set(volume_types, 'create', + return_volume_types_create) + self.stubs.Set(volume_types, 'get_volume_type_by_name', + return_volume_types_get_by_name) + + req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.create, req, '') + + +class VolumeTypesSerializerTest(test.TestCase): + def _verify_volume_type(self, vtype, tree): + self.assertEqual('volume_type', tree.tag) + self.assertEqual(vtype['name'], tree.get('name')) + self.assertEqual(str(vtype['id']), tree.get('id')) + self.assertEqual(1, len(tree)) + extra_specs = tree[0] + self.assertEqual('extra_specs', extra_specs.tag) + seen = set(vtype['extra_specs'].keys()) + for child in extra_specs: + self.assertTrue(child.tag in seen) + self.assertEqual(vtype['extra_specs'][child.tag], child.text) + seen.remove(child.tag) + self.assertEqual(len(seen), 0) + + def test_index_serializer(self): + serializer = volumetypes.VolumeTypesTemplate() + + # Just getting some input data + vtypes = return_volume_types_get_all_types(None) + text = serializer.serialize(vtypes) + + print text + tree = etree.fromstring(text) + + self.assertEqual('volume_types', tree.tag) + self.assertEqual(len(vtypes), len(tree)) + for child in tree: + name = child.get('name') + self.assertTrue(name in vtypes) + self._verify_volume_type(vtypes[name], child) + + def test_voltype_serializer(self): + serializer = volumetypes.VolumeTypeTemplate() + + vtype = stub_volume_type(1) + text = serializer.serialize(dict(volume_type=vtype)) + + print text + tree = etree.fromstring(text) + + self._verify_volume_type(vtype, tree) diff --git a/nova/tests/api/openstack/compute/contrib/test_volume_types_extra_specs.py b/nova/tests/api/openstack/compute/contrib/test_volume_types_extra_specs.py new file mode 100644 index 000000000..e9c4034f0 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_volume_types_extra_specs.py @@ -0,0 +1,198 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# 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. + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import volumetypes +from nova import test +from nova.tests.api.openstack import fakes +import nova.wsgi + + +def return_create_volume_type_extra_specs(context, volume_type_id, + extra_specs): + return stub_volume_type_extra_specs() + + +def return_volume_type_extra_specs(context, volume_type_id): + return stub_volume_type_extra_specs() + + +def return_empty_volume_type_extra_specs(context, volume_type_id): + return {} + + +def delete_volume_type_extra_specs(context, volume_type_id, key): + pass + + +def stub_volume_type_extra_specs(): + specs = { + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5"} + return specs + + +class VolumeTypesExtraSpecsTest(test.TestCase): + + def setUp(self): + super(VolumeTypesExtraSpecsTest, self).setUp() + fakes.stub_out_key_pair_funcs(self.stubs) + self.api_path = '/v2/fake/os-volume-types/1/extra_specs' + self.controller = volumetypes.VolumeTypeExtraSpecsController() + + def test_index(self): + self.stubs.Set(nova.db, 'volume_type_extra_specs_get', + return_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path) + 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, 'volume_type_extra_specs_get', + return_empty_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path) + res_dict = self.controller.index(req, 1) + + self.assertEqual(0, len(res_dict['extra_specs'])) + + def test_show(self): + self.stubs.Set(nova.db, 'volume_type_extra_specs_get', + return_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path + '/key5') + 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, 'volume_type_extra_specs_get', + return_empty_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path + '/key6') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, 1, 'key6') + + def test_delete(self): + self.stubs.Set(nova.db, 'volume_type_extra_specs_delete', + delete_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path + '/key5') + self.controller.delete(req, 1, 'key5') + + def test_create(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + body = {"extra_specs": {"key1": "value1"}} + + req = fakes.HTTPRequest.blank(self.api_path) + res_dict = self.controller.create(req, 1, body) + + self.assertEqual('value1', res_dict['extra_specs']['key1']) + + def test_create_empty_body(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, 1, '') + + def test_update_item(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + body = {"key1": "value1"} + + req = fakes.HTTPRequest.blank(self.api_path + '/key1') + res_dict = self.controller.update(req, 1, 'key1', body) + + self.assertEqual('value1', res_dict['key1']) + + def test_update_item_empty_body(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path + '/key1') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'key1', '') + + def test_update_item_too_many_keys(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + body = {"key1": "value1", "key2": "value2"} + + req = fakes.HTTPRequest.blank(self.api_path + '/key1') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'key1', body) + + def test_update_item_body_uri_mismatch(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + body = {"key1": "value1"} + + req = fakes.HTTPRequest.blank(self.api_path + '/bad') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'bad', body) + + +class VolumeTypeExtraSpecsSerializerTest(test.TestCase): + def test_index_create_serializer(self): + serializer = volumetypes.VolumeTypeExtraSpecsTemplate() + + # Just getting some input data + extra_specs = stub_volume_type_extra_specs() + text = serializer.serialize(dict(extra_specs=extra_specs)) + + print text + tree = etree.fromstring(text) + + self.assertEqual('extra_specs', tree.tag) + self.assertEqual(len(extra_specs), len(tree)) + seen = set(extra_specs.keys()) + for child in tree: + self.assertTrue(child.tag in seen) + self.assertEqual(extra_specs[child.tag], child.text) + seen.remove(child.tag) + self.assertEqual(len(seen), 0) + + def test_update_show_serializer(self): + serializer = volumetypes.VolumeTypeExtraSpecTemplate() + + exemplar = dict(key1='value1') + text = serializer.serialize(exemplar) + + print text + tree = etree.fromstring(text) + + self.assertEqual('key1', tree.tag) + self.assertEqual('value1', tree.text) + self.assertEqual(0, len(tree)) diff --git a/nova/tests/api/openstack/compute/contrib/test_volumes.py b/nova/tests/api/openstack/compute/contrib/test_volumes.py new file mode 100644 index 000000000..f32f4d867 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_volumes.py @@ -0,0 +1,239 @@ +# Copyright 2013 Josh Durgin +# 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 json + +from lxml import etree +import webob + +import nova +from nova.api.openstack.compute.contrib import volumes +from nova.compute import instance_types +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS + + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +IMAGE_UUID = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + + +def fake_compute_api_create(cls, context, instance_type, image_href, **kwargs): + global _block_device_mapping_seen + _block_device_mapping_seen = kwargs.get('block_device_mapping') + + inst_type = instance_types.get_instance_type_by_flavor_id(2) + resv_id = None + return ([{'id': 1, + 'display_name': 'test_server', + 'uuid': FAKE_UUID, + 'instance_type': dict(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) + + +class BootFromVolumeTest(test.TestCase): + + def setUp(self): + super(BootFromVolumeTest, self).setUp() + self.stubs.Set(nova.compute.API, 'create', fake_compute_api_create) + fakes.stub_out_nw_api(self.stubs) + + 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, + )] + )) + global _block_device_mapping_seen + _block_device_mapping_seen = None + req = webob.Request.blank('/v2/fake/os-volumes_boot') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + server = json.loads(res.body)['server'] + self.assertEqual(FAKE_UUID, server['id']) + self.assertEqual(FLAGS.password_length, len(server['adminPass'])) + self.assertEqual(len(_block_device_mapping_seen), 1) + self.assertEqual(_block_device_mapping_seen[0]['volume_id'], 1) + self.assertEqual(_block_device_mapping_seen[0]['device_name'], + '/dev/vda') + + +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.assertTrue(child.tag in ('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.assertTrue(gr_child.tag in not_seen) + self.assertEqual(str(vol['metadata'][gr_child.tag]), + gr_child.text) + not_seen.remove(gr_child.tag) + 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)) + + print text + 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)) + + print text + 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=datetime.datetime.now(), + 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)) + + print text + 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=datetime.datetime.now(), + 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=datetime.datetime.now(), + 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)) + + print text + 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) diff --git a/nova/tests/api/openstack/compute/contrib/test_vsa.py b/nova/tests/api/openstack/compute/contrib/test_vsa.py new file mode 100644 index 000000000..e19aeedba --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_vsa.py @@ -0,0 +1,715 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import json + +from lxml import etree +import stubout +import webob + +from nova.api.openstack.compute.contrib\ + import virtual_storage_arrays as vsa_ext +from nova import context +import nova.db +from nova import exception +from nova import flags +from nova import log as logging +from nova import test +from nova.tests.api.openstack import fakes +from nova import volume +from nova import vsa + + +FLAGS = flags.FLAGS + +LOG = logging.getLogger('nova.tests.api.openstack.compute.contrib.test_vsa') + +last_param = {} + + +def _get_default_vsa_param(): + return { + 'display_name': 'Test_VSA_name', + 'display_description': 'Test_VSA_description', + 'vc_count': 1, + 'instance_type': 'm1.small', + 'instance_type_id': 5, + 'image_name': None, + 'availability_zone': None, + 'storage': [], + 'shared': False, + } + + +def stub_vsa_create(self, context, **param): + global last_param + LOG.debug(_("_create: param=%s"), param) + param['id'] = 123 + param['name'] = 'Test name' + param['instance_type_id'] = 5 + last_param = param + return param + + +def stub_vsa_delete(self, context, vsa_id): + global last_param + last_param = dict(vsa_id=vsa_id) + + LOG.debug(_("_delete: %s"), locals()) + if vsa_id != '123': + raise exception.NotFound + + +def stub_vsa_get(self, context, vsa_id): + global last_param + last_param = dict(vsa_id=vsa_id) + + LOG.debug(_("_get: %s"), locals()) + if vsa_id != '123': + raise exception.NotFound + + param = _get_default_vsa_param() + param['id'] = vsa_id + return param + + +def stub_vsa_get_all(self, context): + LOG.debug(_("_get_all: %s"), locals()) + param = _get_default_vsa_param() + param['id'] = 123 + return [param] + + +class VSAApiTest(test.TestCase): + def setUp(self): + super(VSAApiTest, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.reset_fake_data() + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + self.stubs.Set(vsa.api.API, "create", stub_vsa_create) + self.stubs.Set(vsa.api.API, "delete", stub_vsa_delete) + self.stubs.Set(vsa.api.API, "get", stub_vsa_get) + self.stubs.Set(vsa.api.API, "get_all", stub_vsa_get_all) + + self.context = context.get_admin_context() + + def tearDown(self): + self.stubs.UnsetAll() + super(VSAApiTest, self).tearDown() + + def test_vsa_create(self): + global last_param + last_param = {} + + vsa = {"displayName": "VSA Test Name", + "displayDescription": "VSA Test Desc"} + body = dict(vsa=vsa) + req = webob.Request.blank('/v2/fake/zadr-vsa') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + # Compare if parameters were correctly passed to stub + self.assertEqual(last_param['display_name'], "VSA Test Name") + self.assertEqual(last_param['display_description'], "VSA Test Desc") + + resp_dict = json.loads(resp.body) + self.assertTrue('vsa' in resp_dict) + self.assertEqual(resp_dict['vsa']['displayName'], vsa['displayName']) + self.assertEqual(resp_dict['vsa']['displayDescription'], + vsa['displayDescription']) + + def test_vsa_create_no_body(self): + req = webob.Request.blank('/v2/fake/zadr-vsa') + req.method = 'POST' + req.body = json.dumps({}) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 422) + + def test_vsa_delete(self): + global last_param + last_param = {} + + vsa_id = 123 + req = webob.Request.blank('/v2/fake/zadr-vsa/%d' % vsa_id) + req.method = 'DELETE' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + self.assertEqual(str(last_param['vsa_id']), str(vsa_id)) + + def test_vsa_delete_invalid_id(self): + global last_param + last_param = {} + + vsa_id = 234 + req = webob.Request.blank('/v2/fake/zadr-vsa/%d' % vsa_id) + req.method = 'DELETE' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 404) + self.assertEqual(str(last_param['vsa_id']), str(vsa_id)) + + def test_vsa_show(self): + global last_param + last_param = {} + + vsa_id = 123 + req = webob.Request.blank('/v2/fake/zadr-vsa/%d' % vsa_id) + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + self.assertEqual(str(last_param['vsa_id']), str(vsa_id)) + + resp_dict = json.loads(resp.body) + self.assertTrue('vsa' in resp_dict) + self.assertEqual(resp_dict['vsa']['id'], str(vsa_id)) + + def test_vsa_show_invalid_id(self): + global last_param + last_param = {} + + vsa_id = 234 + req = webob.Request.blank('/v2/fake/zadr-vsa/%d' % vsa_id) + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 404) + self.assertEqual(str(last_param['vsa_id']), str(vsa_id)) + + def test_vsa_index(self): + req = webob.Request.blank('/v2/fake/zadr-vsa') + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + resp_dict = json.loads(resp.body) + + self.assertTrue('vsaSet' in resp_dict) + resp_vsas = resp_dict['vsaSet'] + self.assertEqual(len(resp_vsas), 1) + + resp_vsa = resp_vsas.pop() + self.assertEqual(resp_vsa['id'], 123) + + def test_vsa_detail(self): + req = webob.Request.blank('/v2/fake/zadr-vsa/detail') + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + resp_dict = json.loads(resp.body) + + self.assertTrue('vsaSet' in resp_dict) + resp_vsas = resp_dict['vsaSet'] + self.assertEqual(len(resp_vsas), 1) + + resp_vsa = resp_vsas.pop() + self.assertEqual(resp_vsa['id'], 123) + + +def _get_default_volume_param(): + return { + 'id': 123, + 'status': 'available', + 'size': 100, + 'availability_zone': 'nova', + 'created_at': None, + 'attach_status': 'detached', + 'name': 'vol name', + 'display_name': 'Default vol name', + 'display_description': 'Default vol description', + 'volume_type_id': 1, + 'volume_metadata': [], + 'snapshot_id': None, + } + + +def stub_get_vsa_volume_type(self, context): + return {'id': 1, + 'name': 'VSA volume type', + 'extra_specs': {'type': 'vsa_volume'}} + + +def stub_volume_create(self, context, size, snapshot_id, name, description, + **param): + LOG.debug(_("_create: param=%s"), size) + vol = _get_default_volume_param() + vol['size'] = size + vol['display_name'] = name + vol['display_description'] = description + vol['snapshot_id'] = snapshot_id + return vol + + +def stub_volume_update(self, context, **param): + LOG.debug(_("_volume_update: param=%s"), param) + pass + + +def stub_volume_delete(self, context, **param): + LOG.debug(_("_volume_delete: param=%s"), param) + pass + + +def stub_volume_get(self, context, volume_id): + LOG.debug(_("_volume_get: volume_id=%s"), volume_id) + vol = _get_default_volume_param() + vol['id'] = volume_id + meta = {'key': 'from_vsa_id', 'value': '123'} + if volume_id == '345': + meta = {'key': 'to_vsa_id', 'value': '123'} + vol['volume_metadata'].append(meta) + return vol + + +def stub_volume_get_notfound(self, context, volume_id): + raise exception.NotFound + + +def stub_volume_get_all(self, context, search_opts): + vol = stub_volume_get(self, context, '123') + vol['metadata'] = search_opts['metadata'] + return [vol] + + +def return_vsa(context, vsa_id): + return {'id': vsa_id} + + +class VSAVolumeApiTest(test.TestCase): + + def setUp(self, test_obj=None, test_objs=None): + super(VSAVolumeApiTest, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.reset_fake_data() + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + self.stubs.Set(nova.db, 'vsa_get', return_vsa) + self.stubs.Set(vsa.api.API, "get_vsa_volume_type", + stub_get_vsa_volume_type) + + self.stubs.Set(volume.api.API, "update", stub_volume_update) + self.stubs.Set(volume.api.API, "delete", stub_volume_delete) + self.stubs.Set(volume.api.API, "get", stub_volume_get) + self.stubs.Set(volume.api.API, "get_all", stub_volume_get_all) + + self.context = context.get_admin_context() + self.test_obj = test_obj if test_obj else "volume" + self.test_objs = test_objs if test_objs else "volumes" + + def tearDown(self): + self.stubs.UnsetAll() + super(VSAVolumeApiTest, self).tearDown() + + def test_vsa_volume_create(self): + self.stubs.Set(volume.api.API, "create", stub_volume_create) + + vol = {"size": 100, + "displayName": "VSA Volume Test Name", + "displayDescription": "VSA Volume Test Desc"} + body = {self.test_obj: vol} + req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s' % self.test_objs) + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + resp = req.get_response(fakes.wsgi_app()) + + if self.test_obj == "volume": + self.assertEqual(resp.status_int, 200) + + resp_dict = json.loads(resp.body) + self.assertTrue(self.test_obj in resp_dict) + self.assertEqual(resp_dict[self.test_obj]['size'], + vol['size']) + self.assertEqual(resp_dict[self.test_obj]['displayName'], + vol['displayName']) + self.assertEqual(resp_dict[self.test_obj]['displayDescription'], + vol['displayDescription']) + else: + self.assertEqual(resp.status_int, 400) + + def test_vsa_volume_create_no_body(self): + req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s' % self.test_objs) + req.method = 'POST' + req.body = json.dumps({}) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + if self.test_obj == "volume": + self.assertEqual(resp.status_int, 422) + else: + self.assertEqual(resp.status_int, 400) + + def test_vsa_volume_index(self): + req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s' % self.test_objs) + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + def test_vsa_volume_detail(self): + req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/detail' % \ + self.test_objs) + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + def test_vsa_volume_show(self): + obj_num = 234 if self.test_objs == "volumes" else 345 + req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/%s' % \ + (self.test_objs, obj_num)) + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + def test_vsa_volume_show_no_vsa_assignment(self): + req = webob.Request.blank('/v2/fake/zadr-vsa/4/%s/333' % \ + (self.test_objs)) + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) + + def test_vsa_volume_show_no_volume(self): + self.stubs.Set(volume.api.API, "get", stub_volume_get_notfound) + + req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/333' % \ + (self.test_objs)) + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 404) + + def test_vsa_volume_update(self): + obj_num = 234 if self.test_objs == "volumes" else 345 + update = {"status": "available", + "displayName": "Test Display name"} + body = {self.test_obj: update} + req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/%s' % \ + (self.test_objs, obj_num)) + req.method = 'PUT' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + if self.test_obj == "volume": + self.assertEqual(resp.status_int, 202) + else: + self.assertEqual(resp.status_int, 400) + + def test_vsa_volume_delete(self): + obj_num = 234 if self.test_objs == "volumes" else 345 + req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/%s' % \ + (self.test_objs, obj_num)) + req.method = 'DELETE' + resp = req.get_response(fakes.wsgi_app()) + if self.test_obj == "volume": + self.assertEqual(resp.status_int, 202) + else: + self.assertEqual(resp.status_int, 400) + + def test_vsa_volume_delete_no_vsa_assignment(self): + req = webob.Request.blank('/v2/fake/zadr-vsa/4/%s/333' % \ + (self.test_objs)) + req.method = 'DELETE' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) + + def test_vsa_volume_delete_no_volume(self): + self.stubs.Set(volume.api.API, "get", stub_volume_get_notfound) + + req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/333' % \ + (self.test_objs)) + req.method = 'DELETE' + resp = req.get_response(fakes.wsgi_app()) + if self.test_obj == "volume": + self.assertEqual(resp.status_int, 404) + else: + self.assertEqual(resp.status_int, 400) + + +class VSADriveApiTest(VSAVolumeApiTest): + def setUp(self): + super(VSADriveApiTest, self).setUp(test_obj="drive", + test_objs="drives") + + def tearDown(self): + self.stubs.UnsetAll() + super(VSADriveApiTest, self).tearDown() + + +class SerializerTestCommon(test.TestCase): + def _verify_attrs(self, obj, tree, attrs): + for attr in attrs: + self.assertEqual(str(obj[attr]), tree.get(attr)) + + +class VsaSerializerTest(SerializerTestCommon): + def test_serialize_show_create(self): + serializer = vsa_ext.VsaTemplate() + exemplar = dict( + id='vsa_id', + name='vsa_name', + displayName='vsa_display_name', + displayDescription='vsa_display_desc', + createTime=datetime.datetime.now(), + status='active', + vcType='vsa_instance_type', + vcCount=24, + driveCount=48, + ipAddress='10.11.12.13') + text = serializer.serialize(dict(vsa=exemplar)) + + print text + tree = etree.fromstring(text) + + self.assertEqual('vsa', tree.tag) + self._verify_attrs(exemplar, tree, exemplar.keys()) + + def test_serialize_index_detail(self): + serializer = vsa_ext.VsaSetTemplate() + exemplar = [dict( + id='vsa1_id', + name='vsa1_name', + displayName='vsa1_display_name', + displayDescription='vsa1_display_desc', + createTime=datetime.datetime.now(), + status='active', + vcType='vsa1_instance_type', + vcCount=24, + driveCount=48, + ipAddress='10.11.12.13'), + dict( + id='vsa2_id', + name='vsa2_name', + displayName='vsa2_display_name', + displayDescription='vsa2_display_desc', + createTime=datetime.datetime.now(), + status='active', + vcType='vsa2_instance_type', + vcCount=42, + driveCount=84, + ipAddress='11.12.13.14')] + text = serializer.serialize(dict(vsaSet=exemplar)) + + print text + tree = etree.fromstring(text) + + self.assertEqual('vsaSet', tree.tag) + self.assertEqual(len(exemplar), len(tree)) + for idx, child in enumerate(tree): + self.assertEqual('vsa', child.tag) + self._verify_attrs(exemplar[idx], child, exemplar[idx].keys()) + + +class VsaVolumeSerializerTest(SerializerTestCommon): + show_serializer = vsa_ext.VsaVolumeTemplate + index_serializer = vsa_ext.VsaVolumesTemplate + object = 'volume' + objects = 'volumes' + + def _verify_voldrive(self, vol, tree): + self.assertEqual(self.object, tree.tag) + + self._verify_attrs(vol, tree, ('id', 'status', 'size', + 'availabilityZone', 'createdAt', + 'displayName', 'displayDescription', + 'volumeType', 'vsaId', 'name')) + + for child in tree: + self.assertTrue(child.tag in ('attachments', 'metadata')) + if child.tag == 'attachments': + self.assertEqual(1, len(child)) + self.assertEqual('attachment', child[0].tag) + self._verify_attrs(vol['attachments'][0], child[0], + ('id', 'volumeId', 'serverId', 'device')) + elif child.tag == 'metadata': + not_seen = set(vol['metadata'].keys()) + for gr_child in child: + self.assertTrue(gr_child.tag in not_seen) + self.assertEqual(str(vol['metadata'][gr_child.tag]), + gr_child.text) + not_seen.remove(gr_child.tag) + self.assertEqual(0, len(not_seen)) + + def test_show_create_serializer(self): + serializer = self.show_serializer() + raw_volume = dict( + id='vol_id', + status='vol_status', + size=1024, + availabilityZone='vol_availability', + createdAt=datetime.datetime.now(), + attachments=[dict( + id='vol_id', + volumeId='vol_id', + serverId='instance_uuid', + device='/foo')], + displayName='vol_name', + displayDescription='vol_desc', + volumeType='vol_type', + metadata=dict( + foo='bar', + baz='quux', + ), + vsaId='vol_vsa_id', + name='vol_vsa_name', + ) + text = serializer.serialize({self.object: raw_volume}) + + print text + tree = etree.fromstring(text) + + self._verify_voldrive(raw_volume, tree) + + def test_index_detail_serializer(self): + serializer = self.index_serializer() + raw_volumes = [dict( + id='vol1_id', + status='vol1_status', + size=1024, + availabilityZone='vol1_availability', + createdAt=datetime.datetime.now(), + attachments=[dict( + id='vol1_id', + volumeId='vol1_id', + serverId='instance_uuid', + device='/foo1')], + displayName='vol1_name', + displayDescription='vol1_desc', + volumeType='vol1_type', + metadata=dict( + foo='vol1_foo', + bar='vol1_bar', + ), + vsaId='vol1_vsa_id', + name='vol1_vsa_name', + ), + dict( + id='vol2_id', + status='vol2_status', + size=1024, + availabilityZone='vol2_availability', + createdAt=datetime.datetime.now(), + attachments=[dict( + id='vol2_id', + volumeId='vol2_id', + serverId='instance_uuid', + device='/foo2')], + displayName='vol2_name', + displayDescription='vol2_desc', + volumeType='vol2_type', + metadata=dict( + foo='vol2_foo', + bar='vol2_bar', + ), + vsaId='vol2_vsa_id', + name='vol2_vsa_name', + )] + text = serializer.serialize({self.objects: raw_volumes}) + + print text + tree = etree.fromstring(text) + + self.assertEqual(self.objects, tree.tag) + self.assertEqual(len(raw_volumes), len(tree)) + for idx, child in enumerate(tree): + self._verify_voldrive(raw_volumes[idx], child) + + +class VsaDriveSerializerTest(VsaVolumeSerializerTest): + show_serializer = vsa_ext.VsaDriveTemplate + index_serializer = vsa_ext.VsaDrivesTemplate + object = 'drive' + objects = 'drives' + + +class VsaVPoolSerializerTest(SerializerTestCommon): + def _verify_vpool(self, vpool, tree): + self._verify_attrs(vpool, tree, ('id', 'vsaId', 'name', 'displayName', + 'displayDescription', 'driveCount', + 'protection', 'stripeSize', + 'stripeWidth', 'createTime', + 'status')) + + self.assertEqual(1, len(tree)) + self.assertEqual('driveIds', tree[0].tag) + self.assertEqual(len(vpool['driveIds']), len(tree[0])) + for idx, gr_child in enumerate(tree[0]): + self.assertEqual('driveId', gr_child.tag) + self.assertEqual(str(vpool['driveIds'][idx]), gr_child.text) + + def test_vpool_create_show_serializer(self): + serializer = vsa_ext.VsaVPoolTemplate() + exemplar = dict( + id='vpool_id', + vsaId='vpool_vsa_id', + name='vpool_vsa_name', + displayName='vpool_display_name', + displayDescription='vpool_display_desc', + driveCount=24, + driveIds=['drive1', 'drive2', 'drive3'], + protection='protected', + stripeSize=1024, + stripeWidth=2048, + createTime=datetime.datetime.now(), + status='available') + text = serializer.serialize(dict(vpool=exemplar)) + + print text + tree = etree.fromstring(text) + + self._verify_vpool(exemplar, tree) + + def test_vpool_index_serializer(self): + serializer = vsa_ext.VsaVPoolsTemplate() + exemplar = [dict( + id='vpool1_id', + vsaId='vpool1_vsa_id', + name='vpool1_vsa_name', + displayName='vpool1_display_name', + displayDescription='vpool1_display_desc', + driveCount=24, + driveIds=['drive1', 'drive2', 'drive3'], + protection='protected', + stripeSize=1024, + stripeWidth=2048, + createTime=datetime.datetime.now(), + status='available'), + dict( + id='vpool2_id', + vsaId='vpool2_vsa_id', + name='vpool2_vsa_name', + displayName='vpool2_display_name', + displayDescription='vpool2_display_desc', + driveCount=42, + driveIds=['drive4', 'drive5', 'drive6'], + protection='protected', + stripeSize=512, + stripeWidth=256, + createTime=datetime.datetime.now(), + status='available')] + text = serializer.serialize(dict(vpools=exemplar)) + + print text + tree = etree.fromstring(text) + + self.assertEqual('vpools', tree.tag) + self.assertEqual(len(exemplar), len(tree)) + for idx, child in enumerate(tree): + self._verify_vpool(exemplar[idx], child) diff --git a/nova/tests/api/openstack/compute/contrib/test_zones.py b/nova/tests/api/openstack/compute/contrib/test_zones.py new file mode 100644 index 000000000..e23ea85e6 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_zones.py @@ -0,0 +1,284 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import json + +from lxml import etree + +from nova.api.openstack.compute.contrib import zones +from nova.api.openstack import xmlutil +from nova import crypto +import nova.db +from nova import flags +from nova.scheduler import api +from nova import test +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS + + +def zone_get(context, zone_id): + return dict(id=1, api_url='http://example.com', username='bob', + password='xxx', weight_scale=1.0, weight_offset=0.0, + name='darksecret') + + +def zone_create(context, values): + zone = dict(id=1) + zone.update(values) + return zone + + +def zone_update(context, zone_id, values): + zone = dict(id=zone_id, api_url='http://example.com', username='bob', + password='xxx') + zone.update(values) + return zone + + +def zone_delete(context, zone_id): + pass + + +def zone_get_all_scheduler(*args): + return [ + dict(id=1, api_url='http://example.com', username='bob', + password='xxx', weight_scale=1.0, weight_offset=0.0), + dict(id=2, api_url='http://example.org', username='alice', + password='qwerty', weight_scale=1.0, weight_offset=0.0), + ] + + +def zone_get_all_scheduler_empty(*args): + return [] + + +def zone_get_all_db(context): + return [ + dict(id=1, api_url='http://example.com', username='bob', + password='xxx', weight_scale=1.0, weight_offset=0.0), + dict(id=2, api_url='http://example.org', username='alice', + password='qwerty', weight_scale=1.0, weight_offset=0.0), + ] + + +def zone_capabilities(method, context): + return dict() + + +GLOBAL_BUILD_PLAN = [ + dict(name='host1', weight=10, ip='10.0.0.1', zone='zone1'), + dict(name='host2', weight=9, ip='10.0.0.2', zone='zone2'), + dict(name='host3', weight=8, ip='10.0.0.3', zone='zone3'), + dict(name='host4', weight=7, ip='10.0.0.4', zone='zone4'), + ] + + +def zone_select(context, specs): + return GLOBAL_BUILD_PLAN + + +class ZonesTest(test.TestCase): + def setUp(self): + super(ZonesTest, self).setUp() + self.flags(verbose=True, allow_admin_api=True) + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + + self.stubs.Set(nova.db, 'zone_get', zone_get) + self.stubs.Set(nova.db, 'zone_update', zone_update) + self.stubs.Set(nova.db, 'zone_create', zone_create) + self.stubs.Set(nova.db, 'zone_delete', zone_delete) + + self.controller = zones.Controller() + + def test_get_zone_list_scheduler(self): + self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler) + + req = fakes.HTTPRequest.blank('/v2/fake/zones') + res_dict = self.controller.index(req) + + self.assertEqual(len(res_dict['zones']), 2) + + def test_get_zone_list_db(self): + self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler_empty) + self.stubs.Set(nova.db, 'zone_get_all', zone_get_all_db) + + req = fakes.HTTPRequest.blank('/v2/fake/zones') + res_dict = self.controller.index(req) + + self.assertEqual(len(res_dict['zones']), 2) + + def test_get_zone_by_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/zones/1') + res_dict = self.controller.show(req, 1) + + self.assertEqual(res_dict['zone']['id'], 1) + self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') + self.assertFalse('password' in res_dict['zone']) + + def test_zone_delete(self): + req = fakes.HTTPRequest.blank('/v2/fake/zones/1') + self.controller.delete(req, 1) + + def test_zone_create(self): + body = dict(zone=dict(api_url='http://example.com', username='fred', + password='fubar')) + + req = fakes.HTTPRequest.blank('/v2/fake/zones') + res_dict = self.controller.create(req, body) + + self.assertEqual(res_dict['zone']['id'], 1) + self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') + self.assertFalse('username' in res_dict['zone']) + + def test_zone_update(self): + body = dict(zone=dict(username='zeb', password='sneaky')) + + req = fakes.HTTPRequest.blank('/v2/fake/zones/1') + res_dict = self.controller.update(req, 1, body) + + self.assertEqual(res_dict['zone']['id'], 1) + self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') + self.assertFalse('username' in res_dict['zone']) + + def test_zone_info(self): + caps = ['cap1=a;b', 'cap2=c;d'] + self.flags(zone_name='darksecret', zone_capabilities=caps) + self.stubs.Set(api, '_call_scheduler', zone_capabilities) + + req = fakes.HTTPRequest.blank('/v2/fake/zones/info') + res_dict = self.controller.info(req) + + self.assertEqual(res_dict['zone']['name'], 'darksecret') + self.assertEqual(res_dict['zone']['cap1'], 'a;b') + self.assertEqual(res_dict['zone']['cap2'], 'c;d') + + def test_zone_select(self): + key = 'c286696d887c9aa0611bbb3e2025a45a' + self.flags(build_plan_encryption_key=key) + self.stubs.Set(api, 'select', zone_select) + + # Select queries end up being JSON encoded twice. + # Once to a string and again as an HTTP POST Body + body = json.dumps({}) + + req = fakes.HTTPRequest.blank('/v2/fake/zones/select') + res_dict = self.controller.select(req, body) + + self.assertTrue('weights' in res_dict) + + for item in res_dict['weights']: + blob = item['blob'] + decrypt = crypto.decryptor(FLAGS.build_plan_encryption_key) + secret_item = json.loads(decrypt(blob)) + found = False + for original_item in GLOBAL_BUILD_PLAN: + if original_item['name'] != secret_item['name']: + continue + found = True + for key in ('weight', 'ip', 'zone'): + self.assertEqual(secret_item[key], original_item[key]) + + self.assertTrue(found) + self.assertEqual(len(item), 2) + self.assertTrue('weight' in item) + + +class TestZonesXMLSerializer(test.TestCase): + + def test_select(self): + serializer = zones.WeightsTemplate() + + key = 'c286696d887c9aa0611bbb3e2025a45a' + + encrypt = crypto.encryptor(key) + decrypt = crypto.decryptor(key) + + item = GLOBAL_BUILD_PLAN[0] + fixture = {'weights': {'blob': encrypt(json.dumps(item)), + 'weight': item['weight']}} + + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}weights' % xmlutil.XMLNS_V10) + + for item in res_tree: + self.assertEqual(item.tag, '{%s}weight' % xmlutil.XMLNS_V10) + blob = None + weight = None + for chld in item: + if chld.tag.endswith('blob'): + blob = chld.text + elif chld.tag.endswith('weight'): + weight = chld.text + + secret_item = json.loads(decrypt(blob)) + found = False + for original_item in GLOBAL_BUILD_PLAN: + if original_item['name'] != secret_item['name']: + continue + found = True + for key in ('weight', 'ip', 'zone'): + self.assertEqual(secret_item[key], original_item[key]) + + self.assertTrue(found) + self.assertEqual(len(item), 2) + self.assertTrue(weight) + + def test_index(self): + serializer = zones.ZonesTemplate() + + fixture = {'zones': zone_get_all_scheduler()} + + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}zones' % xmlutil.XMLNS_V10) + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, '{%s}zone' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree[1].tag, '{%s}zone' % xmlutil.XMLNS_V10) + + def test_show(self): + serializer = zones.ZoneTemplate() + + zone = {'id': 1, + 'api_url': 'http://example.com', + 'name': 'darksecret', + 'cap1': 'a;b', + 'cap2': 'c;d'} + fixture = {'zone': zone} + + output = serializer.serialize(fixture) + print repr(output) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}zone' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('id'), '1') + self.assertEqual(res_tree.get('api_url'), 'http://example.com') + self.assertEqual(res_tree.get('password'), None) + + self.assertEqual(res_tree.get('name'), 'darksecret') + for elem in res_tree: + self.assertEqual(elem.tag in ('{%s}cap1' % xmlutil.XMLNS_V10, + '{%s}cap2' % xmlutil.XMLNS_V10), + True) + 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') diff --git a/nova/tests/api/openstack/compute/extensions/__init__.py b/nova/tests/api/openstack/compute/extensions/__init__.py new file mode 100644 index 000000000..848908a95 --- /dev/null +++ b/nova/tests/api/openstack/compute/extensions/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# +# 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. diff --git a/nova/tests/api/openstack/compute/extensions/foxinsocks.py b/nova/tests/api/openstack/compute/extensions/foxinsocks.py new file mode 100644 index 000000000..302fe7ddf --- /dev/null +++ b/nova/tests/api/openstack/compute/extensions/foxinsocks.py @@ -0,0 +1,93 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob.exc + +from nova.api.openstack import extensions + + +class FoxInSocksController(object): + + def index(self, req): + return "Try to say this Mr. Knox, sir..." + + +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_actions(self): + actions = [] + actions.append(extensions.ActionExtension('servers', 'add_tweedle', + self._add_tweedle)) + actions.append(extensions.ActionExtension('servers', 'delete_tweedle', + self._delete_tweedle)) + actions.append(extensions.ActionExtension('servers', 'fail', + self._fail)) + return actions + + def get_request_extensions(self): + request_exts = [] + + def _goose_handler(req, res, body): + #NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + body['flavor']['googoose'] = req.GET.get('chewing') + return res + + req_ext1 = extensions.RequestExtension('GET', + '/v2/:(project_id)/flavors/:(id)', + _goose_handler) + request_exts.append(req_ext1) + + def _bands_handler(req, res, body): + #NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + body['big_bands'] = 'Pig Bands!' + return res + + req_ext2 = extensions.RequestExtension('GET', + '/v2/:(project_id)/flavors/:(id)', + _bands_handler) + request_exts.append(req_ext2) + return request_exts + + def _add_tweedle(self, input_dict, req, id): + + return "Tweedle Beetle Added." + + def _delete_tweedle(self, input_dict, req, id): + + return "Tweedle Beetle Deleted." + + def _fail(self, input_dict, req, id): + + raise webob.exc.HTTPBadRequest(explanation='Tweedle fail') diff --git a/nova/tests/api/openstack/compute/test_api.py b/nova/tests/api/openstack/compute/test_api.py new file mode 100644 index 000000000..15768ff33 --- /dev/null +++ b/nova/tests/api/openstack/compute/test_api.py @@ -0,0 +1,122 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from lxml import etree +import webob.exc +import webob.dec +from webob import Request + +from nova import test +from nova.api import openstack as openstack_api +from nova.api.openstack import compute +from nova.api.openstack.compute import wsgi +from nova.tests.api.openstack import fakes + + +class APITest(test.TestCase): + + 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 = '' + 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) + + body = json.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) + + body = 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 = 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 = Request.blank('/').get_response(api) + self.assertTrue('itemNotFound' in resp.body, 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 = Request.blank('/').get_response(api) + self.assertTrue('{"computeFault' in resp.body, 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 = Request.blank('/.xml').get_response(api) + self.assertTrue(' 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, + "local_gb": 0, + "hostname": "", + "host": host, + "instance_type": {}, + "user_data": "", + "reservation_id": reservation_id, + "mac_address": "", + "scheduled_at": utils.utcnow(), + "launched_at": utils.utcnow(), + "terminated_at": utils.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.TestCase): + 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(utils.gen_uuid()) + 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, 'create_console', fake_create_console) + + req = fakes.HTTPRequest.blank(self.url) + self.controller.create(req, self.uuid) + + 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, 'get_console', fake_get_console) + + req = fakes.HTTPRequest.blank(self.url + '/20') + res_dict = self.controller.show(req, self.uuid, '20') + self.assertDictMatch(res_dict, 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, '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, '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, 'get_consoles', fake_get_consoles) + + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.index(req, self.uuid) + self.assertDictMatch(res_dict, 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, 'get_console', fake_get_console) + self.stubs.Set(console.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, '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, '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.TestCase): + 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/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py new file mode 100644 index 000000000..a4585781b --- /dev/null +++ b/nova/tests/api/openstack/compute/test_extensions.py @@ -0,0 +1,562 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import webob +from lxml import etree + +from nova.api.openstack import compute +from nova.api.openstack import extensions as base_extensions +from nova.api.openstack.compute import extensions as compute_extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes +from nova import wsgi as base_wsgi + +FLAGS = flags.FLAGS + +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" +response_body = "Try to say this Mr. Knox, sir..." + + +class StubController(object): + + def __init__(self, body): + self.body = body + + def index(self, req): + return self.body + + def create(self, req): + msg = 'All aboard the fail train!' + raise webob.exc.HTTPBadRequest(explanation=msg) + + def show(self, req, id): + raise webob.exc.HTTPNotFound() + + +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): + self.resource_ext = resource_ext + self.action_ext = action_ext + self.request_ext = request_ext + + def get_resources(self): + resource_exts = [] + if self.resource_ext: + resource_exts.append(self.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 + + +class ExtensionTestCase(test.TestCase): + def setUp(self): + super(ExtensionTestCase, self).setUp() + ext_list = FLAGS.osapi_compute_extension[:] + fox = ('nova.tests.api.openstack.compute.extensions.' + 'foxinsocks.Foxinsocks') + if fox not in ext_list: + ext_list.append(fox) + self.flags(osapi_compute_extension=ext_list) + compute_extensions.ExtensionManager.reset() + + +class ExtensionControllerTest(ExtensionTestCase): + + def setUp(self): + super(ExtensionControllerTest, self).setUp() + self.flags(allow_admin_api=True) + self.ext_list = [ + "Accounts", + "AdminActions", + "Cloudpipe", + "Console_output", + "Createserverext", + "DeferredDelete", + "DiskConfig", + "ExtendedStatus", + "FlavorExtraSpecs", + "FlavorExtraData", + "Floating_ips", + "Floating_ip_dns", + "Floating_ip_pools", + "Fox In Socks", + "Hosts", + "Keypairs", + "Multinic", + "Quotas", + "Rescue", + "SecurityGroups", + "ServerActionList", + "ServerDiagnostics", + "SimpleTenantUsage", + "Users", + "VSAs", + "VirtualInterfaces", + "Volumes", + "VolumeTypes", + "Zones", + "Networks", + ] + self.ext_list.sort() + + def test_list_extensions_json(self): + app = compute.APIRouter() + ext_midware = compute_extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/fake/extensions") + response = request.get_response(ser_midware) + self.assertEqual(200, response.status_int) + + # Make sure we have all the extensions. + data = json.loads(response.body) + names = [str(x['name']) for x in data['extensions']] + names.sort() + print names + print self.ext_list + self.assertEqual(names, self.ext_list) + + # 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': [] + }, + ) + + def test_get_extension_json(self): + app = compute.APIRouter() + ext_midware = compute_extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/fake/extensions/FOXNSOX") + response = request.get_response(ser_midware) + self.assertEqual(200, response.status_int) + + data = json.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() + ext_midware = compute_extensions.ExtensionMiddleware(app) + request = webob.Request.blank("/fake/extensions/4") + response = request.get_response(ext_midware) + self.assertEqual(404, response.status_int) + + def test_list_extensions_xml(self): + app = compute.APIRouter() + ext_midware = compute_extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/fake/extensions") + request.accept = "application/xml" + response = request.get_response(ser_midware) + self.assertEqual(200, response.status_int) + print response.body + + root = etree.XML(response.body) + self.assertEqual(root.tag.split('extensions')[0], NS) + + # Make sure we have all the extensions. + exts = root.findall('{0}extension'.format(NS)) + self.assertEqual(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() + ext_midware = compute_extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/fake/extensions/FOXNSOX") + request.accept = "application/xml" + response = request.get_response(ser_midware) + self.assertEqual(200, response.status_int) + xml = response.body + print xml + + 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) + ext_midware = compute_extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/blah") + response = request.get_response(ser_midware) + 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) + ext_midware = compute_extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/fake/tweedles") + response = request.get_response(ser_midware) + 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) + ext_midware = compute_extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/fake/tweedles") + response = request.get_response(ser_midware) + 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) + ext_midware = compute_extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/fake/tweedles") + request.method = "POST" + response = request.get_response(ser_midware) + self.assertEqual(400, response.status_int) + self.assertEqual('application/json', response.content_type) + body = json.loads(response.body) + expected = { + "badRequest": { + "message": "All aboard the fail train!", + "code": 400 + } + } + self.assertDictMatch(expected, body) + + def test_non_exist_resource(self): + res_ext = base_extensions.ResourceExtension('tweedles', + StubController(response_body)) + manager = StubExtensionManager(res_ext) + app = compute.APIRouter(manager) + ext_midware = compute_extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/fake/tweedles/1") + response = request.get_response(ser_midware) + self.assertEqual(404, response.status_int) + self.assertEqual('application/json', response.content_type) + body = json.loads(response.body) + expected = { + "itemNotFound": { + "message": "The resource could not be found.", + "code": 404 + } + } + self.assertDictMatch(expected, body) + + +class InvalidExtension(object): + + alias = "THIRD" + + +class AdminExtension(base_extensions.ExtensionDescriptor): + """Admin-only extension""" + + name = "Admin Ext" + alias = "ADMIN" + namespace = "http://www.example.com/" + updated = "2011-01-22T13:25:27-06:00" + admin_only = True + + def __init__(self, *args, **kwargs): + pass + + +class ExtensionManagerTest(ExtensionTestCase): + + response_body = "Try to say this Mr. Knox, sir..." + + def test_get_resources(self): + app = compute.APIRouter() + ext_midware = compute_extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/fake/foxnsocks") + response = request.get_response(ser_midware) + 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 + app = compute.APIRouter() + ext_midware = compute_extensions.ExtensionMiddleware(app) + ext_mgr = ext_midware.ext_mgr + ext_mgr.register(InvalidExtension()) + self.assertTrue('FOXNSOX' in ext_mgr.extensions) + self.assertTrue('THIRD' not in ext_mgr.extensions) + + def test_admin_extensions(self): + self.flags(allow_admin_api=True) + app = compute.APIRouter() + ext_midware = compute_extensions.ExtensionMiddleware(app) + ext_mgr = ext_midware.ext_mgr + ext_mgr.register(AdminExtension()) + self.assertTrue('FOXNSOX' in ext_mgr.extensions) + self.assertTrue('ADMIN' in ext_mgr.extensions) + + def test_admin_extensions_no_admin_api(self): + self.flags(allow_admin_api=False) + app = compute.APIRouter() + ext_midware = compute_extensions.ExtensionMiddleware(app) + ext_mgr = ext_midware.ext_mgr + ext_mgr.register(AdminExtension()) + self.assertTrue('FOXNSOX' in ext_mgr.extensions) + self.assertTrue('ADMIN' not in ext_mgr.extensions) + + +class ActionExtensionTest(ExtensionTestCase): + + def _send_server_action_request(self, url, body): + app = compute.APIRouter() + ext_midware = compute_extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank(url) + request.method = 'POST' + request.content_type = 'application/json' + request.body = json.dumps(body) + response = request.get_response(ser_midware) + 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 = json.loads(response.body) + expected = { + "badRequest": { + "message": "There is no such server action: blah", + "code": 400 + } + } + self.assertDictMatch(expected, 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 = json.loads(response.body) + expected = { + "badRequest": { + "message": "Tweedle fail", + "code": 400 + } + } + self.assertDictMatch(expected, body) + + +class RequestExtensionTest(ExtensionTestCase): + + def test_get_resources_with_stub_mgr(self): + + def _req_handler(req, res, body): + # only handle JSON responses + body['flavor']['googoose'] = req.GET.get('chewing') + return res + + req_ext = base_extensions.RequestExtension('GET', + '/v2/fake/flavors/:(id)', + _req_handler) + + manager = StubExtensionManager(None, None, req_ext) + app = fakes.wsgi_app(serialization=base_wsgi.Middleware) + ext_midware = compute_extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/v2/fake/flavors/1?chewing=bluegoo") + request.environ['api.version'] = '2' + response = request.get_response(ser_midware) + self.assertEqual(200, response.status_int) + response_data = json.loads(response.body) + self.assertEqual('bluegoo', response_data['flavor']['googoose']) + + def test_get_resources_with_mgr(self): + + app = fakes.wsgi_app(serialization=base_wsgi.Middleware) + ext_midware = compute_extensions.ExtensionMiddleware(app) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/v2/fake/flavors/1?chewing=newblue") + request.environ['api.version'] = '2' + response = request.get_response(ser_midware) + self.assertEqual(200, response.status_int) + response_data = json.loads(response.body) + print response_data + self.assertEqual('newblue', response_data['flavor']['googoose']) + self.assertEqual("Pig Bands!", response_data['big_bands']) + + +class ExtensionsXMLSerializerTest(test.TestCase): + + def test_serialize_extension(self): + serializer = base_extensions.ExtensionsXMLSerializer() + 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, 'show') + print xml + 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.ExtensionsXMLSerializer() + 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", + "type": "application/vnd.sun.wadl+xml", + "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, 'index') + print xml + 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') diff --git a/nova/tests/api/openstack/compute/test_flavors.py b/nova/tests/api/openstack/compute/test_flavors.py new file mode 100644 index 000000000..37b67fc32 --- /dev/null +++ b/nova/tests/api/openstack/compute/test_flavors.py @@ -0,0 +1,652 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from lxml import etree +import webob + +from nova.api.openstack.compute import flavors +from nova.api.openstack import xmlutil +import nova.compute.instance_types +from nova import exception +from nova import test +from nova.tests.api.openstack import fakes + + +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', + "local_gb": '10' + }, + 'flavor 2': { + "flavorid": '2', + "name": 'flavor 2', + "memory_mb": '512', + "local_gb": '20' + }, +} + + +def fake_instance_type_get_by_flavor_id(flavorid): + return FAKE_FLAVORS['flavor %s' % flavorid] + + +def fake_instance_type_get_all(inactive=False, filters=None): + def reject_min(db_attr, filter_attr): + return filter_attr in filters and\ + int(flavor[db_attr]) < int(filters[filter_attr]) + + filters = filters or {} + output = {} + for (flavor_name, flavor) in FAKE_FLAVORS.items(): + if reject_min('memory_mb', 'min_memory_mb'): + continue + elif reject_min('local_gb', 'min_local_gb'): + continue + + output[flavor_name] = flavor + + return output + + +def empty_instance_type_get_all(inactive=False, filters=None): + return {} + + +def return_instance_type_not_found(flavor_id): + raise exception.InstanceTypeNotFound(flavor_id=flavor_id) + + +class FlavorsTest(test.TestCase): + def setUp(self): + super(FlavorsTest, self).setUp() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + self.stubs.Set(nova.compute.instance_types, "get_all_types", + fake_instance_type_get_all) + self.stubs.Set(nova.compute.instance_types, + "get_instance_type_by_flavor_id", + fake_instance_type_get_by_flavor_id) + + self.controller = flavors.Controller() + + def tearDown(self): + self.stubs.UnsetAll() + super(FlavorsTest, self).tearDown() + + def test_get_flavor_by_invalid_id(self): + self.stubs.Set(nova.compute.instance_types, + "get_instance_type_by_flavor_id", + return_instance_type_not_found) + req = fakes.HTTPRequest.blank('/v2/fake/flavors/asdf') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, 'asdf') + + def test_get_flavor_by_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1') + flavor = self.controller.show(req, '1') + expected = { + "flavor": { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "rxtx_factor": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/1", + }, + ], + }, + } + self.assertEqual(flavor, expected) + + def test_get_flavor_list(self): + req = fakes.HTTPRequest.blank('/v2/fake/flavors') + flavor = self.controller.index(req) + 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", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + def test_get_flavor_list_detail(self): + req = fakes.HTTPRequest.blank('/v2/fake/flavors/detail') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "rxtx_factor": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "rxtx_factor": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + def test_get_empty_flavor_list(self): + self.stubs.Set(nova.compute.instance_types, "get_all_types", + empty_instance_type_get_all) + + req = fakes.HTTPRequest.blank('/v2/fake/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 = fakes.HTTPRequest.blank('/v2/fake/flavors?minRam=512') + flavor = self.controller.index(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + def test_get_flavor_list_filter_min_disk(self): + """Flavor lists may be filtered by minRam""" + req = fakes.HTTPRequest.blank('/v2/fake/flavors?minDisk=20') + flavor = self.controller.index(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + 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 = fakes.HTTPRequest.blank('/v2/fake/flavors/detail' + '?minRam=256&minDisk=20') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "rxtx_factor": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + def test_get_flavor_list_detail_bogus_min_ram(self): + """Tests that bogus minRam filtering values are ignored""" + req = fakes.HTTPRequest.blank('/v2/fake/flavors/detail?minRam=16GB') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "rxtx_factor": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "rxtx_factor": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + def test_get_flavor_list_detail_bogus_min_disk(self): + """Tests that bogus minDisk filtering values are ignored""" + req = fakes.HTTPRequest.blank('/v2/fake/flavors/detail?minDisk=16GB') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "rxtx_factor": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "rxtx_factor": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + +class FlavorsXMLSerializationTest(test.TestCase): + + def test_xml_declaration(self): + serializer = flavors.FlavorTemplate() + + fixture = { + "flavor": { + "id": "12", + "name": "asdf", + "ram": "256", + "disk": "10", + "rxtx_factor": "1", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture) + print output + has_dec = output.startswith("") + self.assertTrue(has_dec) + + def test_show(self): + serializer = flavors.FlavorTemplate() + + fixture = { + "flavor": { + "id": "12", + "name": "asdf", + "ram": "256", + "disk": "10", + "rxtx_factor": "1", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture) + print output + 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.FlavorTemplate() + + fixture = { + "flavor": { + "id": 12, + "name": "asdf", + "ram": 256, + "disk": 10, + "rxtx_factor": "1", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture) + print output + 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.FlavorsTemplate() + + fixture = { + "flavors": [ + { + "id": "23", + "name": "flavor 23", + "ram": "512", + "disk": "20", + "rxtx_factor": "1", + "swap": "", + "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", + "rxtx_factor": "1", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/13", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/13", + }, + ], + }, + ], + } + + output = serializer.serialize(fixture) + print output + 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.MinimalFlavorsTemplate() + + fixture = { + "flavors": [ + { + "id": "23", + "name": "flavor 23", + "ram": "512", + "disk": "20", + "rxtx_factor": "1", + "swap": "", + "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", + "rxtx_factor": "1", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/13", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/13", + }, + ], + }, + ], + } + + output = serializer.serialize(fixture) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavors_index') + 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.MinimalFlavorsTemplate() + + fixture = { + "flavors": [], + } + + output = serializer.serialize(fixture) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavors_index') + flavor_elems = root.findall('{0}flavor'.format(NS)) + self.assertEqual(len(flavor_elems), 0) diff --git a/nova/tests/api/openstack/compute/test_image_metadata.py b/nova/tests/api/openstack/compute/test_image_metadata.py new file mode 100644 index 000000000..62f047a2a --- /dev/null +++ b/nova/tests/api/openstack/compute/test_image_metadata.py @@ -0,0 +1,200 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import webob + +from nova.api.openstack.compute import image_metadata +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS + + +class ImageMetaDataTest(test.TestCase): + + def setUp(self): + super(ImageMetaDataTest, self).setUp() + fakes.stub_out_glance(self.stubs) + self.controller = image_metadata.Controller() + + def test_index(self): + 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) + + def test_show(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + res_dict = self.controller.show(req, '123', 'key1') + self.assertTrue('meta' in res_dict) + self.assertEqual(len(res_dict['meta']), 1) + self.assertEqual('value1', res_dict['meta']['key1']) + + def test_show_not_found(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key9') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, '123', 'key9') + + def test_show_image_not_found(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, '100', 'key9') + + def test_create(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') + req.method = 'POST' + body = {"metadata": {"key7": "value7"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, '123', body) + + expected_output = {'metadata': {'key1': 'value1', 'key7': 'value7'}} + self.assertEqual(expected_output, res) + + def test_create_image_not_found(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata') + req.method = 'POST' + body = {"metadata": {"key7": "value7"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.create, req, '100', body) + + def test_update_all(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') + req.method = 'PUT' + body = {"metadata": {"key9": "value9"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.update_all(req, '123', body) + + expected_output = {'metadata': {'key9': 'value9'}} + self.assertEqual(expected_output, res) + + def test_update_all_image_not_found(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata') + req.method = 'PUT' + body = {"metadata": {"key9": "value9"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update_all, req, '100', body) + + def test_update_item(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'PUT' + body = {"meta": {"key1": "zz"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.update(req, '123', 'key1', body) + + expected_output = {'meta': {'key1': 'zz'}} + self.assertEqual(res, expected_output) + + def test_update_item_image_not_found(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1') + req.method = 'PUT' + body = {"meta": {"key1": "zz"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, req, '100', 'key1', body) + + def test_update_item_bad_body(self): + 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) + + def test_update_item_too_many_keys(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'PUT' + overload = {} + for num in range(FLAGS.quota_metadata_items + 1): + overload['key%s' % num] = 'value%s' % num + body = {'meta': overload} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, '123', 'key1', body) + + def test_update_item_body_uri_mismatch(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/bad') + req.method = 'PUT' + body = {"meta": {"key1": "value1"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, '123', 'bad', body) + + def test_delete(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'DELETE' + res = self.controller.delete(req, '123', 'key1') + + self.assertEqual(None, res) + + def test_delete_not_found(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/blah') + req.method = 'DELETE' + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, '123', 'blah') + + def test_delete_image_not_found(self): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1') + req.method = 'DELETE' + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, '100', 'key1') + + def test_too_many_metadata_items_on_create(self): + data = {"metadata": {}} + for num in range(FLAGS.quota_metadata_items + 1): + data['metadata']['key%i' % num] = "blah" + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') + req.method = 'POST' + req.body = json.dumps(data) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller.create, req, '123', data) + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller.create, req, '123', data) + + def test_too_many_metadata_items_on_put(self): + FLAGS.quota_metadata_items = 1 + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/blah') + req.method = 'PUT' + body = {"meta": {"blah": "blah"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller.update, req, '123', 'blah', body) diff --git a/nova/tests/api/openstack/compute/test_images.py b/nova/tests/api/openstack/compute/test_images.py new file mode 100644 index 000000000..7bb1d44d9 --- /dev/null +++ b/nova/tests/api/openstack/compute/test_images.py @@ -0,0 +1,1646 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Tests of the new image services, both as a service layer, +and as a WSGI layer +""" + +import urlparse + +from lxml import etree +import stubout +import webob + +from nova.api.openstack.compute import images +from nova.api.openstack.compute.views import images as images_view +from nova.api.openstack import xmlutil +from nova import test +from nova import utils +from nova.tests.api.openstack import fakes + + +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" +NOW_API_FORMAT = "2010-10-11T10:30:22Z" + + +class ImagesControllerTest(test.TestCase): + """ + Test of the OpenStack API /images application controller w/Glance. + """ + + def setUp(self): + """Run before each test.""" + super(ImagesControllerTest, self).setUp() + self.maxDiff = None + self.stubs = stubout.StubOutForTesting() + 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) + fakes.stub_out_glance(self.stubs) + + self.controller = images.Controller() + + def tearDown(self): + """Run after each test.""" + self.stubs.UnsetAll() + super(ImagesControllerTest, self).tearDown() + + def test_get_image(self): + fake_req = fakes.HTTPRequest.blank('/v2/fake/images/123') + actual_image = self.controller.show(fake_req, '124') + + href = "http://localhost/v2/fake/images/124" + bookmark = "http://localhost/fake/images/124" + alternate = "%s/fake/images/124" % utils.generate_glance_url() + server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" + server_href = "http://localhost/v2/servers/" + server_uuid + server_bookmark = "http://localhost/servers/" + server_uuid + + expected_image = { + "image": { + "id": "124", + "name": "queued snapshot", + "updated": NOW_API_FORMAT, + "created": NOW_API_FORMAT, + "status": "SAVING", + "progress": 25, + "minDisk": 0, + "minRam": 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "metadata": { + "instance_ref": server_href, + "user_id": "fake", + }, + "links": [{ + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "href": bookmark, + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate + }], + }, + } + + self.assertDictMatch(expected_image, actual_image) + + def test_get_image_404(self): + fake_req = fakes.HTTPRequest.blank('/v2/fake/images/unknown') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, fake_req, 'unknown') + + def test_get_image_index(self): + fake_req = fakes.HTTPRequest.blank('/v2/fake/images') + response_list = self.controller.index(fake_req)['images'] + + expected_images = [ + { + "id": "123", + "name": "public image", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/123", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/123", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/123" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "124", + "name": "queued snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/124", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/124", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/124" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "125", + "name": "saving snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/125", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/125", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/125" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "126", + "name": "active snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/126", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/126", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/126" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "127", + "name": "killed snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/127", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/127", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/127" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "128", + "name": "deleted snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/128", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/128", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/128" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "129", + "name": "pending_delete snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/129", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/129", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/129" % + utils.generate_glance_url() + }, + ], + }, + { + "id": "130", + "name": None, + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/130", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/130", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/130" % + utils.generate_glance_url() + }, + ], + }, + ] + + self.assertDictListMatch(response_list, expected_images) + + def test_get_image_index_with_limit(self): + request = fakes.HTTPRequest.blank('/v2/fake/images?limit=3') + response = self.controller.index(request) + response_list = response["images"] + response_links = response["images_links"] + + alternate = "%s/fake/images/%s" + + expected_images = [ + { + "id": "123", + "name": "public image", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/123", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/123", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (utils.generate_glance_url(), 123), + }, + ], + }, + { + "id": "124", + "name": "queued snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/124", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/124", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (utils.generate_glance_url(), 124), + }, + ], + }, + { + "id": "125", + "name": "saving snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/images/125", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/125", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (utils.generate_glance_url(), 125), + }, + ], + }, + ] + + self.assertDictListMatch(response_list, expected_images) + self.assertEqual(response_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(response_links[0]['href']) + self.assertEqual('/v2/fake/images', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + self.assertDictMatch({'limit': ['3'], 'marker': ['125']}, params) + + def test_get_image_index_with_limit_and_extra_params(self): + request = fakes.HTTPRequest.blank('/v2/fake/images?limit=3&extra=bo') + response = self.controller.index(request) + response_links = response["images_links"] + + self.assertEqual(response_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(response_links[0]['href']) + self.assertEqual('/v2/fake/images', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + self.assertDictMatch( + {'limit': ['3'], 'marker': ['125'], 'extra': ['bo']}, + params) + + def test_get_image_index_with_big_limit(self): + """ + Make sure we don't get images_links if limit is set + and the number of images returned is < limit + """ + request = fakes.HTTPRequest.blank('/v2/fake/images?limit=30') + response = self.controller.index(request) + + self.assertEqual(response.keys(), ['images']) + self.assertEqual(len(response['images']), 8) + + def test_get_image_details(self): + request = fakes.HTTPRequest.blank('/v2/fake/images/detail') + response = self.controller.detail(request) + response_list = response["images"] + + server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" + server_href = "http://localhost/v2/servers/" + server_uuid + server_bookmark = "http://localhost/servers/" + server_uuid + alternate = "%s/fake/images/%s" + + expected = [{ + 'id': '123', + 'name': 'public image', + 'metadata': {'key1': 'value1'}, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ACTIVE', + 'progress': 100, + 'minDisk': 10, + 'minRam': 128, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/123", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/123", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (utils.generate_glance_url(), 123), + }], + }, + { + 'id': '124', + 'name': 'queued snapshot', + 'metadata': { + u'instance_ref': server_href, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', + 'progress': 25, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/124", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/124", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (utils.generate_glance_url(), 124), + }], + }, + { + 'id': '125', + 'name': 'saving snapshot', + 'metadata': { + u'instance_ref': server_href, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', + 'progress': 50, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/125", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/125", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/125" % utils.generate_glance_url() + }], + }, + { + 'id': '126', + 'name': 'active snapshot', + 'metadata': { + u'instance_ref': server_href, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ACTIVE', + 'progress': 100, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/126", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/126", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/126" % utils.generate_glance_url() + }], + }, + { + 'id': '127', + 'name': 'killed snapshot', + 'metadata': { + u'instance_ref': server_href, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ERROR', + 'progress': 0, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/127", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/127", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/127" % utils.generate_glance_url() + }], + }, + { + 'id': '128', + 'name': 'deleted snapshot', + 'metadata': { + u'instance_ref': server_href, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/128", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/128", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/128" % utils.generate_glance_url() + }], + }, + { + 'id': '129', + 'name': 'pending_delete snapshot', + 'metadata': { + u'instance_ref': server_href, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/129", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/129", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/129" % utils.generate_glance_url() + }], + }, + { + 'id': '130', + 'name': None, + 'metadata': {}, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ACTIVE', + 'progress': 100, + 'minDisk': 0, + 'minRam': 0, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/130", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/130", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/130" % utils.generate_glance_url() + }], + }, + ] + + self.assertDictListMatch(expected, response_list) + + def test_get_image_details_with_limit(self): + request = fakes.HTTPRequest.blank('/v2/fake/images/detail?limit=2') + response = self.controller.detail(request) + response_list = response["images"] + response_links = response["images_links"] + + server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" + server_href = "http://localhost/v2/servers/" + server_uuid + server_bookmark = "http://localhost/servers/" + server_uuid + alternate = "%s/fake/images/%s" + + expected = [{ + '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": "http://localhost/v2/fake/images/123", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/123", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (utils.generate_glance_url(), 123), + }], + }, + { + 'id': '124', + 'name': 'queued snapshot', + 'metadata': { + u'instance_ref': server_href, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', + 'minDisk': 0, + 'progress': 25, + 'minRam': 0, + 'server': { + 'id': server_uuid, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/124", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/124", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate % (utils.generate_glance_url(), 124), + }], + }] + + self.assertDictListMatch(expected, response_list) + + href_parts = urlparse.urlparse(response_links[0]['href']) + self.assertEqual('/v2/fake/images', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + + self.assertDictMatch({'limit': ['2'], 'marker': ['124']}, params) + + def test_image_filter_with_name(self): + image_service = self.mox.CreateMockAnything() + filters = {'name': 'testname'} + request = fakes.HTTPRequest.blank('/v2/images?name=testname') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_with_min_ram(self): + image_service = self.mox.CreateMockAnything() + filters = {'min_ram': '0'} + request = fakes.HTTPRequest.blank('/v2/images?minRam=0') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_with_min_disk(self): + image_service = self.mox.CreateMockAnything() + filters = {'min_disk': '7'} + request = fakes.HTTPRequest.blank('/v2/images?minDisk=7') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_with_status(self): + image_service = self.mox.CreateMockAnything() + filters = {'status': 'ACTIVE'} + request = fakes.HTTPRequest.blank('/v2/images?status=ACTIVE') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_with_property(self): + image_service = self.mox.CreateMockAnything() + filters = {'property-test': '3'} + request = fakes.HTTPRequest.blank('/v2/images?property-test=3') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_server(self): + image_service = self.mox.CreateMockAnything() + uuid = 'fa95aaf5-ab3b-4cd8-88c0-2be7dd051aaf' + ref = 'http://localhost:8774/servers/' + uuid + filters = {'property-instance_ref': ref} + request = fakes.HTTPRequest.blank('/v2/images?server=' + ref) + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_changes_since(self): + image_service = self.mox.CreateMockAnything() + filters = {'changes-since': '2011-01-24T17:08Z'} + request = fakes.HTTPRequest.blank('/v2/images?changes-since=' + '2011-01-24T17:08Z') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_with_type(self): + image_service = self.mox.CreateMockAnything() + filters = {'property-image_type': 'BASE'} + request = fakes.HTTPRequest.blank('/v2/images?type=BASE') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_not_supported(self): + image_service = self.mox.CreateMockAnything() + filters = {'status': 'ACTIVE'} + request = fakes.HTTPRequest.blank('/v2/images?status=ACTIVE&' + 'UNSUPPORTEDFILTER=testname') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + self.mox.VerifyAll() + + def test_image_no_filters(self): + image_service = self.mox.CreateMockAnything() + filters = {} + request = fakes.HTTPRequest.blank('/v2/images') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_detail_filter_with_name(self): + image_service = self.mox.CreateMockAnything() + filters = {'name': 'testname'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail' + '?name=testname') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + self.mox.VerifyAll() + + def test_image_detail_filter_with_status(self): + image_service = self.mox.CreateMockAnything() + filters = {'status': 'ACTIVE'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail' + '?status=ACTIVE') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + self.mox.VerifyAll() + + def test_image_detail_filter_with_property(self): + image_service = self.mox.CreateMockAnything() + filters = {'property-test': '3'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail' + '?property-test=3') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + self.mox.VerifyAll() + + def test_image_detail_filter_server(self): + image_service = self.mox.CreateMockAnything() + uuid = 'fa95aaf5-ab3b-4cd8-88c0-2be7dd051aaf' + ref = 'http://localhost:8774/servers/' + uuid + url = '/v2/fake/images/detail?server=' + ref + filters = {'property-instance_ref': ref} + request = fakes.HTTPRequest.blank(url) + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_detail_filter_changes_since(self): + image_service = self.mox.CreateMockAnything() + filters = {'changes-since': '2011-01-24T17:08Z'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail' + '?changes-since=2011-01-24T17:08Z') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_detail_filter_with_type(self): + image_service = self.mox.CreateMockAnything() + filters = {'property-image_type': 'BASE'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail?type=BASE') + context = request.environ['nova.context'] + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_detail_filter_not_supported(self): + image_service = self.mox.CreateMockAnything() + filters = {'status': 'ACTIVE'} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail?status=' + 'ACTIVE&UNSUPPORTEDFILTER=testname') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + self.mox.VerifyAll() + + def test_image_detail_no_filters(self): + image_service = self.mox.CreateMockAnything() + filters = {} + request = fakes.HTTPRequest.blank('/v2/fake/images/detail') + context = request.environ['nova.context'] + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + controller = images.Controller(image_service=image_service) + controller.detail(request) + self.mox.VerifyAll() + + def test_generate_alternate_link(self): + view = images_view.ViewBuilder() + request = fakes.HTTPRequest.blank('/v2/fake/images/1') + generated_url = view._get_alternate_link(request, 1) + actual_url = "%s/fake/images/1" % utils.generate_glance_url() + self.assertEqual(generated_url, actual_url) + + def test_delete_image(self): + request = fakes.HTTPRequest.blank('/v2/fake/images/124') + request.method = 'DELETE' + response = self.controller.delete(request, '124') + self.assertEqual(response.status_int, 204) + + def test_delete_image_not_found(self): + request = fakes.HTTPRequest.blank('/v2/fake/images/300') + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, request, '300') + + +class ImageXMLSerializationTest(test.TestCase): + + TIMESTAMP = "2010-10-11T10:30:22Z" + SERVER_UUID = 'aa640691-d1a7-4a67-9d3c-d35ee6b3cc74' + SERVER_HREF = 'http://localhost/v2/servers/' + SERVER_UUID + SERVER_BOOKMARK = 'http://localhost/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("") + 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.assertEqual(server_root, None) + + 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_index') + 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_index') + 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_index') + 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/api/openstack/compute/test_limits.py b/nova/tests/api/openstack/compute/test_limits.py new file mode 100644 index 000000000..20ec0d1a0 --- /dev/null +++ b/nova/tests/api/openstack/compute/test_limits.py @@ -0,0 +1,939 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Tests dealing with HTTP rate-limiting. +""" + +import httplib +import json +import StringIO +import unittest +from xml.dom import minidom + +from lxml import etree +import stubout +import webob + +from nova.api.openstack.compute import limits +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 + + +TEST_LIMITS = [ + limits.Limit("GET", "/delayed", "^/delayed", 1, limits.PER_MINUTE), + limits.Limit("POST", "*", ".*", 7, limits.PER_MINUTE), + limits.Limit("POST", "/servers", "^/servers", 3, limits.PER_MINUTE), + limits.Limit("PUT", "*", "", 10, limits.PER_MINUTE), + limits.Limit("PUT", "/servers", "^/servers", 5, limits.PER_MINUTE), +] +NS = { + 'atom': 'http://www.w3.org/2005/Atom', + 'ns': 'http://docs.openstack.org/compute/api/v1.1' +} + + +class BaseLimitTestSuite(unittest.TestCase): + """Base test suite which provides relevant stubs and time abstraction.""" + + def setUp(self): + """Run before each test.""" + self.time = 0.0 + self.stubs = stubout.StubOutForTesting() + self.stubs.Set(limits.Limit, "_get_time", self._get_time) + self.absolute_limits = {} + + def stub_get_project_quotas(context, project_id): + return self.absolute_limits + + self.stubs.Set(nova.quota, "get_project_quotas", + stub_get_project_quotas) + + def tearDown(self): + """Run after each test.""" + self.stubs.UnsetAll() + + def _get_time(self): + """Return the "time" according to this test suite.""" + return self.time + + +class LimitsControllerTest(BaseLimitTestSuite): + """ + Tests for `limits.LimitsController` class. + """ + + def setUp(self): + """Run before each test.""" + BaseLimitTestSuite.setUp(self) + self.controller = wsgi.LazySerializationMiddleware( + limits.create_resource()) + self.maxDiff = None + + def _get_index_request(self, accept_header="application/json"): + """Helper to set routing arguments.""" + request = webob.Request.blank("/") + 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 = json.loads(response.body) + self.assertEqual(expected, body) + + def test_index_json(self): + """Test getting limit details in JSON.""" + request = self._get_index_request() + request = self._populate_limits(request) + self.absolute_limits = { + 'ram': 512, + 'instances': 5, + 'cores': 21, + } + 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, + }, + { + "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, + }, + }, + } + body = json.loads(response.body) + self.assertEqual(expected, body) + + 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 = json.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 = json.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) + + +class TestLimiter(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.""" + BaseLimitTestSuite.setUp(self) + _limits = '(GET, *, .*, 1, MINUTE)' + self.app = limits.RateLimitingMiddleware(self._empty_app, _limits, + "%s.TestLimiter" % + self.__class__.__module__) + + def test_limit_class(self): + """Test that middleware selected correct limiter class.""" + assert isinstance(self.app._limiter, TestLimiter) + + 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 (413) 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, 413) + + body = json.loads(response.body) + expected = "Only 1 GET request(s) can be made to * every minute." + value = body["overLimitFault"]["details"].strip() + self.assertEqual(value, expected) + + def test_limited_request_xml(self): + """Test a rate-limited (413) 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, 413) + + root = minidom.parseString(response.body).childNodes[0] + expected = "Only 1 GET request(s) can be made to * every minute." + + 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.assertEqual(None, 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.assertEqual(None, 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.assertEqual(None, 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, e: + assert False, str(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 = [limits.PER_MINUTE, limits.PER_HOUR, + limits.PER_SECOND, limits.PER_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.""" + BaseLimitTestSuite.setUp(self) + userlimits = {'user:user3': ''} + 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.failUnlessAlmostEqual(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) + + 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) + + def test_user_limit(self): + """ + Test user-specific limits. + """ + self.assertEqual(self.limiter.levels['user3'], []) + + def test_multiple_users(self): + """ + Tests involving multiple users. + """ + # 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) + + +class WsgiLimiterTest(BaseLimitTestSuite): + """ + Tests for `limits.WsgiLimiter` class. + """ + + def setUp(self): + """Run before each test.""" + BaseLimitTestSuite.setUp(self) + self.app = limits.WsgiLimiter(TEST_LIMITS) + + def _request_data(self, verb, path): + """Get data decribing a limit request verb/path.""" + return json.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.""" + requests = [] + 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.assertEqual(delay, None) + + def test_escaping(self): + delay = self._request("GET", "/something/jump%20up") + self.assertEqual(delay, None) + + def test_response_to_delays(self): + delay = self._request("GET", "/delayed") + self.assertEqual(delay, None) + + delay = self._request("GET", "/delayed") + self.assertEqual(delay, '60.00') + + def test_response_to_delays_usernames(self): + delay = self._request("GET", "/delayed", "user1") + self.assertEqual(delay, None) + + delay = self._request("GET", "/delayed", "user2") + self.assertEqual(delay, None) + + 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. + """ + 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) + + httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) + + +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. + """ + BaseLimitTestSuite.setUp(self) + self.app = limits.WsgiLimiter(TEST_LIMITS) + 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) + + +class LimitsViewBuilderTest(test.TestCase): + + def setUp(self): + 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 tearDown(self): + pass + + 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.assertDictMatch(output, 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.assertDictMatch(output, expected_limits) + + +class LimitsXMLSerializationTest(test.TestCase): + + def setUp(self): + self.maxDiff = None + + def tearDown(self): + pass + + def test_xml_declaration(self): + serializer = limits.LimitsTemplate() + + fixture = {"limits": { + "rate": [], + "absolute": {}}} + + output = serializer.serialize(fixture) + print output + has_dec = output.startswith("") + 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) + print output + 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) + print output + 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/api/openstack/compute/test_server_actions.py b/nova/tests/api/openstack/compute/test_server_actions.py new file mode 100644 index 000000000..2f3976375 --- /dev/null +++ b/nova/tests/api/openstack/compute/test_server_actions.py @@ -0,0 +1,834 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +import datetime + +import mox +import stubout +import webob + +from nova.api.openstack.compute import servers +from nova.compute import vm_states +from nova.compute import instance_types +from nova import context +import nova.db +from nova import exception +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes +from nova import utils + + +FLAGS = flags.FLAGS +FAKE_UUID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + + +def return_server_by_id(context, id): + return stub_instance(id) + + +def return_server_by_uuid(context, uuid): + return stub_instance(1, uuid=uuid) + + +def return_server_by_uuid_not_found(context, uuid): + raise exception.NotFound() + + +def instance_update(context, instance_id, kwargs): + return stub_instance(instance_id) + + +def return_server_with_attributes(**kwargs): + def _return_server(context, id): + return stub_instance(id, **kwargs) + return _return_server + + +def return_server_with_state(vm_state, task_state=None): + return return_server_with_attributes(vm_state=vm_state, + task_state=task_state) + + +def return_server_with_uuid_and_state(vm_state, task_state=None): + def _return_server(context, id): + return return_server_with_state(vm_state, task_state) + return _return_server + + +def stub_instance(id, metadata=None, image_ref="10", flavor_id="1", + name=None, vm_state=None, task_state=None, uuid=None, + access_ip_v4="", access_ip_v6=""): + if metadata is not None: + metadata_items = [{'key':k, 'value':v} for k, v in metadata.items()] + else: + metadata_items = [{'key':'seq', 'value':id}] + + if uuid is None: + uuid = FAKE_UUID + + inst_type = instance_types.get_instance_type_by_flavor_id(int(flavor_id)) + + instance = { + "id": int(id), + "name": str(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": "fake", + "project_id": "fake", + "image_ref": image_ref, + "kernel_id": "", + "ramdisk_id": "", + "launch_index": 0, + "key_name": "", + "key_data": "", + "vm_state": vm_state or vm_states.ACTIVE, + "task_state": task_state, + "memory_mb": 0, + "vcpus": 0, + "local_gb": 0, + "hostname": "", + "host": "", + "instance_type": dict(inst_type), + "user_data": "", + "reservation_id": "", + "mac_address": "", + "scheduled_at": utils.utcnow(), + "launched_at": utils.utcnow(), + "terminated_at": utils.utcnow(), + "availability_zone": "", + "display_name": name or "server%s" % id, + "display_description": "", + "locked": False, + "metadata": metadata_items, + "access_ip_v4": access_ip_v4, + "access_ip_v6": access_ip_v6, + "uuid": uuid, + "virtual_interfaces": [], + "progress": 0, + } + + instance["fixed_ips"] = [{"address": '192.168.0.1', + "network": + {'label': 'public', 'cidr_v6': None}, + "virtual_interface": + {'address': 'aa:aa:aa:aa:aa:aa'}, + "floating_ips": []}] + + return instance + + +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): + + def setUp(self): + self.maxDiff = None + super(ServerActionsControllerTest, self).setUp() + + self.stubs = stubout.StubOutForTesting() + fakes.stub_out_auth(self.stubs) + self.stubs.Set(nova.db, 'instance_get', return_server_by_id) + self.stubs.Set(nova.db, 'instance_get_by_uuid', return_server_by_uuid) + self.stubs.Set(nova.db, 'instance_update', instance_update) + + fakes.stub_out_glance(self.stubs) + fakes.stub_out_nw_api(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + self.snapshot = fakes.stub_out_compute_api_snapshot(self.stubs) + service_class = 'nova.image.glance.GlanceImageService' + self.service = utils.import_object(service_class) + self.context = context.RequestContext(1, None) + self.service.delete_all() + self.sent_to_glance = {} + fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) + self.flags(allow_instance_snapshots=True) + self.uuid = FAKE_UUID + self.url = '/v2/fake/servers/%s/action' % self.uuid + + self.controller = servers.Controller() + + def tearDown(self): + self.stubs.UnsetAll() + super(ServerActionsControllerTest, self).tearDown() + + def test_server_bad_body(self): + body = {} + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + def test_server_unknown_action(self): + body = {'sockTheFox': {'fakekey': '1234'}} + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + def test_server_change_password(self): + mock_method = MockSetAdminPassword() + self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) + body = {'changePassword': {'adminPass': '1234pass'}} + + req = fakes.HTTPRequest.blank(self.url) + self.controller.action(req, FAKE_UUID, body) + + self.assertEqual(mock_method.instance_id, self.uuid) + 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, 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, req, FAKE_UUID, body) + + def test_server_change_password_empty_string(self): + body = {'changePassword': {'adminPass': ''}} + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + 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, req, FAKE_UUID, body) + + def test_reboot_hard(self): + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequest.blank(self.url) + self.controller.action(req, FAKE_UUID, body) + + def test_reboot_soft(self): + body = dict(reboot=dict(type="SOFT")) + req = fakes.HTTPRequest.blank(self.url) + self.controller.action(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, 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, req, FAKE_UUID, body) + + def test_reboot_not_found(self): + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_by_uuid_not_found) + + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.action, + req, str(utils.gen_uuid()), body) + + def test_reboot_raises_conflict_on_invalid_state(self): + body = dict(reboot=dict(type="HARD")) + + def fake_reboot(*args, **kwargs): + raise exception.InstanceInvalidState + + self.stubs.Set(nova.compute.api.API, 'reboot', fake_reboot) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, self.controller.action, + req, FAKE_UUID, body) + + def test_rebuild_accepted_minimum(self): + new_return_server = return_server_with_attributes(image_ref='2') + self.stubs.Set(nova.db, 'instance_get', new_return_server) + self_href = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID + + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + robj = self.controller.action(req, FAKE_UUID, body) + body = robj.obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertEqual(len(body['server']['adminPass']), + FLAGS.password_length) + self.assertEqual(robj['location'], self_href) + + def test_rebuild_raises_conflict_on_invalid_state(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + def fake_rebuild(*args, **kwargs): + raise exception.InstanceInvalidState + + self.stubs.Set(nova.compute.api.API, 'rebuild', fake_rebuild) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller.action, req, FAKE_UUID, body) + + def test_rebuild_accepted_with_metadata(self): + metadata = {'new': 'metadata'} + + new_return_server = return_server_with_attributes(metadata=metadata) + self.stubs.Set(nova.db, 'instance_get', new_return_server) + + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "metadata": metadata, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller.action(req, FAKE_UUID, body).obj + + self.assertEqual(body['server']['metadata'], metadata) + + def test_rebuild_accepted_with_bad_metadata(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "metadata": "stack", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + def test_rebuild_bad_entity(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + def test_rebuild_bad_personality(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "personality": [{ + "path": "/path/to/file", + "contents": "INVALID b64", + }] + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + def test_rebuild_personality(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "personality": [{ + "path": "/path/to/file", + "contents": base64.b64encode("Test String"), + }] + }, + } + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller.action(req, FAKE_UUID, body).obj + + self.assertTrue('personality' not in body['server']) + + def test_rebuild_admin_pass(self): + new_return_server = return_server_with_attributes(image_ref='2') + self.stubs.Set(nova.db, 'instance_get', new_return_server) + + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "adminPass": "asdf", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller.action(req, FAKE_UUID, body).obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertEqual(body['server']['adminPass'], 'asdf') + + def test_rebuild_server_not_found(self): + def server_not_found(self, instance_id): + raise exception.InstanceNotFound(instance_id=instance_id) + self.stubs.Set(nova.db, 'instance_get', server_not_found) + + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.action, 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": "http://localhost/images/2", + "accessIPv4": "172.19.0.1", + "accessIPv6": "fe80::1", + }, + } + + update = self.mox.CreateMockAnything() + self.stubs.Set(nova.compute.API, 'update', update) + req = fakes.HTTPRequest.blank(self.url) + context = req.environ['nova.context'] + update(context, mox.IgnoreArg(), + image_ref='http://localhost/images/2', + vm_state=vm_states.REBUILDING, + task_state=None, progress=0, **attributes).AndReturn(None) + self.mox.ReplayAll() + + self.controller.action(req, FAKE_UUID, body) + self.mox.VerifyAll() + + 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(nova.compute.api.API, 'resize', resize_mock) + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller.action(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, 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, 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 + + self.stubs.Set(nova.compute.api.API, 'resize', fake_resize) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, self.controller.action, + 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(nova.compute.api.API, 'confirm_resize', cr_mock) + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller.action(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(nova.compute.api.API, + 'confirm_resize', + confirm_resize_mock) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, 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 + + self.stubs.Set(nova.compute.api.API, 'confirm_resize', + fake_confirm_resize) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, self.controller.action, + 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(nova.compute.api.API, + 'revert_resize', + revert_resize_mock) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, 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(nova.compute.api.API, 'revert_resize', revert_mock) + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller.action(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 + + self.stubs.Set(nova.compute.api.API, 'revert_resize', + fake_revert_resize) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, self.controller.action, + req, FAKE_UUID, body) + + def test_create_image(self): + body = { + 'createImage': { + 'name': 'Snapshot 1', + }, + } + + req = fakes.HTTPRequest.blank(self.url) + response = self.controller.action(req, FAKE_UUID, body) + + location = response.headers['Location'] + self.assertEqual('http://localhost/v2/fake/images/123', location) + server_location = self.snapshot.extra_props_last_call['instance_ref'] + expected_server_location = 'http://localhost/v2/servers/' + self.uuid + self.assertEqual(expected_server_location, server_location) + + 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, 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(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(FLAGS.quota_metadata_items + 1): + body['createImage']['metadata']['foo%i' % num] = "bar" + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller.action, 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, 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, req, FAKE_UUID, body) + + def test_create_image_raises_conflict_on_invalid_state(self): + def snapshot(*args, **kwargs): + raise exception.InstanceInvalidState + self.stubs.Set(nova.compute.API, 'snapshot', snapshot) + + body = { + "createImage": { + "name": "test_snapshot", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller.action, req, FAKE_UUID, body) + + +class TestServerActionXMLDeserializer(test.TestCase): + + def setUp(self): + self.deserializer = servers.ActionDeserializer() + + def tearDown(self): + pass + + def test_create_image(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "createImage": { + "name": "new-server-test", + }, + } + self.assertEquals(request['body'], expected) + + def test_create_image_with_metadata(self): + serial_request = """ + + + value1 + +""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "createImage": { + "name": "new-server-test", + "metadata": {"key1": "value1"}, + }, + } + self.assertEquals(request['body'], expected) + + def test_change_pass(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "changePassword": { + "adminPass": "1234pass", + }, + } + self.assertEquals(request['body'], expected) + + def test_change_pass_no_pass(self): + serial_request = """ + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_reboot(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "reboot": { + "type": "HARD", + }, + } + self.assertEquals(request['body'], expected) + + def test_reboot_no_type(self): + serial_request = """ + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_resize(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "resize": {"flavorRef": "http://localhost/flavors/3"}, + } + self.assertEquals(request['body'], expected) + + def test_resize_no_flavor_ref(self): + serial_request = """ + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_confirm_resize(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "confirmResize": None, + } + self.assertEquals(request['body'], expected) + + def test_revert_resize(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "revertResize": None, + } + self.assertEquals(request['body'], expected) + + def test_rebuild(self): + serial_request = """ + + + Apache1 + + + Mg== + + """ + 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.assertDictMatch(request['body'], expected) + + def test_rebuild_minimum(self): + serial_request = """ + """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "rebuild": { + "imageRef": "http://localhost/images/1", + }, + } + self.assertDictMatch(request['body'], expected) + + def test_rebuild_no_imageRef(self): + serial_request = """ + + + Apache1 + + + Mg== + + """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') diff --git a/nova/tests/api/openstack/compute/test_server_metadata.py b/nova/tests/api/openstack/compute/test_server_metadata.py new file mode 100644 index 000000000..61bf4fc47 --- /dev/null +++ b/nova/tests/api/openstack/compute/test_server_metadata.py @@ -0,0 +1,361 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import webob + +from nova.api.openstack.compute import server_metadata +import nova.db +from nova import exception +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes +from nova import utils + + +FLAGS = flags.FLAGS + + +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 return_server_metadata(context, server_id): + if not isinstance(server_id, int): + msg = 'id %s must be int 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(FLAGS.quota_metadata_items): + metadata['metadata']['key%i' % num] = "blah" + return metadata + + +def return_server(context, server_id): + return {'id': server_id, 'name': 'fake'} + + +def return_server_by_uuid(context, server_uuid): + return {'id': 1, 'name': 'fake'} + + +def return_server_nonexistant(context, server_id): + raise exception.InstanceNotFound() + + +class ServerMetaDataTest(test.TestCase): + + def setUp(self): + super(ServerMetaDataTest, 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.controller = server_metadata.Controller() + self.uuid = str(utils.gen_uuid()) + self.url = '/v1.1/fake/servers/%s/metadata' % self.uuid + + def test_index(self): + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.index(req, self.uuid) + + expected = { + 'metadata': { + 'key1': 'value1', + 'key2': 'value2', + 'key3': 'value3', + }, + } + self.assertEqual(expected, res_dict) + + def test_index_nonexistant_server(self): + self.stubs.Set(nova.db, 'instance_metadata_get', + return_server_nonexistant) + req = fakes.HTTPRequest.blank(self.url) + 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 = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.index(req, self.uuid) + expected = {'metadata': {}} + self.assertEqual(expected, res_dict) + + def test_show(self): + req = fakes.HTTPRequest.blank(self.url + '/key2') + res_dict = self.controller.show(req, self.uuid, 'key2') + expected = {'meta': {'key2': 'value2'}} + self.assertEqual(expected, res_dict) + + def test_show_nonexistant_server(self): + self.stubs.Set(nova.db, 'instance_metadata_get', + return_server_nonexistant) + req = fakes.HTTPRequest.blank(self.url + '/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 = fakes.HTTPRequest.blank(self.url + '/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 = fakes.HTTPRequest.blank(self.url + '/key2') + req.method = 'DELETE' + res = self.controller.delete(req, self.uuid, 'key2') + + self.assertEqual(None, res) + + def test_delete_nonexistant_server(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) + req = fakes.HTTPRequest.blank(self.url + '/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 = fakes.HTTPRequest.blank(self.url + '/key6') + req.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, self.uuid, 'key6') + + def test_create(self): + self.stubs.Set(nova.db, 'instance_metadata_get', + return_server_metadata) + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'POST' + req.content_type = "application/json" + body = {"metadata": {"key9": "value9"}} + req.body = json.dumps(body) + res_dict = self.controller.create(req, self.uuid, 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 = fakes.HTTPRequest.blank(self.url) + req.method = 'POST' + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, self.uuid, None) + + def test_create_nonexistant_server(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'POST' + body = {"metadata": {"key1": "value1"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.create, req, self.uuid, body) + + def test_update_all(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'PUT' + req.content_type = "application/json" + expected = { + 'metadata': { + 'key10': 'value10', + 'key99': 'value99', + }, + } + req.body = json.dumps(expected) + res_dict = self.controller.update_all(req, self.uuid, expected) + + self.assertEqual(expected, res_dict) + + def test_update_all_empty_container(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'PUT' + req.content_type = "application/json" + expected = {'metadata': {}} + req.body = json.dumps(expected) + res_dict = self.controller.update_all(req, self.uuid, expected) + + self.assertEqual(expected, res_dict) + + def test_update_all_malformed_container(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'PUT' + req.content_type = "application/json" + expected = {'meta': {}} + req.body = json.dumps(expected) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update_all, req, self.uuid, expected) + + def test_update_all_malformed_data(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'PUT' + req.content_type = "application/json" + expected = {'metadata': ['asdf']} + req.body = json.dumps(expected) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update_all, req, self.uuid, expected) + + def test_update_all_nonexistant_server(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'PUT' + req.content_type = "application/json" + body = {'metadata': {'key10': 'value10'}} + req.body = json.dumps(body) + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update_all, req, '100', body) + + def test_update_item(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": {"key1": "value1"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res_dict = self.controller.update(req, self.uuid, 'key1', body) + expected = {'meta': {'key1': 'value1'}} + self.assertEqual(expected, res_dict) + + def test_update_item_nonexistant_server(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) + req = fakes.HTTPRequest.blank('/v1.1/fake/servers/asdf/metadata/key1') + req.method = 'PUT' + body = {"meta": {"key1": "value1"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, req, self.uuid, 'key1', body) + + def test_update_item_empty_body(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(webob.exc.HTTPBadRequest, + self.controller.update, req, self.uuid, 'key1', None) + + def test_update_item_too_many_keys(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": {"key1": "value1", "key2": "value2"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, self.uuid, 'key1', body) + + def test_update_item_body_uri_mismatch(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url + '/bad') + req.method = 'PUT' + body = {"meta": {"key1": "value1"}} + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, self.uuid, 'bad', body) + + 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(FLAGS.quota_metadata_items + 1): + data['metadata']['key%i' % num] = "blah" + req = fakes.HTTPRequest.blank(self.url) + req.method = 'POST' + req.body = json.dumps(data) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller.create, req, self.uuid, 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(FLAGS.quota_metadata_items + 1): + data['metadata']['key%i' % num] = "blah" + req = fakes.HTTPRequest.blank(self.url) + req.method = 'PUT' + req.body = json.dumps(data) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller.update_all, req, self.uuid, data) diff --git a/nova/tests/api/openstack/compute/test_servers.py b/nova/tests/api/openstack/compute/test_servers.py new file mode 100644 index 000000000..83b0e345c --- /dev/null +++ b/nova/tests/api/openstack/compute/test_servers.py @@ -0,0 +1,3850 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# 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 json +import urlparse + +from lxml import etree +import webob + +import nova.api.openstack.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 xmlutil +import nova.compute.api +from nova.compute import instance_types +from nova.compute import task_states +from nova.compute import vm_states +import nova.db +from nova.db.sqlalchemy.models import InstanceActions +from nova.db.sqlalchemy.models import InstanceMetadata +from nova import flags +import nova.image.fake +import nova.rpc +import nova.scheduler.api +from nova import test +from nova.tests.api.openstack import fakes +from nova import utils + + +FLAGS = flags.FLAGS +FAKE_UUID = fakes.FAKE_UUID +FAKE_UUIDS = {0: 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' +} + + +def get_fake_uuid(token=0): + if not token in FAKE_UUIDS: + FAKE_UUIDS[token] = str(utils.gen_uuid()) + return FAKE_UUIDS[token] + + +def fake_gen_uuid(): + return FAKE_UUID + + +def return_server_by_id(context, id): + return fakes.stub_instance(id) + + +def return_server_by_uuid(context, uuid): + id = 1 + return fakes.stub_instance(id, uuid=uuid) + + +def return_server_with_attributes(**kwargs): + def _return_server(context, instance_id): + return fakes.stub_instance(instance_id, **kwargs) + return _return_server + + +def return_server_with_attributes_by_uuid(**kwargs): + def _return_server(context, uuid): + return fakes.stub_instance(1, uuid=uuid, **kwargs) + return _return_server + + +def return_server_with_state(vm_state, task_state=None): + def _return_server(context, uuid): + return fakes.stub_instance(1, uuid=uuid, vm_state=vm_state, + task_state=task_state) + return _return_server + + +def return_server_with_uuid_and_state(vm_state, task_state): + def _return_server(context, id): + return fakes.stub_instance(id, + uuid=FAKE_UUID, + vm_state=vm_state, + task_state=task_state) + return _return_server + + +def return_servers(context, *args, **kwargs): + servers = [] + for i in xrange(5): + server = fakes.stub_instance(i, 'fake', 'fake', uuid=get_fake_uuid(i)) + servers.append(server) + return servers + + +def return_servers_by_reservation(context, reservation_id=""): + return [fakes.stub_instance(i, reservation_id) for i in xrange(5)] + + +def return_servers_by_reservation_empty(context, reservation_id=""): + return [] + + +def return_servers_from_child_zones_empty(*args, **kwargs): + return [] + + +def return_servers_from_child_zones(*args, **kwargs): + class Server(object): + pass + + zones = [] + for zone in xrange(3): + servers = [] + for server_id in xrange(5): + server = Server() + server._info = fakes.stub_instance( + server_id, reservation_id="child") + servers.append(server) + + zones.append(("Zone%d" % zone, servers)) + return zones + + +def return_security_group(context, instance_id, security_group_id): + pass + + +def instance_update(context, instance_id, values): + return fakes.stub_instance(instance_id, name=values.get('display_name')) + + +def instance_addresses(context, instance_id): + return None + + +def fake_compute_api(cls, req, id): + return True + + +def find_host(self, context, instance_id): + return "nova" + + +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 ServersControllerTest(test.TestCase): + def setUp(self): + self.maxDiff = None + super(ServersControllerTest, self).setUp() + self.flags(verbose=True, use_ipv6=False) + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fakes.stub_out_image_service(self.stubs) + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(nova.db, 'instance_get_all_by_filters', + return_servers) + self.stubs.Set(nova.db, 'instance_get', return_server_by_id) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_by_uuid) + self.stubs.Set(nova.db, 'instance_get_all_by_project', + return_servers) + self.stubs.Set(nova.db, 'instance_add_security_group', + return_security_group) + self.stubs.Set(nova.db, 'instance_update', instance_update) + self.stubs.Set(nova.db, 'instance_get_fixed_addresses', + instance_addresses) + self.stubs.Set(nova.db, 'instance_get_floating_address', + instance_addresses) + + self.config_drive = None + + self.controller = servers.Controller() + self.ips_controller = ips.Controller() + + def test_get_server_by_uuid(self): + """ + The steps involved with resolving a UUID are pretty complicated; + here's what's happening in this scenario: + + 1. Show is calling `routing_get` + + 2. `routing_get` is wrapped by `reroute_compute` which does the work + of resolving requests to child zones. + + 3. `reroute_compute` looks up the UUID by hitting the stub + (returns_server_by_uuid) + + 4. Since the stub return that the record exists, `reroute_compute` + considers the request to be 'zone local', so it replaces the UUID + in the argument list with an integer ID and then calls the inner + function ('get'). + + 5. The call to `get` hits the other stub 'returns_server_by_id` which + has the UUID set to FAKE_UUID + + So, counterintuitively, we call `get` twice on the `show` command. + """ + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + res_dict = self.controller.show(req, FAKE_UUID) + self.assertEqual(res_dict['server']['id'], FAKE_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 = { + "server": { + "id": uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "server1", + "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "key_name": '', + "image": { + "id": "10", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + }, + "metadata": { + "seq": "1", + }, + "config_drive": None, + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/servers/%s" % uuid, + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/servers/%s" % uuid, + }, + ], + } + } + + self.assertDictMatch(res_dict, 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 = return_server_with_attributes( + vm_state=vm_states.ACTIVE, progress=100) + self.stubs.Set(nova.db, 'instance_get', new_return_server) + + uuid = FAKE_UUID + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % uuid) + res_dict = self.controller.show(req, uuid) + expected_server = { + "server": { + "id": uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 100, + "name": "server1", + "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "key_name": '', + "image": { + "id": "10", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + }, + "metadata": { + "seq": "1", + }, + "config_drive": None, + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/servers/%s" % uuid, + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/servers/%s" % uuid, + }, + ], + } + } + + self.assertDictMatch(res_dict, 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 = return_server_with_attributes( + vm_state=vm_states.ACTIVE, image_ref=image_ref, + flavor_id=flavor_id, progress=100) + self.stubs.Set(nova.db, 'instance_get', new_return_server) + + uuid = FAKE_UUID + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % uuid) + res_dict = self.controller.show(req, uuid) + expected_server = { + "server": { + "id": uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 100, + "name": "server1", + "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "key_name": '', + "image": { + "id": "10", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + }, + "metadata": { + "seq": "1", + }, + "config_drive": None, + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/servers/%s" % uuid, + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/servers/%s" % uuid, + }, + ], + } + } + + self.assertDictMatch(res_dict, expected_server) + + # NOTE(bcwaldon): lp830817 + def test_get_server_by_id_malformed_networks(self): + def fake_instance_get(context, instance_uuid): + instance = return_server_by_uuid(context, instance_uuid) + instance['fixed_ips'] = [dict(network=None, address='1.2.3.4')] + return instance + + self.stubs.Set(nova.db, 'instance_get_by_uuid', fake_instance_get) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + res_dict = self.controller.show(req, FAKE_UUID) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server1') + + def test_get_server_by_id_malformed_vif(self): + def fake_instance_get(context, uuid): + instance = return_server_by_uuid(context, uuid) + instance['fixed_ips'] = [dict(network={'label': 'meow'}, + address='1.2.3.4', virtual_interface=None)] + return instance + + self.stubs.Set(nova.db, 'instance_get_by_uuid', fake_instance_get) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + res_dict = self.controller.show(req, FAKE_UUID) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server1') + + def test_get_server_by_id_with_addresses(self): + self.flags(use_ipv6=True) + privates = ['192.168.0.3', '192.168.0.4'] + publics = ['172.19.0.1', '172.19.0.2'] + new_return_server = return_server_with_attributes( + public_ips=publics, private_ips=privates) + self.stubs.Set(nova.db, 'instance_get', new_return_server) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + res_dict = self.controller.show(req, FAKE_UUID) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server1') + addresses = res_dict['server']['addresses'] + expected = { + 'private': [ + {'addr': '192.168.0.3', 'version': 4}, + {'addr': '192.168.0.4', 'version': 4}, + ], + 'public': [ + {'addr': 'b33f::fdee:ddff:fecc:bbaa', 'version': 6}, + {'addr': '172.19.0.1', 'version': 4}, + {'addr': '172.19.0.2', 'version': 4}, + ], + } + self.assertDictMatch(addresses, expected) + + def test_get_server_by_id_with_addresses_ipv6_disabled(self): + # ipv6 flag is off by default + privates = ['192.168.0.3', '192.168.0.4'] + publics = ['172.19.0.1', '172.19.0.2'] + new_return_server = return_server_with_attributes( + public_ips=publics, private_ips=privates) + self.stubs.Set(nova.db, 'instance_get', new_return_server) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + res_dict = self.controller.show(req, FAKE_UUID) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server1') + addresses = res_dict['server']['addresses'] + expected = { + 'private': [ + {'addr': '192.168.0.3', 'version': 4}, + {'addr': '192.168.0.4', 'version': 4}, + ], + 'public': [ + {'addr': '172.19.0.1', 'version': 4}, + {'addr': '172.19.0.2', 'version': 4}, + ], + } + self.assertDictMatch(addresses, expected) + + def test_get_server_addresses(self): + self.flags(use_ipv6=True) + + privates = ['192.168.0.3', '192.168.0.4'] + publics = ['172.19.0.1', '1.2.3.4', '172.19.0.2'] + new_return_server = return_server_with_attributes_by_uuid( + public_ips=publics, private_ips=privates) + self.stubs.Set(nova.db, 'instance_get_by_uuid', new_return_server) + + req = fakes.HTTPRequest.blank('/v2/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': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '1.2.3.4'}, + {'version': 4, 'addr': '172.19.0.2'}, + ], + }, + } + self.assertDictMatch(res_dict, expected) + + def test_get_server_addresses_with_floating(self): + privates = ['192.168.0.3', '192.168.0.4'] + publics = ['172.19.0.1', '1.2.3.4', '172.19.0.2'] + new_return_server = return_server_with_attributes_by_uuid( + public_ips=publics, private_ips=privates, + public_ips_are_floating=True) + self.stubs.Set(nova.db, 'instance_get_by_uuid', new_return_server) + + req = fakes.HTTPRequest.blank('/v2/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'}, + {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '1.2.3.4'}, + {'version': 4, 'addr': '172.19.0.2'}, + ], + }, + } + self.assertDictMatch(res_dict, expected) + + def test_get_server_addresses_single_network(self): + self.flags(use_ipv6=True) + privates = ['192.168.0.3', '192.168.0.4'] + publics = ['172.19.0.1', '1.2.3.4', '172.19.0.2'] + new_return_server = return_server_with_attributes_by_uuid( + public_ips=publics, private_ips=privates) + self.stubs.Set(nova.db, 'instance_get_by_uuid', new_return_server) + + url = '/v2/fake/servers/%s/ips/public' % FAKE_UUID + req = fakes.HTTPRequest.blank(url) + res_dict = self.ips_controller.show(req, FAKE_UUID, 'public') + + expected = { + 'public': [ + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '1.2.3.4'}, + {'version': 4, 'addr': '172.19.0.2'}, + ], + } + self.assertDictMatch(res_dict, expected) + + def test_get_server_addresses_nonexistant_network(self): + url = '/v2/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_nonexistant_server(self): + def fake_instance_get(*args, **kwargs): + raise nova.exception.InstanceNotFound() + + self.stubs.Set(nova.db, 'instance_get_by_uuid', fake_instance_get) + + server_id = str(utils.gen_uuid()) + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s/ips' % server_id) + self.assertRaises(webob.exc.HTTPNotFound, + self.ips_controller.index, req, server_id) + + def test_get_server_list_with_reservation_id(self): + self.stubs.Set(nova.db, 'instance_get_all_by_reservation', + return_servers_by_reservation) + self.stubs.Set(nova.scheduler.api, 'call_zone_method', + return_servers_from_child_zones) + + req = fakes.HTTPRequest.blank('/v2/fake/servers?reservation_id=foo') + res_dict = self.controller.index(req) + + i = 0 + for s in res_dict['servers']: + if '_is_precooked' in s: + self.assertEqual(s.get('reservation_id'), 'child') + else: + print s + self.assertEqual(s.get('name'), 'server%d' % i) + i += 1 + + def test_get_server_list_with_reservation_id_empty(self): + self.stubs.Set(nova.db, 'instance_get_all_by_reservation', + return_servers_by_reservation_empty) + self.stubs.Set(nova.scheduler.api, 'call_zone_method', + return_servers_from_child_zones_empty) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/detail?' + 'reservation_id=foo') + res_dict = self.controller.detail(req) + + i = 0 + for s in res_dict['servers']: + if '_is_precooked' in s: + self.assertEqual(s.get('reservation_id'), 'child') + else: + self.assertEqual(s.get('name'), 'server%d' % i) + i += 1 + + def test_get_server_list_with_reservation_id_details(self): + self.stubs.Set(nova.db, 'instance_get_all_by_reservation', + return_servers_by_reservation) + self.stubs.Set(nova.scheduler.api, 'call_zone_method', + return_servers_from_child_zones) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/detail?' + 'reservation_id=foo') + res_dict = self.controller.detail(req) + + i = 0 + for s in res_dict['servers']: + if '_is_precooked' in s: + self.assertEqual(s.get('reservation_id'), 'child') + else: + self.assertEqual(s.get('name'), 'server%d' % i) + i += 1 + + def test_get_server_list(self): + req = fakes.HTTPRequest.blank('/v2/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'], get_fake_uuid(i)) + self.assertEqual(s['name'], 'server%d' % i) + self.assertEqual(s.get('image', None), 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('/v2/fake/servers?limit=3') + res_dict = self.controller.index(req) + + servers = res_dict['servers'] + self.assertEqual([s['id'] for s in servers], + [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': [get_fake_uuid(2)]} + self.assertDictMatch(expected_params, params) + + def test_get_servers_with_limit_bad_value(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers?limit=aaa') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_server_details_with_limit(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers/detail?limit=3') + res = self.controller.detail(req) + + servers = res['servers'] + self.assertEqual([s['id'] for s in servers], + [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', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + expected = {'limit': ['3'], 'marker': [get_fake_uuid(2)]} + self.assertDictMatch(expected, params) + + def test_get_server_details_with_limit_bad_value(self): + req = fakes.HTTPRequest.blank('/v2/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('/v2/fake/servers/detail' + '?limit=3&blah=2:t') + res = self.controller.detail(req) + + servers = res['servers'] + self.assertEqual([s['id'] for s in servers], + [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', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + + self.assertDictMatch({'limit': ['3'], 'blah': ['2:t'], + 'marker': [get_fake_uuid(2)]}, params) + + def test_get_servers_with_too_big_limit(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers?limit=30') + res_dict = self.controller.index(req) + self.assertTrue('servers_links' not in res_dict) + + def test_get_servers_with_bad_limit(self): + req = fakes.HTTPRequest.blank('/v2/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' % get_fake_uuid(2) + 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_limit_and_marker(self): + url = '/v2/fake/servers?limit=2&marker=%s' % get_fake_uuid(1) + req = fakes.HTTPRequest.blank(url) + servers = self.controller.index(req)['servers'] + self.assertEqual([s['name'] for s in servers], ['server2', 'server3']) + + def test_get_servers_with_bad_marker(self): + req = fakes.HTTPRequest.blank('/v2/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(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/v2/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(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('image' in search_opts) + self.assertEqual(search_opts['image'], '12345') + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + self.flags(allow_admin_api=False) + + req = fakes.HTTPRequest.blank('/v2/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, instances=None): + self.assertNotEqual(filters, None) + self.assertEqual(filters['project_id'], 'fake') + self.assertFalse(filters.get('tenant_id')) + return [fakes.stub_instance(100)] + + self.stubs.Set(nova.db, 'instance_get_all_by_filters', + fake_get_all) + self.flags(allow_admin_api=True) + + req = fakes.HTTPRequest.blank('/v2/fake/servers?tenant_id=fake', + use_admin_context=True) + res = self.controller.index(req) + + self.assertTrue('servers' in res) + + def test_get_servers_allows_flavor(self): + server_uuid = str(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('flavor' in search_opts) + # flavor is an integer ID + self.assertEqual(search_opts['flavor'], '12345') + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + self.flags(allow_admin_api=False) + + req = fakes.HTTPRequest.blank('/v2/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_allows_status(self): + server_uuid = str(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('vm_state' in search_opts) + self.assertEqual(search_opts['vm_state'], vm_states.ACTIVE) + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + self.flags(allow_admin_api=False) + + req = fakes.HTTPRequest.blank('/v2/fake/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_invalid_status(self): + """Test getting servers by invalid status""" + self.flags(allow_admin_api=False) + req = fakes.HTTPRequest.blank('/v2/fake/servers?status=unknown') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) + + def test_get_servers_allows_name(self): + server_uuid = str(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('name' in search_opts) + self.assertEqual(search_opts['name'], 'whee.*') + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + self.flags(allow_admin_api=False) + + req = fakes.HTTPRequest.blank('/v2/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(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('changes-since' in search_opts) + changes_since = datetime.datetime(2011, 1, 24, 17, 8, 1) + self.assertEqual(search_opts['changes-since'], changes_since) + self.assertTrue('deleted' not in search_opts) + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + params = 'changes-since=2011-01-24T17:08:01Z' + req = fakes.HTTPRequest.blank('/v2/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('/v2/fake/servers?%s' % params) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) + + def test_get_servers_unknown_or_admin_options1(self): + """Test getting servers by admin-only or unknown options. + This tests when admin_api is off. Make sure the admin and + unknown options are stripped before they get to + compute_api.get_all() + """ + + self.flags(allow_admin_api=False) + + server_uuid = str(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + # Allowed by user + self.assertTrue('name' in search_opts) + self.assertTrue('status' in search_opts) + # Allowed only by admins with admin API on + self.assertFalse('ip' in search_opts) + self.assertFalse('unknown_option' in search_opts) + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = fakes.HTTPRequest.blank('/v2/fake/servers?%s' % query_str, + use_admin_context=True) + res = self.controller.index(req) + + servers = res['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_unknown_or_admin_options2(self): + """Test getting servers by admin-only or unknown options. + This tests when admin_api is on, but context is a user. + Make sure the admin and unknown options are stripped before + they get to compute_api.get_all() + """ + + self.flags(allow_admin_api=True) + + server_uuid = str(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + # Allowed by user + self.assertTrue('name' in search_opts) + self.assertTrue('status' in search_opts) + # Allowed only by admins with admin API on + self.assertFalse('ip' in search_opts) + self.assertFalse('unknown_option' in search_opts) + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = fakes.HTTPRequest.blank('/v2/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_unknown_or_admin_options3(self): + """Test getting servers by admin-only or unknown options. + This tests when admin_api is on and context is admin. + All options should be passed through to compute_api.get_all() + """ + + self.flags(allow_admin_api=True) + + server_uuid = str(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + # Allowed by user + self.assertTrue('name' in search_opts) + self.assertTrue('status' in search_opts) + # Allowed only by admins with admin API on + self.assertTrue('ip' in search_opts) + self.assertTrue('unknown_option' in search_opts) + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = fakes.HTTPRequest.blank('/v2/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_admin_allows_ip(self): + """Test getting servers by ip with admin_api enabled and + admin context + """ + self.flags(allow_admin_api=True) + + server_uuid = str(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('ip' in search_opts) + self.assertEqual(search_opts['ip'], '10\..*') + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/v2/fake/servers?ip=10\..*', + 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_admin_allows_ip6(self): + """Test getting servers by ip6 with admin_api enabled and + admin context + """ + self.flags(allow_admin_api=True) + + server_uuid = str(utils.gen_uuid()) + + def fake_get_all(compute_self, context, search_opts=None): + self.assertNotEqual(search_opts, None) + self.assertTrue('ip6' in search_opts) + self.assertEqual(search_opts['ip6'], 'ffff.*') + return [fakes.stub_instance(100, uuid=server_uuid)] + + self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/v2/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_update_server_no_body(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.update, req, FAKE_UUID, None) + + def test_update_server_all_attributes(self): + self.stubs.Set(nova.db, 'instance_get', + return_server_with_attributes(name='server_test', + access_ipv4='0.0.0.0', + access_ipv6='beef::0123')) + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'accessIPv4': '0.0.0.0', + 'accessIPv6': 'beef::0123', + }} + req.body = json.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') + self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0') + self.assertEqual(res_dict['server']['accessIPv6'], 'beef::0123') + + def test_update_server_name(self): + self.stubs.Set(nova.db, 'instance_get', + return_server_with_attributes(name='server_test')) + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = 'application/json' + body = {'server': {'name': 'server_test'}} + req.body = json.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_access_ipv4(self): + self.stubs.Set(nova.db, 'instance_get', + return_server_with_attributes(access_ipv4='0.0.0.0')) + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = 'application/json' + body = {'server': {'accessIPv4': '0.0.0.0'}} + req.body = json.dumps(body) + 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_ipv6(self): + self.stubs.Set(nova.db, 'instance_get', + return_server_with_attributes(access_ipv6='beef::0123')) + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = 'application/json' + body = {'server': {'accessIPv6': 'beef::0123'}} + req.body = json.dumps(body) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['accessIPv6'], 'beef::0123') + + 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) + return filtered_dict + + self.stubs.Set(nova.db, 'instance_update', server_update) + self.stubs.Set(nova.db, 'instance_get', + return_server_with_attributes(name='server_test')) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = "application/json" + req.body = json.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_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('/v2/fake/servers/detail') + res_dict = self.controller.detail(req) + + for i, s in enumerate(res_dict['servers']): + self.assertEqual(s['id'], get_fake_uuid(i)) + self.assertEqual(s['hostId'], '') + self.assertEqual(s['name'], 'server%d' % i) + self.assertEqual(s['image'], expected_image) + self.assertEqual(s['flavor'], expected_flavor) + self.assertEqual(s['status'], 'BUILD') + self.assertEqual(s['metadata']['seq'], str(i)) + + 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, 'fake', 'fake', i % 2, + uuid=get_fake_uuid(i)) + for i in xrange(5)] + + self.stubs.Set(nova.db, 'instance_get_all_by_filters', + return_servers_with_host) + + req = fakes.HTTPRequest.blank('/v2/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'], get_fake_uuid(i)) + self.assertEqual(s['hostId'], host_ids[i % 2]) + self.assertEqual(s['name'], 'server%d' % i) + + def test_delete_server_instance(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'DELETE' + + self.server_delete_called = False + + new_return_server = return_server_with_attributes( + vm_state=vm_states.ACTIVE) + self.stubs.Set(nova.db, 'instance_get', new_return_server) + + def instance_destroy_mock(context, id): + self.server_delete_called = True + self.stubs.Set(nova.db, 'instance_destroy', instance_destroy_mock) + + self.controller.delete(req, FAKE_UUID) + + self.assertEqual(self.server_delete_called, True) + + def test_delete_server_instance_while_building(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'DELETE' + + self.server_delete_called = False + + new_return_server = return_server_with_attributes( + vm_state=vm_states.BUILDING) + self.stubs.Set(nova.db, 'instance_get', new_return_server) + + def instance_destroy_mock(context, id): + self.server_delete_called = True + self.stubs.Set(nova.db, 'instance_destroy', instance_destroy_mock) + + self.assertRaises(webob.exc.HTTPConflict, + self.controller.delete, + req, + FAKE_UUID) + + def test_delete_server_instance_while_resize(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) + req.method = 'DELETE' + + self.server_delete_called = False + + new_return_server = return_server_with_attributes( + vm_state=vm_states.RESIZING) + self.stubs.Set(nova.db, 'instance_get', new_return_server) + + def instance_destroy_mock(context, id): + self.server_delete_called = True + self.stubs.Set(nova.db, 'instance_destroy', instance_destroy_mock) + + self.assertRaises(webob.exc.HTTPConflict, + self.controller.delete, + req, + FAKE_UUID) + + +class ServerStatusTest(test.TestCase): + + def setUp(self): + super(ServerStatusTest, self).setUp() + fakes.stub_out_nw_api(self.stubs) + + self.controller = servers.Controller() + + def _get_with_state(self, vm_state, task_state=None): + new_server = return_server_with_state(vm_state, task_state) + self.stubs.Set(nova.db, 'instance_get_by_uuid', new_server) + self.stubs.Set(nova.db, 'instance_get', new_server) + + request = fakes.HTTPRequest.blank('/v2/fake/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_rebuild(self): + response = self._get_with_state(vm_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.RESIZING) + self.assertEqual(response['server']['status'], 'RESIZE') + + def test_verify_resize(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.RESIZE_VERIFY) + self.assertEqual(response['server']['status'], 'VERIFY_RESIZE') + + 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'], 'STOPPED') + + +class ServersControllerCreateTest(test.TestCase): + + def setUp(self): + """Shared implementation for tests below that create instance""" + super(ServersControllerCreateTest, self).setUp() + + self.maxDiff = None + self.flags(verbose=True) + self.config_drive = None + self.instance_cache_num = 0 + self.instance_cache = {} + + self.controller = servers.Controller() + + def instance_create(context, inst): + inst_type = instance_types.get_instance_type_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 = { + 'id': self.instance_cache_num, + 'display_name': inst['display_name'] or 'test', + 'uuid': FAKE_UUID, + 'instance_type': dict(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": self.config_drive, + "progress": 0, + "fixed_ips": [] + } + self.instance_cache[instance['id']] = instance + return instance + + def instance_get(context, instance_id): + """Stub for compute/api create() pulling in instance after + scheduling + """ + return self.instance_cache[instance_id] + + def rpc_call_wrapper(context, topic, msg): + """Stub out the scheduler creating the instance entry""" + if topic == FLAGS.scheduler_topic and \ + msg['method'] == 'run_instance': + request_spec = msg['args']['request_spec'] + num_instances = request_spec.get('num_instances', 1) + instances = [] + for x in xrange(num_instances): + instances.append(instance_create(context, + request_spec['instance_properties'])) + return instances + + def server_update(context, instance_id, params): + inst = self.instance_cache[instance_id] + inst.update(params) + return 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_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fakes.stub_out_image_service(self.stubs) + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(utils, 'gen_uuid', fake_gen_uuid) + self.stubs.Set(nova.db, 'instance_add_security_group', + return_security_group) + self.stubs.Set(nova.db, 'project_get_networks', + project_get_networks) + self.stubs.Set(nova.db, 'instance_create', instance_create) + self.stubs.Set(nova.db, 'instance_get', instance_get) + self.stubs.Set(nova.rpc, 'cast', fake_method) + self.stubs.Set(nova.rpc, 'call', rpc_call_wrapper) + self.stubs.Set(nova.db, 'instance_update', server_update) + self.stubs.Set(nova.db, 'queue_get_for', queue_get_for) + self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip', + fake_method) + self.stubs.Set(nova.compute.api.API, "_find_host", find_host) + + def _test_create_instance(self): + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + body = dict(server=dict( + name='server_test', imageRef=image_uuid, flavorRef=2, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + server = self.controller.create(req, body).obj['server'] + + self.assertEqual(FLAGS.password_length, len(server['adminPass'])) + self.assertEqual(FAKE_UUID, server['id']) + + 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': { + 'min_count': 2, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': {'hello': 'world', + 'open': 'stack'}, + 'personality': [] + } + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body).obj + + self.assertEqual(FAKE_UUID, res["server"]["id"]) + self.assertEqual(12, len(res["server"]["adminPass"])) + + def test_create_multiple_instances_resv_id_return(self): + """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': { + 'min_count': 2, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': {'hello': 'world', + 'open': 'stack'}, + 'personality': [], + 'return_reservation_id': True + } + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + reservation_id = res.get('reservation_id') + self.assertNotEqual(reservation_id, "") + self.assertNotEqual(reservation_id, None) + self.assertTrue(len(reservation_id) > 1) + + def test_create_instance_with_user_supplied_reservation_id(self): + """Non-admin supplied reservation_id should be ignored.""" + 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, + 'metadata': {'hello': 'world', + 'open': 'stack'}, + 'personality': [], + 'reservation_id': 'myresid', + 'return_reservation_id': True + } + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + self.assertIn('reservation_id', res) + self.assertNotEqual(res['reservation_id'], 'myresid') + + def test_create_instance_with_admin_supplied_reservation_id(self): + """Admin supplied reservation_id should be honored.""" + 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, + 'metadata': {'hello': 'world', + 'open': 'stack'}, + 'personality': [], + 'reservation_id': 'myresid', + 'return_reservation_id': True + } + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers', + use_admin_context=True) + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + reservation_id = res['reservation_id'] + self.assertEqual(reservation_id, "myresid") + + 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_instance_with_access_ip(self): + # proper local hrefs must start with 'http://localhost/v2/' + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v2/fake/images/%s' % image_uuid + flavor_ref = 'http://localhost/fake/flavors/3' + access_ipv4 = '1.2.3.4' + access_ipv6 = 'fead::1234' + expected_flavor = { + "id": "3", + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/fake/flavors/3', + }, + ], + } + expected_image = { + "id": image_uuid, + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/fake/images/%s' % image_uuid, + }, + ], + } + body = { + 'server': { + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'accessIPv4': access_ipv4, + 'accessIPv6': access_ipv6, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': [ + { + "path": "/etc/banner.txt", + "contents": "MQ==", + }, + ], + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body).obj + + server = res['server'] + self.assertEqual(FLAGS.password_length, len(server['adminPass'])) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance(self): + # proper local hrefs must start with 'http://localhost/v2/' + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v2/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', + }, + 'personality': [ + { + "path": "/etc/banner.txt", + "contents": "MQ==", + }, + ], + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body).obj + + server = res['server'] + self.assertEqual(FLAGS.password_length, len(server['adminPass'])) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_invalid_key_name(self): + image_href = 'http://localhost/v2/images/2' + flavor_ref = 'http://localhost/flavors/3' + body = dict(server=dict( + name='server_test', imageRef=image_href, flavorRef=flavor_ref, + key_name='nonexistentkey')) + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_create_instance_valid_key_name(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/flavors/3' + body = dict(server=dict( + name='server_test', imageRef=image_href, flavorRef=flavor_ref, + key_name='key')) + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body).obj + + self.assertEqual(FAKE_UUID, res["server"]["id"]) + self.assertEqual(12, len(res["server"]["adminPass"])) + + def test_create_instance_invalid_flavor_href(self): + image_href = 'http://localhost/v2/images/2' + flavor_ref = 'http://localhost/v2/flavors/asdf' + body = dict(server=dict( + name='server_test', imageRef=image_href, flavorRef=flavor_ref, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_create_instance_invalid_flavor_id_int(self): + image_href = 'http://localhost/v2/fake/images/2' + flavor_ref = -1 + body = dict(server=dict( + name='server_test', imageRef=image_href, flavorRef=flavor_ref, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_create_instance_bad_flavor_href(self): + image_href = 'http://localhost/v2/images/2' + flavor_ref = 'http://localhost/v2/flavors/17' + body = dict(server=dict( + name='server_test', imageRef=image_href, flavorRef=flavor_ref, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_create_instance_with_config_drive(self): + self.config_drive = True + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/v2/fake/flavors/3' + body = { + 'server': { + 'name': 'config_drive_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + 'config_drive': True, + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body).obj + + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_with_config_drive_as_id(self): + self.config_drive = 2 + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/v2/fake/flavors/3' + body = { + 'server': { + 'name': 'config_drive_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + 'config_drive': image_href, + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body).obj + + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_with_bad_config_drive(self): + self.config_drive = "asdf" + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/v2/fake/flavors/3' + body = { + 'server': { + 'name': 'config_drive_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + 'config_drive': 'asdf', + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_create_instance_without_config_drive(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/v2/fake/flavors/3' + body = { + 'server': { + 'name': 'config_drive_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + 'config_drive': True, + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body).obj + + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_bad_href(self): + image_href = 'asdf' + flavor_ref = 'http://localhost/v2/flavors/3' + body = dict(server=dict( + name='server_test', imageRef=image_href, flavorRef=flavor_ref, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_create_instance_local_href(self): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/v2/flavors/3' + body = { + 'server': { + 'name': 'server_test', + 'imageRef': image_uuid, + 'flavorRef': flavor_ref, + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body).obj + + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_admin_pass(self): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + body = { + 'server': { + 'name': 'server_test', + 'imageRef': image_uuid, + 'flavorRef': 3, + 'adminPass': 'testpass', + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = "application/json" + res = self.controller.create(req, body).obj + + server = res['server'] + self.assertEqual(server['adminPass'], body['server']['adminPass']) + + def test_create_instance_admin_pass_empty(self): + body = { + 'server': { + 'name': 'server_test', + 'imageRef': 3, + 'flavorRef': 3, + 'adminPass': '', + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_create_instance_malformed_entity(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + body = {'server': 'string'} + req.body = json.dumps(body) + req.headers['content-type'] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_create_location(self): + selfhref = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID + bookhref = 'http://localhost/fake/servers/%s' % FAKE_UUID + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v2/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', + }, + 'personality': [ + { + "path": "/etc/banner.txt", + "contents": "MQ==", + }, + ], + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + robj = self.controller.create(req, body) + + self.assertEqual(robj['Location'], selfhref) + + +class TestServerCreateRequestXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestServerCreateRequestXMLDeserializer, self).setUp() + self.deserializer = servers.CreateDeserializer() + + def test_minimal_request(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + }, + } + self.assertEquals(request['body'], expected) + + def test_access_ipv4(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv4": "1.2.3.4", + }, + } + self.assertEquals(request['body'], expected) + + def test_access_ipv6(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv6": "fead::1234", + }, + } + self.assertEquals(request['body'], expected) + + def test_access_ip(self): + serial_request = """ +""" + 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.assertEquals(request['body'], expected) + + def test_admin_pass(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "adminPass": "1234", + }, + } + self.assertEquals(request['body'], expected) + + def test_image_link(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "http://localhost:8774/v2/images/2", + "flavorRef": "3", + }, + } + self.assertEquals(request['body'], expected) + + def test_flavor_link(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "http://localhost:8774/v2/flavors/3", + }, + } + self.assertEquals(request['body'], expected) + + def test_empty_metadata_personality(self): + serial_request = """ + + + +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "metadata": {}, + "personality": [], + }, + } + self.assertEquals(request['body'], expected) + + def test_multiple_metadata_items(self): + serial_request = """ + + + two + snack + +""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "metadata": {"one": "two", "open": "snack"}, + }, + } + self.assertEquals(request['body'], expected) + + def test_multiple_personality_files(self): + serial_request = """ + + + MQ== + Mg== + +""" + 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.assertDictMatch(request['body'], expected) + + def test_spec_request(self): + image_bookmark_link = "http://servers.api.openstack.org/1234/" + \ + "images/52415800-8b69-11e0-9b19-734f6f006e54" + serial_request = """ + + + Apache1 + + + Mg== + +""" % (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.assertEquals(request['body'], expected) + + def test_request_with_empty_networks(self): + serial_request = """ + + +""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_one_network(self): + serial_request = """ + + + + +""" + 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.assertEquals(request['body'], expected) + + def test_request_with_two_networks(self): + serial_request = """ + + + + + +""" + 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.assertEquals(request['body'], expected) + + def test_request_with_second_network_node_ignored(self): + serial_request = """ + + + + + + + +""" + 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.assertEquals(request['body'], expected) + + def test_request_with_one_network_missing_id(self): + serial_request = """ + + + + +""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"fixed_ip": "10.0.1.12"}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_one_network_missing_fixed_ip(self): + serial_request = """ + + + + +""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1"}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_one_network_empty_id(self): + serial_request = """ + + + + + """ + 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.assertEquals(request['body'], expected) + + def test_request_with_one_network_empty_fixed_ip(self): + serial_request = """ + + + + + """ + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": ""}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_networks_duplicate_ids(self): + serial_request = """ + + + + + + """ + 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.assertEquals(request['body'], expected) + + +class TestAddressesXMLSerialization(test.TestCase): + + index_serializer = nova.api.openstack.compute.ips.AddressesTemplate() + show_serializer = nova.api.openstack.compute.ips.NetworkTemplate() + + def test_xml_declaration(self): + fixture = { + 'network_2': [ + {'addr': '192.168.0.1', 'version': 4}, + {'addr': 'fe80::beef', 'version': 6}, + ], + } + output = self.show_serializer.serialize(fixture) + has_dec = output.startswith("") + self.assertTrue(has_dec) + + def test_show(self): + fixture = { + 'network_2': [ + {'addr': '192.168.0.1', 'version': 4}, + {'addr': 'fe80::beef', 'version': 6}, + ], + } + output = self.show_serializer.serialize(fixture) + root = etree.XML(output) + network = fixture['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): + + def setUp(self): + super(ServersViewBuilderTest, self).setUp() + self.flags(use_ipv6=True) + self.instance = fakes.stub_instance( + id=1, + image_ref="5", + uuid="deadbeef-feed-edee-beef-d0ea7beefedd", + display_name="test_server", + public_ips=["192.168.0.3"], + private_ips=["172.19.0.1"], + include_fake_metadata=False) + + self.uuid = self.instance['uuid'] + self.view_builder = views.servers.ViewBuilder() + self.request = fakes.HTTPRequest.blank("/v2") + + def test_build_server(self): + self_link = "http://localhost/v2/fake/servers/%s" % self.uuid + bookmark_link = "http://localhost/fake/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.assertDictMatch(output, 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/v2/fake/servers/%s" % + self.uuid, + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/servers/%s" % self.uuid, + }, + ], + } + } + + output = self.view_builder.basic(self.request, self.instance) + self.assertDictMatch(output, expected_server) + + def test_build_server_detail(self): + image_bookmark = "http://localhost/fake/images/5" + flavor_bookmark = "http://localhost/fake/flavors/1" + self_link = "http://localhost/v2/fake/servers/%s" % self.uuid + bookmark_link = "http://localhost/fake/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "key_name": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'private': [ + {'version': 4, 'addr': '172.19.0.1'} + ], + 'public': [ + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + {'version': 4, 'addr': '192.168.0.3'}, + ], + }, + "metadata": {}, + "config_drive": None, + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertDictMatch(output, expected_server) + + def test_build_server_detail_with_fault(self): + self.instance['vm_state'] = vm_states.ERROR + self.instance['fault'] = { + 'code': 404, + 'instance_uuid': self.uuid, + 'message': "HTTPNotFound", + 'details': "Stock details for test", + 'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0), + } + + image_bookmark = "http://localhost/fake/images/5" + flavor_bookmark = "http://localhost/fake/flavors/1" + self_link = "http://localhost/v2/fake/servers/%s" % self.uuid + bookmark_link = "http://localhost/fake/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "name": "test_server", + "status": "ERROR", + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "key_name": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'private': [ + {'version': 4, 'addr': '172.19.0.1'} + ], + 'public': [ + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + {'version': 4, 'addr': '192.168.0.3'}, + ], + }, + "metadata": {}, + "config_drive": None, + "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", + }, + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertDictMatch(output, expected_server) + + def test_build_server_detail_with_fault_but_active(self): + self.instance['vm_state'] = vm_states.ACTIVE + self.instance['progress'] = 100 + self.instance['fault'] = { + 'code': 404, + 'instance_uuid': self.uuid, + 'message': "HTTPNotFound", + 'details': "Stock details for test", + 'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0), + } + + image_bookmark = "http://localhost/fake/images/5" + flavor_bookmark = "http://localhost/fake/flavors/1" + self_link = "http://localhost/v2/fake/servers/%s" % self.uuid + bookmark_link = "http://localhost/fake/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 100, + "name": "test_server", + "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "key_name": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'private': [ + {'version': 4, 'addr': '172.19.0.1'} + ], + 'public': [ + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + {'version': 4, 'addr': '192.168.0.3'}, + ], + }, + "metadata": {}, + "config_drive": None, + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertDictMatch(output, expected_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/fake/images/5" + flavor_bookmark = "http://localhost/fake/flavors/1" + self_link = "http://localhost/v2/fake/servers/%s" % self.uuid + bookmark_link = "http://localhost/fake/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 100, + "name": "test_server", + "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "key_name": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'private': [ + {'version': 4, 'addr': '172.19.0.1'} + ], + 'public': [ + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + {'version': 4, 'addr': '192.168.0.3'}, + ], + }, + "metadata": {}, + "config_drive": None, + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertDictMatch(output, expected_server) + + def test_build_server_detail_with_accessipv4(self): + + self.instance['access_ip_v4'] = '1.2.3.4' + + image_bookmark = "http://localhost/fake/images/5" + flavor_bookmark = "http://localhost/fake/flavors/1" + self_link = "http://localhost/v2/fake/servers/%s" % self.uuid + bookmark_link = "http://localhost/fake/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "key_name": "", + "status": "BUILD", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'private': [ + {'version': 4, 'addr': '172.19.0.1'} + ], + 'public': [ + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + {'version': 4, 'addr': '192.168.0.3'}, + ], + }, + "metadata": {}, + "config_drive": None, + "accessIPv4": "1.2.3.4", + "accessIPv6": "", + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertDictMatch(output, expected_server) + + def test_build_server_detail_with_accessipv6(self): + + self.instance['access_ip_v6'] = 'fead::1234' + + image_bookmark = "http://localhost/fake/images/5" + flavor_bookmark = "http://localhost/fake/flavors/1" + self_link = "http://localhost/v2/fake/servers/%s" % self.uuid + bookmark_link = "http://localhost/fake/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "key_name": "", + "status": "BUILD", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'private': [ + {'version': 4, 'addr': '172.19.0.1'} + ], + 'public': [ + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + {'version': 4, 'addr': '192.168.0.3'}, + ] + }, + "metadata": {}, + "config_drive": None, + "accessIPv4": "", + "accessIPv6": "fead::1234", + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertDictMatch(output, expected_server) + + def test_build_server_detail_with_metadata(self): + + metadata = [] + metadata.append(InstanceMetadata(key="Open", value="Stack")) + metadata.append(InstanceMetadata(key="Number", value=1)) + self.instance['metadata'] = metadata + + image_bookmark = "http://localhost/fake/images/5" + flavor_bookmark = "http://localhost/fake/flavors/1" + self_link = "http://localhost/v2/fake/servers/%s" % self.uuid + bookmark_link = "http://localhost/fake/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake", + "tenant_id": "fake", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "key_name": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'private': [ + {'version': 4, 'addr': '172.19.0.1'} + ], + 'public': [ + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + {'version': 4, 'addr': '192.168.0.3'}, + ] + }, + "metadata": { + "Open": "Stack", + "Number": "1", + }, + "config_drive": None, + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertDictMatch(output, expected_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' + + def setUp(self): + self.maxDiff = None + test.TestCase.setUp(self) + + def test_xml_declaration(self): + serializer = servers.ServerTemplate() + + fixture = { + "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", + "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', + }, + ], + } + } + + output = serializer.serialize(fixture) + print output + has_dec = output.startswith("") + self.assertTrue(has_dec) + + def test_show(self): + serializer = servers.ServerTemplate() + + fixture = { + "server": { + "id": FAKE_UUID, + "user_id": "fake", + "tenant_id": "fake", + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "key_name": '', + "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', + }, + ], + } + } + + output = serializer.serialize(fixture) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + server_dict = fixture['server'] + + for key in ['name', 'id', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6']: + self.assertEqual(root.get(key), str(server_dict[key])) + + 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 test_create(self): + serializer = servers.FullServerTemplate() + + fixture = { + "server": { + "id": FAKE_UUID, + "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", + "adminPass": "test_password", + "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', + }, + ], + } + } + + output = serializer.serialize(fixture) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + server_dict = fixture['server'] + + for key in ['name', 'id', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6', 'adminPass']: + self.assertEqual(root.get(key), str(server_dict[key])) + + 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 test_index(self): + serializer = servers.MinimalServersTemplate() + + uuid1 = get_fake_uuid(1) + uuid2 = 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": get_fake_uuid(1), + "name": "test_server", + 'links': [ + { + 'href': expected_server_href, + 'rel': 'self', + }, + { + 'href': expected_server_bookmark, + 'rel': 'bookmark', + }, + ], + }, + { + "id": 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) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers_index') + 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 = get_fake_uuid(1) + uuid2 = 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": get_fake_uuid(1), + "name": "test_server", + 'links': [ + { + 'href': expected_server_href, + 'rel': 'self', + }, + { + 'href': expected_server_bookmark, + 'rel': 'bookmark', + }, + ], + }, + { + "id": 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) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers_index') + 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 = 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 = 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": 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", + }, + ], + }, + "metadata": { + "Number": "1", + }, + "links": [ + { + "href": expected_server_href, + "rel": "self", + }, + { + "href": expected_server_bookmark, + "rel": "bookmark", + }, + ], + }, + { + "id": 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", + }, + ], + }, + "metadata": { + "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] + + for key in ['name', 'id', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6']: + 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) + + metadata_root = server_elem.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + 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 = server_elem.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 = server_elem.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 = server_elem.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + 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 test_update(self): + serializer = servers.ServerTemplate() + + fixture = { + "server": { + "id": FAKE_UUID, + "user_id": "fake", + "tenant_id": "fake", + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + "progress": 0, + "name": "test_server", + "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', + }, + ], + "fault": { + "code": 500, + "created": self.TIMESTAMP, + "message": "Error Message", + "details": "Fault details", + } + } + } + + output = serializer.serialize(fixture) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + server_dict = fixture['server'] + + for key in ['name', 'id', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6']: + self.assertEqual(root.get(key), str(server_dict[key])) + + 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'])) + + 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() + + fixture = { + "server": { + "id": FAKE_UUID, + "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", + "adminPass": "test_password", + "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', + }, + ], + } + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + server_dict = fixture['server'] + + for key in ['name', 'id', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6', 'adminPass']: + self.assertEqual(root.get(key), str(server_dict[key])) + + 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'])) diff --git a/nova/tests/api/openstack/compute/test_urlmap.py b/nova/tests/api/openstack/compute/test_urlmap.py new file mode 100644 index 000000000..ae269d01f --- /dev/null +++ b/nova/tests/api/openstack/compute/test_urlmap.py @@ -0,0 +1,115 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import webob + +from nova import log as logging +from nova import test +from nova.tests.api.openstack import fakes + +LOG = logging.getLogger('nova.tests.api.openstack.compute.test_urlmap') + + +class UrlmapTest(test.TestCase): + def setUp(self): + super(UrlmapTest, self).setUp() + fakes.stub_out_rate_limiting(self.stubs) + + 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()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = json.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()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = json.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()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = json.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()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = json.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()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = json.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()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = json.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()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = json.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()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = json.loads(res.body) + self.assertEqual(body['image']['id'], + 'cedef40a-ed67-4d10-800e-17455edce175') diff --git a/nova/tests/api/openstack/compute/test_versions.py b/nova/tests/api/openstack/compute/test_versions.py new file mode 100644 index 000000000..b1383be60 --- /dev/null +++ b/nova/tests/api/openstack/compute/test_versions.py @@ -0,0 +1,668 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import feedparser +from lxml import etree +import stubout +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 context +from nova import test +from nova.tests.api.openstack import common +from nova.tests.api.openstack import fakes +from nova import utils + + +NS = { + 'atom': 'http://www.w3.org/2005/Atom', + 'ns': 'http://docs.openstack.org/compute/api/v1.1' +} + +VERSIONS = { + "v2.0": { + "id": "v2.0", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf", + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml;version=2", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=2", + }, + ], + }, +} + + +class VersionsTest(test.TestCase): + def setUp(self): + super(VersionsTest, self).setUp() + self.context = context.get_admin_context() + self.stubs = stubout.StubOutForTesting() + fakes.stub_out_auth(self.stubs) + #Stub out VERSIONS + self.old_versions = versions.VERSIONS + versions.VERSIONS = VERSIONS + + def tearDown(self): + versions.VERSIONS = self.old_versions + super(VersionsTest, self).tearDown() + + 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 = json.loads(res.body)["versions"] + expected = [ + { + "id": "v2.0", + "status": "CURRENT", + "updated": "2011-01-21T11: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): + req = webob.Request.blank('/v2/') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + version = json.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": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf", + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl", + }, + ], + "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_content_type(self): + req = webob.Request.blank('/') + req.accept = "application/json;version=2" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + version = json.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": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf", + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl", + }, + ], + "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_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 = 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) + print 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), 1) + + for i, v in enumerate(['v2.0']): + version = versions[i] + expected = 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), 3) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') + self.assertEqual(entry.links[0]['rel'], 'self') + self.assertEqual(entry.links[1], { + 'href': 'http://docs.rackspacecloud.com/servers/api/v1.1/'\ + 'cs-devguide-20110125.pdf', + 'type': 'application/pdf', + 'rel': 'describedby'}) + self.assertEqual(entry.links[2], { + 'href': 'http://docs.rackspacecloud.com/servers/api/v1.1/'\ + 'application.wadl', + 'type': 'application/vnd.sun.wadl+xml', + '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, '2011-01-21T11: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), 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), 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" + }, + ], + }, + ], } + + self.assertDictMatch(expected, json.loads(res.body)) + + 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), 1) + + 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, + 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'}])) + + 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(utils.gen_uuid()) + 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" + }, + ], + }, + ], } + + self.assertDictMatch(expected, json.loads(res.body)) + + +class VersionsViewBuilderTests(test.TestCase): + 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() + + self.assertEqual(actual, expected) + + +class VersionsSerializerTests(test.TestCase): + 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": 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": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf", + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl", + }, + ], + "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), 3) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') + self.assertEqual(entry.links[0]['rel'], 'self') + self.assertEqual(entry.links[1], { + 'rel': 'describedby', + 'type': 'application/pdf', + 'href': 'http://docs.rackspacecloud.com/' + 'servers/api/v1.1/cs-devguide-20110125.pdf'}) + self.assertEqual(entry.links[2], { + 'rel': 'describedby', + 'type': 'application/vnd.sun.wadl+xml', + 'href': 'http://docs.rackspacecloud.com/' + 'servers/api/v1.1/application.wadl', + }) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index f9f8dd0a0..9fa1749ef 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -24,14 +24,14 @@ import webob.request from glance import client as glance_client -import nova.api.openstack.v2.auth from nova.api import auth as api_auth -from nova.api.openstack import v2 -from nova.api.openstack.v2 import auth -from nova.api.openstack.v2 import extensions -from nova.api.openstack.v2 import limits -from nova.api.openstack.v2 import urlmap -from nova.api.openstack.v2 import versions +from nova.api import openstack as openstack_api +from nova.api.openstack import compute +from nova.api.openstack import auth +from nova.api.openstack.compute import extensions +from nova.api.openstack.compute import limits +from nova.api.openstack import urlmap +from nova.api.openstack.compute import versions from nova.api.openstack import wsgi as os_wsgi from nova.auth.manager import User, Project from nova.compute import instance_types @@ -77,24 +77,24 @@ def wsgi_app(inner_app_v2=None, fake_auth=True, fake_auth_context=None, serialization=os_wsgi.LazySerializationMiddleware, use_no_auth=False): if not inner_app_v2: - inner_app_v2 = v2.APIRouter() + inner_app_v2 = compute.APIRouter() if fake_auth: if fake_auth_context is not None: ctxt = fake_auth_context else: ctxt = context.RequestContext('fake', 'fake', auth_token=True) - api_v2 = v2.FaultWrapper(api_auth.InjectContext(ctxt, + api_v2 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt, limits.RateLimitingMiddleware( serialization( extensions.ExtensionMiddleware(inner_app_v2))))) elif use_no_auth: - api_v2 = v2.FaultWrapper(auth.NoAuthMiddleware( + api_v2 = openstack_api.FaultWrapper(auth.NoAuthMiddleware( limits.RateLimitingMiddleware( serialization( extensions.ExtensionMiddleware(inner_app_v2))))) else: - api_v2 = v2.FaultWrapper(auth.AuthMiddleware( + api_v2 = openstack_api.FaultWrapper(auth.AuthMiddleware( limits.RateLimitingMiddleware( serialization( extensions.ExtensionMiddleware(inner_app_v2))))) @@ -102,7 +102,7 @@ def wsgi_app(inner_app_v2=None, fake_auth=True, fake_auth_context=None, mapper = urlmap.URLMap() mapper['/v2'] = api_v2 mapper['/v1.1'] = api_v2 - mapper['/'] = v2.FaultWrapper(versions.Versions()) + mapper['/'] = openstack_api.FaultWrapper(versions.Versions()) return mapper @@ -138,9 +138,9 @@ def stub_out_auth(stubs): def fake_auth_init(self, app): self.application = app - stubs.Set(nova.api.openstack.v2.auth.AuthMiddleware, + stubs.Set(auth.AuthMiddleware, '__init__', fake_auth_init) - stubs.Set(nova.api.openstack.v2.auth.AuthMiddleware, + stubs.Set(auth.AuthMiddleware, '__call__', fake_wsgi) @@ -149,10 +149,10 @@ def stub_out_rate_limiting(stubs): super(limits.RateLimitingMiddleware, self).__init__(app) self.application = app - stubs.Set(nova.api.openstack.v2.limits.RateLimitingMiddleware, + stubs.Set(nova.api.openstack.compute.limits.RateLimitingMiddleware, '__init__', fake_rate_init) - stubs.Set(nova.api.openstack.v2.limits.RateLimitingMiddleware, + stubs.Set(nova.api.openstack.compute.limits.RateLimitingMiddleware, '__call__', fake_wsgi) diff --git a/nova/tests/api/openstack/v2/__init__.py b/nova/tests/api/openstack/v2/__init__.py deleted file mode 100644 index 00fcfbb00..000000000 --- a/nova/tests/api/openstack/v2/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. diff --git a/nova/tests/api/openstack/v2/contrib/__init__.py b/nova/tests/api/openstack/v2/contrib/__init__.py deleted file mode 100644 index 848908a95..000000000 --- a/nova/tests/api/openstack/v2/contrib/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC -# -# 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. diff --git a/nova/tests/api/openstack/v2/contrib/test_accounts.py b/nova/tests/api/openstack/v2/contrib/test_accounts.py deleted file mode 100644 index f799853dc..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_accounts.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -import json - -from lxml import etree -import webob - -from nova import test -from nova.api.openstack.v2.contrib import accounts -from nova.auth.manager import User -from nova.tests.api.openstack import fakes - - -def fake_init(self): - self.manager = fakes.FakeAuthManager() - - -def fake_admin_check(self, req): - return True - - -class AccountsTest(test.TestCase): - def setUp(self): - super(AccountsTest, self).setUp() - self.flags(verbose=True, allow_admin_api=True) - self.stubs.Set(accounts.Controller, '__init__', - fake_init) - self.stubs.Set(accounts.Controller, '_check_admin', - fake_admin_check) - fakes.FakeAuthManager.clear_fakes() - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_auth(self.stubs) - - fakemgr = fakes.FakeAuthManager() - joeuser = User('id1', 'guy1', 'acc1', 'secret1', False) - superuser = User('id2', 'guy2', 'acc2', 'secret2', True) - fakemgr.add_user(joeuser) - fakemgr.add_user(superuser) - fakemgr.create_project('test1', joeuser) - fakemgr.create_project('test2', superuser) - - def test_get_account(self): - req = webob.Request.blank('/v2/fake/accounts/test1') - res = req.get_response(fakes.wsgi_app()) - res_dict = json.loads(res.body) - - self.assertEqual(res.status_int, 200) - self.assertEqual(res_dict['account']['id'], 'test1') - self.assertEqual(res_dict['account']['name'], 'test1') - self.assertEqual(res_dict['account']['manager'], 'id1') - - def test_get_account_xml(self): - req = webob.Request.blank('/v2/fake/accounts/test1.xml') - res = req.get_response(fakes.wsgi_app()) - res_tree = etree.fromstring(res.body) - - self.assertEqual(res.status_int, 200) - self.assertEqual('account', res_tree.tag) - self.assertEqual('test1', res_tree.get('id')) - self.assertEqual('test1', res_tree.get('name')) - self.assertEqual('id1', res_tree.get('manager')) - - def test_account_delete(self): - req = webob.Request.blank('/v2/fake/accounts/test1') - req.method = 'DELETE' - res = req.get_response(fakes.wsgi_app()) - self.assertTrue('test1' not in fakes.FakeAuthManager.projects) - self.assertEqual(res.status_int, 200) - - def test_account_create(self): - body = dict(account=dict(description='test account', - manager='id1')) - req = webob.Request.blank('/v2/fake/accounts/newacct') - req.headers["Content-Type"] = "application/json" - req.method = 'PUT' - req.body = json.dumps(body) - - res = req.get_response(fakes.wsgi_app()) - res_dict = json.loads(res.body) - - self.assertEqual(res.status_int, 200) - self.assertEqual(res_dict['account']['id'], 'newacct') - self.assertEqual(res_dict['account']['name'], 'newacct') - self.assertEqual(res_dict['account']['description'], 'test account') - self.assertEqual(res_dict['account']['manager'], 'id1') - self.assertTrue('newacct' in - fakes.FakeAuthManager.projects) - self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 3) - - def test_account_create_xml(self): - body = dict(account=dict(description='test account', - manager='id1')) - req = webob.Request.blank('/v2/fake/accounts/newacct.xml') - req.headers["Content-Type"] = "application/json" - req.method = 'PUT' - req.body = json.dumps(body) - - res = req.get_response(fakes.wsgi_app()) - res_tree = etree.fromstring(res.body) - - self.assertEqual(res.status_int, 200) - self.assertEqual(res_tree.tag, 'account') - self.assertEqual(res_tree.get('id'), 'newacct') - self.assertEqual(res_tree.get('name'), 'newacct') - self.assertEqual(res_tree.get('description'), 'test account') - self.assertEqual(res_tree.get('manager'), 'id1') - self.assertTrue('newacct' in - fakes.FakeAuthManager.projects) - self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 3) - - def test_account_update(self): - body = dict(account=dict(description='test account', - manager='id2')) - req = webob.Request.blank('/v2/fake/accounts/test1') - req.headers["Content-Type"] = "application/json" - req.method = 'PUT' - req.body = json.dumps(body) - - res = req.get_response(fakes.wsgi_app()) - res_dict = json.loads(res.body) - - self.assertEqual(res.status_int, 200) - self.assertEqual(res_dict['account']['id'], 'test1') - self.assertEqual(res_dict['account']['name'], 'test1') - self.assertEqual(res_dict['account']['description'], 'test account') - self.assertEqual(res_dict['account']['manager'], 'id2') - self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 2) - - def test_account_update_xml(self): - body = dict(account=dict(description='test account', - manager='id2')) - req = webob.Request.blank('/v2/fake/accounts/test1.xml') - req.headers["Content-Type"] = "application/json" - req.method = 'PUT' - req.body = json.dumps(body) - - res = req.get_response(fakes.wsgi_app()) - res_tree = etree.fromstring(res.body) - - self.assertEqual(res.status_int, 200) - self.assertEqual(res_tree.tag, 'account') - self.assertEqual(res_tree.get('id'), 'test1') - self.assertEqual(res_tree.get('name'), 'test1') - self.assertEqual(res_tree.get('description'), 'test account') - self.assertEqual(res_tree.get('manager'), 'id2') - self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 2) diff --git a/nova/tests/api/openstack/v2/contrib/test_admin_actions.py b/nova/tests/api/openstack/v2/contrib/test_admin_actions.py deleted file mode 100644 index e3c5e6b08..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_admin_actions.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# -# 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 json - -import webob - -from nova.api.openstack import v2 -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova import compute -from nova import exception -from nova import flags -from nova import test -from nova import utils -from nova.tests.api.openstack import fakes - - -FLAGS = flags.FLAGS - -INSTANCE = { - "id": 1, - "name": "fake", - "display_name": "test_server", - "uuid": "abcd", - "user_id": 'fake_user_id', - "tenant_id": 'fake_tenant_id', - "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), - "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), - "security_groups": [{"id": 1, "name": "test"}], - "progress": 0, - "image_ref": 'http://foo.com/123', - "fixed_ips": [], - "instance_type": {"flavorid": '124'}, - } - - -def fake_compute_api(*args, **kwargs): - return True - - -def fake_compute_api_raises_invalid_state(*args, **kwargs): - raise exception.InstanceInvalidState - - -def fake_compute_api_get(self, context, instance_id): - return {'id': 1, 'uuid': instance_id} - - -class AdminActionsTest(test.TestCase): - - _actions = ('pause', 'unpause', 'suspend', 'resume', 'migrate', - 'resetNetwork', 'injectNetworkInfo', 'lock', 'unlock') - - _methods = ('pause', 'unpause', 'suspend', 'resume', 'resize', - 'reset_network', 'inject_network_info', 'lock', 'unlock') - - _actions_that_check_state = ( - # action, method - ('pause', 'pause'), - ('unpause', 'unpause'), - ('suspend', 'suspend'), - ('resume', 'resume'), - ('migrate', 'resize')) - - def setUp(self): - super(AdminActionsTest, self).setUp() - self.stubs.Set(compute.API, 'get', fake_compute_api_get) - self.UUID = utils.gen_uuid() - self.flags(allow_admin_api=True) - for _method in self._methods: - self.stubs.Set(compute.API, _method, fake_compute_api) - - def test_admin_api_actions(self): - self.maxDiff = None - app = fakes.wsgi_app() - for _action in self._actions: - req = webob.Request.blank('/v2/fake/servers/%s/action' % - self.UUID) - req.method = 'POST' - req.body = json.dumps({_action: None}) - req.content_type = 'application/json' - res = req.get_response(app) - self.assertEqual(res.status_int, 202) - - def test_admin_api_actions_raise_conflict_on_invalid_state(self): - self.maxDiff = None - app = fakes.wsgi_app() - - for _action, _method in self._actions_that_check_state: - self.stubs.Set(compute.API, _method, - fake_compute_api_raises_invalid_state) - - req = webob.Request.blank('/v2/fake/servers/%s/action' % - self.UUID) - req.method = 'POST' - req.body = json.dumps({_action: None}) - req.content_type = 'application/json' - res = req.get_response(app) - self.assertEqual(res.status_int, 409) - self.assertIn("invalid state for '%(_action)s'" % locals(), - res.body) - - -class CreateBackupTests(test.TestCase): - - def setUp(self): - super(CreateBackupTests, self).setUp() - - self.stubs.Set(compute.API, 'get', fake_compute_api_get) - self.backup_stubs = fakes.stub_out_compute_api_backup(self.stubs) - - self.flags(allow_admin_api=True) - router = v2.APIRouter() - ext_middleware = extensions.ExtensionMiddleware(router) - self.app = wsgi.LazySerializationMiddleware(ext_middleware) - - self.uuid = utils.gen_uuid() - - def _get_request(self, body): - url = '/fake/servers/%s/action' % self.uuid - req = fakes.HTTPRequest.blank(url) - req.method = 'POST' - req.content_type = 'application/json' - req.body = json.dumps(body) - return req - - def test_create_backup_with_metadata(self): - body = { - 'createBackup': { - 'name': 'Backup 1', - 'backup_type': 'daily', - 'rotation': 1, - 'metadata': {'123': 'asdf'}, - }, - } - - request = self._get_request(body) - response = request.get_response(self.app) - - self.assertEqual(response.status_int, 202) - self.assertTrue(response.headers['Location']) - - def test_create_backup_with_too_much_metadata(self): - body = { - 'createBackup': { - 'name': 'Backup 1', - 'backup_type': 'daily', - 'rotation': 1, - 'metadata': {'123': 'asdf'}, - }, - } - for num in range(FLAGS.quota_metadata_items + 1): - body['createBackup']['metadata']['foo%i' % num] = "bar" - - request = self._get_request(body) - response = request.get_response(self.app) - self.assertEqual(response.status_int, 413) - - def test_create_backup_no_name(self): - """Name is required for backups""" - body = { - 'createBackup': { - 'backup_type': 'daily', - 'rotation': 1, - }, - } - - request = self._get_request(body) - response = request.get_response(self.app) - self.assertEqual(response.status_int, 400) - - def test_create_backup_no_rotation(self): - """Rotation is required for backup requests""" - body = { - 'createBackup': { - 'name': 'Backup 1', - 'backup_type': 'daily', - }, - } - - request = self._get_request(body) - response = request.get_response(self.app) - self.assertEqual(response.status_int, 400) - - 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, - }, - } - - request = self._get_request(body) - response = request.get_response(self.app) - self.assertEqual(response.status_int, 400) - - def test_create_backup_bad_entity(self): - body = {'createBackup': 'go'} - - request = self._get_request(body) - response = request.get_response(self.app) - self.assertEqual(response.status_int, 400) - - def test_create_backup(self): - """The happy path for creating backups""" - body = { - 'createBackup': { - 'name': 'Backup 1', - 'backup_type': 'daily', - 'rotation': 1, - }, - } - - request = self._get_request(body) - response = request.get_response(self.app) - - self.assertTrue(response.headers['Location']) - instance_ref = self.backup_stubs.extra_props_last_call['instance_ref'] - expected_server_location = 'http://localhost/v2/servers/%s' % self.uuid - self.assertEqual(expected_server_location, instance_ref) - - def test_create_backup_raises_conflict_on_invalid_state(self): - body = { - 'createBackup': { - 'name': 'Backup 1', - 'backup_type': 'daily', - 'rotation': 1, - }, - } - - self.stubs.Set(compute.API, 'backup', - fake_compute_api_raises_invalid_state) - - request = self._get_request(body) - response = request.get_response(self.app) - self.assertEqual(response.status_int, 409) diff --git a/nova/tests/api/openstack/v2/contrib/test_cloudpipe.py b/nova/tests/api/openstack/v2/contrib/test_cloudpipe.py deleted file mode 100644 index b2a5d308a..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_cloudpipe.py +++ /dev/null @@ -1,234 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime -import json - -import webob -from lxml import etree - -from nova.api import auth -from nova.api.openstack import v2 -from nova.api.openstack.v2 import wsgi -from nova.api.openstack.v2.contrib import cloudpipe -from nova.auth import manager -from nova.cloudpipe import pipelib -from nova import context -from nova import crypto -from nova import db -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes -from nova import utils - - -EMPTY_INSTANCE_LIST = True -FLAGS = flags.FLAGS - - -class FakeProject(object): - def __init__(self, id, name, manager, desc, members, ip, port): - self.id = id - self.name = name - self.project_manager_id = manager - self.description = desc - self.member_ids = members - self.vpn_ip = ip - self.vpn_port = port - - -def fake_vpn_instance(): - return {'id': 7, 'image_id': FLAGS.vpn_image_id, 'vm_state': 'active', - 'created_at': utils.parse_strtime('1981-10-20T00:00:00.000000'), - 'uuid': 7777} - - -def fake_vpn_instance_low_id(): - return {'id': 4, 'image_id': FLAGS.vpn_image_id, 'vm_state': 'active', - 'created_at': utils.parse_strtime('1981-10-20T00:00:00.000000')} - - -def fake_project(): - proj = FakeProject(1, '1', 'fakeuser', '', [1], '127.0.0.1', 22) - return proj - - -def db_instance_get_all_by_project(self, project_id): - if EMPTY_INSTANCE_LIST: - return [] - else: - return [fake_vpn_instance()] - - -def db_security_group_exists(context, project_id, group_name): - # used in pipelib - return True - - -def pipelib_launch_vpn_instance(self, project_id, user_id): - global EMPTY_INSTANCE_LIST - EMPTY_INSTANCE_LIST = False - - -def auth_manager_get_project(self, project_id): - return fake_project() - - -def auth_manager_get_projects(self): - return [fake_project()] - - -def utils_vpn_ping(addr, port, timoeout=0.05, session_id=None): - return True - - -def better_not_call_this(*args, **kwargs): - raise Exception("You should not have done that") - - -class FakeAuthManager(object): - def get_projects(self): - return [fake_project()] - - def get_project(self, project_id): - return fake_project() - - -class CloudpipeTest(test.TestCase): - - def setUp(self): - super(CloudpipeTest, self).setUp() - self.flags(allow_admin_api=True) - self.app = fakes.wsgi_app() - inner_app = v2.APIRouter() - self.context = context.RequestContext('fake', 'fake', is_admin=True) - self.app = auth.InjectContext(self.context, inner_app) - route = inner_app.map.match('/1234/os-cloudpipe') - self.controller = route['controller'].controller - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - self.stubs.Set(db, "instance_get_all_by_project", - db_instance_get_all_by_project) - self.stubs.Set(db, "security_group_exists", - db_security_group_exists) - self.stubs.SmartSet(self.controller.cloudpipe, "launch_vpn_instance", - pipelib_launch_vpn_instance) - #self.stubs.SmartSet(self.controller.auth_manager, "get_project", - # auth_manager_get_project) - #self.stubs.SmartSet(self.controller.auth_manager, "get_projects", - # auth_manager_get_projects) - # NOTE(todd): The above code (just setting the stub, not invoking it) - # causes failures in AuthManagerLdapTestCase. So use a fake object. - self.controller.auth_manager = FakeAuthManager() - self.stubs.Set(utils, 'vpn_ping', utils_vpn_ping) - global EMPTY_INSTANCE_LIST - EMPTY_INSTANCE_LIST = True - - def test_cloudpipe_list_none_running(self): - """Should still get an entry per-project, just less descriptive.""" - req = webob.Request.blank('/fake/os-cloudpipe') - res = req.get_response(self.app) - self.assertEqual(res.status_int, 200) - res_dict = json.loads(res.body) - response = {'cloudpipes': [{'project_id': 1, 'public_ip': '127.0.0.1', - 'public_port': 22, 'state': 'pending'}]} - self.assertEqual(res_dict, response) - - def test_cloudpipe_list(self): - global EMPTY_INSTANCE_LIST - EMPTY_INSTANCE_LIST = False - req = webob.Request.blank('/fake/os-cloudpipe') - res = req.get_response(self.app) - self.assertEqual(res.status_int, 200) - res_dict = json.loads(res.body) - response = {'cloudpipes': [{'project_id': 1, 'public_ip': '127.0.0.1', - 'public_port': 22, 'state': 'running', - 'instance_id': 7777, - 'created_at': '1981-10-20T00:00:00Z'}]} - self.assertEqual(res_dict, response) - - def test_cloudpipe_create(self): - body = {'cloudpipe': {'project_id': 1}} - req = webob.Request.blank('/fake/os-cloudpipe') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['Content-Type'] = 'application/json' - res = req.get_response(self.app) - self.assertEqual(res.status_int, 200) - res_dict = json.loads(res.body) - response = {'instance_id': 7777} - self.assertEqual(res_dict, response) - - def test_cloudpipe_create_already_running(self): - global EMPTY_INSTANCE_LIST - EMPTY_INSTANCE_LIST = False - self.stubs.SmartSet(self.controller.cloudpipe, 'launch_vpn_instance', - better_not_call_this) - body = {'cloudpipe': {'project_id': 1}} - req = webob.Request.blank('/fake/os-cloudpipe') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['Content-Type'] = 'application/json' - res = req.get_response(self.app) - self.assertEqual(res.status_int, 200) - res_dict = json.loads(res.body) - response = {'instance_id': 7777} - self.assertEqual(res_dict, response) - - -class CloudpipesXMLSerializerTest(test.TestCase): - def test_default_serializer(self): - serializer = cloudpipe.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.assertTrue(child.tag in exemplar['cloudpipe']) - self.assertEqual(child.text, exemplar['cloudpipe'][child.tag]) - - def test_index_serializer(self): - serializer = cloudpipe.CloudpipesTemplate() - exemplar = dict(cloudpipes=[ - dict(cloudpipe=dict( - project_id='1234', - public_ip='1.2.3.4', - public_port='321', - instance_id='1234-1234-1234-1234', - created_at=utils.isotime(datetime.datetime.utcnow()), - state='running')), - dict(cloudpipe=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): - self.assertEqual('cloudpipe', cl_pipe.tag) - kp_data = exemplar['cloudpipes'][idx]['cloudpipe'] - for child in cl_pipe: - self.assertTrue(child.tag in 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 = ("\n" - '4321') - result = deserializer.deserialize(intext)['body'] - self.assertEqual(result, exemplar) diff --git a/nova/tests/api/openstack/v2/contrib/test_console_output.py b/nova/tests/api/openstack/v2/contrib/test_console_output.py deleted file mode 100644 index ad22ff4cf..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_console_output.py +++ /dev/null @@ -1,97 +0,0 @@ -# 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 json - -import webob - -from nova import compute -from nova import exception -from nova import test -from nova.tests.api.openstack import fakes - - -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(self, context, instance_uuid): - return {'uuid': instance_uuid} - - -def fake_get_not_found(self, context, instance_uuid): - raise exception.NotFound() - - -class ConsoleOutputExtensionTest(test.TestCase): - - def setUp(self): - super(ConsoleOutputExtensionTest, self).setUp() - self.stubs.Set(compute.API, 'get_console_output', - fake_get_console_output) - self.stubs.Set(compute.API, 'get', fake_get) - - def test_get_text_console_instance_action(self): - body = {'os-getConsoleOutput': {}} - req = webob.Request.blank('/v2/fake/servers/1/action') - req.method = "POST" - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - res = req.get_response(fakes.wsgi_app()) - output = json.loads(res.body) - self.assertEqual(res.status_int, 200) - self.assertEqual(output, {'output': '0\n1\n2\n3\n4'}) - - def test_get_console_output_with_tail(self): - body = {'os-getConsoleOutput': {'length': 3}} - req = webob.Request.blank('/v2/fake/servers/1/action') - req.method = "POST" - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = req.get_response(fakes.wsgi_app()) - output = json.loads(res.body) - self.assertEqual(res.status_int, 200) - self.assertEqual(output, {'output': '2\n3\n4'}) - - def test_get_text_console_no_instance(self): - self.stubs.Set(compute.API, 'get', fake_get_not_found) - body = {'os-getConsoleOutput': {}} - req = webob.Request.blank('/v2/fake/servers/1/action') - req.method = "POST" - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 404) - - def test_get_text_console_bad_body(self): - body = {} - req = webob.Request.blank('/v2/fake/servers/1/action') - req.method = "POST" - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) diff --git a/nova/tests/api/openstack/v2/contrib/test_createserverext.py b/nova/tests/api/openstack/v2/contrib/test_createserverext.py deleted file mode 100644 index 2393780a2..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_createserverext.py +++ /dev/null @@ -1,430 +0,0 @@ -# vim: tabstop=5 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import base64 -import datetime -import json -from xml.dom import minidom - -import webob - -import nova -from nova import db -from nova import exception -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes - - -FLAGS = flags.FLAGS -FLAGS.verbose = True - -FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' - -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')] - -INSTANCE = { - "id": 1, - "name": "fake", - "display_name": "test_server", - "uuid": FAKE_UUID, - "user_id": 'fake_user_id', - "tenant_id": 'fake_tenant_id', - "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), - "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), - "security_groups": [{"id": 1, "name": "test"}], - "progress": 0, - "image_ref": 'http://foo.com/123', - "fixed_ips": [], - "instance_type": {"flavorid": '124'}, - } - - -def return_server_by_id(context, id, session=None): - INSTANCE['id'] = id - return INSTANCE - - -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() - - def tearDown(self): - super(CreateserverextTest, self).tearDown() - - def _make_stub_method(self, canned_return): - def stub_method(*args, **kwargs): - return canned_return - return stub_method - - def _setup_mock_compute_api(self): - - class MockComputeAPI(nova.compute.API): - - def __init__(self): - self.injected_files = None - self.networks = None - self.user_data = None - self.db = db - - def create(self, *args, **kwargs): - 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) - - def set_admin_password(self, *args, **kwargs): - pass - - compute_api = MockComputeAPI() - self.stubs.Set(nova.compute, 'API', - self._make_stub_method(compute_api)) - return compute_api - - def _setup_mock_network_api(self): - fakes.stub_out_nw_api(self.stubs) - - 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 = json.dumps(body_dict) - return req - - def _run_create_instance_with_mock_compute_api(self, request): - compute_api = self._setup_mock_compute_api() - self._setup_mock_network_api() - response = request.get_response(fakes.wsgi_app()) - return compute_api, response - - def _format_xml_request_body(self, body_dict): - server = body_dict['server'] - body_parts = [] - body_parts.extend([ - '', - '' % ( - server['name'], server['imageRef'], server['flavorRef'])]) - if 'metadata' in server: - metadata = server['metadata'] - body_parts.append('') - for item in metadata.iteritems(): - body_parts.append('%s' % item) - body_parts.append('') - if 'personality' in server: - personalities = server['personality'] - body_parts.append('') - for file in personalities: - item = (file['path'], file['contents']) - body_parts.append('%s' % item) - body_parts.append('') - if 'networks' in server: - networks = server['networks'] - body_parts.append('') - for network in networks: - item = (network['uuid'], network['fixed_ip']) - body_parts.append('' - % item) - body_parts.append('') - body_parts.append('') - 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) - compute_api, response = \ - self._run_create_instance_with_mock_compute_api(request) - return request, response, compute_api.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) - compute_api, response = \ - self._run_create_instance_with_mock_compute_api(request) - return request, response, compute_api.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) - compute_api, response = \ - self._run_create_instance_with_mock_compute_api(request) - return request, response, compute_api.networks - - def test_create_instance_with_no_networks(self): - request, response, networks = \ - self._create_instance_with_networks_json(networks=None) - self.assertEquals(response.status_int, 202) - self.assertEquals(networks, None) - - def test_create_instance_with_no_networks_xml(self): - request, response, networks = \ - self._create_instance_with_networks_xml(networks=None) - self.assertEquals(response.status_int, 202) - self.assertEquals(networks, None) - - def test_create_instance_with_one_network(self): - request, response, networks = \ - self._create_instance_with_networks_json([FAKE_NETWORKS[0]]) - self.assertEquals(response.status_int, 202) - self.assertEquals(networks, [FAKE_NETWORKS[0]]) - - def test_create_instance_with_one_network_xml(self): - request, response, networks = \ - self._create_instance_with_networks_xml([FAKE_NETWORKS[0]]) - self.assertEquals(response.status_int, 202) - self.assertEquals(networks, [FAKE_NETWORKS[0]]) - - def test_create_instance_with_two_networks(self): - request, response, networks = \ - self._create_instance_with_networks_json(FAKE_NETWORKS) - self.assertEquals(response.status_int, 202) - self.assertEquals(networks, FAKE_NETWORKS) - - def test_create_instance_with_two_networks_xml(self): - request, response, networks = \ - self._create_instance_with_networks_xml(FAKE_NETWORKS) - self.assertEquals(response.status_int, 202) - self.assertEquals(networks, FAKE_NETWORKS) - - def test_create_instance_with_duplicate_networks(self): - request, response, networks = \ - self._create_instance_with_networks_json(DUPLICATE_NETWORKS) - self.assertEquals(response.status_int, 400) - self.assertEquals(networks, None) - - def test_create_instance_with_duplicate_networks_xml(self): - request, response, networks = \ - self._create_instance_with_networks_xml(DUPLICATE_NETWORKS) - self.assertEquals(response.status_int, 400) - self.assertEquals(networks, None) - - 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) - compute_api, response = \ - self._run_create_instance_with_mock_compute_api(request) - self.assertEquals(response.status_int, 400) - self.assertEquals(compute_api.networks, None) - - 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, '') - compute_api, response = \ - self._run_create_instance_with_mock_compute_api(request) - self.assertEquals(response.status_int, 400) - self.assertEquals(compute_api.networks, None) - - def test_create_instance_with_network_invalid_id(self): - request, response, networks = \ - self._create_instance_with_networks_json(INVALID_NETWORKS) - self.assertEquals(response.status_int, 400) - self.assertEquals(networks, None) - - def test_create_instance_with_network_invalid_id_xml(self): - request, response, networks = \ - self._create_instance_with_networks_xml(INVALID_NETWORKS) - self.assertEquals(response.status_int, 400) - self.assertEquals(networks, None) - - def test_create_instance_with_network_empty_fixed_ip(self): - networks = [('1', '')] - request, response, networks = \ - self._create_instance_with_networks_json(networks) - self.assertEquals(response.status_int, 400) - self.assertEquals(networks, None) - - def test_create_instance_with_network_non_string_fixed_ip(self): - networks = [('1', 12345)] - request, response, networks = \ - self._create_instance_with_networks_json(networks) - self.assertEquals(response.status_int, 400) - self.assertEquals(networks, None) - - def test_create_instance_with_network_empty_fixed_ip_xml(self): - networks = [('1', '')] - request, response, networks = \ - self._create_instance_with_networks_xml(networks) - self.assertEquals(response.status_int, 400) - self.assertEquals(networks, None) - - 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) - compute_api, response = \ - self._run_create_instance_with_mock_compute_api(request) - self.assertEquals(response.status_int, 202) - self.assertEquals(compute_api.networks, - [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)]) - - 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"', '') - compute_api, response = \ - self._run_create_instance_with_mock_compute_api(request) - self.assertEquals(response.status_int, 202) - self.assertEquals(compute_api.networks, - [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)]) - - def test_create_instance_with_userdata(self): - user_data_contents = '#!/bin/bash\necho "Oh no!"\n' - user_data_contents = base64.b64encode(user_data_contents) - request, response, user_data = \ - self._create_instance_with_user_data_json(user_data_contents) - self.assertEquals(response.status_int, 202) - self.assertEquals(user_data, user_data_contents) - - def test_create_instance_with_userdata_none(self): - user_data_contents = None - request, response, user_data = \ - self._create_instance_with_user_data_json(user_data_contents) - self.assertEquals(response.status_int, 202) - self.assertEquals(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' - request, response, user_data = \ - self._create_instance_with_user_data_json(user_data_contents) - self.assertEquals(response.status_int, 400) - self.assertEquals(user_data, None) - - def test_create_instance_with_security_group_json(self): - security_groups = ['test', 'test1'] - self.stubs.Set(nova.db, 'security_group_get_by_name', - return_security_group_get_by_name) - self.stubs.Set(nova.db, 'instance_add_security_group', - return_instance_add_security_group) - self._setup_mock_network_api() - body_dict = self._create_security_group_request_dict(security_groups) - request = self._get_create_request_json(body_dict) - compute_api, response = \ - self._run_create_instance_with_mock_compute_api(request) - self.assertEquals(response.status_int, 202) - - def test_get_server_by_id_verify_security_groups_json(self): - self.stubs.Set(nova.db, 'instance_get', return_server_by_id) - self._setup_mock_network_api() - req = webob.Request.blank('/v2/fake/os-create-server-ext/1') - req.headers['Content-Type'] = 'application/json' - response = req.get_response(fakes.wsgi_app()) - self.assertEquals(response.status_int, 200) - res_dict = json.loads(response.body) - expected_security_group = [{"name": "test"}] - self.assertEquals(res_dict['server']['security_groups'], - expected_security_group) - - def test_get_server_by_id_verify_security_groups_xml(self): - self.stubs.Set(nova.db, 'instance_get', return_server_by_id) - self._setup_mock_network_api() - req = webob.Request.blank('/v2/fake/os-create-server-ext/1') - req.headers['Accept'] = 'application/xml' - response = req.get_response(fakes.wsgi_app()) - self.assertEquals(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(INSTANCE['security_groups'][0]['name'], - sec_group.getAttribute("name")) diff --git a/nova/tests/api/openstack/v2/contrib/test_deferred_delete.py b/nova/tests/api/openstack/v2/contrib/test_deferred_delete.py deleted file mode 100644 index 222e62577..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_deferred_delete.py +++ /dev/null @@ -1,120 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mox -import webob - -from nova.api.openstack.v2.contrib import deferred_delete -from nova import compute -from nova import exception -from nova import test - - -class FakeExtensionDescriptor(object): - def register(*args, **kwargs): - pass - - -class FakeRequest(object): - def __init__(self, context): - self.environ = {'nova.context': context} - - -class DeferredDeleteExtensionTest(test.TestCase): - def setUp(self): - super(DeferredDeleteExtensionTest, self).setUp() - self.extension = deferred_delete.Deferred_delete( - FakeExtensionDescriptor()) - self.fake_input_dict = {} - self.fake_uuid = 'fake_uuid' - self.fake_context = 'fake_context' - self.fake_req = FakeRequest(self.fake_context) - - def test_force_delete(self): - self.mox.StubOutWithMock(compute.API, 'get') - self.mox.StubOutWithMock(compute.API, 'force_delete') - - fake_instance = 'fake_instance' - - compute.API.get(self.fake_context, self.fake_uuid).AndReturn( - fake_instance) - compute.API.force_delete(self.fake_context, fake_instance) - - self.mox.ReplayAll() - res = self.extension._force_delete(self.fake_input_dict, - self.fake_req, self.fake_uuid) - self.mox.VerifyAll() - self.assertEqual(res.status_int, 202) - - def test_force_delete_raises_conflict_on_invalid_state(self): - self.mox.StubOutWithMock(compute.API, 'get') - self.mox.StubOutWithMock(compute.API, 'force_delete') - - fake_instance = 'fake_instance' - - compute.API.get(self.fake_context, self.fake_uuid).AndReturn( - fake_instance) - compute.API.force_delete(self.fake_context, fake_instance).AndRaise( - exception.InstanceInvalidState) - - self.mox.ReplayAll() - self.assertRaises(webob.exc.HTTPConflict, - self.extension._force_delete, self.fake_input_dict, - self.fake_req, self.fake_uuid) - self.mox.VerifyAll() - - def test_restore(self): - self.mox.StubOutWithMock(compute.API, 'get') - self.mox.StubOutWithMock(compute.API, 'restore') - - fake_instance = 'fake_instance' - - compute.API.get(self.fake_context, self.fake_uuid).AndReturn( - fake_instance) - compute.API.restore(self.fake_context, fake_instance) - - self.mox.ReplayAll() - res = self.extension._restore(self.fake_input_dict, - self.fake_req, self.fake_uuid) - self.mox.VerifyAll() - self.assertEqual(res.status_int, 202) - - def test_restore_raises_conflict_on_invalid_state(self): - self.mox.StubOutWithMock(compute.API, 'get') - self.mox.StubOutWithMock(compute.API, 'restore') - - fake_instance = 'fake_instance' - - compute.API.get(self.fake_context, self.fake_uuid).AndReturn( - fake_instance) - compute.API.restore(self.fake_context, fake_instance).AndRaise( - exception.InstanceInvalidState) - - self.mox.ReplayAll() - self.assertRaises(webob.exc.HTTPConflict, self.extension._restore, - self.fake_input_dict, self.fake_req, self.fake_uuid) - self.mox.VerifyAll() - - def test_get_actions(self): - result = self.extension.get_actions() - self.assertEqual(len(result), 2) - - action_and_methods = [(x.action_name, x.handler) for x in result] - self.assertIn(('restore', self.extension._restore), - action_and_methods) - self.assertIn(('forceDelete', self.extension._force_delete), - action_and_methods) diff --git a/nova/tests/api/openstack/v2/contrib/test_disk_config.py b/nova/tests/api/openstack/v2/contrib/test_disk_config.py deleted file mode 100644 index 95cfd3747..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_disk_config.py +++ /dev/null @@ -1,252 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime - -from nova.api.openstack import v2 -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -import nova.db.api -from nova import flags -import nova.rpc -from nova import test -from nova.tests.api.openstack import fakes -from nova import utils - - -MANUAL_INSTANCE_UUID = fakes.FAKE_UUID -AUTO_INSTANCE_UUID = fakes.FAKE_UUID.replace('a', 'b') - -stub_instance = fakes.stub_instance -FLAGS = flags.FLAGS - - -def instance_addresses(context, instance_id): - return None - - -class DiskConfigTestCase(test.TestCase): - - def setUp(self): - super(DiskConfigTestCase, self).setUp() - self.flags(verbose=True) - 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(nova.db, 'instance_get', fake_instance_get) - - def fake_instance_get_by_uuid(context, uuid): - for instance in FAKE_INSTANCES: - if uuid == instance['uuid']: - return instance - - self.stubs.Set(nova.db, 'instance_get_by_uuid', - fake_instance_get_by_uuid) - - def fake_instance_get_all(context, *args, **kwargs): - return FAKE_INSTANCES - - self.stubs.Set(nova.db, 'instance_get_all', fake_instance_get_all) - self.stubs.Set(nova.db, 'instance_get_all_by_filters', - fake_instance_get_all) - - def fake_instance_create(context, inst_, session=None): - class FakeModel(dict): - def save(self, session=None): - pass - - inst = FakeModel(**inst_) - inst['id'] = 1 - inst['uuid'] = AUTO_INSTANCE_UUID - inst['created_at'] = datetime.datetime(2010, 10, 10, 12, 0, 0) - inst['updated_at'] = datetime.datetime(2010, 10, 10, 12, 0, 0) - inst['progress'] = 0 - inst['name'] = 'instance-1' # this is a property - - def fake_instance_get_for_create(context, id_, *args, **kwargs): - return inst - - self.stubs.Set(nova.db, 'instance_get', - fake_instance_get_for_create) - self.stubs.Set(nova.db, 'instance_update', - fake_instance_get_for_create) - - def fake_instance_get_all_for_create(context, *args, **kwargs): - return [inst] - self.stubs.Set(nova.db, 'instance_get_all', - fake_instance_get_all_for_create) - self.stubs.Set(nova.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(nova.db, - 'instance_add_security_group', - fake_instance_add_security_group) - - return inst - - self.stubs.Set(nova.db, 'instance_create', fake_instance_create) - - app = v2.APIRouter() - app = extensions.ExtensionMiddleware(app) - app = wsgi.LazySerializationMiddleware(app) - self.app = app - - def assertDiskConfig(self, dict_, value): - self.assert_('RAX-DCF:diskConfig' in dict_) - self.assertEqual(dict_['RAX-DCF:diskConfig'], value) - - def test_show_server(self): - req = fakes.HTTPRequest.blank( - '/fake/servers/%s' % MANUAL_INSTANCE_UUID) - res = req.get_response(self.app) - server_dict = utils.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 = utils.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 = utils.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 = utils.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 = utils.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 = utils.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', - 'RAX-DCF:diskConfig': 'AUTO' - }} - - req.body = utils.dumps(body) - res = req.get_response(self.app) - server_dict = utils.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', - 'RAX-DCF:diskConfig': 'MANUAL' - }} - - req.body = utils.dumps(body) - res = req.get_response(self.app) - server_dict = utils.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 = utils.dumps(body) - res = req.get_response(self.app) - server_dict = utils.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 = utils.dumps(body) - res = req.get_response(self.app) - server_dict = utils.loads(res.body)['server'] - self.assertDiskConfig(server_dict, 'AUTO') - - 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': {'RAX-DCF:diskConfig': 'server_test'}} - req.body = utils.dumps(body) - res = req.get_response(self.app) - self.assertEqual(res.status_int, 400) - expected_msg = '{"badRequest": {"message": "RAX-DCF:diskConfig must'\ - ' be either \'MANUAL\' or \'AUTO\'.", "code": 400}}' - self.assertEqual(res.body, expected_msg) diff --git a/nova/tests/api/openstack/v2/contrib/test_extendedstatus.py b/nova/tests/api/openstack/v2/contrib/test_extendedstatus.py deleted file mode 100644 index dc7f0cefa..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_extendedstatus.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json - -import webob - -from nova import compute -from nova import exception -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes - - -FLAGS = flags.FLAGS -FLAGS.verbose = True - - -def fake_compute_get(*args, **kwargs): - return fakes.stub_instance(1, task_state="kayaking", - vm_state="slightly crunchy", - power_state="empowered") - - -class ExtendedStatusTest(test.TestCase): - - def setUp(self): - super(ExtendedStatusTest, self).setUp() - self.uuid = '70f6db34-de8d-4fbd-aafb-4065bdfa6114' - self.url = '/v2/fake/servers/%s' % self.uuid - fakes.stub_out_nw_api(self.stubs) - self.flags(allow_admin_api=True) - self.stubs.Set(compute.api.API, 'routing_get', fake_compute_get) - - def _make_request(self): - req = webob.Request.blank(self.url) - req.headers['Accept'] = 'application/json' - res = req.get_response(fakes.wsgi_app()) - return res - - def assertServerStates(self, server, vm_state, power_state, task_state): - self.assertEqual(server.get('OS-EXT-STS:vm_state'), vm_state) - self.assertEqual(server.get('OS-EXT-STS:power_state'), power_state) - self.assertEqual(server.get('OS-EXT-STS:task_state'), task_state) - - def test_extended_status(self): - res = self._make_request() - body = json.loads(res.body) - - self.assertEqual(res.status_int, 200) - self.assertServerStates(body['server'], - vm_state='slightly crunchy', - power_state='empowered', - task_state='kayaking') - - def test_extended_status_no_instance_fails(self): - - def fake_compute_get(*args, **kwargs): - raise exception.InstanceNotFound() - - self.stubs.Set(compute.api.API, 'routing_get', fake_compute_get) - res = self._make_request() - - self.assertEqual(res.status_int, 404) diff --git a/nova/tests/api/openstack/v2/contrib/test_flavors_extra_specs.py b/nova/tests/api/openstack/v2/contrib/test_flavors_extra_specs.py deleted file mode 100644 index add8c627d..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_flavors_extra_specs.py +++ /dev/null @@ -1,184 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# 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 webob - -from nova.api.openstack import wsgi -from nova.api.openstack.v2.contrib import flavorextraspecs -from nova import test -from nova.tests.api.openstack import fakes -import nova.wsgi - - -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_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 FlavorsExtraSpecsTest(test.TestCase): - - def setUp(self): - super(FlavorsExtraSpecsTest, self).setUp() - fakes.stub_out_key_pair_funcs(self.stubs) - self.controller = flavorextraspecs.FlavorExtraSpecsController() - - def test_index(self): - self.stubs.Set(nova.db, 'instance_type_extra_specs_get', - return_flavor_extra_specs) - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs') - 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, 'instance_type_extra_specs_get', - return_empty_flavor_extra_specs) - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs') - res_dict = self.controller.index(req, 1) - - self.assertEqual(0, len(res_dict['extra_specs'])) - - def test_show(self): - self.stubs.Set(nova.db, 'instance_type_extra_specs_get', - return_flavor_extra_specs) - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + - '/key5') - 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, 'instance_type_extra_specs_get', - return_empty_flavor_extra_specs) - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + - '/key6') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, - req, 1, 'key6') - - def test_delete(self): - self.stubs.Set(nova.db, 'instance_type_extra_specs_delete', - delete_flavor_extra_specs) - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + - '/key5') - self.controller.delete(req, 1, 'key5') - - def test_create(self): - self.stubs.Set(nova.db, - 'instance_type_extra_specs_update_or_create', - return_create_flavor_extra_specs) - body = {"extra_specs": {"key1": "value1"}} - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs') - res_dict = self.controller.create(req, 1, body) - - self.assertEqual('value1', res_dict['extra_specs']['key1']) - - def test_create_empty_body(self): - self.stubs.Set(nova.db, - 'instance_type_extra_specs_update_or_create', - return_create_flavor_extra_specs) - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, - req, 1, '') - - def test_update_item(self): - self.stubs.Set(nova.db, - 'instance_type_extra_specs_update_or_create', - return_create_flavor_extra_specs) - body = {"key1": "value1"} - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + - '/key1') - res_dict = self.controller.update(req, 1, 'key1', body) - - self.assertEqual('value1', res_dict['key1']) - - def test_update_item_empty_body(self): - self.stubs.Set(nova.db, - 'instance_type_extra_specs_update_or_create', - return_create_flavor_extra_specs) - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + - '/key1') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'key1', '') - - def test_update_item_too_many_keys(self): - self.stubs.Set(nova.db, - 'instance_type_extra_specs_update_or_create', - return_create_flavor_extra_specs) - body = {"key1": "value1", "key2": "value2"} - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs' + - '/key1') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'key1', body) - - def test_update_item_body_uri_mismatch(self): - self.stubs.Set(nova.db, - 'instance_type_extra_specs_update_or_create', - return_create_flavor_extra_specs) - body = {"key1": "value1"} - - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs/bad') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'bad', body) - - -class FlavorsExtraSpecsXMLSerializerTest(test.TestCase): - def test_serializer(self): - serializer = flavorextraspecs.ExtraSpecsTemplate() - expected = ("\n" - 'value1') - text = serializer.serialize(dict(extra_specs={"key1": "value1"})) - print text - self.assertEqual(text, expected) - - def test_deserializer(self): - deserializer = wsgi.XMLDeserializer() - expected = dict(extra_specs={"key1": "value1"}) - intext = ("\n" - 'value1') - result = deserializer.deserialize(intext)['body'] - self.assertEqual(result, expected) diff --git a/nova/tests/api/openstack/v2/contrib/test_floating_ip_dns.py b/nova/tests/api/openstack/v2/contrib/test_floating_ip_dns.py deleted file mode 100644 index 58fdb4788..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_floating_ip_dns.py +++ /dev/null @@ -1,260 +0,0 @@ -# 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 sys - -from lxml import etree -import webob -import urllib - -from nova.api.openstack.v2.contrib import floating_ips -from nova.api.openstack.v2.contrib import floating_ip_dns -from nova import context -from nova import db -from nova import network -from nova import rpc -from nova import test -from nova.tests.api.openstack import fakes -from nova import utils - - -name = "arbitraryname" -name2 = "anotherarbitraryname" - -testaddress = '10.0.0.66' -testaddress2 = '10.0.0.67' - -zone = "example.org" -zone2 = "example.net" -floating_ip_id = '1' - - -def _quote_zone(zone): - """ - Zone 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(zone.replace('.', '%2E')) - - -def network_api_get_floating_ip(self, context, id): - return {'id': floating_ip_id, 'address': testaddress, - 'fixed_ip': None} - - -def network_get_dns_zones(self, context): - return ['foo', 'bar', 'baz', 'quux'] - - -def network_get_dns_entries_by_address(self, context, address, zone): - return [name, name2] - - -def network_get_dns_entries_by_name(self, context, address, zone): - return [testaddress, testaddress2] - - -def network_add_dns_entry(self, context, address, name, dns_type, zone): - return {'dns_entry': {'ip': testaddress, - 'name': name, - 'type': dns_type, - 'zone': zone}} - - -def network_modify_dns_entry(self, context, address, name, zone): - return {'dns_entry': {'name': name, - 'ip': address, - 'zone': zone}} - - -class FloatingIpDNSTest(test.TestCase): - def _create_floating_ip(self): - """Create a floating ip object.""" - host = "fake_host" - return db.floating_ip_create(self.context, - {'address': testaddress, - 'host': host}) - - def _delete_floating_ip(self): - db.floating_ip_destroy(self.context, testaddress) - - def setUp(self): - super(FloatingIpDNSTest, self).setUp() - self.stubs.Set(network.api.API, "get_dns_zones", - network_get_dns_zones) - 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.context = context.get_admin_context() - - self._create_floating_ip() - self.dns_controller = floating_ip_dns.FloatingIPDNSController() - - def tearDown(self): - self._delete_floating_ip() - super(FloatingIpDNSTest, self).tearDown() - - def test_dns_zones_list(self): - req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns') - res_dict = self.dns_controller.index(req) - entries = res_dict['zones'] - self.assertTrue(entries) - self.assertEqual(entries[0]['zone'], "foo") - self.assertEqual(entries[1]['zone'], "bar") - self.assertEqual(entries[2]['zone'], "baz") - self.assertEqual(entries[3]['zone'], "quux") - - def test_get_dns_entries_by_address(self): - qparams = {'ip': testaddress} - params = "?%s" % urllib.urlencode(qparams) if qparams else "" - - req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s%s' % - (_quote_zone(zone), params)) - entries = self.dns_controller.show(req, _quote_zone(zone)) - - 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]['zone'], - zone) - - def test_get_dns_entries_by_name(self): - qparams = {'name': name} - params = "?%s" % urllib.urlencode(qparams) if qparams else "" - - req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s%s' % - (_quote_zone(zone), params)) - entries = self.dns_controller.show(req, _quote_zone(zone)) - - self.assertEqual(len(entries['dns_entries']), 2) - self.assertEqual(entries['dns_entries'][0]['ip'], - testaddress) - self.assertEqual(entries['dns_entries'][1]['ip'], - testaddress2) - self.assertEqual(entries['dns_entries'][0]['zone'], - zone) - - def test_create(self): - body = {'dns_entry': - {'name': name, - 'ip': testaddress, - 'dns_type': 'A', - 'zone': zone}} - req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns') - entry = self.dns_controller.create(req, body) - - self.assertEqual(entry['dns_entry']['ip'], testaddress) - - def test_delete(self): - self.called = False - self.deleted_zone = "" - self.deleted_name = "" - - def network_delete_dns_entry(fakeself, context, req, id): - self.called = True - self.deleted_zone = id - - self.stubs.Set(network.api.API, "delete_dns_entry", - network_delete_dns_entry) - - qparams = {'name': name} - params = "?%s" % urllib.urlencode(qparams) if qparams else "" - - req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s%s' % - (_quote_zone(zone), params)) - entries = self.dns_controller.delete(req, _quote_zone(zone)) - - self.assertTrue(self.called) - self.assertEquals(self.deleted_zone, zone) - - def test_modify(self): - body = {'dns_entry': - {'name': name, - 'ip': testaddress2}} - req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' % - zone) - entry = self.dns_controller.update(req, zone, body) - - self.assertEqual(entry['dns_entry']['ip'], testaddress2) - - -class FloatingIpDNSSerializerTest(test.TestCase): - def test_default_serializer(self): - serializer = floating_ip_dns.FloatingIPDNSTemplate() - text = serializer.serialize(dict( - dns_entry=dict( - ip=testaddress, - type='A', - zone=zone, - name=name))) - - tree = etree.fromstring(text) - - self.assertEqual('dns_entry', tree.tag) - self.assertEqual(testaddress, tree.get('ip')) - self.assertEqual(zone, tree.get('zone')) - self.assertEqual(name, tree.get('name')) - - def test_index_serializer(self): - serializer = floating_ip_dns.ZonesTemplate() - text = serializer.serialize(dict( - zones=[ - dict(zone=zone), - dict(zone=zone2)])) - - tree = etree.fromstring(text) - self.assertEqual('zones', tree.tag) - self.assertEqual(2, len(tree)) - self.assertEqual(zone, tree[0].get('zone')) - self.assertEqual(zone2, tree[1].get('zone')) - - def test_show_serializer(self): - serializer = floating_ip_dns.FloatingIPDNSsTemplate() - text = serializer.serialize(dict( - dns_entries=[ - dict(ip=testaddress, - type='A', - zone=zone, - name=name), - dict(ip=testaddress2, - type='C', - zone=zone, - 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(testaddress, tree[0].get('ip')) - self.assertEqual('A', tree[0].get('type')) - self.assertEqual(zone, tree[0].get('zone')) - self.assertEqual(name, tree[0].get('name')) - self.assertEqual(testaddress2, tree[1].get('ip')) - self.assertEqual('C', tree[1].get('type')) - self.assertEqual(zone, tree[1].get('zone')) - self.assertEqual(name2, tree[1].get('name')) diff --git a/nova/tests/api/openstack/v2/contrib/test_floating_ip_pools.py b/nova/tests/api/openstack/v2/contrib/test_floating_ip_pools.py deleted file mode 100644 index d061f9af3..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_floating_ip_pools.py +++ /dev/null @@ -1,73 +0,0 @@ -# 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.v2.contrib import floating_ip_pools -from nova import context -from nova import network -from nova import test -from nova.tests.api.openstack import fakes - - -def fake_get_floating_ip_pools(self, context): - return [{'name': 'nova'}, - {'name': 'other'}] - - -class FloatingIpPoolTest(test.TestCase): - def setUp(self): - super(FloatingIpPoolTest, 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 = floating_ip_pools.FloatingIPPoolsController() - - def test_translate_floating_ip_pools_view(self): - pools = fake_get_floating_ip_pools(None, self.context) - view = floating_ip_pools._translate_floating_ip_pools_view(pools) - self.assertTrue('floating_ip_pools' in view) - self.assertEqual(view['floating_ip_pools'][0]['name'], - pools[0]['name']) - self.assertEqual(view['floating_ip_pools'][1]['name'], - pools[1]['name']) - - def test_floating_ips_pools_list(self): - req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ip-pools') - res_dict = self.controller.index(req) - - pools = fake_get_floating_ip_pools(None, self.context) - response = {'floating_ip_pools': pools} - self.assertEqual(res_dict, response) - - -class FloatingIpPoolSerializerTest(test.TestCase): - def test_index_serializer(self): - serializer = floating_ip_pools.FloatingIPPoolsSerializer() - text = serializer.serialize(dict( - floating_ip_pools=[ - dict(name='nova'), - dict(name='other') - ]), 'index') - - 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/api/openstack/v2/contrib/test_floating_ips.py b/nova/tests/api/openstack/v2/contrib/test_floating_ips.py deleted file mode 100644 index dae58aa14..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_floating_ips.py +++ /dev/null @@ -1,329 +0,0 @@ -# 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. - -from lxml import etree -import webob - -from nova.api.openstack.v2.contrib import floating_ips -from nova import context -from nova import db -from nova import network -from nova import rpc -from nova import test -from nova.tests.api.openstack import fakes -from nova import utils - -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': None} - - -def network_api_get_floating_ip_by_address(self, context, address): - return {'id': 1, 'address': '10.10.10.10', - 'pool': 'nova', - 'fixed_ip': {'address': '10.0.0.1', - 'instance': {'uuid': FAKE_UUID}}} - - -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}}}, - {'id': 2, - 'pool': 'nova', 'interface': 'eth0', - 'address': '10.10.10.11'}] - - -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, floating_address): - pass - - -def network_get_instance_nw_info(self, context, instance): - info = { - 'label': 'fake', - 'gateway': 'fake', - 'dhcp_server': 'fake', - 'broadcast': 'fake', - 'mac': 'fake', - 'vif_uuid': 'fake', - 'rxtx_cap': 'fake', - 'dns': [], - 'ips': [{'ip': '10.0.0.1'}], - 'should_create_bridge': False, - 'should_create_vlan': False} - - return [['ignore', info]] - - -def fake_instance_get(context, instance_id): - return { - "id": 1, - "uuid": utils.gen_uuid(), - "name": 'fake', - "user_id": 'fakeuser', - "project_id": '123'} - - -class StubExtensionManager(object): - def register(self, *args): - pass - - -class FloatingIpTest(test.TestCase): - floating_ip = "10.10.10.10" - - def _create_floating_ip(self): - """Create a floating ip object.""" - host = "fake_host" - return db.floating_ip_create(self.context, - {'address': self.floating_ip, - 'pool': 'nova', - 'host': host}) - - def _delete_floating_ip(self): - db.floating_ip_destroy(self.context, self.floating_ip) - - def setUp(self): - super(FloatingIpTest, self).setUp() - 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_nw_info", - network_get_instance_nw_info) - self.stubs.Set(db, 'instance_get', - fake_instance_get) - - self.context = context.get_admin_context() - self._create_floating_ip() - - self.controller = floating_ips.FloatingIPController() - self.manager = floating_ips.Floating_ips(StubExtensionManager()) - - def tearDown(self): - self._delete_floating_ip() - super(FloatingIpTest, self).tearDown() - - def test_translate_floating_ip_view(self): - floating_ip_address = self._create_floating_ip() - floating_ip = db.floating_ip_get_by_address(self.context, - floating_ip_address) - view = floating_ips._translate_floating_ip_view(floating_ip) - self.assertTrue('floating_ip' in view) - self.assertTrue(view['floating_ip']['id']) - self.assertEqual(view['floating_ip']['ip'], self.floating_ip) - self.assertEqual(view['floating_ip']['fixed_ip'], None) - self.assertEqual(view['floating_ip']['instance_id'], None) - - 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.assertTrue('floating_ip' in view) - - def test_floating_ips_list(self): - req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips') - 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_show(self): - req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/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']['instance_id'], None) - - 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}}} - self.stubs.Set(network.api.API, "get_floating_ip", get_floating_ip) - - req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/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']['instance_id'], FAKE_UUID) - -# test floating ip allocate/release(deallocate) - def test_floating_ip_allocate_no_free_ips(self): - def fake_call(*args, **kwargs): - raise(rpc.RemoteError('NoMoreFloatingIps', '', '')) - - self.stubs.Set(rpc, "call", fake_call) - - req = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, - req) - - 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 = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips') - 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 = fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/1') - self.controller.delete(req, 1) - -# test floating ip add/remove -> associate/disassociate - - def test_floating_ip_associate(self): - body = dict(addFloatingIp=dict(address=self.floating_ip)) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') - self.manager._add_floating_ip(body, req, 'test_inst') - - def test_floating_ip_disassociate(self): - body = dict(removeFloatingIp=dict(address='10.10.10.10')) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') - self.manager._remove_floating_ip(body, req, 'test_inst') - -# 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 = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') - self.assertRaises(webob.exc.HTTPBadRequest, - self.manager._add_floating_ip, body, req, - 'test_inst') - - def test_missing_dict_param_in_remove_floating_ip(self): - body = dict(removeFloatingIp='11.0.0.1') - - req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') - self.assertRaises(webob.exc.HTTPBadRequest, - self.manager._remove_floating_ip, body, req, - 'test_inst') - - def test_missing_dict_param_in_add_floating_ip(self): - body = dict(addFloatingIp='11.0.0.1') - - req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') - self.assertRaises(webob.exc.HTTPBadRequest, - self.manager._add_floating_ip, body, req, - 'test_inst') - - -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/api/openstack/v2/contrib/test_hosts.py b/nova/tests/api/openstack/v2/contrib/test_hosts.py deleted file mode 100644 index a954890ba..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_hosts.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright (c) 2011 Openstack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from lxml import etree -import webob.exc - -from nova import context -from nova import exception -from nova import flags -from nova import log as logging -from nova import test -from nova.api.openstack.v2.contrib import hosts as os_hosts -from nova.scheduler import api as scheduler_api - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.tests.hosts') -# Simulate the hosts returned by the zone manager. -HOST_LIST = [ - {"host_name": "host_c1", "service": "compute"}, - {"host_name": "host_c2", "service": "compute"}, - {"host_name": "host_v1", "service": "volume"}, - {"host_name": "host_v2", "service": "volume"}] - - -def stub_get_host_list(req): - return HOST_LIST - - -def stub_set_host_enabled(context, host, enabled): - # We'll simulate success and failure by assuming - # that 'host_c1' always succeeds, and 'host_c2' - # always fails - fail = (host == "host_c2") - status = "enabled" if (enabled ^ fail) else "disabled" - return status - - -def stub_host_power_action(context, host, action): - return action - - -class FakeRequest(object): - environ = {"nova.context": context.get_admin_context()} - - -class HostTestCase(test.TestCase): - """Test Case for hosts.""" - - def setUp(self): - super(HostTestCase, self).setUp() - self.controller = os_hosts.HostController() - self.req = FakeRequest() - self.stubs.Set(scheduler_api, 'get_host_list', stub_get_host_list) - self.stubs.Set(self.controller.compute_api, 'set_host_enabled', - stub_set_host_enabled) - self.stubs.Set(self.controller.compute_api, 'host_power_action', - stub_host_power_action) - - def test_list_hosts(self): - """Verify that the compute hosts are returned.""" - hosts = os_hosts._list_hosts(self.req) - self.assertEqual(hosts, HOST_LIST) - - compute_hosts = os_hosts._list_hosts(self.req, "compute") - expected = [host for host in HOST_LIST - if host["service"] == "compute"] - self.assertEqual(compute_hosts, expected) - - def test_disable_host(self): - dis_body = {"status": "disable"} - result_c1 = self.controller.update(self.req, "host_c1", body=dis_body) - self.assertEqual(result_c1["status"], "disabled") - result_c2 = self.controller.update(self.req, "host_c2", body=dis_body) - self.assertEqual(result_c2["status"], "enabled") - - def test_enable_host(self): - en_body = {"status": "enable"} - result_c1 = self.controller.update(self.req, "host_c1", body=en_body) - self.assertEqual(result_c1["status"], "enabled") - result_c2 = self.controller.update(self.req, "host_c2", body=en_body) - self.assertEqual(result_c2["status"], "disabled") - - def test_host_startup(self): - self.flags(allow_admin_api=True) - result = self.controller.startup(self.req, "host_c1") - self.assertEqual(result["power_action"], "startup") - - def test_host_shutdown(self): - self.flags(allow_admin_api=True) - result = self.controller.shutdown(self.req, "host_c1") - self.assertEqual(result["power_action"], "shutdown") - - def test_host_reboot(self): - self.flags(allow_admin_api=True) - result = self.controller.reboot(self.req, "host_c1") - self.assertEqual(result["power_action"], "reboot") - - def test_bad_status_value(self): - bad_body = {"status": "bad"} - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - self.req, "host_c1", body=bad_body) - - def test_bad_update_key(self): - bad_body = {"crazy": "bad"} - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - self.req, "host_c1", body=bad_body) - - def test_bad_host(self): - self.assertRaises(exception.HostNotFound, self.controller.update, - self.req, "bogus_host_name", body={"status": "disable"}) - - -class HostSerializerTest(test.TestCase): - def setUp(self): - super(HostSerializerTest, self).setUp() - self.deserializer = os_hosts.HostDeserializer() - - def test_index_serializer(self): - serializer = os_hosts.HostIndexTemplate() - text = serializer.serialize(HOST_LIST) - - tree = etree.fromstring(text) - - self.assertEqual('hosts', tree.tag) - self.assertEqual(len(HOST_LIST), len(tree)) - for i in range(len(HOST_LIST)): - self.assertEqual('host', tree[i].tag) - self.assertEqual(HOST_LIST[i]['host_name'], - tree[i].get('host_name')) - self.assertEqual(HOST_LIST[i]['service'], - tree[i].get('service')) - - def test_update_serializer(self): - exemplar = dict(host='host_c1', status='enabled') - serializer = os_hosts.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.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', foo='bar') - intext = ("\n" - 'enabledbar') - result = self.deserializer.deserialize(intext) - - self.assertEqual(dict(body=exemplar), result) diff --git a/nova/tests/api/openstack/v2/contrib/test_keypairs.py b/nova/tests/api/openstack/v2/contrib/test_keypairs.py deleted file mode 100644 index 6ecd3a86c..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_keypairs.py +++ /dev/null @@ -1,175 +0,0 @@ -# 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 json - -import webob -from lxml import etree - -from nova.api.openstack import wsgi -from nova.api.openstack.v2.contrib import keypairs -from nova import context -from nova import db -from nova import test -from nova.tests.api.openstack import fakes - - -def fake_keypair(name): - return {'public_key': 'FAKE_KEY', - 'fingerprint': 'FAKE_FINGERPRINT', - 'name': name} - - -def db_key_pair_get_all_by_user(self, user_id): - return [fake_keypair('FAKE')] - - -def db_key_pair_create(self, keypair): - pass - - -def db_key_pair_destroy(context, user_id, name): - if not (user_id and name): - raise Exception() - - -class KeypairsTest(test.TestCase): - - def setUp(self): - super(KeypairsTest, self).setUp() - self.controller = keypairs.KeypairController() - 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.context = context.get_admin_context() - - def test_keypair_list(self): - req = webob.Request.blank('/v2/fake/os-keypairs') - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - res_dict = json.loads(res.body) - response = {'keypairs': [{'keypair': fake_keypair('FAKE')}]} - self.assertEqual(res_dict, response) - - def test_keypair_create(self): - body = {'keypair': {'name': 'create_test'}} - req = webob.Request.blank('/v2/fake/os-keypairs') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['Content-Type'] = 'application/json' - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - res_dict = json.loads(res.body) - self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) - self.assertTrue(len(res_dict['keypair']['private_key']) > 0) - - 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('/v2/fake/os-keypairs') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['Content-Type'] = 'application/json' - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - # FIXME(ja): sholud we check that public_key was sent to create? - res_dict = json.loads(res.body) - self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) - self.assertFalse('private_key' in res_dict['keypair']) - - def test_keypair_delete(self): - req = webob.Request.blank('/v2/fake/os-keypairs/FAKE') - req.method = 'DELETE' - req.headers['Content-Type'] = 'application/json' - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 202) - - -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.KeypairTemplate() - text = serializer.serialize(exemplar) - - print text - tree = etree.fromstring(text) - - self.assertEqual('keypair', tree.tag) - for child in tree: - self.assertTrue(child.tag in 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.KeypairsTemplate() - text = serializer.serialize(exemplar) - - print text - 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.assertTrue(child.tag in 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 = ("\n" - 'key_name' - 'public_key') - - result = self.deserializer.deserialize(intext)['body'] - self.assertEqual(result, exemplar) diff --git a/nova/tests/api/openstack/v2/contrib/test_multinic_xs.py b/nova/tests/api/openstack/v2/contrib/test_multinic_xs.py deleted file mode 100644 index 0d9b6e3ce..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_multinic_xs.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json - -import webob - -from nova import compute -from nova import context -from nova import test -from nova.tests.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): - return {'id': 1, 'uuid': instance_id} - - -class FixedIpTest(test.TestCase): - def setUp(self): - super(FixedIpTest, 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.context = context.get_admin_context() - - 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('/v2/fake/servers/%s/action' % UUID) - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 202) - self.assertEqual(last_add_fixed_ip, (UUID, 'test_net')) - - 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('/v2/fake/servers/%s/action' % UUID) - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 422) - self.assertEqual(last_add_fixed_ip, (None, None)) - - 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('/v2/fake/servers/%s/action' % UUID) - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_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('/v2/fake/servers/%s/action' % UUID) - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 422) - self.assertEqual(last_remove_fixed_ip, (None, None)) diff --git a/nova/tests/api/openstack/v2/contrib/test_networks.py b/nova/tests/api/openstack/v2/contrib/test_networks.py deleted file mode 100644 index 04bd82e2c..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_networks.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright 2011 Grid Dynamics -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import copy - -import webob - -from nova.api.openstack.v2.contrib import networks -from nova import context -from nova import exception -from nova import test -from nova.tests.api.openstack import fakes - - -FAKE_NETWORKS = [ - { - 'bridge': 'br100', 'vpn_public_port': 1000, - 'dhcp_start': '10.0.0.3', 'bridge_interface': 'eth0', - 'updated_at': '2011-08-16 09:26:13.048257', 'id': 1, - 'cidr_v6': None, 'deleted_at': None, - 'gateway': '10.0.0.1', 'label': 'mynet_0', - 'project_id': '1234', - '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, 'host': 'nsokolov-desktop', - 'gateway_v6': None, 'netmask_v6': None, - 'created_at': '2011-08-15 06:19:19.387525', - }, - { - 'bridge': 'br101', 'vpn_public_port': 1001, - 'dhcp_start': '10.0.0.11', 'bridge_interface': 'eth0', - 'updated_at': None, 'id': 2, 'cidr_v6': None, - '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', - 'netmask': '255.255.255.248', 'injected': False, - 'cidr': '10.0.0.10/29', 'vpn_public_address': None, - 'multi_host': False, 'dns1': None, 'host': None, - 'gateway_v6': None, 'netmask_v6': None, - 'created_at': '2011-08-15 06:19:19.885495', - }, -] - - -class FakeNetworkAPI(object): - - def __init__(self): - self.networks = copy.deepcopy(FAKE_NETWORKS) - - def delete(self, context, network_id): - for i, network in enumerate(self.networks): - if network['id'] == network_id: - del self.networks[0] - return True - raise exception.NetworkNotFound() - - #NOTE(bcwaldon): this does nothing other than check for existance - def disassociate(self, context, network_id): - for i, network in enumerate(self.networks): - if network['id'] == network_id: - return True - raise exception.NetworkNotFound() - - def get_all(self, context): - return self.networks - - def get(self, context, network_id): - for network in self.networks: - if network['id'] == network_id: - return network - raise exception.NetworkNotFound() - - -class NetworksTest(test.TestCase): - - def setUp(self): - super(NetworksTest, self).setUp() - self.flags(allow_admin_api=True) - self.fake_network_api = FakeNetworkAPI() - self.controller = networks.NetworkController(self.fake_network_api) - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - self.context = context.RequestContext('user', '1234', is_admin=True) - - def test_network_list_all(self): - req = fakes.HTTPRequest.blank('/v2/1234/os-networks') - res_dict = self.controller.index(req) - self.assertEquals(res_dict, {'networks': FAKE_NETWORKS}) - - def test_network_disassociate(self): - req = fakes.HTTPRequest.blank('/v2/1234/os-networks/1/action') - res = self.controller.action(req, 1, {'disassociate': None}) - self.assertEqual(res.status_int, 202) - - def test_network_disassociate_not_found(self): - req = fakes.HTTPRequest.blank('/v2/1234/os-networks/100/action') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.action, - req, 100, {'disassociate': None}) - - def test_network_get(self): - req = fakes.HTTPRequest.blank('/v2/1234/os-networks/1') - res_dict = self.controller.show(req, 1) - expected = {'network': FAKE_NETWORKS[0]} - self.assertEqual(res_dict, expected) - - def test_network_get_not_found(self): - req = fakes.HTTPRequest.blank('/v2/1234/os-networks/100') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.show, req, 100) - - def test_network_delete(self): - req = fakes.HTTPRequest.blank('/v2/1234/os-networks/1') - res = self.controller.delete(req, 1) - self.assertEqual(res.status_int, 202) - - def test_network_delete_not_found(self): - req = fakes.HTTPRequest.blank('/v2/1234/os-networks/100') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.delete, req, 100) diff --git a/nova/tests/api/openstack/v2/contrib/test_quotas.py b/nova/tests/api/openstack/v2/contrib/test_quotas.py deleted file mode 100644 index 9bcfc7ded..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_quotas.py +++ /dev/null @@ -1,193 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob -from lxml import etree - -from nova.api.openstack import wsgi -from nova.api.openstack.v2.contrib import quotas -from nova import context -from nova import test -from nova.tests.api.openstack import fakes - - -def quota_set(id): - return {'quota_set': {'id': id, 'metadata_items': 128, 'volumes': 10, - 'gigabytes': 1000, 'ram': 51200, 'floating_ips': 10, - 'instances': 10, 'injected_files': 5, 'cores': 20, - 'injected_file_content_bytes': 10240}} - - -def quota_set_list(): - return {'quota_set_list': [quota_set('1234'), quota_set('5678'), - quota_set('update_me')]} - - -class QuotaSetsTest(test.TestCase): - - def setUp(self): - super(QuotaSetsTest, self).setUp() - self.controller = quotas.QuotaSetsController() - self.user_id = 'fake' - self.project_id = 'fake' - self.user_context = context.RequestContext(self.user_id, - self.project_id) - self.admin_context = context.RequestContext(self.user_id, - self.project_id, - is_admin=True) - - def test_format_quota_set(self): - raw_quota_set = { - 'instances': 10, - 'cores': 20, - 'ram': 51200, - 'volumes': 10, - 'floating_ips': 10, - 'metadata_items': 128, - 'gigabytes': 1000, - 'injected_files': 5, - 'injected_file_content_bytes': 10240} - - quota_set = quotas.QuotaSetsController()._format_quota_set('1234', - raw_quota_set) - 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['volumes'], 10) - self.assertEqual(qs['gigabytes'], 1000) - self.assertEqual(qs['floating_ips'], 10) - self.assertEqual(qs['metadata_items'], 128) - self.assertEqual(qs['injected_files'], 5) - self.assertEqual(qs['injected_file_content_bytes'], 10240) - - 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') - - expected = {'quota_set': { - 'id': 'fake_tenant', - 'instances': 10, - 'cores': 20, - 'ram': 51200, - 'volumes': 10, - 'gigabytes': 1000, - 'floating_ips': 10, - 'metadata_items': 128, - 'injected_files': 5, - 'injected_file_content_bytes': 10240}} - - self.assertEqual(res_dict, expected) - - def test_quotas_show_as_admin(self): - req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234', - use_admin_context=True) - res_dict = self.controller.show(req, 1234) - - self.assertEqual(res_dict, quota_set('1234')) - - def test_quotas_show_as_unauthorized_user(self): - 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): - body = {'quota_set': {'instances': 50, 'cores': 50, - 'ram': 51200, 'volumes': 10, - 'gigabytes': 1000, 'floating_ips': 10, - 'metadata_items': 128, 'injected_files': 5, - 'injected_file_content_bytes': 10240}} - - 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, body) - - def test_quotas_update_as_user(self): - body = {'quota_set': {'instances': 50, 'cores': 50, - 'ram': 51200, 'volumes': 10, - 'gigabytes': 1000, 'floating_ips': 10, - 'metadata_items': 128, 'injected_files': 5, - 'injected_file_content_bytes': 10240}} - - req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me') - self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, - req, 'update_me', body) - - -class QuotaXMLSerializerTest(test.TestCase): - def setUp(self): - super(QuotaXMLSerializerTest, self).setUp() - self.serializer = quotas.QuotaTemplate() - self.deserializer = wsgi.XMLDeserializer() - - def test_serializer(self): - exemplar = dict(quota_set=dict( - id='project_id', - metadata_items=10, - injected_file_content_bytes=20, - volumes=30, - gigabytes=40, - ram=50, - floating_ips=60, - instances=70, - injected_files=80, - cores=90)) - text = self.serializer.serialize(exemplar) - - print text - 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.assertTrue(child.tag in 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', - volumes='30', - gigabytes='40', - ram='50', - floating_ips='60', - instances='70', - injected_files='80', - cores='90')) - intext = ("\n" - '' - '10' - '20' - '' - '30' - '40' - '50' - '60' - '70' - '80' - '90' - '') - - result = self.deserializer.deserialize(intext)['body'] - self.assertEqual(result, exemplar) diff --git a/nova/tests/api/openstack/v2/contrib/test_rescue.py b/nova/tests/api/openstack/v2/contrib/test_rescue.py deleted file mode 100644 index 9a3706173..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_rescue.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json - -import webob - -from nova import compute -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes - -FLAGS = flags.FLAGS - - -def rescue(self, context, instance, rescue_password=None): - pass - - -def unrescue(self, context, instance): - pass - - -class RescueTest(test.TestCase): - def setUp(self): - super(RescueTest, self).setUp() - - def fake_compute_get(*args, **kwargs): - uuid = '70f6db34-de8d-4fbd-aafb-4065bdfa6114' - return {'id': 1, 'uuid': uuid} - - 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) - - def test_rescue_with_preset_password(self): - body = {"rescue": {"adminPass": "AABBCC112233"}} - req = webob.Request.blank('/v2/fake/servers/test_inst/action') - req.method = "POST" - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - resp_json = json.loads(resp.body) - self.assertEqual("AABBCC112233", resp_json['adminPass']) - - def test_rescue_generates_password(self): - body = dict(rescue=None) - req = webob.Request.blank('/v2/fake/servers/test_inst/action') - req.method = "POST" - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - resp_json = json.loads(resp.body) - self.assertEqual(FLAGS.password_length, len(resp_json['adminPass'])) - - def test_unrescue(self): - body = dict(unrescue=None) - req = webob.Request.blank('/v2/fake/servers/test_inst/action') - req.method = "POST" - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 202) diff --git a/nova/tests/api/openstack/v2/contrib/test_security_groups.py b/nova/tests/api/openstack/v2/contrib/test_security_groups.py deleted file mode 100644 index 7848ef9bb..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_security_groups.py +++ /dev/null @@ -1,1013 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC -# -# 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 unittest - -from lxml import etree -import mox -import webob - -from nova.api.openstack.v2.contrib import security_groups -from nova.api.openstack import wsgi -import nova.db -from nova import exception -from nova import utils -from nova import test -from nova.tests.api.openstack import fakes - - -FAKE_UUID = 'a47ae74e-ab08-447f-8eee-ffd43fc46c16' - - -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): - return {'id': int(server_id), - 'power_state': 0x01, - 'host': "localhost", - 'uuid': FAKE_UUID, - 'name': 'asdf'} - - -def return_server_by_uuid(context, server_uuid): - return {'id': 1, - 'power_state': 0x01, - 'host': "localhost", - 'uuid': server_uuid, - 'name': 'asdf'} - - -def return_non_running_server(context, server_id): - return {'id': server_id, 'power_state': 0x02, 'uuid': FAKE_UUID, - '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_UUID}]} - - -def return_security_group_without_instances(context, project_id, group_name): - return {'id': 1, 'name': group_name} - - -def return_server_nonexistent(context, server_id): - raise exception.InstanceNotFound(instance_id=server_id) - - -class StubExtensionManager(object): - def register(self, *args): - pass - - -class TestSecurityGroups(test.TestCase): - def setUp(self): - super(TestSecurityGroups, self).setUp() - - self.controller = security_groups.SecurityGroupController() - self.manager = security_groups.Security_groups(StubExtensionManager()) - - def tearDown(self): - super(TestSecurityGroups, self).tearDown() - - 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.HTTPUnprocessableEntity, - self.controller.create, req, sg) - - 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}) - - 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}) - - 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}) - - 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}) - - 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}) - - 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}) - - def test_create_security_group_with_no_body(self): - req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.create, req, None) - - 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.HTTPUnprocessableEntity, - self.controller.create, req, body) - - 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}) - - 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}) - - 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}) - - 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}) - - 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.assertEquals(res_dict, expected) - - def test_get_security_group_by_id(self): - sg = security_group_template(id=2, rules=[]) - - def return_security_group(context, group_id): - self.assertEquals(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.assertEquals(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/111111111') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, - req, '111111111') - - def test_delete_security_group_by_id(self): - sg = security_group_template(id=1, rules=[]) - - self.called = False - - def security_group_destroy(context, id): - self.called = True - - def return_security_group(context, group_id): - self.assertEquals(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_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/11111111') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, - req, '11111111') - - def test_associate_by_non_existing_security_group_name(self): - body = dict(addSecurityGroup=dict(name='non-existing')) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') - self.assertRaises(webob.exc.HTTPNotFound, - self.manager._addSecurityGroup, body, req, '1') - - 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, body, req, 'invalid') - - 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, body, req, '1') - - 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, body, req, '1') - - 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, body, req, '1') - - 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, body, req, '1') - - 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.assertRaises(webob.exc.HTTPBadRequest, - self.manager._addSecurityGroup, body, req, '1') - - 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, body, req, '1') - - 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(body, req, '1') - - def test_disassociate_by_non_existing_security_group_name(self): - body = dict(removeSecurityGroup=dict(name='non-existing')) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') - self.assertRaises(webob.exc.HTTPNotFound, - self.manager._removeSecurityGroup, body, req, '1') - - 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, body, req, - 'invalid') - - 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, body, req, '1') - - 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, body, req, '1') - - 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, body, req, '1') - - 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, body, req, '1') - - 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.assertRaises(webob.exc.HTTPBadRequest, - self.manager._removeSecurityGroup, body, req, '1') - - 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, body, req, '1') - - 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(body, req, '1') - - -class TestSecurityGroupRules(test.TestCase): - def setUp(self): - super(TestSecurityGroupRules, self).setUp() - - sg1 = security_group_template(id=1) - sg2 = security_group_template(id=2, - name='authorize_revoke', - description='authorize-revoke testing') - db1 = security_group_db(sg1) - db2 = security_group_db(sg2) - - def return_security_group(context, group_id): - if group_id == db1['id']: - return db1 - if group_id == db2['id']: - return db2 - raise exception.NotFound() - - self.stubs.Set(nova.db, 'security_group_get', - return_security_group) - - self.parent_security_group = db2 - - self.controller = security_groups.SecurityGroupRulesController() - - def tearDown(self): - super(TestSecurityGroupRules, self).tearDown() - - def test_create_by_cidr(self): - rule = security_group_rule_template(cidr='10.2.3.124/24') - - 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.assertNotEquals(security_group_rule['id'], 0) - self.assertEquals(security_group_rule['parent_group_id'], 2) - self.assertEquals(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='1') - - 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.assertNotEquals(security_group_rule['id'], 0) - self.assertEquals(security_group_rule['parent_group_id'], 2) - - def test_create_by_invalid_cidr_json(self): - rules = { - "security_group_rule": { - "ip_protocol": "tcp", - "from_port": "22", - "to_port": "22", - "parent_group_id": 2, - "cidr": "10.2.3.124/2433"}} - rule = security_group_rule_template( - ip_protocol="tcp", - from_port=22, - to_port=22, - parent_group_id=2, - 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=2, - 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=2, - 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(self): - rule = security_group_rule_template(cidr='10.0.0.0/24') - - 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.HTTPUnprocessableEntity, - 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.HTTPUnprocessableEntity, - 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='invalid', - parent_group_id='1111111111111') - - 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_invalid_protocol(self): - rule = security_group_rule_template(ip_protocol='invalid-protocol', - cidr='10.2.2.0/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_with_no_protocol(self): - rule = security_group_rule_template(cidr='10.2.2.0/24') - 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') - - 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') - - 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') - - 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') - - 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') - 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') - 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') - - 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() - - 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.assertNotEquals(security_group_rule['id'], 0) - self.assertEquals(security_group_rule['parent_group_id'], - self.parent_security_group['id']) - self.assertEquals(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') - - 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='') - - 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='222222') - - 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_rule_with_same_group_parent_id(self): - rule = security_group_rule_template(group_id=2) - - 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_delete(self): - rule = security_group_rule_template(id=10) - - 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/10') - self.controller.delete(req, '10') - - 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' + - '/22222222222222') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, - req, '22222222222222') - - -class TestSecurityGroupRulesXMLDeserializer(unittest.TestCase): - - def setUp(self): - self.deserializer = security_groups.SecurityGroupRulesXMLDeserializer() - - def test_create_request(self): - serial_request = """ - - 12 - 22 - 22 - - tcp - 10.0.0.0/24 -""" - 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.assertEquals(request['body'], expected) - - def test_create_no_protocol_request(self): - serial_request = """ - - 12 - 22 - 22 - - 10.0.0.0/24 -""" - 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.assertEquals(request['body'], expected) - - -class TestSecurityGroupXMLDeserializer(unittest.TestCase): - - def setUp(self): - self.deserializer = security_groups.SecurityGroupXMLDeserializer() - - def test_create_request(self): - serial_request = """ - - test -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "security_group": { - "name": "test", - "description": "test", - }, - } - self.assertEquals(request['body'], expected) - - def test_create_no_description_request(self): - serial_request = """ - -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "security_group": { - "name": "test", - }, - } - self.assertEquals(request['body'], expected) - - def test_create_no_name_request(self): - serial_request = """ - -test -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "security_group": { - "description": "test", - }, - } - self.assertEquals(request['body'], expected) - - -class TestSecurityGroupXMLSerializer(unittest.TestCase): - def setUp(self): - self.namespace = wsgi.XMLNS_V11 - self.rule_serializer = security_groups.SecurityGroupRuleTemplate() - self.index_serializer = security_groups.SecurityGroupsTemplate() - self.default_serializer = security_groups.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.assertTrue(child_tag in 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.assertTrue(gr_child_tag in 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) - - print text - 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) - - print text - 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) - - print text - 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) diff --git a/nova/tests/api/openstack/v2/contrib/test_server_action_list.py b/nova/tests/api/openstack/v2/contrib/test_server_action_list.py deleted file mode 100644 index d943376b3..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_server_action_list.py +++ /dev/null @@ -1,103 +0,0 @@ -# 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 datetime -import json -import unittest - -from lxml import etree - -from nova.api.openstack import v2 -from nova.api.openstack.v2.contrib import server_action_list -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -import nova.compute -from nova import test -from nova.tests.api.openstack import fakes -import nova.utils - - -dt = datetime.datetime.utcnow() - - -def fake_get_actions(self, _context, instance_uuid): - return [ - {'action': 'rebuild', 'error': None, 'created_at': dt}, - {'action': 'reboot', 'error': 'Failed!', 'created_at': dt}, - ] - - -def fake_instance_get(self, _context, instance_uuid): - return {'uuid': instance_uuid} - - -class ServerActionsTest(test.TestCase): - - def setUp(self): - super(ServerActionsTest, self).setUp() - self.flags(allow_admin_api=True) - self.flags(verbose=True) - self.stubs.Set(nova.compute.API, 'get_actions', fake_get_actions) - self.stubs.Set(nova.compute.API, 'get', fake_instance_get) - self.compute_api = nova.compute.API() - - self.router = v2.APIRouter() - ext_middleware = extensions.ExtensionMiddleware(self.router) - self.app = wsgi.LazySerializationMiddleware(ext_middleware) - - def test_get_actions(self): - uuid = nova.utils.gen_uuid() - req = fakes.HTTPRequest.blank('/fake/servers/%s/actions' % uuid) - res = req.get_response(self.app) - output = json.loads(res.body) - expected = {'actions': [ - {'action': 'rebuild', 'error': None, 'created_at': str(dt)}, - {'action': 'reboot', 'error': 'Failed!', 'created_at': str(dt)}, - ]} - self.assertEqual(output, expected) - - -class TestServerActionsXMLSerializer(unittest.TestCase): - 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_action_list.ServerActionsTemplate() - exemplar = [dict( - created_at=datetime.datetime.now(), - action='foo', - error='quxx'), - dict( - created_at=datetime.datetime.now(), - action='bar', - error='xxuq')] - text = serializer.serialize(dict(actions=exemplar)) - - print text - tree = etree.fromstring(text) - - self.assertEqual('actions', self._tag(tree)) - self.assertEqual(len(tree), len(exemplar)) - for idx, child in enumerate(tree): - self.assertEqual('action', self._tag(child)) - for field in ('created_at', 'action', 'error'): - self.assertEqual(str(exemplar[idx][field]), child.get(field)) diff --git a/nova/tests/api/openstack/v2/contrib/test_server_diagnostics.py b/nova/tests/api/openstack/v2/contrib/test_server_diagnostics.py deleted file mode 100644 index 2e2850f32..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_server_diagnostics.py +++ /dev/null @@ -1,86 +0,0 @@ -# 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 json -import unittest - -from lxml import etree - -from nova.api.openstack import v2 -from nova.api.openstack.v2.contrib import server_diagnostics -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -import nova.compute -from nova import test -from nova.tests.api.openstack import fakes -import nova.utils - - -def fake_get_diagnostics(self, _context, instance_uuid): - return {'data': 'Some diagnostic info'} - - -def fake_instance_get(self, _context, instance_uuid): - return {'uuid': instance_uuid} - - -class ServerDiagnosticsTest(test.TestCase): - - def setUp(self): - super(ServerDiagnosticsTest, self).setUp() - self.flags(allow_admin_api=True) - self.flags(verbose=True) - self.stubs.Set(nova.compute.API, 'get_diagnostics', - fake_get_diagnostics) - self.stubs.Set(nova.compute.API, 'get', fake_instance_get) - self.compute_api = nova.compute.API() - - self.router = v2.APIRouter() - ext_middleware = extensions.ExtensionMiddleware(self.router) - self.app = wsgi.LazySerializationMiddleware(ext_middleware) - - def test_get_diagnostics(self): - uuid = nova.utils.gen_uuid() - req = fakes.HTTPRequest.blank('/fake/servers/%s/diagnostics' % uuid) - res = req.get_response(self.app) - output = json.loads(res.body) - self.assertEqual(output, {'data': 'Some diagnostic info'}) - - -class TestServerDiagnosticsXMLSerializer(unittest.TestCase): - 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) - - print text - 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.assertTrue(tag in exemplar) - self.assertEqual(child.text, exemplar[tag]) diff --git a/nova/tests/api/openstack/v2/contrib/test_simple_tenant_usage.py b/nova/tests/api/openstack/v2/contrib/test_simple_tenant_usage.py deleted file mode 100644 index 7a1f2b35c..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_simple_tenant_usage.py +++ /dev/null @@ -1,339 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime -import json - -from lxml import etree -import webob - -from nova.api.openstack.v2.contrib import simple_tenant_usage -from nova.compute import api -from nova import context -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes - - -FLAGS = flags.FLAGS - -SERVERS = 5 -TENANTS = 2 -HOURS = 24 -LOCAL_GB = 10 -MEMORY_MB = 1024 -VCPUS = 2 -STOP = datetime.datetime.utcnow() -START = STOP - datetime.timedelta(hours=HOURS) - - -def fake_instance_type_get(self, context, instance_type_id): - return {'id': 1, - 'vcpus': VCPUS, - 'local_gb': LOCAL_GB, - 'memory_mb': MEMORY_MB, - 'name': - 'fakeflavor'} - - -def get_fake_db_instance(start, end, instance_id, tenant_id): - return {'id': instance_id, - 'image_ref': '1', - 'project_id': tenant_id, - 'user_id': 'fakeuser', - 'display_name': 'name', - 'state_description': 'state', - 'instance_type_id': 1, - 'launched_at': start, - 'terminated_at': end} - - -def fake_instance_get_active_by_window(self, context, begin, end, project_id): - return [get_fake_db_instance(START, - STOP, - x, - "faketenant_%s" % (x / SERVERS)) - for x in xrange(TENANTS * SERVERS)] - - -class SimpleTenantUsageTest(test.TestCase): - def setUp(self): - super(SimpleTenantUsageTest, self).setUp() - self.stubs.Set(api.API, "get_instance_type", - fake_instance_type_get) - self.stubs.Set(api.API, "get_active_by_window", - fake_instance_get_active_by_window) - 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) - FLAGS.allow_admin_api = True - - def test_verify_index(self): - req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage?start=%s&end=%s' % - (START.isoformat(), STOP.isoformat())) - req.method = "GET" - req.headers["content-type"] = "application/json" - - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.admin_context)) - - self.assertEqual(res.status_int, 200) - res_dict = json.loads(res.body) - usages = res_dict['tenant_usages'] - from nova import log as logging - logging.warn(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 * LOCAL_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_detailed_index(self): - req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage?' - 'detailed=1&start=%s&end=%s' % - (START.isoformat(), STOP.isoformat())) - req.method = "GET" - req.headers["content-type"] = "application/json" - - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.admin_context)) - self.assertEqual(res.status_int, 200) - res_dict = json.loads(res.body) - usages = res_dict['tenant_usages'] - 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_index_fails_for_nonadmin(self): - req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage?' - 'detailed=1&start=%s&end=%s' % - (START.isoformat(), STOP.isoformat())) - req.method = "GET" - req.headers["content-type"] = "application/json" - - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.user_context)) - self.assertEqual(res.status_int, 403) - - def test_verify_show(self): - req = webob.Request.blank( - '/v2/faketenant_0/os-simple-tenant-usage/' - 'faketenant_0?start=%s&end=%s' % - (START.isoformat(), STOP.isoformat())) - req.method = "GET" - req.headers["content-type"] = "application/json" - - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.user_context)) - self.assertEqual(res.status_int, 200) - res_dict = json.loads(res.body) - - usage = res_dict['tenant_usage'] - servers = usage['server_usages'] - self.assertEqual(len(usage['server_usages']), SERVERS) - for j in xrange(SERVERS): - self.assertEqual(int(servers[j]['hours']), HOURS) - - def test_verify_show_cant_view_other_tenant(self): - req = webob.Request.blank( - '/v2/faketenant_1/os-simple-tenant-usage/' - 'faketenant_0?start=%s&end=%s' % - (START.isoformat(), STOP.isoformat())) - req.method = "GET" - req.headers["content-type"] = "application/json" - - res = req.get_response(fakes.wsgi_app( - fake_auth_context=self.alt_user_context)) - self.assertEqual(res.status_int, 403) - - -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.assertTrue(child.tag in 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.assertTrue(child.tag in 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.SimpleTenantUsageTemplate() - today = datetime.datetime.now() - 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( - 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( - 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) - - print text - tree = etree.fromstring(text) - - self._verify_tenant_usage(raw_usage, tree) - - def test_serializer_index(self): - serializer = simple_tenant_usage.SimpleTenantUsagesTemplate() - today = datetime.datetime.now() - 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( - 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( - 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( - 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( - 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) - - print text - 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) diff --git a/nova/tests/api/openstack/v2/contrib/test_snapshots.py b/nova/tests/api/openstack/v2/contrib/test_snapshots.py deleted file mode 100644 index 5b585db82..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_snapshots.py +++ /dev/null @@ -1,299 +0,0 @@ -# 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. - -import datetime -import json -import stubout - -from lxml import etree -import webob - -from nova.api.openstack.v2.contrib import volumes -from nova import context -from nova import exception -from nova import flags -from nova import log as logging -from nova import test -from nova import volume -from nova.tests.api.openstack import fakes - -FLAGS = flags.FLAGS - -LOG = logging.getLogger('nova.tests.api.openstack.snapshot') - -_last_param = {} - - -def _get_default_snapshot_param(): - return { - 'id': 123, - 'volume_id': 12, - 'status': 'available', - 'volume_size': 100, - 'created_at': None, - 'display_name': 'Default name', - 'display_description': 'Default description', - } - - -def stub_snapshot_create(self, context, volume_id, name, description): - global _last_param - snapshot = _get_default_snapshot_param() - snapshot['volume_id'] = volume_id - snapshot['display_name'] = name - snapshot['display_description'] = description - - LOG.debug(_("_create: %s"), snapshot) - _last_param = snapshot - return snapshot - - -def stub_snapshot_delete(self, context, snapshot_id): - global _last_param - _last_param = dict(snapshot_id=snapshot_id) - - LOG.debug(_("_delete: %s"), locals()) - if snapshot_id != '123': - raise exception.NotFound - - -def stub_snapshot_get(self, context, snapshot_id): - global _last_param - _last_param = dict(snapshot_id=snapshot_id) - - LOG.debug(_("_get: %s"), locals()) - if snapshot_id != '123': - raise exception.NotFound - - param = _get_default_snapshot_param() - param['id'] = snapshot_id - return param - - -def stub_snapshot_get_all(self, context): - LOG.debug(_("_get_all: %s"), locals()) - param = _get_default_snapshot_param() - param['id'] = 123 - return [param] - - -class SnapshotApiTest(test.TestCase): - def setUp(self): - super(SnapshotApiTest, self).setUp() - self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.reset_fake_data() - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_auth(self.stubs) - self.stubs.Set(volume.api.API, "create_snapshot", stub_snapshot_create) - self.stubs.Set(volume.api.API, "create_snapshot_force", - stub_snapshot_create) - self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete) - self.stubs.Set(volume.api.API, "get_snapshot", stub_snapshot_get) - self.stubs.Set(volume.api.API, "get_all_snapshots", - stub_snapshot_get_all) - - self.context = context.get_admin_context() - - def tearDown(self): - self.stubs.UnsetAll() - super(SnapshotApiTest, self).tearDown() - - def test_snapshot_create(self): - global _last_param - _last_param = {} - - 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 = json.dumps(body) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_app()) - LOG.debug(_("test_snapshot_create: param=%s"), _last_param) - self.assertEqual(resp.status_int, 200) - - # Compare if parameters were correctly passed to stub - self.assertEqual(_last_param['display_name'], "Snapshot Test Name") - self.assertEqual(_last_param['display_description'], - "Snapshot Test Desc") - - resp_dict = json.loads(resp.body) - LOG.debug(_("test_snapshot_create: resp_dict=%s"), resp_dict) - self.assertTrue('snapshot' in resp_dict) - self.assertEqual(resp_dict['snapshot']['displayName'], - snapshot['display_name']) - self.assertEqual(resp_dict['snapshot']['displayDescription'], - snapshot['display_description']) - - def test_snapshot_create_force(self): - global _last_param - _last_param = {} - - 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 = json.dumps(body) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_app()) - LOG.debug(_("test_snapshot_create_force: param=%s"), _last_param) - self.assertEqual(resp.status_int, 200) - - # Compare if parameters were correctly passed to stub - self.assertEqual(_last_param['display_name'], "Snapshot Test Name") - self.assertEqual(_last_param['display_description'], - "Snapshot Test Desc") - - resp_dict = json.loads(resp.body) - LOG.debug(_("test_snapshot_create_force: resp_dict=%s"), resp_dict) - self.assertTrue('snapshot' in resp_dict) - self.assertEqual(resp_dict['snapshot']['displayName'], - snapshot['display_name']) - self.assertEqual(resp_dict['snapshot']['displayDescription'], - snapshot['display_description']) - - def test_snapshot_delete(self): - global _last_param - _last_param = {} - - snapshot_id = 123 - req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) - req.method = 'DELETE' - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 202) - self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) - - def test_snapshot_delete_invalid_id(self): - global _last_param - _last_param = {} - - snapshot_id = 234 - req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) - req.method = 'DELETE' - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 404) - self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) - - def test_snapshot_show(self): - global _last_param - _last_param = {} - - snapshot_id = 123 - req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) - req.method = 'GET' - resp = req.get_response(fakes.wsgi_app()) - - LOG.debug(_("test_snapshot_show: resp=%s"), resp) - self.assertEqual(resp.status_int, 200) - self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) - - resp_dict = json.loads(resp.body) - self.assertTrue('snapshot' in resp_dict) - self.assertEqual(resp_dict['snapshot']['id'], str(snapshot_id)) - - def test_snapshot_show_invalid_id(self): - global _last_param - _last_param = {} - - snapshot_id = 234 - req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) - req.method = 'GET' - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 404) - self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) - - def test_snapshot_detail(self): - req = webob.Request.blank('/v2/fake/os-snapshots/detail') - req.method = 'GET' - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - - resp_dict = json.loads(resp.body) - LOG.debug(_("test_snapshot_detail: resp_dict=%s"), resp_dict) - self.assertTrue('snapshots' in resp_dict) - resp_snapshots = resp_dict['snapshots'] - self.assertEqual(len(resp_snapshots), 1) - - resp_snapshot = resp_snapshots.pop() - self.assertEqual(resp_snapshot['id'], 123) - - -class SnapshotSerializerTest(test.TestCase): - 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=datetime.datetime.now(), - displayName='snap_name', - displayDescription='snap_desc', - volumeId='vol_id', - ) - text = serializer.serialize(dict(snapshot=raw_snapshot)) - - print text - 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=datetime.datetime.now(), - displayName='snap1_name', - displayDescription='snap1_desc', - volumeId='vol1_id', - ), - dict( - id='snap2_id', - status='snap2_status', - size=1024, - createdAt=datetime.datetime.now(), - displayName='snap2_name', - displayDescription='snap2_desc', - volumeId='vol2_id', - )] - text = serializer.serialize(dict(snapshots=raw_snapshots)) - - print text - 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/api/openstack/v2/contrib/test_users.py b/nova/tests/api/openstack/v2/contrib/test_users.py deleted file mode 100644 index ace243f58..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_users.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from lxml import etree - -from nova.api.openstack.v2.contrib import users -from nova.auth.manager import User, Project -from nova import test -from nova.tests.api.openstack import fakes -from nova import utils - - -def fake_init(self): - self.manager = fakes.FakeAuthManager() - - -def fake_admin_check(self, req): - return True - - -class UsersTest(test.TestCase): - def setUp(self): - super(UsersTest, self).setUp() - self.flags(verbose=True, allow_admin_api=True) - self.stubs.Set(users.Controller, '__init__', - fake_init) - self.stubs.Set(users.Controller, '_check_admin', - fake_admin_check) - fakes.FakeAuthManager.clear_fakes() - fakes.FakeAuthManager.projects = dict(testacct=Project('testacct', - 'testacct', - 'id1', - 'test', - [])) - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_auth(self.stubs) - - fakemgr = fakes.FakeAuthManager() - fakemgr.add_user(User('id1', 'guy1', 'acc1', 'secret1', False)) - fakemgr.add_user(User('id2', 'guy2', 'acc2', 'secret2', True)) - - self.controller = users.Controller() - - def test_get_user_list(self): - req = fakes.HTTPRequest.blank('/v2/fake/users') - res_dict = self.controller.index(req) - - self.assertEqual(len(res_dict['users']), 2) - - def test_get_user_by_id(self): - req = fakes.HTTPRequest.blank('/v2/fake/users/id2') - res_dict = self.controller.show(req, 'id2') - - self.assertEqual(res_dict['user']['id'], 'id2') - self.assertEqual(res_dict['user']['name'], 'guy2') - self.assertEqual(res_dict['user']['secret'], 'secret2') - self.assertEqual(res_dict['user']['admin'], True) - - def test_user_delete(self): - req = fakes.HTTPRequest.blank('/v2/fake/users/id1') - self.controller.delete(req, 'id1') - - self.assertTrue('id1' not in [u.id for u in - fakes.FakeAuthManager.auth_data]) - - def test_user_create(self): - secret = utils.generate_password() - body = dict(user=dict(name='test_guy', - access='acc3', - secret=secret, - admin=True)) - req = fakes.HTTPRequest.blank('/v2/fake/users') - res_dict = self.controller.create(req, body) - - # NOTE(justinsb): This is a questionable assertion in general - # fake sets id=name, but others might not... - self.assertEqual(res_dict['user']['id'], 'test_guy') - - self.assertEqual(res_dict['user']['name'], 'test_guy') - self.assertEqual(res_dict['user']['access'], 'acc3') - self.assertEqual(res_dict['user']['secret'], secret) - self.assertEqual(res_dict['user']['admin'], True) - self.assertTrue('test_guy' in [u.id for u in - fakes.FakeAuthManager.auth_data]) - self.assertEqual(len(fakes.FakeAuthManager.auth_data), 3) - - def test_user_update(self): - new_secret = utils.generate_password() - body = dict(user=dict(name='guy2', - access='acc2', - secret=new_secret)) - - req = fakes.HTTPRequest.blank('/v2/fake/users/id2') - res_dict = self.controller.update(req, 'id2', body) - - self.assertEqual(res_dict['user']['id'], 'id2') - self.assertEqual(res_dict['user']['name'], 'guy2') - self.assertEqual(res_dict['user']['access'], 'acc2') - self.assertEqual(res_dict['user']['secret'], new_secret) - self.assertEqual(res_dict['user']['admin'], True) - - -class TestUsersXMLSerializer(test.TestCase): - - def test_index(self): - serializer = users.UsersTemplate() - fixture = {'users': [{'id': 'id1', - 'name': 'guy1', - 'secret': 'secret1', - 'admin': False}, - {'id': 'id2', - 'name': 'guy2', - 'secret': 'secret2', - 'admin': True}]} - - output = serializer.serialize(fixture) - res_tree = etree.XML(output) - - self.assertEqual(res_tree.tag, 'users') - self.assertEqual(len(res_tree), 2) - self.assertEqual(res_tree[0].tag, 'user') - self.assertEqual(res_tree[0].get('id'), 'id1') - self.assertEqual(res_tree[1].tag, 'user') - self.assertEqual(res_tree[1].get('id'), 'id2') - - def test_show(self): - serializer = users.UserTemplate() - fixture = {'user': {'id': 'id2', - 'name': 'guy2', - 'secret': 'secret2', - 'admin': True}} - - output = serializer.serialize(fixture) - res_tree = etree.XML(output) - - self.assertEqual(res_tree.tag, 'user') - self.assertEqual(res_tree.get('id'), 'id2') - self.assertEqual(res_tree.get('name'), 'guy2') - self.assertEqual(res_tree.get('secret'), 'secret2') - self.assertEqual(res_tree.get('admin'), 'True') diff --git a/nova/tests/api/openstack/v2/contrib/test_virtual_interfaces.py b/nova/tests/api/openstack/v2/contrib/test_virtual_interfaces.py deleted file mode 100644 index 3530e68fd..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_virtual_interfaces.py +++ /dev/null @@ -1,93 +0,0 @@ -# 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. - -import json - -from lxml import etree -import webob - -from nova.api.openstack.v2.contrib import virtual_interfaces -from nova.api.openstack import wsgi -from nova import network -from nova import test -from nova.tests.api.openstack import fakes - - -def get_vifs_by_instance(self, context, server_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 ServerVirtualInterfaceTest(test.TestCase): - - def setUp(self): - super(ServerVirtualInterfaceTest, self).setUp() - self.controller = virtual_interfaces.ServerVirtualInterfaceController() - self.stubs.Set(network.api.API, "get_vifs_by_instance", - get_vifs_by_instance) - - def tearDown(self): - super(ServerVirtualInterfaceTest, self).tearDown() - - 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()) - self.assertEqual(res.status_int, 200) - res_dict = json.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) - - -class ServerVirtualInterfaceSerializerTest(test.TestCase): - 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) - - print text - 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/api/openstack/v2/contrib/test_volume_types.py b/nova/tests/api/openstack/v2/contrib/test_volume_types.py deleted file mode 100644 index de3cf86e1..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_volume_types.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from lxml import etree -import webob - -from nova.api.openstack.v2.contrib import volumetypes -from nova import exception -from nova import test -from nova import log as logging -from nova.volume import volume_types -from nova.tests.api.openstack import fakes - - -LOG = logging.getLogger('nova.tests.api.openstack.v2.contrib.' - 'test_volume_types') - -last_param = {} - - -def stub_volume_type(id): - specs = { - "key1": "value1", - "key2": "value2", - "key3": "value3", - "key4": "value4", - "key5": "value5"} - return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs) - - -def return_volume_types_get_all_types(context): - return dict(vol_type_1=stub_volume_type(1), - vol_type_2=stub_volume_type(2), - vol_type_3=stub_volume_type(3)) - - -def return_empty_volume_types_get_all_types(context): - return {} - - -def return_volume_types_get_volume_type(context, id): - if id == "777": - raise exception.VolumeTypeNotFound(volume_type_id=id) - return stub_volume_type(int(id)) - - -def return_volume_types_destroy(context, name): - if name == "777": - raise exception.VolumeTypeNotFoundByName(volume_type_name=name) - pass - - -def return_volume_types_create(context, name, specs): - pass - - -def return_volume_types_get_by_name(context, name): - if name == "777": - raise exception.VolumeTypeNotFoundByName(volume_type_name=name) - return stub_volume_type(int(name.split("_")[2])) - - -class VolumeTypesApiTest(test.TestCase): - def setUp(self): - super(VolumeTypesApiTest, self).setUp() - fakes.stub_out_key_pair_funcs(self.stubs) - self.controller = volumetypes.VolumeTypesController() - - def tearDown(self): - self.stubs.UnsetAll() - super(VolumeTypesApiTest, self).tearDown() - - def test_volume_types_index(self): - self.stubs.Set(volume_types, 'get_all_types', - return_volume_types_get_all_types) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') - res_dict = self.controller.index(req) - - self.assertEqual(3, len(res_dict)) - for name in ['vol_type_1', 'vol_type_2', 'vol_type_3']: - self.assertEqual(name, res_dict[name]['name']) - self.assertEqual('value1', res_dict[name]['extra_specs']['key1']) - - def test_volume_types_index_no_data(self): - self.stubs.Set(volume_types, 'get_all_types', - return_empty_volume_types_get_all_types) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') - res_dict = self.controller.index(req) - - self.assertEqual(0, len(res_dict)) - - def test_volume_types_show(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/1') - res_dict = self.controller.show(req, 1) - - self.assertEqual(1, len(res_dict)) - self.assertEqual('vol_type_1', res_dict['volume_type']['name']) - - def test_volume_types_show_not_found(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/777') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, - req, '777') - - def test_volume_types_delete(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - self.stubs.Set(volume_types, 'destroy', - return_volume_types_destroy) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/1') - self.controller.delete(req, 1) - - def test_volume_types_delete_not_found(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - self.stubs.Set(volume_types, 'destroy', - return_volume_types_destroy) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/777') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, - req, '777') - - def test_create(self): - self.stubs.Set(volume_types, 'create', - return_volume_types_create) - self.stubs.Set(volume_types, 'get_volume_type_by_name', - return_volume_types_get_by_name) - - body = {"volume_type": {"name": "vol_type_1", - "extra_specs": {"key1": "value1"}}} - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') - res_dict = self.controller.create(req, body) - - self.assertEqual(1, len(res_dict)) - self.assertEqual('vol_type_1', res_dict['volume_type']['name']) - - def test_create_empty_body(self): - self.stubs.Set(volume_types, 'create', - return_volume_types_create) - self.stubs.Set(volume_types, 'get_volume_type_by_name', - return_volume_types_get_by_name) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.create, req, '') - - -class VolumeTypesSerializerTest(test.TestCase): - def _verify_volume_type(self, vtype, tree): - self.assertEqual('volume_type', tree.tag) - self.assertEqual(vtype['name'], tree.get('name')) - self.assertEqual(str(vtype['id']), tree.get('id')) - self.assertEqual(1, len(tree)) - extra_specs = tree[0] - self.assertEqual('extra_specs', extra_specs.tag) - seen = set(vtype['extra_specs'].keys()) - for child in extra_specs: - self.assertTrue(child.tag in seen) - self.assertEqual(vtype['extra_specs'][child.tag], child.text) - seen.remove(child.tag) - self.assertEqual(len(seen), 0) - - def test_index_serializer(self): - serializer = volumetypes.VolumeTypesTemplate() - - # Just getting some input data - vtypes = return_volume_types_get_all_types(None) - text = serializer.serialize(vtypes) - - print text - tree = etree.fromstring(text) - - self.assertEqual('volume_types', tree.tag) - self.assertEqual(len(vtypes), len(tree)) - for child in tree: - name = child.get('name') - self.assertTrue(name in vtypes) - self._verify_volume_type(vtypes[name], child) - - def test_voltype_serializer(self): - serializer = volumetypes.VolumeTypeTemplate() - - vtype = stub_volume_type(1) - text = serializer.serialize(dict(volume_type=vtype)) - - print text - tree = etree.fromstring(text) - - self._verify_volume_type(vtype, tree) diff --git a/nova/tests/api/openstack/v2/contrib/test_volume_types_extra_specs.py b/nova/tests/api/openstack/v2/contrib/test_volume_types_extra_specs.py deleted file mode 100644 index 81e57cee9..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_volume_types_extra_specs.py +++ /dev/null @@ -1,198 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Zadara Storage Inc. -# Copyright (c) 2011 OpenStack LLC. -# 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. - -from lxml import etree -import webob - -from nova.api.openstack.v2.contrib import volumetypes -from nova import test -from nova.tests.api.openstack import fakes -import nova.wsgi - - -def return_create_volume_type_extra_specs(context, volume_type_id, - extra_specs): - return stub_volume_type_extra_specs() - - -def return_volume_type_extra_specs(context, volume_type_id): - return stub_volume_type_extra_specs() - - -def return_empty_volume_type_extra_specs(context, volume_type_id): - return {} - - -def delete_volume_type_extra_specs(context, volume_type_id, key): - pass - - -def stub_volume_type_extra_specs(): - specs = { - "key1": "value1", - "key2": "value2", - "key3": "value3", - "key4": "value4", - "key5": "value5"} - return specs - - -class VolumeTypesExtraSpecsTest(test.TestCase): - - def setUp(self): - super(VolumeTypesExtraSpecsTest, self).setUp() - fakes.stub_out_key_pair_funcs(self.stubs) - self.api_path = '/v2/fake/os-volume-types/1/extra_specs' - self.controller = volumetypes.VolumeTypeExtraSpecsController() - - def test_index(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path) - 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, 'volume_type_extra_specs_get', - return_empty_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path) - res_dict = self.controller.index(req, 1) - - self.assertEqual(0, len(res_dict['extra_specs'])) - - def test_show(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key5') - 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, 'volume_type_extra_specs_get', - return_empty_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key6') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, - req, 1, 'key6') - - def test_delete(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_delete', - delete_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key5') - self.controller.delete(req, 1, 'key5') - - def test_create(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"extra_specs": {"key1": "value1"}} - - req = fakes.HTTPRequest.blank(self.api_path) - res_dict = self.controller.create(req, 1, body) - - self.assertEqual('value1', res_dict['extra_specs']['key1']) - - def test_create_empty_body(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path) - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, - req, 1, '') - - def test_update_item(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"key1": "value1"} - - req = fakes.HTTPRequest.blank(self.api_path + '/key1') - res_dict = self.controller.update(req, 1, 'key1', body) - - self.assertEqual('value1', res_dict['key1']) - - def test_update_item_empty_body(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key1') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'key1', '') - - def test_update_item_too_many_keys(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"key1": "value1", "key2": "value2"} - - req = fakes.HTTPRequest.blank(self.api_path + '/key1') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'key1', body) - - def test_update_item_body_uri_mismatch(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"key1": "value1"} - - req = fakes.HTTPRequest.blank(self.api_path + '/bad') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'bad', body) - - -class VolumeTypeExtraSpecsSerializerTest(test.TestCase): - def test_index_create_serializer(self): - serializer = volumetypes.VolumeTypeExtraSpecsTemplate() - - # Just getting some input data - extra_specs = stub_volume_type_extra_specs() - text = serializer.serialize(dict(extra_specs=extra_specs)) - - print text - tree = etree.fromstring(text) - - self.assertEqual('extra_specs', tree.tag) - self.assertEqual(len(extra_specs), len(tree)) - seen = set(extra_specs.keys()) - for child in tree: - self.assertTrue(child.tag in seen) - self.assertEqual(extra_specs[child.tag], child.text) - seen.remove(child.tag) - self.assertEqual(len(seen), 0) - - def test_update_show_serializer(self): - serializer = volumetypes.VolumeTypeExtraSpecTemplate() - - exemplar = dict(key1='value1') - text = serializer.serialize(exemplar) - - print text - tree = etree.fromstring(text) - - self.assertEqual('key1', tree.tag) - self.assertEqual('value1', tree.text) - self.assertEqual(0, len(tree)) diff --git a/nova/tests/api/openstack/v2/contrib/test_volumes.py b/nova/tests/api/openstack/v2/contrib/test_volumes.py deleted file mode 100644 index a5585bd64..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_volumes.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright 2013 Josh Durgin -# 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 json - -from lxml import etree -import webob - -import nova -from nova.api.openstack.v2.contrib import volumes -from nova.compute import instance_types -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes - - -FLAGS = flags.FLAGS - - -FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' -IMAGE_UUID = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' - - -def fake_compute_api_create(cls, context, instance_type, image_href, **kwargs): - global _block_device_mapping_seen - _block_device_mapping_seen = kwargs.get('block_device_mapping') - - inst_type = instance_types.get_instance_type_by_flavor_id(2) - resv_id = None - return ([{'id': 1, - 'display_name': 'test_server', - 'uuid': FAKE_UUID, - 'instance_type': dict(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) - - -class BootFromVolumeTest(test.TestCase): - - def setUp(self): - super(BootFromVolumeTest, self).setUp() - self.stubs.Set(nova.compute.API, 'create', fake_compute_api_create) - fakes.stub_out_nw_api(self.stubs) - - 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, - )] - )) - global _block_device_mapping_seen - _block_device_mapping_seen = None - req = webob.Request.blank('/v2/fake/os-volumes_boot') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = 'application/json' - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 202) - server = json.loads(res.body)['server'] - self.assertEqual(FAKE_UUID, server['id']) - self.assertEqual(FLAGS.password_length, len(server['adminPass'])) - self.assertEqual(len(_block_device_mapping_seen), 1) - self.assertEqual(_block_device_mapping_seen[0]['volume_id'], 1) - self.assertEqual(_block_device_mapping_seen[0]['device_name'], - '/dev/vda') - - -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.assertTrue(child.tag in ('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.assertTrue(gr_child.tag in not_seen) - self.assertEqual(str(vol['metadata'][gr_child.tag]), - gr_child.text) - not_seen.remove(gr_child.tag) - 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)) - - print text - 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)) - - print text - 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=datetime.datetime.now(), - 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)) - - print text - 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=datetime.datetime.now(), - 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=datetime.datetime.now(), - 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)) - - print text - 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) diff --git a/nova/tests/api/openstack/v2/contrib/test_vsa.py b/nova/tests/api/openstack/v2/contrib/test_vsa.py deleted file mode 100644 index fa5127db6..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_vsa.py +++ /dev/null @@ -1,714 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime -import json - -from lxml import etree -import stubout -import webob - -from nova.api.openstack.v2.contrib import virtual_storage_arrays as vsa_ext -from nova import context -import nova.db -from nova import exception -from nova import flags -from nova import log as logging -from nova import test -from nova.tests.api.openstack import fakes -from nova import volume -from nova import vsa - - -FLAGS = flags.FLAGS - -LOG = logging.getLogger('nova.tests.api.openstack.v2.contrib.test_vsa') - -last_param = {} - - -def _get_default_vsa_param(): - return { - 'display_name': 'Test_VSA_name', - 'display_description': 'Test_VSA_description', - 'vc_count': 1, - 'instance_type': 'm1.small', - 'instance_type_id': 5, - 'image_name': None, - 'availability_zone': None, - 'storage': [], - 'shared': False, - } - - -def stub_vsa_create(self, context, **param): - global last_param - LOG.debug(_("_create: param=%s"), param) - param['id'] = 123 - param['name'] = 'Test name' - param['instance_type_id'] = 5 - last_param = param - return param - - -def stub_vsa_delete(self, context, vsa_id): - global last_param - last_param = dict(vsa_id=vsa_id) - - LOG.debug(_("_delete: %s"), locals()) - if vsa_id != '123': - raise exception.NotFound - - -def stub_vsa_get(self, context, vsa_id): - global last_param - last_param = dict(vsa_id=vsa_id) - - LOG.debug(_("_get: %s"), locals()) - if vsa_id != '123': - raise exception.NotFound - - param = _get_default_vsa_param() - param['id'] = vsa_id - return param - - -def stub_vsa_get_all(self, context): - LOG.debug(_("_get_all: %s"), locals()) - param = _get_default_vsa_param() - param['id'] = 123 - return [param] - - -class VSAApiTest(test.TestCase): - def setUp(self): - super(VSAApiTest, self).setUp() - self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.reset_fake_data() - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_auth(self.stubs) - self.stubs.Set(vsa.api.API, "create", stub_vsa_create) - self.stubs.Set(vsa.api.API, "delete", stub_vsa_delete) - self.stubs.Set(vsa.api.API, "get", stub_vsa_get) - self.stubs.Set(vsa.api.API, "get_all", stub_vsa_get_all) - - self.context = context.get_admin_context() - - def tearDown(self): - self.stubs.UnsetAll() - super(VSAApiTest, self).tearDown() - - def test_vsa_create(self): - global last_param - last_param = {} - - vsa = {"displayName": "VSA Test Name", - "displayDescription": "VSA Test Desc"} - body = dict(vsa=vsa) - req = webob.Request.blank('/v2/fake/zadr-vsa') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - - # Compare if parameters were correctly passed to stub - self.assertEqual(last_param['display_name'], "VSA Test Name") - self.assertEqual(last_param['display_description'], "VSA Test Desc") - - resp_dict = json.loads(resp.body) - self.assertTrue('vsa' in resp_dict) - self.assertEqual(resp_dict['vsa']['displayName'], vsa['displayName']) - self.assertEqual(resp_dict['vsa']['displayDescription'], - vsa['displayDescription']) - - def test_vsa_create_no_body(self): - req = webob.Request.blank('/v2/fake/zadr-vsa') - req.method = 'POST' - req.body = json.dumps({}) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 422) - - def test_vsa_delete(self): - global last_param - last_param = {} - - vsa_id = 123 - req = webob.Request.blank('/v2/fake/zadr-vsa/%d' % vsa_id) - req.method = 'DELETE' - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - self.assertEqual(str(last_param['vsa_id']), str(vsa_id)) - - def test_vsa_delete_invalid_id(self): - global last_param - last_param = {} - - vsa_id = 234 - req = webob.Request.blank('/v2/fake/zadr-vsa/%d' % vsa_id) - req.method = 'DELETE' - - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 404) - self.assertEqual(str(last_param['vsa_id']), str(vsa_id)) - - def test_vsa_show(self): - global last_param - last_param = {} - - vsa_id = 123 - req = webob.Request.blank('/v2/fake/zadr-vsa/%d' % vsa_id) - req.method = 'GET' - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - self.assertEqual(str(last_param['vsa_id']), str(vsa_id)) - - resp_dict = json.loads(resp.body) - self.assertTrue('vsa' in resp_dict) - self.assertEqual(resp_dict['vsa']['id'], str(vsa_id)) - - def test_vsa_show_invalid_id(self): - global last_param - last_param = {} - - vsa_id = 234 - req = webob.Request.blank('/v2/fake/zadr-vsa/%d' % vsa_id) - req.method = 'GET' - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 404) - self.assertEqual(str(last_param['vsa_id']), str(vsa_id)) - - def test_vsa_index(self): - req = webob.Request.blank('/v2/fake/zadr-vsa') - req.method = 'GET' - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - - resp_dict = json.loads(resp.body) - - self.assertTrue('vsaSet' in resp_dict) - resp_vsas = resp_dict['vsaSet'] - self.assertEqual(len(resp_vsas), 1) - - resp_vsa = resp_vsas.pop() - self.assertEqual(resp_vsa['id'], 123) - - def test_vsa_detail(self): - req = webob.Request.blank('/v2/fake/zadr-vsa/detail') - req.method = 'GET' - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - - resp_dict = json.loads(resp.body) - - self.assertTrue('vsaSet' in resp_dict) - resp_vsas = resp_dict['vsaSet'] - self.assertEqual(len(resp_vsas), 1) - - resp_vsa = resp_vsas.pop() - self.assertEqual(resp_vsa['id'], 123) - - -def _get_default_volume_param(): - return { - 'id': 123, - 'status': 'available', - 'size': 100, - 'availability_zone': 'nova', - 'created_at': None, - 'attach_status': 'detached', - 'name': 'vol name', - 'display_name': 'Default vol name', - 'display_description': 'Default vol description', - 'volume_type_id': 1, - 'volume_metadata': [], - 'snapshot_id': None, - } - - -def stub_get_vsa_volume_type(self, context): - return {'id': 1, - 'name': 'VSA volume type', - 'extra_specs': {'type': 'vsa_volume'}} - - -def stub_volume_create(self, context, size, snapshot_id, name, description, - **param): - LOG.debug(_("_create: param=%s"), size) - vol = _get_default_volume_param() - vol['size'] = size - vol['display_name'] = name - vol['display_description'] = description - vol['snapshot_id'] = snapshot_id - return vol - - -def stub_volume_update(self, context, **param): - LOG.debug(_("_volume_update: param=%s"), param) - pass - - -def stub_volume_delete(self, context, **param): - LOG.debug(_("_volume_delete: param=%s"), param) - pass - - -def stub_volume_get(self, context, volume_id): - LOG.debug(_("_volume_get: volume_id=%s"), volume_id) - vol = _get_default_volume_param() - vol['id'] = volume_id - meta = {'key': 'from_vsa_id', 'value': '123'} - if volume_id == '345': - meta = {'key': 'to_vsa_id', 'value': '123'} - vol['volume_metadata'].append(meta) - return vol - - -def stub_volume_get_notfound(self, context, volume_id): - raise exception.NotFound - - -def stub_volume_get_all(self, context, search_opts): - vol = stub_volume_get(self, context, '123') - vol['metadata'] = search_opts['metadata'] - return [vol] - - -def return_vsa(context, vsa_id): - return {'id': vsa_id} - - -class VSAVolumeApiTest(test.TestCase): - - def setUp(self, test_obj=None, test_objs=None): - super(VSAVolumeApiTest, self).setUp() - self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.reset_fake_data() - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_auth(self.stubs) - self.stubs.Set(nova.db, 'vsa_get', return_vsa) - self.stubs.Set(vsa.api.API, "get_vsa_volume_type", - stub_get_vsa_volume_type) - - self.stubs.Set(volume.api.API, "update", stub_volume_update) - self.stubs.Set(volume.api.API, "delete", stub_volume_delete) - self.stubs.Set(volume.api.API, "get", stub_volume_get) - self.stubs.Set(volume.api.API, "get_all", stub_volume_get_all) - - self.context = context.get_admin_context() - self.test_obj = test_obj if test_obj else "volume" - self.test_objs = test_objs if test_objs else "volumes" - - def tearDown(self): - self.stubs.UnsetAll() - super(VSAVolumeApiTest, self).tearDown() - - def test_vsa_volume_create(self): - self.stubs.Set(volume.api.API, "create", stub_volume_create) - - vol = {"size": 100, - "displayName": "VSA Volume Test Name", - "displayDescription": "VSA Volume Test Desc"} - body = {self.test_obj: vol} - req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s' % self.test_objs) - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = 'application/json' - resp = req.get_response(fakes.wsgi_app()) - - if self.test_obj == "volume": - self.assertEqual(resp.status_int, 200) - - resp_dict = json.loads(resp.body) - self.assertTrue(self.test_obj in resp_dict) - self.assertEqual(resp_dict[self.test_obj]['size'], - vol['size']) - self.assertEqual(resp_dict[self.test_obj]['displayName'], - vol['displayName']) - self.assertEqual(resp_dict[self.test_obj]['displayDescription'], - vol['displayDescription']) - else: - self.assertEqual(resp.status_int, 400) - - def test_vsa_volume_create_no_body(self): - req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s' % self.test_objs) - req.method = 'POST' - req.body = json.dumps({}) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_app()) - if self.test_obj == "volume": - self.assertEqual(resp.status_int, 422) - else: - self.assertEqual(resp.status_int, 400) - - def test_vsa_volume_index(self): - req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s' % self.test_objs) - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - - def test_vsa_volume_detail(self): - req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/detail' % \ - self.test_objs) - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - - def test_vsa_volume_show(self): - obj_num = 234 if self.test_objs == "volumes" else 345 - req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/%s' % \ - (self.test_objs, obj_num)) - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 200) - - def test_vsa_volume_show_no_vsa_assignment(self): - req = webob.Request.blank('/v2/fake/zadr-vsa/4/%s/333' % \ - (self.test_objs)) - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 400) - - def test_vsa_volume_show_no_volume(self): - self.stubs.Set(volume.api.API, "get", stub_volume_get_notfound) - - req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/333' % \ - (self.test_objs)) - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 404) - - def test_vsa_volume_update(self): - obj_num = 234 if self.test_objs == "volumes" else 345 - update = {"status": "available", - "displayName": "Test Display name"} - body = {self.test_obj: update} - req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/%s' % \ - (self.test_objs, obj_num)) - req.method = 'PUT' - req.body = json.dumps(body) - req.headers['content-type'] = 'application/json' - - resp = req.get_response(fakes.wsgi_app()) - if self.test_obj == "volume": - self.assertEqual(resp.status_int, 202) - else: - self.assertEqual(resp.status_int, 400) - - def test_vsa_volume_delete(self): - obj_num = 234 if self.test_objs == "volumes" else 345 - req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/%s' % \ - (self.test_objs, obj_num)) - req.method = 'DELETE' - resp = req.get_response(fakes.wsgi_app()) - if self.test_obj == "volume": - self.assertEqual(resp.status_int, 202) - else: - self.assertEqual(resp.status_int, 400) - - def test_vsa_volume_delete_no_vsa_assignment(self): - req = webob.Request.blank('/v2/fake/zadr-vsa/4/%s/333' % \ - (self.test_objs)) - req.method = 'DELETE' - resp = req.get_response(fakes.wsgi_app()) - self.assertEqual(resp.status_int, 400) - - def test_vsa_volume_delete_no_volume(self): - self.stubs.Set(volume.api.API, "get", stub_volume_get_notfound) - - req = webob.Request.blank('/v2/fake/zadr-vsa/123/%s/333' % \ - (self.test_objs)) - req.method = 'DELETE' - resp = req.get_response(fakes.wsgi_app()) - if self.test_obj == "volume": - self.assertEqual(resp.status_int, 404) - else: - self.assertEqual(resp.status_int, 400) - - -class VSADriveApiTest(VSAVolumeApiTest): - def setUp(self): - super(VSADriveApiTest, self).setUp(test_obj="drive", - test_objs="drives") - - def tearDown(self): - self.stubs.UnsetAll() - super(VSADriveApiTest, self).tearDown() - - -class SerializerTestCommon(test.TestCase): - def _verify_attrs(self, obj, tree, attrs): - for attr in attrs: - self.assertEqual(str(obj[attr]), tree.get(attr)) - - -class VsaSerializerTest(SerializerTestCommon): - def test_serialize_show_create(self): - serializer = vsa_ext.VsaTemplate() - exemplar = dict( - id='vsa_id', - name='vsa_name', - displayName='vsa_display_name', - displayDescription='vsa_display_desc', - createTime=datetime.datetime.now(), - status='active', - vcType='vsa_instance_type', - vcCount=24, - driveCount=48, - ipAddress='10.11.12.13') - text = serializer.serialize(dict(vsa=exemplar)) - - print text - tree = etree.fromstring(text) - - self.assertEqual('vsa', tree.tag) - self._verify_attrs(exemplar, tree, exemplar.keys()) - - def test_serialize_index_detail(self): - serializer = vsa_ext.VsaSetTemplate() - exemplar = [dict( - id='vsa1_id', - name='vsa1_name', - displayName='vsa1_display_name', - displayDescription='vsa1_display_desc', - createTime=datetime.datetime.now(), - status='active', - vcType='vsa1_instance_type', - vcCount=24, - driveCount=48, - ipAddress='10.11.12.13'), - dict( - id='vsa2_id', - name='vsa2_name', - displayName='vsa2_display_name', - displayDescription='vsa2_display_desc', - createTime=datetime.datetime.now(), - status='active', - vcType='vsa2_instance_type', - vcCount=42, - driveCount=84, - ipAddress='11.12.13.14')] - text = serializer.serialize(dict(vsaSet=exemplar)) - - print text - tree = etree.fromstring(text) - - self.assertEqual('vsaSet', tree.tag) - self.assertEqual(len(exemplar), len(tree)) - for idx, child in enumerate(tree): - self.assertEqual('vsa', child.tag) - self._verify_attrs(exemplar[idx], child, exemplar[idx].keys()) - - -class VsaVolumeSerializerTest(SerializerTestCommon): - show_serializer = vsa_ext.VsaVolumeTemplate - index_serializer = vsa_ext.VsaVolumesTemplate - object = 'volume' - objects = 'volumes' - - def _verify_voldrive(self, vol, tree): - self.assertEqual(self.object, tree.tag) - - self._verify_attrs(vol, tree, ('id', 'status', 'size', - 'availabilityZone', 'createdAt', - 'displayName', 'displayDescription', - 'volumeType', 'vsaId', 'name')) - - for child in tree: - self.assertTrue(child.tag in ('attachments', 'metadata')) - if child.tag == 'attachments': - self.assertEqual(1, len(child)) - self.assertEqual('attachment', child[0].tag) - self._verify_attrs(vol['attachments'][0], child[0], - ('id', 'volumeId', 'serverId', 'device')) - elif child.tag == 'metadata': - not_seen = set(vol['metadata'].keys()) - for gr_child in child: - self.assertTrue(gr_child.tag in not_seen) - self.assertEqual(str(vol['metadata'][gr_child.tag]), - gr_child.text) - not_seen.remove(gr_child.tag) - self.assertEqual(0, len(not_seen)) - - def test_show_create_serializer(self): - serializer = self.show_serializer() - raw_volume = dict( - id='vol_id', - status='vol_status', - size=1024, - availabilityZone='vol_availability', - createdAt=datetime.datetime.now(), - attachments=[dict( - id='vol_id', - volumeId='vol_id', - serverId='instance_uuid', - device='/foo')], - displayName='vol_name', - displayDescription='vol_desc', - volumeType='vol_type', - metadata=dict( - foo='bar', - baz='quux', - ), - vsaId='vol_vsa_id', - name='vol_vsa_name', - ) - text = serializer.serialize({self.object: raw_volume}) - - print text - tree = etree.fromstring(text) - - self._verify_voldrive(raw_volume, tree) - - def test_index_detail_serializer(self): - serializer = self.index_serializer() - raw_volumes = [dict( - id='vol1_id', - status='vol1_status', - size=1024, - availabilityZone='vol1_availability', - createdAt=datetime.datetime.now(), - attachments=[dict( - id='vol1_id', - volumeId='vol1_id', - serverId='instance_uuid', - device='/foo1')], - displayName='vol1_name', - displayDescription='vol1_desc', - volumeType='vol1_type', - metadata=dict( - foo='vol1_foo', - bar='vol1_bar', - ), - vsaId='vol1_vsa_id', - name='vol1_vsa_name', - ), - dict( - id='vol2_id', - status='vol2_status', - size=1024, - availabilityZone='vol2_availability', - createdAt=datetime.datetime.now(), - attachments=[dict( - id='vol2_id', - volumeId='vol2_id', - serverId='instance_uuid', - device='/foo2')], - displayName='vol2_name', - displayDescription='vol2_desc', - volumeType='vol2_type', - metadata=dict( - foo='vol2_foo', - bar='vol2_bar', - ), - vsaId='vol2_vsa_id', - name='vol2_vsa_name', - )] - text = serializer.serialize({self.objects: raw_volumes}) - - print text - tree = etree.fromstring(text) - - self.assertEqual(self.objects, tree.tag) - self.assertEqual(len(raw_volumes), len(tree)) - for idx, child in enumerate(tree): - self._verify_voldrive(raw_volumes[idx], child) - - -class VsaDriveSerializerTest(VsaVolumeSerializerTest): - show_serializer = vsa_ext.VsaDriveTemplate - index_serializer = vsa_ext.VsaDrivesTemplate - object = 'drive' - objects = 'drives' - - -class VsaVPoolSerializerTest(SerializerTestCommon): - def _verify_vpool(self, vpool, tree): - self._verify_attrs(vpool, tree, ('id', 'vsaId', 'name', 'displayName', - 'displayDescription', 'driveCount', - 'protection', 'stripeSize', - 'stripeWidth', 'createTime', - 'status')) - - self.assertEqual(1, len(tree)) - self.assertEqual('driveIds', tree[0].tag) - self.assertEqual(len(vpool['driveIds']), len(tree[0])) - for idx, gr_child in enumerate(tree[0]): - self.assertEqual('driveId', gr_child.tag) - self.assertEqual(str(vpool['driveIds'][idx]), gr_child.text) - - def test_vpool_create_show_serializer(self): - serializer = vsa_ext.VsaVPoolTemplate() - exemplar = dict( - id='vpool_id', - vsaId='vpool_vsa_id', - name='vpool_vsa_name', - displayName='vpool_display_name', - displayDescription='vpool_display_desc', - driveCount=24, - driveIds=['drive1', 'drive2', 'drive3'], - protection='protected', - stripeSize=1024, - stripeWidth=2048, - createTime=datetime.datetime.now(), - status='available') - text = serializer.serialize(dict(vpool=exemplar)) - - print text - tree = etree.fromstring(text) - - self._verify_vpool(exemplar, tree) - - def test_vpool_index_serializer(self): - serializer = vsa_ext.VsaVPoolsTemplate() - exemplar = [dict( - id='vpool1_id', - vsaId='vpool1_vsa_id', - name='vpool1_vsa_name', - displayName='vpool1_display_name', - displayDescription='vpool1_display_desc', - driveCount=24, - driveIds=['drive1', 'drive2', 'drive3'], - protection='protected', - stripeSize=1024, - stripeWidth=2048, - createTime=datetime.datetime.now(), - status='available'), - dict( - id='vpool2_id', - vsaId='vpool2_vsa_id', - name='vpool2_vsa_name', - displayName='vpool2_display_name', - displayDescription='vpool2_display_desc', - driveCount=42, - driveIds=['drive4', 'drive5', 'drive6'], - protection='protected', - stripeSize=512, - stripeWidth=256, - createTime=datetime.datetime.now(), - status='available')] - text = serializer.serialize(dict(vpools=exemplar)) - - print text - tree = etree.fromstring(text) - - self.assertEqual('vpools', tree.tag) - self.assertEqual(len(exemplar), len(tree)) - for idx, child in enumerate(tree): - self._verify_vpool(exemplar[idx], child) diff --git a/nova/tests/api/openstack/v2/contrib/test_zones.py b/nova/tests/api/openstack/v2/contrib/test_zones.py deleted file mode 100644 index a44a7c82e..000000000 --- a/nova/tests/api/openstack/v2/contrib/test_zones.py +++ /dev/null @@ -1,284 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -import json - -from lxml import etree - -from nova.api.openstack.v2.contrib import zones -from nova.api.openstack import xmlutil -from nova import crypto -import nova.db -from nova import flags -from nova.scheduler import api -from nova import test -from nova.tests.api.openstack import fakes - - -FLAGS = flags.FLAGS - - -def zone_get(context, zone_id): - return dict(id=1, api_url='http://example.com', username='bob', - password='xxx', weight_scale=1.0, weight_offset=0.0, - name='darksecret') - - -def zone_create(context, values): - zone = dict(id=1) - zone.update(values) - return zone - - -def zone_update(context, zone_id, values): - zone = dict(id=zone_id, api_url='http://example.com', username='bob', - password='xxx') - zone.update(values) - return zone - - -def zone_delete(context, zone_id): - pass - - -def zone_get_all_scheduler(*args): - return [ - dict(id=1, api_url='http://example.com', username='bob', - password='xxx', weight_scale=1.0, weight_offset=0.0), - dict(id=2, api_url='http://example.org', username='alice', - password='qwerty', weight_scale=1.0, weight_offset=0.0), - ] - - -def zone_get_all_scheduler_empty(*args): - return [] - - -def zone_get_all_db(context): - return [ - dict(id=1, api_url='http://example.com', username='bob', - password='xxx', weight_scale=1.0, weight_offset=0.0), - dict(id=2, api_url='http://example.org', username='alice', - password='qwerty', weight_scale=1.0, weight_offset=0.0), - ] - - -def zone_capabilities(method, context): - return dict() - - -GLOBAL_BUILD_PLAN = [ - dict(name='host1', weight=10, ip='10.0.0.1', zone='zone1'), - dict(name='host2', weight=9, ip='10.0.0.2', zone='zone2'), - dict(name='host3', weight=8, ip='10.0.0.3', zone='zone3'), - dict(name='host4', weight=7, ip='10.0.0.4', zone='zone4'), - ] - - -def zone_select(context, specs): - return GLOBAL_BUILD_PLAN - - -class ZonesTest(test.TestCase): - def setUp(self): - super(ZonesTest, self).setUp() - self.flags(verbose=True, allow_admin_api=True) - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - - self.stubs.Set(nova.db, 'zone_get', zone_get) - self.stubs.Set(nova.db, 'zone_update', zone_update) - self.stubs.Set(nova.db, 'zone_create', zone_create) - self.stubs.Set(nova.db, 'zone_delete', zone_delete) - - self.controller = zones.Controller() - - def test_get_zone_list_scheduler(self): - self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler) - - req = fakes.HTTPRequest.blank('/v2/fake/zones') - res_dict = self.controller.index(req) - - self.assertEqual(len(res_dict['zones']), 2) - - def test_get_zone_list_db(self): - self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler_empty) - self.stubs.Set(nova.db, 'zone_get_all', zone_get_all_db) - - req = fakes.HTTPRequest.blank('/v2/fake/zones') - res_dict = self.controller.index(req) - - self.assertEqual(len(res_dict['zones']), 2) - - def test_get_zone_by_id(self): - req = fakes.HTTPRequest.blank('/v2/fake/zones/1') - res_dict = self.controller.show(req, 1) - - self.assertEqual(res_dict['zone']['id'], 1) - self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') - self.assertFalse('password' in res_dict['zone']) - - def test_zone_delete(self): - req = fakes.HTTPRequest.blank('/v2/fake/zones/1') - self.controller.delete(req, 1) - - def test_zone_create(self): - body = dict(zone=dict(api_url='http://example.com', username='fred', - password='fubar')) - - req = fakes.HTTPRequest.blank('/v2/fake/zones') - res_dict = self.controller.create(req, body) - - self.assertEqual(res_dict['zone']['id'], 1) - self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') - self.assertFalse('username' in res_dict['zone']) - - def test_zone_update(self): - body = dict(zone=dict(username='zeb', password='sneaky')) - - req = fakes.HTTPRequest.blank('/v2/fake/zones/1') - res_dict = self.controller.update(req, 1, body) - - self.assertEqual(res_dict['zone']['id'], 1) - self.assertEqual(res_dict['zone']['api_url'], 'http://example.com') - self.assertFalse('username' in res_dict['zone']) - - def test_zone_info(self): - caps = ['cap1=a;b', 'cap2=c;d'] - self.flags(zone_name='darksecret', zone_capabilities=caps) - self.stubs.Set(api, '_call_scheduler', zone_capabilities) - - req = fakes.HTTPRequest.blank('/v2/fake/zones/info') - res_dict = self.controller.info(req) - - self.assertEqual(res_dict['zone']['name'], 'darksecret') - self.assertEqual(res_dict['zone']['cap1'], 'a;b') - self.assertEqual(res_dict['zone']['cap2'], 'c;d') - - def test_zone_select(self): - key = 'c286696d887c9aa0611bbb3e2025a45a' - self.flags(build_plan_encryption_key=key) - self.stubs.Set(api, 'select', zone_select) - - # Select queries end up being JSON encoded twice. - # Once to a string and again as an HTTP POST Body - body = json.dumps({}) - - req = fakes.HTTPRequest.blank('/v2/fake/zones/select') - res_dict = self.controller.select(req, body) - - self.assertTrue('weights' in res_dict) - - for item in res_dict['weights']: - blob = item['blob'] - decrypt = crypto.decryptor(FLAGS.build_plan_encryption_key) - secret_item = json.loads(decrypt(blob)) - found = False - for original_item in GLOBAL_BUILD_PLAN: - if original_item['name'] != secret_item['name']: - continue - found = True - for key in ('weight', 'ip', 'zone'): - self.assertEqual(secret_item[key], original_item[key]) - - self.assertTrue(found) - self.assertEqual(len(item), 2) - self.assertTrue('weight' in item) - - -class TestZonesXMLSerializer(test.TestCase): - - def test_select(self): - serializer = zones.WeightsTemplate() - - key = 'c286696d887c9aa0611bbb3e2025a45a' - - encrypt = crypto.encryptor(key) - decrypt = crypto.decryptor(key) - - item = GLOBAL_BUILD_PLAN[0] - fixture = {'weights': {'blob': encrypt(json.dumps(item)), - 'weight': item['weight']}} - - output = serializer.serialize(fixture) - res_tree = etree.XML(output) - - self.assertEqual(res_tree.tag, '{%s}weights' % xmlutil.XMLNS_V10) - - for item in res_tree: - self.assertEqual(item.tag, '{%s}weight' % xmlutil.XMLNS_V10) - blob = None - weight = None - for chld in item: - if chld.tag.endswith('blob'): - blob = chld.text - elif chld.tag.endswith('weight'): - weight = chld.text - - secret_item = json.loads(decrypt(blob)) - found = False - for original_item in GLOBAL_BUILD_PLAN: - if original_item['name'] != secret_item['name']: - continue - found = True - for key in ('weight', 'ip', 'zone'): - self.assertEqual(secret_item[key], original_item[key]) - - self.assertTrue(found) - self.assertEqual(len(item), 2) - self.assertTrue(weight) - - def test_index(self): - serializer = zones.ZonesTemplate() - - fixture = {'zones': zone_get_all_scheduler()} - - output = serializer.serialize(fixture) - res_tree = etree.XML(output) - - self.assertEqual(res_tree.tag, '{%s}zones' % xmlutil.XMLNS_V10) - self.assertEqual(len(res_tree), 2) - self.assertEqual(res_tree[0].tag, '{%s}zone' % xmlutil.XMLNS_V10) - self.assertEqual(res_tree[1].tag, '{%s}zone' % xmlutil.XMLNS_V10) - - def test_show(self): - serializer = zones.ZoneTemplate() - - zone = {'id': 1, - 'api_url': 'http://example.com', - 'name': 'darksecret', - 'cap1': 'a;b', - 'cap2': 'c;d'} - fixture = {'zone': zone} - - output = serializer.serialize(fixture) - print repr(output) - res_tree = etree.XML(output) - - self.assertEqual(res_tree.tag, '{%s}zone' % xmlutil.XMLNS_V10) - self.assertEqual(res_tree.get('id'), '1') - self.assertEqual(res_tree.get('api_url'), 'http://example.com') - self.assertEqual(res_tree.get('password'), None) - - self.assertEqual(res_tree.get('name'), 'darksecret') - for elem in res_tree: - self.assertEqual(elem.tag in ('{%s}cap1' % xmlutil.XMLNS_V10, - '{%s}cap2' % xmlutil.XMLNS_V10), - True) - 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') diff --git a/nova/tests/api/openstack/v2/extensions/__init__.py b/nova/tests/api/openstack/v2/extensions/__init__.py deleted file mode 100644 index 848908a95..000000000 --- a/nova/tests/api/openstack/v2/extensions/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC -# -# 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. diff --git a/nova/tests/api/openstack/v2/extensions/foxinsocks.py b/nova/tests/api/openstack/v2/extensions/foxinsocks.py deleted file mode 100644 index ba1508668..000000000 --- a/nova/tests/api/openstack/v2/extensions/foxinsocks.py +++ /dev/null @@ -1,93 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob.exc - -from nova.api.openstack.v2 import extensions - - -class FoxInSocksController(object): - - def index(self, req): - return "Try to say this Mr. Knox, sir..." - - -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_actions(self): - actions = [] - actions.append(extensions.ActionExtension('servers', 'add_tweedle', - self._add_tweedle)) - actions.append(extensions.ActionExtension('servers', 'delete_tweedle', - self._delete_tweedle)) - actions.append(extensions.ActionExtension('servers', 'fail', - self._fail)) - return actions - - def get_request_extensions(self): - request_exts = [] - - def _goose_handler(req, res, body): - #NOTE: This only handles JSON responses. - # You can use content type header to test for XML. - body['flavor']['googoose'] = req.GET.get('chewing') - return res - - req_ext1 = extensions.RequestExtension('GET', - '/v2/:(project_id)/flavors/:(id)', - _goose_handler) - request_exts.append(req_ext1) - - def _bands_handler(req, res, body): - #NOTE: This only handles JSON responses. - # You can use content type header to test for XML. - body['big_bands'] = 'Pig Bands!' - return res - - req_ext2 = extensions.RequestExtension('GET', - '/v2/:(project_id)/flavors/:(id)', - _bands_handler) - request_exts.append(req_ext2) - return request_exts - - def _add_tweedle(self, input_dict, req, id): - - return "Tweedle Beetle Added." - - def _delete_tweedle(self, input_dict, req, id): - - return "Tweedle Beetle Deleted." - - def _fail(self, input_dict, req, id): - - raise webob.exc.HTTPBadRequest(explanation='Tweedle fail') diff --git a/nova/tests/api/openstack/v2/test_api.py b/nova/tests/api/openstack/v2/test_api.py deleted file mode 100644 index d09bb0cfa..000000000 --- a/nova/tests/api/openstack/v2/test_api.py +++ /dev/null @@ -1,121 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json - -from lxml import etree -import webob.exc -import webob.dec -from webob import Request - -from nova import test -from nova.api.openstack import v2 -from nova.api.openstack.v2 import wsgi -from nova.tests.api.openstack import fakes - - -class APITest(test.TestCase): - - def _wsgi_app(self, inner_app): - # simpler version of the app than fakes.wsgi_app - return v2.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 = '' - 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) - - body = json.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) - - body = 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 = 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 = Request.blank('/').get_response(api) - self.assertTrue('itemNotFound' in resp.body, 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 = Request.blank('/').get_response(api) - self.assertTrue('{"computeFault' in resp.body, 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 = Request.blank('/.xml').get_response(api) - self.assertTrue(' 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, - "local_gb": 0, - "hostname": "", - "host": host, - "instance_type": {}, - "user_data": "", - "reservation_id": reservation_id, - "mac_address": "", - "scheduled_at": utils.utcnow(), - "launched_at": utils.utcnow(), - "terminated_at": utils.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.TestCase): - 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(utils.gen_uuid()) - 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, 'create_console', fake_create_console) - - req = fakes.HTTPRequest.blank(self.url) - self.controller.create(req, self.uuid) - - 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, 'get_console', fake_get_console) - - req = fakes.HTTPRequest.blank(self.url + '/20') - res_dict = self.controller.show(req, self.uuid, '20') - self.assertDictMatch(res_dict, 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, '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, '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, 'get_consoles', fake_get_consoles) - - req = fakes.HTTPRequest.blank(self.url) - res_dict = self.controller.index(req, self.uuid) - self.assertDictMatch(res_dict, 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, 'get_console', fake_get_console) - self.stubs.Set(console.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, '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, '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.TestCase): - 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/api/openstack/v2/test_extensions.py b/nova/tests/api/openstack/v2/test_extensions.py deleted file mode 100644 index f2a49c3ed..000000000 --- a/nova/tests/api/openstack/v2/test_extensions.py +++ /dev/null @@ -1,557 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json - -import webob -from lxml import etree - -from nova.api.openstack import v2 -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes -from nova import wsgi as base_wsgi - -FLAGS = flags.FLAGS - -NS = "{http://docs.openstack.org/compute/api/v1.1}" -ATOMNS = "{http://www.w3.org/2005/Atom}" -response_body = "Try to say this Mr. Knox, sir..." - - -class StubController(object): - - def __init__(self, body): - self.body = body - - def index(self, req): - return self.body - - def create(self, req): - msg = 'All aboard the fail train!' - raise webob.exc.HTTPBadRequest(explanation=msg) - - def show(self, req, id): - raise webob.exc.HTTPNotFound() - - -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): - self.resource_ext = resource_ext - self.action_ext = action_ext - self.request_ext = request_ext - - def get_resources(self): - resource_exts = [] - if self.resource_ext: - resource_exts.append(self.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 - - -class ExtensionTestCase(test.TestCase): - def setUp(self): - super(ExtensionTestCase, self).setUp() - ext_list = FLAGS.osapi_extension[:] - ext_list.append('nova.tests.api.openstack.v2.extensions.' - 'foxinsocks.Foxinsocks') - self.flags(osapi_extension=ext_list) - extensions.ExtensionManager.reset() - - -class ExtensionControllerTest(ExtensionTestCase): - - def setUp(self): - super(ExtensionControllerTest, self).setUp() - self.flags(allow_admin_api=True) - self.ext_list = [ - "Accounts", - "AdminActions", - "Cloudpipe", - "Console_output", - "Createserverext", - "DeferredDelete", - "DiskConfig", - "ExtendedStatus", - "FlavorExtraSpecs", - "FlavorExtraData", - "Floating_ips", - "Floating_ip_dns", - "Floating_ip_pools", - "Fox In Socks", - "Hosts", - "Keypairs", - "Multinic", - "Quotas", - "Rescue", - "SecurityGroups", - "ServerActionList", - "ServerDiagnostics", - "SimpleTenantUsage", - "Users", - "VSAs", - "VirtualInterfaces", - "Volumes", - "VolumeTypes", - "Zones", - "Networks", - ] - self.ext_list.sort() - - def test_list_extensions_json(self): - app = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/fake/extensions") - response = request.get_response(ser_midware) - self.assertEqual(200, response.status_int) - - # Make sure we have all the extensions. - data = json.loads(response.body) - names = [x['name'] for x in data['extensions']] - names.sort() - self.assertEqual(names, self.ext_list) - - # 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': [] - }, - ) - - def test_get_extension_json(self): - app = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/fake/extensions/FOXNSOX") - response = request.get_response(ser_midware) - self.assertEqual(200, response.status_int) - - data = json.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 = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - request = webob.Request.blank("/fake/extensions/4") - response = request.get_response(ext_midware) - self.assertEqual(404, response.status_int) - - def test_list_extensions_xml(self): - app = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/fake/extensions") - request.accept = "application/xml" - response = request.get_response(ser_midware) - self.assertEqual(200, response.status_int) - print response.body - - root = etree.XML(response.body) - self.assertEqual(root.tag.split('extensions')[0], NS) - - # Make sure we have all the extensions. - exts = root.findall('{0}extension'.format(NS)) - self.assertEqual(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 = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/fake/extensions/FOXNSOX") - request.accept = "application/xml" - response = request.get_response(ser_midware) - self.assertEqual(200, response.status_int) - xml = response.body - print xml - - 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 = v2.APIRouter(manager) - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/blah") - response = request.get_response(ser_midware) - self.assertEqual(404, response.status_int) - - def test_get_resources(self): - res_ext = extensions.ResourceExtension('tweedles', - StubController(response_body)) - manager = StubExtensionManager(res_ext) - app = v2.APIRouter(manager) - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/fake/tweedles") - response = request.get_response(ser_midware) - self.assertEqual(200, response.status_int) - self.assertEqual(response_body, response.body) - - def test_get_resources_with_controller(self): - res_ext = extensions.ResourceExtension('tweedles', - StubController(response_body)) - manager = StubExtensionManager(res_ext) - app = v2.APIRouter(manager) - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/fake/tweedles") - response = request.get_response(ser_midware) - self.assertEqual(200, response.status_int) - self.assertEqual(response_body, response.body) - - def test_bad_request(self): - res_ext = extensions.ResourceExtension('tweedles', - StubController(response_body)) - manager = StubExtensionManager(res_ext) - app = v2.APIRouter(manager) - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/fake/tweedles") - request.method = "POST" - response = request.get_response(ser_midware) - self.assertEqual(400, response.status_int) - self.assertEqual('application/json', response.content_type) - body = json.loads(response.body) - expected = { - "badRequest": { - "message": "All aboard the fail train!", - "code": 400 - } - } - self.assertDictMatch(expected, body) - - def test_non_exist_resource(self): - res_ext = extensions.ResourceExtension('tweedles', - StubController(response_body)) - manager = StubExtensionManager(res_ext) - app = v2.APIRouter(manager) - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/fake/tweedles/1") - response = request.get_response(ser_midware) - self.assertEqual(404, response.status_int) - self.assertEqual('application/json', response.content_type) - body = json.loads(response.body) - expected = { - "itemNotFound": { - "message": "The resource could not be found.", - "code": 404 - } - } - self.assertDictMatch(expected, body) - - -class InvalidExtension(object): - - alias = "THIRD" - - -class AdminExtension(extensions.ExtensionDescriptor): - """Admin-only extension""" - - name = "Admin Ext" - alias = "ADMIN" - namespace = "http://www.example.com/" - updated = "2011-01-22T13:25:27-06:00" - admin_only = True - - def __init__(self, *args, **kwargs): - pass - - -class ExtensionManagerTest(ExtensionTestCase): - - response_body = "Try to say this Mr. Knox, sir..." - - def test_get_resources(self): - app = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/fake/foxnsocks") - response = request.get_response(ser_midware) - 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 - app = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ext_mgr = ext_midware.ext_mgr - ext_mgr.register(InvalidExtension()) - self.assertTrue('FOXNSOX' in ext_mgr.extensions) - self.assertTrue('THIRD' not in ext_mgr.extensions) - - def test_admin_extensions(self): - self.flags(allow_admin_api=True) - app = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ext_mgr = ext_midware.ext_mgr - ext_mgr.register(AdminExtension()) - self.assertTrue('FOXNSOX' in ext_mgr.extensions) - self.assertTrue('ADMIN' in ext_mgr.extensions) - - def test_admin_extensions_no_admin_api(self): - self.flags(allow_admin_api=False) - app = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ext_mgr = ext_midware.ext_mgr - ext_mgr.register(AdminExtension()) - self.assertTrue('FOXNSOX' in ext_mgr.extensions) - self.assertTrue('ADMIN' not in ext_mgr.extensions) - - -class ActionExtensionTest(ExtensionTestCase): - - def _send_server_action_request(self, url, body): - app = v2.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank(url) - request.method = 'POST' - request.content_type = 'application/json' - request.body = json.dumps(body) - response = request.get_response(ser_midware) - 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 = json.loads(response.body) - expected = { - "badRequest": { - "message": "There is no such server action: blah", - "code": 400 - } - } - self.assertDictMatch(expected, 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 = json.loads(response.body) - expected = { - "badRequest": { - "message": "Tweedle fail", - "code": 400 - } - } - self.assertDictMatch(expected, body) - - -class RequestExtensionTest(ExtensionTestCase): - - def test_get_resources_with_stub_mgr(self): - - def _req_handler(req, res, body): - # only handle JSON responses - body['flavor']['googoose'] = req.GET.get('chewing') - return res - - req_ext = extensions.RequestExtension('GET', - '/v2/fake/flavors/:(id)', - _req_handler) - - manager = StubExtensionManager(None, None, req_ext) - app = fakes.wsgi_app(serialization=base_wsgi.Middleware) - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/v2/fake/flavors/1?chewing=bluegoo") - request.environ['api.version'] = '2' - response = request.get_response(ser_midware) - self.assertEqual(200, response.status_int) - response_data = json.loads(response.body) - self.assertEqual('bluegoo', response_data['flavor']['googoose']) - - def test_get_resources_with_mgr(self): - - app = fakes.wsgi_app(serialization=base_wsgi.Middleware) - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/v2/fake/flavors/1?chewing=newblue") - request.environ['api.version'] = '2' - response = request.get_response(ser_midware) - self.assertEqual(200, response.status_int) - response_data = json.loads(response.body) - print response_data - self.assertEqual('newblue', response_data['flavor']['googoose']) - self.assertEqual("Pig Bands!", response_data['big_bands']) - - -class ExtensionsXMLSerializerTest(test.TestCase): - - def test_serialize_extension(self): - serializer = extensions.ExtensionsXMLSerializer() - 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, 'show') - print xml - 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 = extensions.ExtensionsXMLSerializer() - 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", - "type": "application/vnd.sun.wadl+xml", - "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, 'index') - print xml - 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') diff --git a/nova/tests/api/openstack/v2/test_flavors.py b/nova/tests/api/openstack/v2/test_flavors.py deleted file mode 100644 index d70d581df..000000000 --- a/nova/tests/api/openstack/v2/test_flavors.py +++ /dev/null @@ -1,652 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from lxml import etree -import webob - -from nova.api.openstack.v2 import flavors -from nova.api.openstack import xmlutil -import nova.compute.instance_types -from nova import exception -from nova import test -from nova.tests.api.openstack import fakes - - -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', - "local_gb": '10' - }, - 'flavor 2': { - "flavorid": '2', - "name": 'flavor 2', - "memory_mb": '512', - "local_gb": '20' - }, -} - - -def fake_instance_type_get_by_flavor_id(flavorid): - return FAKE_FLAVORS['flavor %s' % flavorid] - - -def fake_instance_type_get_all(inactive=False, filters=None): - def reject_min(db_attr, filter_attr): - return filter_attr in filters and\ - int(flavor[db_attr]) < int(filters[filter_attr]) - - filters = filters or {} - output = {} - for (flavor_name, flavor) in FAKE_FLAVORS.items(): - if reject_min('memory_mb', 'min_memory_mb'): - continue - elif reject_min('local_gb', 'min_local_gb'): - continue - - output[flavor_name] = flavor - - return output - - -def empty_instance_type_get_all(inactive=False, filters=None): - return {} - - -def return_instance_type_not_found(flavor_id): - raise exception.InstanceTypeNotFound(flavor_id=flavor_id) - - -class FlavorsTest(test.TestCase): - def setUp(self): - super(FlavorsTest, self).setUp() - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - self.stubs.Set(nova.compute.instance_types, "get_all_types", - fake_instance_type_get_all) - self.stubs.Set(nova.compute.instance_types, - "get_instance_type_by_flavor_id", - fake_instance_type_get_by_flavor_id) - - self.controller = flavors.Controller() - - def tearDown(self): - self.stubs.UnsetAll() - super(FlavorsTest, self).tearDown() - - def test_get_flavor_by_invalid_id(self): - self.stubs.Set(nova.compute.instance_types, - "get_instance_type_by_flavor_id", - return_instance_type_not_found) - req = fakes.HTTPRequest.blank('/v2/fake/flavors/asdf') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.show, req, 'asdf') - - def test_get_flavor_by_id(self): - req = fakes.HTTPRequest.blank('/v2/fake/flavors/1') - flavor = self.controller.show(req, '1') - expected = { - "flavor": { - "id": "1", - "name": "flavor 1", - "ram": "256", - "disk": "10", - "rxtx_factor": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/1", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/1", - }, - ], - }, - } - self.assertEqual(flavor, expected) - - def test_get_flavor_list(self): - req = fakes.HTTPRequest.blank('/v2/fake/flavors') - flavor = self.controller.index(req) - 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", - }, - ], - }, - { - "id": "2", - "name": "flavor 2", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/2", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/2", - }, - ], - }, - ], - } - self.assertEqual(flavor, expected) - - def test_get_flavor_list_detail(self): - req = fakes.HTTPRequest.blank('/v2/fake/flavors/detail') - flavor = self.controller.detail(req) - expected = { - "flavors": [ - { - "id": "1", - "name": "flavor 1", - "ram": "256", - "disk": "10", - "rxtx_factor": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/1", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/1", - }, - ], - }, - { - "id": "2", - "name": "flavor 2", - "ram": "512", - "disk": "20", - "rxtx_factor": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/2", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/2", - }, - ], - }, - ], - } - self.assertEqual(flavor, expected) - - def test_get_empty_flavor_list(self): - self.stubs.Set(nova.compute.instance_types, "get_all_types", - empty_instance_type_get_all) - - req = fakes.HTTPRequest.blank('/v2/fake/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 = fakes.HTTPRequest.blank('/v2/fake/flavors?minRam=512') - flavor = self.controller.index(req) - expected = { - "flavors": [ - { - "id": "2", - "name": "flavor 2", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/2", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/2", - }, - ], - }, - ], - } - self.assertEqual(flavor, expected) - - def test_get_flavor_list_filter_min_disk(self): - """Flavor lists may be filtered by minRam""" - req = fakes.HTTPRequest.blank('/v2/fake/flavors?minDisk=20') - flavor = self.controller.index(req) - expected = { - "flavors": [ - { - "id": "2", - "name": "flavor 2", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/2", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/2", - }, - ], - }, - ], - } - self.assertEqual(flavor, expected) - - 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 = fakes.HTTPRequest.blank('/v2/fake/flavors/detail' - '?minRam=256&minDisk=20') - flavor = self.controller.detail(req) - expected = { - "flavors": [ - { - "id": "2", - "name": "flavor 2", - "ram": "512", - "disk": "20", - "rxtx_factor": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/2", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/2", - }, - ], - }, - ], - } - self.assertEqual(flavor, expected) - - def test_get_flavor_list_detail_bogus_min_ram(self): - """Tests that bogus minRam filtering values are ignored""" - req = fakes.HTTPRequest.blank('/v2/fake/flavors/detail?minRam=16GB') - flavor = self.controller.detail(req) - expected = { - "flavors": [ - { - "id": "1", - "name": "flavor 1", - "ram": "256", - "disk": "10", - "rxtx_factor": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/1", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/1", - }, - ], - }, - { - "id": "2", - "name": "flavor 2", - "ram": "512", - "disk": "20", - "rxtx_factor": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/2", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/2", - }, - ], - }, - ], - } - self.assertEqual(flavor, expected) - - def test_get_flavor_list_detail_bogus_min_disk(self): - """Tests that bogus minDisk filtering values are ignored""" - req = fakes.HTTPRequest.blank('/v2/fake/flavors/detail?minDisk=16GB') - flavor = self.controller.detail(req) - expected = { - "flavors": [ - { - "id": "1", - "name": "flavor 1", - "ram": "256", - "disk": "10", - "rxtx_factor": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/1", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/1", - }, - ], - }, - { - "id": "2", - "name": "flavor 2", - "ram": "512", - "disk": "20", - "rxtx_factor": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/2", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/2", - }, - ], - }, - ], - } - self.assertEqual(flavor, expected) - - -class FlavorsXMLSerializationTest(test.TestCase): - - def test_xml_declaration(self): - serializer = flavors.FlavorTemplate() - - fixture = { - "flavor": { - "id": "12", - "name": "asdf", - "ram": "256", - "disk": "10", - "rxtx_factor": "1", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/12", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/12", - }, - ], - }, - } - - output = serializer.serialize(fixture) - print output - has_dec = output.startswith("") - self.assertTrue(has_dec) - - def test_show(self): - serializer = flavors.FlavorTemplate() - - fixture = { - "flavor": { - "id": "12", - "name": "asdf", - "ram": "256", - "disk": "10", - "rxtx_factor": "1", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/12", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/12", - }, - ], - }, - } - - output = serializer.serialize(fixture) - print output - 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.FlavorTemplate() - - fixture = { - "flavor": { - "id": 12, - "name": "asdf", - "ram": 256, - "disk": 10, - "rxtx_factor": "1", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/12", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/12", - }, - ], - }, - } - - output = serializer.serialize(fixture) - print output - 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.FlavorsTemplate() - - fixture = { - "flavors": [ - { - "id": "23", - "name": "flavor 23", - "ram": "512", - "disk": "20", - "rxtx_factor": "1", - "swap": "", - "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", - "rxtx_factor": "1", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/13", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/13", - }, - ], - }, - ], - } - - output = serializer.serialize(fixture) - print output - 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.MinimalFlavorsTemplate() - - fixture = { - "flavors": [ - { - "id": "23", - "name": "flavor 23", - "ram": "512", - "disk": "20", - "rxtx_factor": "1", - "swap": "", - "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", - "rxtx_factor": "1", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/flavors/13", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/13", - }, - ], - }, - ], - } - - output = serializer.serialize(fixture) - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'flavors_index') - 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.MinimalFlavorsTemplate() - - fixture = { - "flavors": [], - } - - output = serializer.serialize(fixture) - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'flavors_index') - flavor_elems = root.findall('{0}flavor'.format(NS)) - self.assertEqual(len(flavor_elems), 0) diff --git a/nova/tests/api/openstack/v2/test_image_metadata.py b/nova/tests/api/openstack/v2/test_image_metadata.py deleted file mode 100644 index e3e774ba3..000000000 --- a/nova/tests/api/openstack/v2/test_image_metadata.py +++ /dev/null @@ -1,200 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json -import webob - -from nova.api.openstack.v2 import image_metadata -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes - - -FLAGS = flags.FLAGS - - -class ImageMetaDataTest(test.TestCase): - - def setUp(self): - super(ImageMetaDataTest, self).setUp() - fakes.stub_out_glance(self.stubs) - self.controller = image_metadata.Controller() - - def test_index(self): - 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) - - def test_show(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') - res_dict = self.controller.show(req, '123', 'key1') - self.assertTrue('meta' in res_dict) - self.assertEqual(len(res_dict['meta']), 1) - self.assertEqual('value1', res_dict['meta']['key1']) - - def test_show_not_found(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key9') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.show, req, '123', 'key9') - - def test_show_image_not_found(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.show, req, '100', 'key9') - - def test_create(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') - req.method = 'POST' - body = {"metadata": {"key7": "value7"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, '123', body) - - expected_output = {'metadata': {'key1': 'value1', 'key7': 'value7'}} - self.assertEqual(expected_output, res) - - def test_create_image_not_found(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata') - req.method = 'POST' - body = {"metadata": {"key7": "value7"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.create, req, '100', body) - - def test_update_all(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') - req.method = 'PUT' - body = {"metadata": {"key9": "value9"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.update_all(req, '123', body) - - expected_output = {'metadata': {'key9': 'value9'}} - self.assertEqual(expected_output, res) - - def test_update_all_image_not_found(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata') - req.method = 'PUT' - body = {"metadata": {"key9": "value9"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.update_all, req, '100', body) - - def test_update_item(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') - req.method = 'PUT' - body = {"meta": {"key1": "zz"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.update(req, '123', 'key1', body) - - expected_output = {'meta': {'key1': 'zz'}} - self.assertEqual(res, expected_output) - - def test_update_item_image_not_found(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1') - req.method = 'PUT' - body = {"meta": {"key1": "zz"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.update, req, '100', 'key1', body) - - def test_update_item_bad_body(self): - 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) - - def test_update_item_too_many_keys(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') - req.method = 'PUT' - overload = {} - for num in range(FLAGS.quota_metadata_items + 1): - overload['key%s' % num] = 'value%s' % num - body = {'meta': overload} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.update, req, '123', 'key1', body) - - def test_update_item_body_uri_mismatch(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/bad') - req.method = 'PUT' - body = {"meta": {"key1": "value1"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.update, req, '123', 'bad', body) - - def test_delete(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') - req.method = 'DELETE' - res = self.controller.delete(req, '123', 'key1') - - self.assertEqual(None, res) - - def test_delete_not_found(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/blah') - req.method = 'DELETE' - - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.delete, req, '123', 'blah') - - def test_delete_image_not_found(self): - req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1') - req.method = 'DELETE' - - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.delete, req, '100', 'key1') - - def test_too_many_metadata_items_on_create(self): - data = {"metadata": {}} - for num in range(FLAGS.quota_metadata_items + 1): - data['metadata']['key%i' % num] = "blah" - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') - req.method = 'POST' - req.body = json.dumps(data) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, - self.controller.create, req, '123', data) - self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, - self.controller.create, req, '123', data) - - def test_too_many_metadata_items_on_put(self): - FLAGS.quota_metadata_items = 1 - req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/blah') - req.method = 'PUT' - body = {"meta": {"blah": "blah"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, - self.controller.update, req, '123', 'blah', body) diff --git a/nova/tests/api/openstack/v2/test_images.py b/nova/tests/api/openstack/v2/test_images.py deleted file mode 100644 index 70c6db679..000000000 --- a/nova/tests/api/openstack/v2/test_images.py +++ /dev/null @@ -1,1646 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Tests of the new image services, both as a service layer, -and as a WSGI layer -""" - -import urlparse - -from lxml import etree -import stubout -import webob - -from nova.api.openstack.v2 import images -from nova.api.openstack.v2.views import images as images_view -from nova.api.openstack import xmlutil -from nova import test -from nova import utils -from nova.tests.api.openstack import fakes - - -NS = "{http://docs.openstack.org/compute/api/v1.1}" -ATOMNS = "{http://www.w3.org/2005/Atom}" -NOW_API_FORMAT = "2010-10-11T10:30:22Z" - - -class ImagesControllerTest(test.TestCase): - """ - Test of the OpenStack API /images application controller w/Glance. - """ - - def setUp(self): - """Run before each test.""" - super(ImagesControllerTest, self).setUp() - self.maxDiff = None - self.stubs = stubout.StubOutForTesting() - 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) - fakes.stub_out_glance(self.stubs) - - self.controller = images.Controller() - - def tearDown(self): - """Run after each test.""" - self.stubs.UnsetAll() - super(ImagesControllerTest, self).tearDown() - - def test_get_image(self): - fake_req = fakes.HTTPRequest.blank('/v2/fake/images/123') - actual_image = self.controller.show(fake_req, '124') - - href = "http://localhost/v2/fake/images/124" - bookmark = "http://localhost/fake/images/124" - alternate = "%s/fake/images/124" % utils.generate_glance_url() - server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" - server_href = "http://localhost/v2/servers/" + server_uuid - server_bookmark = "http://localhost/servers/" + server_uuid - - expected_image = { - "image": { - "id": "124", - "name": "queued snapshot", - "updated": NOW_API_FORMAT, - "created": NOW_API_FORMAT, - "status": "SAVING", - "progress": 25, - "minDisk": 0, - "minRam": 0, - 'server': { - 'id': server_uuid, - "links": [{ - "rel": "self", - "href": server_href, - }, - { - "rel": "bookmark", - "href": server_bookmark, - }], - }, - "metadata": { - "instance_ref": server_href, - "user_id": "fake", - }, - "links": [{ - "rel": "self", - "href": href, - }, - { - "rel": "bookmark", - "href": bookmark, - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": alternate - }], - }, - } - - self.assertDictMatch(expected_image, actual_image) - - def test_get_image_404(self): - fake_req = fakes.HTTPRequest.blank('/v2/fake/images/unknown') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.show, fake_req, 'unknown') - - def test_get_image_index(self): - fake_req = fakes.HTTPRequest.blank('/v2/fake/images') - response_list = self.controller.index(fake_req)['images'] - - expected_images = [ - { - "id": "123", - "name": "public image", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/123", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/123", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/123" % - utils.generate_glance_url() - }, - ], - }, - { - "id": "124", - "name": "queued snapshot", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/124", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/124", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/124" % - utils.generate_glance_url() - }, - ], - }, - { - "id": "125", - "name": "saving snapshot", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/125", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/125", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/125" % - utils.generate_glance_url() - }, - ], - }, - { - "id": "126", - "name": "active snapshot", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/126", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/126", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/126" % - utils.generate_glance_url() - }, - ], - }, - { - "id": "127", - "name": "killed snapshot", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/127", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/127", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/127" % - utils.generate_glance_url() - }, - ], - }, - { - "id": "128", - "name": "deleted snapshot", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/128", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/128", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/128" % - utils.generate_glance_url() - }, - ], - }, - { - "id": "129", - "name": "pending_delete snapshot", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/129", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/129", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/129" % - utils.generate_glance_url() - }, - ], - }, - { - "id": "130", - "name": None, - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/130", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/130", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/130" % - utils.generate_glance_url() - }, - ], - }, - ] - - self.assertDictListMatch(response_list, expected_images) - - def test_get_image_index_with_limit(self): - request = fakes.HTTPRequest.blank('/v2/fake/images?limit=3') - response = self.controller.index(request) - response_list = response["images"] - response_links = response["images_links"] - - alternate = "%s/fake/images/%s" - - expected_images = [ - { - "id": "123", - "name": "public image", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/123", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/123", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": alternate % (utils.generate_glance_url(), 123), - }, - ], - }, - { - "id": "124", - "name": "queued snapshot", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/124", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/124", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": alternate % (utils.generate_glance_url(), 124), - }, - ], - }, - { - "id": "125", - "name": "saving snapshot", - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/images/125", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/125", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": alternate % (utils.generate_glance_url(), 125), - }, - ], - }, - ] - - self.assertDictListMatch(response_list, expected_images) - self.assertEqual(response_links[0]['rel'], 'next') - - href_parts = urlparse.urlparse(response_links[0]['href']) - self.assertEqual('/v2/fake/images', href_parts.path) - params = urlparse.parse_qs(href_parts.query) - self.assertDictMatch({'limit': ['3'], 'marker': ['125']}, params) - - def test_get_image_index_with_limit_and_extra_params(self): - request = fakes.HTTPRequest.blank('/v2/fake/images?limit=3&extra=bo') - response = self.controller.index(request) - response_links = response["images_links"] - - self.assertEqual(response_links[0]['rel'], 'next') - - href_parts = urlparse.urlparse(response_links[0]['href']) - self.assertEqual('/v2/fake/images', href_parts.path) - params = urlparse.parse_qs(href_parts.query) - self.assertDictMatch( - {'limit': ['3'], 'marker': ['125'], 'extra': ['bo']}, - params) - - def test_get_image_index_with_big_limit(self): - """ - Make sure we don't get images_links if limit is set - and the number of images returned is < limit - """ - request = fakes.HTTPRequest.blank('/v2/fake/images?limit=30') - response = self.controller.index(request) - - self.assertEqual(response.keys(), ['images']) - self.assertEqual(len(response['images']), 8) - - def test_get_image_details(self): - request = fakes.HTTPRequest.blank('/v2/fake/images/detail') - response = self.controller.detail(request) - response_list = response["images"] - - server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" - server_href = "http://localhost/v2/servers/" + server_uuid - server_bookmark = "http://localhost/servers/" + server_uuid - alternate = "%s/fake/images/%s" - - expected = [{ - 'id': '123', - 'name': 'public image', - 'metadata': {'key1': 'value1'}, - 'updated': NOW_API_FORMAT, - 'created': NOW_API_FORMAT, - 'status': 'ACTIVE', - 'progress': 100, - 'minDisk': 10, - 'minRam': 128, - "links": [{ - "rel": "self", - "href": "http://localhost/v2/fake/images/123", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/123", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": alternate % (utils.generate_glance_url(), 123), - }], - }, - { - 'id': '124', - 'name': 'queued snapshot', - 'metadata': { - u'instance_ref': server_href, - u'user_id': u'fake', - }, - 'updated': NOW_API_FORMAT, - 'created': NOW_API_FORMAT, - 'status': 'SAVING', - 'progress': 25, - 'minDisk': 0, - 'minRam': 0, - 'server': { - 'id': server_uuid, - "links": [{ - "rel": "self", - "href": server_href, - }, - { - "rel": "bookmark", - "href": server_bookmark, - }], - }, - "links": [{ - "rel": "self", - "href": "http://localhost/v2/fake/images/124", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/124", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": alternate % (utils.generate_glance_url(), 124), - }], - }, - { - 'id': '125', - 'name': 'saving snapshot', - 'metadata': { - u'instance_ref': server_href, - u'user_id': u'fake', - }, - 'updated': NOW_API_FORMAT, - 'created': NOW_API_FORMAT, - 'status': 'SAVING', - 'progress': 50, - 'minDisk': 0, - 'minRam': 0, - 'server': { - 'id': server_uuid, - "links": [{ - "rel": "self", - "href": server_href, - }, - { - "rel": "bookmark", - "href": server_bookmark, - }], - }, - "links": [{ - "rel": "self", - "href": "http://localhost/v2/fake/images/125", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/125", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/125" % utils.generate_glance_url() - }], - }, - { - 'id': '126', - 'name': 'active snapshot', - 'metadata': { - u'instance_ref': server_href, - u'user_id': u'fake', - }, - 'updated': NOW_API_FORMAT, - 'created': NOW_API_FORMAT, - 'status': 'ACTIVE', - 'progress': 100, - 'minDisk': 0, - 'minRam': 0, - 'server': { - 'id': server_uuid, - "links": [{ - "rel": "self", - "href": server_href, - }, - { - "rel": "bookmark", - "href": server_bookmark, - }], - }, - "links": [{ - "rel": "self", - "href": "http://localhost/v2/fake/images/126", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/126", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/126" % utils.generate_glance_url() - }], - }, - { - 'id': '127', - 'name': 'killed snapshot', - 'metadata': { - u'instance_ref': server_href, - u'user_id': u'fake', - }, - 'updated': NOW_API_FORMAT, - 'created': NOW_API_FORMAT, - 'status': 'ERROR', - 'progress': 0, - 'minDisk': 0, - 'minRam': 0, - 'server': { - 'id': server_uuid, - "links": [{ - "rel": "self", - "href": server_href, - }, - { - "rel": "bookmark", - "href": server_bookmark, - }], - }, - "links": [{ - "rel": "self", - "href": "http://localhost/v2/fake/images/127", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/127", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/127" % utils.generate_glance_url() - }], - }, - { - 'id': '128', - 'name': 'deleted snapshot', - 'metadata': { - u'instance_ref': server_href, - u'user_id': u'fake', - }, - 'updated': NOW_API_FORMAT, - 'created': NOW_API_FORMAT, - 'status': 'DELETED', - 'progress': 0, - 'minDisk': 0, - 'minRam': 0, - 'server': { - 'id': server_uuid, - "links": [{ - "rel": "self", - "href": server_href, - }, - { - "rel": "bookmark", - "href": server_bookmark, - }], - }, - "links": [{ - "rel": "self", - "href": "http://localhost/v2/fake/images/128", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/128", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/128" % utils.generate_glance_url() - }], - }, - { - 'id': '129', - 'name': 'pending_delete snapshot', - 'metadata': { - u'instance_ref': server_href, - u'user_id': u'fake', - }, - 'updated': NOW_API_FORMAT, - 'created': NOW_API_FORMAT, - 'status': 'DELETED', - 'progress': 0, - 'minDisk': 0, - 'minRam': 0, - 'server': { - 'id': server_uuid, - "links": [{ - "rel": "self", - "href": server_href, - }, - { - "rel": "bookmark", - "href": server_bookmark, - }], - }, - "links": [{ - "rel": "self", - "href": "http://localhost/v2/fake/images/129", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/129", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/129" % utils.generate_glance_url() - }], - }, - { - 'id': '130', - 'name': None, - 'metadata': {}, - 'updated': NOW_API_FORMAT, - 'created': NOW_API_FORMAT, - 'status': 'ACTIVE', - 'progress': 100, - 'minDisk': 0, - 'minRam': 0, - "links": [{ - "rel": "self", - "href": "http://localhost/v2/fake/images/130", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/130", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": "%s/fake/images/130" % utils.generate_glance_url() - }], - }, - ] - - self.assertDictListMatch(expected, response_list) - - def test_get_image_details_with_limit(self): - request = fakes.HTTPRequest.blank('/v2/fake/images/detail?limit=2') - response = self.controller.detail(request) - response_list = response["images"] - response_links = response["images_links"] - - server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" - server_href = "http://localhost/v2/servers/" + server_uuid - server_bookmark = "http://localhost/servers/" + server_uuid - alternate = "%s/fake/images/%s" - - expected = [{ - '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": "http://localhost/v2/fake/images/123", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/123", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": alternate % (utils.generate_glance_url(), 123), - }], - }, - { - 'id': '124', - 'name': 'queued snapshot', - 'metadata': { - u'instance_ref': server_href, - u'user_id': u'fake', - }, - 'updated': NOW_API_FORMAT, - 'created': NOW_API_FORMAT, - 'status': 'SAVING', - 'minDisk': 0, - 'progress': 25, - 'minRam': 0, - 'server': { - 'id': server_uuid, - "links": [{ - "rel": "self", - "href": server_href, - }, - { - "rel": "bookmark", - "href": server_bookmark, - }], - }, - "links": [{ - "rel": "self", - "href": "http://localhost/v2/fake/images/124", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/images/124", - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": alternate % (utils.generate_glance_url(), 124), - }], - }] - - self.assertDictListMatch(expected, response_list) - - href_parts = urlparse.urlparse(response_links[0]['href']) - self.assertEqual('/v2/fake/images', href_parts.path) - params = urlparse.parse_qs(href_parts.query) - - self.assertDictMatch({'limit': ['2'], 'marker': ['124']}, params) - - def test_image_filter_with_name(self): - image_service = self.mox.CreateMockAnything() - filters = {'name': 'testname'} - request = fakes.HTTPRequest.blank('/v2/images?name=testname') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_filter_with_min_ram(self): - image_service = self.mox.CreateMockAnything() - filters = {'min_ram': '0'} - request = fakes.HTTPRequest.blank('/v2/images?minRam=0') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_filter_with_min_disk(self): - image_service = self.mox.CreateMockAnything() - filters = {'min_disk': '7'} - request = fakes.HTTPRequest.blank('/v2/images?minDisk=7') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_filter_with_status(self): - image_service = self.mox.CreateMockAnything() - filters = {'status': 'ACTIVE'} - request = fakes.HTTPRequest.blank('/v2/images?status=ACTIVE') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_filter_with_property(self): - image_service = self.mox.CreateMockAnything() - filters = {'property-test': '3'} - request = fakes.HTTPRequest.blank('/v2/images?property-test=3') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_filter_server(self): - image_service = self.mox.CreateMockAnything() - uuid = 'fa95aaf5-ab3b-4cd8-88c0-2be7dd051aaf' - ref = 'http://localhost:8774/servers/' + uuid - filters = {'property-instance_ref': ref} - request = fakes.HTTPRequest.blank('/v2/images?server=' + ref) - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_filter_changes_since(self): - image_service = self.mox.CreateMockAnything() - filters = {'changes-since': '2011-01-24T17:08Z'} - request = fakes.HTTPRequest.blank('/v2/images?changes-since=' - '2011-01-24T17:08Z') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_filter_with_type(self): - image_service = self.mox.CreateMockAnything() - filters = {'property-image_type': 'BASE'} - request = fakes.HTTPRequest.blank('/v2/images?type=BASE') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_filter_not_supported(self): - image_service = self.mox.CreateMockAnything() - filters = {'status': 'ACTIVE'} - request = fakes.HTTPRequest.blank('/v2/images?status=ACTIVE&' - 'UNSUPPORTEDFILTER=testname') - context = request.environ['nova.context'] - image_service.detail(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.detail(request) - self.mox.VerifyAll() - - def test_image_no_filters(self): - image_service = self.mox.CreateMockAnything() - filters = {} - request = fakes.HTTPRequest.blank('/v2/images') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_detail_filter_with_name(self): - image_service = self.mox.CreateMockAnything() - filters = {'name': 'testname'} - request = fakes.HTTPRequest.blank('/v2/fake/images/detail' - '?name=testname') - context = request.environ['nova.context'] - image_service.detail(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.detail(request) - self.mox.VerifyAll() - - def test_image_detail_filter_with_status(self): - image_service = self.mox.CreateMockAnything() - filters = {'status': 'ACTIVE'} - request = fakes.HTTPRequest.blank('/v2/fake/images/detail' - '?status=ACTIVE') - context = request.environ['nova.context'] - image_service.detail(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.detail(request) - self.mox.VerifyAll() - - def test_image_detail_filter_with_property(self): - image_service = self.mox.CreateMockAnything() - filters = {'property-test': '3'} - request = fakes.HTTPRequest.blank('/v2/fake/images/detail' - '?property-test=3') - context = request.environ['nova.context'] - image_service.detail(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.detail(request) - self.mox.VerifyAll() - - def test_image_detail_filter_server(self): - image_service = self.mox.CreateMockAnything() - uuid = 'fa95aaf5-ab3b-4cd8-88c0-2be7dd051aaf' - ref = 'http://localhost:8774/servers/' + uuid - url = '/v2/fake/images/detail?server=' + ref - filters = {'property-instance_ref': ref} - request = fakes.HTTPRequest.blank(url) - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_detail_filter_changes_since(self): - image_service = self.mox.CreateMockAnything() - filters = {'changes-since': '2011-01-24T17:08Z'} - request = fakes.HTTPRequest.blank('/v2/fake/images/detail' - '?changes-since=2011-01-24T17:08Z') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_detail_filter_with_type(self): - image_service = self.mox.CreateMockAnything() - filters = {'property-image_type': 'BASE'} - request = fakes.HTTPRequest.blank('/v2/fake/images/detail?type=BASE') - context = request.environ['nova.context'] - image_service.index(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.index(request) - self.mox.VerifyAll() - - def test_image_detail_filter_not_supported(self): - image_service = self.mox.CreateMockAnything() - filters = {'status': 'ACTIVE'} - request = fakes.HTTPRequest.blank('/v2/fake/images/detail?status=' - 'ACTIVE&UNSUPPORTEDFILTER=testname') - context = request.environ['nova.context'] - image_service.detail(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.detail(request) - self.mox.VerifyAll() - - def test_image_detail_no_filters(self): - image_service = self.mox.CreateMockAnything() - filters = {} - request = fakes.HTTPRequest.blank('/v2/fake/images/detail') - context = request.environ['nova.context'] - image_service.detail(context, filters=filters).AndReturn([]) - self.mox.ReplayAll() - controller = images.Controller(image_service=image_service) - controller.detail(request) - self.mox.VerifyAll() - - def test_generate_alternate_link(self): - view = images_view.ViewBuilder() - request = fakes.HTTPRequest.blank('/v2/fake/images/1') - generated_url = view._get_alternate_link(request, 1) - actual_url = "%s/fake/images/1" % utils.generate_glance_url() - self.assertEqual(generated_url, actual_url) - - def test_delete_image(self): - request = fakes.HTTPRequest.blank('/v2/fake/images/124') - request.method = 'DELETE' - response = self.controller.delete(request, '124') - self.assertEqual(response.status_int, 204) - - def test_delete_image_not_found(self): - request = fakes.HTTPRequest.blank('/v2/fake/images/300') - request.method = 'DELETE' - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.delete, request, '300') - - -class ImageXMLSerializationTest(test.TestCase): - - TIMESTAMP = "2010-10-11T10:30:22Z" - SERVER_UUID = 'aa640691-d1a7-4a67-9d3c-d35ee6b3cc74' - SERVER_HREF = 'http://localhost/v2/servers/' + SERVER_UUID - SERVER_BOOKMARK = 'http://localhost/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("") - 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.assertEqual(server_root, None) - - 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_index') - 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_index') - 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_index') - 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/api/openstack/v2/test_limits.py b/nova/tests/api/openstack/v2/test_limits.py deleted file mode 100644 index 1c299d751..000000000 --- a/nova/tests/api/openstack/v2/test_limits.py +++ /dev/null @@ -1,939 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Tests dealing with HTTP rate-limiting. -""" - -import httplib -import json -import StringIO -import unittest -from xml.dom import minidom - -from lxml import etree -import stubout -import webob - -from nova.api.openstack.v2 import limits -from nova.api.openstack.v2 import views -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -import nova.context -from nova import test - - -TEST_LIMITS = [ - limits.Limit("GET", "/delayed", "^/delayed", 1, limits.PER_MINUTE), - limits.Limit("POST", "*", ".*", 7, limits.PER_MINUTE), - limits.Limit("POST", "/servers", "^/servers", 3, limits.PER_MINUTE), - limits.Limit("PUT", "*", "", 10, limits.PER_MINUTE), - limits.Limit("PUT", "/servers", "^/servers", 5, limits.PER_MINUTE), -] -NS = { - 'atom': 'http://www.w3.org/2005/Atom', - 'ns': 'http://docs.openstack.org/compute/api/v1.1' -} - - -class BaseLimitTestSuite(unittest.TestCase): - """Base test suite which provides relevant stubs and time abstraction.""" - - def setUp(self): - """Run before each test.""" - self.time = 0.0 - self.stubs = stubout.StubOutForTesting() - self.stubs.Set(limits.Limit, "_get_time", self._get_time) - self.absolute_limits = {} - - def stub_get_project_quotas(context, project_id): - return self.absolute_limits - - self.stubs.Set(nova.quota, "get_project_quotas", - stub_get_project_quotas) - - def tearDown(self): - """Run after each test.""" - self.stubs.UnsetAll() - - def _get_time(self): - """Return the "time" according to this test suite.""" - return self.time - - -class LimitsControllerTest(BaseLimitTestSuite): - """ - Tests for `limits.LimitsController` class. - """ - - def setUp(self): - """Run before each test.""" - BaseLimitTestSuite.setUp(self) - self.controller = wsgi.LazySerializationMiddleware( - limits.create_resource()) - self.maxDiff = None - - def _get_index_request(self, accept_header="application/json"): - """Helper to set routing arguments.""" - request = webob.Request.blank("/") - 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 = json.loads(response.body) - self.assertEqual(expected, body) - - def test_index_json(self): - """Test getting limit details in JSON.""" - request = self._get_index_request() - request = self._populate_limits(request) - self.absolute_limits = { - 'ram': 512, - 'instances': 5, - 'cores': 21, - } - 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, - }, - { - "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, - }, - }, - } - body = json.loads(response.body) - self.assertEqual(expected, body) - - 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 = json.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 = json.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) - - -class TestLimiter(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.""" - BaseLimitTestSuite.setUp(self) - _limits = '(GET, *, .*, 1, MINUTE)' - self.app = limits.RateLimitingMiddleware(self._empty_app, _limits, - "%s.TestLimiter" % - self.__class__.__module__) - - def test_limit_class(self): - """Test that middleware selected correct limiter class.""" - assert isinstance(self.app._limiter, TestLimiter) - - 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 (413) 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, 413) - - body = json.loads(response.body) - expected = "Only 1 GET request(s) can be made to * every minute." - value = body["overLimitFault"]["details"].strip() - self.assertEqual(value, expected) - - def test_limited_request_xml(self): - """Test a rate-limited (413) 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, 413) - - root = minidom.parseString(response.body).childNodes[0] - expected = "Only 1 GET request(s) can be made to * every minute." - - 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.assertEqual(None, 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.assertEqual(None, 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.assertEqual(None, 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, e: - assert False, str(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 = [limits.PER_MINUTE, limits.PER_HOUR, - limits.PER_SECOND, limits.PER_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.""" - BaseLimitTestSuite.setUp(self) - userlimits = {'user:user3': ''} - 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.failUnlessAlmostEqual(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) - - 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) - - def test_user_limit(self): - """ - Test user-specific limits. - """ - self.assertEqual(self.limiter.levels['user3'], []) - - def test_multiple_users(self): - """ - Tests involving multiple users. - """ - # 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) - - -class WsgiLimiterTest(BaseLimitTestSuite): - """ - Tests for `limits.WsgiLimiter` class. - """ - - def setUp(self): - """Run before each test.""" - BaseLimitTestSuite.setUp(self) - self.app = limits.WsgiLimiter(TEST_LIMITS) - - def _request_data(self, verb, path): - """Get data decribing a limit request verb/path.""" - return json.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.""" - requests = [] - 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.assertEqual(delay, None) - - def test_escaping(self): - delay = self._request("GET", "/something/jump%20up") - self.assertEqual(delay, None) - - def test_response_to_delays(self): - delay = self._request("GET", "/delayed") - self.assertEqual(delay, None) - - delay = self._request("GET", "/delayed") - self.assertEqual(delay, '60.00') - - def test_response_to_delays_usernames(self): - delay = self._request("GET", "/delayed", "user1") - self.assertEqual(delay, None) - - delay = self._request("GET", "/delayed", "user2") - self.assertEqual(delay, None) - - 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. - """ - 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) - - httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) - - -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. - """ - BaseLimitTestSuite.setUp(self) - self.app = limits.WsgiLimiter(TEST_LIMITS) - 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) - - -class LimitsViewBuilderTest(test.TestCase): - - def setUp(self): - 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 tearDown(self): - pass - - 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.assertDictMatch(output, 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.assertDictMatch(output, expected_limits) - - -class LimitsXMLSerializationTest(test.TestCase): - - def setUp(self): - self.maxDiff = None - - def tearDown(self): - pass - - def test_xml_declaration(self): - serializer = limits.LimitsTemplate() - - fixture = {"limits": { - "rate": [], - "absolute": {}}} - - output = serializer.serialize(fixture) - print output - has_dec = output.startswith("") - 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) - print output - 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) - print output - 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/api/openstack/v2/test_server_actions.py b/nova/tests/api/openstack/v2/test_server_actions.py deleted file mode 100644 index d2958e3b2..000000000 --- a/nova/tests/api/openstack/v2/test_server_actions.py +++ /dev/null @@ -1,834 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import base64 -import datetime - -import mox -import stubout -import webob - -from nova.api.openstack.v2 import servers -from nova.compute import vm_states -from nova.compute import instance_types -from nova import context -import nova.db -from nova import exception -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes -from nova import utils - - -FLAGS = flags.FLAGS -FAKE_UUID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - - -def return_server_by_id(context, id): - return stub_instance(id) - - -def return_server_by_uuid(context, uuid): - return stub_instance(1, uuid=uuid) - - -def return_server_by_uuid_not_found(context, uuid): - raise exception.NotFound() - - -def instance_update(context, instance_id, kwargs): - return stub_instance(instance_id) - - -def return_server_with_attributes(**kwargs): - def _return_server(context, id): - return stub_instance(id, **kwargs) - return _return_server - - -def return_server_with_state(vm_state, task_state=None): - return return_server_with_attributes(vm_state=vm_state, - task_state=task_state) - - -def return_server_with_uuid_and_state(vm_state, task_state=None): - def _return_server(context, id): - return return_server_with_state(vm_state, task_state) - return _return_server - - -def stub_instance(id, metadata=None, image_ref="10", flavor_id="1", - name=None, vm_state=None, task_state=None, uuid=None, - access_ip_v4="", access_ip_v6=""): - if metadata is not None: - metadata_items = [{'key':k, 'value':v} for k, v in metadata.items()] - else: - metadata_items = [{'key':'seq', 'value':id}] - - if uuid is None: - uuid = FAKE_UUID - - inst_type = instance_types.get_instance_type_by_flavor_id(int(flavor_id)) - - instance = { - "id": int(id), - "name": str(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": "fake", - "project_id": "fake", - "image_ref": image_ref, - "kernel_id": "", - "ramdisk_id": "", - "launch_index": 0, - "key_name": "", - "key_data": "", - "vm_state": vm_state or vm_states.ACTIVE, - "task_state": task_state, - "memory_mb": 0, - "vcpus": 0, - "local_gb": 0, - "hostname": "", - "host": "", - "instance_type": dict(inst_type), - "user_data": "", - "reservation_id": "", - "mac_address": "", - "scheduled_at": utils.utcnow(), - "launched_at": utils.utcnow(), - "terminated_at": utils.utcnow(), - "availability_zone": "", - "display_name": name or "server%s" % id, - "display_description": "", - "locked": False, - "metadata": metadata_items, - "access_ip_v4": access_ip_v4, - "access_ip_v6": access_ip_v6, - "uuid": uuid, - "virtual_interfaces": [], - "progress": 0, - } - - instance["fixed_ips"] = [{"address": '192.168.0.1', - "network": - {'label': 'public', 'cidr_v6': None}, - "virtual_interface": - {'address': 'aa:aa:aa:aa:aa:aa'}, - "floating_ips": []}] - - return instance - - -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): - - def setUp(self): - self.maxDiff = None - super(ServerActionsControllerTest, self).setUp() - - self.stubs = stubout.StubOutForTesting() - fakes.stub_out_auth(self.stubs) - self.stubs.Set(nova.db, 'instance_get', return_server_by_id) - self.stubs.Set(nova.db, 'instance_get_by_uuid', return_server_by_uuid) - self.stubs.Set(nova.db, 'instance_update', instance_update) - - fakes.stub_out_glance(self.stubs) - fakes.stub_out_nw_api(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - self.snapshot = fakes.stub_out_compute_api_snapshot(self.stubs) - service_class = 'nova.image.glance.GlanceImageService' - self.service = utils.import_object(service_class) - self.context = context.RequestContext(1, None) - self.service.delete_all() - self.sent_to_glance = {} - fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) - self.flags(allow_instance_snapshots=True) - self.uuid = FAKE_UUID - self.url = '/v2/fake/servers/%s/action' % self.uuid - - self.controller = servers.Controller() - - def tearDown(self): - self.stubs.UnsetAll() - super(ServerActionsControllerTest, self).tearDown() - - def test_server_bad_body(self): - body = {} - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - def test_server_unknown_action(self): - body = {'sockTheFox': {'fakekey': '1234'}} - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - def test_server_change_password(self): - mock_method = MockSetAdminPassword() - self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) - body = {'changePassword': {'adminPass': '1234pass'}} - - req = fakes.HTTPRequest.blank(self.url) - self.controller.action(req, FAKE_UUID, body) - - self.assertEqual(mock_method.instance_id, self.uuid) - 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, 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, req, FAKE_UUID, body) - - def test_server_change_password_empty_string(self): - body = {'changePassword': {'adminPass': ''}} - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - 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, req, FAKE_UUID, body) - - def test_reboot_hard(self): - body = dict(reboot=dict(type="HARD")) - req = fakes.HTTPRequest.blank(self.url) - self.controller.action(req, FAKE_UUID, body) - - def test_reboot_soft(self): - body = dict(reboot=dict(type="SOFT")) - req = fakes.HTTPRequest.blank(self.url) - self.controller.action(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, 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, req, FAKE_UUID, body) - - def test_reboot_not_found(self): - self.stubs.Set(nova.db, 'instance_get_by_uuid', - return_server_by_uuid_not_found) - - body = dict(reboot=dict(type="HARD")) - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPNotFound, self.controller.action, - req, str(utils.gen_uuid()), body) - - def test_reboot_raises_conflict_on_invalid_state(self): - body = dict(reboot=dict(type="HARD")) - - def fake_reboot(*args, **kwargs): - raise exception.InstanceInvalidState - - self.stubs.Set(nova.compute.api.API, 'reboot', fake_reboot) - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPConflict, self.controller.action, - req, FAKE_UUID, body) - - def test_rebuild_accepted_minimum(self): - new_return_server = return_server_with_attributes(image_ref='2') - self.stubs.Set(nova.db, 'instance_get', new_return_server) - self_href = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID - - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - }, - } - - req = fakes.HTTPRequest.blank(self.url) - robj = self.controller.action(req, FAKE_UUID, body) - body = robj.obj - - self.assertEqual(body['server']['image']['id'], '2') - self.assertEqual(len(body['server']['adminPass']), - FLAGS.password_length) - self.assertEqual(robj['location'], self_href) - - def test_rebuild_raises_conflict_on_invalid_state(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - }, - } - - def fake_rebuild(*args, **kwargs): - raise exception.InstanceInvalidState - - self.stubs.Set(nova.compute.api.API, 'rebuild', fake_rebuild) - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPConflict, - self.controller.action, req, FAKE_UUID, body) - - def test_rebuild_accepted_with_metadata(self): - metadata = {'new': 'metadata'} - - new_return_server = return_server_with_attributes(metadata=metadata) - self.stubs.Set(nova.db, 'instance_get', new_return_server) - - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - "metadata": metadata, - }, - } - - req = fakes.HTTPRequest.blank(self.url) - body = self.controller.action(req, FAKE_UUID, body).obj - - self.assertEqual(body['server']['metadata'], metadata) - - def test_rebuild_accepted_with_bad_metadata(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - "metadata": "stack", - }, - } - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - def test_rebuild_bad_entity(self): - body = { - "rebuild": { - "imageId": 2, - }, - } - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - def test_rebuild_bad_personality(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - "personality": [{ - "path": "/path/to/file", - "contents": "INVALID b64", - }] - }, - } - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - def test_rebuild_personality(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - "personality": [{ - "path": "/path/to/file", - "contents": base64.b64encode("Test String"), - }] - }, - } - - req = fakes.HTTPRequest.blank(self.url) - body = self.controller.action(req, FAKE_UUID, body).obj - - self.assertTrue('personality' not in body['server']) - - def test_rebuild_admin_pass(self): - new_return_server = return_server_with_attributes(image_ref='2') - self.stubs.Set(nova.db, 'instance_get', new_return_server) - - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - "adminPass": "asdf", - }, - } - - req = fakes.HTTPRequest.blank(self.url) - body = self.controller.action(req, FAKE_UUID, body).obj - - self.assertEqual(body['server']['image']['id'], '2') - self.assertEqual(body['server']['adminPass'], 'asdf') - - def test_rebuild_server_not_found(self): - def server_not_found(self, instance_id): - raise exception.InstanceNotFound(instance_id=instance_id) - self.stubs.Set(nova.db, 'instance_get', server_not_found) - - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - }, - } - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.action, 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": "http://localhost/images/2", - "accessIPv4": "172.19.0.1", - "accessIPv6": "fe80::1", - }, - } - - update = self.mox.CreateMockAnything() - self.stubs.Set(nova.compute.API, 'update', update) - req = fakes.HTTPRequest.blank(self.url) - context = req.environ['nova.context'] - update(context, mox.IgnoreArg(), - image_ref='http://localhost/images/2', - vm_state=vm_states.REBUILDING, - task_state=None, progress=0, **attributes).AndReturn(None) - self.mox.ReplayAll() - - self.controller.action(req, FAKE_UUID, body) - self.mox.VerifyAll() - - 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(nova.compute.api.API, 'resize', resize_mock) - - req = fakes.HTTPRequest.blank(self.url) - body = self.controller.action(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, 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, 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 - - self.stubs.Set(nova.compute.api.API, 'resize', fake_resize) - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPConflict, self.controller.action, - 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(nova.compute.api.API, 'confirm_resize', cr_mock) - - req = fakes.HTTPRequest.blank(self.url) - body = self.controller.action(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(nova.compute.api.API, - 'confirm_resize', - confirm_resize_mock) - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, 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 - - self.stubs.Set(nova.compute.api.API, 'confirm_resize', - fake_confirm_resize) - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPConflict, self.controller.action, - 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(nova.compute.api.API, - 'revert_resize', - revert_resize_mock) - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, 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(nova.compute.api.API, 'revert_resize', revert_mock) - - req = fakes.HTTPRequest.blank(self.url) - body = self.controller.action(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 - - self.stubs.Set(nova.compute.api.API, 'revert_resize', - fake_revert_resize) - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPConflict, self.controller.action, - req, FAKE_UUID, body) - - def test_create_image(self): - body = { - 'createImage': { - 'name': 'Snapshot 1', - }, - } - - req = fakes.HTTPRequest.blank(self.url) - response = self.controller.action(req, FAKE_UUID, body) - - location = response.headers['Location'] - self.assertEqual('http://localhost/v2/fake/images/123', location) - server_location = self.snapshot.extra_props_last_call['instance_ref'] - expected_server_location = 'http://localhost/v2/servers/' + self.uuid - self.assertEqual(expected_server_location, server_location) - - 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, 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(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(FLAGS.quota_metadata_items + 1): - body['createImage']['metadata']['foo%i' % num] = "bar" - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, - self.controller.action, 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, 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, req, FAKE_UUID, body) - - def test_create_image_raises_conflict_on_invalid_state(self): - def snapshot(*args, **kwargs): - raise exception.InstanceInvalidState - self.stubs.Set(nova.compute.API, 'snapshot', snapshot) - - body = { - "createImage": { - "name": "test_snapshot", - }, - } - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPConflict, - self.controller.action, req, FAKE_UUID, body) - - -class TestServerActionXMLDeserializer(test.TestCase): - - def setUp(self): - self.deserializer = servers.ActionDeserializer() - - def tearDown(self): - pass - - def test_create_image(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "createImage": { - "name": "new-server-test", - }, - } - self.assertEquals(request['body'], expected) - - def test_create_image_with_metadata(self): - serial_request = """ - - - value1 - -""" - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "createImage": { - "name": "new-server-test", - "metadata": {"key1": "value1"}, - }, - } - self.assertEquals(request['body'], expected) - - def test_change_pass(self): - serial_request = """ - """ - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "changePassword": { - "adminPass": "1234pass", - }, - } - self.assertEquals(request['body'], expected) - - def test_change_pass_no_pass(self): - serial_request = """ - """ - self.assertRaises(AttributeError, - self.deserializer.deserialize, - serial_request, - 'action') - - def test_reboot(self): - serial_request = """ - """ - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "reboot": { - "type": "HARD", - }, - } - self.assertEquals(request['body'], expected) - - def test_reboot_no_type(self): - serial_request = """ - """ - self.assertRaises(AttributeError, - self.deserializer.deserialize, - serial_request, - 'action') - - def test_resize(self): - serial_request = """ - """ - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "resize": {"flavorRef": "http://localhost/flavors/3"}, - } - self.assertEquals(request['body'], expected) - - def test_resize_no_flavor_ref(self): - serial_request = """ - """ - self.assertRaises(AttributeError, - self.deserializer.deserialize, - serial_request, - 'action') - - def test_confirm_resize(self): - serial_request = """ - """ - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "confirmResize": None, - } - self.assertEquals(request['body'], expected) - - def test_revert_resize(self): - serial_request = """ - """ - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "revertResize": None, - } - self.assertEquals(request['body'], expected) - - def test_rebuild(self): - serial_request = """ - - - Apache1 - - - Mg== - - """ - 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.assertDictMatch(request['body'], expected) - - def test_rebuild_minimum(self): - serial_request = """ - """ - request = self.deserializer.deserialize(serial_request, 'action') - expected = { - "rebuild": { - "imageRef": "http://localhost/images/1", - }, - } - self.assertDictMatch(request['body'], expected) - - def test_rebuild_no_imageRef(self): - serial_request = """ - - - Apache1 - - - Mg== - - """ - self.assertRaises(AttributeError, - self.deserializer.deserialize, - serial_request, - 'action') diff --git a/nova/tests/api/openstack/v2/test_server_metadata.py b/nova/tests/api/openstack/v2/test_server_metadata.py deleted file mode 100644 index 49ff1bcd8..000000000 --- a/nova/tests/api/openstack/v2/test_server_metadata.py +++ /dev/null @@ -1,361 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json -import webob - -from nova.api.openstack.v2 import server_metadata -import nova.db -from nova import exception -from nova import flags -from nova import test -from nova.tests.api.openstack import fakes -from nova import utils - - -FLAGS = flags.FLAGS - - -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 return_server_metadata(context, server_id): - if not isinstance(server_id, int): - msg = 'id %s must be int 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(FLAGS.quota_metadata_items): - metadata['metadata']['key%i' % num] = "blah" - return metadata - - -def return_server(context, server_id): - return {'id': server_id, 'name': 'fake'} - - -def return_server_by_uuid(context, server_uuid): - return {'id': 1, 'name': 'fake'} - - -def return_server_nonexistant(context, server_id): - raise exception.InstanceNotFound() - - -class ServerMetaDataTest(test.TestCase): - - def setUp(self): - super(ServerMetaDataTest, 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.controller = server_metadata.Controller() - self.uuid = str(utils.gen_uuid()) - self.url = '/v1.1/fake/servers/%s/metadata' % self.uuid - - def test_index(self): - req = fakes.HTTPRequest.blank(self.url) - res_dict = self.controller.index(req, self.uuid) - - expected = { - 'metadata': { - 'key1': 'value1', - 'key2': 'value2', - 'key3': 'value3', - }, - } - self.assertEqual(expected, res_dict) - - def test_index_nonexistant_server(self): - self.stubs.Set(nova.db, 'instance_metadata_get', - return_server_nonexistant) - req = fakes.HTTPRequest.blank(self.url) - 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 = fakes.HTTPRequest.blank(self.url) - res_dict = self.controller.index(req, self.uuid) - expected = {'metadata': {}} - self.assertEqual(expected, res_dict) - - def test_show(self): - req = fakes.HTTPRequest.blank(self.url + '/key2') - res_dict = self.controller.show(req, self.uuid, 'key2') - expected = {'meta': {'key2': 'value2'}} - self.assertEqual(expected, res_dict) - - def test_show_nonexistant_server(self): - self.stubs.Set(nova.db, 'instance_metadata_get', - return_server_nonexistant) - req = fakes.HTTPRequest.blank(self.url + '/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 = fakes.HTTPRequest.blank(self.url + '/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 = fakes.HTTPRequest.blank(self.url + '/key2') - req.method = 'DELETE' - res = self.controller.delete(req, self.uuid, 'key2') - - self.assertEqual(None, res) - - def test_delete_nonexistant_server(self): - self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) - req = fakes.HTTPRequest.blank(self.url + '/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 = fakes.HTTPRequest.blank(self.url + '/key6') - req.method = 'DELETE' - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.delete, req, self.uuid, 'key6') - - def test_create(self): - self.stubs.Set(nova.db, 'instance_metadata_get', - return_server_metadata) - self.stubs.Set(nova.db, 'instance_metadata_update', - return_create_instance_metadata) - req = fakes.HTTPRequest.blank(self.url) - req.method = 'POST' - req.content_type = "application/json" - body = {"metadata": {"key9": "value9"}} - req.body = json.dumps(body) - res_dict = self.controller.create(req, self.uuid, 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 = fakes.HTTPRequest.blank(self.url) - req.method = 'POST' - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, req, self.uuid, None) - - def test_create_nonexistant_server(self): - self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) - req = fakes.HTTPRequest.blank(self.url) - req.method = 'POST' - body = {"metadata": {"key1": "value1"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.create, req, self.uuid, body) - - def test_update_all(self): - self.stubs.Set(nova.db, 'instance_metadata_update', - return_create_instance_metadata) - req = fakes.HTTPRequest.blank(self.url) - req.method = 'PUT' - req.content_type = "application/json" - expected = { - 'metadata': { - 'key10': 'value10', - 'key99': 'value99', - }, - } - req.body = json.dumps(expected) - res_dict = self.controller.update_all(req, self.uuid, expected) - - self.assertEqual(expected, res_dict) - - def test_update_all_empty_container(self): - self.stubs.Set(nova.db, 'instance_metadata_update', - return_create_instance_metadata) - req = fakes.HTTPRequest.blank(self.url) - req.method = 'PUT' - req.content_type = "application/json" - expected = {'metadata': {}} - req.body = json.dumps(expected) - res_dict = self.controller.update_all(req, self.uuid, expected) - - self.assertEqual(expected, res_dict) - - def test_update_all_malformed_container(self): - self.stubs.Set(nova.db, 'instance_metadata_update', - return_create_instance_metadata) - req = fakes.HTTPRequest.blank(self.url) - req.method = 'PUT' - req.content_type = "application/json" - expected = {'meta': {}} - req.body = json.dumps(expected) - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.update_all, req, self.uuid, expected) - - def test_update_all_malformed_data(self): - self.stubs.Set(nova.db, 'instance_metadata_update', - return_create_instance_metadata) - req = fakes.HTTPRequest.blank(self.url) - req.method = 'PUT' - req.content_type = "application/json" - expected = {'metadata': ['asdf']} - req.body = json.dumps(expected) - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.update_all, req, self.uuid, expected) - - def test_update_all_nonexistant_server(self): - self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) - req = fakes.HTTPRequest.blank(self.url) - req.method = 'PUT' - req.content_type = "application/json" - body = {'metadata': {'key10': 'value10'}} - req.body = json.dumps(body) - - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.update_all, req, '100', body) - - def test_update_item(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": {"key1": "value1"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res_dict = self.controller.update(req, self.uuid, 'key1', body) - expected = {'meta': {'key1': 'value1'}} - self.assertEqual(expected, res_dict) - - def test_update_item_nonexistant_server(self): - self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) - req = fakes.HTTPRequest.blank('/v1.1/fake/servers/asdf/metadata/key1') - req.method = 'PUT' - body = {"meta": {"key1": "value1"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.update, req, self.uuid, 'key1', body) - - def test_update_item_empty_body(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(webob.exc.HTTPBadRequest, - self.controller.update, req, self.uuid, 'key1', None) - - def test_update_item_too_many_keys(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": {"key1": "value1", "key2": "value2"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.update, req, self.uuid, 'key1', body) - - def test_update_item_body_uri_mismatch(self): - self.stubs.Set(nova.db, 'instance_metadata_update', - return_create_instance_metadata) - req = fakes.HTTPRequest.blank(self.url + '/bad') - req.method = 'PUT' - body = {"meta": {"key1": "value1"}} - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.update, req, self.uuid, 'bad', body) - - 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(FLAGS.quota_metadata_items + 1): - data['metadata']['key%i' % num] = "blah" - req = fakes.HTTPRequest.blank(self.url) - req.method = 'POST' - req.body = json.dumps(data) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, - self.controller.create, req, self.uuid, 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(FLAGS.quota_metadata_items + 1): - data['metadata']['key%i' % num] = "blah" - req = fakes.HTTPRequest.blank(self.url) - req.method = 'PUT' - req.body = json.dumps(data) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, - self.controller.update_all, req, self.uuid, data) diff --git a/nova/tests/api/openstack/v2/test_servers.py b/nova/tests/api/openstack/v2/test_servers.py deleted file mode 100644 index 7216699b4..000000000 --- a/nova/tests/api/openstack/v2/test_servers.py +++ /dev/null @@ -1,3849 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# 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 json -import urlparse - -from lxml import etree -import webob - -import nova.api.openstack.v2 -from nova.api.openstack.v2 import ips -from nova.api.openstack.v2 import servers -from nova.api.openstack import xmlutil -import nova.compute.api -from nova.compute import instance_types -from nova.compute import task_states -from nova.compute import vm_states -import nova.db -from nova.db.sqlalchemy.models import InstanceActions -from nova.db.sqlalchemy.models import InstanceMetadata -from nova import flags -import nova.image.fake -import nova.rpc -import nova.scheduler.api -from nova import test -from nova.tests.api.openstack import fakes -from nova import utils - - -FLAGS = flags.FLAGS -FAKE_UUID = fakes.FAKE_UUID -FAKE_UUIDS = {0: 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' -} - - -def get_fake_uuid(token=0): - if not token in FAKE_UUIDS: - FAKE_UUIDS[token] = str(utils.gen_uuid()) - return FAKE_UUIDS[token] - - -def fake_gen_uuid(): - return FAKE_UUID - - -def return_server_by_id(context, id): - return fakes.stub_instance(id) - - -def return_server_by_uuid(context, uuid): - id = 1 - return fakes.stub_instance(id, uuid=uuid) - - -def return_server_with_attributes(**kwargs): - def _return_server(context, instance_id): - return fakes.stub_instance(instance_id, **kwargs) - return _return_server - - -def return_server_with_attributes_by_uuid(**kwargs): - def _return_server(context, uuid): - return fakes.stub_instance(1, uuid=uuid, **kwargs) - return _return_server - - -def return_server_with_state(vm_state, task_state=None): - def _return_server(context, uuid): - return fakes.stub_instance(1, uuid=uuid, vm_state=vm_state, - task_state=task_state) - return _return_server - - -def return_server_with_uuid_and_state(vm_state, task_state): - def _return_server(context, id): - return fakes.stub_instance(id, - uuid=FAKE_UUID, - vm_state=vm_state, - task_state=task_state) - return _return_server - - -def return_servers(context, *args, **kwargs): - servers = [] - for i in xrange(5): - server = fakes.stub_instance(i, 'fake', 'fake', uuid=get_fake_uuid(i)) - servers.append(server) - return servers - - -def return_servers_by_reservation(context, reservation_id=""): - return [fakes.stub_instance(i, reservation_id) for i in xrange(5)] - - -def return_servers_by_reservation_empty(context, reservation_id=""): - return [] - - -def return_servers_from_child_zones_empty(*args, **kwargs): - return [] - - -def return_servers_from_child_zones(*args, **kwargs): - class Server(object): - pass - - zones = [] - for zone in xrange(3): - servers = [] - for server_id in xrange(5): - server = Server() - server._info = fakes.stub_instance( - server_id, reservation_id="child") - servers.append(server) - - zones.append(("Zone%d" % zone, servers)) - return zones - - -def return_security_group(context, instance_id, security_group_id): - pass - - -def instance_update(context, instance_id, values): - return fakes.stub_instance(instance_id, name=values.get('display_name')) - - -def instance_addresses(context, instance_id): - return None - - -def fake_compute_api(cls, req, id): - return True - - -def find_host(self, context, instance_id): - return "nova" - - -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 ServersControllerTest(test.TestCase): - def setUp(self): - self.maxDiff = None - super(ServersControllerTest, self).setUp() - self.flags(verbose=True, use_ipv6=False) - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_key_pair_funcs(self.stubs) - fakes.stub_out_image_service(self.stubs) - fakes.stub_out_nw_api(self.stubs) - self.stubs.Set(nova.db, 'instance_get_all_by_filters', - return_servers) - self.stubs.Set(nova.db, 'instance_get', return_server_by_id) - self.stubs.Set(nova.db, 'instance_get_by_uuid', - return_server_by_uuid) - self.stubs.Set(nova.db, 'instance_get_all_by_project', - return_servers) - self.stubs.Set(nova.db, 'instance_add_security_group', - return_security_group) - self.stubs.Set(nova.db, 'instance_update', instance_update) - self.stubs.Set(nova.db, 'instance_get_fixed_addresses', - instance_addresses) - self.stubs.Set(nova.db, 'instance_get_floating_address', - instance_addresses) - - self.config_drive = None - - self.controller = servers.Controller() - self.ips_controller = ips.Controller() - - def test_get_server_by_uuid(self): - """ - The steps involved with resolving a UUID are pretty complicated; - here's what's happening in this scenario: - - 1. Show is calling `routing_get` - - 2. `routing_get` is wrapped by `reroute_compute` which does the work - of resolving requests to child zones. - - 3. `reroute_compute` looks up the UUID by hitting the stub - (returns_server_by_uuid) - - 4. Since the stub return that the record exists, `reroute_compute` - considers the request to be 'zone local', so it replaces the UUID - in the argument list with an integer ID and then calls the inner - function ('get'). - - 5. The call to `get` hits the other stub 'returns_server_by_id` which - has the UUID set to FAKE_UUID - - So, counterintuitively, we call `get` twice on the `show` command. - """ - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - res_dict = self.controller.show(req, FAKE_UUID) - self.assertEqual(res_dict['server']['id'], FAKE_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 = { - "server": { - "id": uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "progress": 0, - "name": "server1", - "status": "BUILD", - "accessIPv4": "", - "accessIPv6": "", - "hostId": '', - "key_name": '', - "image": { - "id": "10", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - }, - "metadata": { - "seq": "1", - }, - "config_drive": None, - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/servers/%s" % uuid, - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/servers/%s" % uuid, - }, - ], - } - } - - self.assertDictMatch(res_dict, 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 = return_server_with_attributes( - vm_state=vm_states.ACTIVE, progress=100) - self.stubs.Set(nova.db, 'instance_get', new_return_server) - - uuid = FAKE_UUID - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % uuid) - res_dict = self.controller.show(req, uuid) - expected_server = { - "server": { - "id": uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "progress": 100, - "name": "server1", - "status": "ACTIVE", - "accessIPv4": "", - "accessIPv6": "", - "hostId": '', - "key_name": '', - "image": { - "id": "10", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - }, - "metadata": { - "seq": "1", - }, - "config_drive": None, - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/servers/%s" % uuid, - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/servers/%s" % uuid, - }, - ], - } - } - - self.assertDictMatch(res_dict, 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 = return_server_with_attributes( - vm_state=vm_states.ACTIVE, image_ref=image_ref, - flavor_id=flavor_id, progress=100) - self.stubs.Set(nova.db, 'instance_get', new_return_server) - - uuid = FAKE_UUID - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % uuid) - res_dict = self.controller.show(req, uuid) - expected_server = { - "server": { - "id": uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "progress": 100, - "name": "server1", - "status": "ACTIVE", - "accessIPv4": "", - "accessIPv6": "", - "hostId": '', - "key_name": '', - "image": { - "id": "10", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - }, - "metadata": { - "seq": "1", - }, - "config_drive": None, - "links": [ - { - "rel": "self", - "href": "http://localhost/v2/fake/servers/%s" % uuid, - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/servers/%s" % uuid, - }, - ], - } - } - - self.assertDictMatch(res_dict, expected_server) - - # NOTE(bcwaldon): lp830817 - def test_get_server_by_id_malformed_networks(self): - def fake_instance_get(context, instance_uuid): - instance = return_server_by_uuid(context, instance_uuid) - instance['fixed_ips'] = [dict(network=None, address='1.2.3.4')] - return instance - - self.stubs.Set(nova.db, 'instance_get_by_uuid', fake_instance_get) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - res_dict = self.controller.show(req, FAKE_UUID) - - self.assertEqual(res_dict['server']['id'], FAKE_UUID) - self.assertEqual(res_dict['server']['name'], 'server1') - - def test_get_server_by_id_malformed_vif(self): - def fake_instance_get(context, uuid): - instance = return_server_by_uuid(context, uuid) - instance['fixed_ips'] = [dict(network={'label': 'meow'}, - address='1.2.3.4', virtual_interface=None)] - return instance - - self.stubs.Set(nova.db, 'instance_get_by_uuid', fake_instance_get) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - res_dict = self.controller.show(req, FAKE_UUID) - - self.assertEqual(res_dict['server']['id'], FAKE_UUID) - self.assertEqual(res_dict['server']['name'], 'server1') - - def test_get_server_by_id_with_addresses(self): - self.flags(use_ipv6=True) - privates = ['192.168.0.3', '192.168.0.4'] - publics = ['172.19.0.1', '172.19.0.2'] - new_return_server = return_server_with_attributes( - public_ips=publics, private_ips=privates) - self.stubs.Set(nova.db, 'instance_get', new_return_server) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - res_dict = self.controller.show(req, FAKE_UUID) - - self.assertEqual(res_dict['server']['id'], FAKE_UUID) - self.assertEqual(res_dict['server']['name'], 'server1') - addresses = res_dict['server']['addresses'] - expected = { - 'private': [ - {'addr': '192.168.0.3', 'version': 4}, - {'addr': '192.168.0.4', 'version': 4}, - ], - 'public': [ - {'addr': 'b33f::fdee:ddff:fecc:bbaa', 'version': 6}, - {'addr': '172.19.0.1', 'version': 4}, - {'addr': '172.19.0.2', 'version': 4}, - ], - } - self.assertDictMatch(addresses, expected) - - def test_get_server_by_id_with_addresses_ipv6_disabled(self): - # ipv6 flag is off by default - privates = ['192.168.0.3', '192.168.0.4'] - publics = ['172.19.0.1', '172.19.0.2'] - new_return_server = return_server_with_attributes( - public_ips=publics, private_ips=privates) - self.stubs.Set(nova.db, 'instance_get', new_return_server) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - res_dict = self.controller.show(req, FAKE_UUID) - - self.assertEqual(res_dict['server']['id'], FAKE_UUID) - self.assertEqual(res_dict['server']['name'], 'server1') - addresses = res_dict['server']['addresses'] - expected = { - 'private': [ - {'addr': '192.168.0.3', 'version': 4}, - {'addr': '192.168.0.4', 'version': 4}, - ], - 'public': [ - {'addr': '172.19.0.1', 'version': 4}, - {'addr': '172.19.0.2', 'version': 4}, - ], - } - self.assertDictMatch(addresses, expected) - - def test_get_server_addresses(self): - self.flags(use_ipv6=True) - - privates = ['192.168.0.3', '192.168.0.4'] - publics = ['172.19.0.1', '1.2.3.4', '172.19.0.2'] - new_return_server = return_server_with_attributes_by_uuid( - public_ips=publics, private_ips=privates) - self.stubs.Set(nova.db, 'instance_get_by_uuid', new_return_server) - - req = fakes.HTTPRequest.blank('/v2/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': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, - {'version': 4, 'addr': '172.19.0.1'}, - {'version': 4, 'addr': '1.2.3.4'}, - {'version': 4, 'addr': '172.19.0.2'}, - ], - }, - } - self.assertDictMatch(res_dict, expected) - - def test_get_server_addresses_with_floating(self): - privates = ['192.168.0.3', '192.168.0.4'] - publics = ['172.19.0.1', '1.2.3.4', '172.19.0.2'] - new_return_server = return_server_with_attributes_by_uuid( - public_ips=publics, private_ips=privates, - public_ips_are_floating=True) - self.stubs.Set(nova.db, 'instance_get_by_uuid', new_return_server) - - req = fakes.HTTPRequest.blank('/v2/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'}, - {'version': 4, 'addr': '172.19.0.1'}, - {'version': 4, 'addr': '1.2.3.4'}, - {'version': 4, 'addr': '172.19.0.2'}, - ], - }, - } - self.assertDictMatch(res_dict, expected) - - def test_get_server_addresses_single_network(self): - self.flags(use_ipv6=True) - privates = ['192.168.0.3', '192.168.0.4'] - publics = ['172.19.0.1', '1.2.3.4', '172.19.0.2'] - new_return_server = return_server_with_attributes_by_uuid( - public_ips=publics, private_ips=privates) - self.stubs.Set(nova.db, 'instance_get_by_uuid', new_return_server) - - url = '/v2/fake/servers/%s/ips/public' % FAKE_UUID - req = fakes.HTTPRequest.blank(url) - res_dict = self.ips_controller.show(req, FAKE_UUID, 'public') - - expected = { - 'public': [ - {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, - {'version': 4, 'addr': '172.19.0.1'}, - {'version': 4, 'addr': '1.2.3.4'}, - {'version': 4, 'addr': '172.19.0.2'}, - ], - } - self.assertDictMatch(res_dict, expected) - - def test_get_server_addresses_nonexistant_network(self): - url = '/v2/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_nonexistant_server(self): - def fake_instance_get(*args, **kwargs): - raise nova.exception.InstanceNotFound() - - self.stubs.Set(nova.db, 'instance_get_by_uuid', fake_instance_get) - - server_id = str(utils.gen_uuid()) - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s/ips' % server_id) - self.assertRaises(webob.exc.HTTPNotFound, - self.ips_controller.index, req, server_id) - - def test_get_server_list_with_reservation_id(self): - self.stubs.Set(nova.db, 'instance_get_all_by_reservation', - return_servers_by_reservation) - self.stubs.Set(nova.scheduler.api, 'call_zone_method', - return_servers_from_child_zones) - - req = fakes.HTTPRequest.blank('/v2/fake/servers?reservation_id=foo') - res_dict = self.controller.index(req) - - i = 0 - for s in res_dict['servers']: - if '_is_precooked' in s: - self.assertEqual(s.get('reservation_id'), 'child') - else: - print s - self.assertEqual(s.get('name'), 'server%d' % i) - i += 1 - - def test_get_server_list_with_reservation_id_empty(self): - self.stubs.Set(nova.db, 'instance_get_all_by_reservation', - return_servers_by_reservation_empty) - self.stubs.Set(nova.scheduler.api, 'call_zone_method', - return_servers_from_child_zones_empty) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/detail?' - 'reservation_id=foo') - res_dict = self.controller.detail(req) - - i = 0 - for s in res_dict['servers']: - if '_is_precooked' in s: - self.assertEqual(s.get('reservation_id'), 'child') - else: - self.assertEqual(s.get('name'), 'server%d' % i) - i += 1 - - def test_get_server_list_with_reservation_id_details(self): - self.stubs.Set(nova.db, 'instance_get_all_by_reservation', - return_servers_by_reservation) - self.stubs.Set(nova.scheduler.api, 'call_zone_method', - return_servers_from_child_zones) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/detail?' - 'reservation_id=foo') - res_dict = self.controller.detail(req) - - i = 0 - for s in res_dict['servers']: - if '_is_precooked' in s: - self.assertEqual(s.get('reservation_id'), 'child') - else: - self.assertEqual(s.get('name'), 'server%d' % i) - i += 1 - - def test_get_server_list(self): - req = fakes.HTTPRequest.blank('/v2/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'], get_fake_uuid(i)) - self.assertEqual(s['name'], 'server%d' % i) - self.assertEqual(s.get('image', None), 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('/v2/fake/servers?limit=3') - res_dict = self.controller.index(req) - - servers = res_dict['servers'] - self.assertEqual([s['id'] for s in servers], - [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': [get_fake_uuid(2)]} - self.assertDictMatch(expected_params, params) - - def test_get_servers_with_limit_bad_value(self): - req = fakes.HTTPRequest.blank('/v2/fake/servers?limit=aaa') - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.index, req) - - def test_get_server_details_with_limit(self): - req = fakes.HTTPRequest.blank('/v2/fake/servers/detail?limit=3') - res = self.controller.detail(req) - - servers = res['servers'] - self.assertEqual([s['id'] for s in servers], - [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', href_parts.path) - params = urlparse.parse_qs(href_parts.query) - expected = {'limit': ['3'], 'marker': [get_fake_uuid(2)]} - self.assertDictMatch(expected, params) - - def test_get_server_details_with_limit_bad_value(self): - req = fakes.HTTPRequest.blank('/v2/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('/v2/fake/servers/detail' - '?limit=3&blah=2:t') - res = self.controller.detail(req) - - servers = res['servers'] - self.assertEqual([s['id'] for s in servers], - [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', href_parts.path) - params = urlparse.parse_qs(href_parts.query) - - self.assertDictMatch({'limit': ['3'], 'blah': ['2:t'], - 'marker': [get_fake_uuid(2)]}, params) - - def test_get_servers_with_too_big_limit(self): - req = fakes.HTTPRequest.blank('/v2/fake/servers?limit=30') - res_dict = self.controller.index(req) - self.assertTrue('servers_links' not in res_dict) - - def test_get_servers_with_bad_limit(self): - req = fakes.HTTPRequest.blank('/v2/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' % get_fake_uuid(2) - 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_limit_and_marker(self): - url = '/v2/fake/servers?limit=2&marker=%s' % get_fake_uuid(1) - req = fakes.HTTPRequest.blank(url) - servers = self.controller.index(req)['servers'] - self.assertEqual([s['name'] for s in servers], ['server2', 'server3']) - - def test_get_servers_with_bad_marker(self): - req = fakes.HTTPRequest.blank('/v2/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(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - - req = fakes.HTTPRequest.blank('/v2/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(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - self.assertTrue('image' in search_opts) - self.assertEqual(search_opts['image'], '12345') - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - self.flags(allow_admin_api=False) - - req = fakes.HTTPRequest.blank('/v2/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, instances=None): - self.assertNotEqual(filters, None) - self.assertEqual(filters['project_id'], 'fake') - self.assertFalse(filters.get('tenant_id')) - return [fakes.stub_instance(100)] - - self.stubs.Set(nova.db, 'instance_get_all_by_filters', - fake_get_all) - self.flags(allow_admin_api=True) - - req = fakes.HTTPRequest.blank('/v2/fake/servers?tenant_id=fake', - use_admin_context=True) - res = self.controller.index(req) - - self.assertTrue('servers' in res) - - def test_get_servers_allows_flavor(self): - server_uuid = str(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - self.assertTrue('flavor' in search_opts) - # flavor is an integer ID - self.assertEqual(search_opts['flavor'], '12345') - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - self.flags(allow_admin_api=False) - - req = fakes.HTTPRequest.blank('/v2/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_allows_status(self): - server_uuid = str(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - self.assertTrue('vm_state' in search_opts) - self.assertEqual(search_opts['vm_state'], vm_states.ACTIVE) - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - self.flags(allow_admin_api=False) - - req = fakes.HTTPRequest.blank('/v2/fake/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_invalid_status(self): - """Test getting servers by invalid status""" - self.flags(allow_admin_api=False) - req = fakes.HTTPRequest.blank('/v2/fake/servers?status=unknown') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) - - def test_get_servers_allows_name(self): - server_uuid = str(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - self.assertTrue('name' in search_opts) - self.assertEqual(search_opts['name'], 'whee.*') - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - self.flags(allow_admin_api=False) - - req = fakes.HTTPRequest.blank('/v2/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(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - self.assertTrue('changes-since' in search_opts) - changes_since = datetime.datetime(2011, 1, 24, 17, 8, 1) - self.assertEqual(search_opts['changes-since'], changes_since) - self.assertTrue('deleted' not in search_opts) - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - - params = 'changes-since=2011-01-24T17:08:01Z' - req = fakes.HTTPRequest.blank('/v2/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('/v2/fake/servers?%s' % params) - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) - - def test_get_servers_unknown_or_admin_options1(self): - """Test getting servers by admin-only or unknown options. - This tests when admin_api is off. Make sure the admin and - unknown options are stripped before they get to - compute_api.get_all() - """ - - self.flags(allow_admin_api=False) - - server_uuid = str(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - # Allowed by user - self.assertTrue('name' in search_opts) - self.assertTrue('status' in search_opts) - # Allowed only by admins with admin API on - self.assertFalse('ip' in search_opts) - self.assertFalse('unknown_option' in search_opts) - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - - query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" - req = fakes.HTTPRequest.blank('/v2/fake/servers?%s' % query_str, - use_admin_context=True) - res = self.controller.index(req) - - servers = res['servers'] - self.assertEqual(len(servers), 1) - self.assertEqual(servers[0]['id'], server_uuid) - - def test_get_servers_unknown_or_admin_options2(self): - """Test getting servers by admin-only or unknown options. - This tests when admin_api is on, but context is a user. - Make sure the admin and unknown options are stripped before - they get to compute_api.get_all() - """ - - self.flags(allow_admin_api=True) - - server_uuid = str(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - # Allowed by user - self.assertTrue('name' in search_opts) - self.assertTrue('status' in search_opts) - # Allowed only by admins with admin API on - self.assertFalse('ip' in search_opts) - self.assertFalse('unknown_option' in search_opts) - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - - query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" - req = fakes.HTTPRequest.blank('/v2/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_unknown_or_admin_options3(self): - """Test getting servers by admin-only or unknown options. - This tests when admin_api is on and context is admin. - All options should be passed through to compute_api.get_all() - """ - - self.flags(allow_admin_api=True) - - server_uuid = str(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - # Allowed by user - self.assertTrue('name' in search_opts) - self.assertTrue('status' in search_opts) - # Allowed only by admins with admin API on - self.assertTrue('ip' in search_opts) - self.assertTrue('unknown_option' in search_opts) - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - - query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" - req = fakes.HTTPRequest.blank('/v2/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_admin_allows_ip(self): - """Test getting servers by ip with admin_api enabled and - admin context - """ - self.flags(allow_admin_api=True) - - server_uuid = str(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - self.assertTrue('ip' in search_opts) - self.assertEqual(search_opts['ip'], '10\..*') - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - - req = fakes.HTTPRequest.blank('/v2/fake/servers?ip=10\..*', - 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_admin_allows_ip6(self): - """Test getting servers by ip6 with admin_api enabled and - admin context - """ - self.flags(allow_admin_api=True) - - server_uuid = str(utils.gen_uuid()) - - def fake_get_all(compute_self, context, search_opts=None): - self.assertNotEqual(search_opts, None) - self.assertTrue('ip6' in search_opts) - self.assertEqual(search_opts['ip6'], 'ffff.*') - return [fakes.stub_instance(100, uuid=server_uuid)] - - self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - - req = fakes.HTTPRequest.blank('/v2/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_update_server_no_body(self): - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - req.method = 'PUT' - - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.update, req, FAKE_UUID, None) - - def test_update_server_all_attributes(self): - self.stubs.Set(nova.db, 'instance_get', - return_server_with_attributes(name='server_test', - access_ipv4='0.0.0.0', - access_ipv6='beef::0123')) - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - req.method = 'PUT' - req.content_type = 'application/json' - body = {'server': { - 'name': 'server_test', - 'accessIPv4': '0.0.0.0', - 'accessIPv6': 'beef::0123', - }} - req.body = json.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') - self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0') - self.assertEqual(res_dict['server']['accessIPv6'], 'beef::0123') - - def test_update_server_name(self): - self.stubs.Set(nova.db, 'instance_get', - return_server_with_attributes(name='server_test')) - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - req.method = 'PUT' - req.content_type = 'application/json' - body = {'server': {'name': 'server_test'}} - req.body = json.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_access_ipv4(self): - self.stubs.Set(nova.db, 'instance_get', - return_server_with_attributes(access_ipv4='0.0.0.0')) - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - req.method = 'PUT' - req.content_type = 'application/json' - body = {'server': {'accessIPv4': '0.0.0.0'}} - req.body = json.dumps(body) - 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_ipv6(self): - self.stubs.Set(nova.db, 'instance_get', - return_server_with_attributes(access_ipv6='beef::0123')) - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - req.method = 'PUT' - req.content_type = 'application/json' - body = {'server': {'accessIPv6': 'beef::0123'}} - req.body = json.dumps(body) - res_dict = self.controller.update(req, FAKE_UUID, body) - - self.assertEqual(res_dict['server']['id'], FAKE_UUID) - self.assertEqual(res_dict['server']['accessIPv6'], 'beef::0123') - - 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) - return filtered_dict - - self.stubs.Set(nova.db, 'instance_update', server_update) - self.stubs.Set(nova.db, 'instance_get', - return_server_with_attributes(name='server_test')) - - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - req.method = 'PUT' - req.content_type = "application/json" - req.body = json.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_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('/v2/fake/servers/detail') - res_dict = self.controller.detail(req) - - for i, s in enumerate(res_dict['servers']): - self.assertEqual(s['id'], get_fake_uuid(i)) - self.assertEqual(s['hostId'], '') - self.assertEqual(s['name'], 'server%d' % i) - self.assertEqual(s['image'], expected_image) - self.assertEqual(s['flavor'], expected_flavor) - self.assertEqual(s['status'], 'BUILD') - self.assertEqual(s['metadata']['seq'], str(i)) - - 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, 'fake', 'fake', i % 2, - uuid=get_fake_uuid(i)) - for i in xrange(5)] - - self.stubs.Set(nova.db, 'instance_get_all_by_filters', - return_servers_with_host) - - req = fakes.HTTPRequest.blank('/v2/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'], get_fake_uuid(i)) - self.assertEqual(s['hostId'], host_ids[i % 2]) - self.assertEqual(s['name'], 'server%d' % i) - - def test_delete_server_instance(self): - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - req.method = 'DELETE' - - self.server_delete_called = False - - new_return_server = return_server_with_attributes( - vm_state=vm_states.ACTIVE) - self.stubs.Set(nova.db, 'instance_get', new_return_server) - - def instance_destroy_mock(context, id): - self.server_delete_called = True - self.stubs.Set(nova.db, 'instance_destroy', instance_destroy_mock) - - self.controller.delete(req, FAKE_UUID) - - self.assertEqual(self.server_delete_called, True) - - def test_delete_server_instance_while_building(self): - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - req.method = 'DELETE' - - self.server_delete_called = False - - new_return_server = return_server_with_attributes( - vm_state=vm_states.BUILDING) - self.stubs.Set(nova.db, 'instance_get', new_return_server) - - def instance_destroy_mock(context, id): - self.server_delete_called = True - self.stubs.Set(nova.db, 'instance_destroy', instance_destroy_mock) - - self.assertRaises(webob.exc.HTTPConflict, - self.controller.delete, - req, - FAKE_UUID) - - def test_delete_server_instance_while_resize(self): - req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % FAKE_UUID) - req.method = 'DELETE' - - self.server_delete_called = False - - new_return_server = return_server_with_attributes( - vm_state=vm_states.RESIZING) - self.stubs.Set(nova.db, 'instance_get', new_return_server) - - def instance_destroy_mock(context, id): - self.server_delete_called = True - self.stubs.Set(nova.db, 'instance_destroy', instance_destroy_mock) - - self.assertRaises(webob.exc.HTTPConflict, - self.controller.delete, - req, - FAKE_UUID) - - -class ServerStatusTest(test.TestCase): - - def setUp(self): - super(ServerStatusTest, self).setUp() - fakes.stub_out_nw_api(self.stubs) - - self.controller = servers.Controller() - - def _get_with_state(self, vm_state, task_state=None): - new_server = return_server_with_state(vm_state, task_state) - self.stubs.Set(nova.db, 'instance_get_by_uuid', new_server) - self.stubs.Set(nova.db, 'instance_get', new_server) - - request = fakes.HTTPRequest.blank('/v2/fake/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_rebuild(self): - response = self._get_with_state(vm_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.RESIZING) - self.assertEqual(response['server']['status'], 'RESIZE') - - def test_verify_resize(self): - response = self._get_with_state(vm_states.ACTIVE, - task_states.RESIZE_VERIFY) - self.assertEqual(response['server']['status'], 'VERIFY_RESIZE') - - 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'], 'STOPPED') - - -class ServersControllerCreateTest(test.TestCase): - - def setUp(self): - """Shared implementation for tests below that create instance""" - super(ServersControllerCreateTest, self).setUp() - - self.maxDiff = None - self.flags(verbose=True) - self.config_drive = None - self.instance_cache_num = 0 - self.instance_cache = {} - - self.controller = servers.Controller() - - def instance_create(context, inst): - inst_type = instance_types.get_instance_type_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 = { - 'id': self.instance_cache_num, - 'display_name': inst['display_name'] or 'test', - 'uuid': FAKE_UUID, - 'instance_type': dict(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": self.config_drive, - "progress": 0, - "fixed_ips": [] - } - self.instance_cache[instance['id']] = instance - return instance - - def instance_get(context, instance_id): - """Stub for compute/api create() pulling in instance after - scheduling - """ - return self.instance_cache[instance_id] - - def rpc_call_wrapper(context, topic, msg): - """Stub out the scheduler creating the instance entry""" - if topic == FLAGS.scheduler_topic and \ - msg['method'] == 'run_instance': - request_spec = msg['args']['request_spec'] - num_instances = request_spec.get('num_instances', 1) - instances = [] - for x in xrange(num_instances): - instances.append(instance_create(context, - request_spec['instance_properties'])) - return instances - - def server_update(context, instance_id, params): - inst = self.instance_cache[instance_id] - inst.update(params) - return 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_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_key_pair_funcs(self.stubs) - fakes.stub_out_image_service(self.stubs) - fakes.stub_out_nw_api(self.stubs) - self.stubs.Set(utils, 'gen_uuid', fake_gen_uuid) - self.stubs.Set(nova.db, 'instance_add_security_group', - return_security_group) - self.stubs.Set(nova.db, 'project_get_networks', - project_get_networks) - self.stubs.Set(nova.db, 'instance_create', instance_create) - self.stubs.Set(nova.db, 'instance_get', instance_get) - self.stubs.Set(nova.rpc, 'cast', fake_method) - self.stubs.Set(nova.rpc, 'call', rpc_call_wrapper) - self.stubs.Set(nova.db, 'instance_update', server_update) - self.stubs.Set(nova.db, 'queue_get_for', queue_get_for) - self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip', - fake_method) - self.stubs.Set(nova.compute.api.API, "_find_host", find_host) - - def _test_create_instance(self): - image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' - body = dict(server=dict( - name='server_test', imageRef=image_uuid, flavorRef=2, - metadata={'hello': 'world', 'open': 'stack'}, - personality={})) - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - server = self.controller.create(req, body).obj['server'] - - self.assertEqual(FLAGS.password_length, len(server['adminPass'])) - self.assertEqual(FAKE_UUID, server['id']) - - 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': { - 'min_count': 2, - 'name': 'server_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, - 'metadata': {'hello': 'world', - 'open': 'stack'}, - 'personality': [] - } - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body).obj - - self.assertEqual(FAKE_UUID, res["server"]["id"]) - self.assertEqual(12, len(res["server"]["adminPass"])) - - def test_create_multiple_instances_resv_id_return(self): - """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': { - 'min_count': 2, - 'name': 'server_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, - 'metadata': {'hello': 'world', - 'open': 'stack'}, - 'personality': [], - 'return_reservation_id': True - } - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - reservation_id = res.get('reservation_id') - self.assertNotEqual(reservation_id, "") - self.assertNotEqual(reservation_id, None) - self.assertTrue(len(reservation_id) > 1) - - def test_create_instance_with_user_supplied_reservation_id(self): - """Non-admin supplied reservation_id should be ignored.""" - 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, - 'metadata': {'hello': 'world', - 'open': 'stack'}, - 'personality': [], - 'reservation_id': 'myresid', - 'return_reservation_id': True - } - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - self.assertIn('reservation_id', res) - self.assertNotEqual(res['reservation_id'], 'myresid') - - def test_create_instance_with_admin_supplied_reservation_id(self): - """Admin supplied reservation_id should be honored.""" - 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, - 'metadata': {'hello': 'world', - 'open': 'stack'}, - 'personality': [], - 'reservation_id': 'myresid', - 'return_reservation_id': True - } - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers', - use_admin_context=True) - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - reservation_id = res['reservation_id'] - self.assertEqual(reservation_id, "myresid") - - 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_instance_with_access_ip(self): - # proper local hrefs must start with 'http://localhost/v2/' - image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - image_href = 'http://localhost/v2/fake/images/%s' % image_uuid - flavor_ref = 'http://localhost/fake/flavors/3' - access_ipv4 = '1.2.3.4' - access_ipv6 = 'fead::1234' - expected_flavor = { - "id": "3", - "links": [ - { - "rel": "bookmark", - "href": 'http://localhost/fake/flavors/3', - }, - ], - } - expected_image = { - "id": image_uuid, - "links": [ - { - "rel": "bookmark", - "href": 'http://localhost/fake/images/%s' % image_uuid, - }, - ], - } - body = { - 'server': { - 'name': 'server_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, - 'accessIPv4': access_ipv4, - 'accessIPv6': access_ipv6, - 'metadata': { - 'hello': 'world', - 'open': 'stack', - }, - 'personality': [ - { - "path": "/etc/banner.txt", - "contents": "MQ==", - }, - ], - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body).obj - - server = res['server'] - self.assertEqual(FLAGS.password_length, len(server['adminPass'])) - self.assertEqual(FAKE_UUID, server['id']) - - def test_create_instance(self): - # proper local hrefs must start with 'http://localhost/v2/' - image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - image_href = 'http://localhost/v2/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', - }, - 'personality': [ - { - "path": "/etc/banner.txt", - "contents": "MQ==", - }, - ], - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body).obj - - server = res['server'] - self.assertEqual(FLAGS.password_length, len(server['adminPass'])) - self.assertEqual(FAKE_UUID, server['id']) - - def test_create_instance_invalid_key_name(self): - image_href = 'http://localhost/v2/images/2' - flavor_ref = 'http://localhost/flavors/3' - body = dict(server=dict( - name='server_test', imageRef=image_href, flavorRef=flavor_ref, - key_name='nonexistentkey')) - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, req, body) - - def test_create_instance_valid_key_name(self): - image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - flavor_ref = 'http://localhost/flavors/3' - body = dict(server=dict( - name='server_test', imageRef=image_href, flavorRef=flavor_ref, - key_name='key')) - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body).obj - - self.assertEqual(FAKE_UUID, res["server"]["id"]) - self.assertEqual(12, len(res["server"]["adminPass"])) - - def test_create_instance_invalid_flavor_href(self): - image_href = 'http://localhost/v2/images/2' - flavor_ref = 'http://localhost/v2/flavors/asdf' - body = dict(server=dict( - name='server_test', imageRef=image_href, flavorRef=flavor_ref, - metadata={'hello': 'world', 'open': 'stack'}, - personality={})) - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, req, body) - - def test_create_instance_invalid_flavor_id_int(self): - image_href = 'http://localhost/v2/fake/images/2' - flavor_ref = -1 - body = dict(server=dict( - name='server_test', imageRef=image_href, flavorRef=flavor_ref, - metadata={'hello': 'world', 'open': 'stack'}, - personality={})) - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, req, body) - - def test_create_instance_bad_flavor_href(self): - image_href = 'http://localhost/v2/images/2' - flavor_ref = 'http://localhost/v2/flavors/17' - body = dict(server=dict( - name='server_test', imageRef=image_href, flavorRef=flavor_ref, - metadata={'hello': 'world', 'open': 'stack'}, - personality={})) - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, req, body) - - def test_create_instance_with_config_drive(self): - self.config_drive = True - image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - flavor_ref = 'http://localhost/v2/fake/flavors/3' - body = { - 'server': { - 'name': 'config_drive_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, - 'metadata': { - 'hello': 'world', - 'open': 'stack', - }, - 'personality': {}, - 'config_drive': True, - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body).obj - - server = res['server'] - self.assertEqual(FAKE_UUID, server['id']) - - def test_create_instance_with_config_drive_as_id(self): - self.config_drive = 2 - image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - flavor_ref = 'http://localhost/v2/fake/flavors/3' - body = { - 'server': { - 'name': 'config_drive_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, - 'metadata': { - 'hello': 'world', - 'open': 'stack', - }, - 'personality': {}, - 'config_drive': image_href, - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body).obj - - server = res['server'] - self.assertEqual(FAKE_UUID, server['id']) - - def test_create_instance_with_bad_config_drive(self): - self.config_drive = "asdf" - image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - flavor_ref = 'http://localhost/v2/fake/flavors/3' - body = { - 'server': { - 'name': 'config_drive_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, - 'metadata': { - 'hello': 'world', - 'open': 'stack', - }, - 'personality': {}, - 'config_drive': 'asdf', - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, req, body) - - def test_create_instance_without_config_drive(self): - image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - flavor_ref = 'http://localhost/v2/fake/flavors/3' - body = { - 'server': { - 'name': 'config_drive_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, - 'metadata': { - 'hello': 'world', - 'open': 'stack', - }, - 'personality': {}, - 'config_drive': True, - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body).obj - - server = res['server'] - self.assertEqual(FAKE_UUID, server['id']) - - def test_create_instance_bad_href(self): - image_href = 'asdf' - flavor_ref = 'http://localhost/v2/flavors/3' - body = dict(server=dict( - name='server_test', imageRef=image_href, flavorRef=flavor_ref, - metadata={'hello': 'world', 'open': 'stack'}, - personality={})) - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, req, body) - - def test_create_instance_local_href(self): - image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - flavor_ref = 'http://localhost/v2/flavors/3' - body = { - 'server': { - 'name': 'server_test', - 'imageRef': image_uuid, - 'flavorRef': flavor_ref, - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body).obj - - server = res['server'] - self.assertEqual(FAKE_UUID, server['id']) - - def test_create_instance_admin_pass(self): - image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - body = { - 'server': { - 'name': 'server_test', - 'imageRef': image_uuid, - 'flavorRef': 3, - 'adminPass': 'testpass', - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = "application/json" - res = self.controller.create(req, body).obj - - server = res['server'] - self.assertEqual(server['adminPass'], body['server']['adminPass']) - - def test_create_instance_admin_pass_empty(self): - body = { - 'server': { - 'name': 'server_test', - 'imageRef': 3, - 'flavorRef': 3, - 'adminPass': '', - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, req, body) - - def test_create_instance_malformed_entity(self): - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - body = {'server': 'string'} - req.body = json.dumps(body) - req.headers['content-type'] = "application/json" - - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, req, body) - - def test_create_location(self): - selfhref = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID - bookhref = 'http://localhost/fake/servers/%s' % FAKE_UUID - image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - image_href = 'http://localhost/v2/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', - }, - 'personality': [ - { - "path": "/etc/banner.txt", - "contents": "MQ==", - }, - ], - }, - } - - req = fakes.HTTPRequest.blank('/v2/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = 'application/json' - robj = self.controller.create(req, body) - - self.assertEqual(robj['Location'], selfhref) - - -class TestServerCreateRequestXMLDeserializer(test.TestCase): - - def setUp(self): - super(TestServerCreateRequestXMLDeserializer, self).setUp() - self.deserializer = servers.CreateDeserializer() - - def test_minimal_request(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "2", - }, - } - self.assertEquals(request['body'], expected) - - def test_access_ipv4(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "2", - "accessIPv4": "1.2.3.4", - }, - } - self.assertEquals(request['body'], expected) - - def test_access_ipv6(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "2", - "accessIPv6": "fead::1234", - }, - } - self.assertEquals(request['body'], expected) - - def test_access_ip(self): - serial_request = """ -""" - 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.assertEquals(request['body'], expected) - - def test_admin_pass(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "2", - "adminPass": "1234", - }, - } - self.assertEquals(request['body'], expected) - - def test_image_link(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "server": { - "name": "new-server-test", - "imageRef": "http://localhost:8774/v2/images/2", - "flavorRef": "3", - }, - } - self.assertEquals(request['body'], expected) - - def test_flavor_link(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "http://localhost:8774/v2/flavors/3", - }, - } - self.assertEquals(request['body'], expected) - - def test_empty_metadata_personality(self): - serial_request = """ - - - -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "2", - "metadata": {}, - "personality": [], - }, - } - self.assertEquals(request['body'], expected) - - def test_multiple_metadata_items(self): - serial_request = """ - - - two - snack - -""" - request = self.deserializer.deserialize(serial_request) - expected = { - "server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "2", - "metadata": {"one": "two", "open": "snack"}, - }, - } - self.assertEquals(request['body'], expected) - - def test_multiple_personality_files(self): - serial_request = """ - - - MQ== - Mg== - -""" - 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.assertDictMatch(request['body'], expected) - - def test_spec_request(self): - image_bookmark_link = "http://servers.api.openstack.org/1234/" + \ - "images/52415800-8b69-11e0-9b19-734f6f006e54" - serial_request = """ - - - Apache1 - - - Mg== - -""" % (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.assertEquals(request['body'], expected) - - def test_request_with_empty_networks(self): - serial_request = """ - - -""" - request = self.deserializer.deserialize(serial_request) - expected = {"server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "1", - "networks": [], - }} - self.assertEquals(request['body'], expected) - - def test_request_with_one_network(self): - serial_request = """ - - - - -""" - 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.assertEquals(request['body'], expected) - - def test_request_with_two_networks(self): - serial_request = """ - - - - - -""" - 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.assertEquals(request['body'], expected) - - def test_request_with_second_network_node_ignored(self): - serial_request = """ - - - - - - - -""" - 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.assertEquals(request['body'], expected) - - def test_request_with_one_network_missing_id(self): - serial_request = """ - - - - -""" - request = self.deserializer.deserialize(serial_request) - expected = {"server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "1", - "networks": [{"fixed_ip": "10.0.1.12"}], - }} - self.assertEquals(request['body'], expected) - - def test_request_with_one_network_missing_fixed_ip(self): - serial_request = """ - - - - -""" - request = self.deserializer.deserialize(serial_request) - expected = {"server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "1", - "networks": [{"uuid": "1"}], - }} - self.assertEquals(request['body'], expected) - - def test_request_with_one_network_empty_id(self): - serial_request = """ - - - - - """ - 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.assertEquals(request['body'], expected) - - def test_request_with_one_network_empty_fixed_ip(self): - serial_request = """ - - - - - """ - request = self.deserializer.deserialize(serial_request) - expected = {"server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "1", - "networks": [{"uuid": "1", "fixed_ip": ""}], - }} - self.assertEquals(request['body'], expected) - - def test_request_with_networks_duplicate_ids(self): - serial_request = """ - - - - - - """ - 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.assertEquals(request['body'], expected) - - -class TestAddressesXMLSerialization(test.TestCase): - - index_serializer = nova.api.openstack.v2.ips.AddressesTemplate() - show_serializer = nova.api.openstack.v2.ips.NetworkTemplate() - - def test_xml_declaration(self): - fixture = { - 'network_2': [ - {'addr': '192.168.0.1', 'version': 4}, - {'addr': 'fe80::beef', 'version': 6}, - ], - } - output = self.show_serializer.serialize(fixture) - has_dec = output.startswith("") - self.assertTrue(has_dec) - - def test_show(self): - fixture = { - 'network_2': [ - {'addr': '192.168.0.1', 'version': 4}, - {'addr': 'fe80::beef', 'version': 6}, - ], - } - output = self.show_serializer.serialize(fixture) - root = etree.XML(output) - network = fixture['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): - - def setUp(self): - super(ServersViewBuilderTest, self).setUp() - self.flags(use_ipv6=True) - self.instance = fakes.stub_instance( - id=1, - image_ref="5", - uuid="deadbeef-feed-edee-beef-d0ea7beefedd", - display_name="test_server", - public_ips=["192.168.0.3"], - private_ips=["172.19.0.1"], - include_fake_metadata=False) - - self.uuid = self.instance['uuid'] - self.view_builder = nova.api.openstack.v2.views.servers.ViewBuilder() - self.request = fakes.HTTPRequest.blank("/v2") - - def test_build_server(self): - self_link = "http://localhost/v2/fake/servers/%s" % self.uuid - bookmark_link = "http://localhost/fake/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.assertDictMatch(output, 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/v2/fake/servers/%s" % - self.uuid, - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/servers/%s" % self.uuid, - }, - ], - } - } - - output = self.view_builder.basic(self.request, self.instance) - self.assertDictMatch(output, expected_server) - - def test_build_server_detail(self): - image_bookmark = "http://localhost/fake/images/5" - flavor_bookmark = "http://localhost/fake/flavors/1" - self_link = "http://localhost/v2/fake/servers/%s" % self.uuid - bookmark_link = "http://localhost/fake/servers/%s" % self.uuid - expected_server = { - "server": { - "id": self.uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "progress": 0, - "name": "test_server", - "status": "BUILD", - "accessIPv4": "", - "accessIPv6": "", - "hostId": '', - "key_name": '', - "image": { - "id": "5", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - 'private': [ - {'version': 4, 'addr': '172.19.0.1'} - ], - 'public': [ - {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, - {'version': 4, 'addr': '192.168.0.3'}, - ], - }, - "metadata": {}, - "config_drive": None, - "links": [ - { - "rel": "self", - "href": self_link, - }, - { - "rel": "bookmark", - "href": bookmark_link, - }, - ], - } - } - - output = self.view_builder.show(self.request, self.instance) - self.assertDictMatch(output, expected_server) - - def test_build_server_detail_with_fault(self): - self.instance['vm_state'] = vm_states.ERROR - self.instance['fault'] = { - 'code': 404, - 'instance_uuid': self.uuid, - 'message': "HTTPNotFound", - 'details': "Stock details for test", - 'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0), - } - - image_bookmark = "http://localhost/fake/images/5" - flavor_bookmark = "http://localhost/fake/flavors/1" - self_link = "http://localhost/v2/fake/servers/%s" % self.uuid - bookmark_link = "http://localhost/fake/servers/%s" % self.uuid - expected_server = { - "server": { - "id": self.uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "name": "test_server", - "status": "ERROR", - "accessIPv4": "", - "accessIPv6": "", - "hostId": '', - "key_name": '', - "image": { - "id": "5", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - 'private': [ - {'version': 4, 'addr': '172.19.0.1'} - ], - 'public': [ - {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, - {'version': 4, 'addr': '192.168.0.3'}, - ], - }, - "metadata": {}, - "config_drive": None, - "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", - }, - } - } - - output = self.view_builder.show(self.request, self.instance) - self.assertDictMatch(output, expected_server) - - def test_build_server_detail_with_fault_but_active(self): - self.instance['vm_state'] = vm_states.ACTIVE - self.instance['progress'] = 100 - self.instance['fault'] = { - 'code': 404, - 'instance_uuid': self.uuid, - 'message': "HTTPNotFound", - 'details': "Stock details for test", - 'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0), - } - - image_bookmark = "http://localhost/fake/images/5" - flavor_bookmark = "http://localhost/fake/flavors/1" - self_link = "http://localhost/v2/fake/servers/%s" % self.uuid - bookmark_link = "http://localhost/fake/servers/%s" % self.uuid - expected_server = { - "server": { - "id": self.uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "progress": 100, - "name": "test_server", - "status": "ACTIVE", - "accessIPv4": "", - "accessIPv6": "", - "hostId": '', - "key_name": '', - "image": { - "id": "5", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - 'private': [ - {'version': 4, 'addr': '172.19.0.1'} - ], - 'public': [ - {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, - {'version': 4, 'addr': '192.168.0.3'}, - ], - }, - "metadata": {}, - "config_drive": None, - "links": [ - { - "rel": "self", - "href": self_link, - }, - { - "rel": "bookmark", - "href": bookmark_link, - }, - ], - } - } - - output = self.view_builder.show(self.request, self.instance) - self.assertDictMatch(output, expected_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/fake/images/5" - flavor_bookmark = "http://localhost/fake/flavors/1" - self_link = "http://localhost/v2/fake/servers/%s" % self.uuid - bookmark_link = "http://localhost/fake/servers/%s" % self.uuid - expected_server = { - "server": { - "id": self.uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "progress": 100, - "name": "test_server", - "status": "ACTIVE", - "accessIPv4": "", - "accessIPv6": "", - "hostId": '', - "key_name": '', - "image": { - "id": "5", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - 'private': [ - {'version': 4, 'addr': '172.19.0.1'} - ], - 'public': [ - {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, - {'version': 4, 'addr': '192.168.0.3'}, - ], - }, - "metadata": {}, - "config_drive": None, - "links": [ - { - "rel": "self", - "href": self_link, - }, - { - "rel": "bookmark", - "href": bookmark_link, - }, - ], - } - } - - output = self.view_builder.show(self.request, self.instance) - self.assertDictMatch(output, expected_server) - - def test_build_server_detail_with_accessipv4(self): - - self.instance['access_ip_v4'] = '1.2.3.4' - - image_bookmark = "http://localhost/fake/images/5" - flavor_bookmark = "http://localhost/fake/flavors/1" - self_link = "http://localhost/v2/fake/servers/%s" % self.uuid - bookmark_link = "http://localhost/fake/servers/%s" % self.uuid - expected_server = { - "server": { - "id": self.uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "progress": 0, - "name": "test_server", - "key_name": "", - "status": "BUILD", - "hostId": '', - "image": { - "id": "5", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - 'private': [ - {'version': 4, 'addr': '172.19.0.1'} - ], - 'public': [ - {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, - {'version': 4, 'addr': '192.168.0.3'}, - ], - }, - "metadata": {}, - "config_drive": None, - "accessIPv4": "1.2.3.4", - "accessIPv6": "", - "links": [ - { - "rel": "self", - "href": self_link, - }, - { - "rel": "bookmark", - "href": bookmark_link, - }, - ], - } - } - - output = self.view_builder.show(self.request, self.instance) - self.assertDictMatch(output, expected_server) - - def test_build_server_detail_with_accessipv6(self): - - self.instance['access_ip_v6'] = 'fead::1234' - - image_bookmark = "http://localhost/fake/images/5" - flavor_bookmark = "http://localhost/fake/flavors/1" - self_link = "http://localhost/v2/fake/servers/%s" % self.uuid - bookmark_link = "http://localhost/fake/servers/%s" % self.uuid - expected_server = { - "server": { - "id": self.uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "progress": 0, - "name": "test_server", - "key_name": "", - "status": "BUILD", - "hostId": '', - "image": { - "id": "5", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - 'private': [ - {'version': 4, 'addr': '172.19.0.1'} - ], - 'public': [ - {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, - {'version': 4, 'addr': '192.168.0.3'}, - ] - }, - "metadata": {}, - "config_drive": None, - "accessIPv4": "", - "accessIPv6": "fead::1234", - "links": [ - { - "rel": "self", - "href": self_link, - }, - { - "rel": "bookmark", - "href": bookmark_link, - }, - ], - } - } - - output = self.view_builder.show(self.request, self.instance) - self.assertDictMatch(output, expected_server) - - def test_build_server_detail_with_metadata(self): - - metadata = [] - metadata.append(InstanceMetadata(key="Open", value="Stack")) - metadata.append(InstanceMetadata(key="Number", value=1)) - self.instance['metadata'] = metadata - - image_bookmark = "http://localhost/fake/images/5" - flavor_bookmark = "http://localhost/fake/flavors/1" - self_link = "http://localhost/v2/fake/servers/%s" % self.uuid - bookmark_link = "http://localhost/fake/servers/%s" % self.uuid - expected_server = { - "server": { - "id": self.uuid, - "user_id": "fake", - "tenant_id": "fake", - "updated": "2010-11-11T11:00:00Z", - "created": "2010-10-10T12:00:00Z", - "progress": 0, - "name": "test_server", - "status": "BUILD", - "accessIPv4": "", - "accessIPv6": "", - "hostId": '', - "key_name": '', - "image": { - "id": "5", - "links": [ - { - "rel": "bookmark", - "href": image_bookmark, - }, - ], - }, - "flavor": { - "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], - }, - "addresses": { - 'private': [ - {'version': 4, 'addr': '172.19.0.1'} - ], - 'public': [ - {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, - {'version': 4, 'addr': '192.168.0.3'}, - ] - }, - "metadata": { - "Open": "Stack", - "Number": "1", - }, - "config_drive": None, - "links": [ - { - "rel": "self", - "href": self_link, - }, - { - "rel": "bookmark", - "href": bookmark_link, - }, - ], - } - } - - output = self.view_builder.show(self.request, self.instance) - self.assertDictMatch(output, expected_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' - - def setUp(self): - self.maxDiff = None - test.TestCase.setUp(self) - - def test_xml_declaration(self): - serializer = servers.ServerTemplate() - - fixture = { - "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", - "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', - }, - ], - } - } - - output = serializer.serialize(fixture) - print output - has_dec = output.startswith("") - self.assertTrue(has_dec) - - def test_show(self): - serializer = servers.ServerTemplate() - - fixture = { - "server": { - "id": FAKE_UUID, - "user_id": "fake", - "tenant_id": "fake", - 'created': self.TIMESTAMP, - 'updated': self.TIMESTAMP, - "progress": 0, - "name": "test_server", - "status": "BUILD", - "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', - "key_name": '', - "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', - }, - ], - } - } - - output = serializer.serialize(fixture) - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'server') - - server_dict = fixture['server'] - - for key in ['name', 'id', 'created', 'accessIPv4', - 'updated', 'progress', 'status', 'hostId', - 'accessIPv6']: - self.assertEqual(root.get(key), str(server_dict[key])) - - 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 test_create(self): - serializer = servers.FullServerTemplate() - - fixture = { - "server": { - "id": FAKE_UUID, - "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", - "adminPass": "test_password", - "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', - }, - ], - } - } - - output = serializer.serialize(fixture) - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'server') - - server_dict = fixture['server'] - - for key in ['name', 'id', 'created', 'accessIPv4', - 'updated', 'progress', 'status', 'hostId', - 'accessIPv6', 'adminPass']: - self.assertEqual(root.get(key), str(server_dict[key])) - - 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 test_index(self): - serializer = servers.MinimalServersTemplate() - - uuid1 = get_fake_uuid(1) - uuid2 = 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": get_fake_uuid(1), - "name": "test_server", - 'links': [ - { - 'href': expected_server_href, - 'rel': 'self', - }, - { - 'href': expected_server_bookmark, - 'rel': 'bookmark', - }, - ], - }, - { - "id": 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) - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'servers_index') - 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 = get_fake_uuid(1) - uuid2 = 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": get_fake_uuid(1), - "name": "test_server", - 'links': [ - { - 'href': expected_server_href, - 'rel': 'self', - }, - { - 'href': expected_server_bookmark, - 'rel': 'bookmark', - }, - ], - }, - { - "id": 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) - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'servers_index') - 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 = 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 = 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": 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", - }, - ], - }, - "metadata": { - "Number": "1", - }, - "links": [ - { - "href": expected_server_href, - "rel": "self", - }, - { - "href": expected_server_bookmark, - "rel": "bookmark", - }, - ], - }, - { - "id": 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", - }, - ], - }, - "metadata": { - "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] - - for key in ['name', 'id', 'created', 'accessIPv4', - 'updated', 'progress', 'status', 'hostId', - 'accessIPv6']: - 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) - - metadata_root = server_elem.find('{0}metadata'.format(NS)) - metadata_elems = metadata_root.findall('{0}meta'.format(NS)) - 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 = server_elem.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 = server_elem.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 = server_elem.find('{0}addresses'.format(NS)) - addresses_dict = server_dict['addresses'] - network_elems = addresses_root.findall('{0}network'.format(NS)) - 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 test_update(self): - serializer = servers.ServerTemplate() - - fixture = { - "server": { - "id": FAKE_UUID, - "user_id": "fake", - "tenant_id": "fake", - 'created': self.TIMESTAMP, - 'updated': self.TIMESTAMP, - "progress": 0, - "name": "test_server", - "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', - }, - ], - "fault": { - "code": 500, - "created": self.TIMESTAMP, - "message": "Error Message", - "details": "Fault details", - } - } - } - - output = serializer.serialize(fixture) - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'server') - - server_dict = fixture['server'] - - for key in ['name', 'id', 'created', 'accessIPv4', - 'updated', 'progress', 'status', 'hostId', - 'accessIPv6']: - self.assertEqual(root.get(key), str(server_dict[key])) - - 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'])) - - 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() - - fixture = { - "server": { - "id": FAKE_UUID, - "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", - "adminPass": "test_password", - "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', - }, - ], - } - } - - output = serializer.serialize(fixture) - root = etree.XML(output) - xmlutil.validate_schema(root, 'server') - - server_dict = fixture['server'] - - for key in ['name', 'id', 'created', 'accessIPv4', - 'updated', 'progress', 'status', 'hostId', - 'accessIPv6', 'adminPass']: - self.assertEqual(root.get(key), str(server_dict[key])) - - 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'])) diff --git a/nova/tests/api/openstack/v2/test_urlmap.py b/nova/tests/api/openstack/v2/test_urlmap.py deleted file mode 100644 index 82a870a50..000000000 --- a/nova/tests/api/openstack/v2/test_urlmap.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json -import webob - -from nova import log as logging -from nova import test -from nova.tests.api.openstack import fakes - -LOG = logging.getLogger('nova.tests.api.openstack.v2.test_urlmap') - - -class UrlmapTest(test.TestCase): - def setUp(self): - super(UrlmapTest, self).setUp() - fakes.stub_out_rate_limiting(self.stubs) - - 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()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - body = json.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()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - body = json.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()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - body = json.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()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - body = json.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()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - body = json.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()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - body = json.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()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - body = json.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()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - body = json.loads(res.body) - self.assertEqual(body['image']['id'], - 'cedef40a-ed67-4d10-800e-17455edce175') diff --git a/nova/tests/api/openstack/v2/test_versions.py b/nova/tests/api/openstack/v2/test_versions.py deleted file mode 100644 index 7e250471a..000000000 --- a/nova/tests/api/openstack/v2/test_versions.py +++ /dev/null @@ -1,668 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json - -import feedparser -from lxml import etree -import stubout -import webob - -from nova.api.openstack.v2 import versions -from nova.api.openstack.v2 import views -from nova.api.openstack import xmlutil -from nova import context -from nova import test -from nova.tests.api.openstack import common -from nova.tests.api.openstack import fakes -from nova import utils - - -NS = { - 'atom': 'http://www.w3.org/2005/Atom', - 'ns': 'http://docs.openstack.org/compute/api/v1.1' -} - -VERSIONS = { - "v2.0": { - "id": "v2.0", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf", - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl", - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.compute+xml;version=2", - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute+json;version=2", - }, - ], - }, -} - - -class VersionsTest(test.TestCase): - def setUp(self): - super(VersionsTest, self).setUp() - self.context = context.get_admin_context() - self.stubs = stubout.StubOutForTesting() - fakes.stub_out_auth(self.stubs) - #Stub out VERSIONS - self.old_versions = versions.VERSIONS - versions.VERSIONS = VERSIONS - - def tearDown(self): - versions.VERSIONS = self.old_versions - super(VersionsTest, self).tearDown() - - 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 = json.loads(res.body)["versions"] - expected = [ - { - "id": "v2.0", - "status": "CURRENT", - "updated": "2011-01-21T11: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): - req = webob.Request.blank('/v2/') - req.accept = "application/json" - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - version = json.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": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf", - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl", - }, - ], - "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_content_type(self): - req = webob.Request.blank('/') - req.accept = "application/json;version=2" - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - self.assertEqual(res.content_type, "application/json") - version = json.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": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf", - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl", - }, - ], - "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_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 = 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) - print 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), 1) - - for i, v in enumerate(['v2.0']): - version = versions[i] - expected = 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), 3) - self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') - self.assertEqual(entry.links[0]['rel'], 'self') - self.assertEqual(entry.links[1], { - 'href': 'http://docs.rackspacecloud.com/servers/api/v1.1/'\ - 'cs-devguide-20110125.pdf', - 'type': 'application/pdf', - 'rel': 'describedby'}) - self.assertEqual(entry.links[2], { - 'href': 'http://docs.rackspacecloud.com/servers/api/v1.1/'\ - 'application.wadl', - 'type': 'application/vnd.sun.wadl+xml', - '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, '2011-01-21T11: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), 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), 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" - }, - ], - }, - ], } - - self.assertDictMatch(expected, json.loads(res.body)) - - 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), 1) - - 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, - 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'}])) - - 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(utils.gen_uuid()) - 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" - }, - ], - }, - ], } - - self.assertDictMatch(expected, json.loads(res.body)) - - -class VersionsViewBuilderTests(test.TestCase): - 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() - - self.assertEqual(actual, expected) - - -class VersionsSerializerTests(test.TestCase): - 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": 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": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf", - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl", - }, - ], - "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), 3) - self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') - self.assertEqual(entry.links[0]['rel'], 'self') - self.assertEqual(entry.links[1], { - 'rel': 'describedby', - 'type': 'application/pdf', - 'href': 'http://docs.rackspacecloud.com/' - 'servers/api/v1.1/cs-devguide-20110125.pdf'}) - self.assertEqual(entry.links[2], { - 'rel': 'describedby', - 'type': 'application/vnd.sun.wadl+xml', - 'href': 'http://docs.rackspacecloud.com/' - 'servers/api/v1.1/application.wadl', - }) diff --git a/nova/tests/api/openstack/volume/__init__.py b/nova/tests/api/openstack/volume/__init__.py new file mode 100644 index 000000000..00fcfbb00 --- /dev/null +++ b/nova/tests/api/openstack/volume/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/nova/tests/api/openstack/volume/test_snapshots.py b/nova/tests/api/openstack/volume/test_snapshots.py new file mode 100644 index 000000000..06543ae5f --- /dev/null +++ b/nova/tests/api/openstack/volume/test_snapshots.py @@ -0,0 +1,299 @@ +# 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. + +import datetime +import json +import stubout + +from lxml import etree +import webob + +from nova.api.openstack.volume import snapshots +from nova import context +from nova import exception +from nova import flags +from nova import log as logging +from nova import test +from nova import volume +from nova.tests.api.openstack import fakes + +FLAGS = flags.FLAGS + +LOG = logging.getLogger('nova.tests.api.openstack.snapshot') + +_last_param = {} + + +def _get_default_snapshot_param(): + return { + 'id': 123, + 'volume_id': 12, + 'status': 'available', + 'volume_size': 100, + 'created_at': None, + 'display_name': 'Default name', + 'display_description': 'Default description', + } + + +def stub_snapshot_create(self, context, volume_id, name, description): + global _last_param + snapshot = _get_default_snapshot_param() + snapshot['volume_id'] = volume_id + snapshot['display_name'] = name + snapshot['display_description'] = description + + LOG.debug(_("_create: %s"), snapshot) + _last_param = snapshot + return snapshot + + +def stub_snapshot_delete(self, context, snapshot_id): + global _last_param + _last_param = dict(snapshot_id=snapshot_id) + + LOG.debug(_("_delete: %s"), locals()) + if snapshot_id != '123': + raise exception.NotFound + + +def stub_snapshot_get(self, context, snapshot_id): + global _last_param + _last_param = dict(snapshot_id=snapshot_id) + + LOG.debug(_("_get: %s"), locals()) + if snapshot_id != '123': + raise exception.NotFound + + param = _get_default_snapshot_param() + param['id'] = snapshot_id + return param + + +def stub_snapshot_get_all(self, context): + LOG.debug(_("_get_all: %s"), locals()) + param = _get_default_snapshot_param() + param['id'] = 123 + return [param] + + +class SnapshotApiTest(test.TestCase): + def setUp(self): + super(SnapshotApiTest, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.reset_fake_data() + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + self.stubs.Set(volume.api.API, "create_snapshot", stub_snapshot_create) + self.stubs.Set(volume.api.API, "create_snapshot_force", + stub_snapshot_create) + self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete) + self.stubs.Set(volume.api.API, "get_snapshot", stub_snapshot_get) + self.stubs.Set(volume.api.API, "get_all_snapshots", + stub_snapshot_get_all) + + self.context = context.get_admin_context() + + def tearDown(self): + self.stubs.UnsetAll() + super(SnapshotApiTest, self).tearDown() + + def test_snapshot_create(self): + global _last_param + _last_param = {} + + snapshot = {"volume_id": 12, + "force": False, + "display_name": "Snapshot Test Name", + "display_description": "Snapshot Test Desc"} + body = dict(snapshot=snapshot) + req = webob.Request.blank('/v1.1/fake/os-snapshots') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + LOG.debug(_("test_snapshot_create: param=%s"), _last_param) + self.assertEqual(resp.status_int, 200) + + # Compare if parameters were correctly passed to stub + self.assertEqual(_last_param['display_name'], "Snapshot Test Name") + self.assertEqual(_last_param['display_description'], + "Snapshot Test Desc") + + resp_dict = json.loads(resp.body) + LOG.debug(_("test_snapshot_create: resp_dict=%s"), resp_dict) + self.assertTrue('snapshot' in resp_dict) + self.assertEqual(resp_dict['snapshot']['displayName'], + snapshot['display_name']) + self.assertEqual(resp_dict['snapshot']['displayDescription'], + snapshot['display_description']) + + def test_snapshot_create_force(self): + global _last_param + _last_param = {} + + snapshot = {"volume_id": 12, + "force": True, + "display_name": "Snapshot Test Name", + "display_description": "Snapshot Test Desc"} + body = dict(snapshot=snapshot) + req = webob.Request.blank('/v1.1/fake/os-snapshots') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + LOG.debug(_("test_snapshot_create_force: param=%s"), _last_param) + self.assertEqual(resp.status_int, 200) + + # Compare if parameters were correctly passed to stub + self.assertEqual(_last_param['display_name'], "Snapshot Test Name") + self.assertEqual(_last_param['display_description'], + "Snapshot Test Desc") + + resp_dict = json.loads(resp.body) + LOG.debug(_("test_snapshot_create_force: resp_dict=%s"), resp_dict) + self.assertTrue('snapshot' in resp_dict) + self.assertEqual(resp_dict['snapshot']['displayName'], + snapshot['display_name']) + self.assertEqual(resp_dict['snapshot']['displayDescription'], + snapshot['display_description']) + + def test_snapshot_delete(self): + global _last_param + _last_param = {} + + snapshot_id = 123 + req = webob.Request.blank('/v1.1/fake/os-snapshots/%d' % snapshot_id) + req.method = 'DELETE' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + def test_snapshot_delete_invalid_id(self): + global _last_param + _last_param = {} + + snapshot_id = 234 + req = webob.Request.blank('/v1.1/fake/os-snapshots/%d' % snapshot_id) + req.method = 'DELETE' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 404) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + def test_snapshot_show(self): + global _last_param + _last_param = {} + + snapshot_id = 123 + req = webob.Request.blank('/v1.1/fake/os-snapshots/%d' % snapshot_id) + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + + LOG.debug(_("test_snapshot_show: resp=%s"), resp) + self.assertEqual(resp.status_int, 200) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + resp_dict = json.loads(resp.body) + self.assertTrue('snapshot' in resp_dict) + self.assertEqual(resp_dict['snapshot']['id'], str(snapshot_id)) + + def test_snapshot_show_invalid_id(self): + global _last_param + _last_param = {} + + snapshot_id = 234 + req = webob.Request.blank('/v1.1/fake/os-snapshots/%d' % snapshot_id) + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 404) + self.assertEqual(str(_last_param['snapshot_id']), str(snapshot_id)) + + def test_snapshot_detail(self): + req = webob.Request.blank('/v1.1/fake/os-snapshots/detail') + req.method = 'GET' + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + resp_dict = json.loads(resp.body) + LOG.debug(_("test_snapshot_detail: resp_dict=%s"), resp_dict) + self.assertTrue('snapshots' in resp_dict) + resp_snapshots = resp_dict['snapshots'] + self.assertEqual(len(resp_snapshots), 1) + + resp_snapshot = resp_snapshots.pop() + self.assertEqual(resp_snapshot['id'], 123) + + +class SnapshotSerializerTest(test.TestCase): + 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 = snapshots.SnapshotSerializer() + raw_snapshot = dict( + id='snap_id', + status='snap_status', + size=1024, + createdAt=datetime.datetime.now(), + displayName='snap_name', + displayDescription='snap_desc', + volumeId='vol_id', + ) + text = serializer.serialize(dict(snapshot=raw_snapshot), 'show') + + print text + tree = etree.fromstring(text) + + self._verify_snapshot(raw_snapshot, tree) + + def test_snapshot_index_detail_serializer(self): + serializer = snapshots.SnapshotSerializer() + raw_snapshots = [dict( + id='snap1_id', + status='snap1_status', + size=1024, + createdAt=datetime.datetime.now(), + displayName='snap1_name', + displayDescription='snap1_desc', + volumeId='vol1_id', + ), + dict( + id='snap2_id', + status='snap2_status', + size=1024, + createdAt=datetime.datetime.now(), + displayName='snap2_name', + displayDescription='snap2_desc', + volumeId='vol2_id', + )] + text = serializer.serialize(dict(snapshots=raw_snapshots), 'index') + + print text + 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/api/openstack/volume/test_types.py b/nova/tests/api/openstack/volume/test_types.py new file mode 100644 index 000000000..34d861bed --- /dev/null +++ b/nova/tests/api/openstack/volume/test_types.py @@ -0,0 +1,166 @@ +# Copyright 2011 OpenStack LLC. +# aLL Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from lxml import etree +import webob + +from nova.api.openstack.volume import types +from nova import exception +from nova import test +from nova import log as logging +from nova.volume import volume_types +from nova.tests.api.openstack import fakes + + +LOG = logging.getLogger('nova.tests.api.openstack.volume.' + 'test_volume_types') + +last_param = {} + + +def stub_volume_type(id): + specs = { + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5"} + return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs) + + +def return_volume_types_get_all_types(context): + return dict(vol_type_1=stub_volume_type(1), + vol_type_2=stub_volume_type(2), + vol_type_3=stub_volume_type(3)) + + +def return_empty_volume_types_get_all_types(context): + return {} + + +def return_volume_types_get_volume_type(context, id): + if id == "777": + raise exception.VolumeTypeNotFound(volume_type_id=id) + return stub_volume_type(int(id)) + + +def return_volume_types_destroy(context, name): + if name == "777": + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) + pass + + +def return_volume_types_create(context, name, specs): + pass + + +def return_volume_types_get_by_name(context, name): + if name == "777": + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) + return stub_volume_type(int(name.split("_")[2])) + + +class VolumeTypesApiTest(test.TestCase): + def setUp(self): + super(VolumeTypesApiTest, self).setUp() + fakes.stub_out_key_pair_funcs(self.stubs) + self.controller = types.VolumeTypesController() + + def tearDown(self): + self.stubs.UnsetAll() + super(VolumeTypesApiTest, self).tearDown() + + def test_volume_types_index(self): + self.stubs.Set(volume_types, 'get_all_types', + return_volume_types_get_all_types) + + req = fakes.HTTPRequest.blank('/v2/123/os-volume-types') + res_dict = self.controller.index(req) + + self.assertEqual(3, len(res_dict)) + for name in ['vol_type_1', 'vol_type_2', 'vol_type_3']: + self.assertEqual(name, res_dict[name]['name']) + self.assertEqual('value1', res_dict[name]['extra_specs']['key1']) + + def test_volume_types_index_no_data(self): + self.stubs.Set(volume_types, 'get_all_types', + return_empty_volume_types_get_all_types) + + req = fakes.HTTPRequest.blank('/v2/123/os-volume-types') + res_dict = self.controller.index(req) + + self.assertEqual(0, len(res_dict)) + + def test_volume_types_show(self): + self.stubs.Set(volume_types, 'get_volume_type', + return_volume_types_get_volume_type) + + req = fakes.HTTPRequest.blank('/v2/123/os-volume-types/1') + res_dict = self.controller.show(req, 1) + + self.assertEqual(1, len(res_dict)) + self.assertEqual('vol_type_1', res_dict['volume_type']['name']) + + def test_volume_types_show_not_found(self): + self.stubs.Set(volume_types, 'get_volume_type', + return_volume_types_get_volume_type) + + req = fakes.HTTPRequest.blank('/v2/123/os-volume-types/777') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, '777') + + +class VolumeTypesSerializerTest(test.TestCase): + def setUp(self): + super(VolumeTypesSerializerTest, self).setUp() + self.serializer = types.VolumeTypesSerializer() + + def _verify_volume_type(self, vtype, tree): + self.assertEqual('volume_type', tree.tag) + self.assertEqual(vtype['name'], tree.get('name')) + self.assertEqual(str(vtype['id']), tree.get('id')) + self.assertEqual(1, len(tree)) + extra_specs = tree[0] + self.assertEqual('extra_specs', extra_specs.tag) + seen = set(vtype['extra_specs'].keys()) + for child in extra_specs: + self.assertTrue(child.tag in seen) + self.assertEqual(vtype['extra_specs'][child.tag], child.text) + seen.remove(child.tag) + self.assertEqual(len(seen), 0) + + def test_index_serializer(self): + # Just getting some input data + vtypes = return_volume_types_get_all_types(None) + text = self.serializer.serialize(vtypes, 'index') + + print text + tree = etree.fromstring(text) + + self.assertEqual('volume_types', tree.tag) + self.assertEqual(len(vtypes), len(tree)) + for child in tree: + name = child.get('name') + self.assertTrue(name in vtypes) + self._verify_volume_type(vtypes[name], child) + + def test_voltype_serializer(self): + vtype = stub_volume_type(1) + text = self.serializer.serialize(dict(volume_type=vtype)) + + print text + tree = etree.fromstring(text) + + self._verify_volume_type(vtype, tree) diff --git a/nova/tests/api/openstack/volume/test_volumes.py b/nova/tests/api/openstack/volume/test_volumes.py new file mode 100644 index 000000000..2f17c4b22 --- /dev/null +++ b/nova/tests/api/openstack/volume/test_volumes.py @@ -0,0 +1,179 @@ +# Copyright 2013 Josh Durgin +# 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 json + +from lxml import etree +import webob + +import nova +from nova.api.openstack.volume import volumes +from nova.compute import instance_types +from nova import flags +from nova import test +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS + + +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.assertTrue(child.tag in ('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.assertTrue(gr_child.tag in not_seen) + self.assertEqual(str(vol['metadata'][gr_child.tag]), + gr_child.text) + not_seen.remove(gr_child.tag) + self.assertEqual(0, len(not_seen)) + + def test_attach_show_create_serializer(self): + serializer = volumes.VolumeAttachmentSerializer() + raw_attach = dict( + id='vol_id', + volumeId='vol_id', + serverId='instance_uuid', + device='/foo') + text = serializer.serialize(dict(volumeAttachment=raw_attach), 'show') + + print text + tree = etree.fromstring(text) + + self.assertEqual('volumeAttachment', tree.tag) + self._verify_volume_attachment(raw_attach, tree) + + def test_attach_index_serializer(self): + serializer = volumes.VolumeAttachmentSerializer() + 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), + 'index') + + print text + 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.VolumeSerializer() + raw_volume = dict( + id='vol_id', + status='vol_status', + size=1024, + availabilityZone='vol_availability', + createdAt=datetime.datetime.now(), + 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), 'show') + + print text + tree = etree.fromstring(text) + + self._verify_volume(raw_volume, tree) + + def test_volume_index_detail_serializer(self): + serializer = volumes.VolumeSerializer() + raw_volumes = [dict( + id='vol1_id', + status='vol1_status', + size=1024, + availabilityZone='vol1_availability', + createdAt=datetime.datetime.now(), + 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=datetime.datetime.now(), + 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), 'index') + + print text + 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) diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index c942b0108..ccf83c1da 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -261,17 +261,17 @@ class TestOpenStackClient(object): return self.api_delete('/flavors/%s' % flavor_id) def get_volume(self, volume_id): - return self.api_get('/os-volumes/%s' % volume_id)['volume'] + return self.api_get('/volumes/%s' % volume_id)['volume'] def get_volumes(self, detail=True): - rel_url = '/os-volumes/detail' if detail else '/os-volumes' + rel_url = '/volumes/detail' if detail else '/volumes' return self.api_get(rel_url)['volumes'] def post_volume(self, volume): - return self.api_post('/os-volumes', volume)['volume'] + return self.api_post('/volumes', volume)['volume'] def delete_volume(self, volume_id): - return self.api_delete('/os-volumes/%s' % volume_id) + return self.api_delete('/volumes/%s' % volume_id) def get_server_volume(self, server_id, attachment_id): return self.api_get('/servers/%s/os-volume_attachments/%s' % diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index b18d24bd4..a98c94f65 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -80,10 +80,14 @@ class _IntegratedTestBase(test.TestCase): self.api = client.TestOpenStackClient('fake', 'fake', self.auth_url) + def tearDown(self): + self.osapi.stop() + super(_IntegratedTestBase, self).tearDown() + def _start_api_service(self): - osapi = service.WSGIService("osapi") - osapi.start() - self.auth_url = 'http://%s:%s/v2' % (osapi.host, osapi.port) + self.osapi = service.WSGIService("osapi_compute") + self.osapi.start() + self.auth_url = 'http://%s:%s/v2' % (self.osapi.host, self.osapi.port) LOG.warn(self.auth_url) def _get_flags(self): diff --git a/nova/tests/integrated/test_extensions.py b/nova/tests/integrated/test_extensions.py index b10da166a..70c998ef0 100644 --- a/nova/tests/integrated/test_extensions.py +++ b/nova/tests/integrated/test_extensions.py @@ -17,7 +17,7 @@ import os -from nova.api.openstack.v2 import extensions +from nova.api.openstack.compute import extensions from nova import flags from nova.log import logging from nova.tests.integrated import integrated_helpers @@ -32,9 +32,10 @@ class ExtensionsTest(integrated_helpers._IntegratedTestBase): extensions.ExtensionManager.reset() f = super(ExtensionsTest, self)._get_flags() - f['osapi_extension'] = FLAGS.osapi_extension[:] - f['osapi_extension'].append('nova.tests.api.openstack.v2.extensions.' - 'foxinsocks.Foxinsocks') + f['osapi_compute_extension'] = FLAGS.osapi_compute_extension[:] + f['osapi_compute_extension'].append( + 'nova.tests.api.openstack.compute.extensions.' + 'foxinsocks.Foxinsocks') return f def test_get_foxnsocks(self): diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py index 9e19f2cff..d07865181 100644 --- a/nova/tests/integrated/test_volumes.py +++ b/nova/tests/integrated/test_volumes.py @@ -18,6 +18,7 @@ import unittest import time +from nova import service from nova.log import logging from nova.tests.integrated import integrated_helpers from nova.tests.integrated.api import client @@ -32,6 +33,12 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): super(VolumesTest, self).setUp() driver.LoggingVolumeDriver.clear_logs() + def _start_api_service(self): + self.osapi = service.WSGIService("osapi_volume") + self.osapi.start() + self.auth_url = 'http://%s:%s/v1' % (self.osapi.host, self.osapi.port) + LOG.warn(self.auth_url) + def _get_flags(self): f = super(VolumesTest, self)._get_flags() f['use_local_volumes'] = False # Avoids calling local_path @@ -135,157 +142,6 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): delete_action = export_actions[0] self.assertEquals(delete_action['id'], created_volume_id) - def test_attach_and_detach_volume(self): - """Creates, attaches, detaches and deletes a volume.""" - self.flags(stub_network=True) - - # Create server - server_req = {'server': self._build_minimal_create_server_request()} - # NOTE(justinsb): Create an extra server so that server_id != volume_id - self.api.post_server(server_req) - created_server = self.api.post_server(server_req) - LOG.debug("created_server: %s" % created_server) - server_id = created_server['id'] - - # Create volume - created_volume = self.api.post_volume({'volume': {'size': 1}}) - LOG.debug("created_volume: %s" % created_volume) - volume_id = created_volume['id'] - self._poll_while(volume_id, ['creating']) - - # Check we've got different IDs - self.assertNotEqual(server_id, volume_id) - - # List current server attachments - should be none - attachments = self.api.get_server_volumes(server_id) - self.assertEquals([], attachments) - - # Template attach request - device = '/dev/sdc' - attach_req = {'device': device} - post_req = {'volumeAttachment': attach_req} - - # Try to attach to a non-existent volume; should fail - attach_req['volumeId'] = 3405691582 - self.assertRaises(client.OpenStackApiNotFoundException, - self.api.post_server_volume, server_id, post_req) - - # Try to attach to a non-existent server; should fail - attach_req['volumeId'] = volume_id - self.assertRaises(client.OpenStackApiNotFoundException, - self.api.post_server_volume, 3405691582, post_req) - - # Should still be no attachments... - attachments = self.api.get_server_volumes(server_id) - self.assertEquals([], attachments) - - # Do a real attach - attach_req['volumeId'] = volume_id - attach_result = self.api.post_server_volume(server_id, post_req) - LOG.debug(_("Attachment = %s") % attach_result) - - attachment_id = attach_result['id'] - self.assertEquals(volume_id, attach_result['volumeId']) - - # These fields aren't set because it's async - #self.assertEquals(server_id, attach_result['serverId']) - #self.assertEquals(device, attach_result['device']) - - # This is just an implementation detail, but let's check it... - self.assertEquals(volume_id, attachment_id) - - # NOTE(justinsb): There's an issue with the attach code, in that - # it's currently asynchronous and not recorded until the attach - # completes. So the caller must be 'smart', like this... - attach_done = None - retries = 0 - while True: - try: - attach_done = self.api.get_server_volume(server_id, - attachment_id) - break - except client.OpenStackApiNotFoundException: - LOG.debug("Got 404, waiting") - - time.sleep(1) - retries = retries + 1 - if retries > 10: - break - - expect_attach = {} - expect_attach['id'] = volume_id - expect_attach['volumeId'] = volume_id - expect_attach['serverId'] = server_id - expect_attach['device'] = device - - self.assertEqual(expect_attach, attach_done) - - # Should be one attachemnt - attachments = self.api.get_server_volumes(server_id) - self.assertEquals([expect_attach], attachments) - - # Should be able to get details - attachment_info = self.api.get_server_volume(server_id, attachment_id) - self.assertEquals(expect_attach, attachment_info) - - # Getting details on a different id should fail - self.assertRaises(client.OpenStackApiNotFoundException, - self.api.get_server_volume, server_id, 3405691582) - self.assertRaises(client.OpenStackApiNotFoundException, - self.api.get_server_volume, - 3405691582, attachment_id) - - # Trying to detach a different id should fail - self.assertRaises(client.OpenStackApiNotFoundException, - self.api.delete_server_volume, server_id, 3405691582) - - # Detach should work - self.api.delete_server_volume(server_id, attachment_id) - - # Again, it's async, so wait... - retries = 0 - while True: - try: - attachment = self.api.get_server_volume(server_id, - attachment_id) - LOG.debug("Attachment still there: %s" % attachment) - except client.OpenStackApiNotFoundException: - LOG.debug("Got 404, delete done") - break - - time.sleep(1) - retries = retries + 1 - self.assertTrue(retries < 10) - - # Should be no attachments again - attachments = self.api.get_server_volumes(server_id) - self.assertEquals([], attachments) - - LOG.debug("Logs: %s" % driver.LoggingVolumeDriver.all_logs()) - - # prepare_attach and prepare_detach are called from compute - # on attach/detach - - disco_moves = driver.LoggingVolumeDriver.logs_like( - 'initialize_connection', - id=volume_id) - LOG.debug("initialize_connection actions: %s" % disco_moves) - - self.assertEquals(1, len(disco_moves)) - disco_move = disco_moves[0] - self.assertEquals(disco_move['id'], volume_id) - - last_days_of_disco_moves = driver.LoggingVolumeDriver.logs_like( - 'terminate_connection', - id=volume_id) - LOG.debug("terminate_connection actions: %s" % - last_days_of_disco_moves) - - self.assertEquals(1, len(last_days_of_disco_moves)) - undisco_move = last_days_of_disco_moves[0] - self.assertEquals(undisco_move['id'], volume_id) - self.assertEquals(undisco_move['mountpoint'], device) - def test_create_volume_with_metadata(self): """Creates and deletes a volume.""" diff --git a/setup.py b/setup.py index cf6499782..9eb9ef92b 100644 --- a/setup.py +++ b/setup.py @@ -87,7 +87,8 @@ setup(name='nova', 'bin/nova-api', 'bin/nova-api-ec2', 'bin/nova-api-metadata', - 'bin/nova-api-os', + 'bin/nova-api-os-compute', + 'bin/nova-api-os-volume', 'bin/nova-compute', 'bin/nova-console', 'bin/nova-dhcpbridge', -- cgit