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 0000000..023bd52 --- /dev/null +++ b/tempest_lib/api_schema/response/compute/v2_1/servers.py @@ -0,0 +1,549 @@ +# 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] +} + +get_volume_attachment = copy.deepcopy(attach_volume) +get_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_get_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' + +get_instance_action = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'instanceAction': instance_actions_with_events + }, + 'additionalProperties': False, + 'required': ['instanceAction'] + } +} + +get_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_get_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) 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 0000000..4f961aa --- /dev/null +++ b/tempest_lib/services/compute/server_groups_client.py @@ -0,0 +1,57 @@ +# 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 + + name : Name of the server-group + policies : List of the policies - affinity/anti-affinity) + + """ + 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_get_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 get_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_get_server_group, resp, body) + return rest_client.ResponseBody(resp, body) diff --git a/tempest_lib/tests/services/compute/test_server_groups_client.py b/tempest_lib/tests/services/compute/test_server_groups_client.py new file mode 100644 index 0000000..13b1ca4 --- /dev/null +++ b/tempest_lib/tests/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_lib.tests import fake_auth_provider + +from tempest_lib.services.compute import server_groups_client +from tempest_lib.tests.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_get_server_group(self, bytes_body=False): + expected = {"server_group": TestServerGroupsClient.server_group} + self.check_service_client_function( + self.client.get_server_group, + 'tempest_lib.common.rest_client.RestClient.get', + expected, bytes_body, + server_group_id='5bbcc3c4-1da2-4437-a48a-66f15b1b13f9') + + def test_get_server_group_str_body(self): + self._test_get_server_group(bytes_body=False) + + def test_get_server_group_byte_body(self): + self._test_get_server_group(bytes_body=True)