From de635fc882caebd5d5c9701e755a7174e37a05c5 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 11 Nov 2011 13:26:13 -0500 Subject: Creating new v2 namespace in nova.api.openstack Related to blueprint separate-nova-adminapi Change-Id: Ida35372b7263c4a4efdafd35faa1325c4436459b --- etc/nova/api-paste.ini | 27 +- nova/api/openstack/__init__.py | 187 - nova/api/openstack/accounts.py | 105 - nova/api/openstack/auth.py | 257 -- nova/api/openstack/consoles.py | 140 - nova/api/openstack/contrib/__init__.py | 90 - nova/api/openstack/contrib/admin_actions.py | 206 -- nova/api/openstack/contrib/createserverext.py | 74 - nova/api/openstack/contrib/deferred_delete.py | 66 - nova/api/openstack/contrib/disk_config.py | 189 - nova/api/openstack/contrib/extended_status.py | 110 - nova/api/openstack/contrib/flavorextradata.py | 36 - nova/api/openstack/contrib/flavorextraspecs.py | 115 - nova/api/openstack/contrib/floating_ips.py | 200 -- nova/api/openstack/contrib/hosts.py | 133 - nova/api/openstack/contrib/keypairs.py | 136 - nova/api/openstack/contrib/multinic.py | 106 - nova/api/openstack/contrib/quotas.py | 91 - nova/api/openstack/contrib/rescue.py | 81 - nova/api/openstack/contrib/security_groups.py | 553 --- nova/api/openstack/contrib/simple_tenant_usage.py | 228 -- nova/api/openstack/contrib/virtual_interfaces.py | 89 - .../openstack/contrib/virtual_storage_arrays.py | 597 ---- nova/api/openstack/contrib/volumes.py | 370 -- nova/api/openstack/contrib/volumetypes.py | 185 - nova/api/openstack/contrib/zones.py | 43 - nova/api/openstack/extensions.py | 551 --- nova/api/openstack/faults.py | 116 - nova/api/openstack/flavors.py | 124 - nova/api/openstack/image_metadata.py | 122 - nova/api/openstack/images.py | 210 -- nova/api/openstack/ips.py | 115 - nova/api/openstack/limits.py | 488 --- nova/api/openstack/ratelimiting/__init__.py | 221 -- nova/api/openstack/schemas/atom-link.rng | 141 - nova/api/openstack/schemas/atom.rng | 597 ---- nova/api/openstack/schemas/v1.1/addresses.rng | 14 - nova/api/openstack/schemas/v1.1/extension.rng | 11 - nova/api/openstack/schemas/v1.1/extensions.rng | 6 - nova/api/openstack/schemas/v1.1/flavor.rng | 14 - nova/api/openstack/schemas/v1.1/flavors.rng | 6 - nova/api/openstack/schemas/v1.1/flavors_index.rng | 12 - nova/api/openstack/schemas/v1.1/image.rng | 36 - nova/api/openstack/schemas/v1.1/images.rng | 6 - nova/api/openstack/schemas/v1.1/images_index.rng | 15 - nova/api/openstack/schemas/v1.1/limits.rng | 28 - nova/api/openstack/schemas/v1.1/metadata.rng | 9 - nova/api/openstack/schemas/v1.1/server.rng | 51 - nova/api/openstack/schemas/v1.1/servers.rng | 6 - nova/api/openstack/schemas/v1.1/servers_index.rng | 15 - nova/api/openstack/schemas/v1.1/version.rng | 17 - nova/api/openstack/schemas/v1.1/versions.rng | 11 - nova/api/openstack/server_metadata.py | 178 - nova/api/openstack/servers.py | 1178 ------- nova/api/openstack/urlmap.py | 297 -- nova/api/openstack/users.py | 139 - nova/api/openstack/v2/__init__.py | 186 + nova/api/openstack/v2/accounts.py | 103 + nova/api/openstack/v2/auth.py | 257 ++ nova/api/openstack/v2/consoles.py | 140 + nova/api/openstack/v2/contrib/__init__.py | 90 + nova/api/openstack/v2/contrib/admin_actions.py | 206 ++ nova/api/openstack/v2/contrib/createserverext.py | 74 + nova/api/openstack/v2/contrib/deferred_delete.py | 66 + nova/api/openstack/v2/contrib/disk_config.py | 189 + nova/api/openstack/v2/contrib/extended_status.py | 109 + nova/api/openstack/v2/contrib/flavorextradata.py | 36 + nova/api/openstack/v2/contrib/flavorextraspecs.py | 114 + nova/api/openstack/v2/contrib/floating_ips.py | 201 ++ nova/api/openstack/v2/contrib/hosts.py | 132 + nova/api/openstack/v2/contrib/keypairs.py | 136 + nova/api/openstack/v2/contrib/multinic.py | 106 + nova/api/openstack/v2/contrib/quotas.py | 91 + nova/api/openstack/v2/contrib/rescue.py | 80 + nova/api/openstack/v2/contrib/security_groups.py | 551 +++ .../openstack/v2/contrib/simple_tenant_usage.py | 229 ++ .../api/openstack/v2/contrib/virtual_interfaces.py | 89 + .../openstack/v2/contrib/virtual_storage_arrays.py | 597 ++++ nova/api/openstack/v2/contrib/volumes.py | 370 ++ nova/api/openstack/v2/contrib/volumetypes.py | 185 + nova/api/openstack/v2/contrib/zones.py | 43 + nova/api/openstack/v2/extensions.py | 551 +++ nova/api/openstack/v2/flavors.py | 124 + nova/api/openstack/v2/image_metadata.py | 122 + nova/api/openstack/v2/images.py | 208 ++ nova/api/openstack/v2/ips.py | 114 + nova/api/openstack/v2/limits.py | 486 +++ 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 | 14 + 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 | 51 + 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 | 178 + nova/api/openstack/v2/servers.py | 1177 +++++++ nova/api/openstack/v2/urlmap.py | 297 ++ nova/api/openstack/v2/users.py | 139 + nova/api/openstack/v2/versions.py | 261 ++ nova/api/openstack/v2/views/__init__.py | 0 nova/api/openstack/v2/views/addresses.py | 52 + nova/api/openstack/v2/views/flavors.py | 65 + nova/api/openstack/v2/views/images.py | 139 + nova/api/openstack/v2/views/limits.py | 97 + nova/api/openstack/v2/views/servers.py | 175 + nova/api/openstack/v2/views/versions.py | 94 + nova/api/openstack/v2/zones.py | 217 ++ nova/api/openstack/versions.py | 260 -- nova/api/openstack/views/__init__.py | 0 nova/api/openstack/views/addresses.py | 52 - nova/api/openstack/views/flavors.py | 66 - nova/api/openstack/views/images.py | 139 - nova/api/openstack/views/limits.py | 97 - nova/api/openstack/views/servers.py | 178 - nova/api/openstack/views/versions.py | 94 - nova/api/openstack/wsgi.py | 107 +- nova/api/openstack/xmlutil.py | 4 +- nova/api/openstack/zones.py | 219 -- nova/flags.py | 2 +- nova/tests/api/openstack/__init__.py | 16 - nova/tests/api/openstack/contrib/__init__.py | 15 - .../api/openstack/contrib/test_admin_actions.py | 86 - .../api/openstack/contrib/test_createserverext.py | 431 --- .../api/openstack/contrib/test_disk_config.py | 248 -- .../api/openstack/contrib/test_extendedstatus.py | 87 - .../openstack/contrib/test_flavors_extra_specs.py | 171 - .../api/openstack/contrib/test_floating_ips.py | 270 -- nova/tests/api/openstack/contrib/test_keypairs.py | 112 - .../api/openstack/contrib/test_multinic_xs.py | 113 - nova/tests/api/openstack/contrib/test_quotas.py | 134 - nova/tests/api/openstack/contrib/test_rescue.py | 78 - .../api/openstack/contrib/test_security_groups.py | 848 ----- .../openstack/contrib/test_simple_tenant_usage.py | 172 - .../openstack/contrib/test_virtual_interfaces.py | 55 - .../api/openstack/contrib/test_volume_types.py | 165 - .../contrib/test_volume_types_extra_specs.py | 169 - nova/tests/api/openstack/contrib/test_volumes.py | 88 - nova/tests/api/openstack/contrib/test_vsa.py | 450 --- nova/tests/api/openstack/extensions/__init__.py | 15 - nova/tests/api/openstack/extensions/foxinsocks.py | 94 - nova/tests/api/openstack/fakes.py | 53 +- nova/tests/api/openstack/test_accounts.py | 162 - nova/tests/api/openstack/test_api.py | 127 - nova/tests/api/openstack/test_auth.py | 315 -- nova/tests/api/openstack/test_consoles.py | 299 -- nova/tests/api/openstack/test_extensions.py | 515 --- nova/tests/api/openstack/test_faults.py | 11 +- nova/tests/api/openstack/test_flavors.py | 670 ---- nova/tests/api/openstack/test_image_metadata.py | 201 -- nova/tests/api/openstack/test_images.py | 1645 --------- nova/tests/api/openstack/test_limits.py | 939 ----- nova/tests/api/openstack/test_server_actions.py | 866 ----- nova/tests/api/openstack/test_server_metadata.py | 361 -- nova/tests/api/openstack/test_servers.py | 3638 -------------------- nova/tests/api/openstack/test_urlmap.py | 84 - nova/tests/api/openstack/test_users.py | 157 - nova/tests/api/openstack/test_versions.py | 671 ---- nova/tests/api/openstack/test_zones.py | 283 -- nova/tests/api/openstack/v2/__init__.py | 16 + nova/tests/api/openstack/v2/contrib/__init__.py | 15 + .../api/openstack/v2/contrib/test_admin_actions.py | 86 + .../openstack/v2/contrib/test_createserverext.py | 431 +++ .../api/openstack/v2/contrib/test_disk_config.py | 248 ++ .../openstack/v2/contrib/test_extendedstatus.py | 88 + .../v2/contrib/test_flavors_extra_specs.py | 170 + .../api/openstack/v2/contrib/test_floating_ips.py | 269 ++ .../api/openstack/v2/contrib/test_keypairs.py | 113 + .../api/openstack/v2/contrib/test_multinic_xs.py | 114 + nova/tests/api/openstack/v2/contrib/test_quotas.py | 133 + nova/tests/api/openstack/v2/contrib/test_rescue.py | 79 + .../openstack/v2/contrib/test_security_groups.py | 849 +++++ .../v2/contrib/test_simple_tenant_usage.py | 172 + .../v2/contrib/test_virtual_interfaces.py | 56 + .../api/openstack/v2/contrib/test_volume_types.py | 167 + .../v2/contrib/test_volume_types_extra_specs.py | 168 + .../tests/api/openstack/v2/contrib/test_volumes.py | 89 + nova/tests/api/openstack/v2/contrib/test_vsa.py | 449 +++ nova/tests/api/openstack/v2/extensions/__init__.py | 15 + .../api/openstack/v2/extensions/foxinsocks.py | 94 + nova/tests/api/openstack/v2/test_accounts.py | 162 + nova/tests/api/openstack/v2/test_api.py | 126 + nova/tests/api/openstack/v2/test_auth.py | 315 ++ nova/tests/api/openstack/v2/test_consoles.py | 299 ++ nova/tests/api/openstack/v2/test_extensions.py | 516 +++ nova/tests/api/openstack/v2/test_flavors.py | 670 ++++ 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 | 940 +++++ nova/tests/api/openstack/v2/test_server_actions.py | 881 +++++ .../tests/api/openstack/v2/test_server_metadata.py | 361 ++ nova/tests/api/openstack/v2/test_servers.py | 3631 +++++++++++++++++++ nova/tests/api/openstack/v2/test_urlmap.py | 84 + nova/tests/api/openstack/v2/test_users.py | 157 + nova/tests/api/openstack/v2/test_versions.py | 671 ++++ nova/tests/api/openstack/v2/test_zones.py | 283 ++ nova/tests/integrated/test_extensions.py | 2 +- nova/tests/test_hosts.py | 3 +- 209 files changed, 25006 insertions(+), 25035 deletions(-) delete mode 100644 nova/api/openstack/accounts.py delete mode 100644 nova/api/openstack/auth.py delete mode 100644 nova/api/openstack/consoles.py delete mode 100644 nova/api/openstack/contrib/__init__.py delete mode 100644 nova/api/openstack/contrib/admin_actions.py delete mode 100644 nova/api/openstack/contrib/createserverext.py delete mode 100644 nova/api/openstack/contrib/deferred_delete.py delete mode 100644 nova/api/openstack/contrib/disk_config.py delete mode 100644 nova/api/openstack/contrib/extended_status.py delete mode 100644 nova/api/openstack/contrib/flavorextradata.py delete mode 100644 nova/api/openstack/contrib/flavorextraspecs.py delete mode 100644 nova/api/openstack/contrib/floating_ips.py delete mode 100644 nova/api/openstack/contrib/hosts.py delete mode 100644 nova/api/openstack/contrib/keypairs.py delete mode 100644 nova/api/openstack/contrib/multinic.py delete mode 100644 nova/api/openstack/contrib/quotas.py delete mode 100644 nova/api/openstack/contrib/rescue.py delete mode 100644 nova/api/openstack/contrib/security_groups.py delete mode 100644 nova/api/openstack/contrib/simple_tenant_usage.py delete mode 100644 nova/api/openstack/contrib/virtual_interfaces.py delete mode 100644 nova/api/openstack/contrib/virtual_storage_arrays.py delete mode 100644 nova/api/openstack/contrib/volumes.py delete mode 100644 nova/api/openstack/contrib/volumetypes.py delete mode 100644 nova/api/openstack/contrib/zones.py delete mode 100644 nova/api/openstack/extensions.py delete mode 100644 nova/api/openstack/faults.py delete mode 100644 nova/api/openstack/flavors.py delete mode 100644 nova/api/openstack/image_metadata.py delete mode 100644 nova/api/openstack/images.py delete mode 100644 nova/api/openstack/ips.py delete mode 100644 nova/api/openstack/limits.py delete mode 100644 nova/api/openstack/ratelimiting/__init__.py delete mode 100644 nova/api/openstack/schemas/atom-link.rng delete mode 100644 nova/api/openstack/schemas/atom.rng delete mode 100644 nova/api/openstack/schemas/v1.1/addresses.rng delete mode 100644 nova/api/openstack/schemas/v1.1/extension.rng delete mode 100644 nova/api/openstack/schemas/v1.1/extensions.rng delete mode 100644 nova/api/openstack/schemas/v1.1/flavor.rng delete mode 100644 nova/api/openstack/schemas/v1.1/flavors.rng delete mode 100644 nova/api/openstack/schemas/v1.1/flavors_index.rng delete mode 100644 nova/api/openstack/schemas/v1.1/image.rng delete mode 100644 nova/api/openstack/schemas/v1.1/images.rng delete mode 100644 nova/api/openstack/schemas/v1.1/images_index.rng delete mode 100644 nova/api/openstack/schemas/v1.1/limits.rng delete mode 100644 nova/api/openstack/schemas/v1.1/metadata.rng delete mode 100644 nova/api/openstack/schemas/v1.1/server.rng delete mode 100644 nova/api/openstack/schemas/v1.1/servers.rng delete mode 100644 nova/api/openstack/schemas/v1.1/servers_index.rng delete mode 100644 nova/api/openstack/schemas/v1.1/version.rng delete mode 100644 nova/api/openstack/schemas/v1.1/versions.rng delete mode 100644 nova/api/openstack/server_metadata.py delete mode 100644 nova/api/openstack/servers.py delete mode 100644 nova/api/openstack/urlmap.py delete mode 100644 nova/api/openstack/users.py create mode 100644 nova/api/openstack/v2/__init__.py create mode 100644 nova/api/openstack/v2/accounts.py create mode 100644 nova/api/openstack/v2/auth.py create mode 100644 nova/api/openstack/v2/consoles.py create mode 100644 nova/api/openstack/v2/contrib/__init__.py create mode 100644 nova/api/openstack/v2/contrib/admin_actions.py create mode 100644 nova/api/openstack/v2/contrib/createserverext.py create mode 100644 nova/api/openstack/v2/contrib/deferred_delete.py create mode 100644 nova/api/openstack/v2/contrib/disk_config.py create mode 100644 nova/api/openstack/v2/contrib/extended_status.py create mode 100644 nova/api/openstack/v2/contrib/flavorextradata.py create mode 100644 nova/api/openstack/v2/contrib/flavorextraspecs.py create mode 100644 nova/api/openstack/v2/contrib/floating_ips.py create mode 100644 nova/api/openstack/v2/contrib/hosts.py create mode 100644 nova/api/openstack/v2/contrib/keypairs.py create mode 100644 nova/api/openstack/v2/contrib/multinic.py create mode 100644 nova/api/openstack/v2/contrib/quotas.py create mode 100644 nova/api/openstack/v2/contrib/rescue.py create mode 100644 nova/api/openstack/v2/contrib/security_groups.py create mode 100644 nova/api/openstack/v2/contrib/simple_tenant_usage.py create mode 100644 nova/api/openstack/v2/contrib/virtual_interfaces.py create mode 100644 nova/api/openstack/v2/contrib/virtual_storage_arrays.py create mode 100644 nova/api/openstack/v2/contrib/volumes.py create mode 100644 nova/api/openstack/v2/contrib/volumetypes.py create mode 100644 nova/api/openstack/v2/contrib/zones.py create mode 100644 nova/api/openstack/v2/extensions.py create mode 100644 nova/api/openstack/v2/flavors.py create mode 100644 nova/api/openstack/v2/image_metadata.py create mode 100644 nova/api/openstack/v2/images.py create mode 100644 nova/api/openstack/v2/ips.py create mode 100644 nova/api/openstack/v2/limits.py create mode 100644 nova/api/openstack/v2/ratelimiting/__init__.py create mode 100644 nova/api/openstack/v2/schemas/atom-link.rng create mode 100644 nova/api/openstack/v2/schemas/atom.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/addresses.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/extension.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/extensions.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/flavor.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/flavors.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/flavors_index.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/image.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/images.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/images_index.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/limits.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/metadata.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/server.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/servers.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/servers_index.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/version.rng create mode 100644 nova/api/openstack/v2/schemas/v1.1/versions.rng create mode 100644 nova/api/openstack/v2/server_metadata.py create mode 100644 nova/api/openstack/v2/servers.py create mode 100644 nova/api/openstack/v2/urlmap.py create mode 100644 nova/api/openstack/v2/users.py create mode 100644 nova/api/openstack/v2/versions.py create mode 100644 nova/api/openstack/v2/views/__init__.py create mode 100644 nova/api/openstack/v2/views/addresses.py create mode 100644 nova/api/openstack/v2/views/flavors.py create mode 100644 nova/api/openstack/v2/views/images.py create mode 100644 nova/api/openstack/v2/views/limits.py create mode 100644 nova/api/openstack/v2/views/servers.py create mode 100644 nova/api/openstack/v2/views/versions.py create mode 100644 nova/api/openstack/v2/zones.py delete mode 100644 nova/api/openstack/versions.py delete mode 100644 nova/api/openstack/views/__init__.py delete mode 100644 nova/api/openstack/views/addresses.py delete mode 100644 nova/api/openstack/views/flavors.py delete mode 100644 nova/api/openstack/views/images.py delete mode 100644 nova/api/openstack/views/limits.py delete mode 100644 nova/api/openstack/views/servers.py delete mode 100644 nova/api/openstack/views/versions.py delete mode 100644 nova/api/openstack/zones.py delete mode 100644 nova/tests/api/openstack/contrib/__init__.py delete mode 100644 nova/tests/api/openstack/contrib/test_admin_actions.py delete mode 100644 nova/tests/api/openstack/contrib/test_createserverext.py delete mode 100644 nova/tests/api/openstack/contrib/test_disk_config.py delete mode 100644 nova/tests/api/openstack/contrib/test_extendedstatus.py delete mode 100644 nova/tests/api/openstack/contrib/test_flavors_extra_specs.py delete mode 100644 nova/tests/api/openstack/contrib/test_floating_ips.py delete mode 100644 nova/tests/api/openstack/contrib/test_keypairs.py delete mode 100644 nova/tests/api/openstack/contrib/test_multinic_xs.py delete mode 100644 nova/tests/api/openstack/contrib/test_quotas.py delete mode 100644 nova/tests/api/openstack/contrib/test_rescue.py delete mode 100644 nova/tests/api/openstack/contrib/test_security_groups.py delete mode 100644 nova/tests/api/openstack/contrib/test_simple_tenant_usage.py delete mode 100644 nova/tests/api/openstack/contrib/test_virtual_interfaces.py delete mode 100644 nova/tests/api/openstack/contrib/test_volume_types.py delete mode 100644 nova/tests/api/openstack/contrib/test_volume_types_extra_specs.py delete mode 100644 nova/tests/api/openstack/contrib/test_volumes.py delete mode 100644 nova/tests/api/openstack/contrib/test_vsa.py delete mode 100644 nova/tests/api/openstack/extensions/__init__.py delete mode 100644 nova/tests/api/openstack/extensions/foxinsocks.py delete mode 100644 nova/tests/api/openstack/test_accounts.py delete mode 100644 nova/tests/api/openstack/test_api.py delete mode 100644 nova/tests/api/openstack/test_auth.py delete mode 100644 nova/tests/api/openstack/test_consoles.py delete mode 100644 nova/tests/api/openstack/test_extensions.py delete mode 100644 nova/tests/api/openstack/test_flavors.py delete mode 100644 nova/tests/api/openstack/test_image_metadata.py delete mode 100644 nova/tests/api/openstack/test_images.py delete mode 100644 nova/tests/api/openstack/test_limits.py delete mode 100644 nova/tests/api/openstack/test_server_actions.py delete mode 100644 nova/tests/api/openstack/test_server_metadata.py delete mode 100644 nova/tests/api/openstack/test_servers.py delete mode 100644 nova/tests/api/openstack/test_urlmap.py delete mode 100644 nova/tests/api/openstack/test_users.py delete mode 100644 nova/tests/api/openstack/test_versions.py delete mode 100644 nova/tests/api/openstack/test_zones.py create mode 100644 nova/tests/api/openstack/v2/__init__.py create mode 100644 nova/tests/api/openstack/v2/contrib/__init__.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_admin_actions.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_createserverext.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_disk_config.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_extendedstatus.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_flavors_extra_specs.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_floating_ips.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_keypairs.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_multinic_xs.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_quotas.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_rescue.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_security_groups.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_simple_tenant_usage.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_virtual_interfaces.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_volume_types.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_volume_types_extra_specs.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_volumes.py create mode 100644 nova/tests/api/openstack/v2/contrib/test_vsa.py create mode 100644 nova/tests/api/openstack/v2/extensions/__init__.py create mode 100644 nova/tests/api/openstack/v2/extensions/foxinsocks.py create mode 100644 nova/tests/api/openstack/v2/test_accounts.py create mode 100644 nova/tests/api/openstack/v2/test_api.py create mode 100644 nova/tests/api/openstack/v2/test_auth.py create mode 100644 nova/tests/api/openstack/v2/test_consoles.py create mode 100644 nova/tests/api/openstack/v2/test_extensions.py create mode 100644 nova/tests/api/openstack/v2/test_flavors.py create mode 100644 nova/tests/api/openstack/v2/test_image_metadata.py create mode 100644 nova/tests/api/openstack/v2/test_images.py create mode 100644 nova/tests/api/openstack/v2/test_limits.py create mode 100644 nova/tests/api/openstack/v2/test_server_actions.py create mode 100644 nova/tests/api/openstack/v2/test_server_metadata.py create mode 100644 nova/tests/api/openstack/v2/test_servers.py create mode 100644 nova/tests/api/openstack/v2/test_urlmap.py create mode 100644 nova/tests/api/openstack/v2/test_users.py create mode 100644 nova/tests/api/openstack/v2/test_versions.py create mode 100644 nova/tests/api/openstack/v2/test_zones.py diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index 580191c01..145044df7 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -79,38 +79,39 @@ paste.app_factory = nova.api.ec2:Executor.factory ############# [composite:osapi] -use = call:nova.api.openstack.urlmap:urlmap_factory +use = call:nova.api.openstack.v2.urlmap:urlmap_factory /: osversions -/v1.1: openstackapi11 +/v1.1: openstack_api_v2 +/v2: openstack_api_v2 -[pipeline:openstackapi11] -pipeline = faultwrap noauth ratelimit serialize extensions osapiapp11 +[pipeline:openstack_api_v2] +pipeline = faultwrap noauth ratelimit serialize extensions osapi_app_v2 # NOTE(vish): use the following pipeline for deprecated auth -# pipeline = faultwrap auth ratelimit serialize extensions osapiapp11 +# pipeline = faultwrap auth ratelimit serialize extensions osapi_app_v2 [filter:faultwrap] -paste.filter_factory = nova.api.openstack:FaultWrapper.factory +paste.filter_factory = nova.api.openstack.v2:FaultWrapper.factory [filter:auth] -paste.filter_factory = nova.api.openstack.auth:AuthMiddleware.factory +paste.filter_factory = nova.api.openstack.v2.auth:AuthMiddleware.factory [filter:noauth] -paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory +paste.filter_factory = nova.api.openstack.v2.auth:NoAuthMiddleware.factory [filter:ratelimit] -paste.filter_factory = nova.api.openstack.limits:RateLimitingMiddleware.factory +paste.filter_factory = nova.api.openstack.v2.limits:RateLimitingMiddleware.factory [filter:serialize] paste.filter_factory = nova.api.openstack.wsgi:LazySerializationMiddleware.factory [filter:extensions] -paste.filter_factory = nova.api.openstack.extensions:ExtensionMiddleware.factory +paste.filter_factory = nova.api.openstack.v2.extensions:ExtensionMiddleware.factory -[app:osapiapp11] -paste.app_factory = nova.api.openstack:APIRouter.factory +[app:osapi_app_v2] +paste.app_factory = nova.api.openstack.v2:APIRouter.factory [pipeline:osversions] pipeline = faultwrap osversionapp [app:osversionapp] -paste.app_factory = nova.api.openstack.versions:Versions.factory +paste.app_factory = nova.api.openstack.v2.versions:Versions.factory diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 07911221d..e69de29bb 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -1,187 +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 import accounts -from nova.api.openstack import faults -from nova.api.openstack import consoles -from nova.api.openstack import flavors -from nova.api.openstack import images -from nova.api.openstack import image_metadata -from nova.api.openstack import ips -from nova.api.openstack import limits -from nova.api.openstack import servers -from nova.api.openstack import server_metadata -from nova.api.openstack import users -from nova.api.openstack import versions -from nova.api.openstack import wsgi -from nova.api.openstack import zones -from nova import flags -from nova import log as logging -from nova import wsgi as base_wsgi - - -LOG = logging.getLogger('nova.api.openstack') -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 faults.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): - self.server_members = {} - mapper = ProjectMapper() - self._setup_routes(mapper) - super(APIRouter, self).__init__(mapper) - - def _setup_routes(self, mapper): - server_members = self.server_members - server_members['action'] = 'POST' - if FLAGS.allow_admin_api: - LOG.debug(_("Including admin operations in API.")) - - server_members['diagnostics'] = 'GET' - server_members['actions'] = 'GET' - - mapper.resource("user", "users", - controller=users.create_resource(), - collection={'detail': 'GET'}) - - mapper.resource("account", "accounts", - controller=accounts.create_resource(), - collection={'detail': 'GET'}) - - mapper.resource("zone", "zones", - controller=zones.create_resource(), - collection={'detail': 'GET', - 'info': 'GET', - 'select': 'POST'}) - - 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=self.server_members) - - 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/accounts.py b/nova/api/openstack/accounts.py deleted file mode 100644 index 3a19d5c89..000000000 --- a/nova/api/openstack/accounts.py +++ /dev/null @@ -1,105 +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 import exception -from nova import flags -from nova import log as logging - -from nova.auth import manager -from nova.api.openstack import faults -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack') - - -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() - - def detail(self, req): - raise webob.exc.HTTPNotImplemented() - - 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() - - 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 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) - - -class AccountXMLSerializer(xmlutil.XMLTemplateSerializer): - def default(self): - return AccountTemplate() - - -def create_resource(): - body_serializers = { - 'application/xml': AccountXMLSerializer(), - } - serializer = wsgi.ResponseSerializer(body_serializers) - return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py deleted file mode 100644 index d435f8318..000000000 --- a/nova/api/openstack/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.exc -import webob.dec - -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 -from nova.api.openstack import common -from nova.api.openstack import faults - -LOG = logging.getLogger('nova.api.openstack') -FLAGS = flags.FLAGS -flags.DECLARE('use_forwarded_for', 'nova.api.auth') - - -class NoAuthMiddleware(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(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 faults.Fault(webob.exc.HTTPUnauthorized()) - - # Get all valid projects for the user - projects = self.auth.get_projects(user_id) - if not projects: - return faults.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] == 'v1.1': - project_id = path_parts[2] - # Check that the project for project_id exists, and that user - # is authorized to use it - try: - project = self.auth.get_project(project_id) - except exception.ProjectNotFound: - return faults.Fault(webob.exc.HTTPUnauthorized()) - if project_id not in [p.id for p in projects]: - return faults.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 faults.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. /v1.0 or /v1.1).") - LOG.warn(msg) - return faults.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 faults.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 faults.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 == '1.1': - 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/consoles.py b/nova/api/openstack/consoles.py deleted file mode 100644 index 7d8c0523e..000000000 --- a/nova/api/openstack/consoles.py +++ /dev/null @@ -1,140 +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 webob import exc -import webob - -from nova import console -from nova import exception -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil - - -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'], - 'port': cons['port'], - 'host': pool['public_hostname']} - return dict(console=info) - - -class Controller(object): - """The Consoles controller for the Openstack API""" - - def __init__(self): - self.console_api = console.API() - - 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) - - 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) - - -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 ConsoleXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return ConsolesTemplate() - - def show(self): - return ConsoleTemplate() - - -def create_resource(): - body_serializers = { - 'application/xml': ConsoleXMLSerializer(), - } - serializer = wsgi.ResponseSerializer(body_serializers) - return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/contrib/__init__.py b/nova/api/openstack/contrib/__init__.py deleted file mode 100644 index b3d4df69b..000000000 --- a/nova/api/openstack/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.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/contrib/admin_actions.py b/nova/api/openstack/contrib/admin_actions.py deleted file mode 100644 index 81b23a8a0..000000000 --- a/nova/api/openstack/contrib/admin_actions.py +++ /dev/null @@ -1,206 +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 traceback - -import webob -from webob import exc - -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova.api.openstack import extensions -from nova.scheduler import api as scheduler_api - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.contrib.admin_actions") - - -class Admin_actions(extensions.ExtensionDescriptor): - """Adds admin-only server actions: pause, unpause, suspend, - resume, migrate, resetNetwork, injectNetworkInfo, lock and unlock - """ - - name = "AdminActions" - alias = "os-admin-actions" - namespace = "http://docs.openstack.org/ext/admin-actions/api/v1.1" - updated = "2011-09-20T00:00:00+00:00" - - def __init__(self, ext_mgr): - super(Admin_actions, self).__init__(ext_mgr) - self.compute_api = compute.API() - - @extensions.admin_only - @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: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::pause %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @extensions.admin_only - @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: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::unpause %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @extensions.admin_only - @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: - readable = traceback.format_exc() - LOG.exception(_("compute.api::suspend %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @extensions.admin_only - @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: - readable = traceback.format_exc() - LOG.exception(_("compute.api::resume %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @extensions.admin_only - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _migrate(self, input_dict, req, id): - """Permit admins to migrate a server to a new host""" - try: - self.compute_api.resize(req.environ['nova.context'], id) - except Exception, e: - LOG.exception(_("Error in migrate %s"), e) - raise exc.HTTPBadRequest() - return webob.Response(status_int=202) - - @extensions.admin_only - @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) - - @extensions.admin_only - @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) - - @extensions.admin_only - @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) - - @extensions.admin_only - @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 get_actions(self): - actions = [ - 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", - "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/contrib/createserverext.py b/nova/api/openstack/contrib/createserverext.py deleted file mode 100644 index 4e6556f5d..000000000 --- a/nova/api/openstack/contrib/createserverext.py +++ /dev/null @@ -1,74 +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 import extensions -from nova.api.openstack import servers -from nova.api.openstack 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/ext/createserverext/api/v1.1" - updated = "2011-07-19T00:00:00+00:00" - - def get_resources(self): - resources = [] - - headers_serializer = servers.HeadersSerializer() - body_serializers = { - 'application/xml': servers.ServerXMLSerializer(), - } - - body_deserializers = { - 'application/xml': servers.ServerXMLDeserializer(), - } - - serializer = wsgi.ResponseSerializer(body_serializers, - headers_serializer) - deserializer = wsgi.RequestDeserializer(body_deserializers) - controller = Controller() - - res = extensions.ResourceExtension('os-create-server-ext', - controller=controller, - deserializer=deserializer, - serializer=serializer) - resources.append(res) - - return resources diff --git a/nova/api/openstack/contrib/deferred_delete.py b/nova/api/openstack/contrib/deferred_delete.py deleted file mode 100644 index 8415ca4b3..000000000 --- a/nova/api/openstack/contrib/deferred_delete.py +++ /dev/null @@ -1,66 +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 import compute -from nova import log as logging -from nova.api.openstack import extensions -from nova.api.openstack import servers - - -LOG = logging.getLogger("nova.api.contrib.deferred-delete") - - -class Deferred_delete(extensions.ExtensionDescriptor): - """Instance deferred delete""" - - name = "DeferredDelete" - alias = "os-deferred-delete" - namespace = "http://docs.openstack.org/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) - self.compute_api.restore(context, instance) - 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) - self.compute_api.force_delete(context, instance) - 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/contrib/disk_config.py b/nova/api/openstack/contrib/disk_config.py deleted file mode 100644 index 20da999a7..000000000 --- a/nova/api/openstack/contrib/disk_config.py +++ /dev/null @@ -1,189 +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 webob import exc -from xml.dom import minidom - -from nova.api.openstack import extensions -from nova.api.openstack import servers -from nova.api.openstack import xmlutil -from nova import compute -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()) - - for server in servers: - db_server = self.compute_api.routing_get(context, server['id']) - 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/contrib/extended_status.py b/nova/api/openstack/contrib/extended_status.py deleted file mode 100644 index 625f3ab2b..000000000 --- a/nova/api/openstack/contrib/extended_status.py +++ /dev/null @@ -1,110 +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.""" - -import traceback - -import webob -from webob import exc - -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova.api.openstack import extensions -from nova.api.openstack import faults -from nova.api.openstack import xmlutil - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.contrib.extendedstatus") - - -class Extended_status(extensions.ExtensionDescriptor): - """Extended Status support""" - - name = "ExtendedStatus" - alias = "OS-EXT-STS" - namespace = "http://docs.openstack.org/ext/extended_status/api/v1.1" - updated = "2011-11-03T00:00:00+00:00" - - 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: - 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 body['servers']: - try: - inst_ref = compute_api.routing_get(context, server['id']) - except exception.NotFound: - 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) - 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 - - if FLAGS.allow_admin_api: - 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/contrib/flavorextradata.py b/nova/api/openstack/contrib/flavorextradata.py deleted file mode 100644 index f696d161b..000000000 --- a/nova/api/openstack/contrib/flavorextradata.py +++ /dev/null @@ -1,36 +0,0 @@ -# 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/ext/flavor_extra_data/api/v1.1" - updated = "2011-09-14T00:00:00+00:00" - -# vim: tabstop=4 shiftwidth=4 softtabstop=4 diff --git a/nova/api/openstack/contrib/flavorextraspecs.py b/nova/api/openstack/contrib/flavorextraspecs.py deleted file mode 100644 index d71e9cf8b..000000000 --- a/nova/api/openstack/contrib/flavorextraspecs.py +++ /dev/null @@ -1,115 +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 import db -from nova import exception -from nova.api.openstack import extensions -from nova.api.openstack import wsgi - - -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 == None or body == "": - expl = _('No Request Body') - raise exc.HTTPBadRequest(explanation=expl) - - 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) - - 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 - - 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 - - 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/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/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py deleted file mode 100644 index b64251e53..000000000 --- a/nova/api/openstack/contrib/floating_ips.py +++ /dev/null @@ -1,200 +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 import network -from nova import rpc -from nova.api.openstack import extensions - - -LOG = logging.getLogger('nova.api.openstack.contrib.floating_ips') - - -def _translate_floating_ip_view(floating_ip): - result = {'id': floating_ip['id'], - 'ip': floating_ip['address']} - 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_id'] - 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.""" - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "floating_ip": [ - "id", - "ip", - "instance_id", - "fixed_ip", - ]}}} - - def __init__(self): - self.network_api = network.API() - super(FloatingIPController, self).__init__() - - 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) - - def index(self, req): - context = req.environ['nova.context'] - - try: - get_floating_ips = self.network_api.get_floating_ips_by_project - floating_ips = get_floating_ips(context) - except exception.FloatingIpNotFoundForProject: - floating_ips = [] - - return _translate_floating_ips_view(floating_ips) - - def create(self, req, body=None): - context = req.environ['nova.context'] - - try: - address = self.network_api.allocate_floating_ip(context) - 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': - 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 Floating_ips(extensions.ExtensionDescriptor): - """Floating IPs support""" - - name = "Floating_ips" - alias = "os-floating-ips" - namespace = "http://docs.openstack.org/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: - self.compute_api.associate_floating_ip(context, instance_id, - 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/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py deleted file mode 100644 index 736843c3d..000000000 --- a/nova/api/openstack/contrib/hosts.py +++ /dev/null @@ -1,133 +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 nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova.api.openstack import common -from nova.api.openstack import extensions -from nova.api.openstack import faults -from nova.scheduler import api as scheduler_api - - -LOG = logging.getLogger("nova.api.hosts") -FLAGS = flags.FLAGS - - -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__() - - def index(self, req): - return {'hosts': _list_hosts(req)} - - @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} - - @extensions.admin_only - def startup(self, req, id): - return self._host_power_action(req, host=id, action="startup") - - @extensions.admin_only - def shutdown(self, req, id): - return self._host_power_action(req, host=id, action="shutdown") - - @extensions.admin_only - def reboot(self, req, id): - return self._host_power_action(req, host=id, action="reboot") - - -class Hosts(extensions.ExtensionDescriptor): - """Host administration""" - - name = "Hosts" - alias = "os-hosts" - namespace = "http://docs.openstack.org/ext/hosts/api/v1.1" - updated = "2011-06-29T00:00:00+00:00" - - 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/contrib/keypairs.py b/nova/api/openstack/contrib/keypairs.py deleted file mode 100644 index ee5cad530..000000000 --- a/nova/api/openstack/contrib/keypairs.py +++ /dev/null @@ -1,136 +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 import crypto -from nova import db -from nova import exception -from nova.api.openstack import extensions - - -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} - - 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) - - 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 Keypairs(extensions.ExtensionDescriptor): - """Keypair Support""" - - name = "Keypairs" - alias = "os-keypairs" - namespace = "http://docs.openstack.org/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/contrib/multinic.py b/nova/api/openstack/contrib/multinic.py deleted file mode 100644 index 26968646d..000000000 --- a/nova/api/openstack/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 import extensions -from nova import compute -from nova import exception -from nova import log as logging - - -LOG = logging.getLogger("nova.api.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/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/contrib/quotas.py b/nova/api/openstack/contrib/quotas.py deleted file mode 100644 index 184b4d12f..000000000 --- a/nova/api/openstack/contrib/quotas.py +++ /dev/null @@ -1,91 +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 import db -from nova import exception -from nova import quota -from nova.api.openstack import extensions - - -class QuotaSetsController(object): - - def _format_quota_set(self, project_id, quota_set): - """Convert the quota object to a result dict""" - - return {'quota_set': { - 'id': str(project_id), - 'metadata_items': quota_set['metadata_items'], - 'injected_file_content_bytes': - quota_set['injected_file_content_bytes'], - 'volumes': quota_set['volumes'], - 'gigabytes': quota_set['gigabytes'], - 'ram': quota_set['ram'], - 'floating_ips': quota_set['floating_ips'], - 'instances': quota_set['instances'], - 'injected_files': quota_set['injected_files'], - 'cores': quota_set['cores'], - }} - - 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() - - def update(self, req, id, body): - context = req.environ['nova.context'] - project_id = id - resources = ['metadata_items', 'injected_file_content_bytes', - 'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances', - 'injected_files', 'cores'] - for key in body['quota_set'].keys(): - if key in 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/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/contrib/rescue.py b/nova/api/openstack/contrib/rescue.py deleted file mode 100644 index d3f38b200..000000000 --- a/nova/api/openstack/contrib/rescue.py +++ /dev/null @@ -1,81 +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 import extensions as exts -from nova.api.openstack import faults -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.contrib.rescue") - - -class Rescue(exts.ExtensionDescriptor): - """Instance rescue mode""" - - name = "Rescue" - alias = "os-rescue" - namespace = "http://docs.openstack.org/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/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py deleted file mode 100644 index fd1187629..000000000 --- a/nova/api/openstack/contrib/security_groups.py +++ /dev/null @@ -1,553 +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 webob import exc -import webob - -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 rpc -from nova import utils -from nova.api.openstack import common -from nova.api.openstack import extensions -from nova.api.openstack import wsgi -from nova.compute import power_state - -from xml.dom import minidom - - -LOG = logging.getLogger("nova.api.contrib.security_groups") -FLAGS = flags.FLAGS - - -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 - - 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) - - 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'])))} - - 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): - - 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/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 = [] - - metadata = _get_metadata() - body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, - xmlns=wsgi.XMLNS_V11), - } - serializer = wsgi.ResponseSerializer(body_serializers, None) - - body_deserializers = { - 'application/xml': SecurityGroupXMLDeserializer(), - } - deserializer = wsgi.RequestDeserializer(body_deserializers) - - res = extensions.ResourceExtension('os-security-groups', - controller=SecurityGroupController(), - deserializer=deserializer, - serializer=serializer) - - resources.append(res) - - body_deserializers = { - 'application/xml': SecurityGroupRulesXMLDeserializer(), - } - deserializer = wsgi.RequestDeserializer(body_deserializers) - - res = extensions.ResourceExtension('os-security-group-rules', - controller=SecurityGroupRulesController(), - deserializer=deserializer, - serializer=serializer) - resources.append(res) - return resources - - -class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): - """ - Deserializer to handle xml-formatted security group requests. - """ - def create(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 create(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 - - -def _get_metadata(): - metadata = { - "attributes": { - "security_group": ["id", "tenant_id", "name"], - "rule": ["id", "parent_group_id"], - "security_group_rule": ["id", "parent_group_id"], - } - } - return metadata diff --git a/nova/api/openstack/contrib/simple_tenant_usage.py b/nova/api/openstack/contrib/simple_tenant_usage.py deleted file mode 100644 index 3399ac2be..000000000 --- a/nova/api/openstack/contrib/simple_tenant_usage.py +++ /dev/null @@ -1,228 +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 urlparse -import webob - -from datetime import datetime -from nova import exception -from nova import flags -from nova.compute import api -from nova.api.openstack import extensions -from nova.api.openstack import views -from nova.db.sqlalchemy.session import get_session -from webob import exc - - -FLAGS = flags.FLAGS - - -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: - try: - return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S.%f") - except: - 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) - - def index(self, req): - """Retrive tenant_usage for all tenants""" - context = req.environ['nova.context'] - - if not context.is_admin and FLAGS.allow_admin_api: - 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} - - 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 and FLAGS.allow_admin_api: - 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/ext/os-simple-tenant-usage/api/v1.1" - updated = "2011-08-19T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-simple-tenant-usage', - SimpleTenantUsageController()) - resources.append(res) - - return resources diff --git a/nova/api/openstack/contrib/virtual_interfaces.py b/nova/api/openstack/contrib/virtual_interfaces.py deleted file mode 100644 index 0afd2119d..000000000 --- a/nova/api/openstack/contrib/virtual_interfaces.py +++ /dev/null @@ -1,89 +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 import log as logging -from nova import network -from nova.api.openstack import common -from nova.api.openstack import extensions -from nova.api.openstack import wsgi - - -LOG = logging.getLogger("nova.api.virtual_interfaces") - - -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 - - -def _get_metadata(): - metadata = { - "attributes": { - 'virtual_interface': ["id", "mac_address"]}} - return metadata - - -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} - - 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/ext/virtual_interfaces/api/v1.1" - updated = "2011-08-17T00:00:00+00:00" - - def get_resources(self): - resources = [] - - metadata = _get_metadata() - body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, - xmlns=wsgi.XMLNS_V11)} - serializer = wsgi.ResponseSerializer(body_serializers, None) - res = extensions.ResourceExtension( - 'os-virtual-interfaces', - controller=ServerVirtualInterfaceController(), - parent=dict(member_name='server', collection_name='servers'), - serializer=serializer) - resources.append(res) - - return resources diff --git a/nova/api/openstack/contrib/virtual_storage_arrays.py b/nova/api/openstack/contrib/virtual_storage_arrays.py deleted file mode 100644 index a6bf72960..000000000 --- a/nova/api/openstack/contrib/virtual_storage_arrays.py +++ /dev/null @@ -1,597 +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 import vsa -from nova import volume -from nova import compute -from nova import network -from nova import db -from nova import quota -from nova import exception -from nova import log as logging -from nova.api.openstack import common -from nova.api.openstack import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import servers -from nova.api.openstack.contrib import volumes -from nova.compute import instance_types - -from nova import flags -FLAGS = flags.FLAGS - -LOG = logging.getLogger("nova.api.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 - - -class VsaController(object): - """The Virtual Storage Array API controller for the OpenStack API.""" - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "vsa": [ - "id", - "name", - "displayName", - "displayDescription", - "createTime", - "status", - "vcType", - "vcCount", - "driveCount", - "ipAddress", - ]}}} - - 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} - - def index(self, req): - """Return a short list of VSAs.""" - return self._items(req, details=False) - - def detail(self, req): - """Return a detailed list of VSAs.""" - return self._items(req, details=True) - - 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)} - - 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 - - -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 - - """ - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "volume": [ - "id", - "name", - "status", - "size", - "availabilityZone", - "createdAt", - "displayName", - "displayDescription", - "vsaId", - ]}}} - - 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 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__() - - -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() - - -class VsaVPoolController(object): - """The vPool VSA API controller for the OpenStack API.""" - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "vpool": [ - "id", - "vsaId", - "name", - "displayName", - "displayDescription", - "driveCount", - "driveIds", - "protection", - "stripeSize", - "stripeWidth", - "createTime", - "status", - ]}}} - - def __init__(self): - self.vsa_api = vsa.API() - super(VsaVPoolController, self).__init__() - - 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) - - 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() - - 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/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/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py deleted file mode 100644 index 917cfc61b..000000000 --- a/nova/api/openstack/contrib/volumes.py +++ /dev/null @@ -1,370 +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 import compute -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import quota -from nova import volume -from nova.volume import volume_types -from nova.api.openstack import common -from nova.api.openstack import extensions -from nova.api.openstack import servers - - -LOG = logging.getLogger("nova.api.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'] - - 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.""" - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "volume": [ - "id", - "status", - "size", - "availabilityZone", - "createdAt", - "displayName", - "displayDescription", - "volumeType", - "metadata", - ]}}} - - 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, None, - 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 - - -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) - - """ - - _serialization_metadata = { - 'application/xml': { - 'attributes': { - 'volumeAttachment': ['id', - 'serverId', - 'volumeId', - 'device']}}} - - def __init__(self): - self.compute_api = compute.API() - self.volume_api = volume.API() - super(VolumeAttachmentController, self).__init__() - - 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) - - 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)} - - 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') - - -class Volumes(extensions.ExtensionDescriptor): - """Volumes support""" - - name = "Volumes" - alias = "os-volumes" - namespace = "http://docs.openstack.org/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) - - return resources diff --git a/nova/api/openstack/contrib/volumetypes.py b/nova/api/openstack/contrib/volumetypes.py deleted file mode 100644 index 64a9e0f02..000000000 --- a/nova/api/openstack/contrib/volumetypes.py +++ /dev/null @@ -1,185 +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 import db -from nova import exception -from nova.volume import volume_types -from nova.api.openstack import extensions -from nova.api.openstack import wsgi - - -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 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} - - 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 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 == None or body == "": - expl = _('No Request Body') - raise exc.HTTPBadRequest(explanation=expl) - - 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) - - 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 - - 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 - - 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/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/contrib/zones.py b/nova/api/openstack/contrib/zones.py deleted file mode 100644 index 628b8182f..000000000 --- a/nova/api/openstack/contrib/zones.py +++ /dev/null @@ -1,43 +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.""" - - -from nova import flags -from nova import log as logging -from nova.api.openstack import extensions - - -LOG = logging.getLogger("nova.api.zones") -FLAGS = flags.FLAGS - - -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/ext/zones/api/v1.1" - updated = "2011-09-21T00:00:00+00:00" - - def get_resources(self): - # Nothing yet. - return [] diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py deleted file mode 100644 index 7da10513e..000000000 --- a/nova/api/openstack/extensions.py +++ /dev/null @@ -1,551 +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 functools -import imp -import inspect -import os -import sys - -from lxml import etree -import routes -import webob.dec -import webob.exc - -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 -import nova.api.openstack -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil - - -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 - - 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 - - -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) - - 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) - - # 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) - - def add_handler(self, handler): - self.controller.add_handler(handler) - - def add_pre_handler(self, pre_handler): - self.controller.add_pre_handler(pre_handler) - - -class ExtensionsResource(wsgi.Resource): - - def __init__(self, extension_manager): - self.extension_manager = extension_manager - - 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 - - def index(self, req): - extensions = [] - for _alias, ext in self.extension_manager.extensions.iteritems(): - extensions.append(self._translate(ext)) - return dict(extensions=extensions) - - 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() - - serializer = wsgi.ResponseSerializer( - {'application/xml': ExtensionsXMLSerializer()}) - # extended resources - 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) - - # 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. - - """ - - def __init__(self): - LOG.audit(_('Initializing extension manager.')) - - self.extensions = {} - self._load_extensions() - - 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) - except AttributeError as ex: - LOG.exception(_("Exception loading extension: %s"), unicode(ex)) - 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.""" - - for ext_factory in FLAGS.osapi_extension: - 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(wsgi.XMLDictSerializer): - - NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - def show(self, ext_dict): - ext = etree.Element('extension', nsmap=self.NSMAP) - self._populate_ext(ext, ext_dict['extension']) - return self._to_xml(ext) - - def index(self, exts_dict): - exts = etree.Element('extensions', nsmap=self.NSMAP) - for ext_dict in exts_dict['extensions']: - ext = etree.SubElement(exts, 'extension') - self._populate_ext(ext, ext_dict) - return self._to_xml(exts) - - def _populate_ext(self, ext_elem, ext_dict): - """Populate an extension xml element from a dict.""" - - ext_elem.set('name', ext_dict['name']) - ext_elem.set('namespace', ext_dict['namespace']) - ext_elem.set('alias', ext_dict['alias']) - ext_elem.set('updated', ext_dict['updated']) - desc = etree.Element('description') - desc.text = ext_dict['description'] - ext_elem.append(desc) - for link in ext_dict.get('links', []): - elem = etree.SubElement(ext_elem, '{%s}link' % xmlutil.XMLNS_ATOM) - elem.set('rel', link['rel']) - elem.set('href', link['href']) - elem.set('type', link['type']) - return ext_elem - - def _to_xml(self, root): - """Convert the xml object to an xml string.""" - - return etree.tostring(root, encoding='UTF-8') - - -def admin_only(fnc): - @functools.wraps(fnc) - def _wrapped(self, *args, **kwargs): - if FLAGS.allow_admin_api: - return fnc(self, *args, **kwargs) - raise webob.exc.HTTPNotFound() - _wrapped.func_name = fnc.func_name - return _wrapped - - -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/faults.py b/nova/api/openstack/faults.py deleted file mode 100644 index 83381d755..000000000 --- a/nova/api/openstack/faults.py +++ /dev/null @@ -1,116 +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.dec -import webob.exc - -from nova.api.openstack import common -from nova.api.openstack import wsgi - - -class Fault(webob.exc.HTTPException): - """Wrap webob.exc.HTTPException to provide API friendly response.""" - - _fault_names = { - 400: "badRequest", - 401: "unauthorized", - 403: "resizeNotAllowed", - 404: "itemNotFound", - 405: "badMethod", - 409: "inProgress", - 413: "overLimit", - 415: "badMediaType", - 501: "notImplemented", - 503: "serviceUnavailable"} - - def __init__(self, exception): - """Create a Fault for the given webob.exc.exception.""" - self.wrapped_exc = exception - self.status_int = exception.status_int - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - """Generate a WSGI response based on the exception passed to ctor.""" - # Replace the body with fault details. - code = self.wrapped_exc.status_int - fault_name = self._fault_names.get(code, "cloudServersFault") - fault_data = { - fault_name: { - 'code': code, - 'message': self.wrapped_exc.explanation}} - if code == 413: - retry = self.wrapped_exc.headers['Retry-After'] - fault_data[fault_name]['retryAfter'] = retry - - # 'code' is an attribute on the fault tag itself - metadata = {'attributes': {fault_name: 'code'}} - - xml_serializer = wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V11) - - content_type = req.best_match_content_type() - serializer = { - 'application/xml': xml_serializer, - 'application/json': wsgi.JSONDictSerializer(), - }[content_type] - - self.wrapped_exc.body = serializer.serialize(fault_data) - self.wrapped_exc.content_type = content_type - - return self.wrapped_exc - - def __str__(self): - return self.wrapped_exc.__str__() - - -class OverLimitFault(webob.exc.HTTPException): - """ - Rate-limited request response. - """ - - def __init__(self, message, details, retry_time): - """ - Initialize new `OverLimitFault` with relevant information. - """ - self.wrapped_exc = webob.exc.HTTPRequestEntityTooLarge() - self.content = { - "overLimitFault": { - "code": self.wrapped_exc.status_int, - "message": message, - "details": details, - }, - } - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, request): - """ - Return the wrapped exception with a serialized body conforming to our - error format. - """ - content_type = request.best_match_content_type() - metadata = {"attributes": {"overLimitFault": "code"}} - - xml_serializer = wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V11) - serializer = { - 'application/xml': xml_serializer, - 'application/json': wsgi.JSONDictSerializer(), - }[content_type] - - content = serializer.serialize(self.content) - self.wrapped_exc.body = content - - return self.wrapped_exc diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py deleted file mode 100644 index 7680ef1ca..000000000 --- a/nova/api/openstack/flavors.py +++ /dev/null @@ -1,124 +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 lxml import etree - -from nova.api.openstack.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 db -from nova import exception - - -class Controller(wsgi.Controller): - """Flavor controller for the OpenStack API.""" - - _view_builder_class = flavors_view.ViewBuilder - - def index(self, req): - """Return all flavors in brief.""" - flavors = self._get_flavors(req) - return self._view_builder.index(req, flavors) - - def detail(self, req): - """Return all flavors in detail.""" - flavors = self._get_flavors(req) - return self._view_builder.detail(req, flavors) - - 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 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_quota", "rxtx_cap"): - 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 FlavorXMLSerializer(xmlutil.XMLTemplateSerializer): - def show(self): - return FlavorTemplate() - - def detail(self): - return FlavorsTemplate() - - def index(self): - return MinimalFlavorsTemplate() - - -def create_resource(): - body_serializers = {'application/xml': FlavorXMLSerializer()} - serializer = wsgi.ResponseSerializer(body_serializers) - return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py deleted file mode 100644 index adb6bee4b..000000000 --- a/nova/api/openstack/image_metadata.py +++ /dev/null @@ -1,122 +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 import exception -from nova import flags -from nova import image -from nova import utils -from nova.api.openstack import common -from nova.api.openstack import wsgi - - -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) - - 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) - - 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() - - 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']) - - 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) - - 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) - - 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(): - headers_serializer = common.MetadataHeadersSerializer() - - body_deserializers = { - 'application/xml': common.MetadataXMLDeserializer(), - } - - body_serializers = { - 'application/xml': common.MetadataXMLSerializer(), - } - serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) - deserializer = wsgi.RequestDeserializer(body_deserializers) - - return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py deleted file mode 100644 index f3ef63b49..000000000 --- a/nova/api/openstack/images.py +++ /dev/null @@ -1,210 +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 os.path - -from lxml import etree -import webob.exc - -from nova import compute -from nova import exception -from nova import flags -import nova.image -from nova import log -from nova.api.openstack import common -from nova.api.openstack import image_metadata -from nova.api.openstack import servers -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.views import images as views_images - - -LOG = log.getLogger('nova.api.openstack.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', -} - - -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.str_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.str_params.get(param) - return filters - - 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() - - 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) - - 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 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 ImageXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return MinimalImagesTemplate() - - def detail(self): - return ImagesTemplate() - - def show(self): - return ImageTemplate() - - -def create_resource(): - body_serializers = {'application/xml': ImageXMLSerializer()} - serializer = wsgi.ResponseSerializer(body_serializers) - return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py deleted file mode 100644 index 2bef453a2..000000000 --- a/nova/api/openstack/ips.py +++ /dev/null @@ -1,115 +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 lxml import etree - -from webob import exc - -import nova -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.views import addresses as view_addresses -from nova import log as logging -from nova import flags - - -LOG = logging.getLogger('nova.api.openstack.ips') -FLAGS = flags.FLAGS - - -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() - - 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) - - 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 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 IPXMLSerializer(xmlutil.XMLTemplateSerializer): - def show(self): - return NetworkTemplate() - - def index(self): - return AddressesTemplate() - - -def create_resource(): - body_serializers = {'application/xml': IPXMLSerializer()} - serializer = wsgi.ResponseSerializer(body_serializers) - return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py deleted file mode 100644 index 56e839526..000000000 --- a/nova/api/openstack/limits.py +++ /dev/null @@ -1,488 +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. -""" - -import copy -import httplib -import json -from lxml import etree -import math -import re -import time -import urllib -import webob.exc - -from collections import defaultdict - -from webob.dec import wsgify - -from nova import quota -from nova import utils -from nova import wsgi as base_wsgi -from nova.api.openstack import common -from nova.api.openstack import faults -from nova.api.openstack.views import limits as limits_views -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil - - -# 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 LimitsController(object): - """ - Controller for accessing limits in the OpenStack API. - """ - - 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() - - -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 LimitsXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return LimitsTemplate() - - -def create_resource(): - body_serializers = {'application/xml': LimitsXMLSerializer()} - serializer = wsgi.ResponseSerializer(body_serializers) - return wsgi.Resource(LimitsController(), serializer=serializer) - - -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 faults.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/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py deleted file mode 100644 index 9ede548c2..000000000 --- a/nova/api/openstack/ratelimiting/__init__.py +++ /dev/null @@ -1,221 +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 faults - -# 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 faults.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/schemas/atom-link.rng b/nova/api/openstack/schemas/atom-link.rng deleted file mode 100644 index edba5eee6..000000000 --- a/nova/api/openstack/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/schemas/atom.rng b/nova/api/openstack/schemas/atom.rng deleted file mode 100644 index c2df4e410..000000000 --- a/nova/api/openstack/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/schemas/v1.1/addresses.rng b/nova/api/openstack/schemas/v1.1/addresses.rng deleted file mode 100644 index b498e8a63..000000000 --- a/nova/api/openstack/schemas/v1.1/addresses.rng +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/extension.rng b/nova/api/openstack/schemas/v1.1/extension.rng deleted file mode 100644 index 336659755..000000000 --- a/nova/api/openstack/schemas/v1.1/extension.rng +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/extensions.rng b/nova/api/openstack/schemas/v1.1/extensions.rng deleted file mode 100644 index 4d8bff646..000000000 --- a/nova/api/openstack/schemas/v1.1/extensions.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/schemas/v1.1/flavor.rng b/nova/api/openstack/schemas/v1.1/flavor.rng deleted file mode 100644 index 6d3adc8dc..000000000 --- a/nova/api/openstack/schemas/v1.1/flavor.rng +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/flavors.rng b/nova/api/openstack/schemas/v1.1/flavors.rng deleted file mode 100644 index b7a3acc01..000000000 --- a/nova/api/openstack/schemas/v1.1/flavors.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/schemas/v1.1/flavors_index.rng b/nova/api/openstack/schemas/v1.1/flavors_index.rng deleted file mode 100644 index d1a4fedb1..000000000 --- a/nova/api/openstack/schemas/v1.1/flavors_index.rng +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/image.rng b/nova/api/openstack/schemas/v1.1/image.rng deleted file mode 100644 index 505081fba..000000000 --- a/nova/api/openstack/schemas/v1.1/image.rng +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/images.rng b/nova/api/openstack/schemas/v1.1/images.rng deleted file mode 100644 index 064d4d9cc..000000000 --- a/nova/api/openstack/schemas/v1.1/images.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/schemas/v1.1/images_index.rng b/nova/api/openstack/schemas/v1.1/images_index.rng deleted file mode 100644 index 3db0b2672..000000000 --- a/nova/api/openstack/schemas/v1.1/images_index.rng +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/limits.rng b/nova/api/openstack/schemas/v1.1/limits.rng deleted file mode 100644 index 1af8108ec..000000000 --- a/nova/api/openstack/schemas/v1.1/limits.rng +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/metadata.rng b/nova/api/openstack/schemas/v1.1/metadata.rng deleted file mode 100644 index b2f5d702a..000000000 --- a/nova/api/openstack/schemas/v1.1/metadata.rng +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/server.rng b/nova/api/openstack/schemas/v1.1/server.rng deleted file mode 100644 index 2e86ccffe..000000000 --- a/nova/api/openstack/schemas/v1.1/server.rng +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/servers.rng b/nova/api/openstack/schemas/v1.1/servers.rng deleted file mode 100644 index 4e2bb8853..000000000 --- a/nova/api/openstack/schemas/v1.1/servers.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/schemas/v1.1/servers_index.rng b/nova/api/openstack/schemas/v1.1/servers_index.rng deleted file mode 100644 index 023e4b66a..000000000 --- a/nova/api/openstack/schemas/v1.1/servers_index.rng +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/version.rng b/nova/api/openstack/schemas/v1.1/version.rng deleted file mode 100644 index ae76270ba..000000000 --- a/nova/api/openstack/schemas/v1.1/version.rng +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/schemas/v1.1/versions.rng b/nova/api/openstack/schemas/v1.1/versions.rng deleted file mode 100644 index 8b2cc7f71..000000000 --- a/nova/api/openstack/schemas/v1.1/versions.rng +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py deleted file mode 100644 index 4145898c1..000000000 --- a/nova/api/openstack/server_metadata.py +++ /dev/null @@ -1,178 +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 import compute -from nova.api.openstack import common -from nova.api.openstack import wsgi -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 - - 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)} - - 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} - - 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} - - 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) - - 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) - - 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(): - headers_serializer = common.MetadataHeadersSerializer() - - body_deserializers = { - 'application/xml': common.MetadataXMLDeserializer(), - } - - body_serializers = { - 'application/xml': common.MetadataXMLSerializer(), - } - serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) - deserializer = wsgi.RequestDeserializer(body_deserializers) - - return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py deleted file mode 100644 index 8b11b30f6..000000000 --- a/nova/api/openstack/servers.py +++ /dev/null @@ -1,1178 +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 -import traceback - -from lxml import etree -from webob import exc -import webob -from xml.dom import minidom - -import nova.api.openstack -from nova.api.openstack import common -from nova.api.openstack import ips -from nova.api.openstack.views import addresses as views_addresses -from nova.api.openstack.views import flavors as views_flavors -from nova.api.openstack.views import images as views_images -from nova.api.openstack.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 network -from nova import db -from nova import exception -from nova import flags -from nova import image -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.servers') -FLAGS = flags.FLAGS - - -class ConvertedException(exc.WSGIHTTPException): - def __init__(self, code, title, explanation): - self.code = code - self.title = title - self.explanation = explanation - super(ConvertedException, self).__init__() - - -class Controller(wsgi.Controller): - """ The Server API base controller class for the OpenStack API """ - - _view_builder_class = views_servers.ViewBuilder - - def __init__(self, **kwargs): - super(Controller, self).__init__(**kwargs) - self.compute_api = compute.API() - self.network_api = network.API() - - 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 - - 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 _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: - 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"), - "InstanceLimitExceeded": - _("Instance quotas have been exceeded")} - - 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 _deserialize_create(self, request): - """ - Deserialize a create request - - Overrides normal behavior in the case of xml content - """ - if request.content_type == "application/xml": - deserializer = ServerXMLDeserializer() - return deserializer.deserialize(request.body) - else: - return self._deserialize(request.body, request.get_content_type()) - - 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_server_admin_password_old_style(self, server): - """ Determine the admin password for a server on creation """ - return utils.generate_password(FLAGS.password_length) - - def _get_server_admin_password_new_style(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_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) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def show(self, req, id): - """ Returns server details by server id """ - try: - instance = self.compute_api.routing_get( - req.environ['nova.context'], id) - return self._view_builder.show(req, instance) - except exception.NotFound: - raise exc.HTTPNotFound() - - 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 - - return server - - 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) - - @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 - - try: - self.compute_api.update(ctxt, id, **update_dict) - except exception.NotFound: - raise exc.HTTPNotFound() - - instance = self.compute_api.routing_get(ctxt, id) - return self._view_builder.show(req, instance) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def action(self, req, id, body): - """Multi-purpose method used to take actions on a server""" - self.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, - } - - if FLAGS.allow_admin_api: - admin_actions = { - 'createBackup': self._action_create_backup, - } - self.actions.update(admin_actions) - - for key in body: - if key in self.actions: - return self.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_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"] - entity = input_dict["createBackup"] - - 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) - - instance = self._get_server(context, instance_id) - - image = self.compute_api.backup(context, - instance, - image_name, - backup_type, - rotation, - extra_properties=props) - - # 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 _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, 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, 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, e: - LOG.exception(_("Error in reboot %s"), e) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def diagnostics(self, req, id): - """Permit Admins to retrieve server diagnostics.""" - ctxt = req.environ["nova.context"] - instance = self._get_server(ctxt, id) - return self.compute_api.get_diagnostics(ctxt, instance) - - def actions(self, req, id): - """Permit Admins to retrieve server actions.""" - ctxt = req.environ["nova.context"] - instance = self._get_server(ctxt, id) - items = self.compute_api.get_actions(ctxt, instance) - actions = [] - # TODO(jk0): Do not do pre-serialization here once the default - # serializer is updated - for item in items: - actions.append(dict( - created_at=str(item.created_at), - action=item.action, - error=item.error)) - return dict(actions=actions) - - 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.CannotResizeToSmallerSize: - msg = _("Resizing to a smaller size is not supported.") - raise exc.HTTPBadRequest(explanation=msg) - - return webob.Response(status_int=202) - - @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() - - 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): - context = request.environ['nova.context'] - instance = self._get_server(context, instance_id) - - try: - image_href = info["rebuild"]["imageRef"] - except (KeyError, TypeError): - msg = _("Could not parse imageRef from request.") - LOG.debug(msg) - raise exc.HTTPBadRequest(explanation=msg) - - personality = info["rebuild"].get("personality", []) - injected_files = [] - if personality: - injected_files = self._get_injected_files(personality) - - metadata = info["rebuild"].get("metadata") - name = info["rebuild"].get("name") - - if metadata: - self._validate_metadata(metadata) - - if 'rebuild' in info and 'adminPass' in info['rebuild']: - password = info['rebuild']['adminPass'] - else: - password = utils.generate_password(FLAGS.password_length) - - try: - self.compute_api.rebuild(context, instance, image_href, - password, name=name, metadata=metadata, - files_to_inject=injected_files) - except exception.RebuildRequiresActiveInstance: - msg = _("Instance %s must be active to rebuild.") % instance_id - raise exc.HTTPConflict(explanation=msg) - except exception.InstanceNotFound: - msg = _("Instance %s could not be found") % instance_id - raise exc.HTTPNotFound(explanation=msg) - - instance = self._get_server(context, instance_id) - view = self._view_builder.show(request, instance) - view['server']['adminPass'] = password - - return view - - @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.InstanceBusy: - msg = _("Server is currently creating an image. Please wait.") - raise webob.exc.HTTPConflict(explanation=msg) - - # 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_default_xmlns(self, req): - return common.XML_NS_V11 - - def _get_server_admin_password(self, server): - """ Determine the admin password for a server on creation """ - return self._get_server_admin_password_new_style(server) - - 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') - - -class HeadersSerializer(wsgi.ResponseHeadersSerializer): - - def create(self, response, data): - response.status_int = 202 - - def delete(self, response, data): - response.status_int = 204 - - def action(self, response, data): - response.status_int = 202 - - -class SecurityGroupsTemplateElement(xmlutil.TemplateElement): - def will_render(self, datum): - return 'security_groups' in datum - - -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 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) - - -class ServerXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return MinimalServersTemplate() - - def detail(self): - return ServersTemplate() - - def show(self): - return ServerTemplate() - - def update(self): - return ServerTemplate() - - def create(self): - master = ServerTemplate() - master.attach(ServerAdminPassTemplate()) - return master - - def action(self): - return self.create() - - -class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer): - """ - 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 action(self, string): - dom = minidom.parseString(string) - action_node = dom.childNodes[0] - action_name = action_node.tagName - - action_deserializer = { - 'createImage': self._action_create_image, - 'createBackup': self._action_create_backup, - '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, 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_create_backup(self, node): - attributes = ('name', 'backup_type', 'rotation') - return self._deserialize_image_action(node, attributes) - - 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 - - def create(self, string): - """Deserialize an xml-formatted server create request""" - dom = minidom.parseString(string) - server = self._extract_server(dom) - return {'body': {'server': server}} - - 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_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_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 - - -def create_resource(): - headers_serializer = HeadersSerializer() - body_serializers = {'application/xml': ServerXMLSerializer()} - serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) - body_deserializers = {'application/xml': ServerXMLDeserializer()} - deserializer = wsgi.RequestDeserializer(body_deserializers) - return wsgi.Resource(Controller(), deserializer, serializer) - - -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/urlmap.py b/nova/api/openstack/urlmap.py deleted file mode 100644 index 199fa6130..000000000 --- a/nova/api/openstack/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.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/users.py b/nova/api/openstack/users.py deleted file mode 100644 index 9fac45763..000000000 --- a/nova/api/openstack/users.py +++ /dev/null @@ -1,139 +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 import exception -from nova import flags -from nova import log as logging -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.auth import manager - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack') - - -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() - - 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) - - def detail(self, req): - """Return all users in detail""" - return self.index(req) - - 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 {} - - 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)) - - 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))) - - -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) - - -class UserXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return UsersTemplate() - - def default(self): - return UserTemplate() - - -def create_resource(): - body_serializers = { - 'application/xml': UserXMLSerializer(), - } - - serializer = wsgi.ResponseSerializer(body_serializers) - - return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/v2/__init__.py b/nova/api/openstack/v2/__init__.py new file mode 100644 index 000000000..1ea2ba250 --- /dev/null +++ b/nova/api/openstack/v2/__init__.py @@ -0,0 +1,186 @@ +# 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 accounts +from nova.api.openstack.v2 import consoles +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 users +from nova.api.openstack.v2 import versions +from nova.api.openstack.v2 import zones +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): + self.server_members = {} + mapper = ProjectMapper() + self._setup_routes(mapper) + super(APIRouter, self).__init__(mapper) + + def _setup_routes(self, mapper): + server_members = self.server_members + server_members['action'] = 'POST' + if FLAGS.allow_admin_api: + LOG.debug(_("Including admin operations in API.")) + + server_members['diagnostics'] = 'GET' + server_members['actions'] = 'GET' + + mapper.resource("user", "users", + controller=users.create_resource(), + collection={'detail': 'GET'}) + + mapper.resource("account", "accounts", + controller=accounts.create_resource(), + collection={'detail': 'GET'}) + + mapper.resource("zone", "zones", + controller=zones.create_resource(), + collection={'detail': 'GET', + 'info': 'GET', + 'select': 'POST'}) + + 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=self.server_members) + + 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/accounts.py b/nova/api/openstack/v2/accounts.py new file mode 100644 index 000000000..0f1584261 --- /dev/null +++ b/nova/api/openstack/v2/accounts.py @@ -0,0 +1,103 @@ +# 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 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.accounts') + + +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() + + def detail(self, req): + raise webob.exc.HTTPNotImplemented() + + 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() + + 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 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) + + +class AccountXMLSerializer(xmlutil.XMLTemplateSerializer): + def default(self): + return AccountTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': AccountXMLSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/v2/auth.py b/nova/api/openstack/v2/auth.py new file mode 100644 index 000000000..c0ea30671 --- /dev/null +++ b/nova/api/openstack/v2/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.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] == 'v1.1': + project_id = path_parts[2] + # Check that the project for project_id exists, and that user + # is authorized to use it + try: + project = 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. /v1.0 or /v1.1).") + 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 == '1.1': + 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 new file mode 100644 index 000000000..ad0d8cbb4 --- /dev/null +++ b/nova/api/openstack/v2/consoles.py @@ -0,0 +1,140 @@ +# 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'], + 'port': cons['port'], + 'host': pool['public_hostname']} + return dict(console=info) + + +class Controller(object): + """The Consoles controller for the Openstack API""" + + def __init__(self): + self.console_api = console.API() + + 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) + + 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) + + +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 ConsoleXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return ConsolesTemplate() + + def show(self): + return ConsoleTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': ConsoleXMLSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/v2/contrib/__init__.py b/nova/api/openstack/v2/contrib/__init__.py new file mode 100644 index 000000000..d361dac9c --- /dev/null +++ b/nova/api/openstack/v2/contrib/__init__.py @@ -0,0 +1,90 @@ +# 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/admin_actions.py b/nova/api/openstack/v2/contrib/admin_actions.py new file mode 100644 index 000000000..632a8dc0e --- /dev/null +++ b/nova/api/openstack/v2/contrib/admin_actions.py @@ -0,0 +1,206 @@ +# 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 traceback + +import webob +from webob import exc + +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): + """Adds admin-only server actions: pause, unpause, suspend, + resume, migrate, resetNetwork, injectNetworkInfo, lock and unlock + """ + + name = "AdminActions" + alias = "os-admin-actions" + namespace = "http://docs.openstack.org/ext/admin-actions/api/v1.1" + updated = "2011-09-20T00:00:00+00:00" + + def __init__(self, ext_mgr): + super(Admin_actions, self).__init__(ext_mgr) + self.compute_api = compute.API() + + @extensions.admin_only + @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: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::pause %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @extensions.admin_only + @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: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::unpause %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @extensions.admin_only + @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: + readable = traceback.format_exc() + LOG.exception(_("compute.api::suspend %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @extensions.admin_only + @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: + readable = traceback.format_exc() + LOG.exception(_("compute.api::resume %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @extensions.admin_only + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _migrate(self, input_dict, req, id): + """Permit admins to migrate a server to a new host""" + try: + self.compute_api.resize(req.environ['nova.context'], id) + except Exception, e: + LOG.exception(_("Error in migrate %s"), e) + raise exc.HTTPBadRequest() + return webob.Response(status_int=202) + + @extensions.admin_only + @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) + + @extensions.admin_only + @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) + + @extensions.admin_only + @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) + + @extensions.admin_only + @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 get_actions(self): + actions = [ + 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", + "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/createserverext.py b/nova/api/openstack/v2/contrib/createserverext.py new file mode 100644 index 000000000..a207d1bc5 --- /dev/null +++ b/nova/api/openstack/v2/contrib/createserverext.py @@ -0,0 +1,74 @@ +# 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/ext/createserverext/api/v1.1" + updated = "2011-07-19T00:00:00+00:00" + + def get_resources(self): + resources = [] + + headers_serializer = servers.HeadersSerializer() + body_serializers = { + 'application/xml': servers.ServerXMLSerializer(), + } + + body_deserializers = { + 'application/xml': servers.ServerXMLDeserializer(), + } + + serializer = wsgi.ResponseSerializer(body_serializers, + headers_serializer) + deserializer = wsgi.RequestDeserializer(body_deserializers) + controller = Controller() + + res = extensions.ResourceExtension('os-create-server-ext', + controller=controller, + deserializer=deserializer, + serializer=serializer) + 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 new file mode 100644 index 000000000..95eb8f42c --- /dev/null +++ b/nova/api/openstack/v2/contrib/deferred_delete.py @@ -0,0 +1,66 @@ +# 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.v2 import extensions +from nova.api.openstack.v2 import servers +from nova import compute +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/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) + self.compute_api.restore(context, instance) + 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) + self.compute_api.force_delete(context, instance) + 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 new file mode 100644 index 000000000..6e26c1daa --- /dev/null +++ b/nova/api/openstack/v2/contrib/disk_config.py @@ -0,0 +1,189 @@ +# 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 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()) + + for server in servers: + db_server = self.compute_api.routing_get(context, server['id']) + 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 new file mode 100644 index 000000000..5e0a69780 --- /dev/null +++ b/nova/api/openstack/v2/contrib/extended_status.py @@ -0,0 +1,109 @@ +# 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.""" + +import traceback + +import webob +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/ext/extended_status/api/v1.1" + updated = "2011-11-03T00:00:00+00:00" + + 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: + 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 body['servers']: + try: + inst_ref = compute_api.routing_get(context, server['id']) + except exception.NotFound: + 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) + 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 + + if FLAGS.allow_admin_api: + 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 new file mode 100644 index 000000000..0b0938054 --- /dev/null +++ b/nova/api/openstack/v2/contrib/flavorextradata.py @@ -0,0 +1,36 @@ +# 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/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 new file mode 100644 index 000000000..bb38ea64e --- /dev/null +++ b/nova/api/openstack/v2/contrib/flavorextraspecs.py @@ -0,0 +1,114 @@ +# 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.v2 import extensions +from nova import db +from nova import exception + + +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 == None or body == "": + expl = _('No Request Body') + raise exc.HTTPBadRequest(explanation=expl) + + 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) + + 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 + + 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 + + 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/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_ips.py b/nova/api/openstack/v2/contrib/floating_ips.py new file mode 100644 index 000000000..9689eac48 --- /dev/null +++ b/nova/api/openstack/v2/contrib/floating_ips.py @@ -0,0 +1,201 @@ +# 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.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 _translate_floating_ip_view(floating_ip): + result = {'id': floating_ip['id'], + 'ip': floating_ip['address']} + 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_id'] + 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.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "floating_ip": [ + "id", + "ip", + "instance_id", + "fixed_ip", + ]}}} + + def __init__(self): + self.network_api = network.API() + super(FloatingIPController, self).__init__() + + 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) + + def index(self, req): + context = req.environ['nova.context'] + + try: + get_floating_ips = self.network_api.get_floating_ips_by_project + floating_ips = get_floating_ips(context) + except exception.FloatingIpNotFoundForProject: + floating_ips = [] + + return _translate_floating_ips_view(floating_ips) + + def create(self, req, body=None): + context = req.environ['nova.context'] + + try: + address = self.network_api.allocate_floating_ip(context) + 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': + 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 Floating_ips(extensions.ExtensionDescriptor): + """Floating IPs support""" + + name = "Floating_ips" + alias = "os-floating-ips" + namespace = "http://docs.openstack.org/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: + self.compute_api.associate_floating_ip(context, instance_id, + 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 new file mode 100644 index 000000000..eb5074520 --- /dev/null +++ b/nova/api/openstack/v2/contrib/hosts.py @@ -0,0 +1,132 @@ +# 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 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 + + +LOG = logging.getLogger("nova.api.openstack.v2.contrib.hosts") +FLAGS = flags.FLAGS + + +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__() + + def index(self, req): + return {'hosts': _list_hosts(req)} + + @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} + + @extensions.admin_only + def startup(self, req, id): + return self._host_power_action(req, host=id, action="startup") + + @extensions.admin_only + def shutdown(self, req, id): + return self._host_power_action(req, host=id, action="shutdown") + + @extensions.admin_only + def reboot(self, req, id): + return self._host_power_action(req, host=id, action="reboot") + + +class Hosts(extensions.ExtensionDescriptor): + """Host administration""" + + name = "Hosts" + alias = "os-hosts" + namespace = "http://docs.openstack.org/ext/hosts/api/v1.1" + updated = "2011-06-29T00:00:00+00:00" + + 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 new file mode 100644 index 000000000..e57d40001 --- /dev/null +++ b/nova/api/openstack/v2/contrib/keypairs.py @@ -0,0 +1,136 @@ +# 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.v2 import extensions +from nova import crypto +from nova import db +from nova import exception + + +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} + + 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) + + 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 Keypairs(extensions.ExtensionDescriptor): + """Keypair Support""" + + name = "Keypairs" + alias = "os-keypairs" + namespace = "http://docs.openstack.org/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 new file mode 100644 index 000000000..561e246b5 --- /dev/null +++ b/nova/api/openstack/v2/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.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/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/quotas.py b/nova/api/openstack/v2/contrib/quotas.py new file mode 100644 index 000000000..7ad426c44 --- /dev/null +++ b/nova/api/openstack/v2/contrib/quotas.py @@ -0,0 +1,91 @@ +# 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.v2 import extensions +from nova import db +from nova import exception +from nova import quota + + +class QuotaSetsController(object): + + def _format_quota_set(self, project_id, quota_set): + """Convert the quota object to a result dict""" + + return {'quota_set': { + 'id': str(project_id), + 'metadata_items': quota_set['metadata_items'], + 'injected_file_content_bytes': + quota_set['injected_file_content_bytes'], + 'volumes': quota_set['volumes'], + 'gigabytes': quota_set['gigabytes'], + 'ram': quota_set['ram'], + 'floating_ips': quota_set['floating_ips'], + 'instances': quota_set['instances'], + 'injected_files': quota_set['injected_files'], + 'cores': quota_set['cores'], + }} + + 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() + + def update(self, req, id, body): + context = req.environ['nova.context'] + project_id = id + resources = ['metadata_items', 'injected_file_content_bytes', + 'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances', + 'injected_files', 'cores'] + for key in body['quota_set'].keys(): + if key in 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/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 new file mode 100644 index 000000000..b63bf9ec5 --- /dev/null +++ b/nova/api/openstack/v2/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.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/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 new file mode 100644 index 000000000..50f129bd6 --- /dev/null +++ b/nova/api/openstack/v2/contrib/security_groups.py @@ -0,0 +1,551 @@ +# 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 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 + + +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 + + 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) + + 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'])))} + + 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): + + 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/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 = [] + + metadata = _get_metadata() + body_serializers = { + 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, + xmlns=wsgi.XMLNS_V11), + } + serializer = wsgi.ResponseSerializer(body_serializers, None) + + body_deserializers = { + 'application/xml': SecurityGroupXMLDeserializer(), + } + deserializer = wsgi.RequestDeserializer(body_deserializers) + + res = extensions.ResourceExtension('os-security-groups', + controller=SecurityGroupController(), + deserializer=deserializer, + serializer=serializer) + + resources.append(res) + + body_deserializers = { + 'application/xml': SecurityGroupRulesXMLDeserializer(), + } + deserializer = wsgi.RequestDeserializer(body_deserializers) + + res = extensions.ResourceExtension('os-security-group-rules', + controller=SecurityGroupRulesController(), + deserializer=deserializer, + serializer=serializer) + resources.append(res) + return resources + + +class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): + """ + Deserializer to handle xml-formatted security group requests. + """ + def create(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 create(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 + + +def _get_metadata(): + metadata = { + "attributes": { + "security_group": ["id", "tenant_id", "name"], + "rule": ["id", "parent_group_id"], + "security_group_rule": ["id", "parent_group_id"], + } + } + return metadata diff --git a/nova/api/openstack/v2/contrib/simple_tenant_usage.py b/nova/api/openstack/v2/contrib/simple_tenant_usage.py new file mode 100644 index 000000000..e896371ed --- /dev/null +++ b/nova/api/openstack/v2/contrib/simple_tenant_usage.py @@ -0,0 +1,229 @@ +# 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 webob import exc + +from nova.api.openstack.v2 import extensions +from nova.api.openstack.v2 import views +from nova.compute import api +from nova.db.sqlalchemy.session import get_session +from nova import exception +from nova import flags + + +FLAGS = flags.FLAGS + + +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: + try: + return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S.%f") + except: + 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) + + def index(self, req): + """Retrive tenant_usage for all tenants""" + context = req.environ['nova.context'] + + if not context.is_admin and FLAGS.allow_admin_api: + 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} + + 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 and FLAGS.allow_admin_api: + 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/ext/os-simple-tenant-usage/api/v1.1" + updated = "2011-08-19T00:00:00+00:00" + + 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/virtual_interfaces.py b/nova/api/openstack/v2/contrib/virtual_interfaces.py new file mode 100644 index 000000000..1975a8dc0 --- /dev/null +++ b/nova/api/openstack/v2/contrib/virtual_interfaces.py @@ -0,0 +1,89 @@ +# 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 import log as logging +from nova import network + + +LOG = logging.getLogger("nova.api.openstack.v2.contrib.virtual_interfaces") + + +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 + + +def _get_metadata(): + metadata = { + "attributes": { + 'virtual_interface': ["id", "mac_address"]}} + return metadata + + +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} + + 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/ext/virtual_interfaces/api/v1.1" + updated = "2011-08-17T00:00:00+00:00" + + def get_resources(self): + resources = [] + + metadata = _get_metadata() + body_serializers = { + 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, + xmlns=wsgi.XMLNS_V11)} + serializer = wsgi.ResponseSerializer(body_serializers, None) + res = extensions.ResourceExtension( + 'os-virtual-interfaces', + controller=ServerVirtualInterfaceController(), + parent=dict(member_name='server', collection_name='servers'), + serializer=serializer) + 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 new file mode 100644 index 000000000..f3a7831d5 --- /dev/null +++ b/nova/api/openstack/v2/contrib/virtual_storage_arrays.py @@ -0,0 +1,597 @@ +# 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 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 + + +class VsaController(object): + """The Virtual Storage Array API controller for the OpenStack API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "vsa": [ + "id", + "name", + "displayName", + "displayDescription", + "createTime", + "status", + "vcType", + "vcCount", + "driveCount", + "ipAddress", + ]}}} + + 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} + + def index(self, req): + """Return a short list of VSAs.""" + return self._items(req, details=False) + + def detail(self, req): + """Return a detailed list of VSAs.""" + return self._items(req, details=True) + + 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)} + + 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 + + +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 + + """ + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "name", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + "vsaId", + ]}}} + + 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 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__() + + +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() + + +class VsaVPoolController(object): + """The vPool VSA API controller for the OpenStack API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "vpool": [ + "id", + "vsaId", + "name", + "displayName", + "displayDescription", + "driveCount", + "driveIds", + "protection", + "stripeSize", + "stripeWidth", + "createTime", + "status", + ]}}} + + def __init__(self): + self.vsa_api = vsa.API() + super(VsaVPoolController, self).__init__() + + 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) + + 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() + + 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/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 new file mode 100644 index 000000000..5d86282f2 --- /dev/null +++ b/nova/api/openstack/v2/contrib/volumes.py @@ -0,0 +1,370 @@ +# 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 import compute +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova import quota +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'] + + 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.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + "volumeType", + "metadata", + ]}}} + + 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, None, + 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 + + +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) + + """ + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(VolumeAttachmentController, self).__init__() + + 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) + + 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)} + + 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') + + +class Volumes(extensions.ExtensionDescriptor): + """Volumes support""" + + name = "Volumes" + alias = "os-volumes" + namespace = "http://docs.openstack.org/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) + + return resources diff --git a/nova/api/openstack/v2/contrib/volumetypes.py b/nova/api/openstack/v2/contrib/volumetypes.py new file mode 100644 index 000000000..ca3866c99 --- /dev/null +++ b/nova/api/openstack/v2/contrib/volumetypes.py @@ -0,0 +1,185 @@ +# 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 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 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} + + 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 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 == None or body == "": + expl = _('No Request Body') + raise exc.HTTPBadRequest(explanation=expl) + + 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) + + 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 + + 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 + + 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/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 new file mode 100644 index 000000000..76a235713 --- /dev/null +++ b/nova/api/openstack/v2/contrib/zones.py @@ -0,0 +1,43 @@ +# 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.""" + + +from nova import flags +from nova import log as logging +from nova.api.openstack.v2 import extensions + + +LOG = logging.getLogger("nova.api.openstack.v2.contrib.zones") +FLAGS = flags.FLAGS + + +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/ext/zones/api/v1.1" + updated = "2011-09-21T00:00:00+00:00" + + def get_resources(self): + # Nothing yet. + return [] diff --git a/nova/api/openstack/v2/extensions.py b/nova/api/openstack/v2/extensions.py new file mode 100644 index 000000000..a5cf863ed --- /dev/null +++ b/nova/api/openstack/v2/extensions.py @@ -0,0 +1,551 @@ +# 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 functools +import imp +import inspect +import os +import sys + +from lxml import etree +import routes +import webob.dec +import webob.exc + +import nova.api.openstack.v2 +from nova.api.openstack import common +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 + + 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 + + +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) + + 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) + + # 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) + + def add_handler(self, handler): + self.controller.add_handler(handler) + + def add_pre_handler(self, pre_handler): + self.controller.add_pre_handler(pre_handler) + + +class ExtensionsResource(wsgi.Resource): + + def __init__(self, extension_manager): + self.extension_manager = extension_manager + + 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 + + def index(self, req): + extensions = [] + for _alias, ext in self.extension_manager.extensions.iteritems(): + extensions.append(self._translate(ext)) + return dict(extensions=extensions) + + 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() + + serializer = wsgi.ResponseSerializer( + {'application/xml': ExtensionsXMLSerializer()}) + # extended resources + 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) + + # 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. + + """ + + def __init__(self): + LOG.audit(_('Initializing extension manager.')) + + self.extensions = {} + self._load_extensions() + + 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) + except AttributeError as ex: + LOG.exception(_("Exception loading extension: %s"), unicode(ex)) + 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.""" + + for ext_factory in FLAGS.osapi_extension: + 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(wsgi.XMLDictSerializer): + + NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + def show(self, ext_dict): + ext = etree.Element('extension', nsmap=self.NSMAP) + self._populate_ext(ext, ext_dict['extension']) + return self._to_xml(ext) + + def index(self, exts_dict): + exts = etree.Element('extensions', nsmap=self.NSMAP) + for ext_dict in exts_dict['extensions']: + ext = etree.SubElement(exts, 'extension') + self._populate_ext(ext, ext_dict) + return self._to_xml(exts) + + def _populate_ext(self, ext_elem, ext_dict): + """Populate an extension xml element from a dict.""" + + ext_elem.set('name', ext_dict['name']) + ext_elem.set('namespace', ext_dict['namespace']) + ext_elem.set('alias', ext_dict['alias']) + ext_elem.set('updated', ext_dict['updated']) + desc = etree.Element('description') + desc.text = ext_dict['description'] + ext_elem.append(desc) + for link in ext_dict.get('links', []): + elem = etree.SubElement(ext_elem, '{%s}link' % xmlutil.XMLNS_ATOM) + elem.set('rel', link['rel']) + elem.set('href', link['href']) + elem.set('type', link['type']) + return ext_elem + + def _to_xml(self, root): + """Convert the xml object to an xml string.""" + + return etree.tostring(root, encoding='UTF-8') + + +def admin_only(fnc): + @functools.wraps(fnc) + def _wrapped(self, *args, **kwargs): + if FLAGS.allow_admin_api: + return fnc(self, *args, **kwargs) + raise webob.exc.HTTPNotFound() + _wrapped.func_name = fnc.func_name + return _wrapped + + +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 new file mode 100644 index 000000000..d5b673c7f --- /dev/null +++ b/nova/api/openstack/v2/flavors.py @@ -0,0 +1,124 @@ +# 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.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 db +from nova import exception + + +class Controller(wsgi.Controller): + """Flavor controller for the OpenStack API.""" + + _view_builder_class = flavors_view.ViewBuilder + + def index(self, req): + """Return all flavors in brief.""" + flavors = self._get_flavors(req) + return self._view_builder.index(req, flavors) + + def detail(self, req): + """Return all flavors in detail.""" + flavors = self._get_flavors(req) + return self._view_builder.detail(req, flavors) + + 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 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_quota", "rxtx_cap"): + 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 FlavorXMLSerializer(xmlutil.XMLTemplateSerializer): + def show(self): + return FlavorTemplate() + + def detail(self): + return FlavorsTemplate() + + def index(self): + return MinimalFlavorsTemplate() + + +def create_resource(): + body_serializers = {'application/xml': FlavorXMLSerializer()} + serializer = wsgi.ResponseSerializer(body_serializers) + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/v2/image_metadata.py b/nova/api/openstack/v2/image_metadata.py new file mode 100644 index 000000000..e3cfac4e0 --- /dev/null +++ b/nova/api/openstack/v2/image_metadata.py @@ -0,0 +1,122 @@ +# 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 +from nova import utils + + +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) + + 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) + + 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() + + 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']) + + 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) + + 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) + + 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(): + headers_serializer = common.MetadataHeadersSerializer() + + body_deserializers = { + 'application/xml': common.MetadataXMLDeserializer(), + } + + body_serializers = { + 'application/xml': common.MetadataXMLSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) + deserializer = wsgi.RequestDeserializer(body_deserializers) + + return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/nova/api/openstack/v2/images.py b/nova/api/openstack/v2/images.py new file mode 100644 index 000000000..84b7b17db --- /dev/null +++ b/nova/api/openstack/v2/images.py @@ -0,0 +1,208 @@ +# 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 os.path + +from lxml import etree +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', +} + + +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.str_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.str_params.get(param) + return filters + + 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() + + 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) + + 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 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 ImageXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return MinimalImagesTemplate() + + def detail(self): + return ImagesTemplate() + + def show(self): + return ImageTemplate() + + +def create_resource(): + body_serializers = {'application/xml': ImageXMLSerializer()} + serializer = wsgi.ResponseSerializer(body_serializers) + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/v2/ips.py b/nova/api/openstack/v2/ips.py new file mode 100644 index 000000000..705754665 --- /dev/null +++ b/nova/api/openstack/v2/ips.py @@ -0,0 +1,114 @@ +# 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 lxml import etree +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 + + +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() + + 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) + + 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 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 IPXMLSerializer(xmlutil.XMLTemplateSerializer): + def show(self): + return NetworkTemplate() + + def index(self): + return AddressesTemplate() + + +def create_resource(): + body_serializers = {'application/xml': IPXMLSerializer()} + serializer = wsgi.ResponseSerializer(body_serializers) + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/v2/limits.py b/nova/api/openstack/v2/limits.py new file mode 100644 index 000000000..04ff9e647 --- /dev/null +++ b/nova/api/openstack/v2/limits.py @@ -0,0 +1,486 @@ +# 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 +import urllib + +from lxml import etree +from webob.dec import wsgify +import webob.exc + +from nova.api.openstack import common +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 + + +class LimitsController(object): + """ + Controller for accessing limits in the OpenStack API. + """ + + 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() + + +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 LimitsXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return LimitsTemplate() + + +def create_resource(): + body_serializers = {'application/xml': LimitsXMLSerializer()} + serializer = wsgi.ResponseSerializer(body_serializers) + return wsgi.Resource(LimitsController(), serializer=serializer) + + +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 new file mode 100644 index 000000000..78dc465a7 --- /dev/null +++ b/nova/api/openstack/v2/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/v2/schemas/atom-link.rng b/nova/api/openstack/v2/schemas/atom-link.rng new file mode 100644 index 000000000..edba5eee6 --- /dev/null +++ b/nova/api/openstack/v2/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/v2/schemas/atom.rng b/nova/api/openstack/v2/schemas/atom.rng new file mode 100644 index 000000000..c2df4e410 --- /dev/null +++ b/nova/api/openstack/v2/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/v2/schemas/v1.1/addresses.rng b/nova/api/openstack/v2/schemas/v1.1/addresses.rng new file mode 100644 index 000000000..b498e8a63 --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/addresses.rng @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/extension.rng b/nova/api/openstack/v2/schemas/v1.1/extension.rng new file mode 100644 index 000000000..336659755 --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/extension.rng @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/extensions.rng b/nova/api/openstack/v2/schemas/v1.1/extensions.rng new file mode 100644 index 000000000..4d8bff646 --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/extensions.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/flavor.rng b/nova/api/openstack/v2/schemas/v1.1/flavor.rng new file mode 100644 index 000000000..6d3adc8dc --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/flavor.rng @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/flavors.rng b/nova/api/openstack/v2/schemas/v1.1/flavors.rng new file mode 100644 index 000000000..b7a3acc01 --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/flavors.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/flavors_index.rng b/nova/api/openstack/v2/schemas/v1.1/flavors_index.rng new file mode 100644 index 000000000..d1a4fedb1 --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/flavors_index.rng @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/image.rng b/nova/api/openstack/v2/schemas/v1.1/image.rng new file mode 100644 index 000000000..505081fba --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/image.rng @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/images.rng b/nova/api/openstack/v2/schemas/v1.1/images.rng new file mode 100644 index 000000000..064d4d9cc --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/images.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/images_index.rng b/nova/api/openstack/v2/schemas/v1.1/images_index.rng new file mode 100644 index 000000000..3db0b2672 --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/images_index.rng @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/limits.rng b/nova/api/openstack/v2/schemas/v1.1/limits.rng new file mode 100644 index 000000000..1af8108ec --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/limits.rng @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/metadata.rng b/nova/api/openstack/v2/schemas/v1.1/metadata.rng new file mode 100644 index 000000000..b2f5d702a --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/metadata.rng @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/server.rng b/nova/api/openstack/v2/schemas/v1.1/server.rng new file mode 100644 index 000000000..2e86ccffe --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/server.rng @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/servers.rng b/nova/api/openstack/v2/schemas/v1.1/servers.rng new file mode 100644 index 000000000..4e2bb8853 --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/servers.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/servers_index.rng b/nova/api/openstack/v2/schemas/v1.1/servers_index.rng new file mode 100644 index 000000000..023e4b66a --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/servers_index.rng @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/version.rng b/nova/api/openstack/v2/schemas/v1.1/version.rng new file mode 100644 index 000000000..ae76270ba --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/version.rng @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/v2/schemas/v1.1/versions.rng b/nova/api/openstack/v2/schemas/v1.1/versions.rng new file mode 100644 index 000000000..8b2cc7f71 --- /dev/null +++ b/nova/api/openstack/v2/schemas/v1.1/versions.rng @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/nova/api/openstack/v2/server_metadata.py b/nova/api/openstack/v2/server_metadata.py new file mode 100644 index 000000000..47dbe2fbd --- /dev/null +++ b/nova/api/openstack/v2/server_metadata.py @@ -0,0 +1,178 @@ +# 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 + + 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)} + + 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} + + 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} + + 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) + + 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) + + 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(): + headers_serializer = common.MetadataHeadersSerializer() + + body_deserializers = { + 'application/xml': common.MetadataXMLDeserializer(), + } + + body_serializers = { + 'application/xml': common.MetadataXMLSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) + deserializer = wsgi.RequestDeserializer(body_deserializers) + + return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/nova/api/openstack/v2/servers.py b/nova/api/openstack/v2/servers.py new file mode 100644 index 000000000..1ae7f80bf --- /dev/null +++ b/nova/api/openstack/v2/servers.py @@ -0,0 +1,1177 @@ +# 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 +import traceback + +from lxml import etree +from webob import exc +import webob +from xml.dom import minidom + +from nova.api.openstack import common +from nova.api.openstack.v2 import ips +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.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 network +from nova import db +from nova import exception +from nova import flags +from nova import image +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 ConvertedException(exc.WSGIHTTPException): + def __init__(self, code, title, explanation): + self.code = code + self.title = title + self.explanation = explanation + super(ConvertedException, self).__init__() + + +class Controller(wsgi.Controller): + """ The Server API base controller class for the OpenStack API """ + + _view_builder_class = views_servers.ViewBuilder + + def __init__(self, **kwargs): + super(Controller, self).__init__(**kwargs) + self.compute_api = compute.API() + self.network_api = network.API() + + 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 + + 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 _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: + 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"), + "InstanceLimitExceeded": + _("Instance quotas have been exceeded")} + + 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 _deserialize_create(self, request): + """ + Deserialize a create request + + Overrides normal behavior in the case of xml content + """ + if request.content_type == "application/xml": + deserializer = ServerXMLDeserializer() + return deserializer.deserialize(request.body) + else: + return self._deserialize(request.body, request.get_content_type()) + + 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_server_admin_password_old_style(self, server): + """ Determine the admin password for a server on creation """ + return utils.generate_password(FLAGS.password_length) + + def _get_server_admin_password_new_style(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_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) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def show(self, req, id): + """ Returns server details by server id """ + try: + instance = self.compute_api.routing_get( + req.environ['nova.context'], id) + return self._view_builder.show(req, instance) + except exception.NotFound: + raise exc.HTTPNotFound() + + 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 + + return server + + 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) + + @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 + + try: + self.compute_api.update(ctxt, id, **update_dict) + except exception.NotFound: + raise exc.HTTPNotFound() + + instance = self.compute_api.routing_get(ctxt, id) + return self._view_builder.show(req, instance) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def action(self, req, id, body): + """Multi-purpose method used to take actions on a server""" + self.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, + } + + if FLAGS.allow_admin_api: + admin_actions = { + 'createBackup': self._action_create_backup, + } + self.actions.update(admin_actions) + + for key in body: + if key in self.actions: + return self.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_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"] + entity = input_dict["createBackup"] + + 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) + + instance = self._get_server(context, instance_id) + + image = self.compute_api.backup(context, + instance, + image_name, + backup_type, + rotation, + extra_properties=props) + + # 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 _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, 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, 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, e: + LOG.exception(_("Error in reboot %s"), e) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def diagnostics(self, req, id): + """Permit Admins to retrieve server diagnostics.""" + ctxt = req.environ["nova.context"] + instance = self._get_server(ctxt, id) + return self.compute_api.get_diagnostics(ctxt, instance) + + def actions(self, req, id): + """Permit Admins to retrieve server actions.""" + ctxt = req.environ["nova.context"] + instance = self._get_server(ctxt, id) + items = self.compute_api.get_actions(ctxt, instance) + actions = [] + # TODO(jk0): Do not do pre-serialization here once the default + # serializer is updated + for item in items: + actions.append(dict( + created_at=str(item.created_at), + action=item.action, + error=item.error)) + return dict(actions=actions) + + 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.CannotResizeToSmallerSize: + msg = _("Resizing to a smaller size is not supported.") + raise exc.HTTPBadRequest(explanation=msg) + + return webob.Response(status_int=202) + + @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() + + 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): + context = request.environ['nova.context'] + instance = self._get_server(context, instance_id) + + try: + image_href = info["rebuild"]["imageRef"] + except (KeyError, TypeError): + msg = _("Could not parse imageRef from request.") + LOG.debug(msg) + raise exc.HTTPBadRequest(explanation=msg) + + personality = info["rebuild"].get("personality", []) + injected_files = [] + if personality: + injected_files = self._get_injected_files(personality) + + metadata = info["rebuild"].get("metadata") + name = info["rebuild"].get("name") + + if metadata: + self._validate_metadata(metadata) + + if 'rebuild' in info and 'adminPass' in info['rebuild']: + password = info['rebuild']['adminPass'] + else: + password = utils.generate_password(FLAGS.password_length) + + try: + self.compute_api.rebuild(context, instance, image_href, + password, name=name, metadata=metadata, + files_to_inject=injected_files) + except exception.RebuildRequiresActiveInstance: + msg = _("Instance %s must be active to rebuild.") % instance_id + raise exc.HTTPConflict(explanation=msg) + except exception.InstanceNotFound: + msg = _("Instance %s could not be found") % instance_id + raise exc.HTTPNotFound(explanation=msg) + + instance = self._get_server(context, instance_id) + view = self._view_builder.show(request, instance) + view['server']['adminPass'] = password + + return view + + @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.InstanceBusy: + msg = _("Server is currently creating an image. Please wait.") + raise webob.exc.HTTPConflict(explanation=msg) + + # 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_default_xmlns(self, req): + return common.XML_NS_V11 + + def _get_server_admin_password(self, server): + """ Determine the admin password for a server on creation """ + return self._get_server_admin_password_new_style(server) + + 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') + + +class HeadersSerializer(wsgi.ResponseHeadersSerializer): + + def create(self, response, data): + response.status_int = 202 + + def delete(self, response, data): + response.status_int = 204 + + def action(self, response, data): + response.status_int = 202 + + +class SecurityGroupsTemplateElement(xmlutil.TemplateElement): + def will_render(self, datum): + return 'security_groups' in datum + + +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 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) + + +class ServerXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return MinimalServersTemplate() + + def detail(self): + return ServersTemplate() + + def show(self): + return ServerTemplate() + + def update(self): + return ServerTemplate() + + def create(self): + master = ServerTemplate() + master.attach(ServerAdminPassTemplate()) + return master + + def action(self): + return self.create() + + +class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer): + """ + 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 action(self, string): + dom = minidom.parseString(string) + action_node = dom.childNodes[0] + action_name = action_node.tagName + + action_deserializer = { + 'createImage': self._action_create_image, + 'createBackup': self._action_create_backup, + '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, 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_create_backup(self, node): + attributes = ('name', 'backup_type', 'rotation') + return self._deserialize_image_action(node, attributes) + + 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 + + def create(self, string): + """Deserialize an xml-formatted server create request""" + dom = minidom.parseString(string) + server = self._extract_server(dom) + return {'body': {'server': server}} + + 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_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_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 + + +def create_resource(): + headers_serializer = HeadersSerializer() + body_serializers = {'application/xml': ServerXMLSerializer()} + serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) + body_deserializers = {'application/xml': ServerXMLDeserializer()} + deserializer = wsgi.RequestDeserializer(body_deserializers) + return wsgi.Resource(Controller(), deserializer, serializer) + + +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 new file mode 100644 index 000000000..bae69198e --- /dev/null +++ b/nova/api/openstack/v2/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.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/users.py b/nova/api/openstack/v2/users.py new file mode 100644 index 000000000..9dba79e03 --- /dev/null +++ b/nova/api/openstack/v2/users.py @@ -0,0 +1,139 @@ +# 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.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 _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() + + 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) + + def detail(self, req): + """Return all users in detail""" + return self.index(req) + + 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 {} + + 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)) + + 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))) + + +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) + + +class UserXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return UsersTemplate() + + def default(self): + return UserTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': UserXMLSerializer(), + } + + serializer = wsgi.ResponseSerializer(body_serializers) + + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/v2/versions.py b/nova/api/openstack/v2/versions.py new file mode 100644 index 000000000..3c3d4068c --- /dev/null +++ b/nova/api/openstack/v2/versions.py @@ -0,0 +1,261 @@ +# 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 +import webob +import webob.dec + +from nova.api.openstack.v2.views import versions as views_versions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil + + +VERSIONS = { + "v1.1": { + "id": "v1.1", + "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=1.1", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=1.1", + } + ], + }, +} + + +class Versions(wsgi.Resource): + def __init__(self): + metadata = { + "attributes": { + "version": ["status", "id"], + "link": ["rel", "href"], + } + } + + headers_serializer = VersionsHeadersSerializer() + + body_serializers = { + 'application/atom+xml': VersionsAtomSerializer(metadata=metadata), + 'application/xml': VersionsXMLSerializer(metadata=metadata), + } + serializer = wsgi.ResponseSerializer( + body_serializers=body_serializers, + headers_serializer=headers_serializer) + + deserializer = VersionsRequestDeserializer() + + wsgi.Resource.__init__(self, None, serializer=serializer, + deserializer=deserializer) + + 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 VersionV11(object): + def show(self, req): + builder = views_versions.get_view_builder(req) + return builder.build_version(VERSIONS['v1.1']) + + +class VersionsRequestDeserializer(wsgi.RequestDeserializer): + 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 VersionsXMLSerializer(wsgi.XMLDictSerializer): + + def _populate_version(self, version_node, version): + version_node.set('id', version['id']) + version_node.set('status', version['status']) + if 'updated' in version: + version_node.set('updated', version['updated']) + if 'media-types' in version: + media_types = etree.SubElement(version_node, 'media-types') + for mtype in version['media-types']: + elem = etree.SubElement(media_types, 'media-type') + elem.set('base', mtype['base']) + elem.set('type', mtype['type']) + for link in version.get('links', []): + elem = etree.SubElement(version_node, + '{%s}link' % xmlutil.XMLNS_ATOM) + elem.set('rel', link['rel']) + elem.set('href', link['href']) + if 'type' in link: + elem.set('type', link['type']) + + NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + def index(self, data): + root = etree.Element('versions', nsmap=self.NSMAP) + for version in data['versions']: + version_elem = etree.SubElement(root, 'version') + self._populate_version(version_elem, version) + return self._to_xml(root) + + def show(self, data): + root = etree.Element('version', nsmap=self.NSMAP) + self._populate_version(root, data['version']) + return self._to_xml(root) + + def multi(self, data): + root = etree.Element('choices', nsmap=self.NSMAP) + for version in data['choices']: + version_elem = etree.SubElement(root, 'version') + self._populate_version(version_elem, version) + return self._to_xml(root) + + +class VersionsAtomSerializer(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 + + def index(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) + + def show(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 VersionsHeadersSerializer(wsgi.ResponseHeadersSerializer): + def multi(self, response, data): + response.status_int = 300 + + +def create_resource(): + body_serializers = { + 'application/xml': VersionsXMLSerializer(), + 'application/atom+xml': VersionsAtomSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + + deserializer = wsgi.RequestDeserializer() + + return wsgi.Resource(VersionV11(), serializer=serializer, + deserializer=deserializer) diff --git a/nova/api/openstack/v2/views/__init__.py b/nova/api/openstack/v2/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/api/openstack/v2/views/addresses.py b/nova/api/openstack/v2/views/addresses.py new file mode 100644 index 000000000..6f518b11a --- /dev/null +++ b/nova/api/openstack/v2/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.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 new file mode 100644 index 000000000..7b170a6b2 --- /dev/null +++ b/nova/api/openstack/v2/views/flavors.py @@ -0,0 +1,65 @@ +# 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 + + +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_quota": flavor.get("rxtx_quota") or "", + "rxtx_cap": flavor.get("rxtx_cap") 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 new file mode 100644 index 000000000..c4cfe8031 --- /dev/null +++ b/nova/api/openstack/v2/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/v2/views/limits.py b/nova/api/openstack/v2/views/limits.py new file mode 100644 index 000000000..138a40cb8 --- /dev/null +++ b/nova/api/openstack/v2/views/limits.py @@ -0,0 +1,97 @@ +# 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.api.openstack import common +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 new file mode 100644 index 000000000..979be930f --- /dev/null +++ b/nova/api/openstack/v2/views/servers.py @@ -0,0 +1,175 @@ +# 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", + ) + + 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"]), + }, + } + + 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.""" + list_func = self.basic + return self._list_view(list_func, request, instances) + + def detail(self, request, instances): + """Detailed view of a list of instance.""" + list_func = self.show + return self._list_view(list_func, 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, + }], + } diff --git a/nova/api/openstack/v2/views/versions.py b/nova/api/openstack/v2/views/versions.py new file mode 100644 index 000000000..1ac398706 --- /dev/null +++ b/nova/api/openstack/v2/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(version['id'], 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(version_data["id"]) + + links = [ + { + "rel": "self", + "href": href, + }, + ] + + return links + + def generate_href(self, version_number, path=None): + """Create an url that refers to a specific version_number.""" + version_number = version_number.strip('/') + 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/v2/zones.py b/nova/api/openstack/v2/zones.py new file mode 100644 index 000000000..8c5add2d1 --- /dev/null +++ b/nova/api/openstack/v2/zones.py @@ -0,0 +1,217 @@ +# 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 urlparse + +from nova.api.openstack import common +from nova.api.openstack.v2 import servers +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 db +from nova import exception +from nova import flags +from nova import log as logging +from nova.scheduler import api + + +FLAGS = flags.FLAGS + + +LOG = logging.getLogger('nova.api.openstack.v2.zones') + + +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() + + 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 = 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) + + def detail(self, req): + """Return all zones in detail""" + return self.index(req) + + def info(self, req): + """Return name and capabilities for this zone.""" + items = api.get_zone_capabilities(req.environ['nova.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) + + def show(self, req, id): + """Return data about the given zone id""" + zone_id = int(id) + zone = api.zone_get(req.environ['nova.context'], zone_id) + return dict(zone=_scrub_zone(zone)) + + def delete(self, req, id): + """Delete a child zone entry.""" + zone_id = int(id) + api.zone_delete(req.environ['nova.context'], zone_id) + return {} + + def create(self, req, body): + """Create a child zone entry.""" + context = req.environ['nova.context'] + zone = api.zone_create(context, body["zone"]) + return dict(zone=_scrub_zone(zone)) + + def update(self, req, id, body): + """Update a child zone entry.""" + context = req.environ['nova.context'] + zone_id = int(id) + zone = api.zone_update(context, zone_id, body["zone"]) + return dict(zone=_scrub_zone(zone)) + + @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 = 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 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 = xmlutil.SubTemplateElement(parent, 'zone', selector=selector) + 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) + + +class ZonesXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return ZonesTemplate() + + def detail(self): + return ZonesTemplate() + + def select(self): + return WeightsTemplate() + + def default(self): + return ZoneTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': ZonesXMLSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + + body_deserializers = { + 'application/xml': servers.ServerXMLDeserializer(), + } + deserializer = wsgi.RequestDeserializer(body_deserializers) + + return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py deleted file mode 100644 index 5461ffce2..000000000 --- a/nova/api/openstack/versions.py +++ /dev/null @@ -1,260 +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 -import webob -import webob.dec - -import nova.api.openstack.views.versions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil - - -VERSIONS = { - "v1.1": { - "id": "v1.1", - "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=1.1", - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute+json;version=1.1", - } - ], - }, -} - - -class Versions(wsgi.Resource): - def __init__(self): - metadata = { - "attributes": { - "version": ["status", "id"], - "link": ["rel", "href"], - } - } - - headers_serializer = VersionsHeadersSerializer() - - body_serializers = { - 'application/atom+xml': VersionsAtomSerializer(metadata=metadata), - 'application/xml': VersionsXMLSerializer(metadata=metadata), - } - serializer = wsgi.ResponseSerializer( - body_serializers=body_serializers, - headers_serializer=headers_serializer) - - deserializer = VersionsRequestDeserializer() - - wsgi.Resource.__init__(self, None, serializer=serializer, - deserializer=deserializer) - - def dispatch(self, request, *args): - """Respond to a request for all OpenStack API versions.""" - builder = nova.api.openstack.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 VersionV11(object): - def show(self, req): - builder = nova.api.openstack.views.versions.get_view_builder(req) - return builder.build_version(VERSIONS['v1.1']) - - -class VersionsRequestDeserializer(wsgi.RequestDeserializer): - 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 VersionsXMLSerializer(wsgi.XMLDictSerializer): - - def _populate_version(self, version_node, version): - version_node.set('id', version['id']) - version_node.set('status', version['status']) - if 'updated' in version: - version_node.set('updated', version['updated']) - if 'media-types' in version: - media_types = etree.SubElement(version_node, 'media-types') - for mtype in version['media-types']: - elem = etree.SubElement(media_types, 'media-type') - elem.set('base', mtype['base']) - elem.set('type', mtype['type']) - for link in version.get('links', []): - elem = etree.SubElement(version_node, - '{%s}link' % xmlutil.XMLNS_ATOM) - elem.set('rel', link['rel']) - elem.set('href', link['href']) - if 'type' in link: - elem.set('type', link['type']) - - NSMAP = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - def index(self, data): - root = etree.Element('versions', nsmap=self.NSMAP) - for version in data['versions']: - version_elem = etree.SubElement(root, 'version') - self._populate_version(version_elem, version) - return self._to_xml(root) - - def show(self, data): - root = etree.Element('version', nsmap=self.NSMAP) - self._populate_version(root, data['version']) - return self._to_xml(root) - - def multi(self, data): - root = etree.Element('choices', nsmap=self.NSMAP) - for version in data['choices']: - version_elem = etree.SubElement(root, 'version') - self._populate_version(version_elem, version) - return self._to_xml(root) - - -class VersionsAtomSerializer(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 - - def index(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) - - def show(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 VersionsHeadersSerializer(wsgi.ResponseHeadersSerializer): - def multi(self, response, data): - response.status_int = 300 - - -def create_resource(): - body_serializers = { - 'application/xml': VersionsXMLSerializer(), - 'application/atom+xml': VersionsAtomSerializer(), - } - serializer = wsgi.ResponseSerializer(body_serializers) - - deserializer = wsgi.RequestDeserializer() - - return wsgi.Resource(VersionV11(), serializer=serializer, - deserializer=deserializer) diff --git a/nova/api/openstack/views/__init__.py b/nova/api/openstack/views/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py deleted file mode 100644 index 1dbcaffe0..000000000 --- a/nova/api/openstack/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.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/views/flavors.py b/nova/api/openstack/views/flavors.py deleted file mode 100644 index f84ae5987..000000000 --- a/nova/api/openstack/views/flavors.py +++ /dev/null @@ -1,66 +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 - - -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_quota": flavor.get("rxtx_quota") or "", - "rxtx_cap": flavor.get("rxtx_cap") 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/views/images.py b/nova/api/openstack/views/images.py deleted file mode 100644 index c4cfe8031..000000000 --- a/nova/api/openstack/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/views/limits.py b/nova/api/openstack/views/limits.py deleted file mode 100644 index 138a40cb8..000000000 --- a/nova/api/openstack/views/limits.py +++ /dev/null @@ -1,97 +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.api.openstack import common -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/views/servers.py b/nova/api/openstack/views/servers.py deleted file mode 100644 index e6f4bc2c4..000000000 --- a/nova/api/openstack/views/servers.py +++ /dev/null @@ -1,178 +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 -import os - -from nova.api.openstack import common -from nova.api.openstack.views import addresses as views_addresses -from nova.api.openstack.views import flavors as views_flavors -from nova.api.openstack.views import images as views_images -from nova.compute import vm_states -from nova import exception -from nova import log as logging -from nova import utils - - -LOG = logging.getLogger('nova.api.openstack.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", - ) - - 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"]), - }, - } - - 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.""" - list_func = self.basic - return self._list_view(list_func, request, instances) - - def detail(self, request, instances): - """Detailed view of a list of instance.""" - list_func = self.show - return self._list_view(list_func, 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, - }], - } diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py deleted file mode 100644 index 1ac398706..000000000 --- a/nova/api/openstack/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(version['id'], 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(version_data["id"]) - - links = [ - { - "rel": "self", - "href": href, - }, - ] - - return links - - def generate_href(self, version_number, path=None): - """Create an url that refers to a specific version_number.""" - version_number = version_number.strip('/') - 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/wsgi.py b/nova/api/openstack/wsgi.py index 8c6b30ca9..7e4875066 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -15,13 +15,11 @@ # License for the specific language governing permissions and limitations # under the License. -import json from lxml import etree import webob from xml.dom import minidom from xml.parsers import expat -import faults from nova import exception from nova import log as logging from nova import utils @@ -528,7 +526,7 @@ class Resource(wsgi.Application): serialized by requested content type. Exceptions derived from webob.exc.HTTPException will be automatically - wrapped in faults.Fault() to provide API friendly error responses. + wrapped in Fault() to provide API friendly error responses. """ @@ -556,10 +554,10 @@ class Resource(wsgi.Application): action, args, accept = self.deserializer.deserialize(request) except exception.InvalidContentType: msg = _("Unsupported Content-Type") - return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg)) + return Fault(webob.exc.HTTPBadRequest(explanation=msg)) except exception.MalformedRequestBody: msg = _("Malformed request body") - return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg)) + return Fault(webob.exc.HTTPBadRequest(explanation=msg)) project_id = args.pop("project_id", None) if 'nova.context' in request.environ and project_id: @@ -567,12 +565,12 @@ class Resource(wsgi.Application): try: action_result = self.dispatch(request, action, args) - except faults.Fault as ex: + except Fault as ex: LOG.info(_("Fault thrown: %s"), unicode(ex)) action_result = ex except webob.exc.HTTPException as ex: LOG.info(_("HTTP exception thrown: %s"), unicode(ex)) - action_result = faults.Fault(ex) + action_result = Fault(ex) if type(action_result) is dict or action_result is None: response = self.serializer.serialize(request, @@ -601,7 +599,7 @@ class Resource(wsgi.Application): return controller_method(req=request, **action_args) except TypeError as exc: LOG.exception(exc) - return faults.Fault(webob.exc.HTTPBadRequest()) + return Fault(webob.exc.HTTPBadRequest()) class Controller(object): @@ -612,3 +610,96 @@ class Controller(object): def __init__(self, view_builder=None): """Initialize controller with a view builder instance.""" self._view_builder = view_builder or self._view_builder_class() + + +class Fault(webob.exc.HTTPException): + """Wrap webob.exc.HTTPException to provide API friendly response.""" + + _fault_names = { + 400: "badRequest", + 401: "unauthorized", + 403: "resizeNotAllowed", + 404: "itemNotFound", + 405: "badMethod", + 409: "inProgress", + 413: "overLimit", + 415: "badMediaType", + 501: "notImplemented", + 503: "serviceUnavailable"} + + def __init__(self, exception): + """Create a Fault for the given webob.exc.exception.""" + self.wrapped_exc = exception + self.status_int = exception.status_int + + @webob.dec.wsgify(RequestClass=Request) + def __call__(self, req): + """Generate a WSGI response based on the exception passed to ctor.""" + # Replace the body with fault details. + code = self.wrapped_exc.status_int + fault_name = self._fault_names.get(code, "cloudServersFault") + fault_data = { + fault_name: { + 'code': code, + 'message': self.wrapped_exc.explanation}} + if code == 413: + retry = self.wrapped_exc.headers['Retry-After'] + fault_data[fault_name]['retryAfter'] = retry + + # 'code' is an attribute on the fault tag itself + metadata = {'attributes': {fault_name: 'code'}} + + xml_serializer = XMLDictSerializer(metadata, XMLNS_V11) + + content_type = req.best_match_content_type() + serializer = { + 'application/xml': xml_serializer, + 'application/json': JSONDictSerializer(), + }[content_type] + + self.wrapped_exc.body = serializer.serialize(fault_data) + self.wrapped_exc.content_type = content_type + + return self.wrapped_exc + + def __str__(self): + return self.wrapped_exc.__str__() + + +class OverLimitFault(webob.exc.HTTPException): + """ + Rate-limited request response. + """ + + def __init__(self, message, details, retry_time): + """ + Initialize new `OverLimitFault` with relevant information. + """ + self.wrapped_exc = webob.exc.HTTPRequestEntityTooLarge() + self.content = { + "overLimitFault": { + "code": self.wrapped_exc.status_int, + "message": message, + "details": details, + }, + } + + @webob.dec.wsgify(RequestClass=Request) + def __call__(self, request): + """ + Return the wrapped exception with a serialized body conforming to our + error format. + """ + content_type = request.best_match_content_type() + metadata = {"attributes": {"overLimitFault": "code"}} + + xml_serializer = XMLDictSerializer(metadata, XMLNS_V11) + serializer = { + 'application/xml': xml_serializer, + 'application/json': JSONDictSerializer(), + }[content_type] + + content = serializer.serialize(self.content) + self.wrapped_exc.body = content + + return self.wrapped_exc diff --git a/nova/api/openstack/xmlutil.py b/nova/api/openstack/xmlutil.py index 5779849b3..db490e652 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 type(xml) is str: xml = etree.fromstring(xml) - base_path = 'nova/api/openstack/schemas/v1.1/' + base_path = 'nova/api/openstack/v2/schemas/v1.1/' if schema_name in ('atom', 'atom-link'): - base_path = 'nova/api/openstack/schemas/' + base_path = 'nova/api/openstack/v2/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/api/openstack/zones.py b/nova/api/openstack/zones.py deleted file mode 100644 index 2a95cd0a4..000000000 --- a/nova/api/openstack/zones.py +++ /dev/null @@ -1,219 +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 urlparse - -from nova import crypto -from nova import db -from nova import exception -from nova import flags -from nova import log as logging - -from nova.compute import api as compute -from nova.scheduler import api - -from nova.api.openstack import common -from nova.api.openstack import servers -from nova.api.openstack import xmlutil -from nova.api.openstack import wsgi - - -FLAGS = flags.FLAGS - - -LOG = logging.getLogger('nova.api.openstack.zones') - - -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() - - 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 = 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) - - def detail(self, req): - """Return all zones in detail""" - return self.index(req) - - def info(self, req): - """Return name and capabilities for this zone.""" - items = api.get_zone_capabilities(req.environ['nova.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) - - def show(self, req, id): - """Return data about the given zone id""" - zone_id = int(id) - zone = api.zone_get(req.environ['nova.context'], zone_id) - return dict(zone=_scrub_zone(zone)) - - def delete(self, req, id): - """Delete a child zone entry.""" - zone_id = int(id) - api.zone_delete(req.environ['nova.context'], zone_id) - return {} - - def create(self, req, body): - """Create a child zone entry.""" - context = req.environ['nova.context'] - zone = api.zone_create(context, body["zone"]) - return dict(zone=_scrub_zone(zone)) - - def update(self, req, id, body): - """Update a child zone entry.""" - context = req.environ['nova.context'] - zone_id = int(id) - zone = api.zone_update(context, zone_id, body["zone"]) - return dict(zone=_scrub_zone(zone)) - - @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 = 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 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 = xmlutil.SubTemplateElement(parent, 'zone', selector=selector) - 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) - - -class ZonesXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return ZonesTemplate() - - def detail(self): - return ZonesTemplate() - - def select(self): - return WeightsTemplate() - - def default(self): - return ZoneTemplate() - - -def create_resource(): - body_serializers = { - 'application/xml': ZonesXMLSerializer(), - } - serializer = wsgi.ResponseSerializer(body_serializers) - - body_deserializers = { - 'application/xml': servers.ServerXMLDeserializer(), - } - deserializer = wsgi.RequestDeserializer(body_deserializers) - - return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/nova/flags.py b/nova/flags.py index 32cec119f..c47c77ab1 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -369,7 +369,7 @@ 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.contrib.standard_extensions'], + ['nova.api.openstack.v2.contrib.standard_extensions'], 'osapi extension to load') DEFINE_string('osapi_host', '$my_ip', 'ip of api server') DEFINE_string('osapi_scheme', 'http', 'prefix for openstack') diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py index 00fcfbb00..e69de29bb 100644 --- a/nova/tests/api/openstack/__init__.py +++ b/nova/tests/api/openstack/__init__.py @@ -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/contrib/__init__.py b/nova/tests/api/openstack/contrib/__init__.py deleted file mode 100644 index 848908a95..000000000 --- a/nova/tests/api/openstack/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/contrib/test_admin_actions.py b/nova/tests/api/openstack/contrib/test_admin_actions.py deleted file mode 100644 index 4b62c0ba7..000000000 --- a/nova/tests/api/openstack/contrib/test_admin_actions.py +++ /dev/null @@ -1,86 +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 import compute -from nova import flags -from nova import test -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(cls, req, id): - return True - - -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') - - def setUp(self): - super(AdminActionsTest, self).setUp() - self.flags(allow_admin_api=True) - self.stubs.Set(compute.API, 'get', fake_compute_api_get) - for _method in self._methods: - self.stubs.Set(compute.API, _method, fake_compute_api) - - def test_admin_api_enabled(self): - app = fakes.wsgi_app() - for _action in self._actions: - req = webob.Request.blank('/v1.1/fake/servers/abcd/action') - 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_disabled(self): - FLAGS.allow_admin_api = False - app = fakes.wsgi_app() - for _action in self._actions: - req = webob.Request.blank('/v1.1/fake/servers/abcd/action') - req.method = 'POST' - req.body = json.dumps({_action: None}) - req.content_type = 'application/json' - res = req.get_response(app) - self.assertEqual(res.status_int, 404) diff --git a/nova/tests/api/openstack/contrib/test_createserverext.py b/nova/tests/api/openstack/contrib/test_createserverext.py deleted file mode 100644 index f7ee96917..000000000 --- a/nova/tests/api/openstack/contrib/test_createserverext.py +++ /dev/null @@ -1,431 +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 - -from nova import db -from nova import exception -from nova import flags -from nova import rpc -from nova import test -import nova.api.openstack -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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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/contrib/test_disk_config.py b/nova/tests/api/openstack/contrib/test_disk_config.py deleted file mode 100644 index 40875cb84..000000000 --- a/nova/tests/api/openstack/contrib/test_disk_config.py +++ /dev/null @@ -1,248 +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 nova.db.api -import nova.rpc - -from nova import flags -from nova import test -from nova import utils -from nova.api import openstack -from nova.api.openstack import extensions -from nova.api.openstack import servers -from nova.api.openstack import wsgi -from nova.tests.api.openstack import fakes - -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.api, 'instance_get', fake_instance_get) - 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.api, '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_, session=None): - return inst - - self.stubs.Set(nova.db, 'instance_get', - fake_instance_get_for_create) - self.stubs.Set(nova.db.api, 'instance_get', - fake_instance_get_for_create) - self.stubs.Set(nova.db.sqlalchemy.api, 'instance_get', - fake_instance_get_for_create) - - def fake_instance_add_security_group(context, instance_id, - security_group_id): - pass - - self.stubs.Set(nova.db.sqlalchemy.api, - 'instance_add_security_group', - fake_instance_add_security_group) - - return inst - - self.stubs.Set(nova.db, 'instance_create', fake_instance_create) - - app = openstack.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/contrib/test_extendedstatus.py b/nova/tests/api/openstack/contrib/test_extendedstatus.py deleted file mode 100644 index 0065f6fde..000000000 --- a/nova/tests/api/openstack/contrib/test_extendedstatus.py +++ /dev/null @@ -1,87 +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 = '/v1.1/openstack/servers/%s' % self.uuid - fakes.stub_out_nw_api(self.stubs) - 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_with_admin(self): - self.flags(allow_admin_api=True) - 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_admin(self): - self.flags(allow_admin_api=False) - res = self._make_request() - body = json.loads(res.body) - - self.assertEqual(res.status_int, 200) - self.assertServerStates(body['server'], - vm_state=None, - power_state=None, - task_state=None) - - def test_extended_status_no_instance_fails(self): - self.flags(allow_admin_api=True) - - 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/contrib/test_flavors_extra_specs.py b/nova/tests/api/openstack/contrib/test_flavors_extra_specs.py deleted file mode 100644 index 5784743ee..000000000 --- a/nova/tests/api/openstack/contrib/test_flavors_extra_specs.py +++ /dev/null @@ -1,171 +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 json -import stubout -import webob -import os.path - - -from nova import test -from nova.api import openstack -from nova.api.openstack import extensions -from nova.api.openstack.contrib import flavorextraspecs -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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/flavors/1/os-extra_specs/bad') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'bad', body) diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py deleted file mode 100644 index 63831f31f..000000000 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ /dev/null @@ -1,270 +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 stubout -import webob - -from nova import compute -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.tests.api.openstack import test_servers - - -from nova.api.openstack.contrib import floating_ips -from nova.api.openstack.contrib.floating_ips import _translate_floating_ip_view - - -def network_api_get_floating_ip(self, context, id): - return {'id': 1, 'address': '10.10.10.10', - 'fixed_ip': None} - - -def network_api_get_floating_ip_by_address(self, context, address): - return {'id': 1, 'address': '10.10.10.10', - 'fixed_ip': {'address': '10.0.0.1', 'instance_id': 1}} - - -def network_api_get_floating_ips_by_project(self, context): - return [{'id': 1, - 'address': '10.10.10.10', - 'fixed_ip': {'address': '10.0.0.1', 'instance_id': 1}}, - {'id': 2, - '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, - "name": 'fake', - "user_id": 'fakeuser', - "project_id": '123'} - - -class StubExtensionManager(object): - def register(self, *args): - pass - - -class FloatingIpTest(test.TestCase): - address = "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.address, - 'host': host}) - - def _delete_floating_ip(self): - db.floating_ip_destroy(self.context, self.address) - - 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 = _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.address) - 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', 'fixed_ip': None} - view = _translate_floating_ip_view(floating_ip) - self.assertTrue('floating_ip' in view) - - def test_floating_ips_list(self): - req = fakes.HTTPRequest.blank('/v1.1/123/os-floating-ips') - res_dict = self.controller.index(req) - - response = {'floating_ips': [{'instance_id': 1, - 'ip': '10.10.10.10', - 'fixed_ip': '10.0.0.1', - 'id': 1}, - {'instance_id': None, - 'ip': '10.10.10.11', - 'fixed_ip': None, - 'id': 2}]} - self.assertEqual(res_dict, response) - - def test_floating_ip_show(self): - req = fakes.HTTPRequest.blank('/v1.1/123/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', - 'fixed_ip': {'address': '10.0.0.1', 'instance_id': 1}} - self.stubs.Set(network.api.API, "get_floating_ip", get_floating_ip) - - req = fakes.HTTPRequest.blank('/v1.1/123/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'], 1) - -# 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('/v1.1/123/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'} - - 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('/v1.1/123/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} - self.assertEqual(ip, expected) - - def test_floating_ip_release(self): - req = fakes.HTTPRequest.blank('/v1.1/123/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.address)) - - req = fakes.HTTPRequest.blank('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/servers/test_inst/action') - self.assertRaises(webob.exc.HTTPBadRequest, - self.manager._add_floating_ip, body, req, - 'test_inst') diff --git a/nova/tests/api/openstack/contrib/test_keypairs.py b/nova/tests/api/openstack/contrib/test_keypairs.py deleted file mode 100644 index 92e401aac..000000000 --- a/nova/tests/api/openstack/contrib/test_keypairs.py +++ /dev/null @@ -1,112 +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 context -from nova import db -from nova import test -from nova.api.openstack.contrib.keypairs import KeypairController -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 = 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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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) diff --git a/nova/tests/api/openstack/contrib/test_multinic_xs.py b/nova/tests/api/openstack/contrib/test_multinic_xs.py deleted file mode 100644 index 90999a384..000000000 --- a/nova/tests/api/openstack/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 stubout -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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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/contrib/test_quotas.py b/nova/tests/api/openstack/contrib/test_quotas.py deleted file mode 100644 index 6374dfd93..000000000 --- a/nova/tests/api/openstack/contrib/test_quotas.py +++ /dev/null @@ -1,134 +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 import context -from nova import test -from nova.tests.api.openstack import fakes - -from nova.api.openstack.contrib.quotas import QuotaSetsController - - -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 = 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 = 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 = '/v1.1/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('/v1.1/1234/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('/v1.1/1234/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('/v1.1/1234/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('/v1.1/1234/os-quota-sets/update_me') - self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, - req, 'update_me', body) diff --git a/nova/tests/api/openstack/contrib/test_rescue.py b/nova/tests/api/openstack/contrib/test_rescue.py deleted file mode 100644 index f5b69865c..000000000 --- a/nova/tests/api/openstack/contrib/test_rescue.py +++ /dev/null @@ -1,78 +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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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/contrib/test_security_groups.py b/nova/tests/api/openstack/contrib/test_security_groups.py deleted file mode 100644 index 1e7a439b0..000000000 --- a/nova/tests/api/openstack/contrib/test_security_groups.py +++ /dev/null @@ -1,848 +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 json -import mox -import nova -import unittest -import webob -from xml.dom import minidom - -from nova import exception -from nova import test -from nova.api.openstack.contrib import security_groups -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, - 'host': "localhost", 'name': 'asdf'} - - -def return_security_group_by_name(context, project_id, group_name): - return {'id': 1, 'name': group_name, "instances": [{'id': 1}]} - - -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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/os-security-groups') - self.controller.create(req, {'security_group': sg}) - - req = fakes.HTTPRequest.blank('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/servers/1/action') - self.manager._removeSecurityGroup(body, req, '1') - - -class TestSecurityGroupRules(test.TestCase): - def setUp(self): - super(TestSecurityGroupRules, self).setUp() - - controller = security_groups.SecurityGroupController() - - 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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/os-security-group-rules/10') - self.controller.delete(req, '10') - - def test_delete_invalid_rule_id(self): - req = fakes.HTTPRequest.blank('/v1.1/123/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('/v1.1/123/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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - expected = { - "security_group": { - "description": "test", - }, - } - self.assertEquals(request['body'], expected) diff --git a/nova/tests/api/openstack/contrib/test_simple_tenant_usage.py b/nova/tests/api/openstack/contrib/test_simple_tenant_usage.py deleted file mode 100644 index 2430b9d51..000000000 --- a/nova/tests/api/openstack/contrib/test_simple_tenant_usage.py +++ /dev/null @@ -1,172 +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 -import webob - -from nova import context -from nova import flags -from nova import test -from nova.compute import api -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( - '/v1.1/123/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( - '/v1.1/123/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( - '/v1.1/123/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()) - self.assertEqual(res.status_int, 403) - - def test_verify_show(self): - req = webob.Request.blank( - '/v1.1/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( - '/v1.1/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) diff --git a/nova/tests/api/openstack/contrib/test_virtual_interfaces.py b/nova/tests/api/openstack/contrib/test_virtual_interfaces.py deleted file mode 100644 index 0e3b45c10..000000000 --- a/nova/tests/api/openstack/contrib/test_virtual_interfaces.py +++ /dev/null @@ -1,55 +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 -import webob - -from nova import test -from nova import network -from nova.tests.api.openstack import fakes -from nova.api.openstack.contrib.virtual_interfaces import \ - ServerVirtualInterfaceController - - -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 = 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 = '/v1.1/123/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) diff --git a/nova/tests/api/openstack/contrib/test_volume_types.py b/nova/tests/api/openstack/contrib/test_volume_types.py deleted file mode 100644 index ec1c44854..000000000 --- a/nova/tests/api/openstack/contrib/test_volume_types.py +++ /dev/null @@ -1,165 +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 stubout -import webob - -from nova import exception -from nova import context -from nova import test -from nova import log as logging -from nova.api.openstack.contrib import volumetypes -from nova.volume import volume_types -from nova.tests.api.openstack import fakes - -LOG = logging.getLogger('nova.tests.api.openstack.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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/os-volume-types') - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.create, req, '') diff --git a/nova/tests/api/openstack/contrib/test_volume_types_extra_specs.py b/nova/tests/api/openstack/contrib/test_volume_types_extra_specs.py deleted file mode 100644 index 796478838..000000000 --- a/nova/tests/api/openstack/contrib/test_volume_types_extra_specs.py +++ /dev/null @@ -1,169 +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. - -import json -import stubout -import webob -import os.path - - -from nova import test -from nova.api import openstack -from nova.api.openstack import extensions -from nova.api.openstack.contrib import volumetypes -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 = '/v1.1/123/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) diff --git a/nova/tests/api/openstack/contrib/test_volumes.py b/nova/tests/api/openstack/contrib/test_volumes.py deleted file mode 100644 index a130d1140..000000000 --- a/nova/tests/api/openstack/contrib/test_volumes.py +++ /dev/null @@ -1,88 +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 -import webob - -import nova -from nova import flags -from nova import test -from nova.compute import instance_types -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('/v1.1/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, 200) - 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') diff --git a/nova/tests/api/openstack/contrib/test_vsa.py b/nova/tests/api/openstack/contrib/test_vsa.py deleted file mode 100644 index 6f6ea8e32..000000000 --- a/nova/tests/api/openstack/contrib/test_vsa.py +++ /dev/null @@ -1,450 +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 stubout -import unittest -import webob - -from nova import context -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import test -from nova import volume -from nova import vsa -from nova.api import openstack -from nova.tests.api.openstack import fakes -import nova.wsgi - -from nova.api.openstack.contrib.virtual_storage_arrays import _vsa_view - -FLAGS = flags.FLAGS - -LOG = logging.getLogger('nova.tests.api.openstack.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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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': [], - } - - -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 - 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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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() diff --git a/nova/tests/api/openstack/extensions/__init__.py b/nova/tests/api/openstack/extensions/__init__.py deleted file mode 100644 index 848908a95..000000000 --- a/nova/tests/api/openstack/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/extensions/foxinsocks.py b/nova/tests/api/openstack/extensions/foxinsocks.py deleted file mode 100644 index cbf101f47..000000000 --- a/nova/tests/api/openstack/extensions/foxinsocks.py +++ /dev/null @@ -1,94 +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.exc - -from nova.api.openstack import extensions - - -class FoxInSocksController(object): - - def index(self, req): - return "Try to say this Mr. Knox, sir..." - - -class Foxinsocks(object): - """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', - '/v1.1/:(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', - '/v1.1/:(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/fakes.py b/nova/tests/api/openstack/fakes.py index 0f58cf55b..c12ae5eab 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -23,25 +23,25 @@ import webob.request from glance import client as glance_client -from nova import context -from nova import exception as exc -from nova import utils -from nova import wsgi -import nova.api.openstack.auth -from nova.api import openstack +import nova.api.openstack.v2.auth from nova.api import auth as api_auth -from nova.api.openstack import auth -from nova.api.openstack import extensions -from nova.api.openstack import limits -from nova.api.openstack import urlmap -from nova.api.openstack import versions +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.openstack import wsgi as os_wsgi from nova.auth.manager import User, Project from nova.compute import instance_types from nova.compute import vm_states +from nova import context from nova.db.sqlalchemy import models +from nova import exception as exc import nova.image.fake from nova.tests.glance import stubs as glance_stubs +from nova import utils +from nova import wsgi class Context(object): @@ -72,35 +72,36 @@ def fake_wsgi(self, req): return self.application -def wsgi_app(inner_app11=None, fake_auth=True, fake_auth_context=None, +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_app11: - inner_app11 = openstack.APIRouter() + if not inner_app_v2: + inner_app_v2 = v2.APIRouter() if fake_auth: if fake_auth_context is not None: ctxt = fake_auth_context else: ctxt = context.RequestContext('fake', 'fake', auth_token=True) - api11 = openstack.FaultWrapper(api_auth.InjectContext(ctxt, + api_v2 = v2.FaultWrapper(api_auth.InjectContext(ctxt, limits.RateLimitingMiddleware( serialization( - extensions.ExtensionMiddleware(inner_app11))))) + extensions.ExtensionMiddleware(inner_app_v2))))) elif use_no_auth: - api11 = openstack.FaultWrapper(auth.NoAuthMiddleware( + api_v2 = v2.FaultWrapper(auth.NoAuthMiddleware( limits.RateLimitingMiddleware( serialization( - extensions.ExtensionMiddleware(inner_app11))))) + extensions.ExtensionMiddleware(inner_app_v2))))) else: - api11 = openstack.FaultWrapper(auth.AuthMiddleware( + api_v2 = v2.FaultWrapper(auth.AuthMiddleware( limits.RateLimitingMiddleware( serialization( - extensions.ExtensionMiddleware(inner_app11))))) + extensions.ExtensionMiddleware(inner_app_v2))))) Auth = auth mapper = urlmap.URLMap() - mapper['/v1.1'] = api11 - mapper['/'] = openstack.FaultWrapper(versions.Versions()) + mapper['/v2'] = api_v2 + mapper['/v1.1'] = api_v2 + mapper['/'] = v2.FaultWrapper(versions.Versions()) return mapper @@ -136,9 +137,9 @@ def stub_out_auth(stubs): def fake_auth_init(self, app): self.application = app - stubs.Set(nova.api.openstack.auth.AuthMiddleware, + stubs.Set(nova.api.openstack.v2.auth.AuthMiddleware, '__init__', fake_auth_init) - stubs.Set(nova.api.openstack.auth.AuthMiddleware, + stubs.Set(nova.api.openstack.v2.auth.AuthMiddleware, '__call__', fake_wsgi) @@ -147,10 +148,10 @@ def stub_out_rate_limiting(stubs): super(limits.RateLimitingMiddleware, self).__init__(app) self.application = app - stubs.Set(nova.api.openstack.limits.RateLimitingMiddleware, + stubs.Set(nova.api.openstack.v2.limits.RateLimitingMiddleware, '__init__', fake_rate_init) - stubs.Set(nova.api.openstack.limits.RateLimitingMiddleware, + stubs.Set(nova.api.openstack.v2.limits.RateLimitingMiddleware, '__call__', fake_wsgi) diff --git a/nova/tests/api/openstack/test_accounts.py b/nova/tests/api/openstack/test_accounts.py deleted file mode 100644 index ea96e1348..000000000 --- a/nova/tests/api/openstack/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 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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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/test_api.py b/nova/tests/api/openstack/test_api.py deleted file mode 100644 index b7a0b01ef..000000000 --- a/nova/tests/api/openstack/test_api.py +++ /dev/null @@ -1,127 +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 - -import webob.exc -import webob.dec - -from lxml import etree -from webob import Request - -from nova import test -from nova.api import openstack -from nova.api.openstack import faults -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.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(self): - - @webob.dec.wsgify - def succeed(req): - return 'Succeeded' - - @webob.dec.wsgify - def raise_webob_exc(req): - raise webob.exc.HTTPNotFound(explanation='Raised a webob.exc') - - @webob.dec.wsgify - def fail(req): - raise Exception("Threw an exception") - - @webob.dec.wsgify - def raise_api_fault(req): - exc = webob.exc.HTTPNotFound(explanation='Raised a webob.exc') - return faults.Fault(exc) - - #api.application = succeed - api = self._wsgi_app(succeed) - resp = Request.blank('/').get_response(api) - self.assertFalse('cloudServersFault' in resp.body, resp.body) - self.assertEqual(resp.status_int, 200, resp.body) - - #api.application = raise_webob_exc - api = self._wsgi_app(raise_webob_exc) - resp = Request.blank('/').get_response(api) - self.assertFalse('cloudServersFault' in resp.body, resp.body) - self.assertEqual(resp.status_int, 404, resp.body) - - #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) - - #api.application = fail - api = self._wsgi_app(fail) - resp = Request.blank('/').get_response(api) - self.assertTrue('{"cloudServersFault' in resp.body, resp.body) - self.assertEqual(resp.status_int, 500, resp.body) - - #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 = '/v1.1/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) - - expected = {'console': {'id': 20, - 'port': 'fake_port', - 'host': 'fake_hostname', - 'password': 'fake_password', - '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): - - serializer = consoles.ConsoleXMLSerializer() - - def test_show(self): - fixture = {'console': {'id': 20, - 'password': 'fake_password', - 'port': 'fake_port', - 'host': 'fake_hostname', - 'console_type': 'fake_type'}} - - output = self.serializer.serialize(fixture, 'show') - 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 = self.serializer.serialize(fixture, 'index') - 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/test_extensions.py b/nova/tests/api/openstack/test_extensions.py deleted file mode 100644 index b4fe3e730..000000000 --- a/nova/tests/api/openstack/test_extensions.py +++ /dev/null @@ -1,515 +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 os.path -import webob -from lxml import etree - -from nova import context -from nova import flags -from nova import test -from nova import wsgi as base_wsgi -from nova.api import openstack -from nova.api.openstack import extensions -from nova.api.openstack import flavors -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.tests.api.openstack import fakes - -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.extensions.' - 'foxinsocks.Foxinsocks') - self.flags(osapi_extension=ext_list) - - -class ExtensionControllerTest(ExtensionTestCase): - - def setUp(self): - super(ExtensionControllerTest, self).setUp() - self.ext_list = [ - "AdminActions", - "Createserverext", - "DeferredDelete", - "DiskConfig", - "ExtendedStatus", - "FlavorExtraSpecs", - "FlavorExtraData", - "Floating_ips", - "Fox In Socks", - "Hosts", - "Keypairs", - "Multinic", - "Quotas", - "Rescue", - "SecurityGroups", - "SimpleTenantUsage", - "VSAs", - "VirtualInterfaces", - "Volumes", - "VolumeTypes", - "Zones", - ] - self.ext_list.sort() - - def test_list_extensions_json(self): - app = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/123/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 = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/123/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 = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - request = webob.Request.blank("/123/extensions/4") - response = request.get_response(ext_midware) - self.assertEqual(404, response.status_int) - - def test_list_extensions_xml(self): - app = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/123/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 = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/123/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 = openstack.APIRouter() - 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 = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/123/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 = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/123/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 = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/123/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 = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app, manager) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/123/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 ExtensionManagerTest(ExtensionTestCase): - - response_body = "Try to say this Mr. Knox, sir..." - - def test_get_resources(self): - app = openstack.APIRouter() - ext_midware = extensions.ExtensionMiddleware(app) - ser_midware = wsgi.LazySerializationMiddleware(ext_midware) - request = webob.Request.blank("/123/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 = openstack.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) - - -class ActionExtensionTest(ExtensionTestCase): - - def _send_server_action_request(self, url, body): - app = openstack.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 = "/123/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 = "/123/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 = "/123/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 = "/123/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', - '/v1.1/123/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("/v1.1/123/flavors/1?chewing=bluegoo") - request.environ['api.version'] = '1.1' - 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("/v1.1/123/flavors/1?chewing=newblue") - request.environ['api.version'] = '1.1' - response = request.get_response(ser_midware) - self.assertEqual(200, response.status_int) - response_data = json.loads(response.body) - 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/test_faults.py b/nova/tests/api/openstack/test_faults.py index 18bd136df..87cb2d3fe 100644 --- a/nova/tests/api/openstack/test_faults.py +++ b/nova/tests/api/openstack/test_faults.py @@ -24,7 +24,6 @@ import webob.exc from nova import test from nova.api.openstack import common -from nova.api.openstack import faults from nova.api.openstack import wsgi @@ -46,7 +45,7 @@ class TestFaults(test.TestCase): ] for request in requests: - fault = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='scram')) response = request.get_response(fault) expected = { @@ -69,7 +68,7 @@ class TestFaults(test.TestCase): for request in requests: exc = webob.exc.HTTPRequestEntityTooLarge - fault = faults.Fault(exc(explanation='sorry', + fault = wsgi.Fault(exc(explanation='sorry', headers={'Retry-After': 4})) response = request.get_response(fault) @@ -89,7 +88,7 @@ class TestFaults(test.TestCase): """Ensure the ability to raise `Fault`s in WSGI-ified methods.""" @webob.dec.wsgify def raiser(req): - raise faults.Fault(webob.exc.HTTPNotFound(explanation='whut?')) + raise wsgi.Fault(webob.exc.HTTPNotFound(explanation='whut?')) req = webob.Request.blank('/.xml') resp = req.get_response(raiser) @@ -99,7 +98,7 @@ class TestFaults(test.TestCase): def test_fault_has_status_int(self): """Ensure the status_int is set correctly on faults""" - fault = faults.Fault(webob.exc.HTTPBadRequest(explanation='what?')) + fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='what?')) self.assertEqual(fault.status_int, 400) def test_xml_serializer(self): @@ -107,7 +106,7 @@ class TestFaults(test.TestCase): request = webob.Request.blank('/v1.1', headers={"Accept": "application/xml"}) - fault = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='scram')) response = request.get_response(fault) self.assertTrue(common.XML_NS_V11 in response.body) diff --git a/nova/tests/api/openstack/test_flavors.py b/nova/tests/api/openstack/test_flavors.py deleted file mode 100644 index 24d81ef67..000000000 --- a/nova/tests/api/openstack/test_flavors.py +++ /dev/null @@ -1,670 +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 - -from nova.api.openstack 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 -from nova import wsgi - - -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('/v1.1/fake/flavors/asdf') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.show, req, 'asdf') - - def test_get_flavor_by_id(self): - req = fakes.HTTPRequest.blank('/v1.1/fake/flavors/1') - flavor = self.controller.show(req, '1') - expected = { - "flavor": { - "id": "1", - "name": "flavor 1", - "ram": "256", - "disk": "10", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/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('/v1.1/fake/flavors') - flavor = self.controller.index(req) - expected = { - "flavors": [ - { - "id": "1", - "name": "flavor 1", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/1", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/1", - }, - ], - }, - { - "id": "2", - "name": "flavor 2", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/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('/v1.1/fake/flavors/detail') - flavor = self.controller.detail(req) - expected = { - "flavors": [ - { - "id": "1", - "name": "flavor 1", - "ram": "256", - "disk": "10", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/1", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/1", - }, - ], - }, - { - "id": "2", - "name": "flavor 2", - "ram": "512", - "disk": "20", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/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('/v1.1/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('/v1.1/fake/flavors?minRam=512') - flavor = self.controller.index(req) - expected = { - "flavors": [ - { - "id": "2", - "name": "flavor 2", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/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('/v1.1/fake/flavors?minDisk=20') - flavor = self.controller.index(req) - expected = { - "flavors": [ - { - "id": "2", - "name": "flavor 2", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/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('/v1.1/fake/flavors/detail' - '?minRam=256&minDisk=20') - flavor = self.controller.detail(req) - expected = { - "flavors": [ - { - "id": "2", - "name": "flavor 2", - "ram": "512", - "disk": "20", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/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('/v1.1/fake/flavors/detail?minRam=16GB') - flavor = self.controller.detail(req) - expected = { - "flavors": [ - { - "id": "1", - "name": "flavor 1", - "ram": "256", - "disk": "10", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/1", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/1", - }, - ], - }, - { - "id": "2", - "name": "flavor 2", - "ram": "512", - "disk": "20", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/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('/v1.1/fake/flavors/detail?minDisk=16GB') - flavor = self.controller.detail(req) - expected = { - "flavors": [ - { - "id": "1", - "name": "flavor 1", - "ram": "256", - "disk": "10", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/1", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/1", - }, - ], - }, - { - "id": "2", - "name": "flavor 2", - "ram": "512", - "disk": "20", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/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.FlavorXMLSerializer() - - fixture = { - "flavor": { - "id": "12", - "name": "asdf", - "ram": "256", - "disk": "10", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/12", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/12", - }, - ], - }, - } - - output = serializer.serialize(fixture, 'show') - print output - has_dec = output.startswith("") - self.assertTrue(has_dec) - - def test_show(self): - serializer = flavors.FlavorXMLSerializer() - - fixture = { - "flavor": { - "id": "12", - "name": "asdf", - "ram": "256", - "disk": "10", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/12", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/12", - }, - ], - }, - } - - output = serializer.serialize(fixture, 'show') - 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.FlavorXMLSerializer() - - fixture = { - "flavor": { - "id": 12, - "name": "asdf", - "ram": 256, - "disk": 10, - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/12", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/12", - }, - ], - }, - } - - output = serializer.serialize(fixture, 'show') - 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.FlavorXMLSerializer() - - fixture = { - "flavors": [ - { - "id": "23", - "name": "flavor 23", - "ram": "512", - "disk": "20", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/23", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/23", - }, - ], - }, - { - "id": "13", - "name": "flavor 13", - "ram": "256", - "disk": "10", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/13", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/13", - }, - ], - }, - ], - } - - output = serializer.serialize(fixture, 'detail') - 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.FlavorXMLSerializer() - - fixture = { - "flavors": [ - { - "id": "23", - "name": "flavor 23", - "ram": "512", - "disk": "20", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/23", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/23", - }, - ], - }, - { - "id": "13", - "name": "flavor 13", - "ram": "256", - "disk": "10", - "rxtx_cap": "", - "rxtx_quota": "", - "swap": "", - "vcpus": "", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/fake/flavors/13", - }, - { - "rel": "bookmark", - "href": "http://localhost/fake/flavors/13", - }, - ], - }, - ], - } - - output = serializer.serialize(fixture, 'index') - 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.FlavorXMLSerializer() - - fixture = { - "flavors": [], - } - - output = serializer.serialize(fixture, 'index') - 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/test_image_metadata.py b/nova/tests/api/openstack/test_image_metadata.py deleted file mode 100644 index dc0cf1671..000000000 --- a/nova/tests/api/openstack/test_image_metadata.py +++ /dev/null @@ -1,201 +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.tests.api.openstack import fakes -from nova.api.openstack import image_metadata -from nova import flags -from nova import test - - -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('/v1.1/123/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('/v1.1/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('/v1.1/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('/v1.1/fake/images/100/metadata/key1') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.show, req, '100', 'key9') - - def test_create(self): - req = fakes.HTTPRequest.blank('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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/test_images.py b/nova/tests/api/openstack/test_images.py deleted file mode 100644 index eb200046f..000000000 --- a/nova/tests/api/openstack/test_images.py +++ /dev/null @@ -1,1645 +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 -""" - -from lxml import etree -import stubout -import urlparse -import webob - -from nova.api.openstack import images -from nova.api.openstack import xmlutil -from nova.api.openstack.views import images as images_view -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('/v1.1/fake/images/123') - actual_image = self.controller.show(fake_req, '124') - - href = "http://localhost/v1.1/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/v1.1/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('/v1.1/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('/v1.1/fake/images') - response_list = self.controller.index(fake_req)['images'] - - expected_images = [ - { - "id": "123", - "name": "public image", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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('/v1.1/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/v1.1/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/v1.1/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/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/fake/images/detail') - response = self.controller.detail(request) - response_list = response["images"] - - server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" - server_href = "http://localhost/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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('/v1.1/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/v1.1/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/v1.1/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/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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 = '/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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/v1.1/servers/' + SERVER_UUID - SERVER_BOOKMARK = 'http://localhost/servers/' + SERVER_UUID - IMAGE_HREF = 'http://localhost/v1.1/fake/images/%s' - IMAGE_NEXT = 'http://localhost/v1.1/fake/images?limit=%s&marker=%s' - IMAGE_BOOKMARK = 'http://localhost/fake/images/%s' - - def test_xml_declaration(self): - serializer = images.ImageXMLSerializer() - - 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, 'show') - has_dec = output.startswith("") - self.assertTrue(has_dec) - - def test_show(self): - serializer = images.ImageXMLSerializer() - - 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, 'show') - 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.ImageXMLSerializer() - - 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, 'show') - 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.ImageXMLSerializer() - - 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, 'show') - 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.ImageXMLSerializer() - - 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, 'show') - 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.ImageXMLSerializer() - - 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, 'show') - 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.ImageXMLSerializer() - - 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, 'show') - 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.ImageXMLSerializer() - - 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, 'index') - 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.ImageXMLSerializer() - - 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, 'index') - 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.ImageXMLSerializer() - - fixtures = { - 'images': [], - } - - output = serializer.serialize(fixtures, 'index') - 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.ImageXMLSerializer() - - 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, 'detail') - 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/test_limits.py b/nova/tests/api/openstack/test_limits.py deleted file mode 100644 index 96e30f756..000000000 --- a/nova/tests/api/openstack/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 -from lxml import etree -import StringIO -import stubout -import time -import unittest -import webob -from xml.dom import minidom - -import nova.context -from nova.api.openstack import limits -from nova.api.openstack import views -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -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.LimitsXMLSerializer() - - fixture = {"limits": { - "rate": [], - "absolute": {}}} - - output = serializer.serialize(fixture, 'index') - print output - has_dec = output.startswith("") - self.assertTrue(has_dec) - - def test_index(self): - serializer = limits.LimitsXMLSerializer() - 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, 'index') - 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.LimitsXMLSerializer() - - fixture = {"limits": { - "rate": [], - "absolute": {}}} - - output = serializer.serialize(fixture, 'index') - 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/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py deleted file mode 100644 index 9d50483b8..000000000 --- a/nova/tests/api/openstack/test_server_actions.py +++ /dev/null @@ -1,866 +0,0 @@ -import base64 -import datetime -import json - -import stubout -import webob - -from nova import context -from nova import utils -from nova import exception -from nova import flags -from nova.api.openstack import servers -from nova.compute import vm_states -from nova.compute import instance_types -import nova.db -from nova import test -from nova.tests.api.openstack import common -from nova.tests.api.openstack import fakes - - -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): - 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_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) - self.backup = fakes.stub_out_compute_api_backup(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 = '/v1.1/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_server_rebuild_accepted_minimum(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", - }, - } - - req = fakes.HTTPRequest.blank(self.url) - body = self.controller.action(req, FAKE_UUID, body) - - self.assertEqual(body['server']['image']['id'], '2') - self.assertEqual(len(body['server']['adminPass']), - FLAGS.password_length) - - def test_server_rebuild_rejected_when_building(self): - body = { - "rebuild": { - "imageRef": "http://localhost/images/2", - }, - } - - def fake_rebuild(*args, **kwargs): - raise exception.RebuildRequiresActiveInstance - - 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_server_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) - - self.assertEqual(body['server']['metadata'], metadata) - - def test_server_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_server_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_server_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_server_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) - - self.assertTrue('personality' not in body['server']) - - def test_server_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) - - self.assertEqual(body['server']['image']['id'], '2') - self.assertEqual(body['server']['adminPass'], 'asdf') - - def test_server_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_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_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_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_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/v1.1/fake/images/123', location) - server_location = self.snapshot.extra_props_last_call['instance_ref'] - expected_server_location = 'http://localhost/v1.1/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/v1.1/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_conflict_snapshot(self): - """Attempt to create image when image is already being created.""" - def snapshot(*args, **kwargs): - raise exception.InstanceSnapshotting - 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) - - def test_create_backup(self): - """The happy path for creating backups""" - self.flags(allow_admin_api=True) - - body = { - 'createBackup': { - 'name': 'Backup 1', - 'backup_type': 'daily', - 'rotation': 1, - }, - } - - req = fakes.HTTPRequest.blank(self.url) - response = self.controller.action(req, FAKE_UUID, body) - - self.assertTrue(response.headers['Location']) - server_location = self.backup.extra_props_last_call['instance_ref'] - expected_server_location = 'http://localhost/v1.1/servers/' + self.uuid - self.assertEqual(expected_server_location, server_location) - - def test_create_backup_admin_api_off(self): - """The happy path for creating backups""" - self.flags(allow_admin_api=False) - - body = { - 'createBackup': { - 'name': 'Backup 1', - 'backup_type': 'daily', - 'rotation': 1, - }, - } - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - def test_create_backup_with_metadata(self): - self.flags(allow_admin_api=True) - - body = { - 'createBackup': { - 'name': 'Backup 1', - 'backup_type': 'daily', - 'rotation': 1, - 'metadata': {'123': 'asdf'}, - }, - } - - req = fakes.HTTPRequest.blank(self.url) - response = self.controller.action(req, FAKE_UUID, body) - - self.assertTrue(response.headers['Location']) - - def test_create_backup_with_too_much_metadata(self): - self.flags(allow_admin_api=True) - - 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" - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, - self.controller.action, req, FAKE_UUID, body) - - def test_create_backup_no_name(self): - """Name is required for backups""" - self.flags(allow_admin_api=True) - - body = { - 'createBackup': { - 'backup_type': 'daily', - 'rotation': 1, - }, - } - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - def test_create_backup_no_rotation(self): - """Rotation is required for backup requests""" - self.flags(allow_admin_api=True) - - body = { - 'createBackup': { - 'name': 'Backup 1', - 'backup_type': 'daily', - }, - } - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - def test_create_backup_no_backup_type(self): - """Backup Type (daily or weekly) is required for backup requests""" - self.flags(allow_admin_api=True) - - body = { - 'createBackup': { - 'name': 'Backup 1', - 'rotation': 1, - }, - } - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - def test_create_backup_bad_entity(self): - self.flags(allow_admin_api=True) - - body = {'createBackup': 'go'} - - req = fakes.HTTPRequest.blank(self.url) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.action, req, FAKE_UUID, body) - - -class TestServerActionXMLDeserializer(test.TestCase): - - def setUp(self): - self.deserializer = servers.ServerXMLDeserializer() - - 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/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py deleted file mode 100644 index 1e0cba541..000000000 --- a/nova/tests/api/openstack/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 import server_metadata -import nova.db -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 - - -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/test_servers.py b/nova/tests/api/openstack/test_servers.py deleted file mode 100644 index 7eaaaa489..000000000 --- a/nova/tests/api/openstack/test_servers.py +++ /dev/null @@ -1,3638 +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 base64 -import datetime -import json -import unittest -import urlparse -from xml.dom import minidom - -from lxml import etree -import webob - -import nova.api.openstack -from nova.api.openstack import servers -from nova.api.openstack import ips -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 -from nova import context -from nova import db -import nova.db -from nova.db.sqlalchemy.models import InstanceMetadata -from nova import exception -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 common -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.stubs.Set(nova.compute.API, "get_diagnostics", fake_compute_api) - self.stubs.Set(nova.compute.API, "get_actions", fake_compute_api) - - 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('/v1.1/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('/v1.1/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/v1.1/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('/v1.1/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/v1.1/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('/v1.1/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/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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 = '/v1.1/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 = '/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/fake/servers?limit=asdf') - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.index, req) - - def test_get_servers_with_marker(self): - url = '/v1.1/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 = '/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/123/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('/v1.1/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('/v1.1/123/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('/v1.1/123/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/fake/servers/%s' % FAKE_UUID) - req.method = 'DELETE' - - self.server_delete_called = False - - 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) - - -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('/v1.1/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('/v1.1/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - server = self.controller.create(req, body)['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('/v1.1/123/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - 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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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/v1.1/' - image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - image_href = 'http://localhost/v1.1/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('/v1.1/123/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - 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/v1.1/' - image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' - image_href = 'http://localhost/v1.1/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('/v1.1/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - 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/v1.1/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('/v1.1/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('/v1.1/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - 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/v1.1/images/2' - flavor_ref = 'http://localhost/v1.1/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('/v1.1/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/v1.1/123/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('/v1.1/123/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/v1.1/images/2' - flavor_ref = 'http://localhost/v1.1/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('/v1.1/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/v1.1/123/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('/v1.1/123/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - 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/v1.1/123/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('/v1.1/123/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - 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/v1.1/123/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('/v1.1/123/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/v1.1/123/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('/v1.1/123/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - server = res['server'] - self.assertEqual(FAKE_UUID, server['id']) - - def test_create_instance_bad_href(self): - image_href = 'asdf' - flavor_ref = 'http://localhost/v1.1/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('/v1.1/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/v1.1/flavors/3' - body = { - 'server': { - 'name': 'server_test', - 'imageRef': image_uuid, - 'flavorRef': flavor_ref, - }, - } - - req = fakes.HTTPRequest.blank('/v1.1/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers["content-type"] = "application/json" - res = self.controller.create(req, body) - - 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('/v1.1/fake/servers') - req.method = 'POST' - req.body = json.dumps(body) - req.headers['content-type'] = "application/json" - res = self.controller.create(req, body) - - 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('/v1.1/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('/v1.1/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) - - -class TestServerCreateRequestXMLDeserializer(test.TestCase): - - def setUp(self): - super(TestServerCreateRequestXMLDeserializer, self).setUp() - self.deserializer = servers.ServerXMLDeserializer() - - def test_minimal_request(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - expected = { - "server": { - "name": "new-server-test", - "imageRef": "http://localhost:8774/v1.1/images/2", - "flavorRef": "3", - }, - } - self.assertEquals(request['body'], expected) - - def test_flavor_link(self): - serial_request = """ -""" - request = self.deserializer.deserialize(serial_request, 'create') - expected = { - "server": { - "name": "new-server-test", - "imageRef": "1", - "flavorRef": "http://localhost:8774/v1.1/flavors/3", - }, - } - self.assertEquals(request['body'], expected) - - def test_empty_metadata_personality(self): - serial_request = """ - - - -""" - request = self.deserializer.deserialize(serial_request, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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, 'create') - 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): - - serializer = nova.api.openstack.ips.IPXMLSerializer() - - def test_xml_declaration(self): - fixture = { - 'network_2': [ - {'addr': '192.168.0.1', 'version': 4}, - {'addr': 'fe80::beef', 'version': 6}, - ], - } - output = self.serializer.serialize(fixture, 'show') - 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.serializer.serialize(fixture, 'show') - 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.serializer.serialize(fixture, 'index') - 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.views.servers.ViewBuilder() - self.request = fakes.HTTPRequest.blank("/v1.1") - - def test_build_server(self): - self_link = "http://localhost/v1.1/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/v1.1/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/v1.1/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_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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/servers/%s' % FAKE_UUID - SERVER_NEXT = 'http://localhost/v1.1/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.ServerXMLSerializer() - - 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, 'show') - print output - has_dec = output.startswith("") - self.assertTrue(has_dec) - - def test_show(self): - serializer = servers.ServerXMLSerializer() - - 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, 'show') - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'server') - - expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK - expected_image_bookmark = self.IMAGE_BOOKMARK - expected_flavor_bookmark = self.FLAVOR_BOOKMARK - expected_now = self.TIMESTAMP - 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.ServerXMLSerializer() - - 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, 'create') - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'server') - - expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK - expected_image_bookmark = self.IMAGE_BOOKMARK - expected_flavor_bookmark = self.FLAVOR_BOOKMARK - expected_now = self.TIMESTAMP - 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.ServerXMLSerializer() - - uuid1 = get_fake_uuid(1) - uuid2 = get_fake_uuid(2) - expected_server_href = 'http://localhost/v1.1/servers/%s' % uuid1 - expected_server_bookmark = 'http://localhost/servers/%s' % uuid1 - expected_server_href_2 = 'http://localhost/v1.1/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, 'index') - 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.ServerXMLSerializer() - - uuid1 = get_fake_uuid(1) - uuid2 = get_fake_uuid(2) - expected_server_href = 'http://localhost/v1.1/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/v1.1/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, 'index') - 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.ServerXMLSerializer() - - uuid1 = get_fake_uuid(1) - expected_server_href = 'http://localhost/v1.1/servers/%s' % uuid1 - expected_server_bookmark = 'http://localhost/servers/%s' % uuid1 - expected_image_bookmark = self.IMAGE_BOOKMARK - expected_flavor_bookmark = self.FLAVOR_BOOKMARK - expected_now = self.TIMESTAMP - - uuid2 = get_fake_uuid(2) - expected_server_href_2 = 'http://localhost/v1.1/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, 'detail') - 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.ServerXMLSerializer() - - 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', - }, - ], - } - } - - output = serializer.serialize(fixture, 'update') - print output - root = etree.XML(output) - xmlutil.validate_schema(root, 'server') - - expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK - expected_image_bookmark = self.IMAGE_BOOKMARK - expected_flavor_bookmark = self.FLAVOR_BOOKMARK - expected_now = self.TIMESTAMP - 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_action(self): - serializer = servers.ServerXMLSerializer() - - 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, 'action') - root = etree.XML(output) - xmlutil.validate_schema(root, 'server') - - expected_server_href = self.SERVER_HREF - expected_server_bookmark = self.SERVER_BOOKMARK - expected_image_bookmark = self.IMAGE_BOOKMARK - expected_flavor_bookmark = self.FLAVOR_BOOKMARK - expected_now = self.TIMESTAMP - 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/test_urlmap.py b/nova/tests/api/openstack/test_urlmap.py deleted file mode 100644 index 3995765e5..000000000 --- a/nova/tests/api/openstack/test_urlmap.py +++ /dev/null @@ -1,84 +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 test -from nova import log as logging -from nova.tests.api.openstack import fakes - -LOG = logging.getLogger('nova.tests.api.openstack.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 v1.1 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'], 'v1.1') - - def test_content_type_version_v1_1(self): - """Test Content-Type specifying v1.1 returns v1.1 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'], 'v1.1') - - def test_accept_version_v1_1(self): - """Test Accept header specifying v1.1 returns v1.1 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'], 'v1.1') - - def test_path_content_type(self): - """Test URL path specifying JSON returns JSON content.""" - url = '/v1.1/foobar/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 = '/v1.1/foobar/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/test_users.py b/nova/tests/api/openstack/test_users.py deleted file mode 100644 index 82fce68ff..000000000 --- a/nova/tests/api/openstack/test_users.py +++ /dev/null @@ -1,157 +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 import utils -from nova.api.openstack import users -from nova.auth.manager import User, Project -from nova.tests.api.openstack import fakes - - -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('/v1.1/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('/v1.1/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('/v1.1/fake/users/id1') - res_dict = 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('/v1.1/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('/v1.1/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): - - serializer = users.UserXMLSerializer() - - def test_index(self): - fixture = {'users': [{'id': 'id1', - 'name': 'guy1', - 'secret': 'secret1', - 'admin': False}, - {'id': 'id2', - 'name': 'guy2', - 'secret': 'secret2', - 'admin': True}]} - - output = self.serializer.serialize(fixture, 'index') - 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): - fixture = {'user': {'id': 'id2', - 'name': 'guy2', - 'secret': 'secret2', - 'admin': True}} - - output = self.serializer.serialize(fixture, 'show') - 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/test_versions.py b/nova/tests/api/openstack/test_versions.py deleted file mode 100644 index a6e30187b..000000000 --- a/nova/tests/api/openstack/test_versions.py +++ /dev/null @@ -1,671 +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 import versions -from nova.api.openstack import views -from nova.api.openstack import wsgi -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 = { - "v1.1": { - "id": "v1.1", - "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=1.1", - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute+json;version=1.1", - }, - ], - }, -} - - -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": "v1.1", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/", - }], - }, - ] - self.assertEqual(versions, expected) - - def test_get_version_list_302(self): - req = webob.Request.blank('/v1.1') - req.accept = "application/json" - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 302) - redirect_req = webob.Request.blank('/v1.1/') - self.assertEqual(res.location, redirect_req.url) - - def test_get_version_1_1_detail(self): - 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") - version = json.loads(res.body) - expected = { - "version": { - "id": "v1.1", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/", - }, - { - "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=1.1", - }, - { - "base": "application/json", - "type": "application/" - "vnd.openstack.compute+json;version=1.1", - }, - ], - }, - } - self.assertEqual(expected, version) - - def test_get_version_1_1_detail_content_type(self): - 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") - version = json.loads(res.body) - expected = { - "version": { - "id": "v1.1", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/", - }, - { - "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=1.1", - }, - { - "base": "application/json", - "type": "application/" - "vnd.openstack.compute+json;version=1.1", - }, - ], - }, - } - self.assertEqual(expected, version) - - def test_get_version_1_1_detail_xml(self): - req = webob.Request.blank('/v1.1/') - 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['v1.1'] - 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/v1.1/'}] - + 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(['v1.1']): - 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_1_1_detail_atom(self): - req = webob.Request.blank('/v1.1/') - 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/v1.1/') - 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/v1.1/') - self.assertEqual(f.feed.links[0]['rel'], 'self') - - self.assertEqual(len(f.entries), 1) - entry = f.entries[0] - self.assertEqual(entry.id, 'http://localhost/v1.1/') - self.assertEqual(entry.title, 'Version v1.1') - self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') - self.assertEqual(len(entry.content), 1) - self.assertEqual(entry.content[0].value, - 'Version v1.1 CURRENT (2011-01-21T11:33:21Z)') - self.assertEqual(len(entry.links), 3) - self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.1/') - 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/v1.1/') - self.assertEqual(entry.title, 'Version v1.1') - self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') - self.assertEqual(len(entry.content), 1) - self.assertEqual(entry.content[0].value, - 'Version v1.1 CURRENT (2011-01-21T11:33:21Z)') - self.assertEqual(len(entry.links), 1) - self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.1/') - 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": "v1.1", - "status": "CURRENT", - "links": [ - { - "href": "http://localhost/v1.1/images/1", - "rel": "self", - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.compute+xml" - ";version=1.1" - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute+json" - ";version=1.1" - }, - ], - }, - ], } - - 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'), 'v1.1') - 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['v1.1']['media-types'])) - links = version.xpath('atom:link', namespaces=NS) - self.assertTrue(common.compare_links(links, - [{'rel': 'self', 'href': 'http://localhost/v1.1/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": "v1.1", - "status": "CURRENT", - "links": [ - { - "href": "http://localhost/v1.1/servers/" + uuid, - "rel": "self", - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.compute+xml" - ";version=1.1" - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute+json" - ";version=1.1" - }, - ], - }, - ], } - - 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/3.2.1/", - }, - ], - } - ] - } - - 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/" - version_number = "v1.4.6" - - expected = "http://example.org/app/v1.4.6/" - - builder = views.versions.ViewBuilder(base_url) - actual = builder.generate_href(version_number) - - self.assertEqual(actual, expected) - - -class VersionsSerializerTests(test.TestCase): - def test_versions_list_xml_serializer(self): - versions_data = { - 'versions': [ - { - "id": "2.7.1", - "updated": "2011-07-18T11:30:00Z", - "status": "DEPRECATED", - "links": [ - { - "rel": "self", - "href": "http://test/2.7.1", - }, - ], - }, - ] - } - - serializer = versions.VersionsXMLSerializer() - response = serializer.index(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/2.7.1', - 'type': 'application/atom+xml'}])) - - def test_versions_multi_xml_serializer(self): - versions_data = { - 'choices': [ - { - "id": "2.7.1", - "updated": "2011-07-18T11:30:00Z", - "status": "DEPRECATED", - "media-types": VERSIONS['v1.1']['media-types'], - "links": [ - { - "rel": "self", - "href": "http://test/2.7.1/images", - }, - ], - }, - ] - } - - serializer = versions.VersionsXMLSerializer() - response = serializer.multi(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] - media_type_nodes = list(media_types) - 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.index(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": "v1.1", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/", - }, - { - "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=1.1", - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute+json" - ";version=1.1", - } - ], - }, - } - - serializer = versions.VersionsAtomSerializer() - response = serializer.show(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/v1.1/') - 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/v1.1/') - self.assertEqual(f.feed.links[0]['rel'], 'self') - - self.assertEqual(len(f.entries), 1) - entry = f.entries[0] - self.assertEqual(entry.id, 'http://localhost/v1.1/') - self.assertEqual(entry.title, 'Version v1.1') - self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') - self.assertEqual(len(entry.content), 1) - self.assertEqual(entry.content[0].value, - 'Version v1.1 CURRENT (2011-01-21T11:33:21Z)') - self.assertEqual(len(entry.links), 3) - self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.1/') - 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/test_zones.py b/nova/tests/api/openstack/test_zones.py deleted file mode 100644 index 496c3e8e6..000000000 --- a/nova/tests/api/openstack/test_zones.py +++ /dev/null @@ -1,283 +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 -import stubout -import webob - -import nova.db -from nova import context -from nova import crypto -from nova import flags -from nova import test -from nova.api.openstack import xmlutil -from nova.api.openstack import zones -from nova.tests.api.openstack import fakes -from nova.scheduler import api - - -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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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): - - serializer = zones.ZonesXMLSerializer() - - def test_select(self): - 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 = self.serializer.serialize(fixture, 'select') - 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): - fixture = {'zones': zone_get_all_scheduler()} - - output = self.serializer.serialize(fixture, 'index') - 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): - zone = {'id': 1, - 'api_url': 'http://example.com', - 'name': 'darksecret', - 'cap1': 'a;b', - 'cap2': 'c;d'} - fixture = {'zone': zone} - - output = self.serializer.serialize(fixture, 'show') - 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/__init__.py b/nova/tests/api/openstack/v2/__init__.py new file mode 100644 index 000000000..00fcfbb00 --- /dev/null +++ b/nova/tests/api/openstack/v2/__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/v2/contrib/__init__.py b/nova/tests/api/openstack/v2/contrib/__init__.py new file mode 100644 index 000000000..848908a95 --- /dev/null +++ b/nova/tests/api/openstack/v2/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/v2/contrib/test_admin_actions.py b/nova/tests/api/openstack/v2/contrib/test_admin_actions.py new file mode 100644 index 000000000..4b62c0ba7 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_admin_actions.py @@ -0,0 +1,86 @@ +# 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 import compute +from nova import flags +from nova import test +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(cls, req, id): + return True + + +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') + + def setUp(self): + super(AdminActionsTest, self).setUp() + self.flags(allow_admin_api=True) + self.stubs.Set(compute.API, 'get', fake_compute_api_get) + for _method in self._methods: + self.stubs.Set(compute.API, _method, fake_compute_api) + + def test_admin_api_enabled(self): + app = fakes.wsgi_app() + for _action in self._actions: + req = webob.Request.blank('/v1.1/fake/servers/abcd/action') + 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_disabled(self): + FLAGS.allow_admin_api = False + app = fakes.wsgi_app() + for _action in self._actions: + req = webob.Request.blank('/v1.1/fake/servers/abcd/action') + req.method = 'POST' + req.body = json.dumps({_action: None}) + req.content_type = 'application/json' + res = req.get_response(app) + self.assertEqual(res.status_int, 404) diff --git a/nova/tests/api/openstack/v2/contrib/test_createserverext.py b/nova/tests/api/openstack/v2/contrib/test_createserverext.py new file mode 100644 index 000000000..5c9e0499a --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_createserverext.py @@ -0,0 +1,431 @@ +# 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 rpc +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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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_disk_config.py b/nova/tests/api/openstack/v2/contrib/test_disk_config.py new file mode 100644 index 000000000..ac2391a04 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_disk_config.py @@ -0,0 +1,248 @@ +# 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.api, 'instance_get', fake_instance_get) + 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.api, '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_, session=None): + return inst + + self.stubs.Set(nova.db, 'instance_get', + fake_instance_get_for_create) + self.stubs.Set(nova.db.api, 'instance_get', + fake_instance_get_for_create) + self.stubs.Set(nova.db.sqlalchemy.api, 'instance_get', + fake_instance_get_for_create) + + def fake_instance_add_security_group(context, instance_id, + security_group_id): + pass + + self.stubs.Set(nova.db.sqlalchemy.api, + '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 new file mode 100644 index 000000000..54246727c --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_extendedstatus.py @@ -0,0 +1,88 @@ +# 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 = '/v1.1/openstack/servers/%s' % self.uuid + fakes.stub_out_nw_api(self.stubs) + 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_with_admin(self): + self.flags(allow_admin_api=True) + 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_admin(self): + self.flags(allow_admin_api=False) + res = self._make_request() + body = json.loads(res.body) + + self.assertEqual(res.status_int, 200) + self.assertServerStates(body['server'], + vm_state=None, + power_state=None, + task_state=None) + + def test_extended_status_no_instance_fails(self): + self.flags(allow_admin_api=True) + + 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 new file mode 100644 index 000000000..5f9be886f --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_flavors_extra_specs.py @@ -0,0 +1,170 @@ +# 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 json +import os.path + +import stubout +import webob + +from nova.api.openstack.v2 import extensions +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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/flavors/1/os-extra_specs/bad') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'bad', body) diff --git a/nova/tests/api/openstack/v2/contrib/test_floating_ips.py b/nova/tests/api/openstack/v2/contrib/test_floating_ips.py new file mode 100644 index 000000000..ded363406 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_floating_ips.py @@ -0,0 +1,269 @@ +# 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 stubout + +import webob + +from nova.api.openstack.v2.contrib import floating_ips +from nova.api.openstack.v2.contrib import floating_ips +from nova import compute +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.tests.api.openstack.v2 import test_servers + + +def network_api_get_floating_ip(self, context, id): + return {'id': 1, 'address': '10.10.10.10', + 'fixed_ip': None} + + +def network_api_get_floating_ip_by_address(self, context, address): + return {'id': 1, 'address': '10.10.10.10', + 'fixed_ip': {'address': '10.0.0.1', 'instance_id': 1}} + + +def network_api_get_floating_ips_by_project(self, context): + return [{'id': 1, + 'address': '10.10.10.10', + 'fixed_ip': {'address': '10.0.0.1', 'instance_id': 1}}, + {'id': 2, + '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, + "name": 'fake', + "user_id": 'fakeuser', + "project_id": '123'} + + +class StubExtensionManager(object): + def register(self, *args): + pass + + +class FloatingIpTest(test.TestCase): + address = "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.address, + 'host': host}) + + def _delete_floating_ip(self): + db.floating_ip_destroy(self.context, self.address) + + 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.address) + 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', '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('/v1.1/123/os-floating-ips') + res_dict = self.controller.index(req) + + response = {'floating_ips': [{'instance_id': 1, + 'ip': '10.10.10.10', + 'fixed_ip': '10.0.0.1', + 'id': 1}, + {'instance_id': None, + 'ip': '10.10.10.11', + 'fixed_ip': None, + 'id': 2}]} + self.assertEqual(res_dict, response) + + def test_floating_ip_show(self): + req = fakes.HTTPRequest.blank('/v1.1/123/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', + 'fixed_ip': {'address': '10.0.0.1', 'instance_id': 1}} + self.stubs.Set(network.api.API, "get_floating_ip", get_floating_ip) + + req = fakes.HTTPRequest.blank('/v1.1/123/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'], 1) + +# 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('/v1.1/123/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'} + + 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('/v1.1/123/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} + self.assertEqual(ip, expected) + + def test_floating_ip_release(self): + req = fakes.HTTPRequest.blank('/v1.1/123/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.address)) + + req = fakes.HTTPRequest.blank('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/servers/test_inst/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._add_floating_ip, body, req, + 'test_inst') diff --git a/nova/tests/api/openstack/v2/contrib/test_keypairs.py b/nova/tests/api/openstack/v2/contrib/test_keypairs.py new file mode 100644 index 000000000..b2f595e5e --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_keypairs.py @@ -0,0 +1,113 @@ +# 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.api.openstack.v2.contrib.keypairs import KeypairController +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 = 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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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) diff --git a/nova/tests/api/openstack/v2/contrib/test_multinic_xs.py b/nova/tests/api/openstack/v2/contrib/test_multinic_xs.py new file mode 100644 index 000000000..68f99d363 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_multinic_xs.py @@ -0,0 +1,114 @@ +# 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 stubout +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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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_quotas.py b/nova/tests/api/openstack/v2/contrib/test_quotas.py new file mode 100644 index 000000000..15fb20f94 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_quotas.py @@ -0,0 +1,133 @@ +# 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.contrib.quotas import QuotaSetsController +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 = 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 = 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 = '/v1.1/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('/v1.1/1234/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('/v1.1/1234/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('/v1.1/1234/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('/v1.1/1234/os-quota-sets/update_me') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, + req, 'update_me', body) diff --git a/nova/tests/api/openstack/v2/contrib/test_rescue.py b/nova/tests/api/openstack/v2/contrib/test_rescue.py new file mode 100644 index 000000000..9058d101e --- /dev/null +++ b/nova/tests/api/openstack/v2/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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 new file mode 100644 index 000000000..fd4e0841d --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_security_groups.py @@ -0,0 +1,849 @@ +# 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 json +import unittest +from xml.dom import minidom + +import mox +import webob + +from nova.api.openstack.v2.contrib import security_groups +import nova.db +from nova import exception +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, + 'host': "localhost", 'name': 'asdf'} + + +def return_security_group_by_name(context, project_id, group_name): + return {'id': 1, 'name': group_name, "instances": [{'id': 1}]} + + +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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/os-security-groups') + self.controller.create(req, {'security_group': sg}) + + req = fakes.HTTPRequest.blank('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/servers/1/action') + self.manager._removeSecurityGroup(body, req, '1') + + +class TestSecurityGroupRules(test.TestCase): + def setUp(self): + super(TestSecurityGroupRules, self).setUp() + + controller = security_groups.SecurityGroupController() + + 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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/os-security-group-rules/10') + self.controller.delete(req, '10') + + def test_delete_invalid_rule_id(self): + req = fakes.HTTPRequest.blank('/v1.1/123/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('/v1.1/123/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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + expected = { + "security_group": { + "description": "test", + }, + } + self.assertEquals(request['body'], expected) 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 new file mode 100644 index 000000000..56e7be0d2 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_simple_tenant_usage.py @@ -0,0 +1,172 @@ +# 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 +import webob + +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( + '/v1.1/123/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( + '/v1.1/123/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( + '/v1.1/123/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()) + self.assertEqual(res.status_int, 403) + + def test_verify_show(self): + req = webob.Request.blank( + '/v1.1/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( + '/v1.1/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) diff --git a/nova/tests/api/openstack/v2/contrib/test_virtual_interfaces.py b/nova/tests/api/openstack/v2/contrib/test_virtual_interfaces.py new file mode 100644 index 000000000..4285eaaaf --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_virtual_interfaces.py @@ -0,0 +1,56 @@ +# 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 + +import webob + +from nova.api.openstack.v2.contrib.virtual_interfaces import \ + ServerVirtualInterfaceController +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 = 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 = '/v1.1/123/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) diff --git a/nova/tests/api/openstack/v2/contrib/test_volume_types.py b/nova/tests/api/openstack/v2/contrib/test_volume_types.py new file mode 100644 index 000000000..9da77349d --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_volume_types.py @@ -0,0 +1,167 @@ +# 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 stubout +import webob + +from nova.api.openstack.v2.contrib import volumetypes +from nova import exception +from nova import context +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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/os-volume-types') + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.create, req, '') 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 new file mode 100644 index 000000000..9083d9d72 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_volume_types_extra_specs.py @@ -0,0 +1,168 @@ +# 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. + +import json +import os.path + +import stubout +import webob + +from nova.api.openstack.v2 import extensions +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 = '/v1.1/123/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) diff --git a/nova/tests/api/openstack/v2/contrib/test_volumes.py b/nova/tests/api/openstack/v2/contrib/test_volumes.py new file mode 100644 index 000000000..529abf225 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_volumes.py @@ -0,0 +1,89 @@ +# 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 + +import webob + +import nova +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('/v1.1/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, 200) + 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') diff --git a/nova/tests/api/openstack/v2/contrib/test_vsa.py b/nova/tests/api/openstack/v2/contrib/test_vsa.py new file mode 100644 index 000000000..b4055a204 --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_vsa.py @@ -0,0 +1,449 @@ +# 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 unittest + +import stubout +import webob + +from nova.api.openstack.v2.contrib.virtual_storage_arrays import _vsa_view +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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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': [], + } + + +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 + 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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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('/v1.1/777/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() diff --git a/nova/tests/api/openstack/v2/extensions/__init__.py b/nova/tests/api/openstack/v2/extensions/__init__.py new file mode 100644 index 000000000..848908a95 --- /dev/null +++ b/nova/tests/api/openstack/v2/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/v2/extensions/foxinsocks.py b/nova/tests/api/openstack/v2/extensions/foxinsocks.py new file mode 100644 index 000000000..05a995496 --- /dev/null +++ b/nova/tests/api/openstack/v2/extensions/foxinsocks.py @@ -0,0 +1,94 @@ +# 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.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(object): + """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', + '/v1.1/:(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', + '/v1.1/:(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_accounts.py b/nova/tests/api/openstack/v2/test_accounts.py new file mode 100644 index 000000000..ab3ecafde --- /dev/null +++ b/nova/tests/api/openstack/v2/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.v2 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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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/test_api.py b/nova/tests/api/openstack/v2/test_api.py new file mode 100644 index 000000000..318c03a88 --- /dev/null +++ b/nova/tests/api/openstack/v2/test_api.py @@ -0,0 +1,126 @@ +# 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(self): + + @webob.dec.wsgify + def succeed(req): + return 'Succeeded' + + @webob.dec.wsgify + def raise_webob_exc(req): + raise webob.exc.HTTPNotFound(explanation='Raised a webob.exc') + + @webob.dec.wsgify + def fail(req): + raise Exception("Threw an exception") + + @webob.dec.wsgify + def raise_api_fault(req): + exc = webob.exc.HTTPNotFound(explanation='Raised a webob.exc') + return wsgi.Fault(exc) + + #api.application = succeed + api = self._wsgi_app(succeed) + resp = Request.blank('/').get_response(api) + self.assertFalse('cloudServersFault' in resp.body, resp.body) + self.assertEqual(resp.status_int, 200, resp.body) + + #api.application = raise_webob_exc + api = self._wsgi_app(raise_webob_exc) + resp = Request.blank('/').get_response(api) + self.assertFalse('cloudServersFault' in resp.body, resp.body) + self.assertEqual(resp.status_int, 404, resp.body) + + #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) + + #api.application = fail + api = self._wsgi_app(fail) + resp = Request.blank('/').get_response(api) + self.assertTrue('{"cloudServersFault' in resp.body, resp.body) + self.assertEqual(resp.status_int, 500, resp.body) + + #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 = '/v1.1/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) + + expected = {'console': {'id': 20, + 'port': 'fake_port', + 'host': 'fake_hostname', + 'password': 'fake_password', + '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): + + serializer = consoles.ConsoleXMLSerializer() + + def test_show(self): + fixture = {'console': {'id': 20, + 'password': 'fake_password', + 'port': 'fake_port', + 'host': 'fake_hostname', + 'console_type': 'fake_type'}} + + output = self.serializer.serialize(fixture, 'show') + 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 = self.serializer.serialize(fixture, 'index') + 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 new file mode 100644 index 000000000..5d388ddd3 --- /dev/null +++ b/nova/tests/api/openstack/v2/test_extensions.py @@ -0,0 +1,516 @@ +# 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 os.path + +import webob +from lxml import etree + +from nova.api.openstack import v2 +from nova.api.openstack.v2 import extensions +from nova.api.openstack.v2 import flavors +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import context +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) + + +class ExtensionControllerTest(ExtensionTestCase): + + def setUp(self): + super(ExtensionControllerTest, self).setUp() + self.ext_list = [ + "AdminActions", + "Createserverext", + "DeferredDelete", + "DiskConfig", + "ExtendedStatus", + "FlavorExtraSpecs", + "FlavorExtraData", + "Floating_ips", + "Fox In Socks", + "Hosts", + "Keypairs", + "Multinic", + "Quotas", + "Rescue", + "SecurityGroups", + "SimpleTenantUsage", + "VSAs", + "VirtualInterfaces", + "Volumes", + "VolumeTypes", + "Zones", + ] + 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("/123/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("/123/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("/123/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("/123/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("/123/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() + 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() + ext_midware = extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/123/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() + ext_midware = extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/123/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() + ext_midware = extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/123/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() + ext_midware = extensions.ExtensionMiddleware(app, manager) + ser_midware = wsgi.LazySerializationMiddleware(ext_midware) + request = webob.Request.blank("/123/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 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("/123/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) + + +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 = "/123/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 = "/123/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 = "/123/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 = "/123/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', + '/v1.1/123/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("/v1.1/123/flavors/1?chewing=bluegoo") + request.environ['api.version'] = '1.1' + 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("/v1.1/123/flavors/1?chewing=newblue") + request.environ['api.version'] = '1.1' + response = request.get_response(ser_midware) + self.assertEqual(200, response.status_int) + response_data = json.loads(response.body) + 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 new file mode 100644 index 000000000..c62291ad1 --- /dev/null +++ b/nova/tests/api/openstack/v2/test_flavors.py @@ -0,0 +1,670 @@ +# 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 + +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 +from nova import wsgi + + +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('/v1.1/fake/flavors/asdf') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, 'asdf') + + def test_get_flavor_by_id(self): + req = fakes.HTTPRequest.blank('/v1.1/fake/flavors/1') + flavor = self.controller.show(req, '1') + expected = { + "flavor": { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/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('/v1.1/fake/flavors') + flavor = self.controller.index(req) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/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('/v1.1/fake/flavors/detail') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/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('/v1.1/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('/v1.1/fake/flavors?minRam=512') + flavor = self.controller.index(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/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('/v1.1/fake/flavors?minDisk=20') + flavor = self.controller.index(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/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('/v1.1/fake/flavors/detail' + '?minRam=256&minDisk=20') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/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('/v1.1/fake/flavors/detail?minRam=16GB') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/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('/v1.1/fake/flavors/detail?minDisk=16GB') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/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.FlavorXMLSerializer() + + fixture = { + "flavor": { + "id": "12", + "name": "asdf", + "ram": "256", + "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + print output + has_dec = output.startswith("") + self.assertTrue(has_dec) + + def test_show(self): + serializer = flavors.FlavorXMLSerializer() + + fixture = { + "flavor": { + "id": "12", + "name": "asdf", + "ram": "256", + "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + 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.FlavorXMLSerializer() + + fixture = { + "flavor": { + "id": 12, + "name": "asdf", + "ram": 256, + "disk": 10, + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + 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.FlavorXMLSerializer() + + fixture = { + "flavors": [ + { + "id": "23", + "name": "flavor 23", + "ram": "512", + "disk": "20", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/23", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/23", + }, + ], + }, + { + "id": "13", + "name": "flavor 13", + "ram": "256", + "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/13", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/13", + }, + ], + }, + ], + } + + output = serializer.serialize(fixture, 'detail') + 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.FlavorXMLSerializer() + + fixture = { + "flavors": [ + { + "id": "23", + "name": "flavor 23", + "ram": "512", + "disk": "20", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/23", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/23", + }, + ], + }, + { + "id": "13", + "name": "flavor 13", + "ram": "256", + "disk": "10", + "rxtx_cap": "", + "rxtx_quota": "", + "swap": "", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/flavors/13", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/13", + }, + ], + }, + ], + } + + output = serializer.serialize(fixture, 'index') + 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.FlavorXMLSerializer() + + fixture = { + "flavors": [], + } + + output = serializer.serialize(fixture, 'index') + 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 new file mode 100644 index 000000000..a4f2f155d --- /dev/null +++ b/nova/tests/api/openstack/v2/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.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('/v1.1/123/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('/v1.1/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('/v1.1/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('/v1.1/fake/images/100/metadata/key1') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, '100', 'key9') + + def test_create(self): + req = fakes.HTTPRequest.blank('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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 new file mode 100644 index 000000000..b02b6ff2d --- /dev/null +++ b/nova/tests/api/openstack/v2/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.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('/v1.1/fake/images/123') + actual_image = self.controller.show(fake_req, '124') + + href = "http://localhost/v1.1/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/v1.1/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('/v1.1/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('/v1.1/fake/images') + response_list = self.controller.index(fake_req)['images'] + + expected_images = [ + { + "id": "123", + "name": "public image", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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('/v1.1/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/v1.1/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/v1.1/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/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/fake/images/detail') + response = self.controller.detail(request) + response_list = response["images"] + + server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" + server_href = "http://localhost/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/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('/v1.1/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/v1.1/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/v1.1/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/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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 = '/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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/v1.1/servers/' + SERVER_UUID + SERVER_BOOKMARK = 'http://localhost/servers/' + SERVER_UUID + IMAGE_HREF = 'http://localhost/v1.1/fake/images/%s' + IMAGE_NEXT = 'http://localhost/v1.1/fake/images?limit=%s&marker=%s' + IMAGE_BOOKMARK = 'http://localhost/fake/images/%s' + + def test_xml_declaration(self): + serializer = images.ImageXMLSerializer() + + 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, 'show') + has_dec = output.startswith("") + self.assertTrue(has_dec) + + def test_show(self): + serializer = images.ImageXMLSerializer() + + 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, 'show') + 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.ImageXMLSerializer() + + 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, 'show') + 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.ImageXMLSerializer() + + 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, 'show') + 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.ImageXMLSerializer() + + 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, 'show') + 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.ImageXMLSerializer() + + 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, 'show') + 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.ImageXMLSerializer() + + 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, 'show') + 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.ImageXMLSerializer() + + 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, 'index') + 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.ImageXMLSerializer() + + 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, 'index') + 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.ImageXMLSerializer() + + fixtures = { + 'images': [], + } + + output = serializer.serialize(fixtures, 'index') + 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.ImageXMLSerializer() + + 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, 'detail') + 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 new file mode 100644 index 000000000..e167f450c --- /dev/null +++ b/nova/tests/api/openstack/v2/test_limits.py @@ -0,0 +1,940 @@ +# 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 time +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.LimitsXMLSerializer() + + fixture = {"limits": { + "rate": [], + "absolute": {}}} + + output = serializer.serialize(fixture, 'index') + print output + has_dec = output.startswith("") + self.assertTrue(has_dec) + + def test_index(self): + serializer = limits.LimitsXMLSerializer() + 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, 'index') + 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.LimitsXMLSerializer() + + fixture = {"limits": { + "rate": [], + "absolute": {}}} + + output = serializer.serialize(fixture, 'index') + 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 new file mode 100644 index 000000000..3adde3b7a --- /dev/null +++ b/nova/tests/api/openstack/v2/test_server_actions.py @@ -0,0 +1,881 @@ +# 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 json + +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 common +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): + 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_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) + self.backup = fakes.stub_out_compute_api_backup(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 = '/v1.1/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_server_rebuild_accepted_minimum(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", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller.action(req, FAKE_UUID, body) + + self.assertEqual(body['server']['image']['id'], '2') + self.assertEqual(len(body['server']['adminPass']), + FLAGS.password_length) + + def test_server_rebuild_rejected_when_building(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + def fake_rebuild(*args, **kwargs): + raise exception.RebuildRequiresActiveInstance + + 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_server_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) + + self.assertEqual(body['server']['metadata'], metadata) + + def test_server_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_server_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_server_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_server_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) + + self.assertTrue('personality' not in body['server']) + + def test_server_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) + + self.assertEqual(body['server']['image']['id'], '2') + self.assertEqual(body['server']['adminPass'], 'asdf') + + def test_server_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_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_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_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_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/v1.1/fake/images/123', location) + server_location = self.snapshot.extra_props_last_call['instance_ref'] + expected_server_location = 'http://localhost/v1.1/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/v1.1/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_conflict_snapshot(self): + """Attempt to create image when image is already being created.""" + def snapshot(*args, **kwargs): + raise exception.InstanceSnapshotting + 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) + + def test_create_backup(self): + """The happy path for creating backups""" + self.flags(allow_admin_api=True) + + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + response = self.controller.action(req, FAKE_UUID, body) + + self.assertTrue(response.headers['Location']) + server_location = self.backup.extra_props_last_call['instance_ref'] + expected_server_location = 'http://localhost/v1.1/servers/' + self.uuid + self.assertEqual(expected_server_location, server_location) + + def test_create_backup_admin_api_off(self): + """The happy path for creating backups""" + self.flags(allow_admin_api=False) + + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + def test_create_backup_with_metadata(self): + self.flags(allow_admin_api=True) + + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + 'metadata': {'123': 'asdf'}, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + response = self.controller.action(req, FAKE_UUID, body) + + self.assertTrue(response.headers['Location']) + + def test_create_backup_with_too_much_metadata(self): + self.flags(allow_admin_api=True) + + 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" + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller.action, req, FAKE_UUID, body) + + def test_create_backup_no_name(self): + """Name is required for backups""" + self.flags(allow_admin_api=True) + + body = { + 'createBackup': { + 'backup_type': 'daily', + 'rotation': 1, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + def test_create_backup_no_rotation(self): + """Rotation is required for backup requests""" + self.flags(allow_admin_api=True) + + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + def test_create_backup_no_backup_type(self): + """Backup Type (daily or weekly) is required for backup requests""" + self.flags(allow_admin_api=True) + + body = { + 'createBackup': { + 'name': 'Backup 1', + 'rotation': 1, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + def test_create_backup_bad_entity(self): + self.flags(allow_admin_api=True) + + body = {'createBackup': 'go'} + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.action, req, FAKE_UUID, body) + + +class TestServerActionXMLDeserializer(test.TestCase): + + def setUp(self): + self.deserializer = servers.ServerXMLDeserializer() + + 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 new file mode 100644 index 000000000..49ff1bcd8 --- /dev/null +++ b/nova/tests/api/openstack/v2/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.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 new file mode 100644 index 000000000..34a5dfa51 --- /dev/null +++ b/nova/tests/api/openstack/v2/test_servers.py @@ -0,0 +1,3631 @@ +# 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 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.stubs.Set(nova.compute.API, "get_diagnostics", fake_compute_api) + self.stubs.Set(nova.compute.API, "get_actions", fake_compute_api) + + 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('/v1.1/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('/v1.1/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/v1.1/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('/v1.1/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/v1.1/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('/v1.1/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/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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 = '/v1.1/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 = '/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/fake/servers?limit=asdf') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_servers_with_marker(self): + url = '/v1.1/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 = '/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/123/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('/v1.1/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('/v1.1/123/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('/v1.1/123/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/fake/servers/%s' % FAKE_UUID) + req.method = 'DELETE' + + self.server_delete_called = False + + 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) + + +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('/v1.1/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('/v1.1/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + server = self.controller.create(req, body)['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('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + 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('/v1.1/123/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('/v1.1/123/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('/v1.1/123/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/v1.1/' + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v1.1/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('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + 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/v1.1/' + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v1.1/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('/v1.1/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + 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/v1.1/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('/v1.1/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('/v1.1/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + 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/v1.1/images/2' + flavor_ref = 'http://localhost/v1.1/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('/v1.1/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/v1.1/123/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('/v1.1/123/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/v1.1/images/2' + flavor_ref = 'http://localhost/v1.1/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('/v1.1/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/v1.1/123/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('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + 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/v1.1/123/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('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + 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/v1.1/123/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('/v1.1/123/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/v1.1/123/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('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_bad_href(self): + image_href = 'asdf' + flavor_ref = 'http://localhost/v1.1/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('/v1.1/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/v1.1/flavors/3' + body = { + 'server': { + 'name': 'server_test', + 'imageRef': image_uuid, + 'flavorRef': flavor_ref, + }, + } + + req = fakes.HTTPRequest.blank('/v1.1/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body) + + 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('/v1.1/fake/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = "application/json" + res = self.controller.create(req, body) + + 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('/v1.1/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('/v1.1/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) + + +class TestServerCreateRequestXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestServerCreateRequestXMLDeserializer, self).setUp() + self.deserializer = servers.ServerXMLDeserializer() + + def test_minimal_request(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + expected = { + "server": { + "name": "new-server-test", + "imageRef": "http://localhost:8774/v1.1/images/2", + "flavorRef": "3", + }, + } + self.assertEquals(request['body'], expected) + + def test_flavor_link(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "http://localhost:8774/v1.1/flavors/3", + }, + } + self.assertEquals(request['body'], expected) + + def test_empty_metadata_personality(self): + serial_request = """ + + + +""" + request = self.deserializer.deserialize(serial_request, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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, 'create') + 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): + + serializer = nova.api.openstack.v2.ips.IPXMLSerializer() + + def test_xml_declaration(self): + fixture = { + 'network_2': [ + {'addr': '192.168.0.1', 'version': 4}, + {'addr': 'fe80::beef', 'version': 6}, + ], + } + output = self.serializer.serialize(fixture, 'show') + 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.serializer.serialize(fixture, 'show') + 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.serializer.serialize(fixture, 'index') + 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("/v1.1") + + def test_build_server(self): + self_link = "http://localhost/v1.1/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/v1.1/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/v1.1/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_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/v1.1/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/v1.1/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/v1.1/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/v1.1/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/v1.1/servers/%s' % FAKE_UUID + SERVER_NEXT = 'http://localhost/v1.1/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.ServerXMLSerializer() + + 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, 'show') + print output + has_dec = output.startswith("") + self.assertTrue(has_dec) + + def test_show(self): + serializer = servers.ServerXMLSerializer() + + 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, 'show') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_image_bookmark = self.IMAGE_BOOKMARK + expected_flavor_bookmark = self.FLAVOR_BOOKMARK + expected_now = self.TIMESTAMP + 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.ServerXMLSerializer() + + 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, 'create') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_image_bookmark = self.IMAGE_BOOKMARK + expected_flavor_bookmark = self.FLAVOR_BOOKMARK + expected_now = self.TIMESTAMP + 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.ServerXMLSerializer() + + uuid1 = get_fake_uuid(1) + uuid2 = get_fake_uuid(2) + expected_server_href = 'http://localhost/v1.1/servers/%s' % uuid1 + expected_server_bookmark = 'http://localhost/servers/%s' % uuid1 + expected_server_href_2 = 'http://localhost/v1.1/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, 'index') + 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.ServerXMLSerializer() + + uuid1 = get_fake_uuid(1) + uuid2 = get_fake_uuid(2) + expected_server_href = 'http://localhost/v1.1/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/v1.1/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, 'index') + 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.ServerXMLSerializer() + + uuid1 = get_fake_uuid(1) + expected_server_href = 'http://localhost/v1.1/servers/%s' % uuid1 + expected_server_bookmark = 'http://localhost/servers/%s' % uuid1 + expected_image_bookmark = self.IMAGE_BOOKMARK + expected_flavor_bookmark = self.FLAVOR_BOOKMARK + expected_now = self.TIMESTAMP + + uuid2 = get_fake_uuid(2) + expected_server_href_2 = 'http://localhost/v1.1/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, 'detail') + 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.ServerXMLSerializer() + + 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', + }, + ], + } + } + + output = serializer.serialize(fixture, 'update') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_image_bookmark = self.IMAGE_BOOKMARK + expected_flavor_bookmark = self.FLAVOR_BOOKMARK + expected_now = self.TIMESTAMP + 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_action(self): + serializer = servers.ServerXMLSerializer() + + 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, 'action') + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_image_bookmark = self.IMAGE_BOOKMARK + expected_flavor_bookmark = self.FLAVOR_BOOKMARK + expected_now = self.TIMESTAMP + 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 new file mode 100644 index 000000000..61a237347 --- /dev/null +++ b/nova/tests/api/openstack/v2/test_urlmap.py @@ -0,0 +1,84 @@ +# 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 v1.1 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'], 'v1.1') + + def test_content_type_version_v1_1(self): + """Test Content-Type specifying v1.1 returns v1.1 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'], 'v1.1') + + def test_accept_version_v1_1(self): + """Test Accept header specifying v1.1 returns v1.1 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'], 'v1.1') + + def test_path_content_type(self): + """Test URL path specifying JSON returns JSON content.""" + url = '/v1.1/foobar/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 = '/v1.1/foobar/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_users.py b/nova/tests/api/openstack/v2/test_users.py new file mode 100644 index 000000000..454b7cae7 --- /dev/null +++ b/nova/tests/api/openstack/v2/test_users.py @@ -0,0 +1,157 @@ +# 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 import utils +from nova.api.openstack.v2 import users +from nova.auth.manager import User, Project +from nova.tests.api.openstack import fakes + + +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('/v1.1/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('/v1.1/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('/v1.1/fake/users/id1') + res_dict = 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('/v1.1/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('/v1.1/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): + + serializer = users.UserXMLSerializer() + + def test_index(self): + fixture = {'users': [{'id': 'id1', + 'name': 'guy1', + 'secret': 'secret1', + 'admin': False}, + {'id': 'id2', + 'name': 'guy2', + 'secret': 'secret2', + 'admin': True}]} + + output = self.serializer.serialize(fixture, 'index') + 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): + fixture = {'user': {'id': 'id2', + 'name': 'guy2', + 'secret': 'secret2', + 'admin': True}} + + output = self.serializer.serialize(fixture, 'show') + 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/test_versions.py b/nova/tests/api/openstack/v2/test_versions.py new file mode 100644 index 000000000..392e31a46 --- /dev/null +++ b/nova/tests/api/openstack/v2/test_versions.py @@ -0,0 +1,671 @@ +# 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 wsgi +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 = { + "v1.1": { + "id": "v1.1", + "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=1.1", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=1.1", + }, + ], + }, +} + + +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": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/", + }], + }, + ] + self.assertEqual(versions, expected) + + def test_get_version_list_302(self): + req = webob.Request.blank('/v1.1') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 302) + redirect_req = webob.Request.blank('/v1.1/') + self.assertEqual(res.location, redirect_req.url) + + def test_get_version_1_1_detail(self): + 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") + version = json.loads(res.body) + expected = { + "version": { + "id": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/", + }, + { + "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=1.1", + }, + { + "base": "application/json", + "type": "application/" + "vnd.openstack.compute+json;version=1.1", + }, + ], + }, + } + self.assertEqual(expected, version) + + def test_get_version_1_1_detail_content_type(self): + 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") + version = json.loads(res.body) + expected = { + "version": { + "id": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/", + }, + { + "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=1.1", + }, + { + "base": "application/json", + "type": "application/" + "vnd.openstack.compute+json;version=1.1", + }, + ], + }, + } + self.assertEqual(expected, version) + + def test_get_version_1_1_detail_xml(self): + req = webob.Request.blank('/v1.1/') + 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['v1.1'] + 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/v1.1/'}] + + 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(['v1.1']): + 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_1_1_detail_atom(self): + req = webob.Request.blank('/v1.1/') + 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/v1.1/') + 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/v1.1/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v1.1/') + self.assertEqual(entry.title, 'Version v1.1') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v1.1 CURRENT (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 3) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.1/') + 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/v1.1/') + self.assertEqual(entry.title, 'Version v1.1') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v1.1 CURRENT (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 1) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.1/') + 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": "v1.1", + "status": "CURRENT", + "links": [ + { + "href": "http://localhost/v1.1/images/1", + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml" + ";version=1.1" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json" + ";version=1.1" + }, + ], + }, + ], } + + 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'), 'v1.1') + 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['v1.1']['media-types'])) + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v1.1/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": "v1.1", + "status": "CURRENT", + "links": [ + { + "href": "http://localhost/v1.1/servers/" + uuid, + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml" + ";version=1.1" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json" + ";version=1.1" + }, + ], + }, + ], } + + 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/3.2.1/", + }, + ], + } + ] + } + + 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/" + version_number = "v1.4.6" + + expected = "http://example.org/app/v1.4.6/" + + builder = views.versions.ViewBuilder(base_url) + actual = builder.generate_href(version_number) + + self.assertEqual(actual, expected) + + +class VersionsSerializerTests(test.TestCase): + def test_versions_list_xml_serializer(self): + versions_data = { + 'versions': [ + { + "id": "2.7.1", + "updated": "2011-07-18T11:30:00Z", + "status": "DEPRECATED", + "links": [ + { + "rel": "self", + "href": "http://test/2.7.1", + }, + ], + }, + ] + } + + serializer = versions.VersionsXMLSerializer() + response = serializer.index(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/2.7.1', + 'type': 'application/atom+xml'}])) + + def test_versions_multi_xml_serializer(self): + versions_data = { + 'choices': [ + { + "id": "2.7.1", + "updated": "2011-07-18T11:30:00Z", + "status": "DEPRECATED", + "media-types": VERSIONS['v1.1']['media-types'], + "links": [ + { + "rel": "self", + "href": "http://test/2.7.1/images", + }, + ], + }, + ] + } + + serializer = versions.VersionsXMLSerializer() + response = serializer.multi(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] + media_type_nodes = list(media_types) + 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.index(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": "v1.1", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/", + }, + { + "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=1.1", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json" + ";version=1.1", + } + ], + }, + } + + serializer = versions.VersionsAtomSerializer() + response = serializer.show(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/v1.1/') + 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/v1.1/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v1.1/') + self.assertEqual(entry.title, 'Version v1.1') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v1.1 CURRENT (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 3) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v1.1/') + 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/v2/test_zones.py b/nova/tests/api/openstack/v2/test_zones.py new file mode 100644 index 000000000..4a3e3ed47 --- /dev/null +++ b/nova/tests/api/openstack/v2/test_zones.py @@ -0,0 +1,283 @@ +# 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 +import stubout +import webob + +from nova.api.openstack.v2 import zones +from nova.api.openstack import xmlutil +from nova import context +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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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('/v1.1/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): + + serializer = zones.ZonesXMLSerializer() + + def test_select(self): + 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 = self.serializer.serialize(fixture, 'select') + 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): + fixture = {'zones': zone_get_all_scheduler()} + + output = self.serializer.serialize(fixture, 'index') + 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): + zone = {'id': 1, + 'api_url': 'http://example.com', + 'name': 'darksecret', + 'cap1': 'a;b', + 'cap2': 'c;d'} + fixture = {'zone': zone} + + output = self.serializer.serialize(fixture, 'show') + 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/integrated/test_extensions.py b/nova/tests/integrated/test_extensions.py index 958f584ab..4318e30f2 100644 --- a/nova/tests/integrated/test_extensions.py +++ b/nova/tests/integrated/test_extensions.py @@ -30,7 +30,7 @@ class ExtensionsTest(integrated_helpers._IntegratedTestBase): def _get_flags(self): f = super(ExtensionsTest, self)._get_flags() f['osapi_extension'] = FLAGS.osapi_extension[:] - f['osapi_extension'].append('nova.tests.api.openstack.extensions.' + f['osapi_extension'].append('nova.tests.api.openstack.v2.extensions.' 'foxinsocks.Foxinsocks') return f diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py index becc74cdd..32fb5d2cf 100644 --- a/nova/tests/test_hosts.py +++ b/nova/tests/test_hosts.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import stubout import webob.exc from nova import context @@ -21,7 +20,7 @@ from nova import exception from nova import flags from nova import log as logging from nova import test -from nova.api.openstack.contrib import hosts as os_hosts +from nova.api.openstack.v2.contrib import hosts as os_hosts from nova.scheduler import api as scheduler_api -- cgit