From 9e26ca87600903f7383f7a9a9d6a7fa88593b649 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 23 Feb 2016 11:43:20 -0500 Subject: [PATCH] Migrate tempest-lib code into new lib dir This commit migrates all of the code from tempest-lib as of it's current HEAD, 6ad0ce42c2791a28125d38b40e7dcddf32dbeed7. The only changes made to the tempest-lib code is to update the imports and other references to tempest_lib. Since in it's new home it should be tempest.lib. Partially implements bp tempest-lib-reintegration Change-Id: Iadc1b61953a86fa9de34e285a0bb083b1ba06fa8 --- tempest/hacking/checks.py | 10 +- tempest/lib/__init__.py | 0 tempest/lib/api_schema/__init__.py | 0 tempest/lib/api_schema/response/__init__.py | 0 .../api_schema/response/compute/__init__.py | 0 .../response/compute/v2_1/__init__.py | 0 .../response/compute/v2_1/agents.py | 82 ++ .../response/compute/v2_1/aggregates.py | 92 ++ .../compute/v2_1/availability_zone.py | 78 ++ .../response/compute/v2_1/baremetal_nodes.py | 63 + .../response/compute/v2_1/certificates.py | 41 + .../response/compute/v2_1/extensions.py | 47 + .../response/compute/v2_1/fixed_ips.py | 41 + .../response/compute/v2_1/flavors.py | 103 ++ .../response/compute/v2_1/flavors_access.py | 36 + .../compute/v2_1/flavors_extra_specs.py | 40 + .../response/compute/v2_1/floating_ips.py | 148 +++ .../api_schema/response/compute/v2_1/hosts.py | 116 ++ .../response/compute/v2_1/hypervisors.py | 195 +++ .../response/compute/v2_1/images.py | 154 +++ .../compute/v2_1/instance_usage_audit_logs.py | 62 + .../response/compute/v2_1/interfaces.py | 73 ++ .../response/compute/v2_1/keypairs.py | 107 ++ .../response/compute/v2_1/limits.py | 106 ++ .../response/compute/v2_1/migrations.py | 51 + .../response/compute/v2_1/parameter_types.py | 96 ++ .../response/compute/v2_1/quota_classes.py | 31 + .../response/compute/v2_1/quotas.py | 65 + .../v2_1/security_group_default_rule.py | 65 + .../response/compute/v2_1/security_groups.py | 113 ++ .../response/compute/v2_1/servers.py | 553 +++++++++ .../response/compute/v2_1/services.py | 65 + .../response/compute/v2_1/snapshots.py | 61 + .../response/compute/v2_1/tenant_networks.py | 53 + .../response/compute/v2_1/tenant_usages.py | 92 ++ .../response/compute/v2_1/versions.py | 110 ++ .../response/compute/v2_1/volumes.py | 120 ++ tempest/lib/auth.py | 676 +++++++++++ tempest/lib/base.py | 71 ++ tempest/lib/cli/__init__.py | 0 tempest/lib/cli/base.py | 410 +++++++ tempest/lib/cli/output_parser.py | 170 +++ tempest/lib/cmd/__init__.py | 0 tempest/lib/cmd/check_uuid.py | 358 ++++++ tempest/lib/cmd/skip_tracker.py | 162 +++ tempest/lib/common/__init__.py | 0 tempest/lib/common/http.py | 25 + tempest/lib/common/rest_client.py | 894 ++++++++++++++ tempest/lib/common/ssh.py | 174 +++ tempest/lib/common/utils/__init__.py | 0 tempest/lib/common/utils/data_utils.py | 186 +++ tempest/lib/common/utils/misc.py | 87 ++ tempest/lib/decorators.py | 80 ++ tempest/lib/exceptions.py | 205 ++++ tempest/lib/services/__init__.py | 0 tempest/lib/services/compute/__init__.py | 0 tempest/lib/services/compute/agents_client.py | 63 + .../lib/services/compute/aggregates_client.py | 116 ++ .../compute/availability_zone_client.py | 35 + .../compute/baremetal_nodes_client.py | 42 + .../services/compute/certificates_client.py | 37 + .../lib/services/compute/extensions_client.py | 34 + .../lib/services/compute/fixed_ips_client.py | 40 + .../lib/services/compute/flavors_client.py | 176 +++ .../compute/floating_ip_pools_client.py | 34 + .../compute/floating_ips_bulk_client.py | 50 + .../services/compute/floating_ips_client.py | 103 ++ tempest/lib/services/compute/hosts_client.py | 85 ++ .../lib/services/compute/hypervisor_client.py | 70 ++ tempest/lib/services/compute/images_client.py | 142 +++ .../instance_usage_audit_log_client.py | 38 + .../lib/services/compute/interfaces_client.py | 55 + .../lib/services/compute/keypairs_client.py | 51 + tempest/lib/services/compute/limits_client.py | 28 + .../lib/services/compute/migrations_client.py | 38 + .../lib/services/compute/networks_client.py | 33 + .../services/compute/quota_classes_client.py | 48 + tempest/lib/services/compute/quotas_client.py | 68 ++ .../security_group_default_rules_client.py | 64 + .../compute/security_group_rules_client.py | 43 + .../compute/security_groups_client.py | 89 ++ .../services/compute/server_groups_client.py | 56 + .../lib/services/compute/servers_client.py | 570 +++++++++ .../lib/services/compute/services_client.py | 58 + .../lib/services/compute/snapshots_client.py | 76 ++ .../compute/tenant_networks_client.py | 34 + .../services/compute/tenant_usages_client.py | 43 + .../lib/services/compute/versions_client.py | 55 + .../lib/services/compute/volumes_client.py | 76 ++ tempest/lib/services/identity/__init__.py | 0 tempest/lib/services/identity/v2/__init__.py | 0 .../lib/services/identity/v2/token_client.py | 121 ++ tempest/lib/services/identity/v3/__init__.py | 0 .../lib/services/identity/v3/token_client.py | 183 +++ tempest/lib/services/network/__init__.py | 0 tempest/lib/services/network/agents_client.py | 68 ++ tempest/lib/services/network/base.py | 71 ++ .../lib/services/network/extensions_client.py | 24 + .../services/network/floating_ips_client.py | 38 + .../network/metering_label_rules_client.py | 33 + .../network/metering_labels_client.py | 33 + .../lib/services/network/networks_client.py | 47 + tempest/lib/services/network/ports_client.py | 47 + tempest/lib/services/network/quotas_client.py | 35 + .../network/security_group_rules_client.py | 33 + .../network/security_groups_client.py | 38 + .../services/network/subnetpools_client.py | 40 + .../lib/services/network/subnets_client.py | 47 + tempest/tests/lib/__init__.py | 0 tempest/tests/lib/base.py | 44 + tempest/tests/lib/cli/__init__.py | 0 tempest/tests/lib/cli/test_command_failed.py | 30 + tempest/tests/lib/cli/test_execute.py | 37 + tempest/tests/lib/cli/test_output_parser.py | 177 +++ tempest/tests/lib/common/__init__.py | 0 tempest/tests/lib/common/utils/__init__.py | 0 .../tests/lib/common/utils/test_data_utils.py | 162 +++ tempest/tests/lib/common/utils/test_misc.py | 88 ++ tempest/tests/lib/fake_auth_provider.py | 34 + tempest/tests/lib/fake_credentials.py | 59 + tempest/tests/lib/fake_http.py | 74 ++ tempest/tests/lib/fake_identity.py | 164 +++ tempest/tests/lib/services/__init__.py | 0 .../tests/lib/services/compute/__init__.py | 0 tempest/tests/lib/services/compute/base.py | 45 + .../services/compute/test_agents_client.py | 103 ++ .../compute/test_aggregates_client.py | 192 +++ .../compute/test_availability_zone_client.py | 51 + .../compute/test_baremetal_nodes_client.py | 74 ++ .../compute/test_certificates_client.py | 64 + .../compute/test_extensions_client.py | 65 + .../services/compute/test_fixedIPs_client.py | 58 + .../services/compute/test_flavors_client.py | 255 ++++ .../compute/test_floating_ip_pools_client.py | 46 + .../compute/test_floating_ips_bulk_client.py | 88 ++ .../compute/test_floating_ips_client.py | 113 ++ .../lib/services/compute/test_hosts_client.py | 147 +++ .../compute/test_hypervisor_client.py | 167 +++ .../services/compute/test_images_client.py | 265 ++++ .../test_instance_usage_audit_log_client.py | 73 ++ .../compute/test_interfaces_client.py | 98 ++ .../services/compute/test_keypairs_client.py | 94 ++ .../services/compute/test_limits_client.py | 66 + .../compute/test_migrations_client.py | 52 + .../services/compute/test_networks_client.py | 94 ++ .../compute/test_quota_classes_client.py | 71 ++ .../services/compute/test_quotas_client.py | 130 ++ ...est_security_group_default_rules_client.py | 88 ++ .../test_security_group_rules_client.py | 66 + .../compute/test_security_groups_client.py | 113 ++ .../compute/test_server_groups_client.py | 84 ++ .../services/compute/test_servers_client.py | 1011 ++++++++++++++++ .../services/compute/test_services_client.py | 94 ++ .../services/compute/test_snapshots_client.py | 103 ++ .../compute/test_tenant_networks_client.py | 63 + .../compute/test_tenant_usages_client.py | 79 ++ .../services/compute/test_versions_client.py | 96 ++ .../services/compute/test_volumes_client.py | 114 ++ .../tests/lib/services/identity/__init__.py | 0 .../lib/services/identity/v2/__init__.py | 0 .../services/identity/v2/test_token_client.py | 92 ++ .../lib/services/identity/v3/__init__.py | 0 .../services/identity/v3/test_token_client.py | 145 +++ tempest/tests/lib/test_auth.py | 480 ++++++++ tempest/tests/lib/test_base.py | 64 + tempest/tests/lib/test_credentials.py | 180 +++ tempest/tests/lib/test_decorators.py | 126 ++ tempest/tests/lib/test_rest_client.py | 1065 +++++++++++++++++ tempest/tests/lib/test_ssh.py | 253 ++++ tempest/tests/lib/test_tempest_lib.py | 28 + 170 files changed, 17595 insertions(+), 4 deletions(-) create mode 100644 tempest/lib/__init__.py create mode 100644 tempest/lib/api_schema/__init__.py create mode 100644 tempest/lib/api_schema/response/__init__.py create mode 100644 tempest/lib/api_schema/response/compute/__init__.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/__init__.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/agents.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/aggregates.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/availability_zone.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/baremetal_nodes.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/certificates.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/extensions.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/fixed_ips.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/flavors.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/flavors_access.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/flavors_extra_specs.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/floating_ips.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/hosts.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/hypervisors.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/images.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/instance_usage_audit_logs.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/interfaces.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/keypairs.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/limits.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/migrations.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/parameter_types.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/quota_classes.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/quotas.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/security_group_default_rule.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/security_groups.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/servers.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/services.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/snapshots.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/tenant_networks.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/versions.py create mode 100644 tempest/lib/api_schema/response/compute/v2_1/volumes.py create mode 100644 tempest/lib/auth.py create mode 100644 tempest/lib/base.py create mode 100644 tempest/lib/cli/__init__.py create mode 100644 tempest/lib/cli/base.py create mode 100644 tempest/lib/cli/output_parser.py create mode 100644 tempest/lib/cmd/__init__.py create mode 100755 tempest/lib/cmd/check_uuid.py create mode 100755 tempest/lib/cmd/skip_tracker.py create mode 100644 tempest/lib/common/__init__.py create mode 100644 tempest/lib/common/http.py create mode 100644 tempest/lib/common/rest_client.py create mode 100644 tempest/lib/common/ssh.py create mode 100644 tempest/lib/common/utils/__init__.py create mode 100644 tempest/lib/common/utils/data_utils.py create mode 100644 tempest/lib/common/utils/misc.py create mode 100644 tempest/lib/decorators.py create mode 100644 tempest/lib/exceptions.py create mode 100644 tempest/lib/services/__init__.py create mode 100644 tempest/lib/services/compute/__init__.py create mode 100644 tempest/lib/services/compute/agents_client.py create mode 100644 tempest/lib/services/compute/aggregates_client.py create mode 100644 tempest/lib/services/compute/availability_zone_client.py create mode 100644 tempest/lib/services/compute/baremetal_nodes_client.py create mode 100644 tempest/lib/services/compute/certificates_client.py create mode 100644 tempest/lib/services/compute/extensions_client.py create mode 100644 tempest/lib/services/compute/fixed_ips_client.py create mode 100644 tempest/lib/services/compute/flavors_client.py create mode 100644 tempest/lib/services/compute/floating_ip_pools_client.py create mode 100644 tempest/lib/services/compute/floating_ips_bulk_client.py create mode 100644 tempest/lib/services/compute/floating_ips_client.py create mode 100644 tempest/lib/services/compute/hosts_client.py create mode 100644 tempest/lib/services/compute/hypervisor_client.py create mode 100644 tempest/lib/services/compute/images_client.py create mode 100644 tempest/lib/services/compute/instance_usage_audit_log_client.py create mode 100644 tempest/lib/services/compute/interfaces_client.py create mode 100644 tempest/lib/services/compute/keypairs_client.py create mode 100644 tempest/lib/services/compute/limits_client.py create mode 100644 tempest/lib/services/compute/migrations_client.py create mode 100644 tempest/lib/services/compute/networks_client.py create mode 100644 tempest/lib/services/compute/quota_classes_client.py create mode 100644 tempest/lib/services/compute/quotas_client.py create mode 100644 tempest/lib/services/compute/security_group_default_rules_client.py create mode 100644 tempest/lib/services/compute/security_group_rules_client.py create mode 100644 tempest/lib/services/compute/security_groups_client.py create mode 100644 tempest/lib/services/compute/server_groups_client.py create mode 100644 tempest/lib/services/compute/servers_client.py create mode 100644 tempest/lib/services/compute/services_client.py create mode 100644 tempest/lib/services/compute/snapshots_client.py create mode 100644 tempest/lib/services/compute/tenant_networks_client.py create mode 100644 tempest/lib/services/compute/tenant_usages_client.py create mode 100644 tempest/lib/services/compute/versions_client.py create mode 100644 tempest/lib/services/compute/volumes_client.py create mode 100644 tempest/lib/services/identity/__init__.py create mode 100644 tempest/lib/services/identity/v2/__init__.py create mode 100644 tempest/lib/services/identity/v2/token_client.py create mode 100644 tempest/lib/services/identity/v3/__init__.py create mode 100644 tempest/lib/services/identity/v3/token_client.py create mode 100644 tempest/lib/services/network/__init__.py create mode 100644 tempest/lib/services/network/agents_client.py create mode 100644 tempest/lib/services/network/base.py create mode 100644 tempest/lib/services/network/extensions_client.py create mode 100644 tempest/lib/services/network/floating_ips_client.py create mode 100644 tempest/lib/services/network/metering_label_rules_client.py create mode 100644 tempest/lib/services/network/metering_labels_client.py create mode 100644 tempest/lib/services/network/networks_client.py create mode 100644 tempest/lib/services/network/ports_client.py create mode 100644 tempest/lib/services/network/quotas_client.py create mode 100644 tempest/lib/services/network/security_group_rules_client.py create mode 100644 tempest/lib/services/network/security_groups_client.py create mode 100644 tempest/lib/services/network/subnetpools_client.py create mode 100644 tempest/lib/services/network/subnets_client.py create mode 100644 tempest/tests/lib/__init__.py create mode 100644 tempest/tests/lib/base.py create mode 100644 tempest/tests/lib/cli/__init__.py create mode 100644 tempest/tests/lib/cli/test_command_failed.py create mode 100644 tempest/tests/lib/cli/test_execute.py create mode 100644 tempest/tests/lib/cli/test_output_parser.py create mode 100644 tempest/tests/lib/common/__init__.py create mode 100644 tempest/tests/lib/common/utils/__init__.py create mode 100644 tempest/tests/lib/common/utils/test_data_utils.py create mode 100644 tempest/tests/lib/common/utils/test_misc.py create mode 100644 tempest/tests/lib/fake_auth_provider.py create mode 100644 tempest/tests/lib/fake_credentials.py create mode 100644 tempest/tests/lib/fake_http.py create mode 100644 tempest/tests/lib/fake_identity.py create mode 100644 tempest/tests/lib/services/__init__.py create mode 100644 tempest/tests/lib/services/compute/__init__.py create mode 100644 tempest/tests/lib/services/compute/base.py create mode 100644 tempest/tests/lib/services/compute/test_agents_client.py create mode 100644 tempest/tests/lib/services/compute/test_aggregates_client.py create mode 100644 tempest/tests/lib/services/compute/test_availability_zone_client.py create mode 100644 tempest/tests/lib/services/compute/test_baremetal_nodes_client.py create mode 100644 tempest/tests/lib/services/compute/test_certificates_client.py create mode 100644 tempest/tests/lib/services/compute/test_extensions_client.py create mode 100644 tempest/tests/lib/services/compute/test_fixedIPs_client.py create mode 100644 tempest/tests/lib/services/compute/test_flavors_client.py create mode 100644 tempest/tests/lib/services/compute/test_floating_ip_pools_client.py create mode 100644 tempest/tests/lib/services/compute/test_floating_ips_bulk_client.py create mode 100644 tempest/tests/lib/services/compute/test_floating_ips_client.py create mode 100644 tempest/tests/lib/services/compute/test_hosts_client.py create mode 100644 tempest/tests/lib/services/compute/test_hypervisor_client.py create mode 100644 tempest/tests/lib/services/compute/test_images_client.py create mode 100644 tempest/tests/lib/services/compute/test_instance_usage_audit_log_client.py create mode 100644 tempest/tests/lib/services/compute/test_interfaces_client.py create mode 100644 tempest/tests/lib/services/compute/test_keypairs_client.py create mode 100644 tempest/tests/lib/services/compute/test_limits_client.py create mode 100644 tempest/tests/lib/services/compute/test_migrations_client.py create mode 100644 tempest/tests/lib/services/compute/test_networks_client.py create mode 100644 tempest/tests/lib/services/compute/test_quota_classes_client.py create mode 100644 tempest/tests/lib/services/compute/test_quotas_client.py create mode 100644 tempest/tests/lib/services/compute/test_security_group_default_rules_client.py create mode 100644 tempest/tests/lib/services/compute/test_security_group_rules_client.py create mode 100644 tempest/tests/lib/services/compute/test_security_groups_client.py create mode 100644 tempest/tests/lib/services/compute/test_server_groups_client.py create mode 100644 tempest/tests/lib/services/compute/test_servers_client.py create mode 100644 tempest/tests/lib/services/compute/test_services_client.py create mode 100644 tempest/tests/lib/services/compute/test_snapshots_client.py create mode 100644 tempest/tests/lib/services/compute/test_tenant_networks_client.py create mode 100644 tempest/tests/lib/services/compute/test_tenant_usages_client.py create mode 100644 tempest/tests/lib/services/compute/test_versions_client.py create mode 100644 tempest/tests/lib/services/compute/test_volumes_client.py create mode 100644 tempest/tests/lib/services/identity/__init__.py create mode 100644 tempest/tests/lib/services/identity/v2/__init__.py create mode 100644 tempest/tests/lib/services/identity/v2/test_token_client.py create mode 100644 tempest/tests/lib/services/identity/v3/__init__.py create mode 100644 tempest/tests/lib/services/identity/v3/test_token_client.py create mode 100644 tempest/tests/lib/test_auth.py create mode 100644 tempest/tests/lib/test_base.py create mode 100644 tempest/tests/lib/test_credentials.py create mode 100644 tempest/tests/lib/test_decorators.py create mode 100644 tempest/tests/lib/test_rest_client.py create mode 100644 tempest/tests/lib/test_ssh.py create mode 100644 tempest/tests/lib/test_tempest_lib.py diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py index cd4f50d670..c666c96e21 100644 --- a/tempest/hacking/checks.py +++ b/tempest/hacking/checks.py @@ -69,10 +69,12 @@ def no_setup_teardown_class_for_tests(physical_line, filename): if pep8.noqa(physical_line): return - if 'tempest/test.py' not in filename: - if SETUP_TEARDOWN_CLASS_DEFINITION.match(physical_line): - return (physical_line.find('def'), - "T105: (setUp|tearDown)Class can not be used in tests") + if 'tempest/test.py' in filename or 'tempest/lib/' in filename: + return + + if SETUP_TEARDOWN_CLASS_DEFINITION.match(physical_line): + return (physical_line.find('def'), + "T105: (setUp|tearDown)Class can not be used in tests") def no_vi_headers(physical_line, line_number, lines): diff --git a/tempest/lib/__init__.py b/tempest/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/api_schema/__init__.py b/tempest/lib/api_schema/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/api_schema/response/__init__.py b/tempest/lib/api_schema/response/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/api_schema/response/compute/__init__.py b/tempest/lib/api_schema/response/compute/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/api_schema/response/compute/v2_1/__init__.py b/tempest/lib/api_schema/response/compute/v2_1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/api_schema/response/compute/v2_1/agents.py b/tempest/lib/api_schema/response/compute/v2_1/agents.py new file mode 100644 index 0000000000..6f712b41ef --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/agents.py @@ -0,0 +1,82 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +common_agent_info = { + 'type': 'object', + 'properties': { + 'agent_id': {'type': ['integer', 'string']}, + 'hypervisor': {'type': 'string'}, + 'os': {'type': 'string'}, + 'architecture': {'type': 'string'}, + 'version': {'type': 'string'}, + 'url': {'type': 'string', 'format': 'uri'}, + 'md5hash': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['agent_id', 'hypervisor', 'os', 'architecture', + 'version', 'url', 'md5hash'] +} + +list_agents = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'agents': { + 'type': 'array', + 'items': common_agent_info + } + }, + 'additionalProperties': False, + 'required': ['agents'] + } +} + +create_agent = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'agent': common_agent_info + }, + 'additionalProperties': False, + 'required': ['agent'] + } +} + +update_agent = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'agent': { + 'type': 'object', + 'properties': { + 'agent_id': {'type': ['integer', 'string']}, + 'version': {'type': 'string'}, + 'url': {'type': 'string', 'format': 'uri'}, + 'md5hash': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['agent_id', 'version', 'url', 'md5hash'] + } + }, + 'additionalProperties': False, + 'required': ['agent'] + } +} + +delete_agent = { + 'status_code': [200] +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/aggregates.py b/tempest/lib/api_schema/response/compute/v2_1/aggregates.py new file mode 100644 index 0000000000..1a9fe41cdd --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/aggregates.py @@ -0,0 +1,92 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +# create-aggregate api doesn't have 'hosts' and 'metadata' attributes. +aggregate_for_create = { + 'type': 'object', + 'properties': { + 'availability_zone': {'type': ['string', 'null']}, + 'created_at': {'type': 'string'}, + 'deleted': {'type': 'boolean'}, + 'deleted_at': {'type': ['string', 'null']}, + 'id': {'type': 'integer'}, + 'name': {'type': 'string'}, + 'updated_at': {'type': ['string', 'null']} + }, + 'additionalProperties': False, + 'required': ['availability_zone', 'created_at', 'deleted', + 'deleted_at', 'id', 'name', 'updated_at'], +} + +common_aggregate_info = copy.deepcopy(aggregate_for_create) +common_aggregate_info['properties'].update({ + 'hosts': {'type': 'array'}, + 'metadata': {'type': 'object'} +}) +common_aggregate_info['required'].extend(['hosts', 'metadata']) + +list_aggregates = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'aggregates': { + 'type': 'array', + 'items': common_aggregate_info + } + }, + 'additionalProperties': False, + 'required': ['aggregates'], + } +} + +get_aggregate = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'aggregate': common_aggregate_info + }, + 'additionalProperties': False, + 'required': ['aggregate'], + } +} + +aggregate_set_metadata = get_aggregate +# The 'updated_at' attribute of 'update_aggregate' can't be null. +update_aggregate = copy.deepcopy(get_aggregate) +update_aggregate['response_body']['properties']['aggregate']['properties'][ + 'updated_at'] = { + 'type': 'string' + } + +delete_aggregate = { + 'status_code': [200] +} + +create_aggregate = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'aggregate': aggregate_for_create + }, + 'additionalProperties': False, + 'required': ['aggregate'], + } +} + +aggregate_add_remove_host = get_aggregate diff --git a/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py b/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py new file mode 100644 index 0000000000..d9aebce7b9 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py @@ -0,0 +1,78 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + + +base = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'availabilityZoneInfo': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'zoneName': {'type': 'string'}, + 'zoneState': { + 'type': 'object', + 'properties': { + 'available': {'type': 'boolean'} + }, + 'additionalProperties': False, + 'required': ['available'] + }, + # NOTE: Here is the difference between detail and + # non-detail. + 'hosts': {'type': 'null'} + }, + 'additionalProperties': False, + 'required': ['zoneName', 'zoneState', 'hosts'] + } + } + }, + 'additionalProperties': False, + 'required': ['availabilityZoneInfo'] + } +} + +detail = { + 'type': 'object', + 'patternProperties': { + # NOTE: Here is for a hostname + '^[a-zA-Z0-9-_.]+$': { + 'type': 'object', + 'patternProperties': { + # NOTE: Here is for a service name + '^.*$': { + 'type': 'object', + 'properties': { + 'available': {'type': 'boolean'}, + 'active': {'type': 'boolean'}, + 'updated_at': {'type': ['string', 'null']} + }, + 'additionalProperties': False, + 'required': ['available', 'active', 'updated_at'] + } + } + } + } +} + +list_availability_zone_list = copy.deepcopy(base) + +list_availability_zone_list_detail = copy.deepcopy(base) +list_availability_zone_list_detail['response_body']['properties'][ + 'availabilityZoneInfo']['items']['properties']['hosts'] = detail diff --git a/tempest/lib/api_schema/response/compute/v2_1/baremetal_nodes.py b/tempest/lib/api_schema/response/compute/v2_1/baremetal_nodes.py new file mode 100644 index 0000000000..d1ee87728d --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/baremetal_nodes.py @@ -0,0 +1,63 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +node = { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'interfaces': {'type': 'array'}, + 'host': {'type': 'string'}, + 'task_state': {'type': ['string', 'null']}, + 'cpus': {'type': ['integer', 'string']}, + 'memory_mb': {'type': ['integer', 'string']}, + 'disk_gb': {'type': ['integer', 'string']}, + }, + 'additionalProperties': False, + 'required': ['id', 'interfaces', 'host', 'task_state', 'cpus', 'memory_mb', + 'disk_gb'] +} + +list_baremetal_nodes = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'nodes': { + 'type': 'array', + 'items': node + } + }, + 'additionalProperties': False, + 'required': ['nodes'] + } +} + +baremetal_node = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'node': node + }, + 'additionalProperties': False, + 'required': ['node'] + } +} +get_baremetal_node = copy.deepcopy(baremetal_node) +get_baremetal_node['response_body']['properties']['node'][ + 'properties'].update({'instance_uuid': {'type': ['string', 'null']}}) +get_baremetal_node['response_body']['properties']['node'][ + 'required'].append('instance_uuid') diff --git a/tempest/lib/api_schema/response/compute/v2_1/certificates.py b/tempest/lib/api_schema/response/compute/v2_1/certificates.py new file mode 100644 index 0000000000..4e7cbe4d20 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/certificates.py @@ -0,0 +1,41 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +_common_schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'certificate': { + 'type': 'object', + 'properties': { + 'data': {'type': 'string'}, + 'private_key': {'type': 'string'}, + }, + 'additionalProperties': False, + 'required': ['data', 'private_key'] + } + }, + 'additionalProperties': False, + 'required': ['certificate'] + } +} + +get_certificate = copy.deepcopy(_common_schema) +get_certificate['response_body']['properties']['certificate'][ + 'properties']['private_key'].update({'type': 'null'}) + +create_certificate = copy.deepcopy(_common_schema) diff --git a/tempest/lib/api_schema/response/compute/v2_1/extensions.py b/tempest/lib/api_schema/response/compute/v2_1/extensions.py new file mode 100644 index 0000000000..a6a455c1b8 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/extensions.py @@ -0,0 +1,47 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +list_extensions = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'extensions': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'updated': { + 'type': 'string', + 'format': 'data-time' + }, + 'name': {'type': 'string'}, + 'links': {'type': 'array'}, + 'namespace': { + 'type': 'string', + 'format': 'uri' + }, + 'alias': {'type': 'string'}, + 'description': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['updated', 'name', 'links', 'namespace', + 'alias', 'description'] + } + } + }, + 'additionalProperties': False, + 'required': ['extensions'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/fixed_ips.py b/tempest/lib/api_schema/response/compute/v2_1/fixed_ips.py new file mode 100644 index 0000000000..a653213f05 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/fixed_ips.py @@ -0,0 +1,41 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.api_schema.response.compute.v2_1 import parameter_types + +get_fixed_ip = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'fixed_ip': { + 'type': 'object', + 'properties': { + 'address': parameter_types.ip_address, + 'cidr': {'type': 'string'}, + 'host': {'type': 'string'}, + 'hostname': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['address', 'cidr', 'host', 'hostname'] + } + }, + 'additionalProperties': False, + 'required': ['fixed_ip'] + } +} + +reserve_unreserve_fixed_ip = { + 'status_code': [202] +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/flavors.py b/tempest/lib/api_schema/response/compute/v2_1/flavors.py new file mode 100644 index 0000000000..547d94d572 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/flavors.py @@ -0,0 +1,103 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.api_schema.response.compute.v2_1 import parameter_types + +list_flavors = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'flavors': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'links': parameter_types.links, + 'id': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['name', 'links', 'id'] + } + }, + 'flavors_links': parameter_types.links + }, + 'additionalProperties': False, + # NOTE(gmann): flavors_links attribute is not necessary + # to be present always So it is not 'required'. + 'required': ['flavors'] + } +} + +common_flavor_info = { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'links': parameter_types.links, + 'ram': {'type': 'integer'}, + 'vcpus': {'type': 'integer'}, + # 'swap' attributes comes as integer value but if it is empty + # it comes as "". So defining type of as string and integer. + 'swap': {'type': ['integer', 'string']}, + 'disk': {'type': 'integer'}, + 'id': {'type': 'string'}, + 'OS-FLV-DISABLED:disabled': {'type': 'boolean'}, + 'os-flavor-access:is_public': {'type': 'boolean'}, + 'rxtx_factor': {'type': 'number'}, + 'OS-FLV-EXT-DATA:ephemeral': {'type': 'integer'} + }, + 'additionalProperties': False, + # 'OS-FLV-DISABLED', 'os-flavor-access', 'rxtx_factor' and + # 'OS-FLV-EXT-DATA' are API extensions. So they are not 'required'. + 'required': ['name', 'links', 'ram', 'vcpus', 'swap', 'disk', 'id'] +} + +list_flavors_details = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'flavors': { + 'type': 'array', + 'items': common_flavor_info + }, + # NOTE(gmann): flavors_links attribute is not necessary + # to be present always So it is not 'required'. + 'flavors_links': parameter_types.links + }, + 'additionalProperties': False, + 'required': ['flavors'] + } +} + +unset_flavor_extra_specs = { + 'status_code': [200] +} + +create_get_flavor_details = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'flavor': common_flavor_info + }, + 'additionalProperties': False, + 'required': ['flavor'] + } +} + +delete_flavor = { + 'status_code': [202] +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/flavors_access.py b/tempest/lib/api_schema/response/compute/v2_1/flavors_access.py new file mode 100644 index 0000000000..a4d6af0d75 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/flavors_access.py @@ -0,0 +1,36 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +add_remove_list_flavor_access = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'flavor_access': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'flavor_id': {'type': 'string'}, + 'tenant_id': {'type': 'string'}, + }, + 'additionalProperties': False, + 'required': ['flavor_id', 'tenant_id'], + } + } + }, + 'additionalProperties': False, + 'required': ['flavor_access'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/flavors_extra_specs.py b/tempest/lib/api_schema/response/compute/v2_1/flavors_extra_specs.py new file mode 100644 index 0000000000..a438d48694 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/flavors_extra_specs.py @@ -0,0 +1,40 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +set_get_flavor_extra_specs = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'extra_specs': { + 'type': 'object', + 'patternProperties': { + '^[a-zA-Z0-9_\-\. :]+$': {'type': 'string'} + } + } + }, + 'additionalProperties': False, + 'required': ['extra_specs'] + } +} + +set_get_flavor_extra_specs_key = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'patternProperties': { + '^[a-zA-Z0-9_\-\. :]+$': {'type': 'string'} + } + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/floating_ips.py b/tempest/lib/api_schema/response/compute/v2_1/floating_ips.py new file mode 100644 index 0000000000..0c665905fe --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/floating_ips.py @@ -0,0 +1,148 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.api_schema.response.compute.v2_1 import parameter_types + +common_floating_ip_info = { + 'type': 'object', + 'properties': { + # NOTE: Now the type of 'id' is integer, but + # here allows 'string' also because we will be + # able to change it to 'uuid' in the future. + 'id': {'type': ['integer', 'string']}, + 'pool': {'type': ['string', 'null']}, + 'instance_id': {'type': ['string', 'null']}, + 'ip': parameter_types.ip_address, + 'fixed_ip': parameter_types.ip_address + }, + 'additionalProperties': False, + 'required': ['id', 'pool', 'instance_id', + 'ip', 'fixed_ip'], + +} +list_floating_ips = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'floating_ips': { + 'type': 'array', + 'items': common_floating_ip_info + }, + }, + 'additionalProperties': False, + 'required': ['floating_ips'], + } +} + +create_get_floating_ip = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'floating_ip': common_floating_ip_info + }, + 'additionalProperties': False, + 'required': ['floating_ip'], + } +} + +list_floating_ip_pools = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'floating_ip_pools': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['name'], + } + } + }, + 'additionalProperties': False, + 'required': ['floating_ip_pools'], + } +} + +add_remove_floating_ip = { + 'status_code': [202] +} + +create_floating_ips_bulk = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'floating_ips_bulk_create': { + 'type': 'object', + 'properties': { + 'interface': {'type': ['string', 'null']}, + 'ip_range': {'type': 'string'}, + 'pool': {'type': ['string', 'null']}, + }, + 'additionalProperties': False, + 'required': ['interface', 'ip_range', 'pool'], + } + }, + 'additionalProperties': False, + 'required': ['floating_ips_bulk_create'], + } +} + +delete_floating_ips_bulk = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'floating_ips_bulk_delete': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['floating_ips_bulk_delete'], + } +} + +list_floating_ips_bulk = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'floating_ip_info': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'address': parameter_types.ip_address, + 'instance_uuid': {'type': ['string', 'null']}, + 'interface': {'type': ['string', 'null']}, + 'pool': {'type': ['string', 'null']}, + 'project_id': {'type': ['string', 'null']}, + 'fixed_ip': parameter_types.ip_address + }, + 'additionalProperties': False, + # NOTE: fixed_ip is introduced after JUNO release, + # So it is not defined as 'required'. + 'required': ['address', 'instance_uuid', 'interface', + 'pool', 'project_id'], + } + } + }, + 'additionalProperties': False, + 'required': ['floating_ip_info'], + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/hosts.py b/tempest/lib/api_schema/response/compute/v2_1/hosts.py new file mode 100644 index 0000000000..ae70ff1fdb --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/hosts.py @@ -0,0 +1,116 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + + +list_hosts = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'hosts': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'host_name': {'type': 'string'}, + 'service': {'type': 'string'}, + 'zone': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['host_name', 'service', 'zone'] + } + } + }, + 'additionalProperties': False, + 'required': ['hosts'] + } +} + +get_host_detail = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'host': { + 'type': 'array', + 'item': { + 'type': 'object', + 'properties': { + 'resource': { + 'type': 'object', + 'properties': { + 'cpu': {'type': 'integer'}, + 'disk_gb': {'type': 'integer'}, + 'host': {'type': 'string'}, + 'memory_mb': {'type': 'integer'}, + 'project': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['cpu', 'disk_gb', 'host', + 'memory_mb', 'project'] + } + }, + 'additionalProperties': False, + 'required': ['resource'] + } + } + }, + 'additionalProperties': False, + 'required': ['host'] + } +} + +startup_host = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'host': {'type': 'string'}, + 'power_action': {'enum': ['startup']} + }, + 'additionalProperties': False, + 'required': ['host', 'power_action'] + } +} + +# The 'power_action' attribute of 'shutdown_host' API is 'shutdown' +shutdown_host = copy.deepcopy(startup_host) + +shutdown_host['response_body']['properties']['power_action'] = { + 'enum': ['shutdown'] +} + +# The 'power_action' attribute of 'reboot_host' API is 'reboot' +reboot_host = copy.deepcopy(startup_host) + +reboot_host['response_body']['properties']['power_action'] = { + 'enum': ['reboot'] +} + +update_host = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'host': {'type': 'string'}, + 'maintenance_mode': {'enum': ['on_maintenance', + 'off_maintenance']}, + 'status': {'enum': ['enabled', 'disabled']} + }, + 'additionalProperties': False, + 'required': ['host', 'maintenance_mode', 'status'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/hypervisors.py b/tempest/lib/api_schema/response/compute/v2_1/hypervisors.py new file mode 100644 index 0000000000..d15b4f66a0 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/hypervisors.py @@ -0,0 +1,195 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.api_schema.response.compute.v2_1 import parameter_types + +get_hypervisor_statistics = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'hypervisor_statistics': { + 'type': 'object', + 'properties': { + 'count': {'type': 'integer'}, + 'current_workload': {'type': 'integer'}, + 'disk_available_least': {'type': ['integer', 'null']}, + 'free_disk_gb': {'type': 'integer'}, + 'free_ram_mb': {'type': 'integer'}, + 'local_gb': {'type': 'integer'}, + 'local_gb_used': {'type': 'integer'}, + 'memory_mb': {'type': 'integer'}, + 'memory_mb_used': {'type': 'integer'}, + 'running_vms': {'type': 'integer'}, + 'vcpus': {'type': 'integer'}, + 'vcpus_used': {'type': 'integer'} + }, + 'additionalProperties': False, + 'required': ['count', 'current_workload', + 'disk_available_least', 'free_disk_gb', + 'free_ram_mb', 'local_gb', 'local_gb_used', + 'memory_mb', 'memory_mb_used', 'running_vms', + 'vcpus', 'vcpus_used'] + } + }, + 'additionalProperties': False, + 'required': ['hypervisor_statistics'] + } +} + + +hypervisor_detail = { + 'type': 'object', + 'properties': { + 'status': {'type': 'string'}, + 'state': {'type': 'string'}, + 'cpu_info': {'type': 'string'}, + 'current_workload': {'type': 'integer'}, + 'disk_available_least': {'type': ['integer', 'null']}, + 'host_ip': parameter_types.ip_address, + 'free_disk_gb': {'type': 'integer'}, + 'free_ram_mb': {'type': 'integer'}, + 'hypervisor_hostname': {'type': 'string'}, + 'hypervisor_type': {'type': 'string'}, + 'hypervisor_version': {'type': 'integer'}, + 'id': {'type': ['integer', 'string']}, + 'local_gb': {'type': 'integer'}, + 'local_gb_used': {'type': 'integer'}, + 'memory_mb': {'type': 'integer'}, + 'memory_mb_used': {'type': 'integer'}, + 'running_vms': {'type': 'integer'}, + 'service': { + 'type': 'object', + 'properties': { + 'host': {'type': 'string'}, + 'id': {'type': ['integer', 'string']}, + 'disabled_reason': {'type': ['string', 'null']} + }, + 'additionalProperties': False, + 'required': ['host', 'id'] + }, + 'vcpus': {'type': 'integer'}, + 'vcpus_used': {'type': 'integer'} + }, + 'additionalProperties': False, + # NOTE: When loading os-hypervisor-status extension, + # a response contains status and state. So these params + # should not be required. + 'required': ['cpu_info', 'current_workload', + 'disk_available_least', 'host_ip', + 'free_disk_gb', 'free_ram_mb', + 'hypervisor_hostname', 'hypervisor_type', + 'hypervisor_version', 'id', 'local_gb', + 'local_gb_used', 'memory_mb', 'memory_mb_used', + 'running_vms', 'service', 'vcpus', 'vcpus_used'] +} + +list_hypervisors_detail = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'hypervisors': { + 'type': 'array', + 'items': hypervisor_detail + } + }, + 'additionalProperties': False, + 'required': ['hypervisors'] + } +} + +get_hypervisor = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'hypervisor': hypervisor_detail + }, + 'additionalProperties': False, + 'required': ['hypervisor'] + } +} + +list_search_hypervisors = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'hypervisors': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'status': {'type': 'string'}, + 'state': {'type': 'string'}, + 'id': {'type': ['integer', 'string']}, + 'hypervisor_hostname': {'type': 'string'} + }, + 'additionalProperties': False, + # NOTE: When loading os-hypervisor-status extension, + # a response contains status and state. So these params + # should not be required. + 'required': ['id', 'hypervisor_hostname'] + } + } + }, + 'additionalProperties': False, + 'required': ['hypervisors'] + } +} + +get_hypervisor_uptime = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'hypervisor': { + 'type': 'object', + 'properties': { + 'status': {'type': 'string'}, + 'state': {'type': 'string'}, + 'id': {'type': ['integer', 'string']}, + 'hypervisor_hostname': {'type': 'string'}, + 'uptime': {'type': 'string'} + }, + 'additionalProperties': False, + # NOTE: When loading os-hypervisor-status extension, + # a response contains status and state. So these params + # should not be required. + 'required': ['id', 'hypervisor_hostname', 'uptime'] + } + }, + 'additionalProperties': False, + 'required': ['hypervisor'] + } +} + +get_hypervisors_servers = copy.deepcopy(list_search_hypervisors) +get_hypervisors_servers['response_body']['properties']['hypervisors']['items'][ + 'properties']['servers'] = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'uuid': {'type': 'string'}, + 'name': {'type': 'string'} + }, + 'additionalProperties': False, + } + } +# In V2 API, if there is no servers (VM) on the Hypervisor host then 'servers' +# attribute will not be present in response body So it is not 'required'. diff --git a/tempest/lib/api_schema/response/compute/v2_1/images.py b/tempest/lib/api_schema/response/compute/v2_1/images.py new file mode 100644 index 0000000000..daab898822 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/images.py @@ -0,0 +1,154 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.api_schema.response.compute.v2_1 import parameter_types + +image_links = copy.deepcopy(parameter_types.links) +image_links['items']['properties'].update({'type': {'type': 'string'}}) + +common_image_schema = { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'status': {'type': 'string'}, + 'updated': {'type': 'string'}, + 'links': image_links, + 'name': {'type': ['string', 'null']}, + 'created': {'type': 'string'}, + 'minDisk': {'type': 'integer'}, + 'minRam': {'type': 'integer'}, + 'progress': {'type': 'integer'}, + 'metadata': {'type': 'object'}, + 'server': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'links': parameter_types.links + }, + 'additionalProperties': False, + 'required': ['id', 'links'] + }, + 'OS-EXT-IMG-SIZE:size': {'type': ['integer', 'null']}, + 'OS-DCF:diskConfig': {'type': 'string'} + }, + 'additionalProperties': False, + # 'server' attributes only comes in response body if image is + # associated with any server. 'OS-EXT-IMG-SIZE:size' & 'OS-DCF:diskConfig' + # are API extension, So those are not defined as 'required'. + 'required': ['id', 'status', 'updated', 'links', 'name', + 'created', 'minDisk', 'minRam', 'progress', + 'metadata'] +} + +get_image = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'image': common_image_schema + }, + 'additionalProperties': False, + 'required': ['image'] + } +} + +list_images = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'images': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'links': image_links, + 'name': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['id', 'links', 'name'] + } + }, + 'images_links': parameter_types.links + }, + 'additionalProperties': False, + # NOTE(gmann): images_links attribute is not necessary to be + # present always So it is not 'required'. + 'required': ['images'] + } +} + +create_image = { + 'status_code': [202], + 'response_header': { + 'type': 'object', + 'properties': parameter_types.response_header + } +} +create_image['response_header']['properties'].update( + {'location': { + 'type': 'string', + 'format': 'uri'} + } +) +create_image['response_header']['required'] = ['location'] + +delete = { + 'status_code': [204] +} + +image_metadata = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'metadata': {'type': 'object'} + }, + 'additionalProperties': False, + 'required': ['metadata'] + } +} + +image_meta_item = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'meta': {'type': 'object'} + }, + 'additionalProperties': False, + 'required': ['meta'] + } +} + +list_images_details = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'images': { + 'type': 'array', + 'items': common_image_schema + }, + 'images_links': parameter_types.links + }, + 'additionalProperties': False, + # NOTE(gmann): images_links attribute is not necessary to be + # present always So it is not 'required'. + 'required': ['images'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/instance_usage_audit_logs.py b/tempest/lib/api_schema/response/compute/v2_1/instance_usage_audit_logs.py new file mode 100644 index 0000000000..c6c4debd85 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/instance_usage_audit_logs.py @@ -0,0 +1,62 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +common_instance_usage_audit_log = { + 'type': 'object', + 'properties': { + 'hosts_not_run': { + 'type': 'array', + 'items': {'type': 'string'} + }, + 'log': {'type': 'object'}, + 'num_hosts': {'type': 'integer'}, + 'num_hosts_done': {'type': 'integer'}, + 'num_hosts_not_run': {'type': 'integer'}, + 'num_hosts_running': {'type': 'integer'}, + 'overall_status': {'type': 'string'}, + 'period_beginning': {'type': 'string'}, + 'period_ending': {'type': 'string'}, + 'total_errors': {'type': 'integer'}, + 'total_instances': {'type': 'integer'} + }, + 'additionalProperties': False, + 'required': ['hosts_not_run', 'log', 'num_hosts', 'num_hosts_done', + 'num_hosts_not_run', 'num_hosts_running', 'overall_status', + 'period_beginning', 'period_ending', 'total_errors', + 'total_instances'] +} + +get_instance_usage_audit_log = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'instance_usage_audit_log': common_instance_usage_audit_log + }, + 'additionalProperties': False, + 'required': ['instance_usage_audit_log'] + } +} + +list_instance_usage_audit_log = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'instance_usage_audit_logs': common_instance_usage_audit_log + }, + 'additionalProperties': False, + 'required': ['instance_usage_audit_logs'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/interfaces.py b/tempest/lib/api_schema/response/compute/v2_1/interfaces.py new file mode 100644 index 0000000000..99847502c0 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/interfaces.py @@ -0,0 +1,73 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.api_schema.response.compute.v2_1 import parameter_types + +interface_common_info = { + 'type': 'object', + 'properties': { + 'port_state': {'type': 'string'}, + 'fixed_ips': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'subnet_id': { + 'type': 'string', + 'format': 'uuid' + }, + 'ip_address': parameter_types.ip_address + }, + 'additionalProperties': False, + 'required': ['subnet_id', 'ip_address'] + } + }, + 'port_id': {'type': 'string', 'format': 'uuid'}, + 'net_id': {'type': 'string', 'format': 'uuid'}, + 'mac_addr': parameter_types.mac_address + }, + 'additionalProperties': False, + 'required': ['port_state', 'fixed_ips', 'port_id', 'net_id', 'mac_addr'] +} + +get_create_interfaces = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'interfaceAttachment': interface_common_info + }, + 'additionalProperties': False, + 'required': ['interfaceAttachment'] + } +} + +list_interfaces = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'interfaceAttachments': { + 'type': 'array', + 'items': interface_common_info + } + }, + 'additionalProperties': False, + 'required': ['interfaceAttachments'] + } +} + +delete_interface = { + 'status_code': [202] +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/keypairs.py b/tempest/lib/api_schema/response/compute/v2_1/keypairs.py new file mode 100644 index 0000000000..9c04c79b49 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/keypairs.py @@ -0,0 +1,107 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +get_keypair = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'keypair': { + 'type': 'object', + 'properties': { + 'public_key': {'type': 'string'}, + 'name': {'type': 'string'}, + 'fingerprint': {'type': 'string'}, + 'user_id': {'type': 'string'}, + 'deleted': {'type': 'boolean'}, + 'created_at': {'type': 'string'}, + 'updated_at': {'type': ['string', 'null']}, + 'deleted_at': {'type': ['string', 'null']}, + 'id': {'type': 'integer'} + + }, + 'additionalProperties': False, + # When we run the get keypair API, response body includes + # all the above mentioned attributes. + # But in Nova API sample file, response body includes only + # 'public_key', 'name' & 'fingerprint'. So only 'public_key', + # 'name' & 'fingerprint' are defined as 'required'. + 'required': ['public_key', 'name', 'fingerprint'] + } + }, + 'additionalProperties': False, + 'required': ['keypair'] + } +} + +create_keypair = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'keypair': { + 'type': 'object', + 'properties': { + 'fingerprint': {'type': 'string'}, + 'name': {'type': 'string'}, + 'public_key': {'type': 'string'}, + 'user_id': {'type': 'string'}, + 'private_key': {'type': 'string'} + }, + 'additionalProperties': False, + # When create keypair API is being called with 'Public key' + # (Importing keypair) then, response body does not contain + # 'private_key' So it is not defined as 'required' + 'required': ['fingerprint', 'name', 'public_key', 'user_id'] + } + }, + 'additionalProperties': False, + 'required': ['keypair'] + } +} + +delete_keypair = { + 'status_code': [202], +} + +list_keypairs = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'keypairs': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'keypair': { + 'type': 'object', + 'properties': { + 'public_key': {'type': 'string'}, + 'name': {'type': 'string'}, + 'fingerprint': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['public_key', 'name', 'fingerprint'] + } + }, + 'additionalProperties': False, + 'required': ['keypair'] + } + } + }, + 'additionalProperties': False, + 'required': ['keypairs'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/limits.py b/tempest/lib/api_schema/response/compute/v2_1/limits.py new file mode 100644 index 0000000000..81f175fa7b --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/limits.py @@ -0,0 +1,106 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +get_limit = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'limits': { + 'type': 'object', + 'properties': { + 'absolute': { + 'type': 'object', + 'properties': { + 'maxTotalRAMSize': {'type': 'integer'}, + 'totalCoresUsed': {'type': 'integer'}, + 'maxTotalInstances': {'type': 'integer'}, + 'maxTotalFloatingIps': {'type': 'integer'}, + 'totalSecurityGroupsUsed': {'type': 'integer'}, + 'maxTotalCores': {'type': 'integer'}, + 'totalFloatingIpsUsed': {'type': 'integer'}, + 'maxSecurityGroups': {'type': 'integer'}, + 'maxServerMeta': {'type': 'integer'}, + 'maxPersonality': {'type': 'integer'}, + 'maxImageMeta': {'type': 'integer'}, + 'maxPersonalitySize': {'type': 'integer'}, + 'maxSecurityGroupRules': {'type': 'integer'}, + 'maxTotalKeypairs': {'type': 'integer'}, + 'totalRAMUsed': {'type': 'integer'}, + 'totalInstancesUsed': {'type': 'integer'}, + 'maxServerGroupMembers': {'type': 'integer'}, + 'maxServerGroups': {'type': 'integer'}, + 'totalServerGroupsUsed': {'type': 'integer'} + }, + 'additionalProperties': False, + # NOTE(gmann): maxServerGroupMembers, maxServerGroups + # and totalServerGroupsUsed are API extension, + # and some environments return a response without these + # attributes.So they are not 'required'. + 'required': ['maxImageMeta', + 'maxPersonality', + 'maxPersonalitySize', + 'maxSecurityGroupRules', + 'maxSecurityGroups', + 'maxServerMeta', + 'maxTotalCores', + 'maxTotalFloatingIps', + 'maxTotalInstances', + 'maxTotalKeypairs', + 'maxTotalRAMSize', + 'totalCoresUsed', + 'totalFloatingIpsUsed', + 'totalInstancesUsed', + 'totalRAMUsed', + 'totalSecurityGroupsUsed'] + }, + 'rate': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'limit': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'next-available': + {'type': 'string'}, + 'remaining': + {'type': 'integer'}, + 'unit': + {'type': 'string'}, + 'value': + {'type': 'integer'}, + 'verb': + {'type': 'string'} + }, + 'additionalProperties': False, + } + }, + 'regex': {'type': 'string'}, + 'uri': {'type': 'string'} + }, + 'additionalProperties': False, + } + } + }, + 'additionalProperties': False, + 'required': ['absolute', 'rate'] + } + }, + 'additionalProperties': False, + 'required': ['limits'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/migrations.py b/tempest/lib/api_schema/response/compute/v2_1/migrations.py new file mode 100644 index 0000000000..b7d66ea486 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/migrations.py @@ -0,0 +1,51 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +list_migrations = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'migrations': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'integer'}, + 'status': {'type': ['string', 'null']}, + 'instance_uuid': {'type': ['string', 'null']}, + 'source_node': {'type': ['string', 'null']}, + 'source_compute': {'type': ['string', 'null']}, + 'dest_node': {'type': ['string', 'null']}, + 'dest_compute': {'type': ['string', 'null']}, + 'dest_host': {'type': ['string', 'null']}, + 'old_instance_type_id': {'type': ['integer', 'null']}, + 'new_instance_type_id': {'type': ['integer', 'null']}, + 'created_at': {'type': 'string'}, + 'updated_at': {'type': ['string', 'null']} + }, + 'additionalProperties': False, + 'required': [ + 'id', 'status', 'instance_uuid', 'source_node', + 'source_compute', 'dest_node', 'dest_compute', + 'dest_host', 'old_instance_type_id', + 'new_instance_type_id', 'created_at', 'updated_at' + ] + } + } + }, + 'additionalProperties': False, + 'required': ['migrations'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py new file mode 100644 index 0000000000..07cc890a12 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py @@ -0,0 +1,96 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +links = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'href': { + 'type': 'string', + 'format': 'uri' + }, + 'rel': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['href', 'rel'] + } +} + +mac_address = { + 'type': 'string', + 'pattern': '(?:[a-f0-9]{2}:){5}[a-f0-9]{2}' +} + +ip_address = { + 'oneOf': [ + { + 'type': 'string', + 'oneOf': [ + {'format': 'ipv4'}, + {'format': 'ipv6'} + ] + }, + {'type': 'null'} + ] +} + +access_ip_v4 = { + 'type': 'string', + 'oneOf': [{'format': 'ipv4'}, {'enum': ['']}] +} + +access_ip_v6 = { + 'type': 'string', + 'oneOf': [{'format': 'ipv6'}, {'enum': ['']}] +} + +addresses = { + 'type': 'object', + 'patternProperties': { + # NOTE: Here is for 'private' or something. + '^[a-zA-Z0-9-_.]+$': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'version': {'type': 'integer'}, + 'addr': { + 'type': 'string', + 'oneOf': [ + {'format': 'ipv4'}, + {'format': 'ipv6'} + ] + } + }, + 'additionalProperties': False, + 'required': ['version', 'addr'] + } + } + } +} + +response_header = { + 'connection': {'type': 'string'}, + 'content-length': {'type': 'string'}, + 'content-type': {'type': 'string'}, + 'status': {'type': 'string'}, + 'x-compute-request-id': {'type': 'string'}, + 'vary': {'type': 'string'}, + 'x-openstack-nova-api-version': {'type': 'string'}, + 'date': { + 'type': 'string', + 'format': 'data-time' + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/quota_classes.py b/tempest/lib/api_schema/response/compute/v2_1/quota_classes.py new file mode 100644 index 0000000000..03d7f12814 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/quota_classes.py @@ -0,0 +1,31 @@ +# Copyright 2014 IBM Corporation. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.api_schema.response.compute.v2_1 import quotas + +# NOTE(mriedem): os-quota-class-sets responses are the same as os-quota-sets +# except for the key in the response body is quota_class_set instead of +# quota_set, so update this copy of the schema from os-quota-sets. +get_quota_class_set = copy.deepcopy(quotas.get_quota_set) +get_quota_class_set['response_body']['properties']['quota_class_set'] = ( + get_quota_class_set['response_body']['properties'].pop('quota_set')) +get_quota_class_set['response_body']['required'] = ['quota_class_set'] + +update_quota_class_set = copy.deepcopy(quotas.update_quota_set) +update_quota_class_set['response_body']['properties']['quota_class_set'] = ( + update_quota_class_set['response_body']['properties'].pop('quota_set')) +update_quota_class_set['response_body']['required'] = ['quota_class_set'] diff --git a/tempest/lib/api_schema/response/compute/v2_1/quotas.py b/tempest/lib/api_schema/response/compute/v2_1/quotas.py new file mode 100644 index 0000000000..7953983cf2 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/quotas.py @@ -0,0 +1,65 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +update_quota_set = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'quota_set': { + 'type': 'object', + 'properties': { + 'instances': {'type': 'integer'}, + 'cores': {'type': 'integer'}, + 'ram': {'type': 'integer'}, + 'floating_ips': {'type': 'integer'}, + 'fixed_ips': {'type': 'integer'}, + 'metadata_items': {'type': 'integer'}, + 'key_pairs': {'type': 'integer'}, + 'security_groups': {'type': 'integer'}, + 'security_group_rules': {'type': 'integer'}, + 'server_group_members': {'type': 'integer'}, + 'server_groups': {'type': 'integer'}, + 'injected_files': {'type': 'integer'}, + 'injected_file_content_bytes': {'type': 'integer'}, + 'injected_file_path_bytes': {'type': 'integer'} + }, + 'additionalProperties': False, + # NOTE: server_group_members and server_groups are represented + # when enabling quota_server_group extension. So they should + # not be required. + 'required': ['instances', 'cores', 'ram', + 'floating_ips', 'fixed_ips', + 'metadata_items', 'key_pairs', + 'security_groups', 'security_group_rules', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes'] + } + }, + 'additionalProperties': False, + 'required': ['quota_set'] + } +} + +get_quota_set = copy.deepcopy(update_quota_set) +get_quota_set['response_body']['properties']['quota_set']['properties'][ + 'id'] = {'type': 'string'} +get_quota_set['response_body']['properties']['quota_set']['required'].extend([ + 'id']) + +delete_quota = { + 'status_code': [202] +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/security_group_default_rule.py b/tempest/lib/api_schema/response/compute/v2_1/security_group_default_rule.py new file mode 100644 index 0000000000..2ec282698d --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/security_group_default_rule.py @@ -0,0 +1,65 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +common_security_group_default_rule_info = { + 'type': 'object', + 'properties': { + 'from_port': {'type': 'integer'}, + 'id': {'type': 'integer'}, + 'ip_protocol': {'type': 'string'}, + 'ip_range': { + 'type': 'object', + 'properties': { + 'cidr': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['cidr'], + }, + 'to_port': {'type': 'integer'}, + }, + 'additionalProperties': False, + 'required': ['from_port', 'id', 'ip_protocol', 'ip_range', 'to_port'], +} + +create_get_security_group_default_rule = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'security_group_default_rule': + common_security_group_default_rule_info + }, + 'additionalProperties': False, + 'required': ['security_group_default_rule'] + } +} + +delete_security_group_default_rule = { + 'status_code': [204] +} + +list_security_group_default_rules = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'security_group_default_rules': { + 'type': 'array', + 'items': common_security_group_default_rule_info + } + }, + 'additionalProperties': False, + 'required': ['security_group_default_rules'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/security_groups.py b/tempest/lib/api_schema/response/compute/v2_1/security_groups.py new file mode 100644 index 0000000000..5ed5a5c808 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/security_groups.py @@ -0,0 +1,113 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +common_security_group_rule = { + 'from_port': {'type': ['integer', 'null']}, + 'to_port': {'type': ['integer', 'null']}, + 'group': { + 'type': 'object', + 'properties': { + 'tenant_id': {'type': 'string'}, + 'name': {'type': 'string'} + }, + 'additionalProperties': False, + }, + 'ip_protocol': {'type': ['string', 'null']}, + # 'parent_group_id' can be UUID so defining it as 'string' also. + 'parent_group_id': {'type': ['string', 'integer', 'null']}, + 'ip_range': { + 'type': 'object', + 'properties': { + 'cidr': {'type': 'string'} + }, + 'additionalProperties': False, + # When optional argument is provided in request body + # like 'group_id' then, attribute 'cidr' does not + # comes in response body. So it is not 'required'. + }, + 'id': {'type': ['string', 'integer']} +} + +common_security_group = { + 'type': 'object', + 'properties': { + 'id': {'type': ['integer', 'string']}, + 'name': {'type': 'string'}, + 'tenant_id': {'type': 'string'}, + 'rules': { + 'type': 'array', + 'items': { + 'type': ['object', 'null'], + 'properties': common_security_group_rule, + 'additionalProperties': False, + } + }, + 'description': {'type': 'string'}, + }, + 'additionalProperties': False, + 'required': ['id', 'name', 'tenant_id', 'rules', 'description'], +} + +list_security_groups = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'security_groups': { + 'type': 'array', + 'items': common_security_group + } + }, + 'additionalProperties': False, + 'required': ['security_groups'] + } +} + +get_security_group = create_security_group = update_security_group = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'security_group': common_security_group + }, + 'additionalProperties': False, + 'required': ['security_group'] + } +} + +delete_security_group = { + 'status_code': [202] +} + +create_security_group_rule = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'security_group_rule': { + 'type': 'object', + 'properties': common_security_group_rule, + 'additionalProperties': False, + 'required': ['from_port', 'to_port', 'group', 'ip_protocol', + 'parent_group_id', 'id', 'ip_range'] + } + }, + 'additionalProperties': False, + 'required': ['security_group_rule'] + } +} + +delete_security_group_rule = { + 'status_code': [202] +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/servers.py b/tempest/lib/api_schema/response/compute/v2_1/servers.py new file mode 100644 index 0000000000..485c51a914 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/servers.py @@ -0,0 +1,553 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.api_schema.response.compute.v2_1 import parameter_types + +create_server = { + 'status_code': [202], + 'response_body': { + 'type': 'object', + 'properties': { + 'server': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'security_groups': {'type': 'array'}, + 'links': parameter_types.links, + 'OS-DCF:diskConfig': {'type': 'string'} + }, + 'additionalProperties': False, + # NOTE: OS-DCF:diskConfig & security_groups are API extension, + # and some environments return a response without these + # attributes.So they are not 'required'. + 'required': ['id', 'links'] + } + }, + 'additionalProperties': False, + 'required': ['server'] + } +} + +create_server_with_admin_pass = copy.deepcopy(create_server) +create_server_with_admin_pass['response_body']['properties']['server'][ + 'properties'].update({'adminPass': {'type': 'string'}}) +create_server_with_admin_pass['response_body']['properties']['server'][ + 'required'].append('adminPass') + +list_servers = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'servers': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'links': parameter_types.links, + 'name': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['id', 'links', 'name'] + } + }, + 'servers_links': parameter_types.links + }, + 'additionalProperties': False, + # NOTE(gmann): servers_links attribute is not necessary to be + # present always So it is not 'required'. + 'required': ['servers'] + } +} + +delete_server = { + 'status_code': [204], +} + +common_show_server = { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'name': {'type': 'string'}, + 'status': {'type': 'string'}, + 'image': {'oneOf': [ + {'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'links': parameter_types.links + }, + 'additionalProperties': False, + 'required': ['id', 'links']}, + {'type': ['string', 'null']} + ]}, + 'flavor': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'links': parameter_types.links + }, + 'additionalProperties': False, + 'required': ['id', 'links'] + }, + 'fault': { + 'type': 'object', + 'properties': { + 'code': {'type': 'integer'}, + 'created': {'type': 'string'}, + 'message': {'type': 'string'}, + 'details': {'type': 'string'}, + }, + 'additionalProperties': False, + # NOTE(gmann): 'details' is not necessary to be present + # in the 'fault'. So it is not defined as 'required'. + 'required': ['code', 'created', 'message'] + }, + 'user_id': {'type': 'string'}, + 'tenant_id': {'type': 'string'}, + 'created': {'type': 'string'}, + 'updated': {'type': 'string'}, + 'progress': {'type': 'integer'}, + 'metadata': {'type': 'object'}, + 'links': parameter_types.links, + 'addresses': parameter_types.addresses, + 'hostId': {'type': 'string'}, + 'OS-DCF:diskConfig': {'type': 'string'}, + 'accessIPv4': parameter_types.access_ip_v4, + 'accessIPv6': parameter_types.access_ip_v6 + }, + 'additionalProperties': False, + # NOTE(GMann): 'progress' attribute is present in the response + # only when server's status is one of the progress statuses + # ("ACTIVE","BUILD", "REBUILD", "RESIZE","VERIFY_RESIZE") + # 'fault' attribute is present in the response + # only when server's status is one of the "ERROR", "DELETED". + # OS-DCF:diskConfig and accessIPv4/v6 are API + # extensions, and some environments return a response + # without these attributes.So these are not defined as 'required'. + 'required': ['id', 'name', 'status', 'image', 'flavor', + 'user_id', 'tenant_id', 'created', 'updated', + 'metadata', 'links', 'addresses', 'hostId'] +} + +update_server = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'server': common_show_server + }, + 'additionalProperties': False, + 'required': ['server'] + } +} + +server_detail = copy.deepcopy(common_show_server) +server_detail['properties'].update({ + 'key_name': {'type': ['string', 'null']}, + 'security_groups': {'type': 'array'}, + + # NOTE: Non-admin users also can see "OS-SRV-USG" and "OS-EXT-AZ" + # attributes. + 'OS-SRV-USG:launched_at': {'type': ['string', 'null']}, + 'OS-SRV-USG:terminated_at': {'type': ['string', 'null']}, + 'OS-EXT-AZ:availability_zone': {'type': 'string'}, + + # NOTE: Admin users only can see "OS-EXT-STS" and "OS-EXT-SRV-ATTR" + # attributes. + 'OS-EXT-STS:task_state': {'type': ['string', 'null']}, + 'OS-EXT-STS:vm_state': {'type': 'string'}, + 'OS-EXT-STS:power_state': {'type': 'integer'}, + 'OS-EXT-SRV-ATTR:host': {'type': ['string', 'null']}, + 'OS-EXT-SRV-ATTR:instance_name': {'type': 'string'}, + 'OS-EXT-SRV-ATTR:hypervisor_hostname': {'type': ['string', 'null']}, + 'os-extended-volumes:volumes_attached': {'type': 'array'}, + 'config_drive': {'type': 'string'} +}) +server_detail['properties']['addresses']['patternProperties'][ + '^[a-zA-Z0-9-_.]+$']['items']['properties'].update({ + 'OS-EXT-IPS:type': {'type': 'string'}, + 'OS-EXT-IPS-MAC:mac_addr': parameter_types.mac_address}) +# NOTE(gmann): Update OS-EXT-IPS:type and OS-EXT-IPS-MAC:mac_addr +# attributes in server address. Those are API extension, +# and some environments return a response without +# these attributes. So they are not 'required'. + +get_server = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'server': server_detail + }, + 'additionalProperties': False, + 'required': ['server'] + } +} + +list_servers_detail = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'servers': { + 'type': 'array', + 'items': server_detail + }, + 'servers_links': parameter_types.links + }, + 'additionalProperties': False, + # NOTE(gmann): servers_links attribute is not necessary to be + # present always So it is not 'required'. + 'required': ['servers'] + } +} + +rebuild_server = copy.deepcopy(update_server) +rebuild_server['status_code'] = [202] + +rebuild_server_with_admin_pass = copy.deepcopy(rebuild_server) +rebuild_server_with_admin_pass['response_body']['properties']['server'][ + 'properties'].update({'adminPass': {'type': 'string'}}) +rebuild_server_with_admin_pass['response_body']['properties']['server'][ + 'required'].append('adminPass') + +rescue_server = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'adminPass': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['adminPass'] + } +} + +list_virtual_interfaces = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'virtual_interfaces': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'mac_address': parameter_types.mac_address, + 'OS-EXT-VIF-NET:net_id': {'type': 'string'} + }, + 'additionalProperties': False, + # 'OS-EXT-VIF-NET:net_id' is API extension So it is + # not defined as 'required' + 'required': ['id', 'mac_address'] + } + } + }, + 'additionalProperties': False, + 'required': ['virtual_interfaces'] + } +} + +common_attach_volume_info = { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'device': {'type': 'string'}, + 'volumeId': {'type': 'string'}, + 'serverId': {'type': ['integer', 'string']} + }, + 'additionalProperties': False, + 'required': ['id', 'device', 'volumeId', 'serverId'] +} + +attach_volume = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'volumeAttachment': common_attach_volume_info + }, + 'additionalProperties': False, + 'required': ['volumeAttachment'] + } +} + +detach_volume = { + 'status_code': [202] +} + +show_volume_attachment = copy.deepcopy(attach_volume) +show_volume_attachment['response_body']['properties'][ + 'volumeAttachment']['properties'].update({'serverId': {'type': 'string'}}) + +list_volume_attachments = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'volumeAttachments': { + 'type': 'array', + 'items': common_attach_volume_info + } + }, + 'additionalProperties': False, + 'required': ['volumeAttachments'] + } +} +list_volume_attachments['response_body']['properties'][ + 'volumeAttachments']['items']['properties'].update( + {'serverId': {'type': 'string'}}) + +list_addresses_by_network = { + 'status_code': [200], + 'response_body': parameter_types.addresses +} + +list_addresses = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'addresses': parameter_types.addresses + }, + 'additionalProperties': False, + 'required': ['addresses'] + } +} + +common_server_group = { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'name': {'type': 'string'}, + 'policies': { + 'type': 'array', + 'items': {'type': 'string'} + }, + # 'members' attribute contains the array of instance's UUID of + # instances present in server group + 'members': { + 'type': 'array', + 'items': {'type': 'string'} + }, + 'metadata': {'type': 'object'} + }, + 'additionalProperties': False, + 'required': ['id', 'name', 'policies', 'members', 'metadata'] +} + +create_show_server_group = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'server_group': common_server_group + }, + 'additionalProperties': False, + 'required': ['server_group'] + } +} + +delete_server_group = { + 'status_code': [204] +} + +list_server_groups = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'server_groups': { + 'type': 'array', + 'items': common_server_group + } + }, + 'additionalProperties': False, + 'required': ['server_groups'] + } +} + +instance_actions = { + 'type': 'object', + 'properties': { + 'action': {'type': 'string'}, + 'request_id': {'type': 'string'}, + 'user_id': {'type': 'string'}, + 'project_id': {'type': 'string'}, + 'start_time': {'type': 'string'}, + 'message': {'type': ['string', 'null']}, + 'instance_uuid': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['action', 'request_id', 'user_id', 'project_id', + 'start_time', 'message', 'instance_uuid'] +} + +instance_action_events = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'event': {'type': 'string'}, + 'start_time': {'type': 'string'}, + 'finish_time': {'type': 'string'}, + 'result': {'type': 'string'}, + 'traceback': {'type': ['string', 'null']} + }, + 'additionalProperties': False, + 'required': ['event', 'start_time', 'finish_time', 'result', + 'traceback'] + } +} + +list_instance_actions = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'instanceActions': { + 'type': 'array', + 'items': instance_actions + } + }, + 'additionalProperties': False, + 'required': ['instanceActions'] + } +} + +instance_actions_with_events = copy.deepcopy(instance_actions) +instance_actions_with_events['properties'].update({ + 'events': instance_action_events}) +# 'events' does not come in response body always so it is not +# defined as 'required' + +show_instance_action = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'instanceAction': instance_actions_with_events + }, + 'additionalProperties': False, + 'required': ['instanceAction'] + } +} + +show_password = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'password': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['password'] + } +} + +get_vnc_console = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'console': { + 'type': 'object', + 'properties': { + 'type': {'type': 'string'}, + 'url': { + 'type': 'string', + 'format': 'uri' + } + }, + 'additionalProperties': False, + 'required': ['type', 'url'] + } + }, + 'additionalProperties': False, + 'required': ['console'] + } +} + +get_console_output = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'output': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['output'] + } +} + +set_server_metadata = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'metadata': { + 'type': 'object', + 'patternProperties': { + '^.+$': {'type': 'string'} + } + } + }, + 'additionalProperties': False, + 'required': ['metadata'] + } +} + +list_server_metadata = copy.deepcopy(set_server_metadata) + +update_server_metadata = copy.deepcopy(set_server_metadata) + +delete_server_metadata_item = { + 'status_code': [204] +} + +set_show_server_metadata_item = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'meta': { + 'type': 'object', + 'patternProperties': { + '^.+$': {'type': 'string'} + } + } + }, + 'additionalProperties': False, + 'required': ['meta'] + } +} + +server_actions_common_schema = { + 'status_code': [202] +} + +server_actions_delete_password = { + 'status_code': [204] +} + +server_actions_confirm_resize = copy.deepcopy( + server_actions_delete_password) + +update_attached_volume = { + 'status_code': [202] +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/services.py b/tempest/lib/api_schema/response/compute/v2_1/services.py new file mode 100644 index 0000000000..ddef7b2291 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/services.py @@ -0,0 +1,65 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +list_services = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'services': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': ['integer', 'string'], + 'pattern': '^[a-zA-Z!]*@[0-9]+$'}, + 'zone': {'type': 'string'}, + 'host': {'type': 'string'}, + 'state': {'type': 'string'}, + 'binary': {'type': 'string'}, + 'status': {'type': 'string'}, + 'updated_at': {'type': ['string', 'null']}, + 'disabled_reason': {'type': ['string', 'null']} + }, + 'additionalProperties': False, + 'required': ['id', 'zone', 'host', 'state', 'binary', + 'status', 'updated_at', 'disabled_reason'] + } + } + }, + 'additionalProperties': False, + 'required': ['services'] + } +} + +enable_disable_service = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'service': { + 'type': 'object', + 'properties': { + 'status': {'type': 'string'}, + 'binary': {'type': 'string'}, + 'host': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['status', 'binary', 'host'] + } + }, + 'additionalProperties': False, + 'required': ['service'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/snapshots.py b/tempest/lib/api_schema/response/compute/v2_1/snapshots.py new file mode 100644 index 0000000000..01a524bbc9 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/snapshots.py @@ -0,0 +1,61 @@ +# Copyright 2015 Fujitsu(fnst) Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +common_snapshot_info = { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'volumeId': {'type': 'string'}, + 'status': {'type': 'string'}, + 'size': {'type': 'integer'}, + 'createdAt': {'type': 'string'}, + 'displayName': {'type': ['string', 'null']}, + 'displayDescription': {'type': ['string', 'null']} + }, + 'additionalProperties': False, + 'required': ['id', 'volumeId', 'status', 'size', + 'createdAt', 'displayName', 'displayDescription'] +} + +create_get_snapshot = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'snapshot': common_snapshot_info + }, + 'additionalProperties': False, + 'required': ['snapshot'] + } +} + +list_snapshots = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'snapshots': { + 'type': 'array', + 'items': common_snapshot_info + } + }, + 'additionalProperties': False, + 'required': ['snapshots'] + } +} + +delete_snapshot = { + 'status_code': [202] +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/tenant_networks.py b/tempest/lib/api_schema/response/compute/v2_1/tenant_networks.py new file mode 100644 index 0000000000..ddfab96198 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/tenant_networks.py @@ -0,0 +1,53 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +param_network = { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'cidr': {'type': ['string', 'null']}, + 'label': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['id', 'cidr', 'label'] +} + + +list_tenant_networks = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'networks': { + 'type': 'array', + 'items': param_network + } + }, + 'additionalProperties': False, + 'required': ['networks'] + } +} + + +get_tenant_network = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'network': param_network + }, + 'additionalProperties': False, + 'required': ['network'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py b/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py new file mode 100644 index 0000000000..d51ef12e26 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py @@ -0,0 +1,92 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +_server_usages = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'ended_at': { + 'oneOf': [ + {'type': 'string'}, + {'type': 'null'} + ] + }, + 'flavor': {'type': 'string'}, + 'hours': {'type': 'number'}, + 'instance_id': {'type': 'string'}, + 'local_gb': {'type': 'integer'}, + 'memory_mb': {'type': 'integer'}, + 'name': {'type': 'string'}, + 'started_at': {'type': 'string'}, + 'state': {'type': 'string'}, + 'tenant_id': {'type': 'string'}, + 'uptime': {'type': 'integer'}, + 'vcpus': {'type': 'integer'}, + }, + 'required': ['ended_at', 'flavor', 'hours', 'instance_id', 'local_gb', + 'memory_mb', 'name', 'started_at', 'state', 'tenant_id', + 'uptime', 'vcpus'] + } +} + +_tenant_usage_list = { + 'type': 'object', + 'properties': { + 'server_usages': _server_usages, + 'start': {'type': 'string'}, + 'stop': {'type': 'string'}, + 'tenant_id': {'type': 'string'}, + 'total_hours': {'type': 'number'}, + 'total_local_gb_usage': {'type': 'number'}, + 'total_memory_mb_usage': {'type': 'number'}, + 'total_vcpus_usage': {'type': 'number'}, + }, + 'required': ['start', 'stop', 'tenant_id', + 'total_hours', 'total_local_gb_usage', + 'total_memory_mb_usage', 'total_vcpus_usage'] +} + +# 'required' of get_tenant is different from list_tenant's. +_tenant_usage_get = copy.deepcopy(_tenant_usage_list) +_tenant_usage_get['required'] = ['server_usages', 'start', 'stop', 'tenant_id', + 'total_hours', 'total_local_gb_usage', + 'total_memory_mb_usage', 'total_vcpus_usage'] + +list_tenant_usage = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'tenant_usages': { + 'type': 'array', + 'items': _tenant_usage_list + } + }, + 'required': ['tenant_usages'] + } +} + +get_tenant_usage = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'tenant_usage': _tenant_usage_get + }, + 'required': ['tenant_usage'] + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/versions.py b/tempest/lib/api_schema/response/compute/v2_1/versions.py new file mode 100644 index 0000000000..08a9fab5a8 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/versions.py @@ -0,0 +1,110 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + + +_version = { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'links': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'href': {'type': 'string', 'format': 'uri'}, + 'rel': {'type': 'string'}, + 'type': {'type': 'string'}, + }, + 'required': ['href', 'rel'], + 'additionalProperties': False + } + }, + 'status': {'type': 'string'}, + 'updated': {'type': 'string', 'format': 'date-time'}, + 'version': {'type': 'string'}, + 'min_version': {'type': 'string'}, + 'media-types': { + 'type': 'array', + 'properties': { + 'base': {'type': 'string'}, + 'type': {'type': 'string'}, + } + }, + }, + # NOTE: version and min_version have been added since Kilo, + # so they should not be required. + # NOTE(sdague): media-types only shows up in single version requests. + 'required': ['id', 'links', 'status', 'updated'], + 'additionalProperties': False +} + +list_versions = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'versions': { + 'type': 'array', + 'items': _version + } + }, + 'required': ['versions'], + 'additionalProperties': False + } +} + + +_detail_get_version = copy.deepcopy(_version) +_detail_get_version['properties'].pop('min_version') +_detail_get_version['properties'].pop('version') +_detail_get_version['properties'].pop('updated') +_detail_get_version['properties']['media-types'] = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'base': {'type': 'string'}, + 'type': {'type': 'string'} + } + } +} +_detail_get_version['required'] = ['id', 'links', 'status', 'media-types'] + +get_version = { + 'status_code': [300], + 'response_body': { + 'type': 'object', + 'properties': { + 'choices': { + 'type': 'array', + 'items': _detail_get_version + } + }, + 'required': ['choices'], + 'additionalProperties': False + } +} + +get_one_version = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'version': _version + }, + 'additionalProperties': False + } +} diff --git a/tempest/lib/api_schema/response/compute/v2_1/volumes.py b/tempest/lib/api_schema/response/compute/v2_1/volumes.py new file mode 100644 index 0000000000..bb34acb17b --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_1/volumes.py @@ -0,0 +1,120 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +create_get_volume = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'volume': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'status': {'type': 'string'}, + 'displayName': {'type': ['string', 'null']}, + 'availabilityZone': {'type': 'string'}, + 'createdAt': {'type': 'string'}, + 'displayDescription': {'type': ['string', 'null']}, + 'volumeType': {'type': ['string', 'null']}, + 'snapshotId': {'type': ['string', 'null']}, + 'metadata': {'type': 'object'}, + 'size': {'type': 'integer'}, + 'attachments': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'device': {'type': 'string'}, + 'volumeId': {'type': 'string'}, + 'serverId': {'type': 'string'} + }, + 'additionalProperties': False, + # NOTE- If volume is not attached to any server + # then, 'attachments' attributes comes as array + # with empty objects "[{}]" due to that elements + # of 'attachments' cannot defined as 'required'. + # If it would come as empty array "[]" then, + # those elements can be defined as 'required'. + } + } + }, + 'additionalProperties': False, + 'required': ['id', 'status', 'displayName', 'availabilityZone', + 'createdAt', 'displayDescription', 'volumeType', + 'snapshotId', 'metadata', 'size', 'attachments'] + } + }, + 'additionalProperties': False, + 'required': ['volume'] + } +} + +list_volumes = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'volumes': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'status': {'type': 'string'}, + 'displayName': {'type': ['string', 'null']}, + 'availabilityZone': {'type': 'string'}, + 'createdAt': {'type': 'string'}, + 'displayDescription': {'type': ['string', 'null']}, + 'volumeType': {'type': ['string', 'null']}, + 'snapshotId': {'type': ['string', 'null']}, + 'metadata': {'type': 'object'}, + 'size': {'type': 'integer'}, + 'attachments': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'device': {'type': 'string'}, + 'volumeId': {'type': 'string'}, + 'serverId': {'type': 'string'} + }, + 'additionalProperties': False, + # NOTE- If volume is not attached to any server + # then, 'attachments' attributes comes as array + # with empty object "[{}]" due to that elements + # of 'attachments' cannot defined as 'required' + # If it would come as empty array "[]" then, + # those elements can be defined as 'required'. + } + } + }, + 'additionalProperties': False, + 'required': ['id', 'status', 'displayName', + 'availabilityZone', 'createdAt', + 'displayDescription', 'volumeType', + 'snapshotId', 'metadata', 'size', + 'attachments'] + } + } + }, + 'additionalProperties': False, + 'required': ['volumes'] + } +} + +delete_volume = { + 'status_code': [202] +} diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py new file mode 100644 index 0000000000..806acb5205 --- /dev/null +++ b/tempest/lib/auth.py @@ -0,0 +1,676 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import copy +import datetime +import re + +from oslo_log import log as logging +import six +from six.moves.urllib import parse as urlparse + +from tempest.lib import exceptions +from tempest.lib.services.identity.v2 import token_client as json_v2id +from tempest.lib.services.identity.v3 import token_client as json_v3id + +ISO8601_FLOAT_SECONDS = '%Y-%m-%dT%H:%M:%S.%fZ' +ISO8601_INT_SECONDS = '%Y-%m-%dT%H:%M:%SZ' +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class AuthProvider(object): + """Provide authentication""" + + def __init__(self, credentials): + """Auth provider __init__ + + :param credentials: credentials for authentication + """ + if self.check_credentials(credentials): + self.credentials = credentials + else: + if isinstance(credentials, Credentials): + password = credentials.get('password') + message = "Credentials are: " + str(credentials) + if password is None: + message += " Password is not defined." + else: + message += " Password is defined." + raise exceptions.InvalidCredentials(message) + else: + raise TypeError("credentials object is of type %s, which is" + " not a valid Credentials object type." % + credentials.__class__.__name__) + self.cache = None + self.alt_auth_data = None + self.alt_part = None + + def __str__(self): + return "Creds :{creds}, cached auth data: {cache}".format( + creds=self.credentials, cache=self.cache) + + @abc.abstractmethod + def _decorate_request(self, filters, method, url, headers=None, body=None, + auth_data=None): + """Decorate request with authentication data""" + return + + @abc.abstractmethod + def _get_auth(self): + return + + @abc.abstractmethod + def _fill_credentials(self, auth_data_body): + return + + def fill_credentials(self): + """Fill credentials object with data from auth""" + auth_data = self.get_auth() + self._fill_credentials(auth_data[1]) + return self.credentials + + @classmethod + def check_credentials(cls, credentials): + """Verify credentials are valid.""" + return isinstance(credentials, Credentials) and credentials.is_valid() + + @property + def auth_data(self): + return self.get_auth() + + @auth_data.deleter + def auth_data(self): + self.clear_auth() + + def get_auth(self): + """Returns auth from cache if available, else auth first""" + if self.cache is None or self.is_expired(self.cache): + self.set_auth() + return self.cache + + def set_auth(self): + """Forces setting auth. + + Forces setting auth, ignores cache if it exists. + Refills credentials + """ + self.cache = self._get_auth() + self._fill_credentials(self.cache[1]) + + def clear_auth(self): + """Clear access cache + + Can be called to clear the access cache so that next request + will fetch a new token and base_url. + """ + self.cache = None + self.credentials.reset() + + @abc.abstractmethod + def is_expired(self, auth_data): + return + + def auth_request(self, method, url, headers=None, body=None, filters=None): + """Obtains auth data and decorates a request with that. + + :param method: HTTP method of the request + :param url: relative URL of the request (path) + :param headers: HTTP headers of the request + :param body: HTTP body in case of POST / PUT + :param filters: select a base URL out of the catalog + :returns a Tuple (url, headers, body) + """ + orig_req = dict(url=url, headers=headers, body=body) + + auth_url, auth_headers, auth_body = self._decorate_request( + filters, method, url, headers, body) + auth_req = dict(url=auth_url, headers=auth_headers, body=auth_body) + + # Overwrite part if the request if it has been requested + if self.alt_part is not None: + if self.alt_auth_data is not None: + alt_url, alt_headers, alt_body = self._decorate_request( + filters, method, url, headers, body, + auth_data=self.alt_auth_data) + alt_auth_req = dict(url=alt_url, headers=alt_headers, + body=alt_body) + if auth_req[self.alt_part] == alt_auth_req[self.alt_part]: + raise exceptions.BadAltAuth(part=self.alt_part) + auth_req[self.alt_part] = alt_auth_req[self.alt_part] + + else: + # If the requested part is not affected by auth, we are + # not altering auth as expected, raise an exception + if auth_req[self.alt_part] == orig_req[self.alt_part]: + raise exceptions.BadAltAuth(part=self.alt_part) + # If alt auth data is None, skip auth in the requested part + auth_req[self.alt_part] = orig_req[self.alt_part] + + # Next auth request will be normal, unless otherwise requested + self.reset_alt_auth_data() + + return auth_req['url'], auth_req['headers'], auth_req['body'] + + def reset_alt_auth_data(self): + """Configure auth provider to provide valid authentication data""" + self.alt_part = None + self.alt_auth_data = None + + def set_alt_auth_data(self, request_part, auth_data): + """Alternate auth data on next request + + Configure auth provider to provide alt authentication data + on a part of the *next* auth_request. If credentials are None, + set invalid data. + :param request_part: request part to contain invalid auth: url, + headers, body + :param auth_data: alternative auth_data from which to get the + invalid data to be injected + """ + self.alt_part = request_part + self.alt_auth_data = auth_data + + @abc.abstractmethod + def base_url(self, filters, auth_data=None): + """Extracts the base_url based on provided filters""" + return + + +class KeystoneAuthProvider(AuthProvider): + + EXPIRY_DATE_FORMATS = (ISO8601_FLOAT_SECONDS, ISO8601_INT_SECONDS) + + token_expiry_threshold = datetime.timedelta(seconds=60) + + def __init__(self, credentials, auth_url, + disable_ssl_certificate_validation=None, + ca_certs=None, trace_requests=None): + super(KeystoneAuthProvider, self).__init__(credentials) + self.dsvm = disable_ssl_certificate_validation + self.ca_certs = ca_certs + self.trace_requests = trace_requests + self.auth_client = self._auth_client(auth_url) + + def _decorate_request(self, filters, method, url, headers=None, body=None, + auth_data=None): + if auth_data is None: + auth_data = self.auth_data + token, _ = auth_data + base_url = self.base_url(filters=filters, auth_data=auth_data) + # build authenticated request + # returns new request, it does not touch the original values + _headers = copy.deepcopy(headers) if headers is not None else {} + _headers['X-Auth-Token'] = str(token) + if url is None or url == "": + _url = base_url + else: + # Join base URL and url, and remove multiple contiguous slashes + _url = "/".join([base_url, url]) + parts = [x for x in urlparse.urlparse(_url)] + parts[2] = re.sub("/{2,}", "/", parts[2]) + _url = urlparse.urlunparse(parts) + # no change to method or body + return str(_url), _headers, body + + @abc.abstractmethod + def _auth_client(self): + return + + @abc.abstractmethod + def _auth_params(self): + return + + def _get_auth(self): + # Bypasses the cache + auth_func = getattr(self.auth_client, 'get_token') + auth_params = self._auth_params() + + # returns token, auth_data + token, auth_data = auth_func(**auth_params) + return token, auth_data + + def _parse_expiry_time(self, expiry_string): + expiry = None + for date_format in self.EXPIRY_DATE_FORMATS: + try: + expiry = datetime.datetime.strptime( + expiry_string, date_format) + except ValueError: + pass + if expiry is None: + raise ValueError( + "time data '{data}' does not match any of the" + "expected formats: {formats}".format( + data=expiry_string, formats=self.EXPIRY_DATE_FORMATS)) + return expiry + + def get_token(self): + return self.auth_data[0] + + +class KeystoneV2AuthProvider(KeystoneAuthProvider): + + def _auth_client(self, auth_url): + return json_v2id.TokenClient( + auth_url, disable_ssl_certificate_validation=self.dsvm, + ca_certs=self.ca_certs, trace_requests=self.trace_requests) + + def _auth_params(self): + return dict( + user=self.credentials.username, + password=self.credentials.password, + tenant=self.credentials.tenant_name, + auth_data=True) + + def _fill_credentials(self, auth_data_body): + tenant = auth_data_body['token']['tenant'] + user = auth_data_body['user'] + if self.credentials.tenant_name is None: + self.credentials.tenant_name = tenant['name'] + if self.credentials.tenant_id is None: + self.credentials.tenant_id = tenant['id'] + if self.credentials.username is None: + self.credentials.username = user['name'] + if self.credentials.user_id is None: + self.credentials.user_id = user['id'] + + def base_url(self, filters, auth_data=None): + """Base URL from catalog + + Filters can be: + - service: compute, image, etc + - region: the service region + - endpoint_type: adminURL, publicURL, internalURL + - api_version: replace catalog version with this + - skip_path: take just the base URL + """ + if auth_data is None: + auth_data = self.auth_data + token, _auth_data = auth_data + service = filters.get('service') + region = filters.get('region') + endpoint_type = filters.get('endpoint_type', 'publicURL') + + if service is None: + raise exceptions.EndpointNotFound("No service provided") + + _base_url = None + for ep in _auth_data['serviceCatalog']: + if ep["type"] == service: + for _ep in ep['endpoints']: + if region is not None and _ep['region'] == region: + _base_url = _ep.get(endpoint_type) + if not _base_url: + # No region matching, use the first + _base_url = ep['endpoints'][0].get(endpoint_type) + break + if _base_url is None: + raise exceptions.EndpointNotFound(service) + + parts = urlparse.urlparse(_base_url) + if filters.get('api_version', None) is not None: + path = "/" + filters['api_version'] + noversion_path = "/".join(parts.path.split("/")[2:]) + if noversion_path != "": + path += "/" + noversion_path + _base_url = _base_url.replace(parts.path, path) + if filters.get('skip_path', None) is not None and parts.path != '': + _base_url = _base_url.replace(parts.path, "/") + + return _base_url + + def is_expired(self, auth_data): + _, access = auth_data + expiry = self._parse_expiry_time(access['token']['expires']) + return (expiry - self.token_expiry_threshold <= + datetime.datetime.utcnow()) + + +class KeystoneV3AuthProvider(KeystoneAuthProvider): + + def _auth_client(self, auth_url): + return json_v3id.V3TokenClient( + auth_url, disable_ssl_certificate_validation=self.dsvm, + ca_certs=self.ca_certs, trace_requests=self.trace_requests) + + def _auth_params(self): + return dict( + user_id=self.credentials.user_id, + username=self.credentials.username, + password=self.credentials.password, + project_id=self.credentials.project_id, + project_name=self.credentials.project_name, + user_domain_id=self.credentials.user_domain_id, + user_domain_name=self.credentials.user_domain_name, + project_domain_id=self.credentials.project_domain_id, + project_domain_name=self.credentials.project_domain_name, + domain_id=self.credentials.domain_id, + domain_name=self.credentials.domain_name, + auth_data=True) + + def _fill_credentials(self, auth_data_body): + # project or domain, depending on the scope + project = auth_data_body.get('project', None) + domain = auth_data_body.get('domain', None) + # user is always there + user = auth_data_body['user'] + # Set project fields + if project is not None: + if self.credentials.project_name is None: + self.credentials.project_name = project['name'] + if self.credentials.project_id is None: + self.credentials.project_id = project['id'] + if self.credentials.project_domain_id is None: + self.credentials.project_domain_id = project['domain']['id'] + if self.credentials.project_domain_name is None: + self.credentials.project_domain_name = ( + project['domain']['name']) + # Set domain fields + if domain is not None: + if self.credentials.domain_id is None: + self.credentials.domain_id = domain['id'] + if self.credentials.domain_name is None: + self.credentials.domain_name = domain['name'] + # Set user fields + if self.credentials.username is None: + self.credentials.username = user['name'] + if self.credentials.user_id is None: + self.credentials.user_id = user['id'] + if self.credentials.user_domain_id is None: + self.credentials.user_domain_id = user['domain']['id'] + if self.credentials.user_domain_name is None: + self.credentials.user_domain_name = user['domain']['name'] + + def base_url(self, filters, auth_data=None): + """Base URL from catalog + + Filters can be: + - service: compute, image, etc + - region: the service region + - endpoint_type: adminURL, publicURL, internalURL + - api_version: replace catalog version with this + - skip_path: take just the base URL + """ + if auth_data is None: + auth_data = self.auth_data + token, _auth_data = auth_data + service = filters.get('service') + region = filters.get('region') + endpoint_type = filters.get('endpoint_type', 'public') + + if service is None: + raise exceptions.EndpointNotFound("No service provided") + + if 'URL' in endpoint_type: + endpoint_type = endpoint_type.replace('URL', '') + _base_url = None + catalog = _auth_data['catalog'] + # Select entries with matching service type + service_catalog = [ep for ep in catalog if ep['type'] == service] + if len(service_catalog) > 0: + service_catalog = service_catalog[0]['endpoints'] + else: + # No matching service + raise exceptions.EndpointNotFound(service) + # Filter by endpoint type (interface) + filtered_catalog = [ep for ep in service_catalog if + ep['interface'] == endpoint_type] + if len(filtered_catalog) == 0: + # No matching type, keep all and try matching by region at least + filtered_catalog = service_catalog + # Filter by region + filtered_catalog = [ep for ep in filtered_catalog if + ep['region'] == region] + if len(filtered_catalog) == 0: + # No matching region, take the first endpoint + filtered_catalog = [service_catalog[0]] + # There should be only one match. If not take the first. + _base_url = filtered_catalog[0].get('url', None) + if _base_url is None: + raise exceptions.EndpointNotFound(service) + + parts = urlparse.urlparse(_base_url) + if filters.get('api_version', None) is not None: + path = "/" + filters['api_version'] + noversion_path = "/".join(parts.path.split("/")[2:]) + if noversion_path != "": + path += "/" + noversion_path + _base_url = _base_url.replace(parts.path, path) + if filters.get('skip_path', None) is not None: + _base_url = _base_url.replace(parts.path, "/") + + return _base_url + + def is_expired(self, auth_data): + _, access = auth_data + expiry = self._parse_expiry_time(access['expires_at']) + return (expiry - self.token_expiry_threshold <= + datetime.datetime.utcnow()) + + +def is_identity_version_supported(identity_version): + return identity_version in IDENTITY_VERSION + + +def get_credentials(auth_url, fill_in=True, identity_version='v2', + disable_ssl_certificate_validation=None, ca_certs=None, + trace_requests=None, **kwargs): + """Builds a credentials object based on the configured auth_version + + :param auth_url (string): Full URI of the OpenStack Identity API(Keystone) + which is used to fetch the token from Identity service. + :param fill_in (boolean): obtain a token and fill in all credential + details provided by the identity service. When fill_in is not + specified, credentials are not validated. Validation can be invoked + by invoking ``is_valid()`` + :param identity_version (string): identity API version is used to + select the matching auth provider and credentials class + :param disable_ssl_certificate_validation: whether to enforce SSL + certificate validation in SSL API requests to the auth system + :param ca_certs: CA certificate bundle for validation of certificates + in SSL API requests to the auth system + :param trace_requests: trace in log API requests to the auth system + :param kwargs (dict): Dict of credential key/value pairs + + Examples: + + Returns credentials from the provided parameters: + >>> get_credentials(username='foo', password='bar') + + Returns credentials including IDs: + >>> get_credentials(username='foo', password='bar', fill_in=True) + """ + if not is_identity_version_supported(identity_version): + raise exceptions.InvalidIdentityVersion( + identity_version=identity_version) + + credential_class, auth_provider_class = IDENTITY_VERSION.get( + identity_version) + + creds = credential_class(**kwargs) + # Fill in the credentials fields that were not specified + if fill_in: + dsvm = disable_ssl_certificate_validation + auth_provider = auth_provider_class( + creds, auth_url, disable_ssl_certificate_validation=dsvm, + ca_certs=ca_certs, trace_requests=trace_requests) + creds = auth_provider.fill_credentials() + return creds + + +class Credentials(object): + """Set of credentials for accessing OpenStack services + + ATTRIBUTES: list of valid class attributes representing credentials. + """ + + ATTRIBUTES = [] + + def __init__(self, **kwargs): + """Enforce the available attributes at init time (only). + + Additional attributes can still be set afterwards if tests need + to do so. + """ + self._initial = kwargs + self._apply_credentials(kwargs) + + def _apply_credentials(self, attr): + for key in attr.keys(): + if key in self.ATTRIBUTES: + setattr(self, key, attr[key]) + else: + msg = '%s is not a valid attr for %s' % (key, self.__class__) + raise exceptions.InvalidCredentials(msg) + + def __str__(self): + """Represent only attributes included in self.ATTRIBUTES""" + attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password'] + _repr = dict((k, getattr(self, k)) for k in attrs) + return str(_repr) + + def __eq__(self, other): + """Credentials are equal if attributes in self.ATTRIBUTES are equal""" + return str(self) == str(other) + + def __getattr__(self, key): + # If an attribute is set, __getattr__ is not invoked + # If an attribute is not set, and it is a known one, return None + if key in self.ATTRIBUTES: + return None + else: + raise AttributeError + + def __delitem__(self, key): + # For backwards compatibility, support dict behaviour + if key in self.ATTRIBUTES: + delattr(self, key) + else: + raise AttributeError + + def get(self, item, default=None): + # In this patch act as dict for backward compatibility + try: + return getattr(self, item) + except AttributeError: + return default + + def get_init_attributes(self): + return self._initial.keys() + + def is_valid(self): + raise NotImplementedError + + def reset(self): + # First delete all known attributes + for key in self.ATTRIBUTES: + if getattr(self, key) is not None: + delattr(self, key) + # Then re-apply initial setup + self._apply_credentials(self._initial) + + +class KeystoneV2Credentials(Credentials): + + ATTRIBUTES = ['username', 'password', 'tenant_name', 'user_id', + 'tenant_id'] + + def is_valid(self): + """Check of credentials (no API call) + + Minimum set of valid credentials, are username and password. + Tenant is optional. + """ + return None not in (self.username, self.password) + + +class KeystoneV3Credentials(Credentials): + """Credentials suitable for the Keystone Identity V3 API""" + + ATTRIBUTES = ['domain_id', 'domain_name', 'password', 'username', + 'project_domain_id', 'project_domain_name', 'project_id', + 'project_name', 'tenant_id', 'tenant_name', 'user_domain_id', + 'user_domain_name', 'user_id'] + + def __setattr__(self, key, value): + parent = super(KeystoneV3Credentials, self) + # for tenant_* set both project and tenant + if key == 'tenant_id': + parent.__setattr__('project_id', value) + elif key == 'tenant_name': + parent.__setattr__('project_name', value) + # for project_* set both project and tenant + if key == 'project_id': + parent.__setattr__('tenant_id', value) + elif key == 'project_name': + parent.__setattr__('tenant_name', value) + # for *_domain_* set both user and project if not set yet + if key == 'user_domain_id': + if self.project_domain_id is None: + parent.__setattr__('project_domain_id', value) + if key == 'project_domain_id': + if self.user_domain_id is None: + parent.__setattr__('user_domain_id', value) + if key == 'user_domain_name': + if self.project_domain_name is None: + parent.__setattr__('project_domain_name', value) + if key == 'project_domain_name': + if self.user_domain_name is None: + parent.__setattr__('user_domain_name', value) + # support domain_name coming from config + if key == 'domain_name': + parent.__setattr__('user_domain_name', value) + parent.__setattr__('project_domain_name', value) + # finally trigger default behaviour for all attributes + parent.__setattr__(key, value) + + def is_valid(self): + """Check of credentials (no API call) + + Valid combinations of v3 credentials (excluding token, scope) + - User id, password (optional domain) + - User name, password and its domain id/name + For the scope, valid combinations are: + - None + - Project id (optional domain) + - Project name and its domain id/name + - Domain id + - Domain name + """ + valid_user_domain = any( + [self.user_domain_id is not None, + self.user_domain_name is not None]) + valid_project_domain = any( + [self.project_domain_id is not None, + self.project_domain_name is not None]) + valid_user = any( + [self.user_id is not None, + self.username is not None and valid_user_domain]) + valid_project_scope = any( + [self.project_name is None and self.project_id is None, + self.project_id is not None, + self.project_name is not None and valid_project_domain]) + valid_domain_scope = any( + [self.domain_id is None and self.domain_name is None, + self.domain_id or self.domain_name]) + return all([self.password is not None, + valid_user, + valid_project_scope and valid_domain_scope]) + + +IDENTITY_VERSION = {'v2': (KeystoneV2Credentials, KeystoneV2AuthProvider), + 'v3': (KeystoneV3Credentials, KeystoneV3AuthProvider)} diff --git a/tempest/lib/base.py b/tempest/lib/base.py new file mode 100644 index 0000000000..227ac37ca8 --- /dev/null +++ b/tempest/lib/base.py @@ -0,0 +1,71 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os + +import fixtures +import testtools + +LOG = logging.getLogger(__name__) + + +class BaseTestCase(testtools.testcase.WithAttributes, testtools.TestCase): + setUpClassCalled = False + + # NOTE(sdague): log_format is defined inline here instead of using the oslo + # default because going through the config path recouples config to the + # stress tests too early, and depending on testr order will fail unit tests + log_format = ('%(asctime)s %(process)d %(levelname)-8s ' + '[%(name)s] %(message)s') + + @classmethod + def setUpClass(cls): + if hasattr(super(BaseTestCase, cls), 'setUpClass'): + super(BaseTestCase, cls).setUpClass() + cls.setUpClassCalled = True + + @classmethod + def tearDownClass(cls): + if hasattr(super(BaseTestCase, cls), 'tearDownClass'): + super(BaseTestCase, cls).tearDownClass() + + def setUp(self): + super(BaseTestCase, self).setUp() + if not self.setUpClassCalled: + raise RuntimeError("setUpClass does not calls the super's" + "setUpClass in the " + + self.__class__.__name__) + test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) + try: + test_timeout = int(test_timeout) + except ValueError: + test_timeout = 0 + if test_timeout > 0: + self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) + + if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or + os.environ.get('OS_STDOUT_CAPTURE') == '1'): + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or + os.environ.get('OS_STDERR_CAPTURE') == '1'): + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + if (os.environ.get('OS_LOG_CAPTURE') != 'False' and + os.environ.get('OS_LOG_CAPTURE') != '0'): + self.useFixture(fixtures.LoggerFixture(nuke_handlers=False, + format=self.log_format, + level=None)) diff --git a/tempest/lib/cli/__init__.py b/tempest/lib/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/cli/base.py b/tempest/lib/cli/base.py new file mode 100644 index 0000000000..54f35f4e80 --- /dev/null +++ b/tempest/lib/cli/base.py @@ -0,0 +1,410 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os +import shlex +import subprocess + +import six + +from tempest.lib import base +import tempest.lib.cli.output_parser +from tempest.lib import exceptions + + +LOG = logging.getLogger(__name__) + + +def execute(cmd, action, flags='', params='', fail_ok=False, + merge_stderr=False, cli_dir='/usr/bin'): + """Executes specified command for the given action. + + :param cmd: command to be executed + :type cmd: string + :param action: string of the cli command to run + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: string of any optional positional args to use + :type params: string + :param fail_ok: boolean if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param merge_stderr: boolean if True the stderr buffer is merged into + stdout + :type merge_stderr: boolean + :param cli_dir: The path where the cmd can be executed + :type cli_dir: string + """ + cmd = ' '.join([os.path.join(cli_dir, cmd), + flags, action, params]) + LOG.info("running: '%s'" % cmd) + if six.PY2: + cmd = cmd.encode('utf-8') + cmd = shlex.split(cmd) + result = '' + result_err = '' + stdout = subprocess.PIPE + stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE + proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr) + result, result_err = proc.communicate() + if not fail_ok and proc.returncode != 0: + raise exceptions.CommandFailed(proc.returncode, + cmd, + result, + result_err) + if six.PY2: + return result + else: + return os.fsdecode(result) + + +class CLIClient(object): + """Class to use OpenStack official python client CLI's with auth + + :param username: The username to authenticate with + :type username: string + :param password: The password to authenticate with + :type password: string + :param tenant_name: The name of the tenant to use with the client calls + :type tenant_name: string + :param uri: The auth uri for the OpenStack Deployment + :type uri: string + :param cli_dir: The path where the python client binaries are installed. + defaults to /usr/bin + :type cli_dir: string + :param insecure: if True, --insecure is passed to python client binaries. + :type insecure: boolean + """ + + def __init__(self, username='', password='', tenant_name='', uri='', + cli_dir='', insecure=False, *args, **kwargs): + """Initialize a new CLIClient object.""" + super(CLIClient, self).__init__() + self.cli_dir = cli_dir if cli_dir else '/usr/bin' + self.username = username + self.tenant_name = tenant_name + self.password = password + self.uri = uri + self.insecure = insecure + + def nova(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + """Executes nova command for the given action. + + :param action: the cli command to run using nova + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param endpoint_type: the type of endpoint for the service + :type endpoint_type: string + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + flags += ' --endpoint-type %s' % endpoint_type + return self.cmd_with_auth( + 'nova', action, flags, params, fail_ok, merge_stderr) + + def nova_manage(self, action, flags='', params='', fail_ok=False, + merge_stderr=False): + """Executes nova-manage command for the given action. + + :param action: the cli command to run using nova-manage + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + return execute( + 'nova-manage', action, flags, params, fail_ok, merge_stderr, + self.cli_dir) + + def keystone(self, action, flags='', params='', fail_ok=False, + merge_stderr=False): + """Executes keystone command for the given action. + + :param action: the cli command to run using keystone + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + return self.cmd_with_auth( + 'keystone', action, flags, params, fail_ok, merge_stderr) + + def glance(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + """Executes glance command for the given action. + + :param action: the cli command to run using glance + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param endpoint_type: the type of endpoint for the service + :type endpoint_type: string + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + flags += ' --os-endpoint-type %s' % endpoint_type + return self.cmd_with_auth( + 'glance', action, flags, params, fail_ok, merge_stderr) + + def ceilometer(self, action, flags='', params='', + fail_ok=False, endpoint_type='publicURL', + merge_stderr=False): + """Executes ceilometer command for the given action. + + :param action: the cli command to run using ceilometer + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param endpoint_type: the type of endpoint for the service + :type endpoint_type: string + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + flags += ' --os-endpoint-type %s' % endpoint_type + return self.cmd_with_auth( + 'ceilometer', action, flags, params, fail_ok, merge_stderr) + + def heat(self, action, flags='', params='', + fail_ok=False, endpoint_type='publicURL', merge_stderr=False): + """Executes heat command for the given action. + + :param action: the cli command to run using heat + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param endpoint_type: the type of endpoint for the service + :type endpoint_type: string + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + flags += ' --os-endpoint-type %s' % endpoint_type + return self.cmd_with_auth( + 'heat', action, flags, params, fail_ok, merge_stderr) + + def cinder(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + """Executes cinder command for the given action. + + :param action: the cli command to run using cinder + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param endpoint_type: the type of endpoint for the service + :type endpoint_type: string + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + flags += ' --endpoint-type %s' % endpoint_type + return self.cmd_with_auth( + 'cinder', action, flags, params, fail_ok, merge_stderr) + + def swift(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + """Executes swift command for the given action. + + :param action: the cli command to run using swift + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param endpoint_type: the type of endpoint for the service + :type endpoint_type: string + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + flags += ' --os-endpoint-type %s' % endpoint_type + return self.cmd_with_auth( + 'swift', action, flags, params, fail_ok, merge_stderr) + + def neutron(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + """Executes neutron command for the given action. + + :param action: the cli command to run using neutron + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param endpoint_type: the type of endpoint for the service + :type endpoint_type: string + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + flags += ' --endpoint-type %s' % endpoint_type + return self.cmd_with_auth( + 'neutron', action, flags, params, fail_ok, merge_stderr) + + def sahara(self, action, flags='', params='', + fail_ok=False, endpoint_type='publicURL', merge_stderr=True): + """Executes sahara command for the given action. + + :param action: the cli command to run using sahara + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param endpoint_type: the type of endpoint for the service + :type endpoint_type: string + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + flags += ' --endpoint-type %s' % endpoint_type + return self.cmd_with_auth( + 'sahara', action, flags, params, fail_ok, merge_stderr) + + def openstack(self, action, flags='', params='', fail_ok=False, + merge_stderr=False): + """Executes openstack command for the given action. + + :param action: the cli command to run using openstack + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + return self.cmd_with_auth( + 'openstack', action, flags, params, fail_ok, merge_stderr) + + def cmd_with_auth(self, cmd, action, flags='', params='', + fail_ok=False, merge_stderr=False): + """Executes given command with auth attributes appended. + + :param cmd: command to be executed + :type cmd: string + :param action: command on cli to run + :type action: string + :param flags: optional cli flags to use + :type flags: string + :param params: optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the cli return + code is non-zero + :type fail_ok: boolean + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + creds = ('--os-username %s --os-tenant-name %s --os-password %s ' + '--os-auth-url %s' % + (self.username, + self.tenant_name, + self.password, + self.uri)) + if self.insecure: + flags = creds + ' --insecure ' + flags + else: + flags = creds + ' ' + flags + return execute(cmd, action, flags, params, fail_ok, merge_stderr, + self.cli_dir) + + +class ClientTestBase(base.BaseTestCase): + """Base test class for testing the OpenStack client CLI interfaces.""" + + def setUp(self): + super(ClientTestBase, self).setUp() + self.clients = self._get_clients() + self.parser = tempest.lib.cli.output_parser + + def _get_clients(self): + """Abstract method to initialize CLIClient object. + + This method must be overloaded in child test classes. It should be + used to initialize the CLIClient object with the appropriate + credentials during the setUp() phase of tests. + """ + raise NotImplementedError + + def assertTableStruct(self, items, field_names): + """Verify that all items has keys listed in field_names. + + :param items: items to assert are field names in the output table + :type items: list + :param field_names: field names from the output table of the cmd + :type field_names: list + """ + for item in items: + for field in field_names: + self.assertIn(field, item) + + def assertFirstLineStartsWith(self, lines, beginning): + """Verify that the first line starts with a string + + :param lines: strings for each line of output + :type lines: list + :param beginning: verify this is at the beginning of the first line + :type beginning: string + """ + self.assertTrue(lines[0].startswith(beginning), + msg=('Beginning of first line has invalid content: %s' + % lines[:3])) diff --git a/tempest/lib/cli/output_parser.py b/tempest/lib/cli/output_parser.py new file mode 100644 index 0000000000..0313505904 --- /dev/null +++ b/tempest/lib/cli/output_parser.py @@ -0,0 +1,170 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Collection of utilities for parsing CLI clients output.""" + +import logging +import re + +from tempest.lib import exceptions + + +LOG = logging.getLogger(__name__) + + +delimiter_line = re.compile('^\+\-[\+\-]+\-\+$') + + +def details_multiple(output_lines, with_label=False): + """Return list of dicts with item details from cli output tables. + + If with_label is True, key '__label' is added to each items dict. + For more about 'label' see OutputParser.tables(). + """ + items = [] + tables_ = tables(output_lines) + for table_ in tables_: + if ('Property' not in table_['headers'] + or 'Value' not in table_['headers']): + raise exceptions.InvalidStructure() + item = {} + for value in table_['values']: + item[value[0]] = value[1] + if with_label: + item['__label'] = table_['label'] + items.append(item) + return items + + +def details(output_lines, with_label=False): + """Return dict with details of first item (table) found in output.""" + items = details_multiple(output_lines, with_label) + return items[0] + + +def listing(output_lines): + """Return list of dicts with basic item info parsed from cli output.""" + + items = [] + table_ = table(output_lines) + for row in table_['values']: + item = {} + for col_idx, col_key in enumerate(table_['headers']): + item[col_key] = row[col_idx] + items.append(item) + return items + + +def tables(output_lines): + """Find all ascii-tables in output and parse them. + + Return list of tables parsed from cli output as dicts. + (see OutputParser.table()) + + And, if found, label key (separated line preceding the table) + is added to each tables dict. + """ + tables_ = [] + + table_ = [] + label = None + + start = False + header = False + + if not isinstance(output_lines, list): + output_lines = output_lines.split('\n') + + for line in output_lines: + if delimiter_line.match(line): + if not start: + start = True + elif not header: + # we are after head area + header = True + else: + # table ends here + start = header = None + table_.append(line) + + parsed = table(table_) + parsed['label'] = label + tables_.append(parsed) + + table_ = [] + label = None + continue + if start: + table_.append(line) + else: + if label is None: + label = line + else: + LOG.warning('Invalid line between tables: %s' % line) + if len(table_) > 0: + LOG.warning('Missing end of table') + + return tables_ + + +def table(output_lines): + """Parse single table from cli output. + + Return dict with list of column names in 'headers' key and + rows in 'values' key. + """ + table_ = {'headers': [], 'values': []} + columns = None + + if not isinstance(output_lines, list): + output_lines = output_lines.split('\n') + + if not output_lines[-1]: + # skip last line if empty (just newline at the end) + output_lines = output_lines[:-1] + + for line in output_lines: + if delimiter_line.match(line): + columns = _table_columns(line) + continue + if '|' not in line: + LOG.warning('skipping invalid table line: %s' % line) + continue + row = [] + for col in columns: + row.append(line[col[0]:col[1]].strip()) + if table_['headers']: + table_['values'].append(row) + else: + table_['headers'] = row + + return table_ + + +def _table_columns(first_table_row): + """Find column ranges in output line. + + Return list of tuples (start,end) for each column + detected by plus (+) characters in delimiter line. + """ + positions = [] + start = 1 # there is '+' at 0 + while start < len(first_table_row): + end = first_table_row.find('+', start) + if end == -1: + break + positions.append((start, end)) + start = end + 1 + return positions diff --git a/tempest/lib/cmd/__init__.py b/tempest/lib/cmd/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/cmd/check_uuid.py b/tempest/lib/cmd/check_uuid.py new file mode 100755 index 0000000000..3adeecd419 --- /dev/null +++ b/tempest/lib/cmd/check_uuid.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python + +# Copyright 2014 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import ast +import importlib +import inspect +import os +import sys +import unittest +import uuid + +import six.moves.urllib.parse as urlparse + +DECORATOR_MODULE = 'test' +DECORATOR_NAME = 'idempotent_id' +DECORATOR_IMPORT = 'tempest.%s' % DECORATOR_MODULE +IMPORT_LINE = 'from tempest import %s' % DECORATOR_MODULE +DECORATOR_TEMPLATE = "@%s.%s('%%s')" % (DECORATOR_MODULE, + DECORATOR_NAME) +UNIT_TESTS_EXCLUDE = 'tempest.tests' + + +class SourcePatcher(object): + + """"Lazy patcher for python source files""" + + def __init__(self): + self.source_files = None + self.patches = None + self.clear() + + def clear(self): + """Clear inner state""" + self.source_files = {} + self.patches = {} + + @staticmethod + def _quote(s): + return urlparse.quote(s) + + @staticmethod + def _unquote(s): + return urlparse.unquote(s) + + def add_patch(self, filename, patch, line_no): + """Add lazy patch""" + if filename not in self.source_files: + with open(filename) as f: + self.source_files[filename] = self._quote(f.read()) + patch_id = str(uuid.uuid4()) + if not patch.endswith('\n'): + patch += '\n' + self.patches[patch_id] = self._quote(patch) + lines = self.source_files[filename].split(self._quote('\n')) + lines[line_no - 1] = ''.join(('{%s:s}' % patch_id, lines[line_no - 1])) + self.source_files[filename] = self._quote('\n').join(lines) + + def _save_changes(self, filename, source): + print('%s fixed' % filename) + with open(filename, 'w') as f: + f.write(source) + + def apply_patches(self): + """Apply all patches""" + for filename in self.source_files: + patched_source = self._unquote( + self.source_files[filename].format(**self.patches) + ) + self._save_changes(filename, patched_source) + self.clear() + + +class TestChecker(object): + + def __init__(self, package): + self.package = package + self.base_path = os.path.abspath(os.path.dirname(package.__file__)) + + def _path_to_package(self, path): + relative_path = path[len(self.base_path) + 1:] + if relative_path: + return '.'.join((self.package.__name__,) + + tuple(relative_path.split('/'))) + else: + return self.package.__name__ + + def _modules_search(self): + """Recursive search for python modules in base package""" + modules = [] + for root, dirs, files in os.walk(self.base_path): + if not os.path.exists(os.path.join(root, '__init__.py')): + continue + root_package = self._path_to_package(root) + for item in files: + if item.endswith('.py'): + module_name = '.'.join((root_package, + os.path.splitext(item)[0])) + if not module_name.startswith(UNIT_TESTS_EXCLUDE): + modules.append(module_name) + return modules + + @staticmethod + def _get_idempotent_id(test_node): + """Return key-value dict with all metadata from @test.idempotent_id""" + idempotent_id = None + for decorator in test_node.decorator_list: + if (hasattr(decorator, 'func') and + hasattr(decorator.func, 'attr') and + decorator.func.attr == DECORATOR_NAME and + hasattr(decorator.func, 'value') and + decorator.func.value.id == DECORATOR_MODULE): + for arg in decorator.args: + idempotent_id = ast.literal_eval(arg) + return idempotent_id + + @staticmethod + def _is_decorator(line): + return line.strip().startswith('@') + + @staticmethod + def _is_def(line): + return line.strip().startswith('def ') + + def _add_uuid_to_test(self, patcher, test_node, source_path): + with open(source_path) as src: + src_lines = src.read().split('\n') + lineno = test_node.lineno + insert_position = lineno + while True: + if (self._is_def(src_lines[lineno - 1]) or + (self._is_decorator(src_lines[lineno - 1]) and + (DECORATOR_TEMPLATE.split('(')[0] <= + src_lines[lineno - 1].strip().split('(')[0]))): + insert_position = lineno + break + lineno += 1 + patcher.add_patch( + source_path, + ' ' * test_node.col_offset + DECORATOR_TEMPLATE % uuid.uuid4(), + insert_position + ) + + @staticmethod + def _is_test_case(module, node): + if (node.__class__ is ast.ClassDef and + hasattr(module, node.name) and + inspect.isclass(getattr(module, node.name))): + return issubclass(getattr(module, node.name), unittest.TestCase) + + @staticmethod + def _is_test_method(node): + return (node.__class__ is ast.FunctionDef + and node.name.startswith('test_')) + + @staticmethod + def _next_node(body, node): + if body.index(node) < len(body): + return body[body.index(node) + 1] + + @staticmethod + def _import_name(node): + if type(node) == ast.Import: + return node.names[0].name + elif type(node) == ast.ImportFrom: + return '%s.%s' % (node.module, node.names[0].name) + + def _add_import_for_test_uuid(self, patcher, src_parsed, source_path): + with open(source_path) as f: + src_lines = f.read().split('\n') + line_no = 0 + tempest_imports = [node for node in src_parsed.body + if self._import_name(node) and + 'tempest.' in self._import_name(node)] + if not tempest_imports: + import_snippet = '\n'.join(('', IMPORT_LINE, '')) + else: + for node in tempest_imports: + if self._import_name(node) < DECORATOR_IMPORT: + continue + else: + line_no = node.lineno + import_snippet = IMPORT_LINE + break + else: + line_no = tempest_imports[-1].lineno + while True: + if (not src_lines[line_no - 1] or + getattr(self._next_node(src_parsed.body, + tempest_imports[-1]), + 'lineno') == line_no or + line_no == len(src_lines)): + break + line_no += 1 + import_snippet = '\n'.join((IMPORT_LINE, '')) + patcher.add_patch(source_path, import_snippet, line_no) + + def get_tests(self): + """Get test methods with sources from base package with metadata""" + tests = {} + for module_name in self._modules_search(): + tests[module_name] = {} + module = importlib.import_module(module_name) + source_path = '.'.join( + (os.path.splitext(module.__file__)[0], 'py') + ) + with open(source_path, 'r') as f: + source = f.read() + tests[module_name]['source_path'] = source_path + tests[module_name]['tests'] = {} + source_parsed = ast.parse(source) + tests[module_name]['ast'] = source_parsed + tests[module_name]['import_valid'] = ( + hasattr(module, DECORATOR_MODULE) and + inspect.ismodule(getattr(module, DECORATOR_MODULE)) + ) + test_cases = (node for node in source_parsed.body + if self._is_test_case(module, node)) + for node in test_cases: + for subnode in filter(self._is_test_method, node.body): + test_name = '%s.%s' % (node.name, subnode.name) + tests[module_name]['tests'][test_name] = subnode + return tests + + @staticmethod + def _filter_tests(function, tests): + """Filter tests with condition 'function(test_node) == True'""" + result = {} + for module_name in tests: + for test_name in tests[module_name]['tests']: + if function(module_name, test_name, tests): + if module_name not in result: + result[module_name] = { + 'ast': tests[module_name]['ast'], + 'source_path': tests[module_name]['source_path'], + 'import_valid': tests[module_name]['import_valid'], + 'tests': {} + } + result[module_name]['tests'][test_name] = \ + tests[module_name]['tests'][test_name] + return result + + def find_untagged(self, tests): + """Filter all tests without uuid in metadata""" + def check_uuid_in_meta(module_name, test_name, tests): + idempotent_id = self._get_idempotent_id( + tests[module_name]['tests'][test_name]) + return not idempotent_id + return self._filter_tests(check_uuid_in_meta, tests) + + def report_collisions(self, tests): + """Reports collisions if there are any + + Returns true if collisions exist. + """ + uuids = {} + + def report(module_name, test_name, tests): + test_uuid = self._get_idempotent_id( + tests[module_name]['tests'][test_name]) + if not test_uuid: + return + if test_uuid in uuids: + error_str = "%s:%s\n uuid %s collision: %s<->%s\n%s:%s" % ( + tests[module_name]['source_path'], + tests[module_name]['tests'][test_name].lineno, + test_uuid, + test_name, + uuids[test_uuid]['test_name'], + uuids[test_uuid]['source_path'], + uuids[test_uuid]['test_node'].lineno, + ) + print(error_str) + print("cannot automatically resolve the collision, please " + "manually remove the duplicate value on the new test.") + return True + else: + uuids[test_uuid] = { + 'module': module_name, + 'test_name': test_name, + 'test_node': tests[module_name]['tests'][test_name], + 'source_path': tests[module_name]['source_path'] + } + return bool(self._filter_tests(report, tests)) + + def report_untagged(self, tests): + """Reports untagged tests if there are any + + Returns true if untagged tests exist. + """ + def report(module_name, test_name, tests): + error_str = "%s:%s\nmissing @test.idempotent_id('...')\n%s\n" % ( + tests[module_name]['source_path'], + tests[module_name]['tests'][test_name].lineno, + test_name + ) + print(error_str) + return True + return bool(self._filter_tests(report, tests)) + + def fix_tests(self, tests): + """Add uuids to all specified in tests and fix it in source files""" + patcher = SourcePatcher() + for module_name in tests: + add_import_once = True + for test_name in tests[module_name]['tests']: + if not tests[module_name]['import_valid'] and add_import_once: + self._add_import_for_test_uuid( + patcher, + tests[module_name]['ast'], + tests[module_name]['source_path'] + ) + add_import_once = False + self._add_uuid_to_test( + patcher, tests[module_name]['tests'][test_name], + tests[module_name]['source_path']) + patcher.apply_patches() + + +def run(): + parser = argparse.ArgumentParser() + parser.add_argument('--package', action='store', dest='package', + default='tempest', type=str, + help='Package with tests') + parser.add_argument('--fix', action='store_true', dest='fix_tests', + help='Attempt to fix tests without UUIDs') + args = parser.parse_args() + sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + pkg = importlib.import_module(args.package) + checker = TestChecker(pkg) + errors = False + tests = checker.get_tests() + untagged = checker.find_untagged(tests) + errors = checker.report_collisions(tests) or errors + if args.fix_tests and untagged: + checker.fix_tests(untagged) + else: + errors = checker.report_untagged(untagged) or errors + if errors: + sys.exit("@test.idempotent_id existence and uniqueness checks failed\n" + "Run 'tox -v -euuidgen' to automatically fix tests with\n" + "missing @test.idempotent_id decorators.") + +if __name__ == '__main__': + run() diff --git a/tempest/lib/cmd/skip_tracker.py b/tempest/lib/cmd/skip_tracker.py new file mode 100755 index 0000000000..b5c9b95134 --- /dev/null +++ b/tempest/lib/cmd/skip_tracker.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python2 + +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Track test skips via launchpadlib API and raise alerts if a bug +is fixed but a skip is still in the Tempest test code +""" + +import argparse +import logging +import os +import re + +try: + from launchpadlib import launchpad +except ImportError: + launchpad = None + +LPCACHEDIR = os.path.expanduser('~/.launchpadlib/cache') + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('test_path', help='Path of test dir') + return parser.parse_args() + + +def info(msg, *args, **kwargs): + logging.info(msg, *args, **kwargs) + + +def debug(msg, *args, **kwargs): + logging.debug(msg, *args, **kwargs) + + +def find_skips(start): + """Find the entire list of skiped tests. + + Returns a list of tuples (method, bug) that represent + test methods that have been decorated to skip because of + a particular bug. + """ + results = {} + debug("Searching in %s", start) + for root, _dirs, files in os.walk(start): + for name in files: + if name.startswith('test_') and name.endswith('py'): + path = os.path.join(root, name) + debug("Searching in %s", path) + temp_result = find_skips_in_file(path) + for method_name, bug_no in temp_result: + if results.get(bug_no): + result_dict = results.get(bug_no) + if result_dict.get(name): + result_dict[name].append(method_name) + else: + result_dict[name] = [method_name] + results[bug_no] = result_dict + else: + results[bug_no] = {name: [method_name]} + return results + + +def find_skips_in_file(path): + """Return the skip tuples in a test file.""" + BUG_RE = re.compile(r'\s*@.*skip_because\(bug=[\'"](\d+)[\'"]') + DEF_RE = re.compile(r'\s*def (\w+)\(') + bug_found = False + results = [] + lines = open(path, 'rb').readlines() + for x, line in enumerate(lines): + if not bug_found: + res = BUG_RE.match(line) + if res: + bug_no = int(res.group(1)) + debug("Found bug skip %s on line %d", bug_no, x + 1) + bug_found = True + else: + res = DEF_RE.match(line) + if res: + method = res.group(1) + debug("Found test method %s skips for bug %d", method, bug_no) + results.append((method, bug_no)) + bug_found = False + return results + + +def get_results(result_dict): + results = [] + for bug_no in result_dict.keys(): + for method in result_dict[bug_no]: + results.append((method, bug_no)) + return results + + +def main(): + logging.basicConfig(format='%(levelname)s: %(message)s', + level=logging.INFO) + parser = parse_args() + results = find_skips(parser.test_path) + unique_bugs = sorted(set([bug for (method, bug) in get_results(results)])) + unskips = [] + duplicates = [] + info("Total bug skips found: %d", len(results)) + info("Total unique bugs causing skips: %d", len(unique_bugs)) + if launchpad is not None: + lp = launchpad.Launchpad.login_anonymously('grabbing bugs', + 'production', + LPCACHEDIR) + else: + print("To check the bug status launchpadlib should be installed") + exit(1) + + for bug_no in unique_bugs: + bug = lp.bugs[bug_no] + duplicate = bug.duplicate_of_link + if duplicate is not None: + dup_id = duplicate.split('/')[-1] + duplicates.append((bug_no, dup_id)) + for task in bug.bug_tasks: + info("Bug #%7s (%12s - %12s)", bug_no, + task.importance, task.status) + if task.status in ('Fix Released', 'Fix Committed'): + unskips.append(bug_no) + + for bug_id, dup_id in duplicates: + if bug_id not in unskips: + dup_bug = lp.bugs[dup_id] + for task in dup_bug.bug_tasks: + info("Bug #%7s is a duplicate of Bug#%7s (%12s - %12s)", + bug_id, dup_id, task.importance, task.status) + if task.status in ('Fix Released', 'Fix Committed'): + unskips.append(bug_id) + + unskips = sorted(set(unskips)) + if unskips: + print("The following bugs have been fixed and the corresponding skips") + print("should be removed from the test cases:") + print() + for bug in unskips: + message = " %7s in " % bug + locations = ["%s" % x for x in results[bug].keys()] + message += " and ".join(locations) + print(message) + + +if __name__ == '__main__': + main() diff --git a/tempest/lib/common/__init__.py b/tempest/lib/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/common/http.py b/tempest/lib/common/http.py new file mode 100644 index 0000000000..b3793bc8fa --- /dev/null +++ b/tempest/lib/common/http.py @@ -0,0 +1,25 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 Citrix Systems, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httplib2 + + +class ClosingHttp(httplib2.Http): + def request(self, *args, **kwargs): + original_headers = kwargs.get('headers', {}) + new_headers = dict(original_headers, connection='close') + new_kwargs = dict(kwargs, headers=new_headers) + return super(ClosingHttp, self).request(*args, **new_kwargs) diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py new file mode 100644 index 0000000000..7ce05e3255 --- /dev/null +++ b/tempest/lib/common/rest_client.py @@ -0,0 +1,894 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import collections +import logging as real_logging +import re +import time + +import jsonschema +from oslo_log import log as logging +from oslo_serialization import jsonutils as json +import six + +from tempest.lib.common import http +from tempest.lib.common.utils import misc as misc_utils +from tempest.lib import exceptions + +# redrive rate limited calls at most twice +MAX_RECURSION_DEPTH = 2 + +# All the successful HTTP status codes from RFC 7231 & 4918 +HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207) + +# All the redirection HTTP status codes from RFC 7231 & 4918 +HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307) + +# JSON Schema validator and format checker used for JSON Schema validation +JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator +FORMAT_CHECKER = jsonschema.draft4_format_checker + + +class RestClient(object): + """Unified OpenStack RestClient class + + This class is used for building openstack api clients on top of. It is + intended to provide a base layer for wrapping outgoing http requests in + keystone auth as well as providing response code checking and error + handling. + + :param auth_provider: an auth provider object used to wrap requests in auth + :param str service: The service name to use for the catalog lookup + :param str region: The region to use for the catalog lookup + :param str endpoint_type: The endpoint type to use for the catalog lookup + :param int build_interval: Time in seconds between to status checks in + wait loops + :param int build_timeout: Timeout in seconds to wait for a wait operation. + :param bool disable_ssl_certificate_validation: Set to true to disable ssl + certificate validation + :param str ca_certs: File containing the CA Bundle to use in verifying a + TLS server cert + :param str trace_request: Regex to use for specifying logging the entirety + of the request and response payload + """ + TYPE = "json" + + # The version of the API this client implements + api_version = None + + LOG = logging.getLogger(__name__) + + def __init__(self, auth_provider, service, region, + endpoint_type='publicURL', + build_interval=1, build_timeout=60, + disable_ssl_certificate_validation=False, ca_certs=None, + trace_requests=''): + self.auth_provider = auth_provider + self.service = service + self.region = region + self.endpoint_type = endpoint_type + self.build_interval = build_interval + self.build_timeout = build_timeout + self.trace_requests = trace_requests + + self._skip_path = False + self.general_header_lc = set(('cache-control', 'connection', + 'date', 'pragma', 'trailer', + 'transfer-encoding', 'via', + 'warning')) + self.response_header_lc = set(('accept-ranges', 'age', 'etag', + 'location', 'proxy-authenticate', + 'retry-after', 'server', + 'vary', 'www-authenticate')) + dscv = disable_ssl_certificate_validation + self.http_obj = http.ClosingHttp( + disable_ssl_certificate_validation=dscv, ca_certs=ca_certs) + + def _get_type(self): + return self.TYPE + + def get_headers(self, accept_type=None, send_type=None): + """Return the default headers which will be used with outgoing requests + + :param str accept_type: The media type to use for the Accept header, if + one isn't provided the object var TYPE will be + used + :param str send_type: The media-type to use for the Content-Type + header, if one isn't provided the object var + TYPE will be used + :rtype: dict + :return: The dictionary of headers which can be used in the headers + dict for outgoing request + """ + if accept_type is None: + accept_type = self._get_type() + if send_type is None: + send_type = self._get_type() + return {'Content-Type': 'application/%s' % send_type, + 'Accept': 'application/%s' % accept_type} + + def __str__(self): + STRING_LIMIT = 80 + str_format = ("service:%s, base_url:%s, " + "filters: %s, build_interval:%s, build_timeout:%s" + "\ntoken:%s..., \nheaders:%s...") + return str_format % (self.service, self.base_url, + self.filters, self.build_interval, + self.build_timeout, + str(self.token)[0:STRING_LIMIT], + str(self.get_headers())[0:STRING_LIMIT]) + + @property + def user(self): + """The username used for requests + + :rtype: string + :return: The username being used for requests + """ + + return self.auth_provider.credentials.username + + @property + def user_id(self): + """The user_id used for requests + + :rtype: string + :return: The user id being used for requests + """ + return self.auth_provider.credentials.user_id + + @property + def tenant_name(self): + """The tenant/project being used for requests + + :rtype: string + :return: The tenant/project name being used for requests + """ + return self.auth_provider.credentials.tenant_name + + @property + def tenant_id(self): + """The tenant/project id being used for requests + + :rtype: string + :return: The tenant/project id being used for requests + """ + return self.auth_provider.credentials.tenant_id + + @property + def password(self): + """The password being used for requests + + :rtype: string + :return: The password being used for requests + """ + return self.auth_provider.credentials.password + + @property + def base_url(self): + return self.auth_provider.base_url(filters=self.filters) + + @property + def token(self): + return self.auth_provider.get_token() + + @property + def filters(self): + _filters = dict( + service=self.service, + endpoint_type=self.endpoint_type, + region=self.region + ) + if self.api_version is not None: + _filters['api_version'] = self.api_version + if self._skip_path: + _filters['skip_path'] = self._skip_path + return _filters + + def skip_path(self): + """When set, ignore the path part of the base URL from the catalog""" + self._skip_path = True + + def reset_path(self): + """When reset, use the base URL from the catalog as-is""" + self._skip_path = False + + @classmethod + def expected_success(cls, expected_code, read_code): + """Check expected success response code against the http response + + :param int expected_code: The response code that is expected. + Optionally a list of integers can be used + to specify multiple valid success codes + :param int read_code: The response code which was returned in the + response + :raises AssertionError: if the expected_code isn't a valid http success + response code + :raises exceptions.InvalidHttpSuccessCode: if the read code isn't an + expected http success code + """ + assert_msg = ("This function only allowed to use for HTTP status" + "codes which explicitly defined in the RFC 7231 & 4918." + "{0} is not a defined Success Code!" + ).format(expected_code) + if isinstance(expected_code, list): + for code in expected_code: + assert code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg + else: + assert expected_code in HTTP_SUCCESS + HTTP_REDIRECTION, assert_msg + + # NOTE(afazekas): the http status code above 400 is processed by + # the _error_checker method + if read_code < 400: + pattern = """Unexpected http success status code {0}, + The expected status code is {1}""" + if ((not isinstance(expected_code, list) and + (read_code != expected_code)) or + (isinstance(expected_code, list) and + (read_code not in expected_code))): + details = pattern.format(read_code, expected_code) + raise exceptions.InvalidHttpSuccessCode(details) + + def post(self, url, body, headers=None, extra_headers=False): + """Send a HTTP POST request using keystone auth + + :param str url: the relative url to send the post request to + :param dict body: the request body + :param dict headers: The headers to use for the request + :param dict extra_headers: If the headers returned by the get_headers() + method are to be used but additional headers + are needed in the request pass them in as a + dict + :return: a tuple with the first entry containing the response headers + and the second the response body + :rtype: tuple + """ + return self.request('POST', url, extra_headers, headers, body) + + def get(self, url, headers=None, extra_headers=False): + """Send a HTTP GET request using keystone service catalog and auth + + :param str url: the relative url to send the post request to + :param dict headers: The headers to use for the request + :param dict extra_headers: If the headers returned by the get_headers() + method are to be used but additional headers + are needed in the request pass them in as a + dict + :return: a tuple with the first entry containing the response headers + and the second the response body + :rtype: tuple + """ + return self.request('GET', url, extra_headers, headers) + + def delete(self, url, headers=None, body=None, extra_headers=False): + """Send a HTTP DELETE request using keystone service catalog and auth + + :param str url: the relative url to send the post request to + :param dict headers: The headers to use for the request + :param dict body: the request body + :param dict extra_headers: If the headers returned by the get_headers() + method are to be used but additional headers + are needed in the request pass them in as a + dict + :return: a tuple with the first entry containing the response headers + and the second the response body + :rtype: tuple + """ + return self.request('DELETE', url, extra_headers, headers, body) + + def patch(self, url, body, headers=None, extra_headers=False): + """Send a HTTP PATCH request using keystone service catalog and auth + + :param str url: the relative url to send the post request to + :param dict body: the request body + :param dict headers: The headers to use for the request + :param dict extra_headers: If the headers returned by the get_headers() + method are to be used but additional headers + are needed in the request pass them in as a + dict + :return: a tuple with the first entry containing the response headers + and the second the response body + :rtype: tuple + """ + return self.request('PATCH', url, extra_headers, headers, body) + + def put(self, url, body, headers=None, extra_headers=False): + """Send a HTTP PUT request using keystone service catalog and auth + + :param str url: the relative url to send the post request to + :param dict body: the request body + :param dict headers: The headers to use for the request + :param dict extra_headers: If the headers returned by the get_headers() + method are to be used but additional headers + are needed in the request pass them in as a + dict + :return: a tuple with the first entry containing the response headers + and the second the response body + :rtype: tuple + """ + return self.request('PUT', url, extra_headers, headers, body) + + def head(self, url, headers=None, extra_headers=False): + """Send a HTTP HEAD request using keystone service catalog and auth + + :param str url: the relative url to send the post request to + :param dict headers: The headers to use for the request + :param dict extra_headers: If the headers returned by the get_headers() + method are to be used but additional headers + are needed in the request pass them in as a + dict + :return: a tuple with the first entry containing the response headers + and the second the response body + :rtype: tuple + """ + return self.request('HEAD', url, extra_headers, headers) + + def copy(self, url, headers=None, extra_headers=False): + """Send a HTTP COPY request using keystone service catalog and auth + + :param str url: the relative url to send the post request to + :param dict headers: The headers to use for the request + :param dict extra_headers: If the headers returned by the get_headers() + method are to be used but additional headers + are needed in the request pass them in as a + dict + :return: a tuple with the first entry containing the response headers + and the second the response body + :rtype: tuple + """ + return self.request('COPY', url, extra_headers, headers) + + def get_versions(self): + """Get the versions on a endpoint from the keystone catalog + + This method will make a GET request on the baseurl from the keystone + catalog to return a list of API versions. It is expected that a GET + on the endpoint in the catalog will return a list of supported API + versions. + + :return tuple with response headers and list of version numbers + :rtype: tuple + """ + resp, body = self.get('') + body = self._parse_resp(body) + versions = map(lambda x: x['id'], body) + return resp, versions + + def _get_request_id(self, resp): + for i in ('x-openstack-request-id', 'x-compute-request-id'): + if i in resp: + return resp[i] + return "" + + def _safe_body(self, body, maxlen=4096): + # convert a structure into a string safely + try: + text = six.text_type(body) + except UnicodeDecodeError: + # if this isn't actually text, return marker that + return "" + if len(text) > maxlen: + return text[:maxlen] + else: + return text + + def _log_request_start(self, method, req_url, req_headers=None, + req_body=None): + if req_headers is None: + req_headers = {} + caller_name = misc_utils.find_test_caller() + if self.trace_requests and re.search(self.trace_requests, caller_name): + self.LOG.debug('Starting Request (%s): %s %s' % + (caller_name, method, req_url)) + + def _log_request_full(self, method, req_url, resp, + secs="", req_headers=None, + req_body=None, resp_body=None, + caller_name=None, extra=None): + if 'X-Auth-Token' in req_headers: + req_headers['X-Auth-Token'] = '' + log_fmt = """Request - Headers: %s + Body: %s + Response - Headers: %s + Body: %s""" + + self.LOG.debug( + log_fmt % ( + str(req_headers), + self._safe_body(req_body), + str(resp), + self._safe_body(resp_body)), + extra=extra) + + def _log_request(self, method, req_url, resp, + secs="", req_headers=None, + req_body=None, resp_body=None): + if req_headers is None: + req_headers = {} + # if we have the request id, put it in the right part of the log + extra = dict(request_id=self._get_request_id(resp)) + # NOTE(sdague): while we still have 6 callers to this function + # we're going to just provide work around on who is actually + # providing timings by gracefully adding no content if they don't. + # Once we're down to 1 caller, clean this up. + caller_name = misc_utils.find_test_caller() + if secs: + secs = " %.3fs" % secs + self.LOG.info( + 'Request (%s): %s %s %s%s' % ( + caller_name, + resp['status'], + method, + req_url, + secs), + extra=extra) + + # Also look everything at DEBUG if you want to filter this + # out, don't run at debug. + if self.LOG.isEnabledFor(real_logging.DEBUG): + self._log_request_full(method, req_url, resp, secs, req_headers, + req_body, resp_body, caller_name, extra) + + def _parse_resp(self, body): + try: + body = json.loads(body) + except ValueError: + return body + + # We assume, that if the first value of the deserialized body's + # item set is a dict or a list, that we just return the first value + # of deserialized body. + # Essentially "cutting out" the first placeholder element in a body + # that looks like this: + # + # { + # "users": [ + # ... + # ] + # } + try: + # Ensure there are not more than one top-level keys + # NOTE(freerunner): Ensure, that JSON is not nullable to + # to prevent StopIteration Exception + if len(body.keys()) != 1: + return body + # Just return the "wrapped" element + first_key, first_item = six.next(six.iteritems(body)) + if isinstance(first_item, (dict, list)): + return first_item + except (ValueError, IndexError): + pass + return body + + def response_checker(self, method, resp, resp_body): + """A sanity check on the response from a HTTP request + + This method does a sanity check on whether the response from an HTTP + request conforms the HTTP RFC. + + :param str method: The HTTP verb of the request associated with the + response being passed in. + :param resp: The response headers + :param resp_body: The body of the response + :raises ResponseWithNonEmptyBody: If the response with the status code + is not supposed to have a body + :raises ResponseWithEntity: If the response code is 205 but has an + entity + """ + if (resp.status in set((204, 205, 304)) or resp.status < 200 or + method.upper() == 'HEAD') and resp_body: + raise exceptions.ResponseWithNonEmptyBody(status=resp.status) + # NOTE(afazekas): + # If the HTTP Status Code is 205 + # 'The response MUST NOT include an entity.' + # A HTTP entity has an entity-body and an 'entity-header'. + # In the HTTP response specification (Section 6) the 'entity-header' + # 'generic-header' and 'response-header' are in OR relation. + # All headers not in the above two group are considered as entity + # header in every interpretation. + + if (resp.status == 205 and + 0 != len(set(resp.keys()) - set(('status',)) - + self.response_header_lc - self.general_header_lc)): + raise exceptions.ResponseWithEntity() + # NOTE(afazekas) + # Now the swift sometimes (delete not empty container) + # returns with non json error response, we can create new rest class + # for swift. + # Usually RFC2616 says error responses SHOULD contain an explanation. + # The warning is normal for SHOULD/SHOULD NOT case + + # Likely it will cause an error + if method != 'HEAD' and not resp_body and resp.status >= 400: + self.LOG.warning("status >= 400 response with empty body") + + def _request(self, method, url, headers=None, body=None): + """A simple HTTP request interface.""" + # Authenticate the request with the auth provider + req_url, req_headers, req_body = self.auth_provider.auth_request( + method, url, headers, body, self.filters) + + # Do the actual request, and time it + start = time.time() + self._log_request_start(method, req_url) + resp, resp_body = self.raw_request( + req_url, method, headers=req_headers, body=req_body) + end = time.time() + self._log_request(method, req_url, resp, secs=(end - start), + req_headers=req_headers, req_body=req_body, + resp_body=resp_body) + + # Verify HTTP response codes + self.response_checker(method, resp, resp_body) + + return resp, resp_body + + def raw_request(self, url, method, headers=None, body=None): + """Send a raw HTTP request without the keystone catalog or auth + + This method sends a HTTP request in the same manner as the request() + method, however it does so without using keystone auth or the catalog + to determine the base url. Additionally no response handling is done + the results from the request are just returned. + + :param str url: Full url to send the request + :param str method: The HTTP verb to use for the request + :param str headers: Headers to use for the request if none are specifed + the headers + :param str body: Body to to send with the request + :rtype: tuple + :return: a tuple with the first entry containing the response headers + and the second the response body + """ + if headers is None: + headers = self.get_headers() + return self.http_obj.request(url, method, + headers=headers, body=body) + + def request(self, method, url, extra_headers=False, headers=None, + body=None): + """Send a HTTP request with keystone auth and using the catalog + + This method will send an HTTP request using keystone auth in the + headers and the catalog to determine the endpoint to use for the + baseurl to send the request to. Additionally + + When a response is received it will check it to see if an error + response was received. If it was an exception will be raised to enable + it to be handled quickly. + + This method will also handle rate-limiting, if a 413 response code is + received it will retry the request after waiting the 'retry-after' + duration from the header. + + :param str method: The HTTP verb to use for the request + :param str url: Relative url to send the request to + :param dict extra_headers: If specified without the headers kwarg the + headers sent with the request will be the + combination from the get_headers() method + and this kwarg + :param dict headers: Headers to use for the request if none are + specifed the headers returned from the + get_headers() method are used. If the request + explicitly requires no headers use an empty dict. + :param str body: Body to to send with the request + :rtype: tuple + :return: a tuple with the first entry containing the response headers + and the second the response body + :raises UnexpectedContentType: If the content-type of the response + isn't an expect type + :raises Unauthorized: If a 401 response code is received + :raises Forbidden: If a 403 response code is received + :raises NotFound: If a 404 response code is received + :raises BadRequest: If a 400 response code is received + :raises Gone: If a 410 response code is received + :raises Conflict: If a 409 response code is received + :raises OverLimit: If a 413 response code is received and over_limit is + not in the response body + :raises RateLimitExceeded: If a 413 response code is received and + over_limit is in the response body + :raises InvalidContentType: If a 415 response code is received + :raises UnprocessableEntity: If a 422 response code is received + :raises InvalidHTTPResponseBody: The response body wasn't valid JSON + and couldn't be parsed + :raises NotImplemented: If a 501 response code is received + :raises ServerFault: If a 500 response code is received + :raises UnexpectedResponseCode: If a response code above 400 is + received and it doesn't fall into any + of the handled checks + """ + # if extra_headers is True + # default headers would be added to headers + retry = 0 + + if headers is None: + # NOTE(vponomaryov): if some client do not need headers, + # it should explicitly pass empty dict + headers = self.get_headers() + elif extra_headers: + try: + headers = headers.copy() + headers.update(self.get_headers()) + except (ValueError, TypeError): + headers = self.get_headers() + + resp, resp_body = self._request(method, url, + headers=headers, body=body) + + while (resp.status == 413 and + 'retry-after' in resp and + not self.is_absolute_limit( + resp, self._parse_resp(resp_body)) and + retry < MAX_RECURSION_DEPTH): + retry += 1 + delay = int(resp['retry-after']) + time.sleep(delay) + resp, resp_body = self._request(method, url, + headers=headers, body=body) + self._error_checker(method, url, headers, body, + resp, resp_body) + return resp, resp_body + + def _error_checker(self, method, url, + headers, body, resp, resp_body): + + # NOTE(mtreinish): Check for httplib response from glance_http. The + # object can't be used here because importing httplib breaks httplib2. + # If another object from a class not imported were passed here as + # resp this could possibly fail + if str(type(resp)) == "": + ctype = resp.getheader('content-type') + else: + try: + ctype = resp['content-type'] + # NOTE(mtreinish): Keystone delete user responses doesn't have a + # content-type header. (They don't have a body) So just pretend it + # is set. + except KeyError: + ctype = 'application/json' + + # It is not an error response + if resp.status < 400: + return + + JSON_ENC = ['application/json', 'application/json; charset=utf-8'] + # NOTE(mtreinish): This is for compatibility with Glance and swift + # APIs. These are the return content types that Glance api v1 + # (and occasionally swift) are using. + TXT_ENC = ['text/plain', 'text/html', 'text/html; charset=utf-8', + 'text/plain; charset=utf-8'] + + if ctype.lower() in JSON_ENC: + parse_resp = True + elif ctype.lower() in TXT_ENC: + parse_resp = False + else: + raise exceptions.UnexpectedContentType(str(resp.status), + resp=resp) + + if resp.status == 401: + if parse_resp: + resp_body = self._parse_resp(resp_body) + raise exceptions.Unauthorized(resp_body, resp=resp) + + if resp.status == 403: + if parse_resp: + resp_body = self._parse_resp(resp_body) + raise exceptions.Forbidden(resp_body, resp=resp) + + if resp.status == 404: + if parse_resp: + resp_body = self._parse_resp(resp_body) + raise exceptions.NotFound(resp_body, resp=resp) + + if resp.status == 400: + if parse_resp: + resp_body = self._parse_resp(resp_body) + raise exceptions.BadRequest(resp_body, resp=resp) + + if resp.status == 410: + if parse_resp: + resp_body = self._parse_resp(resp_body) + raise exceptions.Gone(resp_body, resp=resp) + + if resp.status == 409: + if parse_resp: + resp_body = self._parse_resp(resp_body) + raise exceptions.Conflict(resp_body, resp=resp) + + if resp.status == 413: + if parse_resp: + resp_body = self._parse_resp(resp_body) + if self.is_absolute_limit(resp, resp_body): + raise exceptions.OverLimit(resp_body, resp=resp) + else: + raise exceptions.RateLimitExceeded(resp_body, resp=resp) + + if resp.status == 415: + if parse_resp: + resp_body = self._parse_resp(resp_body) + raise exceptions.InvalidContentType(resp_body, resp=resp) + + if resp.status == 422: + if parse_resp: + resp_body = self._parse_resp(resp_body) + raise exceptions.UnprocessableEntity(resp_body, resp=resp) + + if resp.status in (500, 501): + message = resp_body + if parse_resp: + try: + resp_body = self._parse_resp(resp_body) + except ValueError: + # If response body is a non-json string message. + # Use resp_body as is and raise InvalidResponseBody + # exception. + raise exceptions.InvalidHTTPResponseBody(message) + else: + if isinstance(resp_body, dict): + # I'm seeing both computeFault + # and cloudServersFault come back. + # Will file a bug to fix, but leave as is for now. + if 'cloudServersFault' in resp_body: + message = resp_body['cloudServersFault']['message'] + elif 'computeFault' in resp_body: + message = resp_body['computeFault']['message'] + elif 'error' in resp_body: + message = resp_body['error']['message'] + elif 'message' in resp_body: + message = resp_body['message'] + else: + message = resp_body + + if resp.status == 501: + raise exceptions.NotImplemented(resp_body, resp=resp, + message=message) + else: + raise exceptions.ServerFault(resp_body, resp=resp, + message=message) + + if resp.status >= 400: + raise exceptions.UnexpectedResponseCode(str(resp.status), + resp=resp) + + def is_absolute_limit(self, resp, resp_body): + if (not isinstance(resp_body, collections.Mapping) or + 'retry-after' not in resp): + return True + over_limit = resp_body.get('overLimit', None) + if not over_limit: + return True + return 'exceed' in over_limit.get('message', 'blabla') + + def wait_for_resource_deletion(self, id): + """Waits for a resource to be deleted + + This method will loop over is_resource_deleted until either + is_resource_deleted returns True or the build timeout is reached. This + depends on is_resource_deleted being implemented + + :param str id: The id of the resource to check + :raises TimeoutException: If the build_timeout has elapsed and the + resource still hasn't been deleted + """ + start_time = int(time.time()) + while True: + if self.is_resource_deleted(id): + return + if int(time.time()) - start_time >= self.build_timeout: + message = ('Failed to delete %(resource_type)s %(id)s within ' + 'the required time (%(timeout)s s).' % + {'resource_type': self.resource_type, 'id': id, + 'timeout': self.build_timeout}) + caller = misc_utils.find_test_caller() + if caller: + message = '(%s) %s' % (caller, message) + raise exceptions.TimeoutException(message) + time.sleep(self.build_interval) + + def is_resource_deleted(self, id): + """Subclasses override with specific deletion detection.""" + message = ('"%s" does not implement is_resource_deleted' + % self.__class__.__name__) + raise NotImplementedError(message) + + @property + def resource_type(self): + """Returns the primary type of resource this client works with.""" + return 'resource' + + @classmethod + def validate_response(cls, schema, resp, body): + # Only check the response if the status code is a success code + # TODO(cyeoh): Eventually we should be able to verify that a failure + # code if it exists is something that we expect. This is explicitly + # declared in the V3 API and so we should be able to export this in + # the response schema. For now we'll ignore it. + if resp.status in HTTP_SUCCESS + HTTP_REDIRECTION: + cls.expected_success(schema['status_code'], resp.status) + + # Check the body of a response + body_schema = schema.get('response_body') + if body_schema: + try: + jsonschema.validate(body, body_schema, + cls=JSONSCHEMA_VALIDATOR, + format_checker=FORMAT_CHECKER) + except jsonschema.ValidationError as ex: + msg = ("HTTP response body is invalid (%s)") % ex + raise exceptions.InvalidHTTPResponseBody(msg) + else: + if body: + msg = ("HTTP response body should not exist (%s)") % body + raise exceptions.InvalidHTTPResponseBody(msg) + + # Check the header of a response + header_schema = schema.get('response_header') + if header_schema: + try: + jsonschema.validate(resp, header_schema, + cls=JSONSCHEMA_VALIDATOR, + format_checker=FORMAT_CHECKER) + except jsonschema.ValidationError as ex: + msg = ("HTTP response header is invalid (%s)") % ex + raise exceptions.InvalidHTTPResponseHeader(msg) + + +class ResponseBody(dict): + """Class that wraps an http response and dict body into a single value. + + Callers that receive this object will normally use it as a dict but + can extract the response if needed. + """ + + def __init__(self, response, body=None): + body_data = body or {} + self.update(body_data) + self.response = response + + def __str__(self): + body = super(ResponseBody, self).__str__() + return "response: %s\nBody: %s" % (self.response, body) + + +class ResponseBodyData(object): + """Class that wraps an http response and string data into a single value. + + """ + + def __init__(self, response, data): + self.response = response + self.data = data + + def __str__(self): + return "response: %s\nBody: %s" % (self.response, self.data) + + +class ResponseBodyList(list): + """Class that wraps an http response and list body into a single value. + + Callers that receive this object will normally use it as a list but + can extract the response if needed. + """ + + def __init__(self, response, body=None): + body_data = body or [] + self.extend(body_data) + self.response = response + + def __str__(self): + body = super(ResponseBodyList, self).__str__() + return "response: %s\nBody: %s" % (self.response, body) diff --git a/tempest/lib/common/ssh.py b/tempest/lib/common/ssh.py new file mode 100644 index 0000000000..511dd08fdd --- /dev/null +++ b/tempest/lib/common/ssh.py @@ -0,0 +1,174 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import select +import socket +import time +import warnings + +from oslo_log import log as logging +import six + +from tempest.lib import exceptions + + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + import paramiko + + +LOG = logging.getLogger(__name__) + + +class Client(object): + + def __init__(self, host, username, password=None, timeout=300, pkey=None, + channel_timeout=10, look_for_keys=False, key_filename=None): + self.host = host + self.username = username + self.password = password + if isinstance(pkey, six.string_types): + pkey = paramiko.RSAKey.from_private_key( + six.StringIO(str(pkey))) + self.pkey = pkey + self.look_for_keys = look_for_keys + self.key_filename = key_filename + self.timeout = int(timeout) + self.channel_timeout = float(channel_timeout) + self.buf_size = 1024 + + def _get_ssh_connection(self, sleep=1.5, backoff=1): + """Returns an ssh connection to the specified host.""" + bsleep = sleep + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy( + paramiko.AutoAddPolicy()) + _start_time = time.time() + if self.pkey is not None: + LOG.info("Creating ssh connection to '%s' as '%s'" + " with public key authentication", + self.host, self.username) + else: + LOG.info("Creating ssh connection to '%s' as '%s'" + " with password %s", + self.host, self.username, str(self.password)) + attempts = 0 + while True: + try: + ssh.connect(self.host, username=self.username, + password=self.password, + look_for_keys=self.look_for_keys, + key_filename=self.key_filename, + timeout=self.channel_timeout, pkey=self.pkey) + LOG.info("ssh connection to %s@%s successfully created", + self.username, self.host) + return ssh + except (EOFError, + socket.error, + paramiko.SSHException) as e: + if self._is_timed_out(_start_time): + LOG.exception("Failed to establish authenticated ssh" + " connection to %s@%s after %d attempts", + self.username, self.host, attempts) + raise exceptions.SSHTimeout(host=self.host, + user=self.username, + password=self.password) + bsleep += backoff + attempts += 1 + LOG.warning("Failed to establish authenticated ssh" + " connection to %s@%s (%s). Number attempts: %s." + " Retry after %d seconds.", + self.username, self.host, e, attempts, bsleep) + time.sleep(bsleep) + + def _is_timed_out(self, start_time): + return (time.time() - self.timeout) > start_time + + @staticmethod + def _can_system_poll(): + return hasattr(select, 'poll') + + def exec_command(self, cmd, encoding="utf-8"): + """Execute the specified command on the server + + Note that this method is reading whole command outputs to memory, thus + shouldn't be used for large outputs. + + :param str cmd: Command to run at remote server. + :param str encoding: Encoding for result from paramiko. + Result will not be decoded if None. + :returns: data read from standard output of the command. + :raises: SSHExecCommandFailed if command returns nonzero + status. The exception contains command status stderr content. + :raises: TimeoutException if cmd doesn't end when timeout expires. + """ + ssh = self._get_ssh_connection() + transport = ssh.get_transport() + channel = transport.open_session() + channel.fileno() # Register event pipe + channel.exec_command(cmd) + channel.shutdown_write() + exit_status = channel.recv_exit_status() + + # If the executing host is linux-based, poll the channel + if self._can_system_poll(): + out_data_chunks = [] + err_data_chunks = [] + poll = select.poll() + poll.register(channel, select.POLLIN) + start_time = time.time() + + while True: + ready = poll.poll(self.channel_timeout) + if not any(ready): + if not self._is_timed_out(start_time): + continue + raise exceptions.TimeoutException( + "Command: '{0}' executed on host '{1}'.".format( + cmd, self.host)) + if not ready[0]: # If there is nothing to read. + continue + out_chunk = err_chunk = None + if channel.recv_ready(): + out_chunk = channel.recv(self.buf_size) + out_data_chunks += out_chunk, + if channel.recv_stderr_ready(): + err_chunk = channel.recv_stderr(self.buf_size) + err_data_chunks += err_chunk, + if channel.closed and not err_chunk and not out_chunk: + break + out_data = b''.join(out_data_chunks) + err_data = b''.join(err_data_chunks) + # Just read from the channels + else: + out_file = channel.makefile('rb', self.buf_size) + err_file = channel.makefile_stderr('rb', self.buf_size) + out_data = out_file.read() + err_data = err_file.read() + if encoding: + out_data = out_data.decode(encoding) + err_data = err_data.decode(encoding) + + if 0 != exit_status: + raise exceptions.SSHExecCommandFailed( + command=cmd, exit_status=exit_status, + stderr=err_data, stdout=out_data) + return out_data + + def test_connection_auth(self): + """Raises an exception when we can not connect to server via ssh.""" + connection = self._get_ssh_connection() + connection.close() diff --git a/tempest/lib/common/utils/__init__.py b/tempest/lib/common/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/common/utils/data_utils.py b/tempest/lib/common/utils/data_utils.py new file mode 100644 index 0000000000..01b6477029 --- /dev/null +++ b/tempest/lib/common/utils/data_utils.py @@ -0,0 +1,186 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import itertools +import netaddr +import random +import string +import uuid + + +def rand_uuid(): + """Generate a random UUID string + + :return: a random UUID (e.g. '1dc12c7d-60eb-4b61-a7a2-17cf210155b6') + :rtype: string + """ + return str(uuid.uuid4()) + + +def rand_uuid_hex(): + """Generate a random UUID hex string + + :return: a random UUID (e.g. '0b98cf96d90447bda4b46f31aeb1508c') + :rtype: string + """ + return uuid.uuid4().hex + + +def rand_name(name='', prefix=None): + """Generate a random name that inclues a random number + + :param str name: The name that you want to include + :param str prefix: The prefix that you want to include + :return: a random name. The format is + '---'. + (e.g. 'prefixfoo-1308607012-namebar-154876201') + :rtype: string + """ + randbits = str(random.randint(1, 0x7fffffff)) + rand_name = randbits + if name: + rand_name = name + '-' + rand_name + if prefix: + rand_name = prefix + '-' + rand_name + return rand_name + + +def rand_password(length=15): + """Generate a random password + + :param int length: The length of password that you expect to set + (If it's smaller than 3, it's same as 3.) + :return: a random password. The format is + '-- + -' + (e.g. 'G2*ac8&lKFFgh%2') + :rtype: string + """ + upper = random.choice(string.ascii_uppercase) + ascii_char = string.ascii_letters + digits = string.digits + digit = random.choice(string.digits) + puncs = '~!@#$%^&*_=+' + punc = random.choice(puncs) + seed = ascii_char + digits + puncs + pre = upper + digit + punc + password = pre + ''.join(random.choice(seed) for x in range(length - 3)) + return password + + +def rand_url(): + """Generate a random url that inclues a random number + + :return: a random url. The format is 'https://url-.com'. + (e.g. 'https://url-154876201.com') + :rtype: string + """ + randbits = str(random.randint(1, 0x7fffffff)) + return 'https://url-' + randbits + '.com' + + +def rand_int_id(start=0, end=0x7fffffff): + """Generate a random integer value + + :param int start: The value that you expect to start here + :param int end: The value that you expect to end here + :return: a random integer value + :rtype: int + """ + return random.randint(start, end) + + +def rand_mac_address(): + """Generate an Ethernet MAC address + + :return: an random Ethernet MAC address + :rtype: string + """ + # NOTE(vish): We would prefer to use 0xfe here to ensure that linux + # bridge mac addresses don't change, but it appears to + # conflict with libvirt, so we use the next highest octet + # that has the unicast and locally administered bits set + # properly: 0xfa. + # Discussion: https://bugs.launchpad.net/nova/+bug/921838 + mac = [0xfa, 0x16, 0x3e, + random.randint(0x00, 0xff), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff)] + return ':'.join(["%02x" % x for x in mac]) + + +def parse_image_id(image_ref): + """Return the image id from a given image ref + + This function just returns the last word of the given image ref string + splitting with '/'. + :param str image_ref: a string that includes the image id + :return: the image id string + :rtype: string + """ + return image_ref.rsplit('/')[-1] + + +def arbitrary_string(size=4, base_text=None): + """Return size characters from base_text + + This generates a string with an arbitrary number of characters, generated + by looping the base_text string. If the size is smaller than the size of + base_text, returning string is shrinked to the size. + :param int size: a returning charactors size + :param str base_text: a string you want to repeat + :return: size string + :rtype: string + """ + if not base_text: + base_text = 'test' + return ''.join(itertools.islice(itertools.cycle(base_text), size)) + + +def random_bytes(size=1024): + """Return size randomly selected bytes as a string + + :param int size: a returning bytes size + :return: size randomly bytes + :rtype: string + """ + return ''.join([chr(random.randint(0, 255)) + for i in range(size)]) + + +def get_ipv6_addr_by_EUI64(cidr, mac): + """Generate a IPv6 addr by EUI-64 with CIDR and MAC + + :param str cidr: a IPv6 CIDR + :param str mac: a MAC address + :return: an IPv6 Address + :rtype: netaddr.IPAddress + """ + # Check if the prefix is IPv4 address + is_ipv4 = netaddr.valid_ipv4(cidr) + if is_ipv4: + msg = "Unable to generate IP address by EUI64 for IPv4 prefix" + raise TypeError(msg) + try: + eui64 = int(netaddr.EUI(mac).eui64()) + prefix = netaddr.IPNetwork(cidr) + return netaddr.IPAddress(prefix.first + eui64 ^ (1 << 57)) + except (ValueError, netaddr.AddrFormatError): + raise TypeError('Bad prefix or mac format for generating IPv6 ' + 'address by EUI-64: %(prefix)s, %(mac)s:' + % {'prefix': cidr, 'mac': mac}) + except TypeError: + raise TypeError('Bad prefix type for generate IPv6 address by ' + 'EUI-64: %s' % cidr) diff --git a/tempest/lib/common/utils/misc.py b/tempest/lib/common/utils/misc.py new file mode 100644 index 0000000000..b97dd8627a --- /dev/null +++ b/tempest/lib/common/utils/misc.py @@ -0,0 +1,87 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import inspect +import re + +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +def singleton(cls): + """Simple wrapper for classes that should only have a single instance.""" + instances = {} + + def getinstance(): + if cls not in instances: + instances[cls] = cls() + return instances[cls] + return getinstance + + +def find_test_caller(): + """Find the caller class and test name. + + Because we know that the interesting things that call us are + test_* methods, and various kinds of setUp / tearDown, we + can look through the call stack to find appropriate methods, + and the class we were in when those were called. + """ + caller_name = None + names = [] + frame = inspect.currentframe() + is_cleanup = False + # Start climbing the ladder until we hit a good method + while True: + try: + frame = frame.f_back + name = frame.f_code.co_name + names.append(name) + if re.search("^(test_|setUp|tearDown)", name): + cname = "" + if 'self' in frame.f_locals: + cname = frame.f_locals['self'].__class__.__name__ + if 'cls' in frame.f_locals: + cname = frame.f_locals['cls'].__name__ + caller_name = cname + ":" + name + break + elif re.search("^_run_cleanup", name): + is_cleanup = True + elif name == 'main': + caller_name = 'main' + break + else: + cname = "" + if 'self' in frame.f_locals: + cname = frame.f_locals['self'].__class__.__name__ + if 'cls' in frame.f_locals: + cname = frame.f_locals['cls'].__name__ + + # the fact that we are running cleanups is indicated pretty + # deep in the stack, so if we see that we want to just + # start looking for a real class name, and declare victory + # once we do. + if is_cleanup and cname: + if not re.search("^RunTest", cname): + caller_name = cname + ":_run_cleanups" + break + except Exception: + break + # prevents frame leaks + del frame + if caller_name is None: + LOG.debug("Sane call name not found in %s" % names) + return caller_name diff --git a/tempest/lib/decorators.py b/tempest/lib/decorators.py new file mode 100644 index 0000000000..e78e624505 --- /dev/null +++ b/tempest/lib/decorators.py @@ -0,0 +1,80 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 uuid + +import six +import testtools + + +def skip_because(*args, **kwargs): + """A decorator useful to skip tests hitting known bugs + + @param bug: bug number causing the test to skip + @param condition: optional condition to be True for the skip to have place + """ + def decorator(f): + @functools.wraps(f) + def wrapper(self, *func_args, **func_kwargs): + skip = False + if "condition" in kwargs: + if kwargs["condition"] is True: + skip = True + else: + skip = True + if "bug" in kwargs and skip is True: + if not kwargs['bug'].isdigit(): + raise ValueError('bug must be a valid bug number') + msg = "Skipped until Bug: %s is resolved." % kwargs["bug"] + raise testtools.TestCase.skipException(msg) + return f(self, *func_args, **func_kwargs) + return wrapper + return decorator + + +def idempotent_id(id): + """Stub for metadata decorator""" + if not isinstance(id, six.string_types): + raise TypeError('Test idempotent_id must be string not %s' + '' % type(id).__name__) + uuid.UUID(id) + + def decorator(f): + f = testtools.testcase.attr('id-%s' % id)(f) + if f.__doc__: + f.__doc__ = 'Test idempotent id: %s\n%s' % (id, f.__doc__) + else: + f.__doc__ = 'Test idempotent id: %s' % id + return f + return decorator + + +class skip_unless_attr(object): + """Decorator to skip tests if a specified attr does not exists or False""" + def __init__(self, attr, msg=None): + self.attr = attr + self.message = msg or ("Test case attribute %s not found " + "or False") % attr + + def __call__(self, func): + def _skipper(*args, **kw): + """Wrapped skipper function.""" + testobj = args[0] + if not getattr(testobj, self.attr, False): + raise testtools.TestCase.skipException(self.message) + func(*args, **kw) + _skipper.__name__ = func.__name__ + _skipper.__doc__ = func.__doc__ + return _skipper diff --git a/tempest/lib/exceptions.py b/tempest/lib/exceptions.py new file mode 100644 index 0000000000..2bf7cdd324 --- /dev/null +++ b/tempest/lib/exceptions.py @@ -0,0 +1,205 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import testtools + + +class TempestException(Exception): + """Base Tempest Exception + + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + """ + message = "An unknown exception occurred" + + def __init__(self, *args, **kwargs): + super(TempestException, self).__init__() + try: + self._error_string = self.message % kwargs + except Exception: + # at least get the core message out if something happened + self._error_string = self.message + if len(args) > 0: + # If there is a non-kwarg parameter, assume it's the error + # message or reason description and tack it on to the end + # of the exception message + # Convert all arguments into their string representations... + args = ["%s" % arg for arg in args] + self._error_string = (self._error_string + + "\nDetails: %s" % '\n'.join(args)) + + def __str__(self): + return self._error_string + + +class RestClientException(TempestException, + testtools.TestCase.failureException): + def __init__(self, resp_body=None, *args, **kwargs): + if 'resp' in kwargs: + self.resp = kwargs.get('resp') + self.resp_body = resp_body + message = kwargs.get("message", resp_body) + super(RestClientException, self).__init__(message, *args, **kwargs) + + +class OtherRestClientException(RestClientException): + pass + + +class ServerRestClientException(RestClientException): + pass + + +class ClientRestClientException(RestClientException): + pass + + +class InvalidHttpSuccessCode(OtherRestClientException): + message = "The success code is different than the expected one" + + +class NotFound(ClientRestClientException): + message = "Object not found" + + +class Unauthorized(ClientRestClientException): + message = 'Unauthorized' + + +class Forbidden(ClientRestClientException): + message = "Forbidden" + + +class TimeoutException(OtherRestClientException): + message = "Request timed out" + + +class BadRequest(ClientRestClientException): + message = "Bad request" + + +class UnprocessableEntity(ClientRestClientException): + message = "Unprocessable entity" + + +class RateLimitExceeded(ClientRestClientException): + message = "Rate limit exceeded" + + +class OverLimit(ClientRestClientException): + message = "Quota exceeded" + + +class ServerFault(ServerRestClientException): + message = "Got server fault" + + +class NotImplemented(ServerRestClientException): + message = "Got NotImplemented error" + + +class Conflict(ClientRestClientException): + message = "An object with that identifier already exists" + + +class Gone(ClientRestClientException): + message = "The requested resource is no longer available" + + +class ResponseWithNonEmptyBody(OtherRestClientException): + message = ("RFC Violation! Response with %(status)d HTTP Status Code " + "MUST NOT have a body") + + +class ResponseWithEntity(OtherRestClientException): + message = ("RFC Violation! Response with 205 HTTP Status Code " + "MUST NOT have an entity") + + +class InvalidHTTPResponseBody(OtherRestClientException): + message = "HTTP response body is invalid json or xml" + + +class InvalidHTTPResponseHeader(OtherRestClientException): + message = "HTTP response header is invalid" + + +class InvalidContentType(ClientRestClientException): + message = "Invalid content type provided" + + +class UnexpectedContentType(OtherRestClientException): + message = "Unexpected content type provided" + + +class UnexpectedResponseCode(OtherRestClientException): + message = "Unexpected response code received" + + +class InvalidStructure(TempestException): + message = "Invalid structure of table with details" + + +class BadAltAuth(TempestException): + """Used when trying and failing to change to alt creds. + + If alt creds end up the same as primary creds, use this + exception. This is often going to be the case when you assume + project_id is in the url, but it's not. + + """ + message = "The alt auth looks the same as primary auth for %(part)s" + + +class CommandFailed(Exception): + def __init__(self, returncode, cmd, output, stderr): + super(CommandFailed, self).__init__() + self.returncode = returncode + self.cmd = cmd + self.stdout = output + self.stderr = stderr + + def __str__(self): + return ("Command '%s' returned non-zero exit status %d.\n" + "stdout:\n%s\n" + "stderr:\n%s" % (self.cmd, + self.returncode, + self.stdout, + self.stderr)) + + +class IdentityError(TempestException): + message = "Got identity error" + + +class EndpointNotFound(TempestException): + message = "Endpoint not found" + + +class InvalidCredentials(TempestException): + message = "Invalid Credentials" + + +class SSHTimeout(TempestException): + message = ("Connection to the %(host)s via SSH timed out.\n" + "User: %(user)s, Password: %(password)s") + + +class SSHExecCommandFailed(TempestException): + """Raised when remotely executed command returns nonzero status.""" + message = ("Command '%(command)s', exit status: %(exit_status)d, " + "stderr:\n%(stderr)s\n" + "stdout:\n%(stdout)s") diff --git a/tempest/lib/services/__init__.py b/tempest/lib/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/services/compute/__init__.py b/tempest/lib/services/compute/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/services/compute/agents_client.py b/tempest/lib/services/compute/agents_client.py new file mode 100644 index 0000000000..8b11e642bc --- /dev/null +++ b/tempest/lib/services/compute/agents_client.py @@ -0,0 +1,63 @@ +# Copyright 2014 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import agents as schema +from tempest.lib.common import rest_client + + +class AgentsClient(rest_client.RestClient): + """Tests Agents API""" + + def list_agents(self, **params): + """List all agent builds.""" + url = 'os-agents' + if params: + url += '?%s' % urllib.urlencode(params) + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_agents, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_agent(self, **kwargs): + """Create an agent build. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#agentbuild + """ + post_body = json.dumps({'agent': kwargs}) + resp, body = self.post('os-agents', post_body) + body = json.loads(body) + self.validate_response(schema.create_agent, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_agent(self, agent_id): + """Delete an existing agent build.""" + resp, body = self.delete("os-agents/%s" % agent_id) + self.validate_response(schema.delete_agent, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_agent(self, agent_id, **kwargs): + """Update an agent build. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#updatebuild + """ + put_body = json.dumps({'para': kwargs}) + resp, body = self.put('os-agents/%s' % agent_id, put_body) + body = json.loads(body) + self.validate_response(schema.update_agent, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/aggregates_client.py b/tempest/lib/services/compute/aggregates_client.py new file mode 100644 index 0000000000..b481674e2e --- /dev/null +++ b/tempest/lib/services/compute/aggregates_client.py @@ -0,0 +1,116 @@ +# Copyright 2013 NEC Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import aggregates as schema +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc + + +class AggregatesClient(rest_client.RestClient): + + def list_aggregates(self): + """Get aggregate list.""" + resp, body = self.get("os-aggregates") + body = json.loads(body) + self.validate_response(schema.list_aggregates, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_aggregate(self, aggregate_id): + """Get details of the given aggregate.""" + resp, body = self.get("os-aggregates/%s" % aggregate_id) + body = json.loads(body) + self.validate_response(schema.get_aggregate, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_aggregate(self, **kwargs): + """Create a new aggregate. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createaggregate + """ + post_body = json.dumps({'aggregate': kwargs}) + resp, body = self.post('os-aggregates', post_body) + + body = json.loads(body) + self.validate_response(schema.create_aggregate, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_aggregate(self, aggregate_id, **kwargs): + """Update an aggregate. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#updateaggregate + """ + put_body = json.dumps({'aggregate': kwargs}) + resp, body = self.put('os-aggregates/%s' % aggregate_id, put_body) + + body = json.loads(body) + self.validate_response(schema.update_aggregate, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_aggregate(self, aggregate_id): + """Delete the given aggregate.""" + resp, body = self.delete("os-aggregates/%s" % aggregate_id) + self.validate_response(schema.delete_aggregate, resp, body) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + try: + self.show_aggregate(id) + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Return the primary type of resource this client works with.""" + return 'aggregate' + + def add_host(self, aggregate_id, **kwargs): + """Add a host to the given aggregate. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#addhost + """ + post_body = json.dumps({'add_host': kwargs}) + resp, body = self.post('os-aggregates/%s/action' % aggregate_id, + post_body) + body = json.loads(body) + self.validate_response(schema.aggregate_add_remove_host, resp, body) + return rest_client.ResponseBody(resp, body) + + def remove_host(self, aggregate_id, **kwargs): + """Remove a host from the given aggregate. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#removehost + """ + post_body = json.dumps({'remove_host': kwargs}) + resp, body = self.post('os-aggregates/%s/action' % aggregate_id, + post_body) + body = json.loads(body) + self.validate_response(schema.aggregate_add_remove_host, resp, body) + return rest_client.ResponseBody(resp, body) + + def set_metadata(self, aggregate_id, **kwargs): + """Replace the aggregate's existing metadata with new metadata.""" + post_body = json.dumps({'set_metadata': kwargs}) + resp, body = self.post('os-aggregates/%s/action' % aggregate_id, + post_body) + body = json.loads(body) + self.validate_response(schema.aggregate_set_metadata, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/availability_zone_client.py b/tempest/lib/services/compute/availability_zone_client.py new file mode 100644 index 0000000000..00f66d6002 --- /dev/null +++ b/tempest/lib/services/compute/availability_zone_client.py @@ -0,0 +1,35 @@ +# Copyright 2013 NEC Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import availability_zone \ + as schema +from tempest.lib.common import rest_client + + +class AvailabilityZoneClient(rest_client.RestClient): + + def list_availability_zones(self, detail=False): + url = 'os-availability-zone' + schema_list = schema.list_availability_zone_list + if detail: + url += '/detail' + schema_list = schema.list_availability_zone_list_detail + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema_list, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/baremetal_nodes_client.py b/tempest/lib/services/compute/baremetal_nodes_client.py new file mode 100644 index 0000000000..d9a712ee9b --- /dev/null +++ b/tempest/lib/services/compute/baremetal_nodes_client.py @@ -0,0 +1,42 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import baremetal_nodes \ + as schema +from tempest.lib.common import rest_client + + +class BaremetalNodesClient(rest_client.RestClient): + """Tests Baremetal API""" + + def list_baremetal_nodes(self, **params): + """List all baremetal nodes.""" + url = 'os-baremetal-nodes' + if params: + url += '?%s' % urllib.urlencode(params) + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_baremetal_nodes, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_baremetal_node(self, baremetal_node_id): + """Return the details of a single baremetal node.""" + url = 'os-baremetal-nodes/%s' % baremetal_node_id + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.get_baremetal_node, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/certificates_client.py b/tempest/lib/services/compute/certificates_client.py new file mode 100644 index 0000000000..76d830e983 --- /dev/null +++ b/tempest/lib/services/compute/certificates_client.py @@ -0,0 +1,37 @@ +# Copyright 2013 IBM Corp +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import certificates as schema +from tempest.lib.common import rest_client + + +class CertificatesClient(rest_client.RestClient): + + def show_certificate(self, certificate_id): + url = "os-certificates/%s" % certificate_id + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.get_certificate, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_certificate(self): + """Create a certificate.""" + url = "os-certificates" + resp, body = self.post(url, None) + body = json.loads(body) + self.validate_response(schema.create_certificate, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/extensions_client.py b/tempest/lib/services/compute/extensions_client.py new file mode 100644 index 0000000000..85f8f0c760 --- /dev/null +++ b/tempest/lib/services/compute/extensions_client.py @@ -0,0 +1,34 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import extensions as schema +from tempest.lib.common import rest_client + + +class ExtensionsClient(rest_client.RestClient): + + def list_extensions(self): + url = 'extensions' + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_extensions, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_extension(self, extension_alias): + resp, body = self.get('extensions/%s' % extension_alias) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/fixed_ips_client.py b/tempest/lib/services/compute/fixed_ips_client.py new file mode 100644 index 0000000000..76ec59f33b --- /dev/null +++ b/tempest/lib/services/compute/fixed_ips_client.py @@ -0,0 +1,40 @@ +# Copyright 2013 IBM Corp +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import fixed_ips as schema +from tempest.lib.common import rest_client + + +class FixedIPsClient(rest_client.RestClient): + + def show_fixed_ip(self, fixed_ip): + url = "os-fixed-ips/%s" % fixed_ip + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.get_fixed_ip, resp, body) + return rest_client.ResponseBody(resp, body) + + def reserve_fixed_ip(self, fixed_ip, **kwargs): + """Reserve/Unreserve a fixed IP. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#reserveIP + """ + url = "os-fixed-ips/%s/action" % fixed_ip + resp, body = self.post(url, json.dumps(kwargs)) + self.validate_response(schema.reserve_unreserve_fixed_ip, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/flavors_client.py b/tempest/lib/services/compute/flavors_client.py new file mode 100644 index 0000000000..50f1dccc9e --- /dev/null +++ b/tempest/lib/services/compute/flavors_client.py @@ -0,0 +1,176 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import flavors as schema +from tempest.lib.api_schema.response.compute.v2_1 import flavors_access \ + as schema_access +from tempest.lib.api_schema.response.compute.v2_1 import flavors_extra_specs \ + as schema_extra_specs +from tempest.lib.common import rest_client + + +class FlavorsClient(rest_client.RestClient): + + def list_flavors(self, detail=False, **params): + url = 'flavors' + _schema = schema.list_flavors + + if detail: + url += '/detail' + _schema = schema.list_flavors_details + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(_schema, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_flavor(self, flavor_id): + resp, body = self.get("flavors/%s" % flavor_id) + body = json.loads(body) + self.validate_response(schema.create_get_flavor_details, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_flavor(self, **kwargs): + """Create a new flavor or instance type. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#create-flavors + """ + if kwargs.get('ephemeral'): + kwargs['OS-FLV-EXT-DATA:ephemeral'] = kwargs.pop('ephemeral') + if kwargs.get('is_public'): + kwargs['os-flavor-access:is_public'] = kwargs.pop('is_public') + + post_body = json.dumps({'flavor': kwargs}) + resp, body = self.post('flavors', post_body) + + body = json.loads(body) + self.validate_response(schema.create_get_flavor_details, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_flavor(self, flavor_id): + """Delete the given flavor.""" + resp, body = self.delete("flavors/{0}".format(flavor_id)) + self.validate_response(schema.delete_flavor, resp, body) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + # Did not use show_flavor(id) for verification as it gives + # 200 ok even for deleted id. LP #981263 + # we can remove the loop here and use get by ID when bug gets sortedout + flavors = self.list_flavors(detail=True)['flavors'] + for flavor in flavors: + if flavor['id'] == id: + return False + return True + + @property + def resource_type(self): + """Return the primary type of resource this client works with.""" + return 'flavor' + + def set_flavor_extra_spec(self, flavor_id, **kwargs): + """Set extra Specs to the mentioned flavor. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#updateFlavorExtraSpec + """ + post_body = json.dumps({'extra_specs': kwargs}) + resp, body = self.post('flavors/%s/os-extra_specs' % flavor_id, + post_body) + body = json.loads(body) + self.validate_response(schema_extra_specs.set_get_flavor_extra_specs, + resp, body) + return rest_client.ResponseBody(resp, body) + + def list_flavor_extra_specs(self, flavor_id): + """Get extra Specs details of the mentioned flavor.""" + resp, body = self.get('flavors/%s/os-extra_specs' % flavor_id) + body = json.loads(body) + self.validate_response(schema_extra_specs.set_get_flavor_extra_specs, + resp, body) + return rest_client.ResponseBody(resp, body) + + def show_flavor_extra_spec(self, flavor_id, key): + """Get extra Specs key-value of the mentioned flavor and key.""" + resp, body = self.get('flavors/%s/os-extra_specs/%s' % (flavor_id, + key)) + body = json.loads(body) + self.validate_response( + schema_extra_specs.set_get_flavor_extra_specs_key, + resp, body) + return rest_client.ResponseBody(resp, body) + + def update_flavor_extra_spec(self, flavor_id, key, **kwargs): + """Update specified extra Specs of the mentioned flavor and key. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#updateflavorspec + """ + resp, body = self.put('flavors/%s/os-extra_specs/%s' % + (flavor_id, key), json.dumps(kwargs)) + body = json.loads(body) + self.validate_response( + schema_extra_specs.set_get_flavor_extra_specs_key, + resp, body) + return rest_client.ResponseBody(resp, body) + + def unset_flavor_extra_spec(self, flavor_id, key): + """Unset extra Specs from the mentioned flavor.""" + resp, body = self.delete('flavors/%s/os-extra_specs/%s' % + (flavor_id, key)) + self.validate_response(schema.unset_flavor_extra_specs, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_flavor_access(self, flavor_id): + """Get flavor access information given the flavor id.""" + resp, body = self.get('flavors/%s/os-flavor-access' % flavor_id) + body = json.loads(body) + self.validate_response(schema_access.add_remove_list_flavor_access, + resp, body) + return rest_client.ResponseBody(resp, body) + + def add_flavor_access(self, flavor_id, tenant_id): + """Add flavor access for the specified tenant.""" + post_body = { + 'addTenantAccess': { + 'tenant': tenant_id + } + } + post_body = json.dumps(post_body) + resp, body = self.post('flavors/%s/action' % flavor_id, post_body) + body = json.loads(body) + self.validate_response(schema_access.add_remove_list_flavor_access, + resp, body) + return rest_client.ResponseBody(resp, body) + + def remove_flavor_access(self, flavor_id, tenant_id): + """Remove flavor access from the specified tenant.""" + post_body = { + 'removeTenantAccess': { + 'tenant': tenant_id + } + } + post_body = json.dumps(post_body) + resp, body = self.post('flavors/%s/action' % flavor_id, post_body) + body = json.loads(body) + self.validate_response(schema_access.add_remove_list_flavor_access, + resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/floating_ip_pools_client.py b/tempest/lib/services/compute/floating_ip_pools_client.py new file mode 100644 index 0000000000..d4a0193d3d --- /dev/null +++ b/tempest/lib/services/compute/floating_ip_pools_client.py @@ -0,0 +1,34 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import floating_ips as schema +from tempest.lib.common import rest_client + + +class FloatingIPPoolsClient(rest_client.RestClient): + + def list_floating_ip_pools(self, params=None): + """Gets all floating IP Pools list.""" + url = 'os-floating-ip-pools' + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_floating_ip_pools, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/floating_ips_bulk_client.py b/tempest/lib/services/compute/floating_ips_bulk_client.py new file mode 100644 index 0000000000..bfcf74b988 --- /dev/null +++ b/tempest/lib/services/compute/floating_ips_bulk_client.py @@ -0,0 +1,50 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import floating_ips as schema +from tempest.lib.common import rest_client + + +class FloatingIPsBulkClient(rest_client.RestClient): + + def create_floating_ips_bulk(self, ip_range, pool, interface): + """Allocate floating IPs in bulk.""" + post_body = { + 'ip_range': ip_range, + 'pool': pool, + 'interface': interface + } + post_body = json.dumps({'floating_ips_bulk_create': post_body}) + resp, body = self.post('os-floating-ips-bulk', post_body) + body = json.loads(body) + self.validate_response(schema.create_floating_ips_bulk, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_floating_ips_bulk(self): + """Gets all floating IPs in bulk.""" + resp, body = self.get('os-floating-ips-bulk') + body = json.loads(body) + self.validate_response(schema.list_floating_ips_bulk, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_floating_ips_bulk(self, ip_range): + """Deletes the provided floating IPs in bulk.""" + post_body = json.dumps({'ip_range': ip_range}) + resp, body = self.put('os-floating-ips-bulk/delete', post_body) + body = json.loads(body) + self.validate_response(schema.delete_floating_ips_bulk, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/floating_ips_client.py b/tempest/lib/services/compute/floating_ips_client.py new file mode 100644 index 0000000000..2569bf9739 --- /dev/null +++ b/tempest/lib/services/compute/floating_ips_client.py @@ -0,0 +1,103 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import floating_ips as schema +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc + + +class FloatingIPsClient(rest_client.RestClient): + + def list_floating_ips(self, **params): + """Returns a list of all floating IPs filtered by any parameters.""" + url = 'os-floating-ips' + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_floating_ips, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_floating_ip(self, floating_ip_id): + """Get the details of a floating IP.""" + url = "os-floating-ips/%s" % floating_ip_id + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.create_get_floating_ip, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_floating_ip(self, **kwargs): + """Allocate a floating IP to the project. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createFloatingIP + """ + url = 'os-floating-ips' + post_body = json.dumps(kwargs) + resp, body = self.post(url, post_body) + body = json.loads(body) + self.validate_response(schema.create_get_floating_ip, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_floating_ip(self, floating_ip_id): + """Deletes the provided floating IP from the project.""" + url = "os-floating-ips/%s" % floating_ip_id + resp, body = self.delete(url) + self.validate_response(schema.add_remove_floating_ip, resp, body) + return rest_client.ResponseBody(resp, body) + + def associate_floating_ip_to_server(self, floating_ip, server_id): + """Associate the provided floating IP to a specific server.""" + url = "servers/%s/action" % server_id + post_body = { + 'addFloatingIp': { + 'address': floating_ip, + } + } + + post_body = json.dumps(post_body) + resp, body = self.post(url, post_body) + self.validate_response(schema.add_remove_floating_ip, resp, body) + return rest_client.ResponseBody(resp, body) + + def disassociate_floating_ip_from_server(self, floating_ip, server_id): + """Disassociate the provided floating IP from a specific server.""" + url = "servers/%s/action" % server_id + post_body = { + 'removeFloatingIp': { + 'address': floating_ip, + } + } + + post_body = json.dumps(post_body) + resp, body = self.post(url, post_body) + self.validate_response(schema.add_remove_floating_ip, resp, body) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + try: + self.show_floating_ip(id) + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Returns the primary type of resource this client works with.""" + return 'floating_ip' diff --git a/tempest/lib/services/compute/hosts_client.py b/tempest/lib/services/compute/hosts_client.py new file mode 100644 index 0000000000..269160ecdd --- /dev/null +++ b/tempest/lib/services/compute/hosts_client.py @@ -0,0 +1,85 @@ +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import hosts as schema +from tempest.lib.common import rest_client + + +class HostsClient(rest_client.RestClient): + + def list_hosts(self, **params): + """List all hosts.""" + + url = 'os-hosts' + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_hosts, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_host(self, hostname): + """Show detail information for the host.""" + + resp, body = self.get("os-hosts/%s" % hostname) + body = json.loads(body) + self.validate_response(schema.get_host_detail, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_host(self, hostname, **kwargs): + """Update a host. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#enablehost + """ + + request_body = { + 'status': None, + 'maintenance_mode': None, + } + request_body.update(**kwargs) + request_body = json.dumps(request_body) + + resp, body = self.put("os-hosts/%s" % hostname, request_body) + body = json.loads(body) + self.validate_response(schema.update_host, resp, body) + return rest_client.ResponseBody(resp, body) + + def startup_host(self, hostname): + """Startup a host.""" + + resp, body = self.get("os-hosts/%s/startup" % hostname) + body = json.loads(body) + self.validate_response(schema.startup_host, resp, body) + return rest_client.ResponseBody(resp, body) + + def shutdown_host(self, hostname): + """Shutdown a host.""" + + resp, body = self.get("os-hosts/%s/shutdown" % hostname) + body = json.loads(body) + self.validate_response(schema.shutdown_host, resp, body) + return rest_client.ResponseBody(resp, body) + + def reboot_host(self, hostname): + """Reboot a host.""" + + resp, body = self.get("os-hosts/%s/reboot" % hostname) + body = json.loads(body) + self.validate_response(schema.reboot_host, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/hypervisor_client.py b/tempest/lib/services/compute/hypervisor_client.py new file mode 100644 index 0000000000..2e6df1f553 --- /dev/null +++ b/tempest/lib/services/compute/hypervisor_client.py @@ -0,0 +1,70 @@ +# Copyright 2013 IBM Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import hypervisors as schema +from tempest.lib.common import rest_client + + +class HypervisorClient(rest_client.RestClient): + + def list_hypervisors(self, detail=False): + """List hypervisors information.""" + url = 'os-hypervisors' + _schema = schema.list_search_hypervisors + if detail: + url += '/detail' + _schema = schema.list_hypervisors_detail + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(_schema, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_hypervisor(self, hypervisor_id): + """Display the details of the specified hypervisor.""" + resp, body = self.get('os-hypervisors/%s' % hypervisor_id) + body = json.loads(body) + self.validate_response(schema.get_hypervisor, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_servers_on_hypervisor(self, hypervisor_name): + """List instances belonging to the specified hypervisor.""" + resp, body = self.get('os-hypervisors/%s/servers' % hypervisor_name) + body = json.loads(body) + self.validate_response(schema.get_hypervisors_servers, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_hypervisor_statistics(self): + """Get hypervisor statistics over all compute nodes.""" + resp, body = self.get('os-hypervisors/statistics') + body = json.loads(body) + self.validate_response(schema.get_hypervisor_statistics, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_hypervisor_uptime(self, hypervisor_id): + """Display the uptime of the specified hypervisor.""" + resp, body = self.get('os-hypervisors/%s/uptime' % hypervisor_id) + body = json.loads(body) + self.validate_response(schema.get_hypervisor_uptime, resp, body) + return rest_client.ResponseBody(resp, body) + + def search_hypervisor(self, hypervisor_name): + """Search specified hypervisor.""" + resp, body = self.get('os-hypervisors/%s/search' % hypervisor_name) + body = json.loads(body) + self.validate_response(schema.list_search_hypervisors, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/images_client.py b/tempest/lib/services/compute/images_client.py new file mode 100644 index 0000000000..30ff484f4d --- /dev/null +++ b/tempest/lib/services/compute/images_client.py @@ -0,0 +1,142 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import images as schema +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc + + +class ImagesClient(rest_client.RestClient): + + def create_image(self, server_id, **kwargs): + """Create an image of the original server. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createImage + """ + + post_body = {'createImage': kwargs} + post_body = json.dumps(post_body) + resp, body = self.post('servers/%s/action' % server_id, + post_body) + self.validate_response(schema.create_image, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_images(self, detail=False, **params): + """Return a list of all images filtered by any parameter. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#listImages + """ + url = 'images' + _schema = schema.list_images + if detail: + url += '/detail' + _schema = schema.list_images_details + + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(_schema, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_image(self, image_id): + """Return the details of a single image.""" + resp, body = self.get("images/%s" % image_id) + self.expected_success(200, resp.status) + body = json.loads(body) + self.validate_response(schema.get_image, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_image(self, image_id): + """Delete the provided image.""" + resp, body = self.delete("images/%s" % image_id) + self.validate_response(schema.delete, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_image_metadata(self, image_id): + """List all metadata items for an image.""" + resp, body = self.get("images/%s/metadata" % image_id) + body = json.loads(body) + self.validate_response(schema.image_metadata, resp, body) + return rest_client.ResponseBody(resp, body) + + def set_image_metadata(self, image_id, meta): + """Set the metadata for an image. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createImageMetadata + """ + post_body = json.dumps({'metadata': meta}) + resp, body = self.put('images/%s/metadata' % image_id, post_body) + body = json.loads(body) + self.validate_response(schema.image_metadata, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_image_metadata(self, image_id, meta): + """Update the metadata for an image. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#updateImageMetadata + """ + post_body = json.dumps({'metadata': meta}) + resp, body = self.post('images/%s/metadata' % image_id, post_body) + body = json.loads(body) + self.validate_response(schema.image_metadata, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_image_metadata_item(self, image_id, key): + """Return the value for a specific image metadata key.""" + resp, body = self.get("images/%s/metadata/%s" % (image_id, key)) + body = json.loads(body) + self.validate_response(schema.image_meta_item, resp, body) + return rest_client.ResponseBody(resp, body) + + def set_image_metadata_item(self, image_id, key, meta): + """Set the value for a specific image metadata key. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#setImageMetadataItem + """ + post_body = json.dumps({'meta': meta}) + resp, body = self.put('images/%s/metadata/%s' % (image_id, key), + post_body) + body = json.loads(body) + self.validate_response(schema.image_meta_item, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_image_metadata_item(self, image_id, key): + """Delete a single image metadata key/value pair.""" + resp, body = self.delete("images/%s/metadata/%s" % + (image_id, key)) + self.validate_response(schema.delete, resp, body) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + try: + self.show_image(id) + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Return the primary type of resource this client works with.""" + return 'image' diff --git a/tempest/lib/services/compute/instance_usage_audit_log_client.py b/tempest/lib/services/compute/instance_usage_audit_log_client.py new file mode 100644 index 0000000000..4651b2a0d3 --- /dev/null +++ b/tempest/lib/services/compute/instance_usage_audit_log_client.py @@ -0,0 +1,38 @@ +# Copyright 2013 IBM Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import \ + instance_usage_audit_logs as schema +from tempest.lib.common import rest_client + + +class InstanceUsagesAuditLogClient(rest_client.RestClient): + + def list_instance_usage_audit_logs(self): + url = 'os-instance_usage_audit_log' + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_instance_usage_audit_log, + resp, body) + return rest_client.ResponseBody(resp, body) + + def show_instance_usage_audit_log(self, time_before): + url = 'os-instance_usage_audit_log/%s' % time_before + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.get_instance_usage_audit_log, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/interfaces_client.py b/tempest/lib/services/compute/interfaces_client.py new file mode 100644 index 0000000000..e7da5a15b5 --- /dev/null +++ b/tempest/lib/services/compute/interfaces_client.py @@ -0,0 +1,55 @@ +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import interfaces as schema +from tempest.lib.common import rest_client + + +class InterfacesClient(rest_client.RestClient): + + def list_interfaces(self, server_id): + resp, body = self.get('servers/%s/os-interface' % server_id) + body = json.loads(body) + self.validate_response(schema.list_interfaces, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_interface(self, server_id, **kwargs): + """Create an interface. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createAttachInterface + """ + post_body = {'interfaceAttachment': kwargs} + post_body = json.dumps(post_body) + resp, body = self.post('servers/%s/os-interface' % server_id, + body=post_body) + body = json.loads(body) + self.validate_response(schema.get_create_interfaces, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_interface(self, server_id, port_id): + resp, body = self.get('servers/%s/os-interface/%s' % (server_id, + port_id)) + body = json.loads(body) + self.validate_response(schema.get_create_interfaces, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_interface(self, server_id, port_id): + resp, body = self.delete('servers/%s/os-interface/%s' % (server_id, + port_id)) + self.validate_response(schema.delete_interface, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/keypairs_client.py b/tempest/lib/services/compute/keypairs_client.py new file mode 100644 index 0000000000..3e3cf8df66 --- /dev/null +++ b/tempest/lib/services/compute/keypairs_client.py @@ -0,0 +1,51 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import keypairs as schema +from tempest.lib.common import rest_client + + +class KeyPairsClient(rest_client.RestClient): + + def list_keypairs(self): + resp, body = self.get("os-keypairs") + body = json.loads(body) + self.validate_response(schema.list_keypairs, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_keypair(self, keypair_name): + resp, body = self.get("os-keypairs/%s" % keypair_name) + body = json.loads(body) + self.validate_response(schema.get_keypair, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_keypair(self, **kwargs): + """Create a keypair. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createKeypair + """ + post_body = json.dumps({'keypair': kwargs}) + resp, body = self.post("os-keypairs", body=post_body) + body = json.loads(body) + self.validate_response(schema.create_keypair, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_keypair(self, keypair_name): + resp, body = self.delete("os-keypairs/%s" % keypair_name) + self.validate_response(schema.delete_keypair, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/limits_client.py b/tempest/lib/services/compute/limits_client.py new file mode 100644 index 0000000000..c7eba4ec47 --- /dev/null +++ b/tempest/lib/services/compute/limits_client.py @@ -0,0 +1,28 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import limits as schema +from tempest.lib.common import rest_client + + +class LimitsClient(rest_client.RestClient): + + def show_limits(self): + resp, body = self.get("limits") + body = json.loads(body) + self.validate_response(schema.get_limit, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/migrations_client.py b/tempest/lib/services/compute/migrations_client.py new file mode 100644 index 0000000000..21bc37a5cb --- /dev/null +++ b/tempest/lib/services/compute/migrations_client.py @@ -0,0 +1,38 @@ +# Copyright 2014 NEC Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import migrations as schema +from tempest.lib.common import rest_client + + +class MigrationsClient(rest_client.RestClient): + + def list_migrations(self, **params): + """List all migrations. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#returnmigrations + """ + + url = 'os-migrations' + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_migrations, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/networks_client.py b/tempest/lib/services/compute/networks_client.py new file mode 100644 index 0000000000..c0eb5ff76f --- /dev/null +++ b/tempest/lib/services/compute/networks_client.py @@ -0,0 +1,33 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.common import rest_client + + +class NetworksClient(rest_client.RestClient): + + def list_networks(self): + resp, body = self.get("os-networks") + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def show_network(self, network_id): + resp, body = self.get("os-networks/%s" % network_id) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/quota_classes_client.py b/tempest/lib/services/compute/quota_classes_client.py new file mode 100644 index 0000000000..ff4eec0f93 --- /dev/null +++ b/tempest/lib/services/compute/quota_classes_client.py @@ -0,0 +1,48 @@ +# Copyright 2012 NTT Data +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1\ + import quota_classes as classes_schema +from tempest.lib.common import rest_client + + +class QuotaClassesClient(rest_client.RestClient): + + def show_quota_class_set(self, quota_class_id): + """List the quota class set for a quota class.""" + + url = 'os-quota-class-sets/%s' % quota_class_id + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(classes_schema.get_quota_class_set, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_quota_class_set(self, quota_class_id, **kwargs): + """Update the quota class's limits for one or more resources. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#updatequota + """ + post_body = json.dumps({'quota_class_set': kwargs}) + + resp, body = self.put('os-quota-class-sets/%s' % quota_class_id, + post_body) + + body = json.loads(body) + self.validate_response(classes_schema.update_quota_class_set, + resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/quotas_client.py b/tempest/lib/services/compute/quotas_client.py new file mode 100644 index 0000000000..697d004698 --- /dev/null +++ b/tempest/lib/services/compute/quotas_client.py @@ -0,0 +1,68 @@ +# Copyright 2012 NTT Data +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import quotas as schema +from tempest.lib.common import rest_client + + +class QuotasClient(rest_client.RestClient): + + def show_quota_set(self, tenant_id, user_id=None): + """List the quota set for a tenant.""" + + url = 'os-quota-sets/%s' % tenant_id + if user_id: + url += '?user_id=%s' % user_id + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.get_quota_set, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_default_quota_set(self, tenant_id): + """List the default quota set for a tenant.""" + + url = 'os-quota-sets/%s/defaults' % tenant_id + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.get_quota_set, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_quota_set(self, tenant_id, user_id=None, **kwargs): + """Updates the tenant's quota limits for one or more resources. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#updatesquotatenant + """ + + post_body = json.dumps({'quota_set': kwargs}) + + if user_id: + resp, body = self.put('os-quota-sets/%s?user_id=%s' % + (tenant_id, user_id), post_body) + else: + resp, body = self.put('os-quota-sets/%s' % tenant_id, + post_body) + + body = json.loads(body) + self.validate_response(schema.update_quota_set, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_quota_set(self, tenant_id): + """Delete the tenant's quota set.""" + resp, body = self.delete('os-quota-sets/%s' % tenant_id) + self.validate_response(schema.delete_quota, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/security_group_default_rules_client.py b/tempest/lib/services/compute/security_group_default_rules_client.py new file mode 100644 index 0000000000..e5f291c3a3 --- /dev/null +++ b/tempest/lib/services/compute/security_group_default_rules_client.py @@ -0,0 +1,64 @@ +# Copyright 2014 NEC Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import \ + security_group_default_rule as schema +from tempest.lib.common import rest_client + + +class SecurityGroupDefaultRulesClient(rest_client.RestClient): + + def create_security_default_group_rule(self, **kwargs): + """Create security group default rule. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html + #createSecGroupDefaultRule + """ + post_body = json.dumps({'security_group_default_rule': kwargs}) + url = 'os-security-group-default-rules' + resp, body = self.post(url, post_body) + body = json.loads(body) + self.validate_response(schema.create_get_security_group_default_rule, + resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_security_group_default_rule(self, + security_group_default_rule_id): + """Delete the provided Security Group default rule.""" + resp, body = self.delete('os-security-group-default-rules/%s' % ( + security_group_default_rule_id)) + self.validate_response(schema.delete_security_group_default_rule, + resp, body) + return rest_client.ResponseBody(resp, body) + + def list_security_group_default_rules(self): + """List all Security Group default rules.""" + resp, body = self.get('os-security-group-default-rules') + body = json.loads(body) + self.validate_response(schema.list_security_group_default_rules, + resp, body) + return rest_client.ResponseBody(resp, body) + + def show_security_group_default_rule(self, security_group_default_rule_id): + """Return the details of provided Security Group default rule.""" + resp, body = self.get('os-security-group-default-rules/%s' % + security_group_default_rule_id) + body = json.loads(body) + self.validate_response(schema.create_get_security_group_default_rule, + resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/security_group_rules_client.py b/tempest/lib/services/compute/security_group_rules_client.py new file mode 100644 index 0000000000..c0e1245f64 --- /dev/null +++ b/tempest/lib/services/compute/security_group_rules_client.py @@ -0,0 +1,43 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import \ + security_groups as schema +from tempest.lib.common import rest_client + + +class SecurityGroupRulesClient(rest_client.RestClient): + + def create_security_group_rule(self, **kwargs): + """Create a new security group rule. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createSecGroupRule + """ + post_body = json.dumps({'security_group_rule': kwargs}) + url = 'os-security-group-rules' + resp, body = self.post(url, post_body) + body = json.loads(body) + self.validate_response(schema.create_security_group_rule, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_security_group_rule(self, group_rule_id): + """Deletes the provided Security Group rule.""" + resp, body = self.delete('os-security-group-rules/%s' % + group_rule_id) + self.validate_response(schema.delete_security_group_rule, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/security_groups_client.py b/tempest/lib/services/compute/security_groups_client.py new file mode 100644 index 0000000000..4db98c9c44 --- /dev/null +++ b/tempest/lib/services/compute/security_groups_client.py @@ -0,0 +1,89 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import \ + security_groups as schema +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc + + +class SecurityGroupsClient(rest_client.RestClient): + + def list_security_groups(self, **params): + """List all security groups for a user.""" + + url = 'os-security-groups' + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_security_groups, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_security_group(self, security_group_id): + """Get the details of a Security Group.""" + url = "os-security-groups/%s" % security_group_id + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.get_security_group, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_security_group(self, **kwargs): + """Create a new security group. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createSecGroup + """ + post_body = json.dumps({'security_group': kwargs}) + resp, body = self.post('os-security-groups', post_body) + body = json.loads(body) + self.validate_response(schema.get_security_group, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_security_group(self, security_group_id, **kwargs): + """Update a security group. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#updateSecGroup + """ + post_body = json.dumps({'security_group': kwargs}) + resp, body = self.put('os-security-groups/%s' % security_group_id, + post_body) + body = json.loads(body) + self.validate_response(schema.update_security_group, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_security_group(self, security_group_id): + """Delete the provided Security Group.""" + resp, body = self.delete( + 'os-security-groups/%s' % security_group_id) + self.validate_response(schema.delete_security_group, resp, body) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + try: + self.show_security_group(id) + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Return the primary type of resource this client works with.""" + return 'security_group' diff --git a/tempest/lib/services/compute/server_groups_client.py b/tempest/lib/services/compute/server_groups_client.py new file mode 100644 index 0000000000..ea60e9877b --- /dev/null +++ b/tempest/lib/services/compute/server_groups_client.py @@ -0,0 +1,56 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import servers as schema +from tempest.lib.common import rest_client + + +class ServerGroupsClient(rest_client.RestClient): + + def create_server_group(self, **kwargs): + """Create the server group. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createServerGroup + """ + post_body = json.dumps({'server_group': kwargs}) + resp, body = self.post('os-server-groups', post_body) + + body = json.loads(body) + self.validate_response(schema.create_show_server_group, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_server_group(self, server_group_id): + """Delete the given server-group.""" + resp, body = self.delete("os-server-groups/%s" % server_group_id) + self.validate_response(schema.delete_server_group, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_server_groups(self): + """List the server-groups.""" + resp, body = self.get("os-server-groups") + body = json.loads(body) + self.validate_response(schema.list_server_groups, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_server_group(self, server_group_id): + """Get the details of given server_group.""" + resp, body = self.get("os-server-groups/%s" % server_group_id) + body = json.loads(body) + self.validate_response(schema.create_show_server_group, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py new file mode 100644 index 0000000000..46c4a49e09 --- /dev/null +++ b/tempest/lib/services/compute/servers_client.py @@ -0,0 +1,570 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import servers as schema +from tempest.lib.common import rest_client + + +class ServersClient(rest_client.RestClient): + + def __init__(self, auth_provider, service, region, + enable_instance_password=True, **kwargs): + super(ServersClient, self).__init__( + auth_provider, service, region, **kwargs) + self.enable_instance_password = enable_instance_password + + def create_server(self, **kwargs): + """Create server. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createServer + + Most parameters except the following are passed to the API without + any changes. + :param disk_config: The name is changed to OS-DCF:diskConfig + :param scheduler_hints: The name is changed to os:scheduler_hints and + the parameter is set in the same level as the parameter 'server'. + """ + body = copy.deepcopy(kwargs) + if body.get('disk_config'): + body['OS-DCF:diskConfig'] = body.pop('disk_config') + + hints = None + if body.get('scheduler_hints'): + hints = {'os:scheduler_hints': body.pop('scheduler_hints')} + + post_body = {'server': body} + + if hints: + post_body = dict(post_body.items() + hints.items()) + + post_body = json.dumps(post_body) + resp, body = self.post('servers', post_body) + + body = json.loads(body) + # NOTE(maurosr): this deals with the case of multiple server create + # with return reservation id set True + if 'reservation_id' in body: + return rest_client.ResponseBody(resp, body) + if self.enable_instance_password: + create_schema = schema.create_server_with_admin_pass + else: + create_schema = schema.create_server + self.validate_response(create_schema, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_server(self, server_id, **kwargs): + """Update server. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#updateServer + + Most parameters except the following are passed to the API without + any changes. + :param disk_config: The name is changed to OS-DCF:diskConfig + """ + if kwargs.get('disk_config'): + kwargs['OS-DCF:diskConfig'] = kwargs.pop('disk_config') + + post_body = json.dumps({'server': kwargs}) + resp, body = self.put("servers/%s" % server_id, post_body) + body = json.loads(body) + self.validate_response(schema.update_server, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_server(self, server_id): + """Get server details.""" + resp, body = self.get("servers/%s" % server_id) + body = json.loads(body) + self.validate_response(schema.get_server, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_server(self, server_id): + """Delete server.""" + resp, body = self.delete("servers/%s" % server_id) + self.validate_response(schema.delete_server, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_servers(self, detail=False, **params): + """List servers. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#listServers + and http://developer.openstack.org/ + api-ref-compute-v2.1.html#listDetailServers + """ + + url = 'servers' + _schema = schema.list_servers + + if detail: + url += '/detail' + _schema = schema.list_servers_detail + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(_schema, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_addresses(self, server_id): + """Lists all addresses for a server.""" + resp, body = self.get("servers/%s/ips" % server_id) + body = json.loads(body) + self.validate_response(schema.list_addresses, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_addresses_by_network(self, server_id, network_id): + """Lists all addresses of a specific network type for a server.""" + resp, body = self.get("servers/%s/ips/%s" % + (server_id, network_id)) + body = json.loads(body) + self.validate_response(schema.list_addresses_by_network, resp, body) + return rest_client.ResponseBody(resp, body) + + def action(self, server_id, action_name, + schema=schema.server_actions_common_schema, + **kwargs): + post_body = json.dumps({action_name: kwargs}) + resp, body = self.post('servers/%s/action' % server_id, + post_body) + if body: + body = json.loads(body) + self.validate_response(schema, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_backup(self, server_id, **kwargs): + """Backup a server instance. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createBackup + """ + return self.action(server_id, "createBackup", **kwargs) + + def change_password(self, server_id, **kwargs): + """Change the root password for the server. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#changePassword + """ + return self.action(server_id, 'changePassword', **kwargs) + + def show_password(self, server_id): + resp, body = self.get("servers/%s/os-server-password" % + server_id) + body = json.loads(body) + self.validate_response(schema.show_password, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_password(self, server_id): + """Removes the encrypted server password from the metadata server + + Note that this does not actually change the instance server + password. + """ + resp, body = self.delete("servers/%s/os-server-password" % + server_id) + self.validate_response(schema.server_actions_delete_password, + resp, body) + return rest_client.ResponseBody(resp, body) + + def reboot_server(self, server_id, **kwargs): + """Reboot a server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#reboot + """ + return self.action(server_id, 'reboot', **kwargs) + + def rebuild_server(self, server_id, image_ref, **kwargs): + """Rebuild a server with a new image. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#rebuild + + Most parameters except the following are passed to the API without + any changes. + :param disk_config: The name is changed to OS-DCF:diskConfig + """ + kwargs['imageRef'] = image_ref + if 'disk_config' in kwargs: + kwargs['OS-DCF:diskConfig'] = kwargs.pop('disk_config') + if self.enable_instance_password: + rebuild_schema = schema.rebuild_server_with_admin_pass + else: + rebuild_schema = schema.rebuild_server + return self.action(server_id, 'rebuild', + rebuild_schema, **kwargs) + + def resize_server(self, server_id, flavor_ref, **kwargs): + """Change the flavor of a server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#resize + + Most parameters except the following are passed to the API without + any changes. + :param disk_config: The name is changed to OS-DCF:diskConfig + """ + kwargs['flavorRef'] = flavor_ref + if 'disk_config' in kwargs: + kwargs['OS-DCF:diskConfig'] = kwargs.pop('disk_config') + return self.action(server_id, 'resize', **kwargs) + + def confirm_resize_server(self, server_id, **kwargs): + """Confirm the flavor change for a server. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#confirmResize + """ + return self.action(server_id, 'confirmResize', + schema.server_actions_confirm_resize, + **kwargs) + + def revert_resize_server(self, server_id, **kwargs): + """Revert a server back to its original flavor. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#revertResize + """ + return self.action(server_id, 'revertResize', **kwargs) + + def list_server_metadata(self, server_id): + resp, body = self.get("servers/%s/metadata" % server_id) + body = json.loads(body) + self.validate_response(schema.list_server_metadata, resp, body) + return rest_client.ResponseBody(resp, body) + + def set_server_metadata(self, server_id, meta, no_metadata_field=False): + if no_metadata_field: + post_body = "" + else: + post_body = json.dumps({'metadata': meta}) + resp, body = self.put('servers/%s/metadata' % server_id, + post_body) + body = json.loads(body) + self.validate_response(schema.set_server_metadata, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_server_metadata(self, server_id, meta): + post_body = json.dumps({'metadata': meta}) + resp, body = self.post('servers/%s/metadata' % server_id, + post_body) + body = json.loads(body) + self.validate_response(schema.update_server_metadata, + resp, body) + return rest_client.ResponseBody(resp, body) + + def show_server_metadata_item(self, server_id, key): + resp, body = self.get("servers/%s/metadata/%s" % (server_id, key)) + body = json.loads(body) + self.validate_response(schema.set_show_server_metadata_item, + resp, body) + return rest_client.ResponseBody(resp, body) + + def set_server_metadata_item(self, server_id, key, meta): + post_body = json.dumps({'meta': meta}) + resp, body = self.put('servers/%s/metadata/%s' % (server_id, key), + post_body) + body = json.loads(body) + self.validate_response(schema.set_show_server_metadata_item, + resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_server_metadata_item(self, server_id, key): + resp, body = self.delete("servers/%s/metadata/%s" % + (server_id, key)) + self.validate_response(schema.delete_server_metadata_item, + resp, body) + return rest_client.ResponseBody(resp, body) + + def stop_server(self, server_id, **kwargs): + return self.action(server_id, 'os-stop', **kwargs) + + def start_server(self, server_id, **kwargs): + return self.action(server_id, 'os-start', **kwargs) + + def attach_volume(self, server_id, **kwargs): + """Attaches a volume to a server instance.""" + post_body = json.dumps({'volumeAttachment': kwargs}) + resp, body = self.post('servers/%s/os-volume_attachments' % server_id, + post_body) + body = json.loads(body) + self.validate_response(schema.attach_volume, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_attached_volume(self, server_id, attachment_id, **kwargs): + """Swaps a volume attached to an instance for another volume""" + post_body = json.dumps({'volumeAttachment': kwargs}) + resp, body = self.put('servers/%s/os-volume_attachments/%s' % + (server_id, attachment_id), + post_body) + self.validate_response(schema.update_attached_volume, resp, body) + return rest_client.ResponseBody(resp, body) + + def detach_volume(self, server_id, volume_id): # noqa + """Detaches a volume from a server instance.""" + resp, body = self.delete('servers/%s/os-volume_attachments/%s' % + (server_id, volume_id)) + self.validate_response(schema.detach_volume, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_volume_attachment(self, server_id, volume_id): + """Return details about the given volume attachment.""" + resp, body = self.get('servers/%s/os-volume_attachments/%s' % ( + server_id, volume_id)) + body = json.loads(body) + self.validate_response(schema.show_volume_attachment, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_volume_attachments(self, server_id): + """Returns the list of volume attachments for a given instance.""" + resp, body = self.get('servers/%s/os-volume_attachments' % ( + server_id)) + body = json.loads(body) + self.validate_response(schema.list_volume_attachments, resp, body) + return rest_client.ResponseBody(resp, body) + + def add_security_group(self, server_id, **kwargs): + """Add a security group to the server. + + Available params: TODO + """ + # TODO(oomichi): The api-site doesn't contain this API description. + # So the above should be changed to the api-site link after + # adding the description on the api-site. + # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1524199 + return self.action(server_id, 'addSecurityGroup', **kwargs) + + def remove_security_group(self, server_id, **kwargs): + """Remove a security group from the server. + + Available params: TODO + """ + # TODO(oomichi): The api-site doesn't contain this API description. + # So the above should be changed to the api-site link after + # adding the description on the api-site. + # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1524199 + return self.action(server_id, 'removeSecurityGroup', **kwargs) + + def live_migrate_server(self, server_id, **kwargs): + """This should be called with administrator privileges. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#migrateLive + """ + return self.action(server_id, 'os-migrateLive', **kwargs) + + def migrate_server(self, server_id, **kwargs): + """Migrate a server to a new host. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#migrate + """ + return self.action(server_id, 'migrate', **kwargs) + + def lock_server(self, server_id, **kwargs): + """Lock the given server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#lock + """ + return self.action(server_id, 'lock', **kwargs) + + def unlock_server(self, server_id, **kwargs): + """UNlock the given server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#unlock + """ + return self.action(server_id, 'unlock', **kwargs) + + def suspend_server(self, server_id, **kwargs): + """Suspend the provided server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#suspend + """ + return self.action(server_id, 'suspend', **kwargs) + + def resume_server(self, server_id, **kwargs): + """Un-suspend the provided server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#resume + """ + return self.action(server_id, 'resume', **kwargs) + + def pause_server(self, server_id, **kwargs): + """Pause the provided server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#pause + """ + return self.action(server_id, 'pause', **kwargs) + + def unpause_server(self, server_id, **kwargs): + """Un-pause the provided server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#unpause + """ + return self.action(server_id, 'unpause', **kwargs) + + def reset_state(self, server_id, **kwargs): + """Reset the state of a server to active/error. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#resetState + """ + return self.action(server_id, 'os-resetState', **kwargs) + + def shelve_server(self, server_id, **kwargs): + """Shelve the provided server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#shelve + """ + return self.action(server_id, 'shelve', **kwargs) + + def unshelve_server(self, server_id, **kwargs): + """Un-shelve the provided server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#unshelve + """ + return self.action(server_id, 'unshelve', **kwargs) + + def shelve_offload_server(self, server_id, **kwargs): + """Shelve-offload the provided server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#shelveOffload + """ + return self.action(server_id, 'shelveOffload', **kwargs) + + def get_console_output(self, server_id, **kwargs): + """Get console output. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#getConsoleOutput + """ + return self.action(server_id, 'os-getConsoleOutput', + schema.get_console_output, **kwargs) + + def list_virtual_interfaces(self, server_id): + """List the virtual interfaces used in an instance.""" + resp, body = self.get('/'.join(['servers', server_id, + 'os-virtual-interfaces'])) + body = json.loads(body) + self.validate_response(schema.list_virtual_interfaces, resp, body) + return rest_client.ResponseBody(resp, body) + + def rescue_server(self, server_id, **kwargs): + """Rescue the provided server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#rescue + """ + return self.action(server_id, 'rescue', schema.rescue_server, **kwargs) + + def unrescue_server(self, server_id): + """Unrescue the provided server.""" + return self.action(server_id, 'unrescue') + + def show_server_diagnostics(self, server_id): + """Get the usage data for a server.""" + resp, body = self.get("servers/%s/diagnostics" % server_id) + return rest_client.ResponseBody(resp, json.loads(body)) + + def list_instance_actions(self, server_id): + """List the provided server action.""" + resp, body = self.get("servers/%s/os-instance-actions" % + server_id) + body = json.loads(body) + self.validate_response(schema.list_instance_actions, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_instance_action(self, server_id, request_id): + """Returns the action details of the provided server.""" + resp, body = self.get("servers/%s/os-instance-actions/%s" % + (server_id, request_id)) + body = json.loads(body) + self.validate_response(schema.show_instance_action, resp, body) + return rest_client.ResponseBody(resp, body) + + def force_delete_server(self, server_id, **kwargs): + """Force delete a server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#forceDelete + """ + return self.action(server_id, 'forceDelete', **kwargs) + + def restore_soft_deleted_server(self, server_id, **kwargs): + """Restore a soft-deleted server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#restore + """ + return self.action(server_id, 'restore', **kwargs) + + def reset_network(self, server_id, **kwargs): + """Reset the Network of a server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#resetNetwork + """ + return self.action(server_id, 'resetNetwork', **kwargs) + + def inject_network_info(self, server_id, **kwargs): + """Inject the Network Info into server. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#injectNetworkInfo + """ + return self.action(server_id, 'injectNetworkInfo', **kwargs) + + def get_vnc_console(self, server_id, **kwargs): + """Get URL of VNC console. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#getVNCConsole + """ + return self.action(server_id, "os-getVNCConsole", + schema.get_vnc_console, **kwargs) + + def add_fixed_ip(self, server_id, **kwargs): + """Add a fixed IP to server instance. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#addFixedIp + """ + return self.action(server_id, 'addFixedIp', **kwargs) + + def remove_fixed_ip(self, server_id, **kwargs): + """Remove input fixed IP from input server instance. + + Available params: http://developer.openstack.org/ + api-ref-compute-v2.1.html#removeFixedIp + """ + return self.action(server_id, 'removeFixedIp', **kwargs) diff --git a/tempest/lib/services/compute/services_client.py b/tempest/lib/services/compute/services_client.py new file mode 100644 index 0000000000..06aad77bbf --- /dev/null +++ b/tempest/lib/services/compute/services_client.py @@ -0,0 +1,58 @@ +# Copyright 2013 NEC Corporation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import services as schema +from tempest.lib.common import rest_client + + +class ServicesClient(rest_client.RestClient): + + def list_services(self, **params): + url = 'os-services' + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_services, resp, body) + return rest_client.ResponseBody(resp, body) + + def enable_service(self, **kwargs): + """Enable service on a host. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#enableScheduling + """ + post_body = json.dumps(kwargs) + resp, body = self.put('os-services/enable', post_body) + body = json.loads(body) + self.validate_response(schema.enable_disable_service, resp, body) + return rest_client.ResponseBody(resp, body) + + def disable_service(self, **kwargs): + """Disable service on a host. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#disableScheduling + """ + post_body = json.dumps(kwargs) + resp, body = self.put('os-services/disable', post_body) + body = json.loads(body) + self.validate_response(schema.enable_disable_service, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/snapshots_client.py b/tempest/lib/services/compute/snapshots_client.py new file mode 100644 index 0000000000..de776bd387 --- /dev/null +++ b/tempest/lib/services/compute/snapshots_client.py @@ -0,0 +1,76 @@ +# Copyright 2015 Fujitsu(fnst) Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import snapshots as schema +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc + + +class SnapshotsClient(rest_client.RestClient): + + def create_snapshot(self, volume_id, **kwargs): + """Create a snapshot. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createSnapshot + """ + post_body = { + 'volume_id': volume_id + } + post_body.update(kwargs) + post_body = json.dumps({'snapshot': post_body}) + resp, body = self.post('os-snapshots', post_body) + body = json.loads(body) + self.validate_response(schema.create_get_snapshot, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_snapshot(self, snapshot_id): + url = "os-snapshots/%s" % snapshot_id + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.create_get_snapshot, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_snapshots(self, detail=False, params=None): + url = 'os-snapshots' + + if detail: + url += '/detail' + if params: + url += '?%s' % urllib.urlencode(params) + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_snapshots, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_snapshot(self, snapshot_id): + resp, body = self.delete("os-snapshots/%s" % snapshot_id) + self.validate_response(schema.delete_snapshot, resp, body) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + try: + self.show_snapshot(id) + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Return the primary type of resource this client works with.""" + return 'snapshot' diff --git a/tempest/lib/services/compute/tenant_networks_client.py b/tempest/lib/services/compute/tenant_networks_client.py new file mode 100644 index 0000000000..44a97a983b --- /dev/null +++ b/tempest/lib/services/compute/tenant_networks_client.py @@ -0,0 +1,34 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json + +from tempest.lib.api_schema.response.compute.v2_1 import tenant_networks +from tempest.lib.common import rest_client + + +class TenantNetworksClient(rest_client.RestClient): + + def list_tenant_networks(self): + resp, body = self.get("os-tenant-networks") + body = json.loads(body) + self.validate_response(tenant_networks.list_tenant_networks, resp, + body) + return rest_client.ResponseBody(resp, body) + + def show_tenant_network(self, network_id): + resp, body = self.get("os-tenant-networks/%s" % network_id) + body = json.loads(body) + self.validate_response(tenant_networks.get_tenant_network, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/tenant_usages_client.py b/tempest/lib/services/compute/tenant_usages_client.py new file mode 100644 index 0000000000..e8da465f79 --- /dev/null +++ b/tempest/lib/services/compute/tenant_usages_client.py @@ -0,0 +1,43 @@ +# Copyright 2013 NEC Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import tenant_usages +from tempest.lib.common import rest_client + + +class TenantUsagesClient(rest_client.RestClient): + + def list_tenant_usages(self, **params): + url = 'os-simple-tenant-usage' + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(tenant_usages.list_tenant_usage, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_tenant_usage(self, tenant_id, **params): + url = 'os-simple-tenant-usage/%s' % tenant_id + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(tenant_usages.get_tenant_usage, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/versions_client.py b/tempest/lib/services/compute/versions_client.py new file mode 100644 index 0000000000..5898f9370e --- /dev/null +++ b/tempest/lib/services/compute/versions_client.py @@ -0,0 +1,55 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves import urllib + +from tempest.lib.api_schema.response.compute.v2_1 import versions as schema +from tempest.lib.common import rest_client + + +class VersionsClient(rest_client.RestClient): + + def _get_base_version_url(self): + # NOTE: The URL which is gotten from keystone's catalog contains + # API version and project-id like "v2/{project-id}", but we need + # to access the URL which doesn't contain them for getting API + # versions. For that, here should use raw_request() instead of + # get(). + endpoint = self.base_url + url = urllib.parse.urlparse(endpoint) + return '%s://%s/' % (url.scheme, url.netloc) + + def list_versions(self): + version_url = self._get_base_version_url() + resp, body = self.raw_request(version_url, 'GET') + body = json.loads(body) + self.validate_response(schema.list_versions, resp, body) + return rest_client.ResponseBody(resp, body) + + def get_version_by_url(self, version_url): + """Get the version document by url. + + This gets the version document for a url, useful in testing + the contents of things like /v2/ or /v2.1/ in Nova. That + controller needs authenticated access, so we have to get + ourselves a token before making the request. + + """ + # we need a token for this request + resp, body = self.raw_request(version_url, 'GET', + {'X-Auth-Token': self.token}) + body = json.loads(body) + self.validate_response(schema.get_one_version, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/compute/volumes_client.py b/tempest/lib/services/compute/volumes_client.py new file mode 100644 index 0000000000..45a44de8b3 --- /dev/null +++ b/tempest/lib/services/compute/volumes_client.py @@ -0,0 +1,76 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.api_schema.response.compute.v2_1 import volumes as schema +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc + + +class VolumesClient(rest_client.RestClient): + + def list_volumes(self, detail=False, **params): + """List all the volumes created.""" + url = 'os-volumes' + + if detail: + url += '/detail' + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.list_volumes, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_volume(self, volume_id): + """Return the details of a single volume.""" + url = "os-volumes/%s" % volume_id + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(schema.create_get_volume, resp, body) + return rest_client.ResponseBody(resp, body) + + def create_volume(self, **kwargs): + """Create a new Volume. + + Available params: see http://developer.openstack.org/ + api-ref-compute-v2.1.html#createVolume + """ + post_body = json.dumps({'volume': kwargs}) + resp, body = self.post('os-volumes', post_body) + body = json.loads(body) + self.validate_response(schema.create_get_volume, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_volume(self, volume_id): + """Delete the Specified Volume.""" + resp, body = self.delete("os-volumes/%s" % volume_id) + self.validate_response(schema.delete_volume, resp, body) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + try: + self.show_volume(id) + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Return the primary type of resource this client works with.""" + return 'volume' diff --git a/tempest/lib/services/identity/__init__.py b/tempest/lib/services/identity/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/services/identity/v2/__init__.py b/tempest/lib/services/identity/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/services/identity/v2/token_client.py b/tempest/lib/services/identity/v2/token_client.py new file mode 100644 index 0000000000..03501757a7 --- /dev/null +++ b/tempest/lib/services/identity/v2/token_client.py @@ -0,0 +1,121 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from oslo_serialization import jsonutils as json + +from tempest.lib.common import rest_client +from tempest.lib import exceptions + + +class TokenClient(rest_client.RestClient): + + def __init__(self, auth_url, disable_ssl_certificate_validation=None, + ca_certs=None, trace_requests=None): + dscv = disable_ssl_certificate_validation + super(TokenClient, self).__init__( + None, None, None, disable_ssl_certificate_validation=dscv, + ca_certs=ca_certs, trace_requests=trace_requests) + + if auth_url is None: + raise exceptions.IdentityError("Couldn't determine auth_url") + + # Normalize URI to ensure /tokens is in it. + if 'tokens' not in auth_url: + auth_url = auth_url.rstrip('/') + '/tokens' + + self.auth_url = auth_url + + def auth(self, user, password, tenant=None): + creds = { + 'auth': { + 'passwordCredentials': { + 'username': user, + 'password': password, + }, + } + } + + if tenant: + creds['auth']['tenantName'] = tenant + + body = json.dumps(creds, sort_keys=True) + resp, body = self.post(self.auth_url, body=body) + self.expected_success(200, resp.status) + + return rest_client.ResponseBody(resp, body['access']) + + def auth_token(self, token_id, tenant=None): + creds = { + 'auth': { + 'token': { + 'id': token_id, + }, + } + } + + if tenant: + creds['auth']['tenantName'] = tenant + + body = json.dumps(creds) + resp, body = self.post(self.auth_url, body=body) + self.expected_success(200, resp.status) + + return rest_client.ResponseBody(resp, body['access']) + + def request(self, method, url, extra_headers=False, headers=None, + body=None): + """A simple HTTP request interface.""" + if headers is None: + headers = self.get_headers(accept_type="json") + elif extra_headers: + try: + headers.update(self.get_headers(accept_type="json")) + except (ValueError, TypeError): + headers = self.get_headers(accept_type="json") + + resp, resp_body = self.raw_request(url, method, + headers=headers, body=body) + self._log_request(method, url, resp, req_headers=headers, + req_body='', resp_body=resp_body) + + if resp.status in [401, 403]: + resp_body = json.loads(resp_body) + raise exceptions.Unauthorized(resp_body['error']['message']) + elif resp.status not in [200, 201]: + raise exceptions.IdentityError( + 'Unexpected status code {0}'.format(resp.status)) + + return resp, json.loads(resp_body) + + def get_token(self, user, password, tenant, auth_data=False): + """Returns (token id, token data) for supplied credentials.""" + body = self.auth(user, password, tenant) + + if auth_data: + return body['token']['id'], body + else: + return body['token']['id'] + + +class TokenClientJSON(TokenClient): + LOG = logging.getLogger(__name__) + + def _warn(self): + self.LOG.warning("%s class was deprecated and renamed to %s" % + (self.__class__.__name__, 'TokenClient')) + + def __init__(self, *args, **kwargs): + self._warn() + super(TokenClientJSON, self).__init__(*args, **kwargs) diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/services/identity/v3/token_client.py b/tempest/lib/services/identity/v3/token_client.py new file mode 100644 index 0000000000..f342a497e9 --- /dev/null +++ b/tempest/lib/services/identity/v3/token_client.py @@ -0,0 +1,183 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from oslo_serialization import jsonutils as json + +from tempest.lib.common import rest_client +from tempest.lib import exceptions + + +class V3TokenClient(rest_client.RestClient): + + def __init__(self, auth_url, disable_ssl_certificate_validation=None, + ca_certs=None, trace_requests=None): + dscv = disable_ssl_certificate_validation + super(V3TokenClient, self).__init__( + None, None, None, disable_ssl_certificate_validation=dscv, + ca_certs=ca_certs, trace_requests=trace_requests) + + if auth_url is None: + raise exceptions.IdentityError("Couldn't determine auth_url") + + if 'auth/tokens' not in auth_url: + auth_url = auth_url.rstrip('/') + '/auth/tokens' + + self.auth_url = auth_url + + def auth(self, user_id=None, username=None, password=None, project_id=None, + project_name=None, user_domain_id=None, user_domain_name=None, + project_domain_id=None, project_domain_name=None, domain_id=None, + domain_name=None, token=None): + """Obtains a token from the authentication service + + :param user_id: user id + :param username: user name + :param user_domain_id: the user domain id + :param user_domain_name: the user domain name + :param project_domain_id: the project domain id + :param project_domain_name: the project domain name + :param domain_id: a domain id to scope to + :param domain_name: a domain name to scope to + :param project_id: a project id to scope to + :param project_name: a project name to scope to + :param token: a token to re-scope. + + Accepts different combinations of credentials. + Sample sample valid combinations: + - token + - token, project_name, project_domain_id + - user_id, password + - username, password, user_domain_id + - username, password, project_name, user_domain_id, project_domain_id + Validation is left to the server side. + """ + creds = { + 'auth': { + 'identity': { + 'methods': [], + } + } + } + id_obj = creds['auth']['identity'] + if token: + id_obj['methods'].append('token') + id_obj['token'] = { + 'id': token + } + + if (user_id or username) and password: + id_obj['methods'].append('password') + id_obj['password'] = { + 'user': { + 'password': password, + } + } + if user_id: + id_obj['password']['user']['id'] = user_id + else: + id_obj['password']['user']['name'] = username + + _domain = None + if user_domain_id is not None: + _domain = dict(id=user_domain_id) + elif user_domain_name is not None: + _domain = dict(name=user_domain_name) + if _domain: + id_obj['password']['user']['domain'] = _domain + + if (project_id or project_name): + _project = dict() + + if project_id: + _project['id'] = project_id + elif project_name: + _project['name'] = project_name + + if project_domain_id is not None: + _project['domain'] = {'id': project_domain_id} + elif project_domain_name is not None: + _project['domain'] = {'name': project_domain_name} + + creds['auth']['scope'] = dict(project=_project) + elif domain_id: + creds['auth']['scope'] = dict(domain={'id': domain_id}) + elif domain_name: + creds['auth']['scope'] = dict(domain={'name': domain_name}) + + body = json.dumps(creds, sort_keys=True) + resp, body = self.post(self.auth_url, body=body) + self.expected_success(201, resp.status) + return rest_client.ResponseBody(resp, body) + + def request(self, method, url, extra_headers=False, headers=None, + body=None): + """A simple HTTP request interface.""" + if headers is None: + # Always accept 'json', for xml token client too. + # Because XML response is not easily + # converted to the corresponding JSON one + headers = self.get_headers(accept_type="json") + elif extra_headers: + try: + headers.update(self.get_headers(accept_type="json")) + except (ValueError, TypeError): + headers = self.get_headers(accept_type="json") + + resp, resp_body = self.raw_request(url, method, + headers=headers, body=body) + self._log_request(method, url, resp, req_headers=headers, + req_body='', resp_body=resp_body) + + if resp.status in [401, 403]: + resp_body = json.loads(resp_body) + raise exceptions.Unauthorized(resp_body['error']['message']) + elif resp.status not in [200, 201, 204]: + raise exceptions.IdentityError( + 'Unexpected status code {0}'.format(resp.status)) + + return resp, json.loads(resp_body) + + def get_token(self, **kwargs): + """Returns (token id, token data) for supplied credentials""" + + auth_data = kwargs.pop('auth_data', False) + + if not (kwargs.get('user_domain_id') or + kwargs.get('user_domain_name')): + kwargs['user_domain_name'] = 'Default' + + if not (kwargs.get('project_domain_id') or + kwargs.get('project_domain_name')): + kwargs['project_domain_name'] = 'Default' + + body = self.auth(**kwargs) + + token = body.response.get('x-subject-token') + if auth_data: + return token, body['token'] + else: + return token + + +class V3TokenClientJSON(V3TokenClient): + LOG = logging.getLogger(__name__) + + def _warn(self): + self.LOG.warning("%s class was deprecated and renamed to %s" % + (self.__class__.__name__, 'V3TokenClient')) + + def __init__(self, *args, **kwargs): + self._warn() + super(V3TokenClientJSON, self).__init__(*args, **kwargs) diff --git a/tempest/lib/services/network/__init__.py b/tempest/lib/services/network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/services/network/agents_client.py b/tempest/lib/services/network/agents_client.py new file mode 100644 index 0000000000..c5d4c66fa5 --- /dev/null +++ b/tempest/lib/services/network/agents_client.py @@ -0,0 +1,68 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.network import base + + +class AgentsClient(base.BaseNetworkClient): + + def update_agent(self, agent_id, **kwargs): + """Update agent.""" + # TODO(piyush): Current api-site doesn't contain this API description. + # After fixing the api-site, we need to fix here also for putting the + # link to api-site. + # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1526673 + uri = '/agents/%s' % agent_id + return self.update_resource(uri, kwargs) + + def show_agent(self, agent_id, **fields): + uri = '/agents/%s' % agent_id + return self.show_resource(uri, **fields) + + def list_agents(self, **filters): + uri = '/agents' + return self.list_resources(uri, **filters) + + def list_routers_on_l3_agent(self, agent_id): + uri = '/agents/%s/l3-routers' % agent_id + return self.list_resources(uri) + + def create_router_on_l3_agent(self, agent_id, **kwargs): + # TODO(piyush): Current api-site doesn't contain this API description. + # After fixing the api-site, we need to fix here also for putting the + # link to api-site. + # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1526670 + uri = '/agents/%s/l3-routers' % agent_id + return self.create_resource(uri, kwargs) + + def delete_router_from_l3_agent(self, agent_id, router_id): + uri = '/agents/%s/l3-routers/%s' % (agent_id, router_id) + return self.delete_resource(uri) + + def list_networks_hosted_by_one_dhcp_agent(self, agent_id): + uri = '/agents/%s/dhcp-networks' % agent_id + return self.list_resources(uri) + + def delete_network_from_dhcp_agent(self, agent_id, network_id): + uri = '/agents/%s/dhcp-networks/%s' % (agent_id, + network_id) + return self.delete_resource(uri) + + def add_dhcp_agent_to_network(self, agent_id, **kwargs): + # TODO(piyush): Current api-site doesn't contain this API description. + # After fixing the api-site, we need to fix here also for putting the + # link to api-site. + # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1526212 + uri = '/agents/%s/dhcp-networks' % agent_id + return self.create_resource(uri, kwargs) diff --git a/tempest/lib/services/network/base.py b/tempest/lib/services/network/base.py new file mode 100644 index 0000000000..a6ada04309 --- /dev/null +++ b/tempest/lib/services/network/base.py @@ -0,0 +1,71 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest.lib.common import rest_client + + +class BaseNetworkClient(rest_client.RestClient): + + """Base class for Tempest REST clients for Neutron. + + Child classes use v2 of the Neutron API, since the V1 API has been + removed from the code base. + """ + + version = '2.0' + uri_prefix = "v2.0" + + def list_resources(self, uri, **filters): + req_uri = self.uri_prefix + uri + if filters: + req_uri += '?' + urllib.urlencode(filters, doseq=1) + resp, body = self.get(req_uri) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def delete_resource(self, uri): + req_uri = self.uri_prefix + uri + resp, body = self.delete(req_uri) + self.expected_success(204, resp.status) + return rest_client.ResponseBody(resp, body) + + def show_resource(self, uri, **fields): + # fields is a dict which key is 'fields' and value is a + # list of field's name. An example: + # {'fields': ['id', 'name']} + req_uri = self.uri_prefix + uri + if fields: + req_uri += '?' + urllib.urlencode(fields, doseq=1) + resp, body = self.get(req_uri) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def create_resource(self, uri, post_data): + req_uri = self.uri_prefix + uri + req_post_data = json.dumps(post_data) + resp, body = self.post(req_uri, req_post_data) + body = json.loads(body) + self.expected_success(201, resp.status) + return rest_client.ResponseBody(resp, body) + + def update_resource(self, uri, post_data): + req_uri = self.uri_prefix + uri + req_post_data = json.dumps(post_data) + resp, body = self.put(req_uri, req_post_data) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) diff --git a/tempest/lib/services/network/extensions_client.py b/tempest/lib/services/network/extensions_client.py new file mode 100644 index 0000000000..3910c849c2 --- /dev/null +++ b/tempest/lib/services/network/extensions_client.py @@ -0,0 +1,24 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.services.network import base + + +class ExtensionsClient(base.BaseNetworkClient): + + def show_extension(self, ext_alias, **fields): + uri = '/extensions/%s' % ext_alias + return self.show_resource(uri, **fields) + + def list_extensions(self, **filters): + uri = '/extensions' + return self.list_resources(uri, **filters) diff --git a/tempest/lib/services/network/floating_ips_client.py b/tempest/lib/services/network/floating_ips_client.py new file mode 100644 index 0000000000..1968e05c19 --- /dev/null +++ b/tempest/lib/services/network/floating_ips_client.py @@ -0,0 +1,38 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.services.network import base + + +class FloatingIPsClient(base.BaseNetworkClient): + + def create_floatingip(self, **kwargs): + uri = '/floatingips' + post_data = {'floatingip': kwargs} + return self.create_resource(uri, post_data) + + def update_floatingip(self, floatingip_id, **kwargs): + uri = '/floatingips/%s' % floatingip_id + post_data = {'floatingip': kwargs} + return self.update_resource(uri, post_data) + + def show_floatingip(self, floatingip_id, **fields): + uri = '/floatingips/%s' % floatingip_id + return self.show_resource(uri, **fields) + + def delete_floatingip(self, floatingip_id): + uri = '/floatingips/%s' % floatingip_id + return self.delete_resource(uri) + + def list_floatingips(self, **filters): + uri = '/floatingips' + return self.list_resources(uri, **filters) diff --git a/tempest/lib/services/network/metering_label_rules_client.py b/tempest/lib/services/network/metering_label_rules_client.py new file mode 100644 index 0000000000..36cf8e30a8 --- /dev/null +++ b/tempest/lib/services/network/metering_label_rules_client.py @@ -0,0 +1,33 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.services.network import base + + +class MeteringLabelRulesClient(base.BaseNetworkClient): + + def create_metering_label_rule(self, **kwargs): + uri = '/metering/metering-label-rules' + post_data = {'metering_label_rule': kwargs} + return self.create_resource(uri, post_data) + + def show_metering_label_rule(self, metering_label_rule_id, **fields): + uri = '/metering/metering-label-rules/%s' % metering_label_rule_id + return self.show_resource(uri, **fields) + + def delete_metering_label_rule(self, metering_label_rule_id): + uri = '/metering/metering-label-rules/%s' % metering_label_rule_id + return self.delete_resource(uri) + + def list_metering_label_rules(self, **filters): + uri = '/metering/metering-label-rules' + return self.list_resources(uri, **filters) diff --git a/tempest/lib/services/network/metering_labels_client.py b/tempest/lib/services/network/metering_labels_client.py new file mode 100644 index 0000000000..2350ecd325 --- /dev/null +++ b/tempest/lib/services/network/metering_labels_client.py @@ -0,0 +1,33 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.services.network import base + + +class MeteringLabelsClient(base.BaseNetworkClient): + + def create_metering_label(self, **kwargs): + uri = '/metering/metering-labels' + post_data = {'metering_label': kwargs} + return self.create_resource(uri, post_data) + + def show_metering_label(self, metering_label_id, **fields): + uri = '/metering/metering-labels/%s' % metering_label_id + return self.show_resource(uri, **fields) + + def delete_metering_label(self, metering_label_id): + uri = '/metering/metering-labels/%s' % metering_label_id + return self.delete_resource(uri) + + def list_metering_labels(self, **filters): + uri = '/metering/metering-labels' + return self.list_resources(uri, **filters) diff --git a/tempest/lib/services/network/networks_client.py b/tempest/lib/services/network/networks_client.py new file mode 100644 index 0000000000..0926634237 --- /dev/null +++ b/tempest/lib/services/network/networks_client.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.services.network import base + + +class NetworksClient(base.BaseNetworkClient): + + def create_network(self, **kwargs): + uri = '/networks' + post_data = {'network': kwargs} + return self.create_resource(uri, post_data) + + def update_network(self, network_id, **kwargs): + uri = '/networks/%s' % network_id + post_data = {'network': kwargs} + return self.update_resource(uri, post_data) + + def show_network(self, network_id, **fields): + uri = '/networks/%s' % network_id + return self.show_resource(uri, **fields) + + def delete_network(self, network_id): + uri = '/networks/%s' % network_id + return self.delete_resource(uri) + + def list_networks(self, **filters): + uri = '/networks' + return self.list_resources(uri, **filters) + + def create_bulk_networks(self, **kwargs): + """Create multiple networks in a single request. + + Available params: see http://developer.openstack.org/ + api-ref-networking-v2.html#bulkCreateNetwork + """ + uri = '/networks' + return self.create_resource(uri, kwargs) diff --git a/tempest/lib/services/network/ports_client.py b/tempest/lib/services/network/ports_client.py new file mode 100644 index 0000000000..1793d6aebd --- /dev/null +++ b/tempest/lib/services/network/ports_client.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.services.network import base + + +class PortsClient(base.BaseNetworkClient): + + def create_port(self, **kwargs): + uri = '/ports' + post_data = {'port': kwargs} + return self.create_resource(uri, post_data) + + def update_port(self, port_id, **kwargs): + uri = '/ports/%s' % port_id + post_data = {'port': kwargs} + return self.update_resource(uri, post_data) + + def show_port(self, port_id, **fields): + uri = '/ports/%s' % port_id + return self.show_resource(uri, **fields) + + def delete_port(self, port_id): + uri = '/ports/%s' % port_id + return self.delete_resource(uri) + + def list_ports(self, **filters): + uri = '/ports' + return self.list_resources(uri, **filters) + + def create_bulk_ports(self, **kwargs): + """Create multiple ports in a single request. + + Available params: see http://developer.openstack.org/ + api-ref-networking-v2.html#bulkCreatePorts + """ + uri = '/ports' + return self.create_resource(uri, kwargs) diff --git a/tempest/lib/services/network/quotas_client.py b/tempest/lib/services/network/quotas_client.py new file mode 100644 index 0000000000..b5cf35b95d --- /dev/null +++ b/tempest/lib/services/network/quotas_client.py @@ -0,0 +1,35 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.network import base + + +class QuotasClient(base.BaseNetworkClient): + + def update_quotas(self, tenant_id, **kwargs): + put_body = {'quota': kwargs} + uri = '/quotas/%s' % tenant_id + return self.update_resource(uri, put_body) + + def reset_quotas(self, tenant_id): + uri = '/quotas/%s' % tenant_id + return self.delete_resource(uri) + + def show_quotas(self, tenant_id, **fields): + uri = '/quotas/%s' % tenant_id + return self.show_resource(uri, **fields) + + def list_quotas(self, **filters): + uri = '/quotas' + return self.list_resources(uri, **filters) diff --git a/tempest/lib/services/network/security_group_rules_client.py b/tempest/lib/services/network/security_group_rules_client.py new file mode 100644 index 0000000000..944eba6891 --- /dev/null +++ b/tempest/lib/services/network/security_group_rules_client.py @@ -0,0 +1,33 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.services.network import base + + +class SecurityGroupRulesClient(base.BaseNetworkClient): + + def create_security_group_rule(self, **kwargs): + uri = '/security-group-rules' + post_data = {'security_group_rule': kwargs} + return self.create_resource(uri, post_data) + + def show_security_group_rule(self, security_group_rule_id, **fields): + uri = '/security-group-rules/%s' % security_group_rule_id + return self.show_resource(uri, **fields) + + def delete_security_group_rule(self, security_group_rule_id): + uri = '/security-group-rules/%s' % security_group_rule_id + return self.delete_resource(uri) + + def list_security_group_rules(self, **filters): + uri = '/security-group-rules' + return self.list_resources(uri, **filters) diff --git a/tempest/lib/services/network/security_groups_client.py b/tempest/lib/services/network/security_groups_client.py new file mode 100644 index 0000000000..0e25339314 --- /dev/null +++ b/tempest/lib/services/network/security_groups_client.py @@ -0,0 +1,38 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.services.network import base + + +class SecurityGroupsClient(base.BaseNetworkClient): + + def create_security_group(self, **kwargs): + uri = '/security-groups' + post_data = {'security_group': kwargs} + return self.create_resource(uri, post_data) + + def update_security_group(self, security_group_id, **kwargs): + uri = '/security-groups/%s' % security_group_id + post_data = {'security_group': kwargs} + return self.update_resource(uri, post_data) + + def show_security_group(self, security_group_id, **fields): + uri = '/security-groups/%s' % security_group_id + return self.show_resource(uri, **fields) + + def delete_security_group(self, security_group_id): + uri = '/security-groups/%s' % security_group_id + return self.delete_resource(uri) + + def list_security_groups(self, **filters): + uri = '/security-groups' + return self.list_resources(uri, **filters) diff --git a/tempest/lib/services/network/subnetpools_client.py b/tempest/lib/services/network/subnetpools_client.py new file mode 100644 index 0000000000..12349b120d --- /dev/null +++ b/tempest/lib/services/network/subnetpools_client.py @@ -0,0 +1,40 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.network import base + + +class SubnetpoolsClient(base.BaseNetworkClient): + + def list_subnetpools(self, **filters): + uri = '/subnetpools' + return self.list_resources(uri, **filters) + + def create_subnetpool(self, **kwargs): + uri = '/subnetpools' + post_data = {'subnetpool': kwargs} + return self.create_resource(uri, post_data) + + def show_subnetpool(self, subnetpool_id, **fields): + uri = '/subnetpools/%s' % subnetpool_id + return self.show_resource(uri, **fields) + + def update_subnetpool(self, subnetpool_id, **kwargs): + uri = '/subnetpools/%s' % subnetpool_id + post_data = {'subnetpool': kwargs} + return self.update_resource(uri, post_data) + + def delete_subnetpool(self, subnetpool_id): + uri = '/subnetpools/%s' % subnetpool_id + return self.delete_resource(uri) diff --git a/tempest/lib/services/network/subnets_client.py b/tempest/lib/services/network/subnets_client.py new file mode 100644 index 0000000000..63ed13e3ca --- /dev/null +++ b/tempest/lib/services/network/subnets_client.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.services.network import base + + +class SubnetsClient(base.BaseNetworkClient): + + def create_subnet(self, **kwargs): + uri = '/subnets' + post_data = {'subnet': kwargs} + return self.create_resource(uri, post_data) + + def update_subnet(self, subnet_id, **kwargs): + uri = '/subnets/%s' % subnet_id + post_data = {'subnet': kwargs} + return self.update_resource(uri, post_data) + + def show_subnet(self, subnet_id, **fields): + uri = '/subnets/%s' % subnet_id + return self.show_resource(uri, **fields) + + def delete_subnet(self, subnet_id): + uri = '/subnets/%s' % subnet_id + return self.delete_resource(uri) + + def list_subnets(self, **filters): + uri = '/subnets' + return self.list_resources(uri, **filters) + + def create_bulk_subnets(self, **kwargs): + """Create multiple subnets in a single request. + + Available params: see http://developer.openstack.org/ + api-ref-networking-v2.html#bulkCreateSubnet + """ + uri = '/subnets' + return self.create_resource(uri, kwargs) diff --git a/tempest/tests/lib/__init__.py b/tempest/tests/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/lib/base.py b/tempest/tests/lib/base.py new file mode 100644 index 0000000000..fe9268e766 --- /dev/null +++ b/tempest/tests/lib/base.py @@ -0,0 +1,44 @@ +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslotest import base +from oslotest import moxstubout + + +class TestCase(base.BaseTestCase): + + def setUp(self): + super(TestCase, self).setUp() + mox_fixture = self.useFixture(moxstubout.MoxStubout()) + self.mox = mox_fixture.mox + self.stubs = mox_fixture.stubs + + def patch(self, target, **kwargs): + """Returns a started `mock.patch` object for the supplied target. + + The caller may then call the returned patcher to create a mock object. + + The caller does not need to call stop() on the returned + patcher object, as this method automatically adds a cleanup + to the test class to stop the patcher. + + :param target: String module.class or module.object expression to patch + :param **kwargs: Passed as-is to `mock.patch`. See mock documentation + for details. + """ + p = mock.patch(target, **kwargs) + m = p.start() + self.addCleanup(p.stop) + return m diff --git a/tempest/tests/lib/cli/__init__.py b/tempest/tests/lib/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/lib/cli/test_command_failed.py b/tempest/tests/lib/cli/test_command_failed.py new file mode 100644 index 0000000000..8ce34c2f34 --- /dev/null +++ b/tempest/tests/lib/cli/test_command_failed.py @@ -0,0 +1,30 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib import exceptions +from tempest.tests.lib import base + + +class TestOutputParser(base.TestCase): + + def test_command_failed_exception(self): + returncode = 1 + cmd = "foo" + stdout = "output" + stderr = "error" + try: + raise exceptions.CommandFailed(returncode, cmd, stdout, stderr) + except exceptions.CommandFailed as e: + self.assertIn(str(returncode), str(e)) + self.assertIn(cmd, str(e)) + self.assertIn(stdout, str(e)) + self.assertIn(stderr, str(e)) diff --git a/tempest/tests/lib/cli/test_execute.py b/tempest/tests/lib/cli/test_execute.py new file mode 100644 index 0000000000..b5f7145f7c --- /dev/null +++ b/tempest/tests/lib/cli/test_execute.py @@ -0,0 +1,37 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 tempest.lib.cli import base as cli_base +from tempest.lib import exceptions +from tempest.tests.lib import base + + +class TestExecute(base.TestCase): + def test_execute_success(self): + result = cli_base.execute("/bin/ls", action="tempest", + flags="-l -a") + self.assertIsInstance(result, str) + self.assertIn("__init__.py", result) + + def test_execute_failure(self): + result = cli_base.execute("/bin/ls", action="tempest.lib", + flags="--foobar", merge_stderr=True, + fail_ok=True) + self.assertIsInstance(result, str) + self.assertIn("--foobar", result) + + def test_execute_failure_raise_exception(self): + self.assertRaises(exceptions.CommandFailed, cli_base.execute, + "/bin/ls", action="tempest", flags="--foobar", + merge_stderr=True) diff --git a/tempest/tests/lib/cli/test_output_parser.py b/tempest/tests/lib/cli/test_output_parser.py new file mode 100644 index 0000000000..a2c1b2d0ca --- /dev/null +++ b/tempest/tests/lib/cli/test_output_parser.py @@ -0,0 +1,177 @@ +# Copyright 2014 NEC Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from tempest.lib.cli import output_parser +from tempest.lib import exceptions +from tempest.tests.lib import base + + +class TestOutputParser(base.TestCase): + OUTPUT_LINES = """ ++----+------+---------+ +| ID | Name | Status | ++----+------+---------+ +| 11 | foo | BUILD | +| 21 | bar | ERROR | +| 31 | bee | None | ++----+------+---------+ +""" + OUTPUT_LINES2 = """ ++----+-------+---------+ +| ID | Name2 | Status2 | ++----+-------+---------+ +| 41 | aaa | SSSSS | +| 51 | bbb | TTTTT | +| 61 | ccc | AAAAA | ++----+-------+---------+ +""" + + EXPECTED_TABLE = {'headers': ['ID', 'Name', 'Status'], + 'values': [['11', 'foo', 'BUILD'], + ['21', 'bar', 'ERROR'], + ['31', 'bee', 'None']]} + EXPECTED_TABLE2 = {'headers': ['ID', 'Name2', 'Status2'], + 'values': [['41', 'aaa', 'SSSSS'], + ['51', 'bbb', 'TTTTT'], + ['61', 'ccc', 'AAAAA']]} + + def test_table_with_normal_values(self): + actual = output_parser.table(self.OUTPUT_LINES) + self.assertIsInstance(actual, dict) + self.assertEqual(self.EXPECTED_TABLE, actual) + + def test_table_with_list(self): + output_lines = self.OUTPUT_LINES.split('\n') + actual = output_parser.table(output_lines) + self.assertIsInstance(actual, dict) + self.assertEqual(self.EXPECTED_TABLE, actual) + + def test_table_with_invalid_line(self): + output_lines = self.OUTPUT_LINES + "aaaa" + actual = output_parser.table(output_lines) + self.assertIsInstance(actual, dict) + self.assertEqual(self.EXPECTED_TABLE, actual) + + def test_tables_with_normal_values(self): + output_lines = ('test' + self.OUTPUT_LINES + + 'test2' + self.OUTPUT_LINES2) + expected = [{'headers': self.EXPECTED_TABLE['headers'], + 'label': 'test', + 'values': self.EXPECTED_TABLE['values']}, + {'headers': self.EXPECTED_TABLE2['headers'], + 'label': 'test2', + 'values': self.EXPECTED_TABLE2['values']}] + actual = output_parser.tables(output_lines) + self.assertIsInstance(actual, list) + self.assertEqual(expected, actual) + + def test_tables_with_invalid_values(self): + output_lines = ('test' + self.OUTPUT_LINES + + 'test2' + self.OUTPUT_LINES2 + '\n') + expected = [{'headers': self.EXPECTED_TABLE['headers'], + 'label': 'test', + 'values': self.EXPECTED_TABLE['values']}, + {'headers': self.EXPECTED_TABLE2['headers'], + 'label': 'test2', + 'values': self.EXPECTED_TABLE2['values']}] + actual = output_parser.tables(output_lines) + self.assertIsInstance(actual, list) + self.assertEqual(expected, actual) + + def test_tables_with_invalid_line(self): + output_lines = ('test' + self.OUTPUT_LINES + + 'test2' + self.OUTPUT_LINES2 + + '+----+-------+---------+') + expected = [{'headers': self.EXPECTED_TABLE['headers'], + 'label': 'test', + 'values': self.EXPECTED_TABLE['values']}, + {'headers': self.EXPECTED_TABLE2['headers'], + 'label': 'test2', + 'values': self.EXPECTED_TABLE2['values']}] + + actual = output_parser.tables(output_lines) + self.assertIsInstance(actual, list) + self.assertEqual(expected, actual) + + LISTING_OUTPUT = """ ++----+ +| ID | ++----+ +| 11 | +| 21 | +| 31 | ++----+ +""" + + def test_listing(self): + expected = [{'ID': '11'}, {'ID': '21'}, {'ID': '31'}] + actual = output_parser.listing(self.LISTING_OUTPUT) + self.assertIsInstance(actual, list) + self.assertEqual(expected, actual) + + def test_details_multiple_with_invalid_line(self): + self.assertRaises(exceptions.InvalidStructure, + output_parser.details_multiple, + self.OUTPUT_LINES) + + DETAILS_LINES1 = """First Table ++----------+--------+ +| Property | Value | ++----------+--------+ +| foo | BUILD | +| bar | ERROR | +| bee | None | ++----------+--------+ +""" + DETAILS_LINES2 = """Second Table ++----------+--------+ +| Property | Value | ++----------+--------+ +| aaa | VVVVV | +| bbb | WWWWW | +| ccc | XXXXX | ++----------+--------+ +""" + + def test_details_with_normal_line_label_false(self): + expected = {'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'} + actual = output_parser.details(self.DETAILS_LINES1) + self.assertEqual(expected, actual) + + def test_details_with_normal_line_label_true(self): + expected = {'__label': 'First Table', + 'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'} + actual = output_parser.details(self.DETAILS_LINES1, with_label=True) + self.assertEqual(expected, actual) + + def test_details_multiple_with_normal_line_label_false(self): + expected = [{'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'}, + {'aaa': 'VVVVV', 'bbb': 'WWWWW', 'ccc': 'XXXXX'}] + actual = output_parser.details_multiple(self.DETAILS_LINES1 + + self.DETAILS_LINES2) + self.assertIsInstance(actual, list) + self.assertEqual(expected, actual) + + def test_details_multiple_with_normal_line_label_true(self): + expected = [{'__label': 'First Table', + 'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'}, + {'__label': 'Second Table', + 'aaa': 'VVVVV', 'bbb': 'WWWWW', 'ccc': 'XXXXX'}] + actual = output_parser.details_multiple(self.DETAILS_LINES1 + + self.DETAILS_LINES2, + with_label=True) + self.assertIsInstance(actual, list) + self.assertEqual(expected, actual) diff --git a/tempest/tests/lib/common/__init__.py b/tempest/tests/lib/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/lib/common/utils/__init__.py b/tempest/tests/lib/common/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/lib/common/utils/test_data_utils.py b/tempest/tests/lib/common/utils/test_data_utils.py new file mode 100644 index 0000000000..07502d07ad --- /dev/null +++ b/tempest/tests/lib/common/utils/test_data_utils.py @@ -0,0 +1,162 @@ +# Copyright 2014 NEC Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import netaddr + +from tempest.lib.common.utils import data_utils +from tempest.tests.lib import base + + +class TestDataUtils(base.TestCase): + + def test_rand_uuid(self): + actual = data_utils.rand_uuid() + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]" + "{4}-[0-9a-f]{4}-[0-9a-f]{12}$") + actual2 = data_utils.rand_uuid() + self.assertNotEqual(actual, actual2) + + def test_rand_uuid_hex(self): + actual = data_utils.rand_uuid_hex() + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "^[0-9a-f]{32}$") + + actual2 = data_utils.rand_uuid_hex() + self.assertNotEqual(actual, actual2) + + def test_rand_name(self): + actual = data_utils.rand_name() + self.assertIsInstance(actual, str) + actual2 = data_utils.rand_name() + self.assertNotEqual(actual, actual2) + + actual = data_utils.rand_name('foo') + self.assertTrue(actual.startswith('foo')) + actual2 = data_utils.rand_name('foo') + self.assertTrue(actual.startswith('foo')) + self.assertNotEqual(actual, actual2) + + def test_rand_name_with_prefix(self): + actual = data_utils.rand_name(prefix='prefix-str') + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "^prefix-str-") + actual2 = data_utils.rand_name(prefix='prefix-str') + self.assertNotEqual(actual, actual2) + + def test_rand_password(self): + actual = data_utils.rand_password() + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "[A-Za-z0-9~!@#$%^&*_=+]{15,}") + actual2 = data_utils.rand_password() + self.assertNotEqual(actual, actual2) + + def test_rand_password_with_len(self): + actual = data_utils.rand_password(8) + self.assertIsInstance(actual, str) + self.assertEqual(len(actual), 8) + self.assertRegexpMatches(actual, "[A-Za-z0-9~!@#$%^&*_=+]{8}") + actual2 = data_utils.rand_password(8) + self.assertNotEqual(actual, actual2) + + def test_rand_password_with_len_2(self): + actual = data_utils.rand_password(2) + self.assertIsInstance(actual, str) + self.assertEqual(len(actual), 3) + self.assertRegexpMatches(actual, "[A-Za-z0-9~!@#$%^&*_=+]{3}") + actual2 = data_utils.rand_password(2) + self.assertNotEqual(actual, actual2) + + def test_rand_url(self): + actual = data_utils.rand_url() + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "^https://url-[0-9]*\.com$") + actual2 = data_utils.rand_url() + self.assertNotEqual(actual, actual2) + + def test_rand_int(self): + actual = data_utils.rand_int_id() + self.assertIsInstance(actual, int) + + actual2 = data_utils.rand_int_id() + self.assertNotEqual(actual, actual2) + + def test_rand_mac_address(self): + actual = data_utils.rand_mac_address() + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "^([0-9a-f][0-9a-f]:){5}" + "[0-9a-f][0-9a-f]$") + + actual2 = data_utils.rand_mac_address() + self.assertNotEqual(actual, actual2) + + def test_parse_image_id(self): + actual = data_utils.parse_image_id("/foo/bar/deadbeaf") + self.assertEqual("deadbeaf", actual) + + def test_arbitrary_string(self): + actual = data_utils.arbitrary_string() + self.assertEqual(actual, "test") + actual = data_utils.arbitrary_string(size=30, base_text="abc") + self.assertEqual(actual, "abc" * int(30 / len("abc"))) + actual = data_utils.arbitrary_string(size=5, base_text="deadbeaf") + self.assertEqual(actual, "deadb") + + def test_random_bytes(self): + actual = data_utils.random_bytes() # default size=1024 + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "^[\x00-\xFF]{1024}") + actual2 = data_utils.random_bytes() + self.assertNotEqual(actual, actual2) + + actual = data_utils.random_bytes(size=2048) + self.assertRegexpMatches(actual, "^[\x00-\xFF]{2048}") + + def test_get_ipv6_addr_by_EUI64(self): + actual = data_utils.get_ipv6_addr_by_EUI64('2001:db8::', + '00:16:3e:33:44:55') + self.assertIsInstance(actual, netaddr.IPAddress) + self.assertEqual(actual, + netaddr.IPAddress('2001:db8::216:3eff:fe33:4455')) + + def test_get_ipv6_addr_by_EUI64_with_IPv4_prefix(self): + ipv4_prefix = '10.0.8' + mac = '00:16:3e:33:44:55' + self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64, + ipv4_prefix, mac) + + def test_get_ipv6_addr_by_EUI64_bad_cidr_type(self): + bad_cidr = 123 + mac = '00:16:3e:33:44:55' + self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64, + bad_cidr, mac) + + def test_get_ipv6_addr_by_EUI64_bad_cidr_value(self): + bad_cidr = 'bb' + mac = '00:16:3e:33:44:55' + self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64, + bad_cidr, mac) + + def test_get_ipv6_addr_by_EUI64_bad_mac_value(self): + cidr = '2001:db8::' + bad_mac = '00:16:3e:33:44:5Z' + self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64, + cidr, bad_mac) + + def test_get_ipv6_addr_by_EUI64_bad_mac_type(self): + cidr = '2001:db8::' + bad_mac = 99999999999999999999 + self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64, + cidr, bad_mac) diff --git a/tempest/tests/lib/common/utils/test_misc.py b/tempest/tests/lib/common/utils/test_misc.py new file mode 100644 index 0000000000..e23d7fb68c --- /dev/null +++ b/tempest/tests/lib/common/utils/test_misc.py @@ -0,0 +1,88 @@ +# Copyright 2014 NEC Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from tempest.lib.common.utils import misc +from tempest.tests.lib import base + + +@misc.singleton +class TestFoo(object): + + count = 0 + + def increment(self): + self.count += 1 + return self.count + + +@misc.singleton +class TestBar(object): + + count = 0 + + def increment(self): + self.count += 1 + return self.count + + +class TestMisc(base.TestCase): + + def test_singleton(self): + test = TestFoo() + self.assertEqual(0, test.count) + self.assertEqual(1, test.increment()) + test2 = TestFoo() + self.assertEqual(1, test.count) + self.assertEqual(1, test2.count) + self.assertEqual(test, test2) + test3 = TestBar() + self.assertNotEqual(test, test3) + + def test_find_test_caller_test_case(self): + # Calling it from here should give us the method we're in. + self.assertEqual('TestMisc:test_find_test_caller_test_case', + misc.find_test_caller()) + + def test_find_test_caller_setup_self(self): + def setUp(self): + return misc.find_test_caller() + self.assertEqual('TestMisc:setUp', setUp(self)) + + def test_find_test_caller_setup_no_self(self): + def setUp(): + return misc.find_test_caller() + self.assertEqual(':setUp', setUp()) + + def test_find_test_caller_setupclass_cls(self): + def setUpClass(cls): # noqa + return misc.find_test_caller() + self.assertEqual('TestMisc:setUpClass', setUpClass(self.__class__)) + + def test_find_test_caller_teardown_self(self): + def tearDown(self): + return misc.find_test_caller() + self.assertEqual('TestMisc:tearDown', tearDown(self)) + + def test_find_test_caller_teardown_no_self(self): + def tearDown(): + return misc.find_test_caller() + self.assertEqual(':tearDown', tearDown()) + + def test_find_test_caller_teardown_class(self): + def tearDownClass(cls): # noqa + return misc.find_test_caller() + self.assertEqual('TestMisc:tearDownClass', + tearDownClass(self.__class__)) diff --git a/tempest/tests/lib/fake_auth_provider.py b/tempest/tests/lib/fake_auth_provider.py new file mode 100644 index 0000000000..7f00fb8ad0 --- /dev/null +++ b/tempest/tests/lib/fake_auth_provider.py @@ -0,0 +1,34 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class FakeAuthProvider(object): + + def __init__(self, creds_dict=None): + creds_dict = creds_dict or {} + self.credentials = FakeCredentials(creds_dict) + + def auth_request(self, method, url, headers=None, body=None, filters=None): + return url, headers, body + + def base_url(self, filters, auth_data=None): + return "https://example.com" + + +class FakeCredentials(object): + + def __init__(self, creds_dict): + for key in creds_dict.keys(): + setattr(self, key, creds_dict[key]) diff --git a/tempest/tests/lib/fake_credentials.py b/tempest/tests/lib/fake_credentials.py new file mode 100644 index 0000000000..fb81bd6f9a --- /dev/null +++ b/tempest/tests/lib/fake_credentials.py @@ -0,0 +1,59 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib import auth + + +class FakeCredentials(auth.Credentials): + + def is_valid(self): + return True + + +class FakeKeystoneV2Credentials(auth.KeystoneV2Credentials): + + def __init__(self): + creds = dict( + username='fake_username', + password='fake_password', + tenant_name='fake_tenant_name' + ) + super(FakeKeystoneV2Credentials, self).__init__(**creds) + + +class FakeKeystoneV3Credentials(auth.KeystoneV3Credentials): + """Fake credentials suitable for the Keystone Identity V3 API""" + + def __init__(self): + creds = dict( + username='fake_username', + password='fake_password', + user_domain_name='fake_domain_name', + project_name='fake_tenant_name', + project_domain_name='fake_domain_name' + ) + super(FakeKeystoneV3Credentials, self).__init__(**creds) + + +class FakeKeystoneV3DomainCredentials(auth.KeystoneV3Credentials): + """Fake credentials for the Keystone Identity V3 API, with no scope""" + + def __init__(self): + creds = dict( + username='fake_username', + password='fake_password', + user_domain_name='fake_domain_name' + ) + super(FakeKeystoneV3DomainCredentials, self).__init__(**creds) diff --git a/tempest/tests/lib/fake_http.py b/tempest/tests/lib/fake_http.py new file mode 100644 index 0000000000..eda202d436 --- /dev/null +++ b/tempest/tests/lib/fake_http.py @@ -0,0 +1,74 @@ +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +import httplib2 + + +class fake_httplib2(object): + + def __init__(self, return_type=None, *args, **kwargs): + self.return_type = return_type + + def request(self, uri, method="GET", body=None, headers=None, + redirections=5, connection_type=None): + if not self.return_type: + fake_headers = httplib2.Response(headers) + return_obj = { + 'uri': uri, + 'method': method, + 'body': body, + 'headers': headers + } + return (fake_headers, return_obj) + elif isinstance(self.return_type, int): + body = body or "fake_body" + header_info = { + 'content-type': 'text/plain', + 'status': str(self.return_type), + 'content-length': len(body) + } + resp_header = httplib2.Response(header_info) + return (resp_header, body) + else: + msg = "unsupported return type %s" % self.return_type + raise TypeError(msg) + + +class fake_httplib(object): + def __init__(self, headers, body=None, + version=1.0, status=200, reason="Ok"): + """Fake httplib implementation + + :param headers: dict representing HTTP response headers + :param body: file-like object + :param version: HTTP Version + :param status: Response status code + :param reason: Status code related message. + """ + self.body = body + self.status = status + self.reason = reason + self.version = version + self.headers = headers + + def getheaders(self): + return copy.deepcopy(self.headers).items() + + def getheader(self, key, default): + return self.headers.get(key, default) + + def read(self, amt): + return self.body.read(amt) diff --git a/tempest/tests/lib/fake_identity.py b/tempest/tests/lib/fake_identity.py new file mode 100644 index 0000000000..bac26761fd --- /dev/null +++ b/tempest/tests/lib/fake_identity.py @@ -0,0 +1,164 @@ +# Copyright 2014 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import httplib2 + +FAKE_AUTH_URL = 'http://fake_uri.com/auth' + +TOKEN = "fake_token" +ALT_TOKEN = "alt_fake_token" + +# Fake Identity v2 constants +COMPUTE_ENDPOINTS_V2 = { + "endpoints": [ + { + "adminURL": "http://fake_url/v2/first_endpoint/admin", + "region": "NoMatchRegion", + "internalURL": "http://fake_url/v2/first_endpoint/internal", + "publicURL": "http://fake_url/v2/first_endpoint/public" + }, + { + "adminURL": "http://fake_url/v2/second_endpoint/admin", + "region": "FakeRegion", + "internalURL": "http://fake_url/v2/second_endpoint/internal", + "publicURL": "http://fake_url/v2/second_endpoint/public" + }, + ], + "type": "compute", + "name": "nova" +} + +CATALOG_V2 = [COMPUTE_ENDPOINTS_V2, ] + +ALT_IDENTITY_V2_RESPONSE = { + "access": { + "token": { + "expires": "2020-01-01T00:00:10Z", + "id": ALT_TOKEN, + "tenant": { + "id": "fake_alt_tenant_id" + }, + }, + "user": { + "id": "fake_alt_user_id", + }, + "serviceCatalog": CATALOG_V2, + }, +} + +IDENTITY_V2_RESPONSE = { + "access": { + "token": { + "expires": "2020-01-01T00:00:10Z", + "id": TOKEN, + "tenant": { + "id": "fake_tenant_id" + }, + }, + "user": { + "id": "fake_user_id", + }, + "serviceCatalog": CATALOG_V2, + }, +} + +# Fake Identity V3 constants +COMPUTE_ENDPOINTS_V3 = { + "endpoints": [ + { + "id": "first_compute_fake_service", + "interface": "public", + "region": "NoMatchRegion", + "url": "http://fake_url/v3/first_endpoint/api" + }, + { + "id": "second_fake_service", + "interface": "public", + "region": "FakeRegion", + "url": "http://fake_url/v3/second_endpoint/api" + }, + { + "id": "third_fake_service", + "interface": "admin", + "region": "MiddleEarthRegion", + "url": "http://fake_url/v3/third_endpoint/api" + } + + ], + "type": "compute", + "id": "fake_compute_endpoint" +} + +CATALOG_V3 = [COMPUTE_ENDPOINTS_V3, ] + +IDENTITY_V3_RESPONSE = { + "token": { + "methods": [ + "token", + "password" + ], + "expires_at": "2020-01-01T00:00:10.000123Z", + "project": { + "domain": { + "id": "fake_domain_id", + "name": "fake" + }, + "id": "project_id", + "name": "project_name" + }, + "user": { + "domain": { + "id": "fake_domain_id", + "name": "domain_name" + }, + "id": "fake_user_id", + "name": "username" + }, + "issued_at": "2013-05-29T16:55:21.468960Z", + "catalog": CATALOG_V3 + } +} + +ALT_IDENTITY_V3 = IDENTITY_V3_RESPONSE + + +def _fake_v3_response(self, uri, method="GET", body=None, headers=None, + redirections=5, connection_type=None): + fake_headers = { + "status": "201", + "x-subject-token": TOKEN + } + return (httplib2.Response(fake_headers), + json.dumps(IDENTITY_V3_RESPONSE)) + + +def _fake_v2_response(self, uri, method="GET", body=None, headers=None, + redirections=5, connection_type=None): + return (httplib2.Response({"status": "200"}), + json.dumps(IDENTITY_V2_RESPONSE)) + + +def _fake_auth_failure_response(): + # the response body isn't really used in this case, but lets send it anyway + # to have a safe check in some future change on the rest client. + body = { + "unauthorized": { + "message": "Unauthorized", + "code": "401" + } + } + return httplib2.Response({"status": "401"}), json.dumps(body) diff --git a/tempest/tests/lib/services/__init__.py b/tempest/tests/lib/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/lib/services/compute/__init__.py b/tempest/tests/lib/services/compute/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/lib/services/compute/base.py b/tempest/tests/lib/services/compute/base.py new file mode 100644 index 0000000000..56020443da --- /dev/null +++ b/tempest/tests/lib/services/compute/base.py @@ -0,0 +1,45 @@ +# Copyright 2015 Deutsche Telekom AG. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httplib2 +from oslo_serialization import jsonutils as json +from oslotest import mockpatch + +from tempest.tests.lib import base + + +class BaseComputeServiceTest(base.TestCase): + def create_response(self, body, to_utf=False, status=200, headers=None): + json_body = {} + if body: + json_body = json.dumps(body) + if to_utf: + json_body = json_body.encode('utf-8') + resp_dict = {'status': status} + if headers: + resp_dict.update(headers) + response = (httplib2.Response(resp_dict), json_body) + return response + + def check_service_client_function(self, function, function2mock, + body, to_utf=False, status=200, + headers=None, **kwargs): + mocked_response = self.create_response(body, to_utf, status, headers) + self.useFixture(mockpatch.Patch( + function2mock, return_value=mocked_response)) + if kwargs: + resp = function(**kwargs) + else: + resp = function() + self.assertEqual(body, resp) diff --git a/tempest/tests/lib/services/compute/test_agents_client.py b/tempest/tests/lib/services/compute/test_agents_client.py new file mode 100644 index 0000000000..3c5043db0a --- /dev/null +++ b/tempest/tests/lib/services/compute/test_agents_client.py @@ -0,0 +1,103 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import agents_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestAgentsClient(base.BaseComputeServiceTest): + FAKE_CREATE_AGENT = { + "agent": { + "url": "http://foo.com", + "hypervisor": "kvm", + "md5hash": "md5", + "version": "2", + "architecture": "x86_64", + "os": "linux", + "agent_id": 1 + } + } + + FAKE_UPDATE_AGENT = { + "agent": { + "url": "http://foo.com", + "md5hash": "md5", + "version": "2", + "agent_id": 1 + } + } + + def setUp(self): + super(TestAgentsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = agents_client.AgentsClient(fake_auth, + 'compute', 'regionOne') + + def _test_list_agents(self, bytes_body=False): + self.check_service_client_function( + self.client.list_agents, + 'tempest.lib.common.rest_client.RestClient.get', + {"agents": []}, + bytes_body) + self.check_service_client_function( + self.client.list_agents, + 'tempest.lib.common.rest_client.RestClient.get', + {"agents": []}, + bytes_body, + hypervisor="kvm") + + def _test_create_agent(self, bytes_body=False): + self.check_service_client_function( + self.client.create_agent, + 'tempest.lib.common.rest_client.RestClient.post', + self.FAKE_CREATE_AGENT, + bytes_body, + url="http://foo.com", hypervisor="kvm", md5hash="md5", + version="2", architecture="x86_64", os="linux") + + def _test_delete_agent(self): + self.check_service_client_function( + self.client.delete_agent, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, agent_id="1") + + def _test_update_agent(self, bytes_body=False): + self.check_service_client_function( + self.client.update_agent, + 'tempest.lib.common.rest_client.RestClient.put', + self.FAKE_UPDATE_AGENT, + bytes_body, + agent_id="1", url="http://foo.com", md5hash="md5", version="2") + + def test_list_agents_with_str_body(self): + self._test_list_agents() + + def test_list_agents_with_bytes_body(self): + self._test_list_agents(bytes_body=True) + + def test_create_agent_with_str_body(self): + self._test_create_agent() + + def test_create_agent_with_bytes_body(self): + self._test_create_agent(bytes_body=True) + + def test_delete_agent(self): + self._test_delete_agent() + + def test_update_agent_with_str_body(self): + self._test_update_agent() + + def test_update_agent_with_bytes_body(self): + self._test_update_agent(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_aggregates_client.py b/tempest/tests/lib/services/compute/test_aggregates_client.py new file mode 100644 index 0000000000..a63380eb5f --- /dev/null +++ b/tempest/tests/lib/services/compute/test_aggregates_client.py @@ -0,0 +1,192 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import aggregates_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestAggregatesClient(base.BaseComputeServiceTest): + FAKE_SHOW_AGGREGATE = { + "aggregate": + { + "name": "hoge", + "availability_zone": None, + "deleted": False, + "created_at": + "2015-07-16T03:07:32.000000", + "updated_at": None, + "hosts": [], + "deleted_at": None, + "id": 1, + "metadata": {} + } + } + + FAKE_CREATE_AGGREGATE = { + "aggregate": + { + "name": u'\xf4', + "availability_zone": None, + "deleted": False, + "created_at": "2015-07-21T04:11:18.000000", + "updated_at": None, + "deleted_at": None, + "id": 1 + } + } + + FAKE_UPDATE_AGGREGATE = { + "aggregate": + { + "name": u'\xe9', + "availability_zone": None, + "deleted": False, + "created_at": "2015-07-16T03:07:32.000000", + "updated_at": "2015-07-23T05:16:29.000000", + "hosts": [], + "deleted_at": None, + "id": 1, + "metadata": {} + } + } + + FAKE_AGGREGATE = { + "availability_zone": "nova", + "created_at": "2013-08-18T12:17:56.297823", + "deleted": False, + "deleted_at": None, + "hosts": [ + "21549b2f665945baaa7101926a00143c" + ], + "id": 1, + "metadata": { + "availability_zone": "nova" + }, + "name": u'\xe9', + "updated_at": None + } + + FAKE_ADD_HOST = {'aggregate': FAKE_AGGREGATE} + FAKE_REMOVE_HOST = {'aggregate': FAKE_AGGREGATE} + FAKE_SET_METADATA = {'aggregate': FAKE_AGGREGATE} + + def setUp(self): + super(TestAggregatesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = aggregates_client.AggregatesClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_aggregates(self, bytes_body=False): + self.check_service_client_function( + self.client.list_aggregates, + 'tempest.lib.common.rest_client.RestClient.get', + {"aggregates": []}, + bytes_body) + + def test_list_aggregates_with_str_body(self): + self._test_list_aggregates() + + def test_list_aggregates_with_bytes_body(self): + self._test_list_aggregates(bytes_body=True) + + def _test_show_aggregate(self, bytes_body=False): + self.check_service_client_function( + self.client.show_aggregate, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_SHOW_AGGREGATE, + bytes_body, + aggregate_id=1) + + def test_show_aggregate_with_str_body(self): + self._test_show_aggregate() + + def test_show_aggregate_with_bytes_body(self): + self._test_show_aggregate(bytes_body=True) + + def _test_create_aggregate(self, bytes_body=False): + self.check_service_client_function( + self.client.create_aggregate, + 'tempest.lib.common.rest_client.RestClient.post', + self.FAKE_CREATE_AGGREGATE, + bytes_body, + name='hoge') + + def test_create_aggregate_with_str_body(self): + self._test_create_aggregate() + + def test_create_aggregate_with_bytes_body(self): + self._test_create_aggregate(bytes_body=True) + + def test_delete_aggregate(self): + self.check_service_client_function( + self.client.delete_aggregate, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, aggregate_id="1") + + def _test_update_aggregate(self, bytes_body=False): + self.check_service_client_function( + self.client.update_aggregate, + 'tempest.lib.common.rest_client.RestClient.put', + self.FAKE_UPDATE_AGGREGATE, + bytes_body, + aggregate_id=1) + + def test_update_aggregate_with_str_body(self): + self._test_update_aggregate() + + def test_update_aggregate_with_bytes_body(self): + self._test_update_aggregate(bytes_body=True) + + def _test_add_host(self, bytes_body=False): + self.check_service_client_function( + self.client.add_host, + 'tempest.lib.common.rest_client.RestClient.post', + self.FAKE_ADD_HOST, + bytes_body, + aggregate_id=1) + + def test_add_host_with_str_body(self): + self._test_add_host() + + def test_add_host_with_bytes_body(self): + self._test_add_host(bytes_body=True) + + def _test_remove_host(self, bytes_body=False): + self.check_service_client_function( + self.client.remove_host, + 'tempest.lib.common.rest_client.RestClient.post', + self.FAKE_REMOVE_HOST, + bytes_body, + aggregate_id=1) + + def test_remove_host_with_str_body(self): + self._test_remove_host() + + def test_remove_host_with_bytes_body(self): + self._test_remove_host(bytes_body=True) + + def _test_set_metadata(self, bytes_body=False): + self.check_service_client_function( + self.client.set_metadata, + 'tempest.lib.common.rest_client.RestClient.post', + self.FAKE_SET_METADATA, + bytes_body, + aggregate_id=1) + + def test_set_metadata_with_str_body(self): + self._test_set_metadata() + + def test_set_metadata_with_bytes_body(self): + self._test_set_metadata(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_availability_zone_client.py b/tempest/tests/lib/services/compute/test_availability_zone_client.py new file mode 100644 index 0000000000..d16cf0a1b7 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_availability_zone_client.py @@ -0,0 +1,51 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import availability_zone_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestAvailabilityZoneClient(base.BaseComputeServiceTest): + + FAKE_AVAILABIRITY_ZONE_INFO = { + "availabilityZoneInfo": + [ + { + "zoneState": { + "available": True + }, + "hosts": None, + "zoneName": u'\xf4' + } + ] + } + + def setUp(self): + super(TestAvailabilityZoneClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = availability_zone_client.AvailabilityZoneClient( + fake_auth, 'compute', 'regionOne') + + def test_list_availability_zones_with_str_body(self): + self.check_service_client_function( + self.client.list_availability_zones, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_AVAILABIRITY_ZONE_INFO) + + def test_list_availability_zones_with_bytes_body(self): + self.check_service_client_function( + self.client.list_availability_zones, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_AVAILABIRITY_ZONE_INFO, to_utf=True) diff --git a/tempest/tests/lib/services/compute/test_baremetal_nodes_client.py b/tempest/tests/lib/services/compute/test_baremetal_nodes_client.py new file mode 100644 index 0000000000..a867c06152 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_baremetal_nodes_client.py @@ -0,0 +1,74 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.services.compute import baremetal_nodes_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestBareMetalNodesClient(base.BaseComputeServiceTest): + + FAKE_NODE_INFO = {'cpus': '8', + 'disk_gb': '64', + 'host': '10.0.2.15', + 'id': 'Identifier', + 'instance_uuid': "null", + 'interfaces': [ + { + "address": "20::01", + "datapath_id": "null", + "id": 1, + "port_no": None + } + ], + 'memory_mb': '8192', + 'task_state': None} + + def setUp(self): + super(TestBareMetalNodesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.baremetal_nodes_client = (baremetal_nodes_client. + BaremetalNodesClient + (fake_auth, 'compute', + 'regionOne')) + + def _test_bareMetal_nodes(self, operation='list', bytes_body=False): + if operation != 'list': + expected = {"node": self.FAKE_NODE_INFO} + function = self.baremetal_nodes_client.show_baremetal_node + else: + node_info = copy.deepcopy(self.FAKE_NODE_INFO) + del node_info['instance_uuid'] + expected = {"nodes": [node_info]} + function = self.baremetal_nodes_client.list_baremetal_nodes + + self.check_service_client_function( + function, + 'tempest.lib.common.rest_client.RestClient.get', + expected, bytes_body, 200, + baremetal_node_id='Identifier') + + def test_list_bareMetal_nodes_with_str_body(self): + self._test_bareMetal_nodes() + + def test_list_bareMetal_nodes_with_bytes_body(self): + self._test_bareMetal_nodes(bytes_body=True) + + def test_show_bareMetal_node_with_str_body(self): + self._test_bareMetal_nodes('show') + + def test_show_bareMetal_node_with_bytes_body(self): + self._test_bareMetal_nodes('show', True) diff --git a/tempest/tests/lib/services/compute/test_certificates_client.py b/tempest/tests/lib/services/compute/test_certificates_client.py new file mode 100644 index 0000000000..e8123bbe3b --- /dev/null +++ b/tempest/tests/lib/services/compute/test_certificates_client.py @@ -0,0 +1,64 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.services.compute import certificates_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestCertificatesClient(base.BaseComputeServiceTest): + + FAKE_CERTIFICATE = { + "certificate": { + "data": "-----BEGIN----MIICyzCCAjSgAwI----END CERTIFICATE-----\n", + "private_key": None + } + } + + def setUp(self): + super(TestCertificatesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = certificates_client.CertificatesClient( + fake_auth, 'compute', 'regionOne') + + def _test_show_certificate(self, bytes_body=False): + self.check_service_client_function( + self.client.show_certificate, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_CERTIFICATE, + bytes_body, + certificate_id="fake-id") + + def test_show_certificate_with_str_body(self): + self._test_show_certificate() + + def test_show_certificate_with_bytes_body(self): + self._test_show_certificate(bytes_body=True) + + def _test_create_certificate(self, bytes_body=False): + cert = copy.deepcopy(self.FAKE_CERTIFICATE) + cert['certificate']['private_key'] = "my_private_key" + self.check_service_client_function( + self.client.create_certificate, + 'tempest.lib.common.rest_client.RestClient.post', + cert, + bytes_body) + + def test_create_certificate_with_str_body(self): + self._test_create_certificate() + + def test_create_certificate_with_bytes_body(self): + self._test_create_certificate(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_extensions_client.py b/tempest/tests/lib/services/compute/test_extensions_client.py new file mode 100644 index 0000000000..7415988ad6 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_extensions_client.py @@ -0,0 +1,65 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import extensions_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestExtensionsClient(base.BaseComputeServiceTest): + + FAKE_SHOW_EXTENSION = { + "extension": { + "updated": "2011-06-09T00:00:00Z", + "name": "Multinic", + "links": [], + "namespace": + "http://docs.openstack.org/compute/ext/multinic/api/v1.1", + "alias": "NMN", + "description": u'\u2740(*\xb4\u25e1`*)\u2740' + } + } + + def setUp(self): + super(TestExtensionsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = extensions_client.ExtensionsClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_extensions(self, bytes_body=False): + self.check_service_client_function( + self.client.list_extensions, + 'tempest.lib.common.rest_client.RestClient.get', + {"extensions": []}, + bytes_body) + + def test_list_extensions_with_str_body(self): + self._test_list_extensions() + + def test_list_extensions_with_bytes_body(self): + self._test_list_extensions(bytes_body=True) + + def _test_show_extension(self, bytes_body=False): + self.check_service_client_function( + self.client.show_extension, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_SHOW_EXTENSION, + bytes_body, + extension_alias="NMN") + + def test_show_extension_with_str_body(self): + self._test_show_extension() + + def test_show_extension_with_bytes_body(self): + self._test_show_extension(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_fixedIPs_client.py b/tempest/tests/lib/services/compute/test_fixedIPs_client.py new file mode 100644 index 0000000000..6999f248b7 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_fixedIPs_client.py @@ -0,0 +1,58 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import fixed_ips_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestFixedIPsClient(base.BaseComputeServiceTest): + FIXED_IP_INFO = {"fixed_ip": {"address": "10.0.0.1", + "cidr": "10.11.12.0/24", + "host": "localhost", + "hostname": "OpenStack"}} + + def setUp(self): + super(TestFixedIPsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.fixedIPsClient = (fixed_ips_client. + FixedIPsClient + (fake_auth, 'compute', + 'regionOne')) + + def _test_show_fixed_ip(self, bytes_body=False): + self.check_service_client_function( + self.fixedIPsClient.show_fixed_ip, + 'tempest.lib.common.rest_client.RestClient.get', + self.FIXED_IP_INFO, bytes_body, + status=200, fixed_ip='Identifier') + + def test_show_fixed_ip_with_str_body(self): + self._test_show_fixed_ip() + + def test_show_fixed_ip_with_bytes_body(self): + self._test_show_fixed_ip(True) + + def _test_reserve_fixed_ip(self, bytes_body=False): + self.check_service_client_function( + self.fixedIPsClient.reserve_fixed_ip, + 'tempest.lib.common.rest_client.RestClient.post', + {}, bytes_body, + status=202, fixed_ip='Identifier') + + def test_reserve_fixed_ip_with_str_body(self): + self._test_reserve_fixed_ip() + + def test_reserve_fixed_ip_with_bytes_body(self): + self._test_reserve_fixed_ip(True) diff --git a/tempest/tests/lib/services/compute/test_flavors_client.py b/tempest/tests/lib/services/compute/test_flavors_client.py new file mode 100644 index 0000000000..795aff7446 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_flavors_client.py @@ -0,0 +1,255 @@ +# Copyright 2015 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import httplib2 + +from oslo_serialization import jsonutils as json +from oslotest import mockpatch + +from tempest.lib.services.compute import flavors_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestFlavorsClient(base.BaseComputeServiceTest): + + FAKE_FLAVOR = { + "disk": 1, + "id": "1", + "links": [{ + "href": "http://openstack.example.com/v2/openstack/flavors/1", + "rel": "self"}, { + "href": "http://openstack.example.com/openstack/flavors/1", + "rel": "bookmark"}], + "name": "m1.tiny", + "ram": 512, + "swap": 1, + "vcpus": 1 + } + + EXTRA_SPECS = {"extra_specs": { + "key1": "value1", + "key2": "value2"} + } + + FAKE_FLAVOR_ACCESS = { + "flavor_id": "10", + "tenant_id": "1a951d988e264818afe520e78697dcbf" + } + + def setUp(self): + super(TestFlavorsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = flavors_client.FlavorsClient(fake_auth, + 'compute', 'regionOne') + + def _test_list_flavors(self, bytes_body=False): + flavor = copy.deepcopy(TestFlavorsClient.FAKE_FLAVOR) + # Remove extra attributes + for attribute in ('disk', 'vcpus', 'ram', 'swap'): + del flavor[attribute] + expected = {'flavors': [flavor]} + self.check_service_client_function( + self.client.list_flavors, + 'tempest.lib.common.rest_client.RestClient.get', + expected, + bytes_body) + + def test_list_flavors_str_body(self): + self._test_list_flavors(bytes_body=False) + + def test_list_flavors_byte_body(self): + self._test_list_flavors(bytes_body=True) + + def _test_show_flavor(self, bytes_body=False): + expected = {"flavor": TestFlavorsClient.FAKE_FLAVOR} + self.check_service_client_function( + self.client.show_flavor, + 'tempest.lib.common.rest_client.RestClient.get', + expected, + bytes_body, + flavor_id='fake-id') + + def test_show_flavor_str_body(self): + self._test_show_flavor(bytes_body=False) + + def test_show_flavor_byte_body(self): + self._test_show_flavor(bytes_body=True) + + def _test_create_flavor(self, bytes_body=False): + expected = {"flavor": TestFlavorsClient.FAKE_FLAVOR} + request = copy.deepcopy(TestFlavorsClient.FAKE_FLAVOR) + # The 'links' parameter should not be passed in + del request['links'] + self.check_service_client_function( + self.client.create_flavor, + 'tempest.lib.common.rest_client.RestClient.post', + expected, + bytes_body, + **request) + + def test_create_flavor_str_body(self): + self._test_create_flavor(bytes_body=False) + + def test_create_flavor__byte_body(self): + self._test_create_flavor(bytes_body=True) + + def test_delete_flavor(self): + self.check_service_client_function( + self.client.delete_flavor, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, status=202, flavor_id='c782b7a9-33cd-45f0-b795-7f87f456408b') + + def _test_is_resource_deleted(self, flavor_id, is_deleted=True, + bytes_body=False): + body = json.dumps({'flavors': [TestFlavorsClient.FAKE_FLAVOR]}) + if bytes_body: + body = body.encode('utf-8') + response = (httplib2.Response({'status': 200}), body) + self.useFixture(mockpatch.Patch( + 'tempest.lib.common.rest_client.RestClient.get', + return_value=response)) + self.assertEqual(is_deleted, + self.client.is_resource_deleted(flavor_id)) + + def test_is_resource_deleted_true_str_body(self): + self._test_is_resource_deleted('2', bytes_body=False) + + def test_is_resource_deleted_true_byte_body(self): + self._test_is_resource_deleted('2', bytes_body=True) + + def test_is_resource_deleted_false_str_body(self): + self._test_is_resource_deleted('1', is_deleted=False, bytes_body=False) + + def test_is_resource_deleted_false_byte_body(self): + self._test_is_resource_deleted('1', is_deleted=False, bytes_body=True) + + def _test_set_flavor_extra_spec(self, bytes_body=False): + self.check_service_client_function( + self.client.set_flavor_extra_spec, + 'tempest.lib.common.rest_client.RestClient.post', + TestFlavorsClient.EXTRA_SPECS, + bytes_body, + flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6', + **TestFlavorsClient.EXTRA_SPECS) + + def test_set_flavor_extra_spec_str_body(self): + self._test_set_flavor_extra_spec(bytes_body=False) + + def test_set_flavor_extra_spec_byte_body(self): + self._test_set_flavor_extra_spec(bytes_body=True) + + def _test_list_flavor_extra_specs(self, bytes_body=False): + self.check_service_client_function( + self.client.list_flavor_extra_specs, + 'tempest.lib.common.rest_client.RestClient.get', + TestFlavorsClient.EXTRA_SPECS, + bytes_body, + flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6') + + def test_list_flavor_extra_specs_str_body(self): + self._test_list_flavor_extra_specs(bytes_body=False) + + def test_list_flavor_extra_specs__byte_body(self): + self._test_list_flavor_extra_specs(bytes_body=True) + + def _test_show_flavor_extra_spec(self, bytes_body=False): + expected = {"key": "value"} + self.check_service_client_function( + self.client.show_flavor_extra_spec, + 'tempest.lib.common.rest_client.RestClient.get', + expected, + bytes_body, + flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6', + key='key') + + def test_show_flavor_extra_spec_str_body(self): + self._test_show_flavor_extra_spec(bytes_body=False) + + def test_show_flavor_extra_spec__byte_body(self): + self._test_show_flavor_extra_spec(bytes_body=True) + + def _test_update_flavor_extra_spec(self, bytes_body=False): + expected = {"key1": "value"} + self.check_service_client_function( + self.client.update_flavor_extra_spec, + 'tempest.lib.common.rest_client.RestClient.put', + expected, + bytes_body, + flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6', + key='key1', **expected) + + def test_update_flavor_extra_spec_str_body(self): + self._test_update_flavor_extra_spec(bytes_body=False) + + def test_update_flavor_extra_spec_byte_body(self): + self._test_update_flavor_extra_spec(bytes_body=True) + + def test_unset_flavor_extra_spec(self): + self.check_service_client_function( + self.client.unset_flavor_extra_spec, + 'tempest.lib.common.rest_client.RestClient.delete', {}, + flavor_id='c782b7a9-33cd-45f0-b795-7f87f456408b', key='key') + + def _test_list_flavor_access(self, bytes_body=False): + expected = {'flavor_access': [TestFlavorsClient.FAKE_FLAVOR_ACCESS]} + self.check_service_client_function( + self.client.list_flavor_access, + 'tempest.lib.common.rest_client.RestClient.get', + expected, + bytes_body, + flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6') + + def test_list_flavor_access_str_body(self): + self._test_list_flavor_access(bytes_body=False) + + def test_list_flavor_access_byte_body(self): + self._test_list_flavor_access(bytes_body=True) + + def _test_add_flavor_access(self, bytes_body=False): + expected = { + "flavor_access": [TestFlavorsClient.FAKE_FLAVOR_ACCESS] + } + self.check_service_client_function( + self.client.add_flavor_access, + 'tempest.lib.common.rest_client.RestClient.post', + expected, + bytes_body, + flavor_id='8c7aae5a-d315-4216-875b-ed9b6a5bcfc6', + tenant_id='1a951d988e264818afe520e78697dcbf') + + def test_add_flavor_access_str_body(self): + self._test_add_flavor_access(bytes_body=False) + + def test_add_flavor_access_byte_body(self): + self._test_add_flavor_access(bytes_body=True) + + def _test_remove_flavor_access(self, bytes_body=False): + expected = { + "flavor_access": [TestFlavorsClient.FAKE_FLAVOR_ACCESS] + } + self.check_service_client_function( + self.client.remove_flavor_access, + 'tempest.lib.common.rest_client.RestClient.post', + expected, + bytes_body, + flavor_id='10', + tenant_id='a6edd4d66ad04245b5d2d8716ecc91e3') + + def test_remove_flavor_access_str_body(self): + self._test_remove_flavor_access(bytes_body=False) + + def test_remove_flavor_access_byte_body(self): + self._test_remove_flavor_access(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_floating_ip_pools_client.py b/tempest/tests/lib/services/compute/test_floating_ip_pools_client.py new file mode 100644 index 0000000000..f30719eb19 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_floating_ip_pools_client.py @@ -0,0 +1,46 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import floating_ip_pools_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestFloatingIPPoolsClient(base.BaseComputeServiceTest): + + FAKE_FLOATING_IP_POOLS = { + "floating_ip_pools": + [ + {"name": u'\u3042'}, + {"name": u'\u3044'} + ] + } + + def setUp(self): + super(TestFloatingIPPoolsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = floating_ip_pools_client.FloatingIPPoolsClient( + fake_auth, 'compute', 'regionOne') + + def test_list_floating_ip_pools_with_str_body(self): + self.check_service_client_function( + self.client.list_floating_ip_pools, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_FLOATING_IP_POOLS) + + def test_list_floating_ip_pools_with_bytes_body(self): + self.check_service_client_function( + self.client.list_floating_ip_pools, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_FLOATING_IP_POOLS, to_utf=True) diff --git a/tempest/tests/lib/services/compute/test_floating_ips_bulk_client.py b/tempest/tests/lib/services/compute/test_floating_ips_bulk_client.py new file mode 100644 index 0000000000..c16c9857d8 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_floating_ips_bulk_client.py @@ -0,0 +1,88 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.tests.lib import fake_auth_provider + +from tempest.lib.services.compute import floating_ips_bulk_client +from tempest.tests.lib.services.compute import base + + +class TestFloatingIPsBulkClient(base.BaseComputeServiceTest): + + FAKE_FIP_BULK_LIST = {"floating_ip_info": [{ + "address": "10.10.10.1", + "instance_uuid": None, + "fixed_ip": None, + "interface": "eth0", + "pool": "nova", + "project_id": None + }, + { + "address": "10.10.10.2", + "instance_uuid": None, + "fixed_ip": None, + "interface": "eth0", + "pool": "nova", + "project_id": None + }]} + + def setUp(self): + super(TestFloatingIPsBulkClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = floating_ips_bulk_client.FloatingIPsBulkClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_floating_ips_bulk(self, bytes_body=False): + self.check_service_client_function( + self.client.list_floating_ips_bulk, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_FIP_BULK_LIST, + to_utf=bytes_body) + + def _test_create_floating_ips_bulk(self, bytes_body=False): + fake_fip_create_data = {"floating_ips_bulk_create": { + "ip_range": "192.168.1.0/24", "pool": "nova", "interface": "eth0"}} + self.check_service_client_function( + self.client.create_floating_ips_bulk, + 'tempest.lib.common.rest_client.RestClient.post', + fake_fip_create_data, + to_utf=bytes_body, + ip_range="192.168.1.0/24", pool="nova", interface="eth0") + + def _test_delete_floating_ips_bulk(self, bytes_body=False): + fake_fip_delete_data = {"floating_ips_bulk_delete": "192.168.1.0/24"} + self.check_service_client_function( + self.client.delete_floating_ips_bulk, + 'tempest.lib.common.rest_client.RestClient.put', + fake_fip_delete_data, + to_utf=bytes_body, + ip_range="192.168.1.0/24") + + def test_list_floating_ips_bulk_with_str_body(self): + self._test_list_floating_ips_bulk() + + def test_list_floating_ips_bulk_with_bytes_body(self): + self._test_list_floating_ips_bulk(True) + + def test_create_floating_ips_bulk_with_str_body(self): + self._test_create_floating_ips_bulk() + + def test_create_floating_ips_bulk_with_bytes_body(self): + self._test_create_floating_ips_bulk(True) + + def test_delete_floating_ips_bulk_with_str_body(self): + self._test_delete_floating_ips_bulk() + + def test_delete_floating_ips_bulk_with_bytes_body(self): + self._test_delete_floating_ips_bulk(True) diff --git a/tempest/tests/lib/services/compute/test_floating_ips_client.py b/tempest/tests/lib/services/compute/test_floating_ips_client.py new file mode 100644 index 0000000000..3844ba83f2 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_floating_ips_client.py @@ -0,0 +1,113 @@ +# Copyright 2015 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import mockpatch + +from tempest.lib import exceptions as lib_exc +from tempest.lib.services.compute import floating_ips_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestFloatingIpsClient(base.BaseComputeServiceTest): + + floating_ip = {"fixed_ip": None, + "id": "46d61064-13ba-4bf0-9557-69de824c3d6f", + "instance_id": "a1daa443-a6bb-463e-aea2-104b7d912eb8", + "ip": "10.10.10.1", + "pool": "nova"} + + def setUp(self): + super(TestFloatingIpsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = floating_ips_client.FloatingIPsClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_floating_ips(self, bytes_body=False): + expected = {'floating_ips': [TestFloatingIpsClient.floating_ip]} + self.check_service_client_function( + self.client.list_floating_ips, + 'tempest.lib.common.rest_client.RestClient.get', + expected, + bytes_body) + + def test_list_floating_ips_str_body(self): + self._test_list_floating_ips(bytes_body=False) + + def test_list_floating_ips_byte_body(self): + self._test_list_floating_ips(bytes_body=True) + + def _test_show_floating_ip(self, bytes_body=False): + expected = {"floating_ip": TestFloatingIpsClient.floating_ip} + self.check_service_client_function( + self.client.show_floating_ip, + 'tempest.lib.common.rest_client.RestClient.get', + expected, + bytes_body, + floating_ip_id='a1daa443-a6bb-463e-aea2-104b7d912eb8') + + def test_show_floating_ip_str_body(self): + self._test_show_floating_ip(bytes_body=False) + + def test_show_floating_ip_byte_body(self): + self._test_show_floating_ip(bytes_body=True) + + def _test_create_floating_ip(self, bytes_body=False): + expected = {"floating_ip": TestFloatingIpsClient.floating_ip} + self.check_service_client_function( + self.client.create_floating_ip, + 'tempest.lib.common.rest_client.RestClient.post', + expected, + bytes_body, + pool_name='nova') + + def test_create_floating_ip_str_body(self): + self._test_create_floating_ip(bytes_body=False) + + def test_create_floating_ip_byte_body(self): + self._test_create_floating_ip(bytes_body=True) + + def test_delete_floating_ip(self): + self.check_service_client_function( + self.client.delete_floating_ip, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, status=202, floating_ip_id='fake-id') + + def test_associate_floating_ip_to_server(self): + self.check_service_client_function( + self.client.associate_floating_ip_to_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, status=202, floating_ip='10.10.10.1', + server_id='c782b7a9-33cd-45f0-b795-7f87f456408b') + + def test_disassociate_floating_ip_from_server(self): + self.check_service_client_function( + self.client.disassociate_floating_ip_from_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, status=202, floating_ip='10.10.10.1', + server_id='c782b7a9-33cd-45f0-b795-7f87f456408b') + + def test_is_resource_deleted_true(self): + self.useFixture(mockpatch.Patch( + 'tempest.lib.services.compute.floating_ips_client.' + 'FloatingIPsClient.show_floating_ip', + side_effect=lib_exc.NotFound())) + self.assertTrue(self.client.is_resource_deleted('fake-id')) + + def test_is_resource_deleted_false(self): + self.useFixture(mockpatch.Patch( + 'tempest.lib.services.compute.floating_ips_client.' + 'FloatingIPsClient.show_floating_ip', + return_value={"floating_ip": TestFloatingIpsClient.floating_ip})) + self.assertFalse(self.client.is_resource_deleted('fake-id')) diff --git a/tempest/tests/lib/services/compute/test_hosts_client.py b/tempest/tests/lib/services/compute/test_hosts_client.py new file mode 100644 index 0000000000..d9ff51371c --- /dev/null +++ b/tempest/tests/lib/services/compute/test_hosts_client.py @@ -0,0 +1,147 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import hosts_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestHostsClient(base.BaseComputeServiceTest): + FAKE_HOST_DATA = { + "host": { + "resource": { + "cpu": 1, + "disk_gb": 1028, + "host": "c1a7de0ac9d94e4baceae031d05caae3", + "memory_mb": 8192, + "project": "(total)" + } + }, + "hosts": { + "host_name": "c1a7de0ac9d94e4baceae031d05caae3", + "service": "conductor", + "zone": "internal" + }, + "enable_hosts": { + "host": "65c5d5b7e3bd44308e67fc50f362aee6", + "maintenance_mode": "off_maintenance", + "status": "enabled" + } + } + + FAKE_CONTROL_DATA = { + "shutdown": { + "host": "c1a7de0ac9d94e4baceae031d05caae3", + "power_action": "shutdown" + }, + "startup": { + "host": "c1a7de0ac9d94e4baceae031d05caae3", + "power_action": "startup" + }, + "reboot": { + "host": "c1a7de0ac9d94e4baceae031d05caae3", + "power_action": "reboot" + }} + + HOST_DATA = {'host': [FAKE_HOST_DATA['host']]} + HOSTS_DATA = {'hosts': [FAKE_HOST_DATA['hosts']]} + ENABLE_HOST_DATA = FAKE_HOST_DATA['enable_hosts'] + HOST_ID = "c1a7de0ac9d94e4baceae031d05caae3" + TEST_HOST_DATA = { + "status": "enable", + "maintenance_mode": "disable" + } + + def setUp(self): + super(TestHostsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = hosts_client.HostsClient(fake_auth, 'compute', + 'regionOne') + self.params = {'hostname': self.HOST_ID} + self.func2mock = { + 'get': 'tempest.lib.common.rest_client.RestClient.get', + 'put': 'tempest.lib.common.rest_client.RestClient.put'} + + def _test_host_data(self, test_type='list', bytes_body=False): + expected_resp = self.HOST_DATA + if test_type != 'list': + function_call = self.client.show_host + else: + expected_resp = self.HOSTS_DATA + function_call = self.client.list_hosts + self.params = {'host_name': self.HOST_ID} + + self.check_service_client_function( + function_call, self.func2mock['get'], + expected_resp, bytes_body, + 200, **self.params) + + def _test_update_hosts(self, bytes_body=False): + expected_resp = self.ENABLE_HOST_DATA + self.check_service_client_function( + self.client.update_host, self.func2mock['put'], + expected_resp, bytes_body, + 200, **self.params) + + def _test_control_host(self, control_op='reboot', bytes_body=False): + if control_op == 'start': + expected_resp = self.FAKE_CONTROL_DATA['startup'] + function_call = self.client.startup_host + elif control_op == 'stop': + expected_resp = self.FAKE_CONTROL_DATA['shutdown'] + function_call = self.client.shutdown_host + else: + expected_resp = self.FAKE_CONTROL_DATA['reboot'] + function_call = self.client.reboot_host + + self.check_service_client_function( + function_call, self.func2mock['get'], + expected_resp, bytes_body, + 200, **self.params) + + def test_show_host_with_str_body(self): + self._test_host_data('show') + + def test_show_host_with_bytes_body(self): + self._test_host_data('show', True) + + def test_list_host_with_str_body(self): + self._test_host_data() + + def test_list_host_with_bytes_body(self): + self._test_host_data(bytes_body=True) + + def test_start_host_with_str_body(self): + self._test_control_host('start') + + def test_start_host_with_bytes_body(self): + self._test_control_host('start', True) + + def test_stop_host_with_str_body(self): + self._test_control_host('stop') + + def test_stop_host_with_bytes_body(self): + self._test_control_host('stop', True) + + def test_reboot_host_with_str_body(self): + self._test_control_host('reboot') + + def test_reboot_host_with_bytes_body(self): + self._test_control_host('reboot', True) + + def test_update_host_with_str_body(self): + self._test_update_hosts() + + def test_update_host_with_bytes_body(self): + self._test_update_hosts(True) diff --git a/tempest/tests/lib/services/compute/test_hypervisor_client.py b/tempest/tests/lib/services/compute/test_hypervisor_client.py new file mode 100644 index 0000000000..fab34da73d --- /dev/null +++ b/tempest/tests/lib/services/compute/test_hypervisor_client.py @@ -0,0 +1,167 @@ +# Copyright 2015 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import hypervisor_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestHypervisorClient(base.BaseComputeServiceTest): + + hypervisor_id = "1" + hypervisor_name = "hyper.hostname.com" + + def setUp(self): + super(TestHypervisorClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = hypervisor_client.HypervisorClient( + fake_auth, 'compute', 'regionOne') + + def test_list_hypervisor_str_body(self): + self._test_list_hypervisor(bytes_body=False) + + def test_list_hypervisor_byte_body(self): + self._test_list_hypervisor(bytes_body=True) + + def _test_list_hypervisor(self, bytes_body=False): + expected = {"hypervisors": [{ + "id": 1, + "hypervisor_hostname": "hypervisor1.hostname.com"}, + { + "id": 2, + "hypervisor_hostname": "hypervisor2.hostname.com"}]} + self.check_service_client_function( + self.client.list_hypervisors, + 'tempest.lib.common.rest_client.RestClient.get', + expected, bytes_body) + + def test_show_hypervisor_str_body(self): + self._test_show_hypervisor(bytes_body=False) + + def test_show_hypervisor_byte_body(self): + self._test_show_hypervisor(bytes_body=True) + + def _test_show_hypervisor(self, bytes_body=False): + expected = {"hypervisor": { + "cpu_info": "?", + "current_workload": 0, + "disk_available_least": 1, + "host_ip": "10.10.10.10", + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "hypervisor_hostname": "fake-mini", + "hypervisor_type": "fake", + "hypervisor_version": 1, + "id": 1, + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "service": { + "host": "fake_host", + "id": 2}, + "vcpus": 1, + "vcpus_used": 0}} + self.check_service_client_function( + self.client.show_hypervisor, + 'tempest.lib.common.rest_client.RestClient.get', + expected, bytes_body, + hypervisor_id=self.hypervisor_id) + + def test_list_servers_on_hypervisor_str_body(self): + self._test_list_servers_on_hypervisor(bytes_body=False) + + def test_list_servers_on_hypervisor_byte_body(self): + self._test_list_servers_on_hypervisor(bytes_body=True) + + def _test_list_servers_on_hypervisor(self, bytes_body=False): + expected = {"hypervisors": [{ + "id": 1, + "hypervisor_hostname": "hyper.hostname.com", + "servers": [{ + "uuid": "e1ae8fc4-b72d-4c2f-a427-30dd420b6277", + "name": "instance-00000001"}, + { + "uuid": "e1ae8fc4-b72d-4c2f-a427-30dd42066666", + "name": "instance-00000002"} + ]} + ]} + self.check_service_client_function( + self.client.list_servers_on_hypervisor, + 'tempest.lib.common.rest_client.RestClient.get', + expected, bytes_body, + hypervisor_name=self.hypervisor_name) + + def test_show_hypervisor_statistics_str_body(self): + self._test_show_hypervisor_statistics(bytes_body=False) + + def test_show_hypervisor_statistics_byte_body(self): + self._test_show_hypervisor_statistics(bytes_body=True) + + def _test_show_hypervisor_statistics(self, bytes_body=False): + expected = { + "hypervisor_statistics": { + "count": 1, + "current_workload": 0, + "disk_available_least": 0, + "free_disk_gb": 1028, + "free_ram_mb": 7680, + "local_gb": 1028, + "local_gb_used": 0, + "memory_mb": 8192, + "memory_mb_used": 512, + "running_vms": 0, + "vcpus": 1, + "vcpus_used": 0}} + self.check_service_client_function( + self.client.show_hypervisor_statistics, + 'tempest.lib.common.rest_client.RestClient.get', + expected, bytes_body) + + def test_show_hypervisor_uptime_str_body(self): + self._test_show_hypervisor_uptime(bytes_body=False) + + def test_show_hypervisor_uptime_byte_body(self): + self._test_show_hypervisor_uptime(bytes_body=True) + + def _test_show_hypervisor_uptime(self, bytes_body=False): + expected = { + "hypervisor": { + "hypervisor_hostname": "fake-mini", + "id": 1, + "uptime": (" 08:32:11 up 93 days, 18:25, 12 users, " + " load average: 0.20, 0.12, 0.14") + }} + self.check_service_client_function( + self.client.show_hypervisor_uptime, + 'tempest.lib.common.rest_client.RestClient.get', + expected, bytes_body, + hypervisor_id=self.hypervisor_id) + + def test_search_hypervisor_str_body(self): + self._test_search_hypervisor(bytes_body=False) + + def test_search_hypervisor_byte_body(self): + self._test_search_hypervisor(bytes_body=True) + + def _test_search_hypervisor(self, bytes_body=False): + expected = {"hypervisors": [{ + "id": 2, + "hypervisor_hostname": "hyper.hostname.com"}]} + self.check_service_client_function( + self.client.search_hypervisor, + 'tempest.lib.common.rest_client.RestClient.get', + expected, bytes_body, + hypervisor_name=self.hypervisor_name) diff --git a/tempest/tests/lib/services/compute/test_images_client.py b/tempest/tests/lib/services/compute/test_images_client.py new file mode 100644 index 0000000000..28757c39fa --- /dev/null +++ b/tempest/tests/lib/services/compute/test_images_client.py @@ -0,0 +1,265 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from oslotest import mockpatch + +from tempest.lib import exceptions as lib_exc +from tempest.lib.services.compute import images_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestImagesClient(base.BaseComputeServiceTest): + # Data Dictionaries used for testing # + FAKE_IMAGE_METADATA = { + "list": + {"metadata": { + "auto_disk_config": "True", + "Label": "Changed" + }}, + "set_item": + {"meta": { + "auto_disk_config": "True" + }}, + "show_item": + {"meta": { + "kernel_id": "nokernel", + }}, + "update": + {"metadata": { + "kernel_id": "False", + "Label": "UpdatedImage" + }}, + "set": + {"metadata": { + "Label": "Changed", + "auto_disk_config": "True" + }}, + "delete_item": {} + } + + FAKE_IMAGE_DATA = { + "list": + {"images": [ + {"id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + {"href": "http://openstack.example.com/v2/openstack" + + "/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + } + ], + "name": "fakeimage7" + }]}, + "show": {"image": { + "created": "2011-01-01T01:02:03Z", + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack" + + "/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + }, + ], + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage7", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z"}}, + "create": {}, + "delete": {} + } + func2mock = { + 'get': 'tempest.lib.common.rest_client.RestClient.get', + 'post': 'tempest.lib.common.rest_client.RestClient.post', + 'put': 'tempest.lib.common.rest_client.RestClient.put', + 'delete': 'tempest.lib.common.rest_client.RestClient.delete'} + # Variable definition + FAKE_IMAGE_ID = FAKE_IMAGE_DATA['show']['image']['id'] + FAKE_SERVER_ID = "80a599e0-31e7-49b7-b260-868f441e343f" + FAKE_CREATE_INFO = {'location': 'None'} + FAKE_METADATA = FAKE_IMAGE_METADATA['show_item']['meta'] + + def setUp(self): + super(TestImagesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = images_client.ImagesClient(fake_auth, + "compute", "regionOne") + + def _test_image_operation(self, operation="delete", bytes_body=False): + response_code = 200 + mock_operation = self.func2mock['get'] + expected_op = self.FAKE_IMAGE_DATA[operation] + params = {"image_id": self.FAKE_IMAGE_ID} + headers = None + if operation == 'list': + function = self.client.list_images + elif operation == 'show': + function = self.client.show_image + elif operation == 'create': + function = self.client.create_image + mock_operation = self.func2mock['post'] + params = {"server_id": self.FAKE_SERVER_ID} + response_code = 202 + headers = { + 'connection': 'keep-alive', + 'content-length': '0', + 'content-type': 'application/json', + 'status': '202', + 'x-compute-request-id': 'req-fake', + 'vary': 'accept-encoding', + 'x-openstack-nova-api-version': 'v2.1', + 'date': '13 Oct 2015 05:55:36 GMT', + 'location': 'http://fake.com/images/fake' + } + else: + function = self.client.delete_image + mock_operation = self.func2mock['delete'] + response_code = 204 + + self.check_service_client_function( + function, mock_operation, expected_op, + bytes_body, response_code, headers, **params) + + def _test_image_metadata(self, operation="set_item", bytes_body=False): + response_code = 200 + expected_op = self.FAKE_IMAGE_METADATA[operation] + if operation == 'list': + function = self.client.list_image_metadata + mock_operation = self.func2mock['get'] + params = {"image_id": self.FAKE_IMAGE_ID} + + elif operation == 'set': + function = self.client.set_image_metadata + mock_operation = self.func2mock['put'] + params = {"image_id": "_dummy_data", + "meta": self.FAKE_METADATA} + + elif operation == 'update': + function = self.client.update_image_metadata + mock_operation = self.func2mock['post'] + params = {"image_id": self.FAKE_IMAGE_ID, + "meta": self.FAKE_METADATA} + + elif operation == 'show_item': + mock_operation = self.func2mock['get'] + function = self.client.show_image_metadata_item + params = {"image_id": self.FAKE_IMAGE_ID, + "key": "123"} + + elif operation == 'delete_item': + function = self.client.delete_image_metadata_item + mock_operation = self.func2mock['delete'] + response_code = 204 + params = {"image_id": self.FAKE_IMAGE_ID, + "key": "123"} + + else: + function = self.client.set_image_metadata_item + mock_operation = self.func2mock['put'] + params = {"image_id": self.FAKE_IMAGE_ID, + "key": "123", + "meta": self.FAKE_METADATA} + + self.check_service_client_function( + function, mock_operation, expected_op, + bytes_body, response_code, **params) + + def _test_resource_deleted(self, bytes_body=False): + params = {"id": self.FAKE_IMAGE_ID} + expected_op = self.FAKE_IMAGE_DATA['show']['image'] + self.useFixture(mockpatch.Patch('tempest.lib.services.compute' + '.images_client.ImagesClient.show_image', + side_effect=lib_exc.NotFound)) + self.assertEqual(True, self.client.is_resource_deleted(**params)) + tempdata = copy.deepcopy(self.FAKE_IMAGE_DATA['show']) + tempdata['image']['id'] = None + self.useFixture(mockpatch.Patch('tempest.lib.services.compute' + '.images_client.ImagesClient.show_image', + return_value=expected_op)) + self.assertEqual(False, self.client.is_resource_deleted(**params)) + + def test_list_images_with_str_body(self): + self._test_image_operation('list') + + def test_list_images_with_bytes_body(self): + self._test_image_operation('list', True) + + def test_show_image_with_str_body(self): + self._test_image_operation('show') + + def test_show_image_with_bytes_body(self): + self._test_image_operation('show', True) + + def test_create_image_with_str_body(self): + self._test_image_operation('create') + + def test_create_image_with_bytes_body(self): + self._test_image_operation('create', True) + + def test_delete_image_with_str_body(self): + self._test_image_operation('delete') + + def test_delete_image_with_bytes_body(self): + self._test_image_operation('delete', True) + + def test_list_image_metadata_with_str_body(self): + self._test_image_metadata('list') + + def test_list_image_metadata_with_bytes_body(self): + self._test_image_metadata('list', True) + + def test_set_image_metadata_with_str_body(self): + self._test_image_metadata('set') + + def test_set_image_metadata_with_bytes_body(self): + self._test_image_metadata('set', True) + + def test_update_image_metadata_with_str_body(self): + self._test_image_metadata('update') + + def test_update_image_metadata_with_bytes_body(self): + self._test_image_metadata('update', True) + + def test_set_image_metadata_item_with_str_body(self): + self._test_image_metadata() + + def test_set_image_metadata_item_with_bytes_body(self): + self._test_image_metadata(bytes_body=True) + + def test_show_image_metadata_item_with_str_body(self): + self._test_image_metadata('show_item') + + def test_show_image_metadata_item_with_bytes_body(self): + self._test_image_metadata('show_item', True) + + def test_delete_image_metadata_item_with_str_body(self): + self._test_image_metadata('delete_item') + + def test_delete_image_metadata_item_with_bytes_body(self): + self._test_image_metadata('delete_item', True) + + def test_resource_delete_with_str_body(self): + self._test_resource_deleted() + + def test_resource_delete_with_bytes_body(self): + self._test_resource_deleted(True) diff --git a/tempest/tests/lib/services/compute/test_instance_usage_audit_log_client.py b/tempest/tests/lib/services/compute/test_instance_usage_audit_log_client.py new file mode 100644 index 0000000000..e8c22f1378 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_instance_usage_audit_log_client.py @@ -0,0 +1,73 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from tempest.lib.services.compute import instance_usage_audit_log_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestInstanceUsagesAuditLogClient(base.BaseComputeServiceTest): + + FAKE_AUDIT_LOG = { + "hosts_not_run": [ + "f4eb7cfd155f4574967f8b55a7faed75" + ], + "log": {}, + "num_hosts": 1, + "num_hosts_done": 0, + "num_hosts_not_run": 1, + "num_hosts_running": 0, + "overall_status": "0 of 1 hosts done. 0 errors.", + "period_beginning": "2012-12-01 00:00:00", + "period_ending": "2013-01-01 00:00:00", + "total_errors": 0, + "total_instances": 0 + } + + def setUp(self): + super(TestInstanceUsagesAuditLogClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = (instance_usage_audit_log_client. + InstanceUsagesAuditLogClient(fake_auth, 'compute', + 'regionOne')) + + def _test_list_instance_usage_audit_logs(self, bytes_body=False): + self.check_service_client_function( + self.client.list_instance_usage_audit_logs, + 'tempest.lib.common.rest_client.RestClient.get', + {"instance_usage_audit_logs": self.FAKE_AUDIT_LOG}, + bytes_body) + + def test_list_instance_usage_audit_logs_with_str_body(self): + self._test_list_instance_usage_audit_logs() + + def test_list_instance_usage_audit_logs_with_bytes_body(self): + self._test_list_instance_usage_audit_logs(bytes_body=True) + + def _test_show_instance_usage_audit_log(self, bytes_body=False): + before_time = datetime.datetime(2012, 12, 1, 0, 0) + self.check_service_client_function( + self.client.show_instance_usage_audit_log, + 'tempest.lib.common.rest_client.RestClient.get', + {"instance_usage_audit_log": self.FAKE_AUDIT_LOG}, + bytes_body, + time_before=before_time) + + def test_show_instance_usage_audit_log_with_str_body(self): + self._test_show_instance_usage_audit_log() + + def test_show_network_with_bytes_body_with_bytes_body(self): + self._test_show_instance_usage_audit_log(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_interfaces_client.py b/tempest/tests/lib/services/compute/test_interfaces_client.py new file mode 100644 index 0000000000..de8e26871a --- /dev/null +++ b/tempest/tests/lib/services/compute/test_interfaces_client.py @@ -0,0 +1,98 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import interfaces_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestInterfacesClient(base.BaseComputeServiceTest): + # Data Values to be used for testing # + FAKE_INTERFACE_DATA = { + "fixed_ips": [{ + "ip_address": "192.168.1.1", + "subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef" + }], + "mac_addr": "fa:16:3e:4c:2c:30", + "net_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6", + "port_id": "ce531f90-199f-48c0-816c-13e38010b442", + "port_state": "ACTIVE"} + + FAKE_SHOW_DATA = { + "interfaceAttachment": FAKE_INTERFACE_DATA} + FAKE_LIST_DATA = { + "interfaceAttachments": [FAKE_INTERFACE_DATA]} + + FAKE_SERVER_ID = "ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5" + FAKE_PORT_ID = FAKE_SHOW_DATA['interfaceAttachment']['port_id'] + func2mock = { + 'delete': 'tempest.lib.common.rest_client.RestClient.delete', + 'get': 'tempest.lib.common.rest_client.RestClient.get', + 'post': 'tempest.lib.common.rest_client.RestClient.post'} + + def setUp(self): + super(TestInterfacesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = interfaces_client.InterfacesClient(fake_auth, + "compute", + "regionOne") + + def _test_interface_operation(self, operation="create", bytes_body=False): + response_code = 200 + expected_op = self.FAKE_SHOW_DATA + mock_operation = self.func2mock['get'] + params = {'server_id': self.FAKE_SERVER_ID, + 'port_id': self.FAKE_PORT_ID} + if operation == 'list': + expected_op = self.FAKE_LIST_DATA + function = self.client.list_interfaces + params = {'server_id': self.FAKE_SERVER_ID} + elif operation == 'show': + function = self.client.show_interface + elif operation == 'delete': + expected_op = {} + mock_operation = self.func2mock['delete'] + function = self.client.delete_interface + response_code = 202 + else: + function = self.client.create_interface + mock_operation = self.func2mock['post'] + + self.check_service_client_function( + function, mock_operation, expected_op, + bytes_body, response_code, **params) + + def test_list_interfaces_with_str_body(self): + self._test_interface_operation('list') + + def test_list_interfaces_with_bytes_body(self): + self._test_interface_operation('list', True) + + def test_show_interface_with_str_body(self): + self._test_interface_operation('show') + + def test_show_interface_with_bytes_body(self): + self._test_interface_operation('show', True) + + def test_delete_interface_with_str_body(self): + self._test_interface_operation('delete') + + def test_delete_interface_with_bytes_body(self): + self._test_interface_operation('delete', True) + + def test_create_interface_with_str_body(self): + self._test_interface_operation() + + def test_create_interface_with_bytes_body(self): + self._test_interface_operation(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_keypairs_client.py b/tempest/tests/lib/services/compute/test_keypairs_client.py new file mode 100644 index 0000000000..7c595ca9fd --- /dev/null +++ b/tempest/tests/lib/services/compute/test_keypairs_client.py @@ -0,0 +1,94 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.services.compute import keypairs_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestKeyPairsClient(base.BaseComputeServiceTest): + + FAKE_KEYPAIR = {"keypair": { + "public_key": "ssh-rsa foo Generated-by-Nova", + "name": u'\u2740(*\xb4\u25e1`*)\u2740', + "user_id": "525d55f98980415ba98e634972fa4a10", + "fingerprint": "76:24:66:49:d7:ca:6e:5c:77:ea:8e:bb:9c:15:5f:98" + }} + + def setUp(self): + super(TestKeyPairsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = keypairs_client.KeyPairsClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_keypairs(self, bytes_body=False): + self.check_service_client_function( + self.client.list_keypairs, + 'tempest.lib.common.rest_client.RestClient.get', + {"keypairs": []}, + bytes_body) + + def test_list_keypairs_with_str_body(self): + self._test_list_keypairs() + + def test_list_keypairs_with_bytes_body(self): + self._test_list_keypairs(bytes_body=True) + + def _test_show_keypair(self, bytes_body=False): + fake_keypair = copy.deepcopy(self.FAKE_KEYPAIR) + fake_keypair["keypair"].update({ + "deleted": False, + "created_at": "2015-07-22T04:53:52.000000", + "updated_at": None, + "deleted_at": None, + "id": 1 + }) + + self.check_service_client_function( + self.client.show_keypair, + 'tempest.lib.common.rest_client.RestClient.get', + fake_keypair, + bytes_body, + keypair_name="test") + + def test_show_keypair_with_str_body(self): + self._test_show_keypair() + + def test_show_keypair_with_bytes_body(self): + self._test_show_keypair(bytes_body=True) + + def _test_create_keypair(self, bytes_body=False): + fake_keypair = copy.deepcopy(self.FAKE_KEYPAIR) + fake_keypair["keypair"].update({"private_key": "foo"}) + + self.check_service_client_function( + self.client.create_keypair, + 'tempest.lib.common.rest_client.RestClient.post', + fake_keypair, + bytes_body, + name="test") + + def test_create_keypair_with_str_body(self): + self._test_create_keypair() + + def test_create_keypair_with_bytes_body(self): + self._test_create_keypair(bytes_body=True) + + def test_delete_keypair(self): + self.check_service_client_function( + self.client.delete_keypair, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, status=202, keypair_name='test') diff --git a/tempest/tests/lib/services/compute/test_limits_client.py b/tempest/tests/lib/services/compute/test_limits_client.py new file mode 100644 index 0000000000..d3f0aee025 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_limits_client.py @@ -0,0 +1,66 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import limits_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestLimitsClient(base.BaseComputeServiceTest): + + def setUp(self): + super(TestLimitsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = limits_client.LimitsClient( + fake_auth, 'compute', 'regionOne') + + def _test_show_limits(self, bytes_body=False): + expected = { + "limits": { + "rate": [], + "absolute": { + "maxServerMeta": 128, + "maxPersonality": 5, + "totalServerGroupsUsed": 0, + "maxImageMeta": 128, + "maxPersonalitySize": 10240, + "maxServerGroups": 10, + "maxSecurityGroupRules": 20, + "maxTotalKeypairs": 100, + "totalCoresUsed": 0, + "totalRAMUsed": 0, + "totalInstancesUsed": 0, + "maxSecurityGroups": 10, + "totalFloatingIpsUsed": 0, + "maxTotalCores": 20, + "totalSecurityGroupsUsed": 0, + "maxTotalFloatingIps": 10, + "maxTotalInstances": 10, + "maxTotalRAMSize": 51200, + "maxServerGroupMembers": 10 + } + } + } + + self.check_service_client_function( + self.client.show_limits, + 'tempest.lib.common.rest_client.RestClient.get', + expected, + bytes_body) + + def test_show_limits_with_str_body(self): + self._test_show_limits() + + def test_show_limits_with_bytes_body(self): + self._test_show_limits(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_migrations_client.py b/tempest/tests/lib/services/compute/test_migrations_client.py new file mode 100644 index 0000000000..5b1578de29 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_migrations_client.py @@ -0,0 +1,52 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import migrations_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestMigrationsClient(base.BaseComputeServiceTest): + FAKE_MIGRATION_INFO = {"migrations": [{ + "created_at": "2012-10-29T13:42:02", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1234, + "instance_uuid": "e9e4fdd7-f956-44ff-bfeb-d654a96ab3a2", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "finished", + "updated_at": "2012-10-29T13:42:02"}]} + + def setUp(self): + super(TestMigrationsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.mg_client_obj = migrations_client.MigrationsClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_migrations(self, bytes_body=False): + self.check_service_client_function( + self.mg_client_obj.list_migrations, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_MIGRATION_INFO, + bytes_body) + + def test_list_migration_with_str_body(self): + self._test_list_migrations() + + def test_list_migration_with_bytes_body(self): + self._test_list_migrations(True) diff --git a/tempest/tests/lib/services/compute/test_networks_client.py b/tempest/tests/lib/services/compute/test_networks_client.py new file mode 100644 index 0000000000..4f5c8b93ef --- /dev/null +++ b/tempest/tests/lib/services/compute/test_networks_client.py @@ -0,0 +1,94 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import networks_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestNetworksClient(base.BaseComputeServiceTest): + + FAKE_NETWORK = { + "bridge": None, + "vpn_public_port": None, + "dhcp_start": None, + "bridge_interface": None, + "share_address": None, + "updated_at": None, + "id": "34d5ae1e-5659-49cf-af80-73bccd7d7ad3", + "cidr_v6": None, + "deleted_at": None, + "gateway": None, + "rxtx_base": None, + "label": u'30d7', + "priority": None, + "project_id": None, + "vpn_private_address": None, + "deleted": None, + "vlan": None, + "broadcast": None, + "netmask": None, + "injected": None, + "cidr": None, + "vpn_public_address": None, + "multi_host": None, + "enable_dhcp": None, + "dns2": None, + "created_at": None, + "host": None, + "mtu": None, + "gateway_v6": None, + "netmask_v6": None, + "dhcp_server": None, + "dns1": None + } + + network_id = "34d5ae1e-5659-49cf-af80-73bccd7d7ad3" + + FAKE_NETWORKS = [FAKE_NETWORK] + + def setUp(self): + super(TestNetworksClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = networks_client.NetworksClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_networks(self, bytes_body=False): + fake_list = {"networks": self.FAKE_NETWORKS} + self.check_service_client_function( + self.client.list_networks, + 'tempest.lib.common.rest_client.RestClient.get', + fake_list, + bytes_body) + + def test_list_networks_with_str_body(self): + self._test_list_networks() + + def test_list_networks_with_bytes_body(self): + self._test_list_networks(bytes_body=True) + + def _test_show_network(self, bytes_body=False): + self.check_service_client_function( + self.client.show_network, + 'tempest.lib.common.rest_client.RestClient.get', + {"network": self.FAKE_NETWORK}, + bytes_body, + network_id=self.network_id + ) + + def test_show_network_with_str_body(self): + self._test_show_network() + + def test_show_network_with_bytes_body(self): + self._test_show_network(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_quota_classes_client.py b/tempest/tests/lib/services/compute/test_quota_classes_client.py new file mode 100644 index 0000000000..4b675767d8 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_quota_classes_client.py @@ -0,0 +1,71 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.services.compute import quota_classes_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestQuotaClassesClient(base.BaseComputeServiceTest): + + FAKE_QUOTA_CLASS_SET = { + "injected_file_content_bytes": 10240, + "metadata_items": 128, + "server_group_members": 10, + "server_groups": 10, + "ram": 51200, + "floating_ips": 10, + "key_pairs": 100, + "id": u'\u2740(*\xb4\u25e1`*)\u2740', + "instances": 10, + "security_group_rules": 20, + "security_groups": 10, + "injected_files": 5, + "cores": 20, + "fixed_ips": -1, + "injected_file_path_bytes": 255, + } + + def setUp(self): + super(TestQuotaClassesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = quota_classes_client.QuotaClassesClient( + fake_auth, 'compute', 'regionOne') + + def _test_show_quota_class_set(self, bytes_body=False): + fake_body = {'quota_class_set': self.FAKE_QUOTA_CLASS_SET} + self.check_service_client_function( + self.client.show_quota_class_set, + 'tempest.lib.common.rest_client.RestClient.get', + fake_body, + bytes_body, + quota_class_id="test") + + def test_show_quota_class_set_with_str_body(self): + self._test_show_quota_class_set() + + def test_show_quota_class_set_with_bytes_body(self): + self._test_show_quota_class_set(bytes_body=True) + + def test_update_quota_class_set(self): + fake_quota_class_set = copy.deepcopy(self.FAKE_QUOTA_CLASS_SET) + fake_quota_class_set.pop("id") + fake_body = {'quota_class_set': fake_quota_class_set} + self.check_service_client_function( + self.client.update_quota_class_set, + 'tempest.lib.common.rest_client.RestClient.put', + fake_body, + quota_class_id="test") diff --git a/tempest/tests/lib/services/compute/test_quotas_client.py b/tempest/tests/lib/services/compute/test_quotas_client.py new file mode 100644 index 0000000000..9f5d1f60f2 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_quotas_client.py @@ -0,0 +1,130 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.services.compute import quotas_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestQuotasClient(base.BaseComputeServiceTest): + + FAKE_QUOTA_SET = { + "quota_set": { + "injected_file_content_bytes": 10240, + "metadata_items": 128, + "server_group_members": 10, + "server_groups": 10, + "ram": 51200, + "floating_ips": 10, + "key_pairs": 100, + "id": "8421f7be61064f50b680465c07f334af", + "instances": 10, + "security_group_rules": 20, + "injected_files": 5, + "cores": 20, + "fixed_ips": -1, + "injected_file_path_bytes": 255, + "security_groups": 10} + } + + project_id = "8421f7be61064f50b680465c07f334af" + fake_user_id = "65f09168cbb04eb593f3138b63b67b67" + + def setUp(self): + super(TestQuotasClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = quotas_client.QuotasClient( + fake_auth, 'compute', 'regionOne') + + def _test_show_quota_set(self, bytes_body=False, user_id=None): + if user_id: + self.check_service_client_function( + self.client.show_quota_set, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_QUOTA_SET, + to_utf=bytes_body, + tenant_id=self.project_id, + user_id=user_id) + else: + self.check_service_client_function( + self.client.show_quota_set, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_QUOTA_SET, + to_utf=bytes_body, + tenant_id=self.project_id) + + def test_show_quota_set_with_str_body(self): + self._test_show_quota_set() + + def test_show_quota_set_with_bytes_body(self): + self._test_show_quota_set(bytes_body=True) + + def test_show_quota_set_for_user_with_str_body(self): + self._test_show_quota_set(user_id=self.fake_user_id) + + def test_show_quota_set_for_user_with_bytes_body(self): + self._test_show_quota_set(bytes_body=True, user_id=self.fake_user_id) + + def _test_show_default_quota_set(self, bytes_body=False): + self.check_service_client_function( + self.client.show_default_quota_set, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_QUOTA_SET, + to_utf=bytes_body, + tenant_id=self.project_id) + + def test_show_default_quota_set_with_str_body(self): + self._test_show_default_quota_set() + + def test_show_default_quota_set_with_bytes_body(self): + self._test_show_default_quota_set(bytes_body=True) + + def _test_update_quota_set(self, bytes_body=False, user_id=None): + fake_quota_set = copy.deepcopy(self.FAKE_QUOTA_SET) + fake_quota_set['quota_set'].pop("id") + if user_id: + self.check_service_client_function( + self.client.update_quota_set, + 'tempest.lib.common.rest_client.RestClient.put', + fake_quota_set, + to_utf=bytes_body, + tenant_id=self.project_id, + user_id=user_id) + else: + self.check_service_client_function( + self.client.update_quota_set, + 'tempest.lib.common.rest_client.RestClient.put', + fake_quota_set, + to_utf=bytes_body, + tenant_id=self.project_id) + + def test_update_quota_set_with_str_body(self): + self._test_update_quota_set() + + def test_update_quota_set_with_bytes_body(self): + self._test_update_quota_set(bytes_body=True) + + def test_update_quota_set_for_user_with_str_body(self): + self._test_update_quota_set(user_id=self.fake_user_id) + + def test_update_quota_set_for_user_with_bytes_body(self): + self._test_update_quota_set(bytes_body=True, user_id=self.fake_user_id) + + def test_delete_quota_set(self): + self.check_service_client_function( + self.client.delete_quota_set, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, status=202, tenant_id=self.project_id) diff --git a/tempest/tests/lib/services/compute/test_security_group_default_rules_client.py b/tempest/tests/lib/services/compute/test_security_group_default_rules_client.py new file mode 100644 index 0000000000..581f7b1e8c --- /dev/null +++ b/tempest/tests/lib/services/compute/test_security_group_default_rules_client.py @@ -0,0 +1,88 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import security_group_default_rules_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestSecurityGroupDefaultRulesClient(base.BaseComputeServiceTest): + FAKE_RULE = { + "from_port": 80, + "id": 1, + "ip_protocol": "TCP", + "ip_range": { + "cidr": "10.10.10.0/24" + }, + "to_port": 80 + } + + def setUp(self): + super(TestSecurityGroupDefaultRulesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = (security_group_default_rules_client. + SecurityGroupDefaultRulesClient(fake_auth, 'compute', + 'regionOne')) + + def _test_list_security_group_default_rules(self, bytes_body=False): + self.check_service_client_function( + self.client.list_security_group_default_rules, + 'tempest.lib.common.rest_client.RestClient.get', + {"security_group_default_rules": [self.FAKE_RULE]}, + to_utf=bytes_body) + + def test_list_security_group_default_rules_with_str_body(self): + self._test_list_security_group_default_rules() + + def test_list_security_group_default_rules_with_bytes_body(self): + self._test_list_security_group_default_rules(bytes_body=True) + + def _test_show_security_group_default_rule(self, bytes_body=False): + self.check_service_client_function( + self.client.show_security_group_default_rule, + 'tempest.lib.common.rest_client.RestClient.get', + {"security_group_default_rule": self.FAKE_RULE}, + to_utf=bytes_body, + security_group_default_rule_id=1) + + def test_show_security_group_default_rule_with_str_body(self): + self._test_show_security_group_default_rule() + + def test_show_security_group_default_rule_with_bytes_body(self): + self._test_show_security_group_default_rule(bytes_body=True) + + def _test_create_security_default_group_rule(self, bytes_body=False): + request_body = { + "to_port": 80, + "from_port": 80, + "ip_protocol": "TCP", + "cidr": "10.10.10.0/24" + } + self.check_service_client_function( + self.client.create_security_default_group_rule, + 'tempest.lib.common.rest_client.RestClient.post', + {"security_group_default_rule": self.FAKE_RULE}, + to_utf=bytes_body, **request_body) + + def test_create_security_default_group_rule_with_str_body(self): + self._test_create_security_default_group_rule() + + def test_create_security_default_group_rule_with_bytes_body(self): + self._test_create_security_default_group_rule(bytes_body=True) + + def test_delete_security_group_default_rule(self): + self.check_service_client_function( + self.client.delete_security_group_default_rule, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, status=204, security_group_default_rule_id=1) diff --git a/tempest/tests/lib/services/compute/test_security_group_rules_client.py b/tempest/tests/lib/services/compute/test_security_group_rules_client.py new file mode 100644 index 0000000000..9a7c04d935 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_security_group_rules_client.py @@ -0,0 +1,66 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import security_group_rules_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestSecurityGroupRulesClient(base.BaseComputeServiceTest): + + FAKE_SECURITY_GROUP_RULE = { + "security_group_rule": { + "id": "2d021cf1-ce4b-4292-994f-7a785d62a144", + "ip_range": { + "cidr": "0.0.0.0/0" + }, + "parent_group_id": "48700ff3-30b8-4e63-845f-a79c9633e9fb", + "to_port": 443, + "ip_protocol": "tcp", + "group": {}, + "from_port": 443 + } + } + + def setUp(self): + super(TestSecurityGroupRulesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = security_group_rules_client.SecurityGroupRulesClient( + fake_auth, 'compute', 'regionOne') + + def _test_create_security_group_rule(self, bytes_body=False): + req_body = { + "from_port": "443", + "ip_protocol": "tcp", + "to_port": "443", + "cidr": "0.0.0.0/0", + "parent_group_id": "48700ff3-30b8-4e63-845f-a79c9633e9fb" + } + self.check_service_client_function( + self.client.create_security_group_rule, + 'tempest.lib.common.rest_client.RestClient.post', + self.FAKE_SECURITY_GROUP_RULE, + to_utf=bytes_body, **req_body) + + def test_create_security_group_rule_with_str_body(self): + self._test_create_security_group_rule() + + def test_create_security_group_rule_with_bytes_body(self): + self._test_create_security_group_rule(bytes_body=True) + + def test_delete_security_group_rule(self): + self.check_service_client_function( + self.client.delete_security_group_rule, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, status=202, group_rule_id='group-id') diff --git a/tempest/tests/lib/services/compute/test_security_groups_client.py b/tempest/tests/lib/services/compute/test_security_groups_client.py new file mode 100644 index 0000000000..6a11c29c83 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_security_groups_client.py @@ -0,0 +1,113 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import mockpatch + +from tempest.lib import exceptions as lib_exc +from tempest.lib.services.compute import security_groups_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestSecurityGroupsClient(base.BaseComputeServiceTest): + + FAKE_SECURITY_GROUP_INFO = [{ + "description": "default", + "id": "3fb26eb3-581b-4420-9963-b0879a026506", + "name": "default", + "rules": [], + "tenant_id": "openstack" + }] + + def setUp(self): + super(TestSecurityGroupsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = security_groups_client.SecurityGroupsClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_security_groups(self, bytes_body=False): + self.check_service_client_function( + self.client.list_security_groups, + 'tempest.lib.common.rest_client.RestClient.get', + {"security_groups": self.FAKE_SECURITY_GROUP_INFO}, + to_utf=bytes_body) + + def test_list_security_groups_with_str_body(self): + self._test_list_security_groups() + + def test_list_security_groups_with_bytes_body(self): + self._test_list_security_groups(bytes_body=True) + + def _test_show_security_group(self, bytes_body=False): + self.check_service_client_function( + self.client.show_security_group, + 'tempest.lib.common.rest_client.RestClient.get', + {"security_group": self.FAKE_SECURITY_GROUP_INFO[0]}, + to_utf=bytes_body, + security_group_id='fake-id') + + def test_show_security_group_with_str_body(self): + self._test_show_security_group() + + def test_show_security_group_with_bytes_body(self): + self._test_show_security_group(bytes_body=True) + + def _test_create_security_group(self, bytes_body=False): + post_body = {"name": "test", "description": "test_group"} + self.check_service_client_function( + self.client.create_security_group, + 'tempest.lib.common.rest_client.RestClient.post', + {"security_group": self.FAKE_SECURITY_GROUP_INFO[0]}, + to_utf=bytes_body, + kwargs=post_body) + + def test_create_security_group_with_str_body(self): + self._test_create_security_group() + + def test_create_security_group_with_bytes_body(self): + self._test_create_security_group(bytes_body=True) + + def _test_update_security_group(self, bytes_body=False): + req_body = {"name": "test", "description": "test_group"} + self.check_service_client_function( + self.client.update_security_group, + 'tempest.lib.common.rest_client.RestClient.put', + {"security_group": self.FAKE_SECURITY_GROUP_INFO[0]}, + to_utf=bytes_body, + security_group_id='fake-id', + kwargs=req_body) + + def test_update_security_group_with_str_body(self): + self._test_update_security_group() + + def test_update_security_group_with_bytes_body(self): + self._test_update_security_group(bytes_body=True) + + def test_delete_security_group(self): + self.check_service_client_function( + self.client.delete_security_group, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, status=202, security_group_id='fake-id') + + def test_is_resource_deleted_true(self): + mod = ('tempest.lib.services.compute.security_groups_client.' + 'SecurityGroupsClient.show_security_group') + self.useFixture(mockpatch.Patch(mod, side_effect=lib_exc.NotFound)) + self.assertTrue(self.client.is_resource_deleted('fake-id')) + + def test_is_resource_deleted_false(self): + mod = ('tempest.lib.services.compute.security_groups_client.' + 'SecurityGroupsClient.show_security_group') + self.useFixture(mockpatch.Patch(mod, return_value='success')) + self.assertFalse(self.client.is_resource_deleted('fake-id')) diff --git a/tempest/tests/lib/services/compute/test_server_groups_client.py b/tempest/tests/lib/services/compute/test_server_groups_client.py new file mode 100644 index 0000000000..f1f29067ba --- /dev/null +++ b/tempest/tests/lib/services/compute/test_server_groups_client.py @@ -0,0 +1,84 @@ +# Copyright 2015 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import httplib2 + +from oslotest import mockpatch +from tempest.tests.lib import fake_auth_provider + +from tempest.lib.services.compute import server_groups_client +from tempest.tests.lib.services.compute import base + + +class TestServerGroupsClient(base.BaseComputeServiceTest): + + server_group = { + "id": "5bbcc3c4-1da2-4437-a48a-66f15b1b13f9", + "name": "test", + "policies": ["anti-affinity"], + "members": [], + "metadata": {}} + + def setUp(self): + super(TestServerGroupsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = server_groups_client.ServerGroupsClient( + fake_auth, 'compute', 'regionOne') + + def _test_create_server_group(self, bytes_body=False): + expected = {"server_group": TestServerGroupsClient.server_group} + self.check_service_client_function( + self.client.create_server_group, + 'tempest.lib.common.rest_client.RestClient.post', expected, + bytes_body, name='fake-group', policies=['affinity']) + + def test_create_server_group_str_body(self): + self._test_create_server_group(bytes_body=False) + + def test_create_server_group_byte_body(self): + self._test_create_server_group(bytes_body=True) + + def test_delete_server_group(self): + response = (httplib2.Response({'status': 204}), None) + self.useFixture(mockpatch.Patch( + 'tempest.lib.common.rest_client.RestClient.delete', + return_value=response)) + self.client.delete_server_group('fake-group') + + def _test_list_server_groups(self, bytes_body=False): + expected = {"server_groups": [TestServerGroupsClient.server_group]} + self.check_service_client_function( + self.client.list_server_groups, + 'tempest.lib.common.rest_client.RestClient.get', + expected, bytes_body) + + def test_list_server_groups_str_body(self): + self._test_list_server_groups(bytes_body=False) + + def test_list_server_groups_byte_body(self): + self._test_list_server_groups(bytes_body=True) + + def _test_show_server_group(self, bytes_body=False): + expected = {"server_group": TestServerGroupsClient.server_group} + self.check_service_client_function( + self.client.show_server_group, + 'tempest.lib.common.rest_client.RestClient.get', + expected, bytes_body, + server_group_id='5bbcc3c4-1da2-4437-a48a-66f15b1b13f9') + + def test_show_server_group_str_body(self): + self._test_show_server_group(bytes_body=False) + + def test_show_server_group_byte_body(self): + self._test_show_server_group(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_servers_client.py b/tempest/tests/lib/services/compute/test_servers_client.py new file mode 100644 index 0000000000..007849739d --- /dev/null +++ b/tempest/tests/lib/services/compute/test_servers_client.py @@ -0,0 +1,1011 @@ +# Copyright 2015 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.services.compute import servers_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestServersClient(base.BaseComputeServiceTest): + + FAKE_SERVERS = {'servers': [{ + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "links": [ + { + "href": "http://os.co/v2/616fb98f-46ca-475e-917e-2563e5a8cd19", + "rel": "self" + }, + { + "href": "http://os.co/616fb98f-46ca-475e-917e-2563e5a8cd19", + "rel": "bookmark" + } + ], + "name": u"new\u1234-server-test"}] + } + + FAKE_SERVER_DIAGNOSTICS = { + "cpu0_time": 17300000000, + "memory": 524288, + "vda_errors": -1, + "vda_read": 262144, + "vda_read_req": 112, + "vda_write": 5778432, + "vda_write_req": 488, + "vnet1_rx": 2070139, + "vnet1_rx_drop": 0, + "vnet1_rx_errors": 0, + "vnet1_rx_packets": 26701, + "vnet1_tx": 140208, + "vnet1_tx_drop": 0, + "vnet1_tx_errors": 0, + "vnet1_tx_packets": 662 + } + + FAKE_SERVER_GET = {'server': { + "accessIPv4": "", + "accessIPv6": "", + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "version": 4 + } + ] + }, + "created": "2012-08-20T21:11:09Z", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://os.com/openstack/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "65201c14a29663e06d0748e561207d998b343e1d164bfa0aafa9c45d", + "id": "893c7791-f1df-4c3d-8383-3caae9656c62", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://imgs/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://v2/srvs/893c7791-f1df-4c3d-8383-3caae9656c62", + "rel": "self" + }, + { + "href": "http://srvs/893c7791-f1df-4c3d-8383-3caae9656c62", + "rel": "bookmark" + } + ], + "metadata": { + u"My Server N\u1234me": u"Apa\u1234che1" + }, + "name": u"new\u1234-server-test", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "openstack", + "updated": "2012-08-20T21:11:09Z", + "user_id": "fake"} + } + + FAKE_SERVER_POST = {"server": { + "id": "616fb98f-46ca-475e-917e-2563e5a8cd19", + "adminPass": "fake-admin-pass", + "security_groups": [ + 'fake-security-group-1', + 'fake-security-group-2' + ], + "links": [ + { + "href": "http://os.co/v2/616fb98f-46ca-475e-917e-2563e5a8cd19", + "rel": "self" + }, + { + "href": "http://os.co/616fb98f-46ca-475e-917e-2563e5a8cd19", + "rel": "bookmark" + } + ], + "OS-DCF:diskConfig": "fake-disk-config"} + } + + FAKE_ADDRESS = {"addresses": { + "private": [ + { + "addr": "192.168.0.3", + "version": 4 + } + ]} + } + + FAKE_COMMON_VOLUME = { + "id": "a6b0875b-6b5d-4a5a-81eb-0c3aa62e5fdb", + "device": "fake-device", + "volumeId": "a6b0875b-46ca-475e-917e-0c3aa62e5fdb", + "serverId": "616fb98f-46ca-475e-917e-2563e5a8cd19" + } + + FAKE_VIRTUAL_INTERFACES = { + "id": "a6b0875b-46ca-475e-917e-0c3aa62e5fdb", + "mac_address": "00:25:90:5b:f8:c3", + "OS-EXT-VIF-NET:net_id": "fake-os-net-id" + } + + FAKE_INSTANCE_ACTIONS = { + "action": "fake-action", + "request_id": "16fb98f-46ca-475e-917e-2563e5a8cd19", + "user_id": "16fb98f-46ca-475e-917e-2563e5a8cd12", + "project_id": "16fb98f-46ca-475e-917e-2563e5a8cd34", + "start_time": "09MAR2015 11:15", + "message": "fake-msg", + "instance_uuid": "16fb98f-46ca-475e-917e-2563e5a8cd12" + } + + FAKE_VNC_CONSOLE = { + "type": "fake-type", + "url": "http://os.co/v2/616fb98f-46ca-475e-917e-2563e5a8cd19" + } + + FAKE_INSTANCE_ACTION_EVENTS = { + "event": "fake-event", + "start_time": "09MAR2015 11:15", + "finish_time": "09MAR2015 11:15", + "result": "fake-result", + "traceback": "fake-trace-back" + } + + FAKE_INSTANCE_WITH_EVENTS = copy.deepcopy(FAKE_INSTANCE_ACTIONS) + FAKE_INSTANCE_WITH_EVENTS['events'] = [FAKE_INSTANCE_ACTION_EVENTS] + + FAKE_REBUILD_SERVER = copy.deepcopy(FAKE_SERVER_GET) + FAKE_REBUILD_SERVER['server']['adminPass'] = 'fake-admin-pass' + + server_id = FAKE_SERVER_GET['server']['id'] + network_id = 'a6b0875b-6b5d-4a5a-81eb-0c3aa62e5fdb' + + def setUp(self): + super(TestServersClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = servers_client.ServersClient( + fake_auth, 'compute', 'regionOne') + + def test_list_servers_with_str_body(self): + self._test_list_servers() + + def test_list_servers_with_bytes_body(self): + self._test_list_servers(bytes_body=True) + + def _test_list_servers(self, bytes_body=False): + self.check_service_client_function( + self.client.list_servers, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_SERVERS, + bytes_body) + + def test_show_server_with_str_body(self): + self._test_show_server() + + def test_show_server_with_bytes_body(self): + self._test_show_server(bytes_body=True) + + def _test_show_server(self, bytes_body=False): + self.check_service_client_function( + self.client.show_server, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_SERVER_GET, + bytes_body, + server_id=self.server_id + ) + + def test_delete_server(self, bytes_body=False): + self.check_service_client_function( + self.client.delete_server, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, + status=204, + server_id=self.server_id + ) + + def test_create_server_with_str_body(self): + self._test_create_server() + + def test_create_server_with_bytes_body(self): + self._test_create_server(True) + + def _test_create_server(self, bytes_body=False): + self.check_service_client_function( + self.client.create_server, + 'tempest.lib.common.rest_client.RestClient.post', + self.FAKE_SERVER_POST, + bytes_body, + status=202, + name='fake-name', + imageRef='fake-image-ref', + flavorRef='fake-flavor-ref' + ) + + def test_list_addresses_with_str_body(self): + self._test_list_addresses() + + def test_list_addresses_with_bytes_body(self): + self._test_list_addresses(True) + + def _test_list_addresses(self, bytes_body=False): + self.check_service_client_function( + self.client.list_addresses, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_ADDRESS, + bytes_body, + server_id=self.server_id + ) + + def test_list_addresses_by_network_with_str_body(self): + self._test_list_addresses_by_network() + + def test_list_addresses_by_network_with_bytes_body(self): + self._test_list_addresses_by_network(True) + + def _test_list_addresses_by_network(self, bytes_body=False): + self.check_service_client_function( + self.client.list_addresses_by_network, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_ADDRESS['addresses'], + server_id=self.server_id, + network_id=self.network_id + ) + + def test_action_with_str_body(self): + self._test_action() + + def test_action_with_bytes_body(self): + self._test_action(True) + + def _test_action(self, bytes_body=False): + self.check_service_client_function( + self.client.action, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + server_id=self.server_id, + action_name='fake-action-name', + schema={'status_code': 200} + ) + + def test_create_backup_with_str_body(self): + self._test_create_backup() + + def test_create_backup_with_bytes_body(self): + self._test_create_backup(True) + + def _test_create_backup(self, bytes_body=False): + self.check_service_client_function( + self.client.create_backup, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id, + backup_type='fake-backup', + rotation='fake-rotation', + name='fake-name' + ) + + def test_change_password_with_str_body(self): + self._test_change_password() + + def test_change_password_with_bytes_body(self): + self._test_change_password(True) + + def _test_change_password(self, bytes_body=False): + self.check_service_client_function( + self.client.change_password, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id, + adminPass='fake-admin-pass' + ) + + def test_show_password_with_str_body(self): + self._test_show_password() + + def test_show_password_with_bytes_body(self): + self._test_show_password(True) + + def _test_show_password(self, bytes_body=False): + self.check_service_client_function( + self.client.show_password, + 'tempest.lib.common.rest_client.RestClient.get', + {'password': 'fake-password'}, + server_id=self.server_id + ) + + def test_delete_password_with_str_body(self): + self._test_delete_password() + + def test_delete_password_with_bytes_body(self): + self._test_delete_password(True) + + def _test_delete_password(self, bytes_body=False): + self.check_service_client_function( + self.client.delete_password, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, + status=204, + server_id=self.server_id + ) + + def test_reboot_server_with_str_body(self): + self._test_reboot_server() + + def test_reboot_server_with_bytes_body(self): + self._test_reboot_server(True) + + def _test_reboot_server(self, bytes_body=False): + self.check_service_client_function( + self.client.reboot_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id, + type='fake-reboot-type' + ) + + def test_rebuild_server_with_str_body(self): + self._test_rebuild_server() + + def test_rebuild_server_with_bytes_body(self): + self._test_rebuild_server(True) + + def _test_rebuild_server(self, bytes_body=False): + self.check_service_client_function( + self.client.rebuild_server, + 'tempest.lib.common.rest_client.RestClient.post', + self.FAKE_REBUILD_SERVER, + status=202, + server_id=self.server_id, + image_ref='fake-image-ref' + ) + + def test_resize_server_with_str_body(self): + self._test_resize_server() + + def test_resize_server_with_bytes_body(self): + self._test_resize_server(True) + + def _test_resize_server(self, bytes_body=False): + self.check_service_client_function( + self.client.resize_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id, + flavor_ref='fake-flavor-ref' + ) + + def test_confirm_resize_server_with_str_body(self): + self._test_confirm_resize_server() + + def test_confirm_resize_server_with_bytes_body(self): + self._test_confirm_resize_server(True) + + def _test_confirm_resize_server(self, bytes_body=False): + self.check_service_client_function( + self.client.confirm_resize_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=204, + server_id=self.server_id + ) + + def test_revert_resize_server_with_str_body(self): + self._test_revert_resize() + + def test_revert_resize_server_with_bytes_body(self): + self._test_revert_resize(True) + + def _test_revert_resize(self, bytes_body=False): + self.check_service_client_function( + self.client.revert_resize_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_list_server_metadata_with_str_body(self): + self._test_list_server_metadata() + + def test_list_server_metadata_with_bytes_body(self): + self._test_list_server_metadata() + + def _test_list_server_metadata(self, bytes_body=False): + self.check_service_client_function( + self.client.list_server_metadata, + 'tempest.lib.common.rest_client.RestClient.get', + {'metadata': {'fake-key': 'fake-meta-data'}}, + server_id=self.server_id + ) + + def test_set_server_metadata_with_str_body(self): + self._test_set_server_metadata() + + def test_set_server_metadata_with_bytes_body(self): + self._test_set_server_metadata(True) + + def _test_set_server_metadata(self, bytes_body=False): + self.check_service_client_function( + self.client.set_server_metadata, + 'tempest.lib.common.rest_client.RestClient.put', + {'metadata': {'fake-key': 'fake-meta-data'}}, + server_id=self.server_id, + meta='fake-meta' + ) + + def test_update_server_metadata_with_str_body(self): + self._test_update_server_metadata() + + def test_update_server_metadata_with_bytes_body(self): + self._test_update_server_metadata(True) + + def _test_update_server_metadata(self, bytes_body=False): + self.check_service_client_function( + self.client.update_server_metadata, + 'tempest.lib.common.rest_client.RestClient.post', + {'metadata': {'fake-key': 'fake-meta-data'}}, + server_id=self.server_id, + meta='fake-meta' + ) + + def test_show_server_metadata_item_with_str_body(self): + self._test_show_server_metadata() + + def test_show_server_metadata_item_with_bytes_body(self): + self._test_show_server_metadata(True) + + def _test_show_server_metadata(self, bytes_body=False): + self.check_service_client_function( + self.client.show_server_metadata_item, + 'tempest.lib.common.rest_client.RestClient.get', + {'meta': {'fake-key': 'fake-meta-data'}}, + server_id=self.server_id, + key='fake-key' + ) + + def test_set_server_metadata_item_with_str_body(self): + self._test_set_server_metadata_item() + + def test_set_server_metadata_item_with_bytes_body(self): + self._test_set_server_metadata_item(True) + + def _test_set_server_metadata_item(self, bytes_body=False): + self.check_service_client_function( + self.client.set_server_metadata_item, + 'tempest.lib.common.rest_client.RestClient.put', + {'meta': {'fake-key': 'fake-meta-data'}}, + server_id=self.server_id, + key='fake-key', + meta='fake-meta' + ) + + def test_delete_server_metadata_item_with_str_body(self): + self._test_delete_server_metadata() + + def test_delete_server_metadata_item_with_bytes_body(self): + self._test_delete_server_metadata(True) + + def _test_delete_server_metadata(self, bytes_body=False): + self.check_service_client_function( + self.client.delete_server_metadata_item, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, + status=204, + server_id=self.server_id, + key='fake-key' + ) + + def test_stop_server_with_str_body(self): + self._test_stop_server() + + def test_stop_server_with_bytes_body(self): + self._test_stop_server(True) + + def _test_stop_server(self, bytes_body=False): + self.check_service_client_function( + self.client.stop_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_start_server_with_str_body(self): + self._test_start_server() + + def test_start_server_with_bytes_body(self): + self._test_start_server(True) + + def _test_start_server(self, bytes_body=False): + self.check_service_client_function( + self.client.start_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_attach_volume_with_str_body(self): + self._test_attach_volume_server() + + def test_attach_volume_with_bytes_body(self): + self._test_attach_volume_server(True) + + def _test_attach_volume_server(self, bytes_body=False): + self.check_service_client_function( + self.client.attach_volume, + 'tempest.lib.common.rest_client.RestClient.post', + {'volumeAttachment': self.FAKE_COMMON_VOLUME}, + server_id=self.server_id + ) + + def test_update_attached_volume(self): + self.check_service_client_function( + self.client.update_attached_volume, + 'tempest.lib.common.rest_client.RestClient.put', + {}, + status=202, + server_id=self.server_id, + attachment_id='fake-attachment-id', + volumeId='fake-volume-id' + ) + + def test_detach_volume_with_str_body(self): + self._test_detach_volume_server() + + def test_detach_volume_with_bytes_body(self): + self._test_detach_volume_server(True) + + def _test_detach_volume_server(self, bytes_body=False): + self.check_service_client_function( + self.client.detach_volume, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, + status=202, + server_id=self.server_id, + volume_id=self.FAKE_COMMON_VOLUME['volumeId'] + ) + + def test_show_volume_attachment_with_str_body(self): + self._test_show_volume_attachment() + + def test_show_volume_attachment_with_bytes_body(self): + self._test_show_volume_attachment(True) + + def _test_show_volume_attachment(self, bytes_body=False): + self.check_service_client_function( + self.client.show_volume_attachment, + 'tempest.lib.common.rest_client.RestClient.get', + {'volumeAttachment': self.FAKE_COMMON_VOLUME}, + server_id=self.server_id, + volume_id=self.FAKE_COMMON_VOLUME['volumeId'] + ) + + def test_list_volume_attachments_with_str_body(self): + self._test_list_volume_attachments() + + def test_list_volume_attachments_with_bytes_body(self): + self._test_list_volume_attachments(True) + + def _test_list_volume_attachments(self, bytes_body=False): + self.check_service_client_function( + self.client.list_volume_attachments, + 'tempest.lib.common.rest_client.RestClient.get', + {'volumeAttachments': [self.FAKE_COMMON_VOLUME]}, + server_id=self.server_id + ) + + def test_add_security_group_with_str_body(self): + self._test_add_security_group() + + def test_add_security_group_with_bytes_body(self): + self._test_add_security_group(True) + + def _test_add_security_group(self, bytes_body=False): + self.check_service_client_function( + self.client.add_security_group, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id, + name='fake-name' + ) + + def test_remove_security_group_with_str_body(self): + self._test_remove_security_group() + + def test_remove_security_group_with_bytes_body(self): + self._test_remove_security_group(True) + + def _test_remove_security_group(self, bytes_body=False): + self.check_service_client_function( + self.client.remove_security_group, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id, + name='fake-name' + ) + + def test_live_migrate_server_with_str_body(self): + self._test_live_migrate_server() + + def test_live_migrate_server_with_bytes_body(self): + self._test_live_migrate_server(True) + + def _test_live_migrate_server(self, bytes_body=False): + self.check_service_client_function( + self.client.live_migrate_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_migrate_server_with_str_body(self): + self._test_migrate_server() + + def test_migrate_server_with_bytes_body(self): + self._test_migrate_server(True) + + def _test_migrate_server(self, bytes_body=False): + self.check_service_client_function( + self.client.migrate_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_lock_server_with_str_body(self): + self._test_lock_server() + + def test_lock_server_with_bytes_body(self): + self._test_lock_server(True) + + def _test_lock_server(self, bytes_body=False): + self.check_service_client_function( + self.client.lock_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_unlock_server_with_str_body(self): + self._test_unlock_server() + + def test_unlock_server_with_bytes_body(self): + self._test_unlock_server(True) + + def _test_unlock_server(self, bytes_body=False): + self.check_service_client_function( + self.client.unlock_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_suspend_server_with_str_body(self): + self._test_suspend_server() + + def test_suspend_server_with_bytes_body(self): + self._test_suspend_server(True) + + def _test_suspend_server(self, bytes_body=False): + self.check_service_client_function( + self.client.suspend_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_resume_server_with_str_body(self): + self._test_resume_server() + + def test_resume_server_with_bytes_body(self): + self._test_resume_server(True) + + def _test_resume_server(self, bytes_body=False): + self.check_service_client_function( + self.client.resume_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_pause_server_with_str_body(self): + self._test_pause_server() + + def test_pause_server_with_bytes_body(self): + self._test_pause_server(True) + + def _test_pause_server(self, bytes_body=False): + self.check_service_client_function( + self.client.pause_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_unpause_server_with_str_body(self): + self._test_unpause_server() + + def test_unpause_server_with_bytes_body(self): + self._test_unpause_server(True) + + def _test_unpause_server(self, bytes_body=False): + self.check_service_client_function( + self.client.unpause_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_reset_state_with_str_body(self): + self._test_reset_state() + + def test_reset_state_with_bytes_body(self): + self._test_reset_state(True) + + def _test_reset_state(self, bytes_body=False): + self.check_service_client_function( + self.client.reset_state, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id, + state='fake-state' + ) + + def test_shelve_server_with_str_body(self): + self._test_shelve_server() + + def test_shelve_server_with_bytes_body(self): + self._test_shelve_server(True) + + def _test_shelve_server(self, bytes_body=False): + self.check_service_client_function( + self.client.shelve_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_unshelve_server_with_str_body(self): + self._test_unshelve_server() + + def test_unshelve_server_with_bytes_body(self): + self._test_unshelve_server(True) + + def _test_unshelve_server(self, bytes_body=False): + self.check_service_client_function( + self.client.unshelve_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_shelve_offload_server_with_str_body(self): + self._test_shelve_offload_server() + + def test_shelve_offload_server_with_bytes_body(self): + self._test_shelve_offload_server(True) + + def _test_shelve_offload_server(self, bytes_body=False): + self.check_service_client_function( + self.client.shelve_offload_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_get_console_output_with_str_body(self): + self._test_get_console_output() + + def test_get_console_output_with_bytes_body(self): + self._test_get_console_output(True) + + def _test_get_console_output(self, bytes_body=False): + self.check_service_client_function( + self.client.get_console_output, + 'tempest.lib.common.rest_client.RestClient.post', + {'output': 'fake-output'}, + server_id=self.server_id, + length='fake-length' + ) + + def test_list_virtual_interfaces_with_str_body(self): + self._test_list_virtual_interfaces() + + def test_list_virtual_interfaces_with_bytes_body(self): + self._test_list_virtual_interfaces(True) + + def _test_list_virtual_interfaces(self, bytes_body=False): + self.check_service_client_function( + self.client.list_virtual_interfaces, + 'tempest.lib.common.rest_client.RestClient.get', + {'virtual_interfaces': [self.FAKE_VIRTUAL_INTERFACES]}, + server_id=self.server_id + ) + + def test_rescue_server_with_str_body(self): + self._test_rescue_server() + + def test_rescue_server_with_bytes_body(self): + self._test_rescue_server(True) + + def _test_rescue_server(self, bytes_body=False): + self.check_service_client_function( + self.client.rescue_server, + 'tempest.lib.common.rest_client.RestClient.post', + {'adminPass': 'fake-admin-pass'}, + server_id=self.server_id + ) + + def test_unrescue_server_with_str_body(self): + self._test_unrescue_server() + + def test_unrescue_server_with_bytes_body(self): + self._test_unrescue_server(True) + + def _test_unrescue_server(self, bytes_body=False): + self.check_service_client_function( + self.client.unrescue_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_show_server_diagnostics_with_str_body(self): + self._test_show_server_diagnostics() + + def test_show_server_diagnostics_with_bytes_body(self): + self._test_show_server_diagnostics(True) + + def _test_show_server_diagnostics(self, bytes_body=False): + self.check_service_client_function( + self.client.show_server_diagnostics, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_SERVER_DIAGNOSTICS, + status=200, + server_id=self.server_id + ) + + def test_list_instance_actions_with_str_body(self): + self._test_list_instance_actions() + + def test_list_instance_actions_with_bytes_body(self): + self._test_list_instance_actions(True) + + def _test_list_instance_actions(self, bytes_body=False): + self.check_service_client_function( + self.client.list_instance_actions, + 'tempest.lib.common.rest_client.RestClient.get', + {'instanceActions': [self.FAKE_INSTANCE_ACTIONS]}, + server_id=self.server_id + ) + + def test_show_instance_action_with_str_body(self): + self._test_show_instance_action() + + def test_show_instance_action_with_bytes_body(self): + self._test_show_instance_action(True) + + def _test_show_instance_action(self, bytes_body=False): + self.check_service_client_function( + self.client.show_instance_action, + 'tempest.lib.common.rest_client.RestClient.get', + {'instanceAction': self.FAKE_INSTANCE_WITH_EVENTS}, + server_id=self.server_id, + request_id='fake-request-id' + ) + + def test_force_delete_server_with_str_body(self): + self._test_force_delete_server() + + def test_force_delete_server_with_bytes_body(self): + self._test_force_delete_server(True) + + def _test_force_delete_server(self, bytes_body=False): + self.check_service_client_function( + self.client.force_delete_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_restore_soft_deleted_server_with_str_body(self): + self._test_restore_soft_deleted_server() + + def test_restore_soft_deleted_server_with_bytes_body(self): + self._test_restore_soft_deleted_server(True) + + def _test_restore_soft_deleted_server(self, bytes_body=False): + self.check_service_client_function( + self.client.restore_soft_deleted_server, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_reset_network_with_str_body(self): + self._test_reset_network() + + def test_reset_network_with_bytes_body(self): + self._test_reset_network(True) + + def _test_reset_network(self, bytes_body=False): + self.check_service_client_function( + self.client.reset_network, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_inject_network_info_with_str_body(self): + self._test_inject_network_info() + + def test_inject_network_info_with_bytes_body(self): + self._test_inject_network_info(True) + + def _test_inject_network_info(self, bytes_body=False): + self.check_service_client_function( + self.client.inject_network_info, + 'tempest.lib.common.rest_client.RestClient.post', + {}, + status=202, + server_id=self.server_id + ) + + def test_get_vnc_console_with_str_body(self): + self._test_get_vnc_console() + + def test_get_vnc_console_with_bytes_body(self): + self._test_get_vnc_console(True) + + def _test_get_vnc_console(self, bytes_body=False): + self.check_service_client_function( + self.client.get_vnc_console, + 'tempest.lib.common.rest_client.RestClient.post', + {'console': self.FAKE_VNC_CONSOLE}, + server_id=self.server_id, + type='fake-console-type' + ) diff --git a/tempest/tests/lib/services/compute/test_services_client.py b/tempest/tests/lib/services/compute/test_services_client.py new file mode 100644 index 0000000000..7add187d73 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_services_client.py @@ -0,0 +1,94 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib.services.compute import services_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestServicesClient(base.BaseComputeServiceTest): + + FAKE_SERVICES = { + "services": + [{ + "status": "enabled", + "binary": "nova-conductor", + "zone": "internal", + "state": "up", + "updated_at": "2015-08-19T06:50:55.000000", + "host": "controller", + "disabled_reason": None, + "id": 1 + }] + } + + FAKE_SERVICE = { + "service": + { + "status": "enabled", + "binary": "nova-conductor", + "host": "controller" + } + } + + def setUp(self): + super(TestServicesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = services_client.ServicesClient( + fake_auth, 'compute', 'regionOne') + + def test_list_services_with_str_body(self): + self.check_service_client_function( + self.client.list_services, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_SERVICES) + + def test_list_services_with_bytes_body(self): + self.check_service_client_function( + self.client.list_services, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_SERVICES, to_utf=True) + + def _test_enable_service(self, bytes_body=False): + self.check_service_client_function( + self.client.enable_service, + 'tempest.lib.common.rest_client.RestClient.put', + self.FAKE_SERVICE, + bytes_body, + host_name="nova-conductor", binary="controller") + + def test_enable_service_with_str_body(self): + self._test_enable_service() + + def test_enable_service_with_bytes_body(self): + self._test_enable_service(bytes_body=True) + + def _test_disable_service(self, bytes_body=False): + fake_service = copy.deepcopy(self.FAKE_SERVICE) + fake_service["service"]["status"] = "disable" + + self.check_service_client_function( + self.client.disable_service, + 'tempest.lib.common.rest_client.RestClient.put', + fake_service, + bytes_body, + host_name="nova-conductor", binary="controller") + + def test_disable_service_with_str_body(self): + self._test_disable_service() + + def test_disable_service_with_bytes_body(self): + self._test_disable_service(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_snapshots_client.py b/tempest/tests/lib/services/compute/test_snapshots_client.py new file mode 100644 index 0000000000..b1d8ade60b --- /dev/null +++ b/tempest/tests/lib/services/compute/test_snapshots_client.py @@ -0,0 +1,103 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import mockpatch + +from tempest.lib import exceptions as lib_exc +from tempest.lib.services.compute import snapshots_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestSnapshotsClient(base.BaseComputeServiceTest): + + FAKE_SNAPSHOT = { + "createdAt": "2015-10-02T16:27:54.724209", + "displayDescription": u"Another \u1234.", + "displayName": u"v\u1234-001", + "id": "100", + "size": 100, + "status": "available", + "volumeId": "12" + } + + FAKE_SNAPSHOTS = {"snapshots": [FAKE_SNAPSHOT]} + + def setUp(self): + super(TestSnapshotsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = snapshots_client.SnapshotsClient( + fake_auth, 'compute', 'regionOne') + + def _test_create_snapshot(self, bytes_body=False): + self.check_service_client_function( + self.client.create_snapshot, + 'tempest.lib.common.rest_client.RestClient.post', + {"snapshot": self.FAKE_SNAPSHOT}, + to_utf=bytes_body, status=200, + volume_id=self.FAKE_SNAPSHOT["volumeId"]) + + def test_create_snapshot_with_str_body(self): + self._test_create_snapshot() + + def test_create_shapshot_with_bytes_body(self): + self._test_create_snapshot(bytes_body=True) + + def _test_show_snapshot(self, bytes_body=False): + self.check_service_client_function( + self.client.show_snapshot, + 'tempest.lib.common.rest_client.RestClient.get', + {"snapshot": self.FAKE_SNAPSHOT}, + to_utf=bytes_body, snapshot_id=self.FAKE_SNAPSHOT["id"]) + + def test_show_snapshot_with_str_body(self): + self._test_show_snapshot() + + def test_show_snapshot_with_bytes_body(self): + self._test_show_snapshot(bytes_body=True) + + def _test_list_snapshots(self, bytes_body=False, **params): + self.check_service_client_function( + self.client.list_snapshots, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_SNAPSHOTS, to_utf=bytes_body, **params) + + def test_list_snapshots_with_str_body(self): + self._test_list_snapshots() + + def test_list_snapshots_with_byte_body(self): + self._test_list_snapshots(bytes_body=True) + + def test_list_snapshots_with_params(self): + self._test_list_snapshots('fake') + + def test_delete_snapshot(self): + self.check_service_client_function( + self.client.delete_snapshot, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, status=202, snapshot_id=self.FAKE_SNAPSHOT['id']) + + def test_is_resource_deleted_true(self): + module = ('tempest.lib.services.compute.snapshots_client.' + 'SnapshotsClient.show_snapshot') + self.useFixture(mockpatch.Patch( + module, side_effect=lib_exc.NotFound)) + self.assertTrue(self.client.is_resource_deleted('fake-id')) + + def test_is_resource_deleted_false(self): + module = ('tempest.lib.services.compute.snapshots_client.' + 'SnapshotsClient.show_snapshot') + self.useFixture(mockpatch.Patch( + module, return_value={})) + self.assertFalse(self.client.is_resource_deleted('fake-id')) diff --git a/tempest/tests/lib/services/compute/test_tenant_networks_client.py b/tempest/tests/lib/services/compute/test_tenant_networks_client.py new file mode 100644 index 0000000000..cfb68cc1d3 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_tenant_networks_client.py @@ -0,0 +1,63 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import tenant_networks_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestTenantNetworksClient(base.BaseComputeServiceTest): + + FAKE_NETWORK = { + "cidr": "None", + "id": "c2329eb4-cc8e-4439-ac4c-932369309e36", + "label": u'\u30d7' + } + + FAKE_NETWORKS = [FAKE_NETWORK] + + NETWORK_ID = FAKE_NETWORK['id'] + + def setUp(self): + super(TestTenantNetworksClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = tenant_networks_client.TenantNetworksClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_tenant_networks(self, bytes_body=False): + self.check_service_client_function( + self.client.list_tenant_networks, + 'tempest.lib.common.rest_client.RestClient.get', + {"networks": self.FAKE_NETWORKS}, + bytes_body) + + def test_list_tenant_networks_with_str_body(self): + self._test_list_tenant_networks() + + def test_list_tenant_networks_with_bytes_body(self): + self._test_list_tenant_networks(bytes_body=True) + + def _test_show_tenant_network(self, bytes_body=False): + self.check_service_client_function( + self.client.show_tenant_network, + 'tempest.lib.common.rest_client.RestClient.get', + {"network": self.FAKE_NETWORK}, + bytes_body, + network_id=self.NETWORK_ID) + + def test_show_tenant_network_with_str_body(self): + self._test_show_tenant_network() + + def test_show_tenant_network_with_bytes_body(self): + self._test_show_tenant_network(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_tenant_usages_client.py b/tempest/tests/lib/services/compute/test_tenant_usages_client.py new file mode 100644 index 0000000000..88d093d306 --- /dev/null +++ b/tempest/tests/lib/services/compute/test_tenant_usages_client.py @@ -0,0 +1,79 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tempest.lib.services.compute import tenant_usages_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestTenantUsagesClient(base.BaseComputeServiceTest): + + FAKE_SERVER_USAGES = [{ + "ended_at": None, + "flavor": "m1.tiny", + "hours": 1.0, + "instance_id": "1f1deceb-17b5-4c04-84c7-e0d4499c8fe0", + "local_gb": 1, + "memory_mb": 512, + "name": "new-server-test", + "started_at": "2012-10-08T20:10:44.541277", + "state": "active", + "tenant_id": "openstack", + "uptime": 3600, + "vcpus": 1 + }] + + FAKE_TENANT_USAGES = [{ + "server_usages": FAKE_SERVER_USAGES, + "start": "2012-10-08T21:10:44.587336", + "stop": "2012-10-08T22:10:44.587336", + "tenant_id": "openstack", + "total_hours": 1, + "total_local_gb_usage": 1, + "total_memory_mb_usage": 512, + "total_vcpus_usage": 1 + }] + + def setUp(self): + super(TestTenantUsagesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = tenant_usages_client.TenantUsagesClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_tenant_usages(self, bytes_body=False): + self.check_service_client_function( + self.client.list_tenant_usages, + 'tempest.lib.common.rest_client.RestClient.get', + {"tenant_usages": self.FAKE_TENANT_USAGES}, + to_utf=bytes_body) + + def test_list_tenant_usages_with_str_body(self): + self._test_list_tenant_usages() + + def test_list_tenant_usages_with_bytes_body(self): + self._test_list_tenant_usages(bytes_body=True) + + def _test_show_tenant_usage(self, bytes_body=False): + self.check_service_client_function( + self.client.show_tenant_usage, + 'tempest.lib.common.rest_client.RestClient.get', + {"tenant_usage": self.FAKE_TENANT_USAGES[0]}, + to_utf=bytes_body, + tenant_id='openstack') + + def test_show_tenant_usage_with_str_body(self): + self._test_show_tenant_usage() + + def test_show_tenant_usage_with_bytes_body(self): + self._test_show_tenant_usage(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_versions_client.py b/tempest/tests/lib/services/compute/test_versions_client.py new file mode 100644 index 0000000000..5ac2f2d89c --- /dev/null +++ b/tempest/tests/lib/services/compute/test_versions_client.py @@ -0,0 +1,96 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +from oslotest import mockpatch + +from tempest.lib.services.compute import versions_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestVersionsClient(base.BaseComputeServiceTest): + + FAKE_INIT_VERSION = { + "version": { + "id": "v2.1", + "links": [ + { + "href": "http://openstack.example.com/v2.1/", + "rel": "self" + }, + { + "href": "http://docs.openstack.org/", + "rel": "describedby", + "type": "text/html" + } + ], + "status": "CURRENT", + "updated": "2013-07-23T11:33:21Z", + "version": "2.1", + "min_version": "2.1" + } + } + + FAKE_VERSIONS_INFO = { + "versions": [FAKE_INIT_VERSION["version"]] + } + + FAKE_VERSION_INFO = copy.deepcopy(FAKE_INIT_VERSION) + + FAKE_VERSION_INFO["version"]["media-types"] = [ + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=2.1" + } + ] + + def setUp(self): + super(TestVersionsClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.versions_client = ( + versions_client.VersionsClient + (fake_auth, 'compute', 'regionOne')) + + def _test_versions_client(self, bytes_body=False): + self.check_service_client_function( + self.versions_client.list_versions, + 'tempest.lib.common.rest_client.RestClient.raw_request', + self.FAKE_VERSIONS_INFO, + bytes_body, + 200) + + def _test_get_version_by_url(self, bytes_body=False): + self.useFixture(mockpatch.Patch( + "tempest.lib.common.rest_client.RestClient.token", + return_value="Dummy Token")) + params = {"version_url": self.versions_client._get_base_version_url()} + self.check_service_client_function( + self.versions_client.get_version_by_url, + 'tempest.lib.common.rest_client.RestClient.raw_request', + self.FAKE_VERSION_INFO, + bytes_body, + 200, **params) + + def test_list_versions_client_with_str_body(self): + self._test_versions_client() + + def test_list_versions_client_with_bytes_body(self): + self._test_versions_client(bytes_body=True) + + def test_get_version_by_url_with_str_body(self): + self._test_get_version_by_url() + + def test_get_version_by_url_with_bytes_body(self): + self._test_get_version_by_url(bytes_body=True) diff --git a/tempest/tests/lib/services/compute/test_volumes_client.py b/tempest/tests/lib/services/compute/test_volumes_client.py new file mode 100644 index 0000000000..d16f5b068e --- /dev/null +++ b/tempest/tests/lib/services/compute/test_volumes_client.py @@ -0,0 +1,114 @@ +# Copyright 2015 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from oslotest import mockpatch + +from tempest.lib import exceptions as lib_exc +from tempest.lib.services.compute import volumes_client +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib.services.compute import base + + +class TestVolumesClient(base.BaseComputeServiceTest): + + FAKE_VOLUME = { + "id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + "displayName": u"v\u12345ol-001", + "displayDescription": u"Another \u1234volume.", + "size": 30, + "status": "Active", + "volumeType": "289da7f8-6440-407c-9fb4-7db01ec49164", + "metadata": { + "contents": "junk" + }, + "availabilityZone": "us-east1", + "snapshotId": None, + "attachments": [], + "createdAt": "2012-02-14T20:53:07Z" + } + + FAKE_VOLUMES = {"volumes": [FAKE_VOLUME]} + + def setUp(self): + super(TestVolumesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = volumes_client.VolumesClient( + fake_auth, 'compute', 'regionOne') + + def _test_list_volumes(self, bytes_body=False, **params): + self.check_service_client_function( + self.client.list_volumes, + 'tempest.lib.common.rest_client.RestClient.get', + self.FAKE_VOLUMES, to_utf=bytes_body, **params) + + def test_list_volumes_with_str_body(self): + self._test_list_volumes() + + def test_list_volumes_with_byte_body(self): + self._test_list_volumes(bytes_body=True) + + def test_list_volumes_with_params(self): + self._test_list_volumes(name='fake') + + def _test_show_volume(self, bytes_body=False): + self.check_service_client_function( + self.client.show_volume, + 'tempest.lib.common.rest_client.RestClient.get', + {"volume": self.FAKE_VOLUME}, + to_utf=bytes_body, volume_id=self.FAKE_VOLUME['id']) + + def test_show_volume_with_str_body(self): + self._test_show_volume() + + def test_show_volume_with_bytes_body(self): + self._test_show_volume(bytes_body=True) + + def _test_create_volume(self, bytes_body=False): + post_body = copy.deepcopy(self.FAKE_VOLUME) + del post_body['id'] + del post_body['createdAt'] + del post_body['status'] + self.check_service_client_function( + self.client.create_volume, + 'tempest.lib.common.rest_client.RestClient.post', + {"volume": self.FAKE_VOLUME}, + to_utf=bytes_body, status=200, **post_body) + + def test_create_volume_with_str_body(self): + self._test_create_volume() + + def test_create_volume_with_bytes_body(self): + self._test_create_volume(bytes_body=True) + + def test_delete_volume(self): + self.check_service_client_function( + self.client.delete_volume, + 'tempest.lib.common.rest_client.RestClient.delete', + {}, status=202, volume_id=self.FAKE_VOLUME['id']) + + def test_is_resource_deleted_true(self): + module = ('tempest.lib.services.compute.volumes_client.' + 'VolumesClient.show_volume') + self.useFixture(mockpatch.Patch( + module, side_effect=lib_exc.NotFound)) + self.assertTrue(self.client.is_resource_deleted('fake-id')) + + def test_is_resource_deleted_false(self): + module = ('tempest.lib.services.compute.volumes_client.' + 'VolumesClient.show_volume') + self.useFixture(mockpatch.Patch( + module, return_value={})) + self.assertFalse(self.client.is_resource_deleted('fake-id')) diff --git a/tempest/tests/lib/services/identity/__init__.py b/tempest/tests/lib/services/identity/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/lib/services/identity/v2/__init__.py b/tempest/tests/lib/services/identity/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/lib/services/identity/v2/test_token_client.py b/tempest/tests/lib/services/identity/v2/test_token_client.py new file mode 100644 index 0000000000..dd3533a4ce --- /dev/null +++ b/tempest/tests/lib/services/identity/v2/test_token_client.py @@ -0,0 +1,92 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 httplib2 +from oslotest import mockpatch + +from tempest.lib.common import rest_client +from tempest.lib import exceptions +from tempest.lib.services.identity.v2 import token_client +from tempest.tests.lib import base +from tempest.tests.lib import fake_http + + +class TestTokenClientV2(base.TestCase): + + def setUp(self): + super(TestTokenClientV2, self).setUp() + self.fake_200_http = fake_http.fake_httplib2(return_type=200) + + def test_init_without_authurl(self): + self.assertRaises(exceptions.IdentityError, + token_client.TokenClient, None) + + def test_auth(self): + token_client_v2 = token_client.TokenClient('fake_url') + post_mock = self.useFixture(mockpatch.PatchObject( + token_client_v2, 'post', return_value=self.fake_200_http.request( + 'fake_url', body={'access': {'token': 'fake_token'}}))) + resp = token_client_v2.auth('fake_user', 'fake_pass') + self.assertIsInstance(resp, rest_client.ResponseBody) + req_dict = json.dumps({ + 'auth': { + 'passwordCredentials': { + 'username': 'fake_user', + 'password': 'fake_pass', + }, + } + }, sort_keys=True) + post_mock.mock.assert_called_once_with('fake_url/tokens', + body=req_dict) + + def test_auth_with_tenant(self): + token_client_v2 = token_client.TokenClient('fake_url') + post_mock = self.useFixture(mockpatch.PatchObject( + token_client_v2, 'post', return_value=self.fake_200_http.request( + 'fake_url', body={'access': {'token': 'fake_token'}}))) + resp = token_client_v2.auth('fake_user', 'fake_pass', 'fake_tenant') + self.assertIsInstance(resp, rest_client.ResponseBody) + req_dict = json.dumps({ + 'auth': { + 'tenantName': 'fake_tenant', + 'passwordCredentials': { + 'username': 'fake_user', + 'password': 'fake_pass', + }, + } + }, sort_keys=True) + post_mock.mock.assert_called_once_with('fake_url/tokens', + body=req_dict) + + def test_request_with_str_body(self): + token_client_v2 = token_client.TokenClient('fake_url') + self.useFixture(mockpatch.PatchObject( + token_client_v2, 'raw_request', return_value=( + httplib2.Response({'status': '200'}), + str('{"access": {"token": "fake_token"}}')))) + resp, body = token_client_v2.request('GET', 'fake_uri') + self.assertIsInstance(resp, httplib2.Response) + self.assertIsInstance(body, dict) + + def test_request_with_bytes_body(self): + token_client_v2 = token_client.TokenClient('fake_url') + self.useFixture(mockpatch.PatchObject( + token_client_v2, 'raw_request', return_value=( + httplib2.Response({'status': '200'}), + bytes(b'{"access": {"token": "fake_token"}}')))) + resp, body = token_client_v2.request('GET', 'fake_uri') + self.assertIsInstance(resp, httplib2.Response) + self.assertIsInstance(body, dict) diff --git a/tempest/tests/lib/services/identity/v3/__init__.py b/tempest/tests/lib/services/identity/v3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/lib/services/identity/v3/test_token_client.py b/tempest/tests/lib/services/identity/v3/test_token_client.py new file mode 100644 index 0000000000..bb4dae3e0f --- /dev/null +++ b/tempest/tests/lib/services/identity/v3/test_token_client.py @@ -0,0 +1,145 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 httplib2 +from oslotest import mockpatch + +from tempest.lib.common import rest_client +from tempest.lib import exceptions +from tempest.lib.services.identity.v3 import token_client +from tempest.tests.lib import base +from tempest.tests.lib import fake_http + + +class TestTokenClientV2(base.TestCase): + + def setUp(self): + super(TestTokenClientV2, self).setUp() + self.fake_201_http = fake_http.fake_httplib2(return_type=201) + + def test_init_without_authurl(self): + self.assertRaises(exceptions.IdentityError, + token_client.V3TokenClient, None) + + def test_auth(self): + token_client_v3 = token_client.V3TokenClient('fake_url') + post_mock = self.useFixture(mockpatch.PatchObject( + token_client_v3, 'post', return_value=self.fake_201_http.request( + 'fake_url', body={'access': {'token': 'fake_token'}}))) + resp = token_client_v3.auth(username='fake_user', password='fake_pass') + self.assertIsInstance(resp, rest_client.ResponseBody) + req_dict = json.dumps({ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'name': 'fake_user', + 'password': 'fake_pass', + } + } + }, + } + }, sort_keys=True) + post_mock.mock.assert_called_once_with('fake_url/auth/tokens', + body=req_dict) + + def test_auth_with_project_id_and_domain_id(self): + token_client_v3 = token_client.V3TokenClient('fake_url') + post_mock = self.useFixture(mockpatch.PatchObject( + token_client_v3, 'post', return_value=self.fake_201_http.request( + 'fake_url', body={'access': {'token': 'fake_token'}}))) + resp = token_client_v3.auth( + username='fake_user', password='fake_pass', + project_id='fcac2a055a294e4c82d0a9c21c620eb4', + user_domain_id='14f4a9a99973404d8c20ba1d2af163ff', + project_domain_id='291f63ae9ac54ee292ca09e5f72d9676') + self.assertIsInstance(resp, rest_client.ResponseBody) + req_dict = json.dumps({ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'name': 'fake_user', + 'password': 'fake_pass', + 'domain': { + 'id': '14f4a9a99973404d8c20ba1d2af163ff' + } + } + } + }, + 'scope': { + 'project': { + 'id': 'fcac2a055a294e4c82d0a9c21c620eb4', + 'domain': { + 'id': '291f63ae9ac54ee292ca09e5f72d9676' + } + } + } + } + }, sort_keys=True) + post_mock.mock.assert_called_once_with('fake_url/auth/tokens', + body=req_dict) + + def test_auth_with_tenant(self): + token_client_v2 = token_client.V3TokenClient('fake_url') + post_mock = self.useFixture(mockpatch.PatchObject( + token_client_v2, 'post', return_value=self.fake_201_http.request( + 'fake_url', body={'access': {'token': 'fake_token'}}))) + resp = token_client_v2.auth(username='fake_user', password='fake_pass', + project_name='fake_tenant') + self.assertIsInstance(resp, rest_client.ResponseBody) + req_dict = json.dumps({ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'name': 'fake_user', + 'password': 'fake_pass', + } + }}, + 'scope': { + 'project': { + 'name': 'fake_tenant' + } + }, + } + }, sort_keys=True) + + post_mock.mock.assert_called_once_with('fake_url/auth/tokens', + body=req_dict) + + def test_request_with_str_body(self): + token_client_v3 = token_client.V3TokenClient('fake_url') + self.useFixture(mockpatch.PatchObject( + token_client_v3, 'raw_request', return_value=( + httplib2.Response({"status": "200"}), + str('{"access": {"token": "fake_token"}}')))) + resp, body = token_client_v3.request('GET', 'fake_uri') + self.assertIsInstance(resp, httplib2.Response) + self.assertIsInstance(body, dict) + + def test_request_with_bytes_body(self): + token_client_v3 = token_client.V3TokenClient('fake_url') + self.useFixture(mockpatch.PatchObject( + token_client_v3, 'raw_request', return_value=( + httplib2.Response({"status": "200"}), + bytes(b'{"access": {"token": "fake_token"}}')))) + resp, body = token_client_v3.request('GET', 'fake_uri') + self.assertIsInstance(resp, httplib2.Response) + self.assertIsInstance(body, dict) diff --git a/tempest/tests/lib/test_auth.py b/tempest/tests/lib/test_auth.py new file mode 100644 index 0000000000..f7bc7e4ae8 --- /dev/null +++ b/tempest/tests/lib/test_auth.py @@ -0,0 +1,480 @@ +# Copyright 2014 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import datetime + +from oslotest import mockpatch + +from tempest.lib import auth +from tempest.lib import exceptions +from tempest.lib.services.identity.v2 import token_client as v2_client +from tempest.lib.services.identity.v3 import token_client as v3_client +from tempest.tests.lib import base +from tempest.tests.lib import fake_credentials +from tempest.tests.lib import fake_http +from tempest.tests.lib import fake_identity + + +def fake_get_credentials(fill_in=True, identity_version='v2', **kwargs): + return fake_credentials.FakeCredentials() + + +class BaseAuthTestsSetUp(base.TestCase): + _auth_provider_class = None + credentials = fake_credentials.FakeCredentials() + + def _auth(self, credentials, auth_url, **params): + """returns auth method according to keystone""" + return self._auth_provider_class(credentials, auth_url, **params) + + def setUp(self): + super(BaseAuthTestsSetUp, self).setUp() + self.fake_http = fake_http.fake_httplib2(return_type=200) + self.stubs.Set(auth, 'get_credentials', fake_get_credentials) + self.auth_provider = self._auth(self.credentials, + fake_identity.FAKE_AUTH_URL) + + +class TestBaseAuthProvider(BaseAuthTestsSetUp): + """Tests for base AuthProvider + + This tests auth.AuthProvider class which is base for the other so we + obviously don't test not implemented method or the ones which strongly + depends on them. + """ + + class FakeAuthProviderImpl(auth.AuthProvider): + def _decorate_request(self): + pass + + def _fill_credentials(self): + pass + + def _get_auth(self): + pass + + def base_url(self): + pass + + def is_expired(self): + pass + + _auth_provider_class = FakeAuthProviderImpl + + def _auth(self, credentials, auth_url, **params): + """returns auth method according to keystone""" + return self._auth_provider_class(credentials, **params) + + def test_check_credentials_bad_type(self): + self.assertFalse(self.auth_provider.check_credentials([])) + + def test_auth_data_property_when_cache_exists(self): + self.auth_provider.cache = 'foo' + self.useFixture(mockpatch.PatchObject(self.auth_provider, + 'is_expired', + return_value=False)) + self.assertEqual('foo', getattr(self.auth_provider, 'auth_data')) + + def test_delete_auth_data_property_through_deleter(self): + self.auth_provider.cache = 'foo' + del self.auth_provider.auth_data + self.assertIsNone(self.auth_provider.cache) + + def test_delete_auth_data_property_through_clear_auth(self): + self.auth_provider.cache = 'foo' + self.auth_provider.clear_auth() + self.assertIsNone(self.auth_provider.cache) + + def test_set_and_reset_alt_auth_data(self): + self.auth_provider.set_alt_auth_data('foo', 'bar') + self.assertEqual(self.auth_provider.alt_part, 'foo') + self.assertEqual(self.auth_provider.alt_auth_data, 'bar') + + self.auth_provider.reset_alt_auth_data() + self.assertIsNone(self.auth_provider.alt_part) + self.assertIsNone(self.auth_provider.alt_auth_data) + + def test_auth_class(self): + self.assertRaises(TypeError, + auth.AuthProvider, + fake_credentials.FakeCredentials) + + +class TestKeystoneV2AuthProvider(BaseAuthTestsSetUp): + _endpoints = fake_identity.IDENTITY_V2_RESPONSE['access']['serviceCatalog'] + _auth_provider_class = auth.KeystoneV2AuthProvider + credentials = fake_credentials.FakeKeystoneV2Credentials() + + def setUp(self): + super(TestKeystoneV2AuthProvider, self).setUp() + self.stubs.Set(v2_client.TokenClient, 'raw_request', + fake_identity._fake_v2_response) + self.target_url = 'test_api' + + def _get_fake_identity(self): + return fake_identity.IDENTITY_V2_RESPONSE['access'] + + def _get_fake_alt_identity(self): + return fake_identity.ALT_IDENTITY_V2_RESPONSE['access'] + + def _get_result_url_from_endpoint(self, ep, endpoint_type='publicURL', + replacement=None): + if replacement: + return ep[endpoint_type].replace('v2', replacement) + return ep[endpoint_type] + + def _get_token_from_fake_identity(self): + return fake_identity.TOKEN + + def _get_from_fake_identity(self, attr): + access = fake_identity.IDENTITY_V2_RESPONSE['access'] + if attr == 'user_id': + return access['user']['id'] + elif attr == 'tenant_id': + return access['token']['tenant']['id'] + + def _test_request_helper(self, filters, expected): + url, headers, body = self.auth_provider.auth_request('GET', + self.target_url, + filters=filters) + + self.assertEqual(expected['url'], url) + self.assertEqual(expected['token'], headers['X-Auth-Token']) + self.assertEqual(expected['body'], body) + + def _auth_data_with_expiry(self, date_as_string): + token, access = self.auth_provider.auth_data + access['token']['expires'] = date_as_string + return token, access + + def test_request(self): + filters = { + 'service': 'compute', + 'endpoint_type': 'publicURL', + 'region': 'FakeRegion' + } + + url = self._get_result_url_from_endpoint( + self._endpoints[0]['endpoints'][1]) + '/' + self.target_url + + expected = { + 'body': None, + 'url': url, + 'token': self._get_token_from_fake_identity(), + } + self._test_request_helper(filters, expected) + + def test_request_with_alt_auth_cleans_alt(self): + """Test alternate auth data for headers + + Assert that when the alt data is provided for headers, after an + auth_request the data alt_data is cleaned-up. + """ + self.auth_provider.set_alt_auth_data( + 'headers', + (fake_identity.ALT_TOKEN, self._get_fake_alt_identity())) + filters = { + 'service': 'compute', + 'endpoint_type': 'publicURL', + 'region': 'fakeRegion' + } + self.auth_provider.auth_request('GET', self.target_url, + filters=filters) + + # Assert alt auth data is clear after it + self.assertIsNone(self.auth_provider.alt_part) + self.assertIsNone(self.auth_provider.alt_auth_data) + + def _test_request_with_identical_alt_auth(self, part): + """Test alternate but identical auth data for headers + + Assert that when the alt data is provided, but it's actually + identical, an exception is raised. + """ + self.auth_provider.set_alt_auth_data( + part, + (fake_identity.TOKEN, self._get_fake_identity())) + filters = { + 'service': 'compute', + 'endpoint_type': 'publicURL', + 'region': 'fakeRegion' + } + + self.assertRaises(exceptions.BadAltAuth, + self.auth_provider.auth_request, + 'GET', self.target_url, filters=filters) + + def test_request_with_identical_alt_auth_headers(self): + self._test_request_with_identical_alt_auth('headers') + + def test_request_with_identical_alt_auth_url(self): + self._test_request_with_identical_alt_auth('url') + + def test_request_with_identical_alt_auth_body(self): + self._test_request_with_identical_alt_auth('body') + + def test_request_with_alt_part_without_alt_data(self): + """Test empty alternate auth data + + Assert that when alt_part is defined, the corresponding original + request element is kept the same. + """ + filters = { + 'service': 'compute', + 'endpoint_type': 'publicURL', + 'region': 'fakeRegion' + } + self.auth_provider.set_alt_auth_data('headers', None) + + url, headers, body = self.auth_provider.auth_request('GET', + self.target_url, + filters=filters) + # The original headers where empty + self.assertNotEqual(url, self.target_url) + self.assertIsNone(headers) + self.assertEqual(body, None) + + def _test_request_with_alt_part_without_alt_data_no_change(self, body): + """Test empty alternate auth data with no effect + + Assert that when alt_part is defined, no auth_data is provided, + and the the corresponding original request element was not going to + be changed anyways, and exception is raised + """ + filters = { + 'service': 'compute', + 'endpoint_type': 'publicURL', + 'region': 'fakeRegion' + } + self.auth_provider.set_alt_auth_data('body', None) + + self.assertRaises(exceptions.BadAltAuth, + self.auth_provider.auth_request, + 'GET', self.target_url, filters=filters) + + def test_request_with_alt_part_without_alt_data_no_change_headers(self): + self._test_request_with_alt_part_without_alt_data_no_change('headers') + + def test_request_with_alt_part_without_alt_data_no_change_url(self): + self._test_request_with_alt_part_without_alt_data_no_change('url') + + def test_request_with_alt_part_without_alt_data_no_change_body(self): + self._test_request_with_alt_part_without_alt_data_no_change('body') + + def test_request_with_bad_service(self): + filters = { + 'service': 'BAD_SERVICE', + 'endpoint_type': 'publicURL', + 'region': 'fakeRegion' + } + self.assertRaises(exceptions.EndpointNotFound, + self.auth_provider.auth_request, 'GET', + self.target_url, filters=filters) + + def test_request_without_service(self): + filters = { + 'service': None, + 'endpoint_type': 'publicURL', + 'region': 'fakeRegion' + } + self.assertRaises(exceptions.EndpointNotFound, + self.auth_provider.auth_request, 'GET', + self.target_url, filters=filters) + + def test_check_credentials_missing_attribute(self): + for attr in ['username', 'password']: + cred = copy.copy(self.credentials) + del cred[attr] + self.assertFalse(self.auth_provider.check_credentials(cred)) + + def test_fill_credentials(self): + self.auth_provider.fill_credentials() + creds = self.auth_provider.credentials + for attr in ['user_id', 'tenant_id']: + self.assertEqual(self._get_from_fake_identity(attr), + getattr(creds, attr)) + + def _test_base_url_helper(self, expected_url, filters, + auth_data=None): + + url = self.auth_provider.base_url(filters, auth_data) + self.assertEqual(url, expected_url) + + def test_base_url(self): + self.filters = { + 'service': 'compute', + 'endpoint_type': 'publicURL', + 'region': 'FakeRegion' + } + expected = self._get_result_url_from_endpoint( + self._endpoints[0]['endpoints'][1]) + self._test_base_url_helper(expected, self.filters) + + def test_base_url_to_get_admin_endpoint(self): + self.filters = { + 'service': 'compute', + 'endpoint_type': 'adminURL', + 'region': 'FakeRegion' + } + expected = self._get_result_url_from_endpoint( + self._endpoints[0]['endpoints'][1], endpoint_type='adminURL') + self._test_base_url_helper(expected, self.filters) + + def test_base_url_unknown_region(self): + """If the region is unknown, the first endpoint is returned.""" + self.filters = { + 'service': 'compute', + 'endpoint_type': 'publicURL', + 'region': 'AintNoBodyKnowThisRegion' + } + expected = self._get_result_url_from_endpoint( + self._endpoints[0]['endpoints'][0]) + self._test_base_url_helper(expected, self.filters) + + def test_base_url_with_non_existent_service(self): + self.filters = { + 'service': 'BAD_SERVICE', + 'endpoint_type': 'publicURL', + 'region': 'FakeRegion' + } + self.assertRaises(exceptions.EndpointNotFound, + self._test_base_url_helper, None, self.filters) + + def test_base_url_without_service(self): + self.filters = { + 'endpoint_type': 'publicURL', + 'region': 'FakeRegion' + } + self.assertRaises(exceptions.EndpointNotFound, + self._test_base_url_helper, None, self.filters) + + def test_base_url_with_api_version_filter(self): + self.filters = { + 'service': 'compute', + 'endpoint_type': 'publicURL', + 'region': 'FakeRegion', + 'api_version': 'v12' + } + expected = self._get_result_url_from_endpoint( + self._endpoints[0]['endpoints'][1], replacement='v12') + self._test_base_url_helper(expected, self.filters) + + def test_base_url_with_skip_path_filter(self): + self.filters = { + 'service': 'compute', + 'endpoint_type': 'publicURL', + 'region': 'FakeRegion', + 'skip_path': True + } + expected = 'http://fake_url/' + self._test_base_url_helper(expected, self.filters) + + def test_token_not_expired(self): + expiry_data = datetime.datetime.utcnow() + datetime.timedelta(days=1) + self._verify_expiry(expiry_data=expiry_data, should_be_expired=False) + + def test_token_expired(self): + expiry_data = datetime.datetime.utcnow() - datetime.timedelta(hours=1) + self._verify_expiry(expiry_data=expiry_data, should_be_expired=True) + + def test_token_not_expired_to_be_renewed(self): + expiry_data = (datetime.datetime.utcnow() + + self.auth_provider.token_expiry_threshold / 2) + self._verify_expiry(expiry_data=expiry_data, should_be_expired=True) + + def _verify_expiry(self, expiry_data, should_be_expired): + for expiry_format in self.auth_provider.EXPIRY_DATE_FORMATS: + auth_data = self._auth_data_with_expiry( + expiry_data.strftime(expiry_format)) + self.assertEqual(self.auth_provider.is_expired(auth_data), + should_be_expired) + + +class TestKeystoneV3AuthProvider(TestKeystoneV2AuthProvider): + _endpoints = fake_identity.IDENTITY_V3_RESPONSE['token']['catalog'] + _auth_provider_class = auth.KeystoneV3AuthProvider + credentials = fake_credentials.FakeKeystoneV3Credentials() + + def setUp(self): + super(TestKeystoneV3AuthProvider, self).setUp() + self.stubs.Set(v3_client.V3TokenClient, 'raw_request', + fake_identity._fake_v3_response) + + def _get_fake_identity(self): + return fake_identity.IDENTITY_V3_RESPONSE['token'] + + def _get_fake_alt_identity(self): + return fake_identity.ALT_IDENTITY_V3['token'] + + def _get_result_url_from_endpoint(self, ep, replacement=None): + if replacement: + return ep['url'].replace('v3', replacement) + return ep['url'] + + def _auth_data_with_expiry(self, date_as_string): + token, access = self.auth_provider.auth_data + access['expires_at'] = date_as_string + return token, access + + def _get_from_fake_identity(self, attr): + token = fake_identity.IDENTITY_V3_RESPONSE['token'] + if attr == 'user_id': + return token['user']['id'] + elif attr == 'project_id': + return token['project']['id'] + elif attr == 'user_domain_id': + return token['user']['domain']['id'] + elif attr == 'project_domain_id': + return token['project']['domain']['id'] + + def test_check_credentials_missing_attribute(self): + # reset credentials to fresh ones + self.credentials.reset() + for attr in ['username', 'password', 'user_domain_name', + 'project_domain_name']: + cred = copy.copy(self.credentials) + del cred[attr] + self.assertFalse(self.auth_provider.check_credentials(cred), + "Credentials should be invalid without %s" % attr) + + def test_check_domain_credentials_missing_attribute(self): + # reset credentials to fresh ones + self.credentials.reset() + domain_creds = fake_credentials.FakeKeystoneV3DomainCredentials() + for attr in ['username', 'password', 'user_domain_name']: + cred = copy.copy(domain_creds) + del cred[attr] + self.assertFalse(self.auth_provider.check_credentials(cred), + "Credentials should be invalid without %s" % attr) + + def test_fill_credentials(self): + self.auth_provider.fill_credentials() + creds = self.auth_provider.credentials + for attr in ['user_id', 'project_id', 'user_domain_id', + 'project_domain_id']: + self.assertEqual(self._get_from_fake_identity(attr), + getattr(creds, attr)) + + # Overwrites v2 test + def test_base_url_to_get_admin_endpoint(self): + self.filters = { + 'service': 'compute', + 'endpoint_type': 'admin', + 'region': 'MiddleEarthRegion' + } + expected = self._get_result_url_from_endpoint( + self._endpoints[0]['endpoints'][2]) + self._test_base_url_helper(expected, self.filters) diff --git a/tempest/tests/lib/test_base.py b/tempest/tests/lib/test_base.py new file mode 100644 index 0000000000..27cda1a2c0 --- /dev/null +++ b/tempest/tests/lib/test_base.py @@ -0,0 +1,64 @@ +# Copyright 2014 Mirantis 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 testtools + +from tempest.lib import base +from tempest.lib import exceptions + + +class TestAttr(base.BaseTestCase): + + def test_has_no_attrs(self): + self.assertEqual( + 'tempest.tests.lib.test_base.TestAttr.test_has_no_attrs', + self.id() + ) + + @testtools.testcase.attr('foo') + def test_has_one_attr(self): + self.assertEqual( + 'tempest.tests.lib.test_base.TestAttr.test_has_one_attr[foo]', + self.id() + ) + + @testtools.testcase.attr('foo') + @testtools.testcase.attr('bar') + def test_has_two_attrs(self): + self.assertEqual( + 'tempest.tests.lib.test_base.TestAttr.test_has_two_attrs[bar,foo]', + self.id(), + ) + + +class TestSetUpClass(base.BaseTestCase): + + @classmethod + def setUpClass(cls): # noqa + """Simulate absence of super() call.""" + + def setUp(self): + try: + # We expect here RuntimeError exception because 'setUpClass' + # has not called 'super'. + super(TestSetUpClass, self).setUp() + except RuntimeError: + pass + else: + raise exceptions.TempestException( + "If you see this, then expected exception was not raised.") + + def test_setup_class_raises_runtime_error(self): + """No-op test just to call setUp.""" diff --git a/tempest/tests/lib/test_credentials.py b/tempest/tests/lib/test_credentials.py new file mode 100644 index 0000000000..791fbb5a7c --- /dev/null +++ b/tempest/tests/lib/test_credentials.py @@ -0,0 +1,180 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from tempest.lib import auth +from tempest.lib import exceptions +from tempest.lib.services.identity.v2 import token_client as v2_client +from tempest.lib.services.identity.v3 import token_client as v3_client +from tempest.tests.lib import base +from tempest.tests.lib import fake_identity + + +class CredentialsTests(base.TestCase): + attributes = {} + credentials_class = auth.Credentials + + def _get_credentials(self, attributes=None): + if attributes is None: + attributes = self.attributes + return self.credentials_class(**attributes) + + def _check(self, credentials, credentials_class, filled): + # Check the right version of credentials has been returned + self.assertIsInstance(credentials, credentials_class) + # Check the id attributes are filled in + attributes = [x for x in credentials.ATTRIBUTES if ( + '_id' in x and x != 'domain_id')] + for attr in attributes: + if filled: + self.assertIsNotNone(getattr(credentials, attr)) + else: + self.assertIsNone(getattr(credentials, attr)) + + def test_create(self): + creds = self._get_credentials() + self.assertEqual(self.attributes, creds._initial) + + def test_create_invalid_attr(self): + self.assertRaises(exceptions.InvalidCredentials, + self._get_credentials, + attributes=dict(invalid='fake')) + + def test_is_valid(self): + creds = self._get_credentials() + self.assertRaises(NotImplementedError, creds.is_valid) + + +class KeystoneV2CredentialsTests(CredentialsTests): + attributes = { + 'username': 'fake_username', + 'password': 'fake_password', + 'tenant_name': 'fake_tenant_name' + } + + identity_response = fake_identity._fake_v2_response + credentials_class = auth.KeystoneV2Credentials + tokenclient_class = v2_client.TokenClient + identity_version = 'v2' + + def setUp(self): + super(KeystoneV2CredentialsTests, self).setUp() + self.stubs.Set(self.tokenclient_class, 'raw_request', + self.identity_response) + + def _verify_credentials(self, credentials_class, creds_dict, filled=True): + creds = auth.get_credentials(fake_identity.FAKE_AUTH_URL, + fill_in=filled, + identity_version=self.identity_version, + **creds_dict) + self._check(creds, credentials_class, filled) + + def test_get_credentials(self): + self._verify_credentials(credentials_class=self.credentials_class, + creds_dict=self.attributes) + + def test_get_credentials_not_filled(self): + self._verify_credentials(credentials_class=self.credentials_class, + creds_dict=self.attributes, + filled=False) + + def test_is_valid(self): + creds = self._get_credentials() + self.assertTrue(creds.is_valid()) + + def _test_is_not_valid(self, ignore_key): + creds = self._get_credentials() + for attr in self.attributes.keys(): + if attr == ignore_key: + continue + temp_attr = getattr(creds, attr) + delattr(creds, attr) + self.assertFalse(creds.is_valid(), + "Credentials should be invalid without %s" % attr) + setattr(creds, attr, temp_attr) + + def test_is_not_valid(self): + # NOTE(mtreinish): A KeystoneV2 credential object is valid without + # a tenant_name. So skip that check. See tempest.auth for the valid + # credential requirements + self._test_is_not_valid('tenant_name') + + def test_reset_all_attributes(self): + creds = self._get_credentials() + initial_creds = copy.deepcopy(creds) + set_attr = creds.__dict__.keys() + missing_attr = set(creds.ATTRIBUTES).difference(set_attr) + # Set all unset attributes, then reset + for attr in missing_attr: + setattr(creds, attr, 'fake' + attr) + creds.reset() + # Check reset credentials are same as initial ones + self.assertEqual(creds, initial_creds) + + def test_reset_single_attribute(self): + creds = self._get_credentials() + initial_creds = copy.deepcopy(creds) + set_attr = creds.__dict__.keys() + missing_attr = set(creds.ATTRIBUTES).difference(set_attr) + # Set one unset attributes, then reset + for attr in missing_attr: + setattr(creds, attr, 'fake' + attr) + creds.reset() + # Check reset credentials are same as initial ones + self.assertEqual(creds, initial_creds) + + +class KeystoneV3CredentialsTests(KeystoneV2CredentialsTests): + attributes = { + 'username': 'fake_username', + 'password': 'fake_password', + 'project_name': 'fake_project_name', + 'user_domain_name': 'fake_domain_name' + } + + credentials_class = auth.KeystoneV3Credentials + identity_response = fake_identity._fake_v3_response + tokenclient_class = v3_client.V3TokenClient + identity_version = 'v3' + + def test_is_not_valid(self): + # NOTE(mtreinish) For a Keystone V3 credential object a project name + # is not required to be valid, so we skip that check. See tempest.auth + # for the valid credential requirements + self._test_is_not_valid('project_name') + + def test_synced_attributes(self): + attributes = self.attributes + # Create V3 credentials with tenant instead of project, and user_domain + for attr in ['project_id', 'user_domain_id']: + attributes[attr] = 'fake_' + attr + creds = self._get_credentials(attributes) + self.assertEqual(creds.project_name, creds.tenant_name) + self.assertEqual(creds.project_id, creds.tenant_id) + self.assertEqual(creds.user_domain_name, creds.project_domain_name) + self.assertEqual(creds.user_domain_id, creds.project_domain_id) + # Replace user_domain with project_domain + del attributes['user_domain_name'] + del attributes['user_domain_id'] + del attributes['project_name'] + del attributes['project_id'] + for attr in ['project_domain_name', 'project_domain_id', + 'tenant_name', 'tenant_id']: + attributes[attr] = 'fake_' + attr + self.assertEqual(creds.tenant_name, creds.project_name) + self.assertEqual(creds.tenant_id, creds.project_id) + self.assertEqual(creds.project_domain_name, creds.user_domain_name) + self.assertEqual(creds.project_domain_id, creds.user_domain_id) diff --git a/tempest/tests/lib/test_decorators.py b/tempest/tests/lib/test_decorators.py new file mode 100644 index 0000000000..558445d06c --- /dev/null +++ b/tempest/tests/lib/test_decorators.py @@ -0,0 +1,126 @@ +# Copyright 2013 IBM Corp +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +import testtools + +from tempest.lib import base as test +from tempest.lib import decorators +from tempest.tests.lib import base + + +class TestSkipBecauseDecorator(base.TestCase): + def _test_skip_because_helper(self, expected_to_skip=True, + **decorator_args): + class TestFoo(test.BaseTestCase): + _interface = 'json' + + @decorators.skip_because(**decorator_args) + def test_bar(self): + return 0 + + t = TestFoo('test_bar') + if expected_to_skip: + self.assertRaises(testtools.TestCase.skipException, t.test_bar) + else: + # assert that test_bar returned 0 + self.assertEqual(TestFoo('test_bar').test_bar(), 0) + + def test_skip_because_bug(self): + self._test_skip_because_helper(bug='12345') + + def test_skip_because_bug_and_condition_true(self): + self._test_skip_because_helper(bug='12348', condition=True) + + def test_skip_because_bug_and_condition_false(self): + self._test_skip_because_helper(expected_to_skip=False, + bug='12349', condition=False) + + def test_skip_because_bug_without_bug_never_skips(self): + """Never skip without a bug parameter.""" + self._test_skip_because_helper(expected_to_skip=False, + condition=True) + self._test_skip_because_helper(expected_to_skip=False) + + def test_skip_because_invalid_bug_number(self): + """Raise ValueError if with an invalid bug number""" + self.assertRaises(ValueError, self._test_skip_because_helper, + bug='critical_bug') + + +class TestIdempotentIdDecorator(base.TestCase): + def _test_helper(self, _id, **decorator_args): + @decorators.idempotent_id(_id) + def foo(): + """Docstring""" + pass + + return foo + + def _test_helper_without_doc(self, _id, **decorator_args): + @decorators.idempotent_id(_id) + def foo(): + pass + + return foo + + def test_positive(self): + _id = str(uuid.uuid4()) + foo = self._test_helper(_id) + self.assertIn('id-%s' % _id, getattr(foo, '__testtools_attrs')) + self.assertTrue(foo.__doc__.startswith('Test idempotent id: %s' % _id)) + + def test_positive_without_doc(self): + _id = str(uuid.uuid4()) + foo = self._test_helper_without_doc(_id) + self.assertTrue(foo.__doc__.startswith('Test idempotent id: %s' % _id)) + + def test_idempotent_id_not_str(self): + _id = 42 + self.assertRaises(TypeError, self._test_helper, _id) + + def test_idempotent_id_not_valid_uuid(self): + _id = '42' + self.assertRaises(ValueError, self._test_helper, _id) + + +class TestSkipUnlessAttrDecorator(base.TestCase): + def _test_skip_unless_attr(self, attr, expected_to_skip=True): + class TestFoo(test.BaseTestCase): + expected_attr = not expected_to_skip + + @decorators.skip_unless_attr(attr) + def test_foo(self): + pass + + t = TestFoo('test_foo') + if expected_to_skip: + self.assertRaises(testtools.TestCase.skipException, + t.test_foo()) + else: + try: + t.test_foo() + except Exception: + raise testtools.TestCase.failureException() + + def test_skip_attr_does_not_exist(self): + self._test_skip_unless_attr('unexpected_attr') + + def test_skip_attr_false(self): + self._test_skip_unless_attr('expected_attr') + + def test_no_skip_for_attr_exist_and_true(self): + self._test_skip_unless_attr('expected_attr', expected_to_skip=False) diff --git a/tempest/tests/lib/test_rest_client.py b/tempest/tests/lib/test_rest_client.py new file mode 100644 index 0000000000..6aff3050ee --- /dev/null +++ b/tempest/tests/lib/test_rest_client.py @@ -0,0 +1,1065 @@ +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import json + +import httplib2 +import jsonschema +from oslotest import mockpatch +import six + +from tempest.lib.common import rest_client +from tempest.lib import exceptions +from tempest.tests.lib import base +from tempest.tests.lib import fake_auth_provider +from tempest.tests.lib import fake_http + + +class BaseRestClientTestClass(base.TestCase): + + url = 'fake_endpoint' + + def setUp(self): + super(BaseRestClientTestClass, self).setUp() + self.fake_auth_provider = fake_auth_provider.FakeAuthProvider() + self.rest_client = rest_client.RestClient( + self.fake_auth_provider, None, None) + self.stubs.Set(httplib2.Http, 'request', self.fake_http.request) + self.useFixture(mockpatch.PatchObject(self.rest_client, + '_log_request')) + + +class TestRestClientHTTPMethods(BaseRestClientTestClass): + def setUp(self): + self.fake_http = fake_http.fake_httplib2() + super(TestRestClientHTTPMethods, self).setUp() + self.useFixture(mockpatch.PatchObject(self.rest_client, + '_error_checker')) + + def test_post(self): + __, return_dict = self.rest_client.post(self.url, {}, {}) + self.assertEqual('POST', return_dict['method']) + + def test_get(self): + __, return_dict = self.rest_client.get(self.url) + self.assertEqual('GET', return_dict['method']) + + def test_delete(self): + __, return_dict = self.rest_client.delete(self.url) + self.assertEqual('DELETE', return_dict['method']) + + def test_patch(self): + __, return_dict = self.rest_client.patch(self.url, {}, {}) + self.assertEqual('PATCH', return_dict['method']) + + def test_put(self): + __, return_dict = self.rest_client.put(self.url, {}, {}) + self.assertEqual('PUT', return_dict['method']) + + def test_head(self): + self.useFixture(mockpatch.PatchObject(self.rest_client, + 'response_checker')) + __, return_dict = self.rest_client.head(self.url) + self.assertEqual('HEAD', return_dict['method']) + + def test_copy(self): + __, return_dict = self.rest_client.copy(self.url) + self.assertEqual('COPY', return_dict['method']) + + +class TestRestClientNotFoundHandling(BaseRestClientTestClass): + def setUp(self): + self.fake_http = fake_http.fake_httplib2(404) + super(TestRestClientNotFoundHandling, self).setUp() + + def test_post(self): + self.assertRaises(exceptions.NotFound, self.rest_client.post, + self.url, {}, {}) + + +class TestRestClientHeadersJSON(TestRestClientHTTPMethods): + TYPE = "json" + + def _verify_headers(self, resp): + self.assertEqual(self.rest_client._get_type(), self.TYPE) + resp = dict((k.lower(), v) for k, v in six.iteritems(resp)) + self.assertEqual(self.header_value, resp['accept']) + self.assertEqual(self.header_value, resp['content-type']) + + def setUp(self): + super(TestRestClientHeadersJSON, self).setUp() + self.rest_client.TYPE = self.TYPE + self.header_value = 'application/%s' % self.rest_client._get_type() + + def test_post(self): + resp, __ = self.rest_client.post(self.url, {}) + self._verify_headers(resp) + + def test_get(self): + resp, __ = self.rest_client.get(self.url) + self._verify_headers(resp) + + def test_delete(self): + resp, __ = self.rest_client.delete(self.url) + self._verify_headers(resp) + + def test_patch(self): + resp, __ = self.rest_client.patch(self.url, {}) + self._verify_headers(resp) + + def test_put(self): + resp, __ = self.rest_client.put(self.url, {}) + self._verify_headers(resp) + + def test_head(self): + self.useFixture(mockpatch.PatchObject(self.rest_client, + 'response_checker')) + resp, __ = self.rest_client.head(self.url) + self._verify_headers(resp) + + def test_copy(self): + resp, __ = self.rest_client.copy(self.url) + self._verify_headers(resp) + + +class TestRestClientUpdateHeaders(BaseRestClientTestClass): + def setUp(self): + self.fake_http = fake_http.fake_httplib2() + super(TestRestClientUpdateHeaders, self).setUp() + self.useFixture(mockpatch.PatchObject(self.rest_client, + '_error_checker')) + self.headers = {'X-Configuration-Session': 'session_id'} + + def test_post_update_headers(self): + __, return_dict = self.rest_client.post(self.url, {}, + extra_headers=True, + headers=self.headers) + + self.assertDictContainsSubset( + {'X-Configuration-Session': 'session_id', + 'Content-Type': 'application/json', + 'Accept': 'application/json'}, + return_dict['headers'] + ) + + def test_get_update_headers(self): + __, return_dict = self.rest_client.get(self.url, + extra_headers=True, + headers=self.headers) + + self.assertDictContainsSubset( + {'X-Configuration-Session': 'session_id', + 'Content-Type': 'application/json', + 'Accept': 'application/json'}, + return_dict['headers'] + ) + + def test_delete_update_headers(self): + __, return_dict = self.rest_client.delete(self.url, + extra_headers=True, + headers=self.headers) + + self.assertDictContainsSubset( + {'X-Configuration-Session': 'session_id', + 'Content-Type': 'application/json', + 'Accept': 'application/json'}, + return_dict['headers'] + ) + + def test_patch_update_headers(self): + __, return_dict = self.rest_client.patch(self.url, {}, + extra_headers=True, + headers=self.headers) + + self.assertDictContainsSubset( + {'X-Configuration-Session': 'session_id', + 'Content-Type': 'application/json', + 'Accept': 'application/json'}, + return_dict['headers'] + ) + + def test_put_update_headers(self): + __, return_dict = self.rest_client.put(self.url, {}, + extra_headers=True, + headers=self.headers) + + self.assertDictContainsSubset( + {'X-Configuration-Session': 'session_id', + 'Content-Type': 'application/json', + 'Accept': 'application/json'}, + return_dict['headers'] + ) + + def test_head_update_headers(self): + self.useFixture(mockpatch.PatchObject(self.rest_client, + 'response_checker')) + + __, return_dict = self.rest_client.head(self.url, + extra_headers=True, + headers=self.headers) + + self.assertDictContainsSubset( + {'X-Configuration-Session': 'session_id', + 'Content-Type': 'application/json', + 'Accept': 'application/json'}, + return_dict['headers'] + ) + + def test_copy_update_headers(self): + __, return_dict = self.rest_client.copy(self.url, + extra_headers=True, + headers=self.headers) + + self.assertDictContainsSubset( + {'X-Configuration-Session': 'session_id', + 'Content-Type': 'application/json', + 'Accept': 'application/json'}, + return_dict['headers'] + ) + + +class TestRestClientParseRespJSON(BaseRestClientTestClass): + TYPE = "json" + + keys = ["fake_key1", "fake_key2"] + values = ["fake_value1", "fake_value2"] + item_expected = dict((key, value) for (key, value) in zip(keys, values)) + list_expected = {"body_list": [ + {keys[0]: values[0]}, + {keys[1]: values[1]}, + ]} + dict_expected = {"body_dict": { + keys[0]: values[0], + keys[1]: values[1], + }} + null_dict = {} + + def setUp(self): + self.fake_http = fake_http.fake_httplib2() + super(TestRestClientParseRespJSON, self).setUp() + self.rest_client.TYPE = self.TYPE + + def test_parse_resp_body_item(self): + body = self.rest_client._parse_resp(json.dumps(self.item_expected)) + self.assertEqual(self.item_expected, body) + + def test_parse_resp_body_list(self): + body = self.rest_client._parse_resp(json.dumps(self.list_expected)) + self.assertEqual(self.list_expected["body_list"], body) + + def test_parse_resp_body_dict(self): + body = self.rest_client._parse_resp(json.dumps(self.dict_expected)) + self.assertEqual(self.dict_expected["body_dict"], body) + + def test_parse_resp_two_top_keys(self): + dict_two_keys = self.dict_expected.copy() + dict_two_keys.update({"second_key": ""}) + body = self.rest_client._parse_resp(json.dumps(dict_two_keys)) + self.assertEqual(dict_two_keys, body) + + def test_parse_resp_one_top_key_without_list_or_dict(self): + data = {"one_top_key": "not_list_or_dict_value"} + body = self.rest_client._parse_resp(json.dumps(data)) + self.assertEqual(data, body) + + def test_parse_nullable_dict(self): + body = self.rest_client._parse_resp(json.dumps(self.null_dict)) + self.assertEqual(self.null_dict, body) + + +class TestRestClientErrorCheckerJSON(base.TestCase): + c_type = "application/json" + + def set_data(self, r_code, enc=None, r_body=None, absolute_limit=True): + if enc is None: + enc = self.c_type + resp_dict = {'status': r_code, 'content-type': enc} + resp_body = {'resp_body': 'fake_resp_body'} + + if absolute_limit is False: + resp_dict.update({'retry-after': 120}) + resp_body.update({'overLimit': {'message': 'fake_message'}}) + resp = httplib2.Response(resp_dict) + data = { + "method": "fake_method", + "url": "fake_url", + "headers": "fake_headers", + "body": "fake_body", + "resp": resp, + "resp_body": json.dumps(resp_body) + } + if r_body is not None: + data.update({"resp_body": r_body}) + return data + + def setUp(self): + super(TestRestClientErrorCheckerJSON, self).setUp() + self.rest_client = rest_client.RestClient( + fake_auth_provider.FakeAuthProvider(), None, None) + + def test_response_less_than_400(self): + self.rest_client._error_checker(**self.set_data("399")) + + def _test_error_checker(self, exception_type, data): + e = self.assertRaises(exception_type, + self.rest_client._error_checker, + **data) + self.assertEqual(e.resp, data['resp']) + self.assertTrue(hasattr(e, 'resp_body')) + return e + + def test_response_400(self): + self._test_error_checker(exceptions.BadRequest, self.set_data("400")) + + def test_response_401(self): + self._test_error_checker(exceptions.Unauthorized, self.set_data("401")) + + def test_response_403(self): + self._test_error_checker(exceptions.Forbidden, self.set_data("403")) + + def test_response_404(self): + self._test_error_checker(exceptions.NotFound, self.set_data("404")) + + def test_response_409(self): + self._test_error_checker(exceptions.Conflict, self.set_data("409")) + + def test_response_410(self): + self._test_error_checker(exceptions.Gone, self.set_data("410")) + + def test_response_413(self): + self._test_error_checker(exceptions.OverLimit, self.set_data("413")) + + def test_response_413_without_absolute_limit(self): + self._test_error_checker(exceptions.RateLimitExceeded, + self.set_data("413", absolute_limit=False)) + + def test_response_415(self): + self._test_error_checker(exceptions.InvalidContentType, + self.set_data("415")) + + def test_response_422(self): + self._test_error_checker(exceptions.UnprocessableEntity, + self.set_data("422")) + + def test_response_500_with_text(self): + # _parse_resp is expected to return 'str' + self._test_error_checker(exceptions.ServerFault, self.set_data("500")) + + def test_response_501_with_text(self): + self._test_error_checker(exceptions.NotImplemented, + self.set_data("501")) + + def test_response_400_with_dict(self): + r_body = '{"resp_body": {"err": "fake_resp_body"}}' + e = self._test_error_checker(exceptions.BadRequest, + self.set_data("400", r_body=r_body)) + + if self.c_type == 'application/json': + expected = {"err": "fake_resp_body"} + else: + expected = r_body + self.assertEqual(expected, e.resp_body) + + def test_response_401_with_dict(self): + r_body = '{"resp_body": {"err": "fake_resp_body"}}' + e = self._test_error_checker(exceptions.Unauthorized, + self.set_data("401", r_body=r_body)) + + if self.c_type == 'application/json': + expected = {"err": "fake_resp_body"} + else: + expected = r_body + self.assertEqual(expected, e.resp_body) + + def test_response_403_with_dict(self): + r_body = '{"resp_body": {"err": "fake_resp_body"}}' + e = self._test_error_checker(exceptions.Forbidden, + self.set_data("403", r_body=r_body)) + + if self.c_type == 'application/json': + expected = {"err": "fake_resp_body"} + else: + expected = r_body + self.assertEqual(expected, e.resp_body) + + def test_response_404_with_dict(self): + r_body = '{"resp_body": {"err": "fake_resp_body"}}' + e = self._test_error_checker(exceptions.NotFound, + self.set_data("404", r_body=r_body)) + + if self.c_type == 'application/json': + expected = {"err": "fake_resp_body"} + else: + expected = r_body + self.assertEqual(expected, e.resp_body) + + def test_response_404_with_invalid_dict(self): + r_body = '{"foo": "bar"]' + e = self._test_error_checker(exceptions.NotFound, + self.set_data("404", r_body=r_body)) + + expected = r_body + self.assertEqual(expected, e.resp_body) + + def test_response_410_with_dict(self): + r_body = '{"resp_body": {"err": "fake_resp_body"}}' + e = self._test_error_checker(exceptions.Gone, + self.set_data("410", r_body=r_body)) + + if self.c_type == 'application/json': + expected = {"err": "fake_resp_body"} + else: + expected = r_body + self.assertEqual(expected, e.resp_body) + + def test_response_410_with_invalid_dict(self): + r_body = '{"foo": "bar"]' + e = self._test_error_checker(exceptions.Gone, + self.set_data("410", r_body=r_body)) + + expected = r_body + self.assertEqual(expected, e.resp_body) + + def test_response_409_with_dict(self): + r_body = '{"resp_body": {"err": "fake_resp_body"}}' + e = self._test_error_checker(exceptions.Conflict, + self.set_data("409", r_body=r_body)) + + if self.c_type == 'application/json': + expected = {"err": "fake_resp_body"} + else: + expected = r_body + self.assertEqual(expected, e.resp_body) + + def test_response_500_with_dict(self): + r_body = '{"resp_body": {"err": "fake_resp_body"}}' + e = self._test_error_checker(exceptions.ServerFault, + self.set_data("500", r_body=r_body)) + + if self.c_type == 'application/json': + expected = {"err": "fake_resp_body"} + else: + expected = r_body + self.assertEqual(expected, e.resp_body) + + def test_response_501_with_dict(self): + r_body = '{"resp_body": {"err": "fake_resp_body"}}' + self._test_error_checker(exceptions.NotImplemented, + self.set_data("501", r_body=r_body)) + + def test_response_bigger_than_400(self): + # Any response code, that bigger than 400, and not in + # (401, 403, 404, 409, 413, 422, 500, 501) + self._test_error_checker(exceptions.UnexpectedResponseCode, + self.set_data("402")) + + +class TestRestClientErrorCheckerTEXT(TestRestClientErrorCheckerJSON): + c_type = "text/plain" + + def test_fake_content_type(self): + # This test is required only in one exemplar + # Any response code, that bigger than 400, and not in + # (401, 403, 404, 409, 413, 422, 500, 501) + self._test_error_checker(exceptions.UnexpectedContentType, + self.set_data("405", enc="fake_enc")) + + def test_response_413_without_absolute_limit(self): + # Skip this test because rest_client cannot get overLimit message + # from text body. + pass + + +class TestRestClientUtils(BaseRestClientTestClass): + + def _is_resource_deleted(self, resource_id): + if not isinstance(self.retry_pass, int): + return False + if self.retry_count >= self.retry_pass: + return True + self.retry_count = self.retry_count + 1 + return False + + def setUp(self): + self.fake_http = fake_http.fake_httplib2() + super(TestRestClientUtils, self).setUp() + self.retry_count = 0 + self.retry_pass = None + self.original_deleted_method = self.rest_client.is_resource_deleted + self.rest_client.is_resource_deleted = self._is_resource_deleted + + def test_wait_for_resource_deletion(self): + self.retry_pass = 2 + # Ensure timeout long enough for loop execution to hit retry count + self.rest_client.build_timeout = 500 + sleep_mock = self.patch('time.sleep') + self.rest_client.wait_for_resource_deletion('1234') + self.assertEqual(len(sleep_mock.mock_calls), 2) + + def test_wait_for_resource_deletion_not_deleted(self): + self.patch('time.sleep') + # Set timeout to be very quick to force exception faster + self.rest_client.build_timeout = 1 + self.assertRaises(exceptions.TimeoutException, + self.rest_client.wait_for_resource_deletion, + '1234') + + def test_wait_for_deletion_with_unimplemented_deleted_method(self): + self.rest_client.is_resource_deleted = self.original_deleted_method + self.assertRaises(NotImplementedError, + self.rest_client.wait_for_resource_deletion, + '1234') + + def test_get_versions(self): + self.rest_client._parse_resp = lambda x: [{'id': 'v1'}, {'id': 'v2'}] + actual_resp, actual_versions = self.rest_client.get_versions() + self.assertEqual(['v1', 'v2'], list(actual_versions)) + + def test__str__(self): + def get_token(): + return "deadbeef" + + self.fake_auth_provider.get_token = get_token + self.assertIsNotNone(str(self.rest_client)) + + +class TestProperties(BaseRestClientTestClass): + + def setUp(self): + self.fake_http = fake_http.fake_httplib2() + super(TestProperties, self).setUp() + creds_dict = { + 'username': 'test-user', + 'user_id': 'test-user_id', + 'tenant_name': 'test-tenant_name', + 'tenant_id': 'test-tenant_id', + 'password': 'test-password' + } + self.rest_client = rest_client.RestClient( + fake_auth_provider.FakeAuthProvider(creds_dict=creds_dict), + None, None) + + def test_properties(self): + self.assertEqual('test-user', self.rest_client.user) + self.assertEqual('test-user_id', self.rest_client.user_id) + self.assertEqual('test-tenant_name', self.rest_client.tenant_name) + self.assertEqual('test-tenant_id', self.rest_client.tenant_id) + self.assertEqual('test-password', self.rest_client.password) + + self.rest_client.api_version = 'v1' + expected = {'api_version': 'v1', + 'endpoint_type': 'publicURL', + 'region': None, + 'service': None, + 'skip_path': True} + self.rest_client.skip_path() + self.assertEqual(expected, self.rest_client.filters) + + self.rest_client.reset_path() + self.rest_client.api_version = 'v1' + expected = {'api_version': 'v1', + 'endpoint_type': 'publicURL', + 'region': None, + 'service': None} + self.assertEqual(expected, self.rest_client.filters) + + +class TestExpectedSuccess(BaseRestClientTestClass): + + def setUp(self): + self.fake_http = fake_http.fake_httplib2() + super(TestExpectedSuccess, self).setUp() + + def test_expected_succes_int_match(self): + expected_code = 202 + read_code = 202 + resp = self.rest_client.expected_success(expected_code, read_code) + # Assert None resp on success + self.assertFalse(resp) + + def test_expected_succes_int_no_match(self): + expected_code = 204 + read_code = 202 + self.assertRaises(exceptions.InvalidHttpSuccessCode, + self.rest_client.expected_success, + expected_code, read_code) + + def test_expected_succes_list_match(self): + expected_code = [202, 204] + read_code = 202 + resp = self.rest_client.expected_success(expected_code, read_code) + # Assert None resp on success + self.assertFalse(resp) + + def test_expected_succes_list_no_match(self): + expected_code = [202, 204] + read_code = 200 + self.assertRaises(exceptions.InvalidHttpSuccessCode, + self.rest_client.expected_success, + expected_code, read_code) + + def test_non_success_expected_int(self): + expected_code = 404 + read_code = 202 + self.assertRaises(AssertionError, self.rest_client.expected_success, + expected_code, read_code) + + def test_non_success_expected_list(self): + expected_code = [404, 202] + read_code = 202 + self.assertRaises(AssertionError, self.rest_client.expected_success, + expected_code, read_code) + + +class TestResponseBody(base.TestCase): + + def test_str(self): + response = {'status': 200} + body = {'key1': 'value1'} + actual = rest_client.ResponseBody(response, body) + self.assertEqual("response: %s\nBody: %s" % (response, body), + str(actual)) + + +class TestResponseBodyData(base.TestCase): + + def test_str(self): + response = {'status': 200} + data = 'data1' + actual = rest_client.ResponseBodyData(response, data) + self.assertEqual("response: %s\nBody: %s" % (response, data), + str(actual)) + + +class TestResponseBodyList(base.TestCase): + + def test_str(self): + response = {'status': 200} + body = ['value1', 'value2', 'value3'] + actual = rest_client.ResponseBodyList(response, body) + self.assertEqual("response: %s\nBody: %s" % (response, body), + str(actual)) + + +class TestJSONSchemaValidationBase(base.TestCase): + + class Response(dict): + + def __getattr__(self, attr): + return self[attr] + + def __setattr__(self, attr, value): + self[attr] = value + + def setUp(self): + super(TestJSONSchemaValidationBase, self).setUp() + self.fake_auth_provider = fake_auth_provider.FakeAuthProvider() + self.rest_client = rest_client.RestClient( + self.fake_auth_provider, None, None) + + def _test_validate_pass(self, schema, resp_body, status=200): + resp = self.Response() + resp.status = status + self.rest_client.validate_response(schema, resp, resp_body) + + def _test_validate_fail(self, schema, resp_body, status=200, + error_msg="HTTP response body is invalid"): + resp = self.Response() + resp.status = status + ex = self.assertRaises(exceptions.InvalidHTTPResponseBody, + self.rest_client.validate_response, + schema, resp, resp_body) + self.assertIn(error_msg, ex._error_string) + + +class TestRestClientJSONSchemaValidation(TestJSONSchemaValidationBase): + + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': 'integer', + }, + }, + 'required': ['foo'] + } + } + + def test_validate_pass_with_http_success_code(self): + body = {'foo': 12} + self._test_validate_pass(self.schema, body, status=200) + + def test_validate_pass_with_http_redirect_code(self): + body = {'foo': 12} + schema = copy.deepcopy(self.schema) + schema['status_code'] = 300 + self._test_validate_pass(schema, body, status=300) + + def test_validate_not_http_success_code(self): + schema = { + 'status_code': [200] + } + body = {} + self._test_validate_pass(schema, body, status=400) + + def test_validate_multiple_allowed_type(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': ['integer', 'string'], + }, + }, + 'required': ['foo'] + } + } + body = {'foo': 12} + self._test_validate_pass(schema, body) + body = {'foo': '12'} + self._test_validate_pass(schema, body) + + def test_validate_enable_additional_property_pass(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': {'type': 'integer'} + }, + 'additionalProperties': True, + 'required': ['foo'] + } + } + body = {'foo': 12, 'foo2': 'foo2value'} + self._test_validate_pass(schema, body) + + def test_validate_disable_additional_property_pass(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': {'type': 'integer'} + }, + 'additionalProperties': False, + 'required': ['foo'] + } + } + body = {'foo': 12} + self._test_validate_pass(schema, body) + + def test_validate_disable_additional_property_fail(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': {'type': 'integer'} + }, + 'additionalProperties': False, + 'required': ['foo'] + } + } + body = {'foo': 12, 'foo2': 'foo2value'} + self._test_validate_fail(schema, body) + + def test_validate_wrong_status_code(self): + schema = { + 'status_code': [202] + } + body = {} + resp = self.Response() + resp.status = 200 + ex = self.assertRaises(exceptions.InvalidHttpSuccessCode, + self.rest_client.validate_response, + schema, resp, body) + self.assertIn("Unexpected http success status code", ex._error_string) + + def test_validate_wrong_attribute_type(self): + body = {'foo': 1.2} + self._test_validate_fail(self.schema, body) + + def test_validate_unexpected_response_body(self): + schema = { + 'status_code': [200], + } + body = {'foo': 12} + self._test_validate_fail( + schema, body, + error_msg="HTTP response body should not exist") + + def test_validate_missing_response_body(self): + body = {} + self._test_validate_fail(self.schema, body) + + def test_validate_missing_required_attribute(self): + body = {'notfoo': 12} + self._test_validate_fail(self.schema, body) + + def test_validate_response_body_not_list(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'list_items': { + 'type': 'array', + 'items': {'foo': {'type': 'integer'}} + } + }, + 'required': ['list_items'], + } + } + body = {'foo': 12} + self._test_validate_fail(schema, body) + + def test_validate_response_body_list_pass(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'list_items': { + 'type': 'array', + 'items': {'foo': {'type': 'integer'}} + } + }, + 'required': ['list_items'], + } + } + body = {'list_items': [{'foo': 12}, {'foo': 10}]} + self._test_validate_pass(schema, body) + + +class TestRestClientJSONHeaderSchemaValidation(TestJSONSchemaValidationBase): + + schema = { + 'status_code': [200], + 'response_header': { + 'type': 'object', + 'properties': { + 'foo': {'type': 'integer'} + }, + 'required': ['foo'] + } + } + + def test_validate_header_schema_pass(self): + resp_body = {} + resp = self.Response() + resp.status = 200 + resp.foo = 12 + self.rest_client.validate_response(self.schema, resp, resp_body) + + def test_validate_header_schema_fail(self): + resp_body = {} + resp = self.Response() + resp.status = 200 + resp.foo = 1.2 + ex = self.assertRaises(exceptions.InvalidHTTPResponseHeader, + self.rest_client.validate_response, + self.schema, resp, resp_body) + self.assertIn("HTTP response header is invalid", ex._error_string) + + +class TestRestClientJSONSchemaFormatValidation(TestJSONSchemaValidationBase): + + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': 'string', + 'format': 'email' + } + }, + 'required': ['foo'] + } + } + + def test_validate_format_pass(self): + body = {'foo': 'example@example.com'} + self._test_validate_pass(self.schema, body) + + def test_validate_format_fail(self): + body = {'foo': 'wrong_email'} + self._test_validate_fail(self.schema, body) + + def test_validate_formats_in_oneOf_pass(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': 'string', + 'oneOf': [ + {'format': 'ipv4'}, + {'format': 'ipv6'} + ] + } + }, + 'required': ['foo'] + } + } + body = {'foo': '10.0.0.0'} + self._test_validate_pass(schema, body) + body = {'foo': 'FE80:0000:0000:0000:0202:B3FF:FE1E:8329'} + self._test_validate_pass(schema, body) + + def test_validate_formats_in_oneOf_fail_both_match(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': 'string', + 'oneOf': [ + {'format': 'ipv4'}, + {'format': 'ipv4'} + ] + } + }, + 'required': ['foo'] + } + } + body = {'foo': '10.0.0.0'} + self._test_validate_fail(schema, body) + + def test_validate_formats_in_oneOf_fail_no_match(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': 'string', + 'oneOf': [ + {'format': 'ipv4'}, + {'format': 'ipv6'} + ] + } + }, + 'required': ['foo'] + } + } + body = {'foo': 'wrong_ip_format'} + self._test_validate_fail(schema, body) + + def test_validate_formats_in_anyOf_pass(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': 'string', + 'anyOf': [ + {'format': 'ipv4'}, + {'format': 'ipv6'} + ] + } + }, + 'required': ['foo'] + } + } + body = {'foo': '10.0.0.0'} + self._test_validate_pass(schema, body) + body = {'foo': 'FE80:0000:0000:0000:0202:B3FF:FE1E:8329'} + self._test_validate_pass(schema, body) + + def test_validate_formats_in_anyOf_pass_both_match(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': 'string', + 'anyOf': [ + {'format': 'ipv4'}, + {'format': 'ipv4'} + ] + } + }, + 'required': ['foo'] + } + } + body = {'foo': '10.0.0.0'} + self._test_validate_pass(schema, body) + + def test_validate_formats_in_anyOf_fail_no_match(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': 'string', + 'anyOf': [ + {'format': 'ipv4'}, + {'format': 'ipv6'} + ] + } + }, + 'required': ['foo'] + } + } + body = {'foo': 'wrong_ip_format'} + self._test_validate_fail(schema, body) + + def test_validate_formats_pass_for_unknow_format(self): + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': { + 'type': 'string', + 'format': 'UNKNOWN' + } + }, + 'required': ['foo'] + } + } + body = {'foo': 'example@example.com'} + self._test_validate_pass(schema, body) + + +class TestRestClientJSONSchemaValidatorVersion(TestJSONSchemaValidationBase): + + schema = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'foo': {'type': 'string'} + } + } + } + + def test_current_json_schema_validator_version(self): + with mockpatch.PatchObject(jsonschema.Draft4Validator, + "check_schema") as chk_schema: + body = {'foo': 'test'} + self._test_validate_pass(self.schema, body) + chk_schema.mock.assert_called_once_with( + self.schema['response_body']) diff --git a/tempest/tests/lib/test_ssh.py b/tempest/tests/lib/test_ssh.py new file mode 100644 index 0000000000..7a4fc09aaf --- /dev/null +++ b/tempest/tests/lib/test_ssh.py @@ -0,0 +1,253 @@ +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from io import StringIO +import socket +import time + +import mock +import six +import testtools + +from tempest.lib.common import ssh +from tempest.lib import exceptions +from tempest.tests.lib import base + + +class TestSshClient(base.TestCase): + + SELECT_POLLIN = 1 + + @mock.patch('paramiko.RSAKey.from_private_key') + @mock.patch('six.StringIO') + def test_pkey_calls_paramiko_RSAKey(self, cs_mock, rsa_mock): + cs_mock.return_value = mock.sentinel.csio + pkey = 'mykey' + ssh.Client('localhost', 'root', pkey=pkey) + rsa_mock.assert_called_once_with(mock.sentinel.csio) + cs_mock.assert_called_once_with('mykey') + rsa_mock.reset_mock() + cs_mock.reset_mock() + pkey = mock.sentinel.pkey + # Shouldn't call out to load a file from RSAKey, since + # a sentinel isn't a basestring... + ssh.Client('localhost', 'root', pkey=pkey) + self.assertEqual(0, rsa_mock.call_count) + self.assertEqual(0, cs_mock.call_count) + + def _set_ssh_connection_mocks(self): + client_mock = mock.MagicMock() + client_mock.connect.return_value = True + return (self.patch('paramiko.SSHClient'), + self.patch('paramiko.AutoAddPolicy'), + client_mock) + + def test_get_ssh_connection(self): + c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks() + s_mock = self.patch('time.sleep') + + c_mock.return_value = client_mock + aa_mock.return_value = mock.sentinel.aa + + # Test normal case for successful connection on first try + client = ssh.Client('localhost', 'root', timeout=2) + client._get_ssh_connection(sleep=1) + + aa_mock.assert_called_once_with() + client_mock.set_missing_host_key_policy.assert_called_once_with( + mock.sentinel.aa) + expected_connect = [mock.call( + 'localhost', + username='root', + pkey=None, + key_filename=None, + look_for_keys=False, + timeout=10.0, + password=None + )] + self.assertEqual(expected_connect, client_mock.connect.mock_calls) + self.assertEqual(0, s_mock.call_count) + + def test_get_ssh_connection_two_attemps(self): + c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks() + + c_mock.return_value = client_mock + client_mock.connect.side_effect = [ + socket.error, + mock.MagicMock() + ] + + client = ssh.Client('localhost', 'root', timeout=1) + start_time = int(time.time()) + client._get_ssh_connection(sleep=1) + end_time = int(time.time()) + self.assertLess((end_time - start_time), 4) + self.assertGreater((end_time - start_time), 1) + + def test_get_ssh_connection_timeout(self): + c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks() + + c_mock.return_value = client_mock + client_mock.connect.side_effect = [ + socket.error, + socket.error, + socket.error, + ] + + client = ssh.Client('localhost', 'root', timeout=2) + start_time = int(time.time()) + with testtools.ExpectedException(exceptions.SSHTimeout): + client._get_ssh_connection() + end_time = int(time.time()) + self.assertLess((end_time - start_time), 5) + self.assertGreaterEqual((end_time - start_time), 2) + + @mock.patch('select.POLLIN', SELECT_POLLIN, create=True) + def test_timeout_in_exec_command(self): + chan_mock, poll_mock, _ = self._set_mocks_for_select([0, 0, 0], True) + + # Test for a timeout condition immediately raised + client = ssh.Client('localhost', 'root', timeout=2) + with testtools.ExpectedException(exceptions.TimeoutException): + client.exec_command("test") + + chan_mock.fileno.assert_called_once_with() + chan_mock.exec_command.assert_called_once_with("test") + chan_mock.shutdown_write.assert_called_once_with() + + poll_mock.register.assert_called_once_with( + chan_mock, self.SELECT_POLLIN) + poll_mock.poll.assert_called_once_with(10) + + @mock.patch('select.POLLIN', SELECT_POLLIN, create=True) + def test_exec_command(self): + chan_mock, poll_mock, select_mock = ( + self._set_mocks_for_select([[1, 0, 0]], True)) + closed_prop = mock.PropertyMock(return_value=True) + type(chan_mock).closed = closed_prop + + chan_mock.recv_exit_status.return_value = 0 + chan_mock.recv.return_value = b'' + chan_mock.recv_stderr.return_value = b'' + + client = ssh.Client('localhost', 'root', timeout=2) + client.exec_command("test") + + chan_mock.fileno.assert_called_once_with() + chan_mock.exec_command.assert_called_once_with("test") + chan_mock.shutdown_write.assert_called_once_with() + + select_mock.assert_called_once_with() + poll_mock.register.assert_called_once_with( + chan_mock, self.SELECT_POLLIN) + poll_mock.poll.assert_called_once_with(10) + chan_mock.recv_ready.assert_called_once_with() + chan_mock.recv.assert_called_once_with(1024) + chan_mock.recv_stderr_ready.assert_called_once_with() + chan_mock.recv_stderr.assert_called_once_with(1024) + chan_mock.recv_exit_status.assert_called_once_with() + closed_prop.assert_called_once_with() + + def _set_mocks_for_select(self, poll_data, ito_value=False): + gsc_mock = self.patch('tempest.lib.common.ssh.Client.' + '_get_ssh_connection') + ito_mock = self.patch('tempest.lib.common.ssh.Client._is_timed_out') + csp_mock = self.patch( + 'tempest.lib.common.ssh.Client._can_system_poll') + csp_mock.return_value = True + + select_mock = self.patch('select.poll', create=True) + client_mock = mock.MagicMock() + tran_mock = mock.MagicMock() + chan_mock = mock.MagicMock() + poll_mock = mock.MagicMock() + + select_mock.return_value = poll_mock + gsc_mock.return_value = client_mock + ito_mock.return_value = ito_value + client_mock.get_transport.return_value = tran_mock + tran_mock.open_session.return_value = chan_mock + if isinstance(poll_data[0], list): + poll_mock.poll.side_effect = poll_data + else: + poll_mock.poll.return_value = poll_data + + return chan_mock, poll_mock, select_mock + + _utf8_string = six.unichr(1071) + _utf8_bytes = _utf8_string.encode("utf-8") + + @mock.patch('select.POLLIN', SELECT_POLLIN, create=True) + def test_exec_good_command_output(self): + chan_mock, poll_mock, _ = self._set_mocks_for_select([1, 0, 0]) + closed_prop = mock.PropertyMock(return_value=True) + type(chan_mock).closed = closed_prop + + chan_mock.recv_exit_status.return_value = 0 + chan_mock.recv.side_effect = [self._utf8_bytes[0:1], + self._utf8_bytes[1:], b'R', b''] + chan_mock.recv_stderr.return_value = b'' + + client = ssh.Client('localhost', 'root', timeout=2) + out_data = client.exec_command("test") + self.assertEqual(self._utf8_string + 'R', out_data) + + @mock.patch('select.POLLIN', SELECT_POLLIN, create=True) + def test_exec_bad_command_output(self): + chan_mock, poll_mock, _ = self._set_mocks_for_select([1, 0, 0]) + closed_prop = mock.PropertyMock(return_value=True) + type(chan_mock).closed = closed_prop + + chan_mock.recv_exit_status.return_value = 1 + chan_mock.recv.return_value = b'' + chan_mock.recv_stderr.side_effect = [b'R', self._utf8_bytes[0:1], + self._utf8_bytes[1:], b''] + + client = ssh.Client('localhost', 'root', timeout=2) + exc = self.assertRaises(exceptions.SSHExecCommandFailed, + client.exec_command, "test") + self.assertIn('R' + self._utf8_string, six.text_type(exc)) + + def test_exec_command_no_select(self): + gsc_mock = self.patch('tempest.lib.common.ssh.Client.' + '_get_ssh_connection') + csp_mock = self.patch( + 'tempest.lib.common.ssh.Client._can_system_poll') + csp_mock.return_value = False + + select_mock = self.patch('select.poll', create=True) + client_mock = mock.MagicMock() + tran_mock = mock.MagicMock() + chan_mock = mock.MagicMock() + + # Test for proper reading of STDOUT and STDERROR + + gsc_mock.return_value = client_mock + client_mock.get_transport.return_value = tran_mock + tran_mock.open_session.return_value = chan_mock + chan_mock.recv_exit_status.return_value = 0 + + std_out_mock = mock.MagicMock(StringIO) + std_err_mock = mock.MagicMock(StringIO) + chan_mock.makefile.return_value = std_out_mock + chan_mock.makefile_stderr.return_value = std_err_mock + + client = ssh.Client('localhost', 'root', timeout=2) + client.exec_command("test") + + chan_mock.makefile.assert_called_once_with('rb', 1024) + chan_mock.makefile_stderr.assert_called_once_with('rb', 1024) + std_out_mock.read.assert_called_once_with() + std_err_mock.read.assert_called_once_with() + self.assertFalse(select_mock.called) diff --git a/tempest/tests/lib/test_tempest_lib.py b/tempest/tests/lib/test_tempest_lib.py new file mode 100644 index 0000000000..9731e967b2 --- /dev/null +++ b/tempest/tests/lib/test_tempest_lib.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +test_tempest.lib +---------------------------------- + +Tests for `tempest.lib` module. +""" + +from tempest.tests.lib import base + + +class TestTempest_lib(base.TestCase): + + def test_something(self): + pass