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:
parent
84d06eca2f
commit
9e26ca8760
|
@ -69,7 +69,9 @@ 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:
|
||||||
|
return
|
||||||
|
|
||||||
if SETUP_TEARDOWN_CLASS_DEFINITION.match(physical_line):
|
if SETUP_TEARDOWN_CLASS_DEFINITION.match(physical_line):
|
||||||
return (physical_line.find('def'),
|
return (physical_line.find('def'),
|
||||||
"T105: (setUp|tearDown)Class can not be used in tests")
|
"T105: (setUp|tearDown)Class can not be used in tests")
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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')
|
|
@ -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)
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'],
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'.
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']
|
|
@ -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]
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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)}
|
|
@ -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))
|
|
@ -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]))
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
Loading…
Reference in New Issue