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
This commit is contained in:
Matthew Treinish 2016-02-23 11:43:20 -05:00
parent 84d06eca2f
commit 9e26ca8760
No known key found for this signature in database
GPG Key ID: FD12A0F214C9E177
170 changed files with 17595 additions and 4 deletions

View File

@ -69,10 +69,12 @@ def no_setup_teardown_class_for_tests(physical_line, filename):
if pep8.noqa(physical_line): if pep8.noqa(physical_line):
return return
if 'tempest/test.py' not in filename: if 'tempest/test.py' in filename or 'tempest/lib/' in filename:
if SETUP_TEARDOWN_CLASS_DEFINITION.match(physical_line): return
return (physical_line.find('def'),
"T105: (setUp|tearDown)Class can not be used in tests") 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): def no_vi_headers(physical_line, line_number, lines):

0
tempest/lib/__init__.py Normal file
View File

View File

View File

@ -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]
}

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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']
}
}

View File

@ -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]
}

View File

@ -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]
}

View File

@ -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']
}
}

View File

@ -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'}
}
}
}

View File

@ -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'],
}
}

View File

@ -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']
}
}

View File

@ -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'.

View File

@ -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']
}
}

View File

@ -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']
}
}

View File

@ -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]
}

View File

@ -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']
}
}

View File

@ -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']
}
}

View File

@ -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']
}
}

View File

@ -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'
}
}

View File

@ -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']

View File

@ -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]
}

View File

@ -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']
}
}

View File

@ -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]
}

View File

@ -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]
}

View File

@ -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']
}
}

View File

@ -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]
}

View File

@ -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']
}
}

View File

@ -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']
}
}

View File

@ -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
}
}

View File

@ -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]
}

676
tempest/lib/auth.py Normal file
View File

@ -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)}

71
tempest/lib/base.py Normal file
View File

@ -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))

View File

410
tempest/lib/cli/base.py Normal file
View File

@ -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]))

View File

@ -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

View File

358
tempest/lib/cmd/check_uuid.py Executable file
View File

@ -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()

162
tempest/lib/cmd/skip_tracker.py Executable file
View File

@ -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()

View File

View File

@ -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)

View File

@ -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 "<BinaryData: removed>"
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'] = '<omitted>'
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)) == "<type 'instance'>":
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)

174
tempest/lib/common/ssh.py Normal file
View File

@ -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()

View File

View File

@ -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
'<prefix>-<random number>-<name>-<random number>'.
(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
'<random upper letter>-<random number>-<random special character>
-<random ascii letters or digit characters or special symbols>'
(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-<random number>.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)

View File

@ -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

80
tempest/lib/decorators.py Normal file
View File

@ -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

205
tempest/lib/exceptions.py Normal file
View File

@ -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")

View File

View File

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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='<omitted>', 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)

View File

@ -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='<omitted>', 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)

View File

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

Some files were not shown because too many files have changed in this diff Show More