Initial Commit

This commit is contained in:
samu4924 2013-03-30 14:48:37 -05:00
commit 481102076c
125 changed files with 8560 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
*.py[co]
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
var
sdist
develop-eggs
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
# IDE Project Files
*.project
*.pydev*

0
HISTORY.rst Normal file
View File

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
# Copyright 2013 Rackspace
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

79
README.md Normal file
View File

@ -0,0 +1,79 @@
CloudCAFE, An CAFE Implementation for OpenStack
================================
<pre>
_ _ _
( ` )_
( ) `) _
(____(__.___`)__)
( (
) )
.........
| |___
| |_ |
| :-) |_| |
| |___|
|_______|
=== CloudCAFE ===
= An Open CAFE Implementation =
</pre>
CloudCAFE is an implementation of the [Open CAFE Framework](https://github.com/stackforge) specifically designed to test deployed
versions of [OpenStack](http://http://www.openstack.org/). It is built using the [Open CAFE Core](https://github.com/stackforge).
Supported Operating Systems
---------------------------
CloudCAFE has been developed primarily in Linux and MAC environments, however, it supports installation and
execution on Windows
Installation
------------
CloudCAFE can be [installed with pip](https://pypi.python.org/pypi/pip) from the git repository after it is cloned to a local machine.
* First follow the README instructions to install [Open CAFE Core](https://github.com/stackforge)
* Clone this repository to your local machine
* CD to the root directory in your cloned repository.
* Run "pip install . --upgrade" and pip will auto install all other dependencies.
Configuration
--------------
CloudCAFE works in tandem with the [Open CAFE Core](https://github.com/stackforge) cafe-runner. This installation of CloudCAFE includes a reference
configuration for each of the CloudCAFE supported OpenStack products. Configurations will be installed to: <USER_HOME>/.cloudcafe/configs/<PRODUCT>
To use CloudCAFE you **will need to create/install your own configurations** based on the reference configs pointing to your deployment of OpenStack.
At this stage you will have the Open CAFE Core engine and the CloudCAFE Framework implementation. From this point you are ready to:
1) Write entirely new tests using the CloudCAFE Framework
or
2) Install the [CloudRoast Test Repository](https://github.com/stackforge), an Open Source body of OpenStack automated tests written with CloudCAFE
that can be executed or extended.
Logging
-------
CloudCAFE leverages the logging capabilities of the CAFE Core engine. If tests are executed with the built-in cafe-runner, runtime logs will be output
to <USER_HOME>/.cloudcafe/logs/<PRODUCT>/<CONFIGURATION>/<TIME_STAMP>. In addition, tests built from the built-in CAFE unittest driver will generate
csv statistics files in <USER_HOME>/.cloudcafe/logs/<PRODUCT>/<CONFIGURATION>/statistics for each and ever execution of each and every test case that
provides metrics of execution over time for elapsed time, pass/fail rates, etc...
Basic CloudCAFE Package Anatomy
-------------------------------
Below is a short description of the top level CloudCAFE Packages.
##cloudcafe
This is the root package for all things CloudCAFE.
##common
Contains modules that extend the CAFE Core engine specific to OpenStack. This is the primary namespace for tools, data generators, common
reporting classes, etc...
##identity
OpenStack Identity Service plug-in based on CAFE Core extensions.
##compute
OpenStack Compute plug-in based on CAFE Core extensions.
##blockstorage
OpenStack Block Storage plug-in based on CAFE Core extensions.
##objectstorage
OpenStack Object Storage plug-in based on CAFE Core extensions.

16
__init__.py Normal file
View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

22
cloudcafe/__init__.py Normal file
View File

@ -0,0 +1,22 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
__title__ = 'cloudcafe'
__version__ = '0.0.1'
#__build__ = 0x010100
__author__ = 'Rackspace Cloud QE'
__license__ = 'Internal Only'
__copyright__ = 'Copyright 2013 Rackspace Inc.'

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,30 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cloudcafe.common.models.configuration import ConfigSectionInterface
class BlockStorageConfig(ConfigSectionInterface):
SECTION_NAME = 'blockstorage'
@property
def identity_service_name(self):
return self.get('identity_service_name')
@property
def region(self):
return self.get('region')

View File

@ -0,0 +1,26 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.behaviors import BaseBehavior, behavior
from cloudcafe.compute.servers_api.client import ServersClient
class ComputeBlockStorageBehaviors(BaseBehavior):
@behavior(ServersClient)
def attach_volume_to_server(*args, **kwargs):
pass

View File

@ -0,0 +1,37 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.provider import BaseProvider
from cloudcafe.blockstorage.config import BlockStorageConfig
from cloudcafe.blockstorage.volumes_api.provider import VolumesProvider
class BlockStorageProvider(BaseProvider):
def get_volumes_provider(self):
# NEED TO IMPORT IDENTITY AND GET THESE THINGS
blockstorage_config = BlockStorageConfig()
blockstorage_service_name = blockstorage_config.identity_service_name
blockstorage_region = blockstorage_config.region
auth_token = '924ur802ur08j2f0984'
volumes_url = 'http://volumes_url'
tenant_id = '234234'
return VolumesProvider(volumes_url, auth_token, tenant_id)

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,374 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 time import time
from cafe.engine.behaviors import BaseBehavior, behavior
from cloudcafe.blockstorage.volumes_api.volumes_client import VolumesClient
from cloudcafe.blockstorage.volumes_api.config import VolumesAPIConfig
class BehaviorResponse(object):
'''An object to represent the result of behavior.
@ivar response: Last response returned from last client call
@ivar ok: Represents the success state of the behavior call
@type ok:C{bool}
@ivar entity: Data model created via behavior calls, if applicable
@TODO: This should probably be moved to the base behavior module,
or even into the engine's models
'''
def __init__(self):
self.response = None
self.ok = False
self.entity = None
class VolumesAPI_Behaviors(BaseBehavior):
def __init__(self, volumes_client=None):
self._client = volumes_client
self.config = VolumesAPIConfig()
@behavior(VolumesClient)
def wait_for_volume_status(
self, volume_id, expected_status, timeout, wait_period=None):
''' Waits for a specific status and returns a BehaviorResponse object
when that status is observed.
Note: Shouldn't be used for transient statuses like 'deleting'.
'''
wait_period = wait_period or self.config.volume_status_poll_frequency
behavior_response = BehaviorResponse()
end_time = time() + timeout
while time() < end_time:
resp = self._client.get_volume_info(volume_id=volume_id)
behavior_response.response = resp
behavior_response.entity = resp.entity
if not resp.ok:
behavior_response.ok = False
self._log.error(
"get_volume_info() call failed with status_code {0} while "
"waiting for volume status".format(resp.status_code))
break
if resp.entity is None:
behavior_response.ok = False
self._log.error(
"get_volume_info() response body did not deserialize as "
"expected")
break
if resp.entity.status == expected_status:
behavior_response.ok = True
self._log.info('Volume status "{0}" observed'.format(
expected_status))
break
else:
behavior_response.ok = False
self._log.info(
"wait_for_volume_status() ran for {0} seconds and did not "
"observe the volume achieving the {1} status.".format(
timeout, expected_status))
return behavior_response
@behavior(VolumesClient)
def wait_for_snapshot_status(
self, snapshot_id, expected_status, timeout, wait_period=None):
''' Waits for a specific status and returns a BehaviorResponse object
when that status is observed.
Note: Shouldn't be used for transient statuses like 'deleting'.
'''
wait_period = wait_period or self.config.snapshot_status_poll_frequency
behavior_response = BehaviorResponse()
end_time = time() + timeout
while time() < end_time:
resp = self._client.get_snapshot_info(snapshot_id=snapshot_id)
behavior_response.response = resp
behavior_response.entity = resp.entity
if not resp.ok:
behavior_response.ok = False
self._log.error(
"get_snapshot_info() call failed with status_code {0} "
"while waiting for snapshot status".format(
resp.status_code))
break
if resp.entity is None:
behavior_response.ok = False
self._log.error(
"get_snapshot_info() response body did not deserialize as "
"expected")
break
if resp.entity.status == expected_status:
behavior_response.ok = True
self._log.info('Snapshot status "{0}" observed'.format(
expected_status))
break
else:
behavior_response.ok = False
self._log.error(
"wait_for_snapshot_status() ran for {0} seconds and did not "
"observe the snapshot achieving the '{1}' status.".format(
timeout, expected_status))
return behavior_response
@behavior(VolumesClient)
def create_available_volume(
self, display_name, size, volume_type, display_description=None,
metadata=None, availability_zone=None, timeout=None,
wait_period=None):
expected_status = 'available'
metadata = metadata or {}
timeout = timeout or self.config.volume_create_timeout
behavior_response = BehaviorResponse()
self._log.info("create_available_volume() is creating a volume")
resp = self._client.create_volume(
display_name=display_name, size=size, volume_type=volume_type,
display_description=display_description, metadata=metadata,
availability_zone=availability_zone)
behavior_response.response = resp
behavior_response.entity = resp.entity
if not resp.ok:
behavior_response.ok = False
self._log.error(
"create_available_volume() call failed with status_code {0} "
"while attempting to create a volume".format(resp.status_code))
if resp.entity is None:
behavior_response.ok = False
self._log.error(
"create_available_volume() response body did not deserialize "
"as expected")
#Bail on fail
if not behavior_response.ok:
return behavior_response
# Wait for expected_status on success
wait_resp = self.wait_for_volume_status(
resp.entity.id_, expected_status, timeout, wait_period)
if not wait_resp.ok:
behavior_response.ok = False
self._log.error(
"Something went wrong while create_available_volume() was "
"waiting for the volume to reach the '{0}' status")
else:
behavior_response.ok = True
return behavior_response
@behavior(VolumesClient)
def create_available_snapshot(
self, volume_id, display_name=None, display_description=None,
force_create='False', name=None, timeout=None, wait_period=None):
expected_status = 'available'
timeout = timeout or self.config.snapshot_create_timeout
behavior_response = BehaviorResponse()
self._log.info("create_available_snapshot() is creating a snapshot")
resp = self._client.create_snapshot(
volume_id, display_name=display_name,
display_description=display_description,
force_create=force_create, name=name)
behavior_response.response = resp
behavior_response.entity = resp.entity
if not resp.ok:
behavior_response.ok = False
self._log.error(
"create_available_volume() call failed with status_code {0} "
"while attempting to create a volume".format(resp.status_code))
if resp.entity is None:
behavior_response.ok = False
self._log.error(
"create_available_volume() response body did not deserialize "
"as expected")
# Bail on fail
if not behavior_response.ok:
return behavior_response
# Wait for expected_status on success
wait_resp = self.wait_for_volume_status(
resp.entity.id_, expected_status, timeout, wait_period)
if not wait_resp.ok:
behavior_response.ok = False
self._log.error(
"Something went wrong while create_available_volume() was "
"waiting for the volume to reach the '{0}' status")
else:
behavior_response.ok = True
return behavior_response
@behavior(VolumesClient)
def list_volume_snapshots(self, volume_id):
behavior_response = BehaviorResponse()
# List all snapshots
resp = self._client.list_all_snapshots_info()
behavior_response.response = resp
behavior_response.entity = resp.entity
if not resp.ok:
behavior_response.ok = False
self._log.error(
"list_volume_snapshots() failed to get a list of all snapshots"
"due to a '{0}' response from list_all_snapshots_info()"
.format(resp.status_code))
return behavior_response
if resp.entity is None:
behavior_response.ok = False
self._log.error(
"list_all_snapshots_info() response body did not deserialize "
"as expected")
return behavior_response
# Expects an entity of type VolumeSnapshotList
volume_snapshots = [s for s in resp.entity if s.volume_id == volume_id]
behavior_response.entity = volume_snapshots
return behavior_response
@behavior(VolumesClient)
def delete_volume_confirmed(
self, volume_id, size=None, timeout=None, wait_period=None):
if size is not None:
if self.config.volume_delete_wait_per_gig is not None:
wait_per_gig = self.config.volume_snapshot_delete_wait_per_gig
timeout = timeout or size * wait_per_gig
if self.config.volume_delete_min_timeout is not None:
min_timeout = self.config.volume_snapshot_delete_min_timeout
timeout = timeout if timeout > min_timeout else min_timeout
if self.config.volume_delete_max_timeout is not None:
max_timeout = self.config.volume_snapshot_delete_max_timeout
timeout = timeout if timeout < max_timeout else max_timeout
end = time() + timeout
while time() < end:
#issue DELETE request on volume
behavior_response = BehaviorResponse()
resp = self._client.delete_volume(volume_id)
behavior_response.response = resp
behavior_response.entity = resp.entity
if not resp.ok:
behavior_response.ok = False
self._log.error(
"delete_volume_confirmed() call to delete_volume() failed "
"with a '{0}'".format(resp.status_code))
return behavior_response
#Poll volume status to make sure it deleted properly
status_resp = self._client.get_volume_info(volume_id)
if status_resp.status_code == 404:
behavior_response.ok = True
self._log.info(
"Status request on volume {0} returned 404, volume delete"
"confirmed".format(volume_id))
break
if (not status_resp.ok) and (status_resp.status_code != 404):
behavior_response.ok = False
self._log.error(
"Status request on volume {0} failed with a {0}".format(
volume_id))
break
else:
behavior_response.ok = False
self._log.error(
"delete_volume_confirmed() was unable to verify the volume"
"delete withing the alloted {0} second timeout".format())
return behavior_response
@behavior(VolumesClient)
def delete_snapshot_confirmed(
self, snapshot_id, size=None, timeout=None, wait_period=None):
if size is not None:
if self.config.volume_snapshot_delete_wait_per_gig is not None:
wait_per_gig = self.config.volume_snapshot_delete_wait_per_gig
timeout = timeout or size * wait_per_gig
if self.config.volume_snapshot_delete_min_timeout is not None:
min_timeout = self.config.volume_snapshot_delete_min_timeout
timeout = timeout if timeout > min_timeout else min_timeout
if self.config.volume_snapshot_delete_max_timeout is not None:
max_timeout = self.config.volume_snapshot_delete_max_timeout
timeout = timeout if timeout < max_timeout else max_timeout
end = time() + timeout
while time() < end:
# issue DELETE request on volume snapshot
behavior_response = BehaviorResponse()
resp = self._client.delete_snapshot(snapshot_id)
behavior_response.response = resp
behavior_response.entity = resp.entity
if not resp.ok:
behavior_response.ok = False
self._log.error(
"delete_snapshot_confirmed() call to delete_snapshot()"
"failed with a '{0}'".format(resp.status_code))
return behavior_response
# Poll snapshot status to make sure it deleted properly
status_resp = self._client.get_snapshot_info(snapshot_id)
if status_resp.status_code == 404:
behavior_response.ok = True
self._log.info(
"Status request on snapshot {0} returned 404, snapshot"
"delete confirmed".format(snapshot_id))
break
if (not status_resp.ok) and (status_resp.status_code != 404):
behavior_response.ok = False
self._log.error(
"Status request on snapshot {0} failed with a {0}".format(
snapshot_id))
break
else:
behavior_response.ok = False
self._log.error(
"delete_snapshot_confirmed() was unable to verify the snapshot"
"delete withing the alloted {0} second timeout".format())
return behavior_response
@behavior(VolumesClient)
def delete_volume_with_snapshots_confirmed(self):
pass

View File

@ -0,0 +1,201 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.blockstorage.volumes_api.models.requests.volumes_api import \
Volume as VolumeRequest, VolumeSnapshot as VolumeSnapshotRequest\
from cloudcafe.blockstorage.volumes_api.models.responses.volumes_api import \
Volume as VolumeResponse, VolumeSnapshot as VolumeSnapshotResponse,\
VolumeType, VolumeList, VolumeTypeList, VolumeSnapshotList
class VolumesClient(AutoMarshallingRestClient):
def __init__(
self, url, auth_token, tenant_id, serialize_format=None,
deserialize_format=None):
super(VolumesClient, self).__init__(
serialize_format, deserialize_format)
self.url = url
self.auth_token = auth_token
self.default_headers['X-Auth-Token'] = auth_token
self.default_headers['Content-Type'] = 'application/%s' % \
self.serialize_format
self.default_headers['Accept'] = 'application/%s' % \
self.deserialize_format
def create_volume(
self, display_name, size, volume_type, availability_zone=None,
metadata={}, display_description='', snapshot_id=None,
requestslib_kwargs=None):
'''POST v1/{tenant_id}/volumes'''
url = '{0}/volumes'.format(self.url)
volume_request_entity = VolumeRequest(
display_name=display_name,
size=size,
volume_type=volume_type,
display_description=display_description,
metadata=metadata,
availability_zone=availability_zone,
snapshot_id=snapshot_id)
return self.request(
'POST', url, response_entity_type=VolumeResponse,
request_entity=volume_request_entity,
requestslib_kwargs=requestslib_kwargs)
def create_volume_from_snapshot(
self, snapshot_id, size, display_name='', volume_type=None,
availability_zone=None, display_description='', metadata={},
requestslib_kwargs=None):
'''POST v1/{tenant_id}/volumes'''
url = '{0}/volumes'.format(self.url)
volume_request_entity = VolumeRequest(
display_name=display_name,
size=size,
volume_type=volume_type,
display_description=display_description,
metadata=metadata,
availability_zone=availability_zone,
snapshot_id=snapshot_id)
return self.request(
'POST', url, response_entity_type=VolumeResponse,
request_entity=volume_request_entity,
requestslib_kwargs=requestslib_kwargs)
def list_all_volumes(self, requestslib_kwargs=None):
'''GET v1/{tenant_id}/volumes'''
url = '{0}/volumes'.format(self.url)
return self.request(
'GET', url, response_entity_type=VolumeList,
requestslib_kwargs=requestslib_kwargs)
def list_all_volumes_info(self, requestslib_kwargs=None):
'''GET v1/{tenant_id}/volumes/detail'''
url = '{0}/volumes/detail'.format(self.url)
return self.request(
'GET', url, response_entity_type=VolumeList,
requestslib_kwargs=requestslib_kwargs)
def get_volume_info(self, volume_id, requestslib_kwargs=None):
'''GET v1/{tenant_id}/volumes/{volume_id}'''
url = '{0}/volumes/{1}'.format(self.url, volume_id)
return self.request(
'GET', url, response_entity_type=VolumeResponse,
requestslib_kwargs=requestslib_kwargs)
def delete_volume(self, volume_id, requestslib_kwargs=None):
'''DELETE v1/{tenant_id}/volumes/{volume_id}'''
url = '{0}/volumes/{1}'.format(self.url, volume_id)
return self.request(
'DELETE', url, response_entity_type=VolumeResponse,
requestslib_kwargs=requestslib_kwargs)
#Volume Types API
def list_all_volume_types(self, requestslib_kwargs=None):
'''GET v1/{tenant_id}/types '''
url = '{0}/types'.format(self.url)
return self.request(
'GET', url, response_entity_type=VolumeTypeList,
requestslib_kwargs=requestslib_kwargs)
def get_volume_type_info(self, volume_type_id, requestslib_kwargs=None):
'''GET v1/{tenant_id}/types/{volume_type_id}'''
url = '{0}/types/{1}'.format(self.url, volume_type_id)
return self.request(
'GET', url, response_entity_type=VolumeType,
requestslib_kwargs=requestslib_kwargs)
#Volume Snapshot API
def create_snapshot(
self, volume_id, display_name=None, display_description=None,
force_create=False, name=None, requestslib_kwargs=None):
'''POST v1/{tenant_id}/snapshots'''
url = '{0}/snapshots'.format(self.url)
volume_snapshot_request_entity = VolumeSnapshotRequest(
volume_id,
force=force_create,
display_name=display_name,
name=name,
display_description=display_description)
return self.request(
'POST', url, response_entity_type=VolumeSnapshotResponse,
request_entity=volume_snapshot_request_entity,
requestslib_kwargs=requestslib_kwargs)
def list_all_snapshots(self, requestslib_kwargs=None):
'''GET v1/{tenant_id}/snapshots'''
url = '{0}/snapshots'.format(self.url)
return self.request(
'GET', url, response_entity_type=VolumeSnapshotList,
requestslib_kwargs=requestslib_kwargs)
def list_all_snapshots_info(self, requestslib_kwargs=None):
'''GET v1/{tenant_id}/snapshots/detail'''
url = '{0}/snapshots/detail'.format(self.url)
return self.request(
'GET', url, response_entity_type=VolumeSnapshotList,
requestslib_kwargs=requestslib_kwargs)
def get_snapshot_info(self, snapshot_id, requestslib_kwargs=None):
'''GET v1/{tenant_id}/snapshots/{snapshot_id}'''
url = '{0}/snapshots/{1}'.format(self.url, snapshot_id)
return self.request(
'GET', url, response_entity_type=VolumeSnapshotResponse,
requestslib_kwargs=requestslib_kwargs)
def delete_snapshot(self, snapshot_id, requestslib_kwargs=None):
'''DELETE v1/{tenant_id}/snapshots/{snapshot_id}'''
url = '{0}/snapshots/{1}'.format(self.url, snapshot_id)
return self.request(
'DELETE', url, response_entity_type=VolumeSnapshotResponse,
requestslib_kwargs=requestslib_kwargs)

View File

@ -0,0 +1,75 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.models.configuration import BaseConfigSectionInterface
class VolumesAPIConfig(BaseConfigSectionInterface):
SECTION_NAME = 'volumes_api'
@property
def serialize_format(self):
return self.get("serialize_format")
@property
def deserialize_format(self):
return self.get("deserialize_format")
@property
def max_volume_size(self):
return self.get("max_volume_size", default='1024')
@property
def min_volume_size(self):
return self.get("min_volume_size", default='1')
@property
def volume_create_timeout(self):
return self.get("volume_create_timeout", default='10')
@property
def volume_status_poll_frequency(self):
return self.get("volume_status_poll_frequency", default='30')
@property
def volume_delete_wait_per_gig(self):
return self.get("volume_delete_wait_per_gig", default='30')
@property
def snapshot_create_timeout(self):
return self.get("snapshot_create_timeout", default='10')
@property
def snapshot_status_poll_frequency(self):
return self.get("snapshot_status_poll_frequency", default='30')
@property
def volume_snapshot_delete_min_timeout(self):
"""Absolute lower limit on calculated volume snapshot delete timeouts
"""
return self.get("volume_delete_wait_per_gig", default=None)
@property
def volume_snapshot_delete_max_timeout(self):
"""Absolute upper limit on calculated volume snapshot delete timeouts
"""
return self.get("volume_delete_wait_per_gig", default=None)
@property
def volume_snapshot_delete_wait_per_gig(self):
"""If set, volume snapshot delete behaviors can estimate the time
it will take a particular volume to delete given it's size"""
return self.get("volume_delete_wait_per_gig", default=None)

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,121 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
from xml.etree import ElementTree
from cafe.engine.models.base import AutoMarshallingModel
class Volume(AutoMarshallingModel):
def __init__(
self, display_name=None, size=None, volume_type=None,
display_description=None, metadata=None, availability_zone=None,
snapshot_id=None, attachments=None, xmlns=None):
self.display_name = display_name
self.display_description = display_description
self.size = size
self.volume_type = volume_type
self.metadata = metadata or {}
self.availability_zone = availability_zone
self.snapshot_id = snapshot_id
def _obj_to_json(self):
return json.dumps(self._obj_to_json_dict())
def _obj_to_json_dict(self):
sub_dict = {}
sub_dict["display_name"] = self.display_name
sub_dict["display_description"] = self.display_description
sub_dict["size"] = self.size
sub_dict["volume_type"] = self.volume_type
sub_dict["metadata"] = self.metadata
sub_dict["availability_zone"] = self.availability_zone
sub_dict['snapshot_id'] = self.snapshot_id
root_dict = {}
root_dict["volume"] = self._remove_empty_values(sub_dict)
return root_dict
def _obj_to_xml_ele(self):
element = ElementTree.Element('volume')
attrs = {}
attrs["xmlns"] = self.xmlns
attrs["display_name"] = self.display_name
attrs["display_description"] = self.display_description
attrs["size"] = str(self.size)
attrs["volume_type"] = self.volume_type
attrs["availability_zone"] = self.availability_zone
element = self._set_xml_etree_element(element, attrs)
if len(self.metadata.keys()) > 0:
metadata_element = ElementTree.Element('metadata')
for key in self.metadata.keys():
meta_element = ElementTree.Element('meta')
meta_element.set('key', key)
meta_element.text = self.metadata[key]
metadata_element.append(meta_element)
element.append(metadata_element)
return element
def _obj_to_xml(self):
return ElementTree.tostring(self._obj_to_xml_ele())
class VolumeSnapshot(AutoMarshallingModel):
def __init__(
self, volume_id, force=True, display_name=None, name=None,
display_description=None):
self.force = force
self.display_name = display_name
self.volume_id = volume_id
self.name = name
self.display_description = display_description
def _obj_to_json(self):
return json.dumps(self._obj_to_json_dict())
def _obj_to_json_dict(self):
attrs = {}
attrs["snapshot"] = {}
sub_attrs = {}
sub_attrs["volume_id"] = self.volume_id
sub_attrs["force"] = self.force
sub_attrs["display_name"] = self.display_name
sub_attrs["display_description"] = self.display_description
attrs["snapshot"] = self._remove_empty_values({}, sub_attrs)
return self._remove_empty_values({}, attrs)
def _obj_to_xml(self):
return ElementTree.tostring(self._obj_to_xml_ele())
def _obj_to_xml_ele(self):
element = ElementTree.Element('snapshot')
attrs = {}
attrs["xmlns"] = self.xmlns
attrs["volume_id"] = self.volume_id
attrs["force"] = str(self.force)
attrs["display_name"] = self.display_name
attrs["display_description"] = self.display_description
element = self._set_xml_etree_element(element, attrs)
return element

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,265 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
from xml.etree import ElementTree
from cafe.engine.models.base import \
AutoMarshallingModel, AutoMarshallingListModel
class Volume(AutoMarshallingModel):
TAG = 'volume'
'''@TODO Make sub data model for attachments element'''
def __init__(
self, id_=None, display_name=None, size=None, volume_type=None,
display_description=None, metadata=None, availability_zone=None,
snapshot_id=None, attachments=None, created_at=None, status=None,
xmlns=None):
#Common attributes
self.id_ = id_
self.display_name = display_name
self.display_description = display_description
self.size = size
self.volume_type = volume_type
self.metadata = metadata or {}
self.availability_zone = availability_zone
self.snapshot_id = snapshot_id
self.attachments = attachments
self.created_at = created_at
self.status = status
self.xmlns = xmlns
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
volume_dict = json_dict.get(cls.TAG)
return cls._json_dict_to_obj(volume_dict)
@classmethod
def _json_dict_to_obj(cls, json_dict):
return Volume(
id_=json_dict.get('id'),
display_name=json_dict.get('display_name'),
size=json_dict.get('size'),
volume_type=json_dict.get('volume_type'),
display_description=json_dict.get('display_description'),
metadata=json_dict.get('metadata'),
availability_zone=json_dict.get('availability_zone'),
snapshot_id=json_dict.get('snapshot_id'),
attachments=json_dict.get('attachments'),
created_at=json_dict.get('created_at'),
status=json_dict.get('status'))
#Response Deserializers
@classmethod
def _xml_to_obj(cls, serialized_str):
element = ElementTree.fromstring(serialized_str)
return cls._xml_ele_to_obj(element)
@classmethod
def _xml_ele_to_obj(cls, element):
return Volume(
id_=element.get('id'),
display_name=element.get('display_name'),
size=element.get('size'),
volume_type=element.get('volume_type'),
display_description=element.get('display_description'),
metadata=element.get('metadata'),
availability_zone=element.get('availability_zone'),
snapshot_id=element.get('snapshot_id'),
attachments=element.get('attachments'),
created_at=element.get('created_at'),
status=element.get('status'))
class VolumeList(AutoMarshallingListModel):
TAG = 'volumes'
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
volume_dict_list = json_dict.get(cls.TAG)
return cls._json_dict_to_obj(volume_dict_list)
@classmethod
def _json_dict_to_obj(cls, json_dict):
volume_list = VolumeList()
for volume_dict in json_dict:
volume_list.append(Volume._json_dict_to_obj(volume_dict))
return volume_list
@classmethod
def _xml_to_obj(cls, serialized_str):
volume_list_element = ElementTree.fromstring(serialized_str)
return cls._xml_ele_to_obj(volume_list_element)
@classmethod
def _xml_ele_to_obj(cls, xml_etree_element):
volume_list = VolumeList()
for volume_element in xml_etree_element:
volume_list.append(Volume._xml_ele_to_obj(volume_element))
return volume_list
class VolumeSnapshot(AutoMarshallingModel):
TAG = 'snapshot'
def __init__(self, id_=None, volume_id=None, display_name=None,
display_description=None, status=None,
size=None, created_at=None, name=None):
self.id_ = id_
self.volume_id = volume_id
self.display_name = display_name
self.display_description = display_description
self.status = status
self.size = size
self.created_at = created_at
self.name = name
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
volume_snap_dict = json_dict.get(cls.TAG)
return cls._json_dict_to_obj(volume_snap_dict)
@classmethod
def _json_dict_to_obj(cls, json_dict):
return VolumeSnapshot(
id_=json_dict.get('id'),
volume_id=json_dict.get('volume_id'),
display_name=json_dict.get('display_name'),
display_description=json_dict.get('display_description'),
status=json_dict.get('status'),
size=json_dict.get('size'),
created_at=json_dict.get('created_at'),
name=json_dict.get('name'))
@classmethod
def _xml_to_obj(cls, serialized_str):
volume_snap_element = ElementTree.fromstring(serialized_str)
return cls._xml_ele_to_obj(volume_snap_element)
@classmethod
def _xml_ele_to_obj(cls, xml_etree_element):
return VolumeSnapshot(
id_=xml_etree_element.get('id'),
volume_id=xml_etree_element.get('volume_id'),
display_name=xml_etree_element.get('display_name'),
display_description=xml_etree_element.get('display_description'),
status=xml_etree_element.get('status'),
size=xml_etree_element.get('size'),
created_at=xml_etree_element.get('created_at'),
name=xml_etree_element.get('name'))
class VolumeSnapshotList(AutoMarshallingListModel):
TAG = 'snapshots'
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
volume_snap_dict_list = json_dict.get(cls.TAG)
return cls._json_dict_to_obj(volume_snap_dict_list)
@classmethod
def _json_dict_to_obj(cls, json_dict):
volume_snap_list = VolumeSnapshotList()
for volume_snap_dict in json_dict:
volume_snap_list.append(VolumeSnapshot._json_dict_to_obj(
volume_snap_dict))
return volume_snap_list
@classmethod
def _xml_to_obj(cls, serialized_str):
volume_snap_list_element = ElementTree.fromstring(serialized_str)
return cls._xml_ele_to_obj(volume_snap_list_element)
@classmethod
def _xml_ele_to_obj(cls, xml_etree_element):
volume_snap_list = VolumeSnapshotList()
for volume_snap_element in xml_etree_element:
volume_snap_list.append(VolumeSnapshot._xml_ele_to_obj(
volume_snap_element))
return volume_snap_list
class VolumeType(AutoMarshallingModel):
TAG = 'volume_type'
def __init__(self, id_=None, name=None, extra_specs=None):
self.id_ = id_
self.name = name
self.extra_specs = extra_specs
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
volume_type_dict = json_dict.get(cls.TAG)
return cls._json_dict_to_obj(volume_type_dict)
@classmethod
def _json_dict_to_obj(cls, json_dict):
return VolumeType(
id_=json_dict.get('id_'),
name=json_dict.get('name'),
extra_specs=json_dict.get('extra_specs'))
@classmethod
def _xml_to_obj(cls, serialized_str):
volume_type_element = ElementTree.fromstring(serialized_str)
return cls._xml_ele_to_obj(volume_type_element)
@classmethod
def _xml_ele_to_obj(cls, xml_etree_element):
return VolumeType(
id_=xml_etree_element.get('id_'),
name=xml_etree_element.get('name'),
extra_specs=xml_etree_element.get('extra_specs'))
class VolumeTypeList(AutoMarshallingListModel):
TAG = 'volume_types'
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
volume_type_dict_list = json_dict.get(cls.TAG)
return cls._json_dict_to_obj(volume_type_dict_list)
@classmethod
def _json_dict_to_obj(cls, json_dict):
volume_type_list = VolumeTypeList()
for volume_type_dict in json_dict:
volume_type_list.append(VolumeType._json_dict_to_obj(
volume_type_dict))
return volume_type_list
@classmethod
def _xml_to_obj(cls, serialized_str):
volume_type_list_element = ElementTree.fromstring(serialized_str)
return cls._xml_ele_to_obj(volume_type_list_element)
@classmethod
def _xml_ele_to_obj(cls, xml_etree_element):
volume_type_list = VolumeTypeList()
for volume_type_element in xml_etree_element:
volume_type_list.append(VolumeType._xml_ele_to_obj(
volume_type_element))
return volume_type_list

View File

@ -0,0 +1,35 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.provider import BaseProvider
from cloudcafe.blockstorage.volumes_api.config import VolumesAPIConfig
from cloudcafe.blockstorage.volumes_api.behaviors import VolumesBehaviors
from cloudcafe.blockstorage.volumes_api.client import VolumesClient
class VolumesProvider(BaseProvider):
def __init__(self, url, auth_token, tenant_id):
volumes_config = VolumesAPIConfig()
serialize_format = volumes_config.serialize_format
deserialize_format = volumes_config.deserialize_format
self.client = VolumesClient(
url, auth_token, tenant_id, serialize_format=serialize_format,
deserialize_format=deserialize_format)
self.behaviors = VolumesBehaviors(self.client)

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,25 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
class InstanceClientConstants:
LAST_REBOOT_TIME_FORMAT = '%Y-%m-%d %H:%M'
LAST_REBOOT_TIME_FORMAT_GENTOO = '%b %d %H:%M %Y'
LINUX_OS_FAMILY = 'linux'
PING_IPV4_COMMAND_LINUX = 'ping -c 3 '
PING_IPV6_COMMAND_LINUX = 'ping6 -c 3 '
PING_IPV4_COMMAND_WINDOWS = 'ping '
PING_IPV6_COMMAND_WINDOWS = 'ping6 '
PING_PACKET_LOSS_REGEX = '(\d{1,3})\.?\d*\%.*loss'

View File

@ -0,0 +1,5 @@
'''
@summary: Classes and Utilities for adapters that provide low level connectivity to various resources
@note: Most often consumed by a L{cafe.engine.clients} or L{cafe.common.reporting}
@note: Should not be used directly by a test case or process
'''

View File

@ -0,0 +1,19 @@
from datetime import datetime
from cafe.common.generators.base import BaseDataGenerator
class PasswordGenerator(BaseDataGenerator):
def __init__(self):
self.test_records = []
stamp = datetime.now().microsecond
cluster_name = "auth_functional_%s" %stamp
self.test_records.append({"false_password":'00000000',
"false_username":'@1234567'})
self.test_records.append({"false_password":'',
"false_username":''})
self.test_records.append({"false_password":'Pass1',
"false_username":'@'})
self.test_records.append({"false_password":'!@#$%^&*()',
"false_username":' 1Afarsf'})
self.test_records.append({"false_password":'102102101031013010311031',
"false_username":'Ricardo0000000000000!'})

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,48 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
from cafe.engine.models.data_interfaces import\
BaseConfigSectionInterface, ConfigEnvironmentVariableError
_TEST_CONFIG_FILE_ENV_VAR = 'OSTNG_CONFIG_FILE'
class ConfigSectionInterface(BaseConfigSectionInterface):
def __init__(self, config_file_path=None, section_name=None):
section_name = (section_name or
getattr(self, 'SECTION_NAME', None) or
getattr(self, 'CONFIG_SECTION_NAME', None))
config_file_path = config_file_path or self.default_config_file
super(ConfigSectionInterface, self).__init__(config_file_path, section_name)
@property
def default_config_file(self):
test_config_file_path = None
try:
test_config_file_path = os.environ[_TEST_CONFIG_FILE_ENV_VAR]
except KeyError:
msg = "'{0}' environment variable was not set.".format(
_TEST_CONFIG_FILE_ENV_VAR)
raise ConfigEnvironmentVariableError(msg)
except Exception as exception:
print ("Unexpected exception when attempting to access '{1}'"
" environment variable.".format(_TEST_CONFIG_FILE_ENV_VAR))
raise exception
return test_config_file_path

View File

@ -0,0 +1,44 @@
class Resource:
"""
@summary: Keeps details of a resource like server or image and how to delete it.
"""
def __init__(self, resource_id, delete_function):
self.resource_id = resource_id
self.delete_function = delete_function
def delete(self):
"""
@summary: Deletes the resource
"""
self.delete_function(self.resource_id)
class ResourcePool:
"""
@summary: Pool of resources to be tracked for deletion.
"""
def __init__(self):
self.resources = []
def add(self, resource_id, delete_function):
"""
@summary: Adds a resource to the resource pool
@param resource_id: Unique identifier of resource
@type resource_id: string
@param delete_function: The function to be called to delete a server
@type delete_function: Function Pointer
"""
self.resources.append(Resource(resource_id, delete_function))
def release(self):
"""
@summary: Delete all the resources in the Resource Pool
"""
for resource in self.resources:
try:
resource.delete()
except:
pass

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,41 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
class Constants:
LAST_REBOOT_TIME_FORMAT = '%Y-%m-%d %H:%M'
LAST_REBOOT_TIME_FORMAT_GENTOO = '%b %d %H:%M %Y'
LINUX_OS_FAMILY = 'linux'
PING_IPV4_COMMAND_LINUX = 'ping -c 3 '
PING_IPV6_COMMAND_LINUX = 'ping6 -c 3 '
PING_IPV4_COMMAND_WINDOWS = 'ping '
PING_IPV6_COMMAND_WINDOWS = 'ping6 '
PING_PACKET_LOSS_REGEX = '(\d{1,3})\.?\d*\%.*loss'
XML_API_NAMESPACE = 'http://docs.openstack.org/compute/api/v1.1'
XML_API_DISK_CONFIG_NAMESPACE = 'http://docs.openstack.org/compute/ext/disk_config/api/v1.1'
XML_API_EXTENDED_STATUS_NAMESPACE = 'http://docs.openstack.org/compute/ext/extended_status/api/v1.1'
XML_API_ATOM_NAMESPACE = 'http://www.w3.org/2005/Atom'
XML_API_RESCUE = 'http://docs.openstack.org/compute/ext/rescue/api/v1.1'
XML_API_UNRESCUE = 'http://docs.rackspacecloud.com/servers/api/v1.1'
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
DATETIME_6AM_FORMAT = "%Y-%m-%d 06:00:00"
DATETIME_0AM_FORMAT = "%Y-%m-%d 00:00:00"
XML_HEADER = "<?xml version='1.0' encoding='UTF-8'?>"
SERVICE_TYPE = 'cloudServersOpenStack'
class HTTPResponseCodes(object):
NOT_FOUND = 404
SERVER_ERROR = 500

View File

@ -0,0 +1,133 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 uuid import uuid4
import random
from math import pow
import time
SOURCE_RANDOM = '/dev/urandom'
SOURCE_ZEROS = '/dev/zero'
TEMP_LOCATION = '/tmp'
#Binary prefixes
#IEE_MAGNITUDE = int(pow(2,10))
EXACT_BYTE = 8
EXACT_KIBIBYTE = int(pow(2,10))
EXACT_MEBIBYTE = int(pow(2,20))
EXACT_GIBIBYTE = int(pow(2,30))
EXACT_TEBIBYTE = int(pow(2,40))
#Decimal prefixes
#SI_MAGNITURE = int(pow(10,3))
EXACT_KILOBYTE = int(pow(10,3))
EXACT_MEGABYTE = int(pow(10,6))
EXACT_GIGABYTE = int(pow(10,9))
EXACT_TERABYTE = int(pow(10,12))
def timestamp_string(prefix=None, suffix=None, decimal_precision=6):
'''
Return a unix timestamp surrounded by any defined prefixes and suffixes
Decimal precision is full (6) by default.
'''
t = str('%f' % time.time())
int_seconds, dec_seconds = t.split('.')
for x in range(6 - decimal_precision):
dec_seconds=dec_seconds[:-1]
int_seconds = str(int_seconds)
dec_seconds = str(dec_seconds)
prefix = prefix or ''
suffix = suffix or ''
final = None
if len(dec_seconds) > 0:
final = '%s%s%s' % ( prefix, int_seconds, suffix)
else:
final = '%s%s.%s%s' % ( prefix, int_seconds, dec_seconds, suffix)
return final
def random_string(prefix=None, suffix=None, size=8):
'''
Return exactly size bytes worth of base_text as a string
surrounded by any defined pre or suf-fixes
'''
base_text = str(uuid4()).replace('-','0')
if size <= 0:
return '%s%s' % (prefix, suffix)
extra = size % len(base_text)
body = ''
if extra == 0:
body = base_text * size
if extra == size:
body = base_text[:size]
if (extra > 0) and (extra < size):
body = (size / len(base_text)) * base_text + base_text[:extra]
body = str(prefix) + str(body) if prefix is not None else body
body = str(body) + str(suffix) if suffix is not None else body
return body
def random_ip(pattern=None):
'''Takes a pattern as a string in the format of #.#.#.# where a # is an
integer, and a can be substituded with an * to produce a random octet.
pattern = 127.0.0.* would return a random string between 127.0.0.1 and
127.0.0.254'''
if pattern is None:
pattern = '*.*.*.*'
num_asterisks = 0
for c in pattern:
if c == '*':
num_asterisks += 1
rand_list = [random.randint(1, 255) for i in range(0, num_asterisks)]
for item in rand_list:
pattern = pattern.replace('*', str(item), 1)
return pattern
def random_cidr(ip_pattern=None, mask=None, min_mask=0, max_mask=30):
'''Gets a random cidr using the random_ip function in this module. If mask
is None then a random mask between 0 and 30 inclusive will be assigned.'''
if mask is None:
mask = random.randint(min_mask, max_mask)
ip = random_ip(ip_pattern)
return ''.join([ip, '/', str(mask)])
def random_int(min_int, max_int):
return random.randint(min_int, max_int)
def rand_name(name='test'):
return name + str(random.randint(99999, 1000000))
def random_item_in_list(selection_list):
return random.choice(selection_list)
def bytes_to_gb(val):
return float(val) / 1073741824
def gb_to_bytes(val):
return int(val * 1073741824)
def bytes_to_mb(val):
return float(val) / 1024

View File

@ -0,0 +1,73 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from datetime import timedelta
class EqualityTools:
@classmethod
def are_not_equal(expected, actual):
return expected != None and expected != actual
@classmethod
def are_lists_equal(expected, actual):
if expected is None and actual is None:
return True
if expected is None or actual is None:
return False
if len(expected) != len(actual):
return False
for i in range(len(expected)):
if not expected[i].equals(actual[i]):
return False
return True
@classmethod
def sanitized_dict(self, dict_to_sanitize={}, keys_not_to_include=[]):
#make a shallow copy
sanitized_dict = dict_to_sanitize.copy()
for key in keys_not_to_include:
try:
del sanitized_dict[str(key)]
except:
continue
return sanitized_dict
@classmethod
def are_objects_equal(cls, expected_object, actual_object, keys_to_exclude=[]):
if(expected_object is None and actual_object is None):
return True
if(expected_object is None or actual_object is None):
return False
for key, expected_value in expected_object.__dict__.items():
if key not in keys_to_exclude and expected_value != actual_object.__dict__[key]:
return False
return True
@classmethod
def are_sizes_equal(cls, size1, size2, leeway):
return abs(size1 - size2) <= leeway
@classmethod
def is_true(cls, value):
return value is not None and (str(value) == '1' or str.lower(value) == 'true')
@classmethod
def are_datetimes_equal(cls, datetime1, datetime2, leeway=timedelta(seconds=0)):
return abs(datetime1 - datetime2) <= leeway

View File

@ -0,0 +1,121 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 xml.etree.ElementTree as ET
import json
from cafe.engine.models.base import AutoMarshallingModel
import cloudcafe.compute.common.exceptions as exceptions
ns = "http://docs.openstack.org/compute/api/v1.1"
class ExceptionHandler:
error_codes_list = [400, 401, 403, 404, 405, 409, 413, 415, 500, 501, 503]
def check_for_errors(self, resp):
if resp.status_code not in self.error_codes_list:
return
resp_body_dict = None
if resp.text != "":
resp_body_dict, type = self._parse_resp_body(resp.text)
if resp.status_code == 400 and type == 'html':
raise exceptions.BadRequest(resp_body_dict['400 Bad Request']['message'])
if resp.status_code == 400:
raise exceptions.BadRequest(resp_body_dict['badRequest']['message'])
if resp.status_code == 401:
raise exceptions.Unauthorized()
if resp.status_code == 413:
if 'overLimit' in resp_body_dict:
message = resp_body_dict['overLimit']['message']
else:
message = 'Rate or absolute limit exceeded'
raise exceptions.OverLimit(message)
if resp.status_code == 500 and type == 'html':
raise exceptions.InternalServerError()
if (resp.status_code == 500) and (resp_body_dict == None):
raise exceptions.ComputeFault(resp.reason)
if resp.status_code in (500, 501):
message = ''
if 'computeFault' in resp_body_dict:
message = resp_body_dict['computeFault']['message']
if 'cloudServersFault' in resp_body_dict:
message = resp_body_dict['cloudServersFault']['message']
if 'x-compute-request-id' in resp_body_dict:
message += ' x-compute-request-id ' + resp_body_dict['x-compute-request-id']
raise exceptions.ComputeFault(message)
if resp.status_code == 404:
raise exceptions.ItemNotFound()
if resp.status_code == 409:
message = ''
if 'conflictingRequest' in resp_body_dict:
message = resp_body_dict['conflictingRequest']['message']
if 'inProgress' in resp_body_dict:
message = resp_body_dict['inProgress']['message']
raise exceptions.ActionInProgress(message)
if resp.status_code == 405:
raise exceptions.BadMethod()
if resp.status_code == 403:
raise exceptions.Forbidden()
if resp.status_code == 503:
raise exceptions.ServiceUnavailable()
if resp.status_code == 415:
raise exceptions.BadMediaType()
def _parse_resp_body(self, resp_body):
#Try parsing as JSON
try:
body = json.loads(resp_body)
type = 'json'
return body, type
except:
#Not JSON
pass
#Try parsing as XML
try:
element = ET.fromstring(resp_body)
# Handle the case where the API returns the exception in HTML
AutoMarshallingModel._remove_namespace(element, ns)
type = 'xml'
return {element.tag: {'message': element.find('message').text}}, type
except:
#Not XML Either
pass
#Parse as HTML
finally:
split_resp = resp_body.split("\n\n")
type = 'html'
return {split_resp[0]: {'message': split_resp[1]}}, type

View File

@ -0,0 +1,184 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
class TimeoutException(Exception):
""" Exception on timeout """
def __init__(self, message='Request timed out'):
self.message = message
def __str__(self):
return repr(self.message)
class BuildErrorException(Exception):
""" Exception on server build """
def __init__(self, message='Build Error'):
self.message = message
def __str__(self):
return repr(self.message)
class DeleteException(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return repr(self.message)
class BadRequest(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return repr(self.message)
class BadRequestHtml(Exception):
def __init__(self):
self.message = '400 - Bad Request.'
def __str__(self):
return repr(self.message)
class OverLimit(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return repr(self.message)
class ComputeFault(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return repr(self.message)
class ItemNotFound(Exception):
def __init__(self):
self.message = '404 - Not found.'
def __str__(self):
return repr(self.message)
class BadMethod(Exception):
def __init__(self, message):
self.message = "405 - Bad Method."
def __str__(self):
return repr(self.message)
class Unauthorized(Exception):
def __init__(self):
self.message = "401 - Unauthorized."
def __str__(self):
return repr(self.message)
class Forbidden(Exception):
def __init__(self, message):
self.message = "403 - Forbidden Operation"
def __str__(self):
return repr(self.message)
class ActionInProgress(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return repr(self.message)
class BuildInProgress(Exception):
def __init__(self):
self.message = "409 - Action failed. Entity is currently building."
def __str__(self):
return repr(self.message)
class ServiceUnavailable(Exception):
def __init__(self):
self.message = "503 - The service is currently unavailable."
def __str__(self):
return repr(self.message)
class FileNotFoundException(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return repr(self.message)
class SshConnectionException(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return repr(self.message)
class ServerUnreachable(Exception):
def __init__(self, address):
self.message = 'Could not reach the server at %s.' % address
def __str__(self):
return repr(self.message)
class InvalidJSON(Exception):
def __init__(self, message, expected_response):
self.message = 'Unexpected JSON response. Parsing of the following JSON failed ' + message + '. Expected response of type ' + expected_response
class AuthenticationTimeoutException(Exception):
def __init__(self, server_id=None):
if server_id is None:
self.message = 'Authentication to the desired failed due to timing out.'
else:
self.message = 'Authentication to server ' + server_id + ' failed due to timing out.'
def __str__(self):
return repr(self.message)
class BadMediaType(Exception):
def __init__(self, message):
self.message = '415 - Bad media type.'
def __str__(self):
return repr(self.message)
class InternalServerError(Exception):
def __init__(self):
self.message = '500 - Internal Server Error.'
def __str__(self):
return repr(self.message)

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,29 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cloudcafe.compute.common.equality_tools import EqualityTools
class FileDetails:
"""
@summary: Represents File details
"""
def __init__(self, absolute_permissions, content, name):
self.absolute_permissions = absolute_permissions
self.content = content
self.name = name
def __eq__(self, other):
return EqualityTools.are_objects_equal(self, other)

View File

@ -0,0 +1,127 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
import xml.etree.ElementTree as ET
from cafe.engine.models.base import AutoMarshallingModel
from cloudcafe.compute.common.constants import Constants
class Links(AutoMarshallingModel):
"""
@summary: Represents links (url) in the system
"""
ROOT_TAG = 'links'
def __init__(self, links_list):
super(Links, self).__init__()
self.links = {}
if links_list is not None:
for link in links_list:
self.links[link['rel']] = link['href']
for key_name in self.links:
setattr(self, key_name, self.links[key_name])
@classmethod
def _xml_to_object(cls, serialized_str):
"""
@summary: Initializes the object from xml response
@param objectified_links: links details
@type objectified_links: objectify.Element
"""
element = ET.fromstring(serialized_str)
cls._remove_namespace(element, Constants.XML_API_NAMESPACE)
cls._remove_namespace(element, Constants.XML_API_ATOM_NAMESPACE)
return cls._xml_ele_to_obj(element)
@classmethod
def _xml_ele_to_obj(cls, element):
'''Helper method to turn ElementTree instance to Links instance.'''
links = []
'''
When we serialize a flavor object to XML, we generate an additional
tag <links> for the links which is the parent to the <link> element.
Hence we need to loop twice to get to the dictionary of links
<links>
<link/>
...
</links>
'''
for child_element in element._children:
if child_element.tag[29:] == 'link':
links.append(child_element.attrib)
if element.findall('link'):
for link in element.findall('link'):
links.append(link.attrib)
return Links(links)
@classmethod
def _dict_to_obj(cls, list_of_links):
"""
@summary: Initializes the object from json response
@param list_of_links: links details
@type list_of_links: list
"""
return Links(list_of_links)
def __repr__(self):
values = []
for prop in __dict__:
values.append("%s: %s" % (prop, __dict__[prop]))
return '[' + ', '.join(values) + ']'
@classmethod
def _json_to_obj(cls, serialized_str):
'''Returns an instance of links based on the json
serialized_str passed in.'''
json_dict = json.loads(serialized_str)
if 'links' in json_dict.keys():
links_list = json_dict['links']
return Links(links_list)
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: Links object to compare with
@type other: Links
@return: True if Links objects are equal, False otherwise
@rtype: bool
"""
if(self is None and other is None):
return True
if(self is None or other is None):
return False
for key in self.links:
#Alternate links are random, equality is impossible..ignoring it
if key != 'alternate' and \
self.links[key] != other.links[key]:
return False
return True
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: Links object to compare with
@type other: Links
@return: True if Links objects are not equal, False otherwise
@rtype: bool
"""
return not self == other

View File

@ -0,0 +1,214 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
import xml.etree.ElementTree as ET
from cafe.engine.models.base import AutoMarshallingModel
from cloudcafe.compute.common.constants import Constants
from cloudcafe.compute.common.equality_tools import EqualityTools
class MetadataItem(AutoMarshallingModel):
'''
@summary: MetadataItem Request/Response Object for Server/Image
'''
ROOT_TAG = 'meta'
def __init__(self, metadata_dict):
for key, value in metadata_dict.items():
setattr(self, key, value)
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element('meta')
element.set('xmlns', Constants.XML_API_NAMESPACE)
for key, value in (self._obj_to_dict(meta_obj=self)).items():
element.set('key', key)
element.text = value
xml += ET.tostring(element)
return xml
@classmethod
def _obj_to_dict(self, meta_obj):
meta = {}
for name in dir(meta_obj):
value = getattr(meta_obj, name)
if not name.startswith('_') and not name.startswith('RO') and not name.startswith('deser') and not name.startswith('sele') and not name.startswith('seria'):
meta[name] = value
return meta
@classmethod
def _xml_to_obj(cls, serialized_str):
"""
@summary: Initializes the object from xml response
@param objectified_links: metadata item
@type objectified_links: objectify.Element
"""
element = ET.fromstring(serialized_str)
cls._remove_namespace(element, Constants.XML_API_NAMESPACE)
metadata_item = cls._xml_ele_to_obj(element)
return metadata_item
@classmethod
def _xml_ele_to_obj(cls, element):
'''Helper method to turn ElementTree instance to metadata instance.'''
metadata_dict = {}
metadata_dict[(element.attrib).get('key')] = element.text
return MetadataItem(metadata_dict)
@classmethod
def _dict_to_object(cls, metadata_dict):
"""
@summary: Initializes the object from json response
@param metadata_dict: metadata items
@type metadata_dict: dictionary
"""
return MetadataItem(metadata_dict)
@classmethod
def _json_to_obj(cls, serialized_str):
'''Returns an instance of metadata item based on the json
serialized_str passed in.'''
json_dict = json.loads(serialized_str)
if 'meta' in json_dict.keys():
metadata_dict = json_dict['meta']
return MetadataItem(metadata_dict)
def __repr__(self):
values = []
for prop in __dict__:
values.append("%s: %s" % (prop, __dict__[prop]))
return '[' + ', '.join(values) + ']'
class Metadata(AutoMarshallingModel):
ROOT_TAG = 'metadata'
'''
@summary: Metadata Request Object for Server/Image
'''
def __init__(self, metadata_dict):
for key, value in metadata_dict.items():
setattr(self, key, value)
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element('metadata')
element.set('xmlns', Constants.XML_API_NAMESPACE)
for name in dir(self):
value = getattr(self, name)
if not name.startswith('_') and not name.startswith('RO') and not name.startswith('deser') and not name.startswith('sele') and not name.startswith('seria'):
element.append(self._dict_to_xml(key=name, value=value))
xml += ET.tostring(element)
return xml
@classmethod
def _dict_to_xml(self, key, value):
meta_element = ET.Element('meta')
meta_element.set('key', key)
meta_element.text = value
return meta_element
@classmethod
def _xml_to_obj(cls, serialized_str):
"""
@summary: Initializes the object from xml response
@param objectified_links: metadata details
@type objectified_links: objectify.Element
"""
element = ET.fromstring(serialized_str)
cls._remove_namespace(element, Constants.XML_API_NAMESPACE)
metadata = cls._xml_ele_to_obj(element)
return metadata
@classmethod
def _xml_ele_to_obj(cls, element):
'''Helper method to turn ElementTree instance to metadata instance.'''
meta_dict = {}
entity = element
if entity.find('metadata') is not None:
meta_list = entity.find("metadata").findall('meta')
for each in meta_list:
meta_dict[each.attrib['key']] = each.text
return Metadata(meta_dict)
if entity.tag == 'metadata':
meta_list = entity.findall('meta')
for each in meta_list:
meta_dict[each.attrib['key']] = each.text
return Metadata(meta_dict)
@classmethod
def _dict_to_obj(cls, metadata_dict):
"""
@summary: Initializes the object from json response
@param metadata_dict: metadata details
@type metadata_dict: dictionary
"""
return Metadata(metadata_dict)
@classmethod
def _json_to_obj(cls, serialized_str):
'''Returns an instance of metadata based on the json
serialized_str passed in.'''
json_dict = json.loads(serialized_str)
if 'metadata' in json_dict.keys():
metadata_dict = json_dict['metadata']
return Metadata(metadata_dict)
@classmethod
def _obj_to_dict(self, meta_obj):
meta = {}
for name in dir(meta_obj):
value = getattr(meta_obj, name)
if not name.startswith('_') and not name.startswith('RO') and not name.startswith('deser') and not name.startswith('sele') and not name.startswith('seria'):
meta[name] = value
return meta
def __repr__(self):
values = []
for prop in __dict__:
values.append("%s: %s" % (prop, __dict__[prop]))
return '[' + ', '.join(values) + ']'
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: Links object to compare with
@type other: Links
@return: True if Links objects are equal, False otherwise
@rtype: bool
"""
return EqualityTools.are_objects_equal(self, other)
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: Links object to compare with
@type other: Links
@return: True if Links objects are not equal, False otherwise
@rtype: bool
"""
return not self == other

View File

@ -0,0 +1,96 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cloudcafe.compute.common.equality_tools import EqualityTools
class Partition:
"""
@summary: Represents a Disk Partition
"""
def __init__(self, name, size, type):
self.name = name
self.size = size
self.type = type
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: Partition object to compare with
@type other: Partition
@return: True if Partition objects are equal, False otherwise
@rtype: bool
"""
return EqualityTools.are_objects_equal(self, other)
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: Partition object to compare with
@type other: Partition
@return: True if Partition objects are not equal, False otherwise
@rtype: bool
"""
return not self == other
def __repr__(self):
"""
@summary: Return string representation of Partition
@return: String representation of Partition
@rtype: string
"""
return "Partition Name : %s, Size: %s, Type : %s" % (self.name, self.size, self.type)
class DiskSize:
"""
@summary: Represents a Disk Size
"""
def __init__(self, value, unit, leeway_for_disk_size=2):
self.value = float(value)
self.unit = unit
self.leeway_for_disk_size = leeway_for_disk_size
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: DiskSize object to compare with
@type other: DiskSize
@return: True if DiskSize objects are equal, False otherwise
@rtype: bool
"""
return self.unit == other.unit and EqualityTools.are_sizes_equal(
self.value, other.value,
self.leeway_for_disk_size)
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: DiskSize object to compare with
@type other: DiskSize
@return: True if DiskSize objects are not equal, False otherwise
@rtype: bool
"""
return not self == other
def __repr__(self):
"""
@summary: Return string representation of DiskSize
@return: String representation of DiskSize
@rtype: string
"""
return "Disk Size : %s %s" % (self.value, self.unit)

View File

@ -0,0 +1,95 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
class NovaServerStatusTypes(object):
'''
@summary: Types dictating an individual Server Status
@cvar ACTIVE: Server is active and available
@type ACTIVE: C{str}
@cvar BUILD: Server is being built
@type BUILD: C{str}
@cvar ERROR: Server is in error
@type ERROR: C{str}
@note: This is essentially an Enumerated Type
'''
ACTIVE = "ACTIVE"
BUILD = "BUILD"
REBUILD = "REBUILD"
ERROR = "ERROR"
DELETING = "DELETING"
DELETED = "DELETED"
RESCUE = "RESCUE"
PREP_RESCUE = "PREP_RESCUE"
INVALID_OPTION = "INVALID_OPTION"
RESIZE = "RESIZE"
VERIFY_RESIZE = "VERIFY_RESIZE"
class NovaImageStatusTypes(object):
'''
@summary: Types dictating an individual Server Status
@cvar ACTIVE: Server is active and available
@type ACTIVE: C{str}
@cvar BUILD: Server is being built
@type BUILD: C{str}
@cvar ERROR: Server is in error
@type ERROR: C{str}
@note: This is essentially an Enumerated Type
'''
ACTIVE = "ACTIVE"
SAVING = "SAVING"
ERROR = "ERROR"
DELETED = "DELETED"
UNKNOWN = "UNKNOWN"
class NovaServerRebootTypes(object):
'''
@summary: Types dictating server reboot types
@cvar HARD: Hard reboot
@type HARD: C{str}
@cvar SOFT: Soft reboot
@type SOFT: C{str}
@note: This is essentially an Enumerated Type
'''
HARD = "HARD"
SOFT = "SOFT"
class NovaVolumeStatusTypes(object):
'''
@summary: Types dictating an individual Volume Status
@cvar AVAILABLE: Volume is active and available
@type AVAILABLE: C{str}
@cvar CREATING: Volume is being created
@type CREATING: C{str}
@cvar ERROR: Volume is in error
@type ERROR: C{str}
@cvar DELETING: Volume is being deleted
@type DELETING: C{str}
@cvar ERROR_DELETING: Volume is in error while being deleted
@type ERROR_DELETING: C{str}
@cvar IN_USE: Volume is active and available
@type IN_USE: C{str}
@note: This is essentially an Enumerated Type
'''
AVAILABLE = "available"
ATTACHING = "attaching"
CREATING = "creating"
DELETING = "deleting"
ERROR = "error"
ERROR_DELETING = "error_deleting"
IN_USE = "in-use"

View File

@ -0,0 +1,30 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cloudcafe.common.models.configuration import ConfigSectionInterface
class ComputeConfig(ConfigSectionInterface):
SECTION_NAME = 'compute'
@property
def region(self):
return self.get("region")
@property
def compute_endpoint_name(self):
return self.get("compute_endpoint_name")

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,115 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 urlparse import urlparse
from cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.compute.flavors_api.models.flavor import Flavor, FlavorMin
class FlavorsClient(AutoMarshallingRestClient):
def __init__(self, url, auth_token, serialize_format=None,
deserialize_format=None):
"""
@param url: Base URL for the compute service
@type url: String
@param auth_token: Auth token to be used for all requests
@type auth_token: String
@param serialize_format: Format for serializing requests
@type serialize_format: String
@param deserialize_format: Format for de-serializing responses
@type deserialize_format: String
"""
super(FlavorsClient, self).__init__(serialize_format,
deserialize_format)
self.auth_token = auth_token
self.default_headers['X-Auth-Token'] = auth_token
ct = ''.join(['application/', self.serialize_format])
accept = ''.join(['application/', self.deserialize_format])
self.default_headers['Content-Type'] = ct
self.default_headers['Accept'] = accept
self.url = url
def list_flavors(self, min_disk=None, min_ram=None, marker=None,
limit=None, requestslib_kwargs=None):
'''
@summary: Returns a list of flavors
@param min_disk: min Disk in GB, to filter by minimum Disk size in MB
@type min_disk:int
@param min_ram: min ram in GB, to filter by minimum RAM size in MB
@type min_Disk:int
@param marker: ID of last item in previous list (paginated collections)
@type marker:C{str}
@param limit: Sets page size
@type limit: int
@return: List of flavors filtered by params on success
@rtype: C{list}
'''
url = '%s/flavors' % (self.url)
params = {'minDisk': min_disk, 'minRam': min_ram, 'marker': marker,
'limit': limit}
flavor_response = self.request('GET', url, params=params,
response_entity_type=FlavorMin,
requestslib_kwargs=requestslib_kwargs)
return flavor_response
def list_flavors_with_detail(self, min_disk=None, min_ram=None,
marker=None, limit=None,
requestslib_kwargs=None):
'''
@summary: Returns details from a list of flavors
@param min_disk: min Disk in GB, to filter by minimum Disk size in MB
@type min_disk:int
@param min_ram: min ram in GB, to filter by minimum RAM size in MB
@type min_Disk:int
@param marker: ID of last item in previous list (paginated collections)
@type marker:C{str}
@param limit: Sets page size
@type limit: int
@return: Detail List of flavors filtered by params on success
@rtype: C{list}
'''
url = '%s/flavors/detail' % (self.url)
params = {'minDisk': min_disk, 'minRam': min_ram, 'marker': marker,
'limit': limit}
flavor_response = self.request('GET', url, params=params,
response_entity_type=Flavor,
requestslib_kwargs=requestslib_kwargs)
return flavor_response
def get_flavor_details(self, flavor_id, requestslib_kwargs=None):
'''
@summary: Returns a dict of details for given filter
@param flavor_id: if of flavor for which details are required
@type flavor_id:C{str}
@return: Details of filter with filter id in the param on success
@rtype: C{dict}
'''
url_new = str(flavor_id)
url_scheme = urlparse(url_new).scheme
url = url_new if url_scheme \
else '%s/flavors/%s' % (self.url, flavor_id)
flavor_response = self.request('GET', url, requestslib_kwargs,
response_entity_type=Flavor,
requestslib_kwargs=requestslib_kwargs)
return flavor_response

View File

@ -0,0 +1,32 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cloudcafe.common.models.configuration import ConfigSectionInterface
class FlavorsConfig(ConfigSectionInterface):
SECTION_NAME = 'flavors'
@property
def primary_flavor(self):
"""Default flavor to be used when building servers in compute tests"""
return self.get("primary_flavor")
@property
def secondary_flavor(self):
"""Alternate flavor to be used in compute tests"""
return self.get("secondary_flavor")

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,185 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
import xml.etree.ElementTree as ET
from cafe.engine.models.base import AutoMarshallingModel
from cloudcafe.compute.common.equality_tools import EqualityTools
from cloudcafe.compute.common.constants import Constants
from cloudcafe.compute.common.models.link import Links
class Flavor(AutoMarshallingModel):
def __init__(self, id=None, name=None, ram=None, disk=None, vcpus=None,
swap=None, rxtx_factor=None, links=None):
'''An object that represents a flavor.
'''
self.id = id
self.name = name
self.ram = ram
self.disk = disk
self.vcpus = vcpus
self.links = links
def __repr__(self):
values = []
for prop in self.__dict__:
values.append("%s: %s" % (prop, self.__dict__[prop]))
return '[' + ', '.join(values) + ']'
@classmethod
def _json_to_obj(cls, serialized_str):
'''Returns an instance of a Flavor based on the json serialized_str
passed in.'''
json_dict = json.loads(serialized_str)
if 'flavor' in json_dict.keys():
flavor = cls._dict_to_obj(json_dict['flavor'])
return flavor
if 'flavors' in json_dict.keys():
flavors = []
for flavor_dict in json_dict['flavors']:
flavor = cls._dict_to_obj(flavor_dict)
flavors.append(flavor)
return flavors
@classmethod
def _dict_to_obj(cls, flavor_dict):
'''Helper method to turn dictionary into Server instance.'''
flavor = Flavor(id=flavor_dict.get('id'),
name=flavor_dict.get('name'),
ram=flavor_dict.get('ram'),
disk=flavor_dict.get('disk'),
vcpus=flavor_dict.get('vcpus'))
flavor.links = Links._dict_to_obj(flavor_dict['links'])
return flavor
@classmethod
def _xml_to_obj(cls, serialized_str):
'''Returns an instance of a Flavor based on the xml serialized_str
passed in.'''
element = ET.fromstring(serialized_str)
cls._remove_xml_etree_namespace(element, Constants.XML_API_NAMESPACE)
cls._remove_xml_etree_namespace(element,
Constants.XML_API_ATOM_NAMESPACE)
if element.tag == 'flavor':
flavor = cls._xml_ele_to_obj(element)
return flavor
if element.tag == 'flavors':
flavors = []
for flavor in element.findall('flavor'):
flavor = cls._xml_ele_to_obj(flavor)
flavors.append(flavor)
return flavors
@classmethod
def _xml_ele_to_obj(cls, element):
'''Helper method to turn ElementTree instance to Flavor instance.'''
flavor_dict = element.attrib
if 'vcpus' in flavor_dict:
flavor_dict['vcpus'] = (flavor_dict.get('vcpus') and
int(flavor_dict.get('vcpus')))
if 'disk' in flavor_dict:
flavor_dict['disk'] = (flavor_dict.get('disk') and
int(flavor_dict.get('disk')))
if 'rxtx_factor' in flavor_dict:
flavor_dict['rxtx_factor'] = flavor_dict.get('rxtx_factor') \
and float(flavor_dict.get('rxtx_factor'))
if 'ram' in flavor_dict:
flavor_dict['ram'] = flavor_dict.get('ram') \
and int(flavor_dict.get('ram'))
if 'swap' in flavor_dict:
flavor_dict['swap'] = flavor_dict.get('swap') \
and int(flavor_dict.get('swap'))
links = Links._xml_ele_to_obj(element)
flavor = Flavor(flavor_dict.get('id'), flavor_dict.get('name'),
flavor_dict.get('ram'), flavor_dict.get('disk'),
flavor_dict.get('vcpus'), flavor_dict.get('swap'),
flavor_dict.get('rxtx_factor'), links)
return flavor
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: Flavor object to compare with
@type other: Flavor
@return: True if Flavor objects are equal, False otherwise
@rtype: bool
"""
return EqualityTools.are_objects_equal(self, other, ['links'])
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: Flavor object to compare with
@type other: Flavor
@return: True if Flavor objects are not equal, False otherwise
@rtype: bool
"""
return not self == other
class FlavorMin(Flavor):
"""
@summary: Represents minimum details of a flavor
"""
def __init__(self, **kwargs):
'''Flavor Min has only id, name and links '''
for keys, values in kwargs.items():
setattr(self, keys, values)
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: FlavorMin object to compare with
@type other: FlavorMin
@return: True if FlavorMin objects are equal, False otherwise
@rtype: bool
"""
return EqualityTools.are_objects_equal(self, other, ['links'])
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: FlavorMin object to compare with
@type other: FlavorMin
@return: True if FlavorMin objects are not equal, False otherwise
@rtype: bool
"""
return not self == other
@classmethod
def _xml_ele_to_obj(cls, element):
'''Helper method to turn ElementTree instance to Server instance.'''
flavor_dict = element.attrib
flavor_min = FlavorMin(id=flavor_dict.get('id'),
name=flavor_dict.get('name'))
flavor_min.links = Links._xml_ele_to_obj(element)
return flavor_min
@classmethod
def _dict_to_obj(cls, flavor_dict):
'''Helper method to turn dictionary into Server instance.'''
flavor_min = FlavorMin(id=flavor_dict.get('id'),
name=flavor_dict.get('name'))
flavor_min.links = Links._dict_to_obj(flavor_dict['links'])
return flavor_min

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,88 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 time
from cloudcafe.compute.common.types import NovaImageStatusTypes as ImageStates
from cloudcafe.compute.common.exceptions import ItemNotFound, \
TimeoutException, BuildErrorException
class ImageBehaviors(object):
def __init__(self, images_client, config):
self.config = config
self.images_client = images_client
def wait_for_image_status(self, image_id, desired_status):
'''Polls image image_id details until status_to_wait_for is met.'''
image_response = self.images_client.get_image(image_id)
image_obj = image_response.entity
time_waited = 0
interval_time = self.config.image_status_interval
while (image_obj.status.lower() != desired_status.lower() and
time_waited < self.config.snapshot_timeout):
image_response = self.images_client.get_image(image_id)
image_obj = image_response.entity
if image_obj.status.lower() is ImageStates.ERROR.lower():
message = 'Snapshot failed. Image with uuid {0} entered ERROR status.'
raise BuildErrorException(message.format(image_id))
time.sleep(interval_time)
time_waited += interval_time
return image_response
def wait_for_image_resp_code(self, image_id, response_code):
'''Polls image resp for the specified status code.'''
image_response = self.images_client.get_image(image_id)
image_obj = image_response.entity
time_waited = 0
interval_time = self.config.image_status_interval
while (image_response.status_code != response_code and
image_obj.status.lower() != ImageStates.ERROR.lower() and
time_waited < self.config.snapshot_timeout):
image_response = self.images_client.get_image(image_id)
image_obj = image_response.entity
time.sleep(interval_time)
time_waited += interval_time
return image_response
def wait_for_image_to_be_deleted(self, image_id):
'''Waits for the image to be deleted. '''
image_response = self.images_client.delete_image(image_id)
image_obj = image_response.entity
time_waited = 0
interval_time = self.config.image_status_interval
try:
while (True):
image_response = self.images_client.get_image(image_id)
image_obj = image_response.entity
if time_waited > self.config.snapshot_timeout:
raise TimeoutException("Timed out while deleting image id: %s" % image_id)
if image_obj.status.lower() == ImageStates.DELETED.lower():
return
if image_obj.status.lower() != ImageStates.ERROR.lower():
raise BuildErrorException("Image entered Error state while deleting, Image id : %s" % image_id)
time.sleep(interval_time)
time_waited += interval_time
except ItemNotFound:
pass

View File

@ -0,0 +1,255 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 urlparse import urlparse
from cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.compute.common.models.metadata import Metadata
from cloudcafe.compute.common.models.metadata import MetadataItem
from cloudcafe.compute.images_api.models.image import Image, ImageMin
class ImagesClient(AutoMarshallingRestClient):
'''
Client for Image API
'''
def __init__(self, url, auth_token, serialize_format, deserialize_format):
"""
@param url: Base URL for the compute service
@type url: String
@param auth_token: Auth token to be used for all requests
@type auth_token: String
@param serialize_format: Format for serializing requests
@type serialize_format: String
@param deserialize_format: Format for de-serializing responses
@type deserialize_format: String
"""
super(ImagesClient, self).__init__(serialize_format,
deserialize_format)
self.auth_token = auth_token
self.default_headers['X-Auth-Token'] = auth_token
ct = ''.join(['application/', self.serialize_format])
accept = ''.join(['application/', self.deserialize_format])
self.default_headers['Content-Type'] = ct
self.default_headers['Accept'] = accept
self.url = url
def list_images(self, server_ref=None, image_name=None, status=None,
image_type=None, marker=None, changes_since=None, limit=None,
requestslib_kwargs=None):
'''
@summary: Lists IDs, names, and links for all available images.
@param server_ref: Server id or Url to server
@type server_ref: String
@param image_name: Image Name
@type image_name: String
@param status: Image Status
@type status: String
@param image_type:BASE|SERVER
@type image_type:String
@param changes_since: changed since the changes-since time
@type changes_since: DateTime
@param marker: The ID of the last item in the previous list
@type marker: String
@param limit:Sets the page size.
@type limit:int
@return: lists all images visible by the account filtered by the params
@rtype: Response with Image List as response.entity
'''
url = '%s/images' % (self.url)
params = {'server': server_ref, 'name': image_name,
'status': status, 'type': image_type, 'marker': marker,
'changes-since': changes_since, 'limit': limit}
return self.request('GET', url, params=params,
response_entity_type=ImageMin,
requestslib_kwargs=requestslib_kwargs)
def list_images_with_detail(self, server_ref=None, image_name=None,
status=None, image_type=None, marker=None,
changes_since=None, limit=None,
requestslib_kwargs=None):
'''
@summary: List all details for all available images.
@param server_ref: Server id or Url to server
@type server_ref: String
@param image_name: Image Name
@type image_name: String
@param status: Image Status
@type status: String
@param type:BASE|SERVER
@type type:String
@param changes_since: changed since the changes-since time
@type changes_since: DateTime
@param marker: The ID of the last item in the previous list
@type marker: String
@param limit:Sets the page size.
@type limit:int
@return: lists all images visible by the account filtered by the params
@rtype: Response with Image List as response.entity
'''
url = '%s/images/detail' % (self.url)
params = {'server': server_ref, 'name': image_name,
'status': status, 'type': image_type, 'marker': marker,
'changes-since': changes_since, 'limit': limit}
return self.request('GET', url, params=params,
response_entity_type=Image,
requestslib_kwargs=requestslib_kwargs)
def get_image(self, image_id, requestslib_kwargs=None):
'''
@summary: Lists details of the specified image.
@param image_id: Image id
@type image_id: String
@return: Details of specified Image. BUT no server_id in image details
@rtype: Response with Image as response.entity
'''
url_new = str(image_id)
url_scheme = urlparse(url_new).scheme
url = url_new if url_scheme else '%s/images/%s' % (self.url, image_id)
return self.request('GET', url, response_entity_type=Image,
requestslib_kwargs=requestslib_kwargs)
def delete_image(self, image_id, requestslib_kwargs=None):
'''
@summary: Deletes the specified image.
@param image_id: Image id
@type image_id: String
@return: Response code 204 if successful
@rtype: Response Object
'''
url = '%s/images/%s' % (self.url, image_id)
return self.request('DELETE', url,
requestslib_kwargs=requestslib_kwargs)
def list_image_metadata(self, image_id, requestslib_kwargs=None):
'''
@summary: Returns metadata associated with an image
@param image_id: Image ID
@type image_id:String
@return: Metadata associated with an image on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/images/%s/metadata' % (self.url, image_id)
image_response = self.request('GET', url,
response_entity_type=Metadata,
requestslib_kwargs=requestslib_kwargs)
return image_response
def set_image_metadata(self, image_id, metadata, requestslib_kwargs=None):
'''
@summary: Sets metadata for the specified image
@param image_id: Image ID
@type image_id:String
@param metadata: Metadata to be set for an image
@type metadata: dictionary
@return: Metadata associated with an image on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/images/%s/metadata' % (self.url, image_id)
request_object = Metadata(metadata)
image_response = self.request('PUT', url,
response_entity_type=Metadata,
request_entity=request_object,
requestslib_kwargs=requestslib_kwargs)
return image_response
def update_image_metadata(self, image_id, metadata,
requestslib_kwargs=None):
'''
@summary: Updates metadata items for the specified image
@param image_id: Image ID
@type image_id:String
@param metadata: Metadata to be updated for an image
@type metadata: dictionary
@return: Metadata associated with an image on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/images/%s/metadata' % (self.url, image_id)
request_object = Metadata(metadata)
image_response = self.request('POST', url,
response_entity_type=Metadata,
request_entity=request_object,
requestslib_kwargs=requestslib_kwargs)
return image_response
def get_image_metadata_item(self, image_id, key, requestslib_kwargs=None):
'''
@summary: Retrieves a single metadata item by key
@param image_id: Image ID
@type image_id:String
@param key: Key for which metadata item needs to be retrieved
@type key: String
@return: Metadata Item for a key on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/images/%s/metadata/%s' % (self.url, image_id, key)
image_response = self.request('GET', url,
response_entity_type=MetadataItem,
requestslib_kwargs=requestslib_kwargs)
return image_response
def set_image_metadata_item(self, image_id, key, value,
requestslib_kwargs=None):
'''
@summary: Sets a metadata item for a specified image
@param image_id: Image ID
@type image_id:String
@param key: Key for which metadata item needs to be set
@type key: String
@param key: Value which the metadata key needs to be set to
@type key: String
@return: Metadata Item for the key on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/images/%s/metadata/%s' % (self.url, image_id, key)
metadata_item = MetadataItem({key: value})
image_response = self.request('PUT', url,
response_entity_type=MetadataItem,
request_entity=metadata_item,
requestslib_kwargs=requestslib_kwargs)
return image_response
def delete_image_metadata_item(self, image_id, key,
requestslib_kwargs=None):
'''
@summary: Sets a metadata item for a specified image
@param image_id: Image ID
@type image_id:String
@param key: Key for which metadata item needs to be set
@type key: String
@return: Metadata Item for the key on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/images/%s/metadata/%s' % (self.url, image_id, key)
image_response = self.request('DELETE', url,
requestslib_kwargs=requestslib_kwargs)
return image_response

View File

@ -0,0 +1,42 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cloudcafe.common.models.configuration import ConfigSectionInterface
class ImagesConfig(ConfigSectionInterface):
SECTION_NAME = 'images'
@property
def primary_image(self):
"""Default image to be used when building servers in compute tests"""
return self.get("primary_image")
@property
def secondary_image(self):
"""Alternate image to be used in compute tests"""
return self.get("secondary_image")
@property
def image_status_interval(self):
"""Amount of time to wait between polling the status of an image"""
return int(self.get("image_status_interval"))
@property
def snapshot_timeout(self):
"""Length of time to wait before giving up on reaching a status"""
return int(self.get("snapshot_timeout"))

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,203 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
import xml.etree.ElementTree as ET
from cafe.engine.models.base import AutoMarshallingModel
from cloudcafe.compute.common.models.link import Links
from cloudcafe.compute.common.equality_tools import EqualityTools
from cloudcafe.compute.common.constants import Constants
from cloudcafe.compute.common.models.metadata import Metadata
class Image(AutoMarshallingModel):
ROOT_TAG = 'image'
def __init__(self, diskConfig, id, name, status, updated, created,
minDisk, minRam, progress, links=None, metadata=None,
server=None):
self.diskConfig = diskConfig
self.id = id
self.name = name
self.status = status
self.updated = updated
self.created = created
self.minDisk = minDisk
self.minRam = minRam
self.progress = progress
self.links = links
self.metadata = metadata
self.server = server
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: Image object to compare with
@type other: Image
@return: True if Image objects are equal, False otherwise
@rtype: bool
"""
return EqualityTools.are_objects_equal(self, other)
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: Image object to compare with
@type other: Image
@return: True if Image objects are not equal, False otherwise
@rtype: bool
"""
return not self == other
def __repr__(self):
values = []
for prop in __dict__:
values.append("%s: %s" % (prop, __dict__[prop]))
return '[' + ', '.join(values) + ']'
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
if 'image' in json_dict.keys():
image = cls._dict_to_obj(json_dict['image'])
return image
if 'images' in json_dict.keys():
images = []
for image_dict in json_dict['images']:
images.append(cls._dict_to_obj(image_dict))
return images
@classmethod
def _dict_to_obj(cls, json_dict):
image = Image(json_dict.get('OS-DCF:diskConfig'), json_dict.get('id'),
json_dict.get('name'), json_dict.get('status'),
json_dict.get('updated'), json_dict.get('created'),
json_dict.get('minDisk'), json_dict.get('minRam'),
json_dict.get('progress'))
if 'links' in json_dict:
image.links = Links._dict_to_obj(json_dict['links'])
if 'metadata' in json_dict:
image.metadata = Metadata._dict_to_obj(json_dict['metadata'])
if 'server' in json_dict:
from cloudcafe.compute.servers_api.models.servers import ServerMin
image.server = ServerMin._dict_to_obj(json_dict['server'])
return image
@classmethod
def _xml_to_obj(cls, serialized_str):
'''Returns an instance of a Image based on the xml serialized_str
passed in.'''
element = ET.fromstring(serialized_str)
cls._remove_xml_etree_namespace(element, Constants.XML_API_NAMESPACE)
cls._remove_xml_etree_namespace(element,
Constants.XML_API_ATOM_NAMESPACE)
cls._set_clean_xml_etree_attrs(element.attrib,
Constants.XML_API_DISK_CONFIG_NAMESPACE)
if element.tag == 'image':
image = cls._xml_ele_to_obj(element)
return image
if element.tag == 'images':
images = []
for image in element.findall('image'):
image = cls._xml_ele_to_obj(image)
images.append(image)
return images
@classmethod
def _xml_ele_to_obj(cls, element):
image_dict = element.attrib
if 'minDisk' in image_dict:
image_dict['minDisk'] = image_dict.get('minDisk') \
and int(image_dict.get('minDisk'))
if 'progress' in image_dict:
image_dict['progress'] = image_dict.get('progress') \
and int(image_dict.get('progress'))
if 'minRam' in image_dict:
image_dict['minRam'] = image_dict.get('minRam') \
and int(image_dict.get('minRam'))
links = None
metadata = None
server = None
if element.find('link') is not None:
links = Links._xml_ele_to_obj(element)
if element.find('metadata') is not None:
metadata = Metadata._xml_ele_to_obj(element)
if element.find('server') is not None:
'''To prevent circular import issue import just in time'''
from cloudcafe.compute.servers_api.models.servers import ServerMin
server = ServerMin._xml_ele_to_obj(element)
image = Image(image_dict.get('diskConfig'),
image_dict.get('id'), image_dict.get('name'),
image_dict.get('status'), image_dict.get('updated'),
image_dict.get('created'), image_dict.get('minDisk'),
image_dict.get('minRam'), image_dict.get('progress'),
links, metadata, server)
return image
class ImageMin(Image):
"""
@summary: Represents minimum details of a image
"""
def __init__(self, **kwargs):
'''Image min should only have id, name and links '''
for keys, values in kwargs.items():
setattr(self, keys, values)
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: ImageMin object to compare with
@type other: ImageMin
@return: True if ImageMin objects are equal, False otherwise
@rtype: bool
"""
return EqualityTools.are_objects_equal(self, other)
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: ImageMin object to compare with
@type other: ImageMin
@return: True if ImageMin objects are not equal, False otherwise
@rtype: bool
"""
return not self == other
@classmethod
def _xml_ele_to_obj(cls, element):
'''Helper method to turn ElementTree instance to Image instance.'''
cls._remove_xml_etree_namespace(element, Constants.XML_API_NAMESPACE)
image_dict = element.attrib
image_min = ImageMin(**image_dict)
image_min.links = Links._xml_ele_to_obj(element)
return image_min
@classmethod
def _dict_to_obj(cls, json_dict):
image_min = ImageMin(**json_dict)
if 'links' in json_dict:
image_min.links = Links(image_min.links)
return image_min

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,133 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.compute.limits_api.models.limit import Limits
class LimitsClient(AutoMarshallingRestClient):
def __init__(self, url, auth_token, serialize_format=None,
deserialize_format=None):
"""
@param url: Base URL for the compute service
@type url: String
@param auth_token: Auth token to be used for all requests
@type auth_token: String
@param serialize_format: Format for serializing requests
@type serialize_format: String
@param deserialize_format: Format for de-serializing responses
@type deserialize_format: String
"""
super(LimitsClient, self).__init__(serialize_format,
deserialize_format)
self.auth_token = auth_token
self.default_headers['X-Auth-Token'] = auth_token
ct = ''.join(['application/', self.serialize_format])
accept = ''.join(['application/', self.deserialize_format])
self.default_headers['Content-Type'] = ct
self.default_headers['Accept'] = accept
self.url = url
def get_limits(self, requestslib_kwargs=None):
"""
@summary: Returns limits.
@param requestslib_kwargs: Overrides any default values injected by
the framework
@type requestslib_kwargs:dict
@return: limit_response
@rtype: Limits Response Domain Object
"""
url = '%s/limits' % (self.url)
limit_response = self.request('GET', url,
response_entity_type=Limits,
requestslib_kwargs=requestslib_kwargs)
return limit_response
def _get_absolute_limits_property(self, limits_property=None):
"""
@summary: Returns the value of the specified key from the
absolute_limits dictionary
@param requestslib_kwargs: Overrides any default values injected by
the framework
@type requestslib_kwargs:dict
"""
if property is None:
return None
limits_response = self.get_limits()
absolute_limits = vars(limits_response.entity).get('absolute')
if absolute_limits is not None:
return absolute_limits.get(limits_property)
else:
return None
def get_max_server_meta(self):
"""
@summary: Returns maximum number of metadata allowed for a server
@return: Maximum number of server meta data
@rtype: Integer
"""
return self._get_absolute_limits_property('maxServerMeta')
def get_max_image_meta(self):
"""
@summary: Returns maximum number of metadata allowed for an Image.
@return: Maximum number of image meta data
@rtype: Integer
"""
return self._get_absolute_limits_property('maxImageMeta')
def get_personality_file_limit(self):
"""
@summary: Returns maximum number of personality files allowed for a
server
@return: Maximum number of personality files.
@rtype: Integer
"""
return self._get_absolute_limits_property('maxPersonality')
def get_personality_file_size_limit(self):
"""
@summary: Returns the maximum size of a personality file.
@return: Maximum size of a personality file.
@rtype: Integer
"""
return self._get_absolute_limits_property('maxPersonalitySize')
def get_max_total_instances(self):
"""
@summary: Returns maximum number of server allowed for a user
@return: Maximum number of server
@rtype: Integer
"""
return self._get_absolute_limits_property('maxTotalInstances')
def get_max_total_RAM_size(self):
"""
@summary: Returns maximum RAM size to create servers for a user
@return: Maximum RAM size
@rtype: Integer
"""
return self._get_absolute_limits_property('maxTotalRAMSize')
def get_total_RAM_used(self):
"""
@summary: Returns total RAM used by a user
@return: total RAM used
@rtype: Integer
"""
return self._get_absolute_limits_property('totalRAMUsed')

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,132 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
import xml.etree.ElementTree as ET
from cafe.engine.models.base import AutoMarshallingModel
from cloudcafe.compute.common.equality_tools import EqualityTools
from cloudcafe.compute.common.constants import Constants
class Limits(AutoMarshallingModel):
def __init__(self, **kwargs):
'''An object that represents Limits.
Keyword arguments:
'''
super(Limits, self).__init__(**kwargs)
for keys, values in kwargs.items():
setattr(self, keys, values)
def __repr__(self):
values = []
for prop in self.__dict__:
values.append("%s: %s" % (prop, self.__dict__[prop]))
return '[' + ', '.join(values) + ']'
def _obj_to_json(self):
'''
Automatically assigns any value that is an int or a str and is not
None to a dictionary. Then returns the str representation of that dict.
'''
'''TODO: Implement serialization of lists, dictionaries, and objects'''
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
'''need to code'''
pass
@classmethod
def _json_to_obj(cls, serialized_str):
'''Returns an instance of a Limits based on the json serialized_str
passed in.'''
json_dict = json.loads(serialized_str)
ret = None
if 'limits' in json_dict.keys():
ret = Limits(**(json_dict.get('limits')))
return ret
@classmethod
def _xml_to_obj(cls, serialized_str):
'''Returns a Limits Response Object based on xml serialized str'''
limits = {}
rate_list = []
absolute_dict = {}
#Removing namespaces
root = ET.fromstring(serialized_str)
cls._remove_namespace(root,
'http://docs.openstack.org/common/api/v1.0')
cls._remove_namespace(root,
'http://docs.openstack.org/common/api/v1.1')
#Rates Limits
rate_element_list = root.find('rates').findall('rate')
for rate_element in rate_element_list:
limit_element_list = rate_element.findall('limit')
limit_list = []
rate_dict = {}
for limit in limit_element_list:
limit_dict = {}
attrib = limit.attrib
for key in attrib.keys():
limit_dict[key] = attrib.get(key)
limit_list.append(limit_dict)
rate_dict['limit'] = limit_list
attrib = rate_element.attrib
for key in attrib.keys():
rate_dict[key] = attrib.get(key)
rate_list.append(rate_dict)
#Absolute Limits
absolute_list = root.find('absolute')
cls._remove_namespace(absolute_list, Constants.XML_API_ATOM_NAMESPACE)
used = 'http://docs.openstack.org/compute/ext/used_limits/api/v1.1'
cls._remove_namespace(absolute_list, used)
for element in absolute_list.findall('limit'):
attrib = element.attrib
absolute_dict[attrib.get('name')] = int(attrib.get('value'))
limits['absolute'] = absolute_dict
limits['rate'] = rate_list
return Limits(**limits)
@classmethod
def _dict_to_obj(cls, limits_dict):
'''Helper method to turn dictionary into Limits instance.'''
limits = Limits(**limits_dict)
return limits
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: Flavor object to compare with
@type other: Flavor
@return: True if Flavor objects are equal, False otherwise
@rtype: bool
"""
return EqualityTools.are_objects_equal(self, other)
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: Flavor object to compare with
@type other: Flavor
@return: True if Flavor objects are not equal, False otherwise
@rtype: bool
"""
return not self == other

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,203 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 time
from cloudcafe.compute.common.types import NovaServerStatusTypes as ServerStates
from cloudcafe.compute.common.datagen import rand_name
from cloudcafe.compute.common.exceptions import ItemNotFound, \
TimeoutException, BuildErrorException
class ServerBehaviors(object):
def __init__(self, servers_client, servers_config,
images_config, flavors_config):
self.config = servers_config
self.servers_client = servers_client
self.images_config = images_config
self.flavors_config = flavors_config
def create_active_server(self, name=None, image_ref=None, flavor_ref=None,
personality=None, metadata=None, accessIPv4=None,
accessIPv6=None, disk_config=None, networks=None):
'''
@summary:Creates a server and waits for server to reach active status
@param name: The name of the server.
@type name: String
@param image_ref: The reference to the image used to build the server.
@type image_ref: String
@param flavor_ref: The flavor used to build the server.
@type flavor_ref: String
@param metadata: A dictionary of values to be used as metadata.
@type metadata: Dictionary. The limit is 5 key/values.
@param personality: A list of dictionaries for files to be
injected into the server.
@type personality: List
@param accessIPv4: IPv4 address for the server.
@type accessIPv4: String
@param accessIPv6: IPv6 address for the server.
@type accessIPv6: String
@param disk_config: MANUAL/AUTO/None
@type disk_config: String
@return: Response Object containing response code and
the server domain object
@rtype: Request Response Object
'''
if name is None:
name = rand_name('testserver')
if image_ref is None:
image_ref = self.images_config.primary_image
if flavor_ref is None:
flavor_ref = self.flavors_config.primary_flavor
resp = self.servers_client.create_server(name, image_ref,
flavor_ref,
personality=personality,
metadata=metadata,
accessIPv4=accessIPv4,
accessIPv6=accessIPv6,
disk_config=disk_config,
networks=networks)
server_obj = resp.entity
resp = self.wait_for_server_status(server_obj.id,
ServerStates.ACTIVE)
# Add the password from the create request into the final response
resp.entity.admin_pass = server_obj.admin_pass
return resp
def wait_for_server_status(self, server_id, desired_status, timeout=None):
"""Polls server until the desired status is reached"""
if desired_status == ServerStates.DELETED:
return self.wait_for_server_to_be_deleted(server_id)
server_response = self.servers_client.get_server(server_id)
server_obj = server_response.entity
time_waited = 0
interval_time = self.config.server_status_interval
timeout = timeout or self.config.server_build_timeout
while (server_obj.status.lower() != desired_status.lower() and
server_obj.status.lower() != ServerStates.ERROR.lower() and
time_waited <= timeout):
server_response = self.servers_client.get_server(server_id)
server_obj = server_response.entity
time.sleep(interval_time)
time_waited += interval_time
if time_waited > timeout:
raise TimeoutException
if server_obj.status.lower() == ServerStates.ERROR.lower():
raise BuildErrorException(
'Build failed. Server with uuid %s entered ERROR status.' %
(server_id))
return server_response
def wait_for_server_error_status(self, server_id, desired_status,
timeout=None):
"""Polls a server until the desired status is reached"""
if desired_status == ServerStates.DELETED:
return self.wait_for_server_to_be_deleted(server_id)
server_response = self.servers_client.get_server(server_id)
server_obj = server_response.entity
time_waited = 0
interval_time = self.config.server_status_interval
timeout = timeout or self.config.compute_api.server_status_timeout
while (server_obj.status.lower() != desired_status.lower()
and time_waited <= timeout * 10):
server_response = self.servers_client.get_server(server_id)
server_obj = server_response.entity
time.sleep(interval_time)
time_waited += interval_time
return server_response
def wait_for_server_status_from_error(self, server_id, desired_status,
timeout=None):
if desired_status == ServerStates.DELETED:
return self.wait_for_server_to_be_deleted(server_id)
server_response = self.servers_client.get_server(server_id)
server_obj = server_response.entity
time_waited = 0
interval_time = self.config.compute_api.build_interval
timeout = timeout or self.config.compute_api.server_status_timeout
while (server_obj.status.lower() != desired_status.lower()
and time_waited <= timeout):
server_response = self.servers_client.get_server(server_id)
server_obj = server_response.entity
time.sleep(interval_time)
time_waited += interval_time
if time_waited > timeout:
raise TimeoutException(server_obj.status, server_obj.status,
id=server_obj.id)
return server_response
def wait_for_server_to_be_deleted(self, server_id):
time_waited = 0
interval_time = self.config.server_status_interval
try:
while (True):
server_response = self.servers_client.get_server(server_id)
server_obj = server_response.entity
if time_waited > self.config.server_build_timeout:
raise TimeoutException(
"Timed out while deleting server id: %s" % server_id)
if server_obj.status.lower() != ServerStates.ERROR.lower():
time.sleep(interval_time)
time_waited += interval_time
continue
if server_obj.status.lower() != ServerStates.ERROR.lower():
raise BuildErrorException(
"Server entered Error state while deleting, \
server id : %s" % server_id)
time.sleep(interval_time)
time_waited += interval_time
except ItemNotFound:
pass
def resize_and_await(self, server_id, new_flavor):
resp = self.servers_client.resize(server_id, new_flavor)
assert resp.status_code is 202
resized_server = self.wait_for_server_status(
server_id, ServerStates.VERIFY_RESIZE)
return resized_server.entity
def resize_and_confirm(self, server_id, new_flavor):
self.resize_and_await(server_id, new_flavor)
resp = self.servers_client.confirm_resize(server_id)
assert resp.status_code is 204
resized_server = self.wait_for_server_status(server_id,
ServerStates.ACTIVE)
return resized_server.entity
def resize_and_revert(self, server_id, new_flavor):
self.resize_and_await(server_id, new_flavor)
resp = self.servers_client.revert_resize(server_id)
assert resp.status_code is 202
resized_server = self.wait_for_server_status(server_id,
ServerStates.ACTIVE)
return resized_server.entity
def reboot_and_await(self, server_id, reboot_type):
resp = self.servers_client.reboot(server_id, reboot_type)
assert resp.status_code is 202
self.wait_for_server_status(server_id,
ServerStates.ACTIVE)
def change_password_and_await(self, server_id, new_password):
resp = self.servers_client.change_password(server_id, new_password)
assert resp.status_code is 202
self.wait_for_server_status(server_id,
ServerStates.ACTIVE)

View File

@ -0,0 +1,634 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 urlparse import urlparse
from cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.compute.common.models.metadata import Metadata
from cloudcafe.compute.common.models.metadata import MetadataItem
from cloudcafe.compute.servers_api.models.servers import Server
from cloudcafe.compute.servers_api.models.servers import Addresses
from cloudcafe.compute.servers_api.models.requests import CreateServer
from cloudcafe.compute.servers_api.models.requests import UpdateServer
from cloudcafe.compute.servers_api.models.requests import ChangePassword, \
ConfirmResize, Resize, Reboot, MigrateServer, Lock, Unlock, \
Start, Stop, Suspend, Resume, Pause, Unpause, CreateImage
class ServersClient(AutoMarshallingRestClient):
def __init__(self, url, auth_token, serialize_format=None,
deserialize_format=None):
"""
@param url: Base URL for the compute service
@type url: String
@param auth_token: Auth token to be used for all requests
@type auth_token: String
@param serialize_format: Format for serializing requests
@type serialize_format: String
@param deserialize_format: Format for de-serializing responses
@type deserialize_format: String
"""
super(ServersClient, self).__init__(serialize_format,
deserialize_format)
self.auth_token = auth_token
self.default_headers['X-Auth-Token'] = auth_token
ct = ''.join(['application/', self.serialize_format])
accept = ''.join(['application/', self.deserialize_format])
self.default_headers['Content-Type'] = ct
self.default_headers['Accept'] = accept
self.url = url
def list_servers(self, name=None, image=None, flavor=None,
status=None, marker=None, limit=None, changes_since=None,
requestslib_kwargs=None):
"""
@summary: Lists all servers with minimal details. Additionally,
can filter results by params. Maps to /servers
@param image: Image id to filter by
@type image: String
@param flavor: Flavor id to filter by
@type flavor: String
@param name: Server name to filter by
@type name: String
@param status: Server status to filter by
@type status: String
@param marker: Server id to be used as a marker for the next list
@type marker: String
@param limit: The maximum number of results to return
@type limit: Int
@param changes-since: Will only return servers where the updated time
is later than the changes-since parameter.
@return: server_response
@rtype: Response
"""
params = {'image': image, 'flavor': flavor, 'name': name,
'status': status, 'marker': marker,
'limit': limit, 'changes-since': changes_since}
url = '%s/servers' % (self.url)
server_response = self.request('GET', url, params=params,
response_entity_type=Server,
requestslib_kwargs=requestslib_kwargs)
return server_response
def list_servers_with_detail(self, image=None, flavor=None, name=None,
status=None, marker=None,
limit=None, changes_since=None,
requestslib_kwargs=None):
"""
@summary: Lists all servers with full details. Additionally,
can filter results by params. Maps to /servers/detail
@param image: Image id to filter by
@type image: String
@param flavor: Flavor id to filter by
@type flavor: String
@param name: Server name to filter by
@type name: String
@param status: Server status to filter by
@type status: String
@param marker: Server id to be used as a marker for the next list
@type marker: String
@param limit: The maximum number of results to return
@type limit: Int
@param changes-since: Will only return servers where the updated time
is later than the changes-since parameter.
@return: server_response
@rtype: Response
"""
params = {'image': image, 'flavor': flavor, 'name': name,
'status': status, 'marker': marker, 'limit': limit,
'changes-since': changes_since}
url = '%s/servers/detail' % (self.url)
server_response = self.request('GET', url, params=params,
response_entity_type=Server,
requestslib_kwargs=requestslib_kwargs)
return server_response
def get_server(self, server_id, requestslib_kwargs=None):
"""
@summary: Retrieves the details of the specified server
@param server_id: The id of an existing server
@type server_id: String
@return: server_response
@rtype: Response
"""
self.server_id = server_id
url_new = str(server_id)
url_scheme = urlparse(url_new).scheme
url = url_new if url_scheme else '%s/servers/%s' % (self.url,
self.server_id)
server_response = self.request('GET', url,
response_entity_type=Server,
requestslib_kwargs=requestslib_kwargs)
return server_response
def delete_server(self, server_id, requestslib_kwargs=None):
"""
@summary: Deletes the specified server
@param server_id: The id of a server
@type server_id: String
@return: server_response
@rtype: Response
"""
self.server_id = server_id
url = '%s/servers/%s' % (self.url, self.server_id)
server_response = self.request('DELETE', url,
requestslib_kwargs=requestslib_kwargs)
return server_response
def create_server(self, name, image_ref, flavor_ref, personality=None,
metadata=None, accessIPv4=None, accessIPv6=None,
disk_config=None, networks=None, admin_pass=None,
requestslib_kwargs=None):
"""
@summary: Creates an instance of a server given the
provided parameters
@param name: Name of the server
@type name: String
@param image_ref: Identifier for the image used to build the server
@type image_ref: String
@param flavor_ref: Identifier for the flavor used to build the server
@type flavor_ref: String
@param metadata: A dictionary of values to be used as server metadata
@type meta: Dictionary
@param personality: A list of dictionaries for files to be
injected into the server.
@type personality: List
@param accessIPv4: IPv4 address for the server.
@type accessIPv4: String
@param accessIPv6: IPv6 address for the server.
@type accessIPv6: String
@param disk_config: MANUAL/AUTO/None
@type disk_config: String
@return: Response Object containing response code and
the server domain object
@rtype: Response Object
"""
server_request_object = CreateServer(name=name, flavorRef=flavor_ref,
imageRef=image_ref,
personality=personality,
metadata=metadata,
accessIPv4=accessIPv4,
accessIPv6=accessIPv6,
diskConfig=disk_config,
networks=networks,
adminPass=admin_pass)
url = '%s/servers' % (self.url)
server_response = self.request('POST', url,
response_entity_type=Server,
request_entity=server_request_object,
requestslib_kwargs=requestslib_kwargs)
return server_response
def update_server(self, server_id, name=None, metadata=None,
accessIPv4=None, accessIPv6=None,
requestslib_kwargs=None):
"""
@summary: Updates the properties of an existing server.
@param server_id: The id of an existing server.
@type server_id: String
@param name: The name of the server.
@type name: String
@param meta: A dictionary of values to be used as metadata.
@type meta: Dictionary. The limit is 5 key/values.
@param ipv4: IPv4 address for the server.
@type ipv4: String
@param ipv6: IPv6 address for the server.
@type ipv6: String
@return: The response code and the updated Server .
@rtype: Integer(Response code) and Object(Server)
"""
self.server_id = server_id
url = '%s/servers/%s' % (self.url, self.server_id)
request = UpdateServer(name=name, metadata=metadata,
accessIPv4=accessIPv4, accessIPv6=accessIPv6)
resp = self.request('PUT', url,
response_entity_type=Server,
request_entity=request,
requestslib_kwargs=requestslib_kwargs)
return resp
def list_addresses(self, server_id, requestslib_kwargs=None):
"""
@summary: Lists all addresses for a server.
@param server_id: The id of an existing server.
@type server_id: String
@return: Response code and the Addresses
@rtype: Integer(Response code) and Object(Addresses)
"""
self.server_id = server_id
url = '%s/servers/%s/ips' % (self.url, self.server_id)
resp = self.request('GET', url,
response_entity_type=Addresses,
requestslib_kwargs=requestslib_kwargs)
return resp
def list_addresses_by_network(self, server_id, network_id,
requestslib_kwargs=None):
"""
@summary: Lists all addresses of a specific network type for a server.
@param server_id: The id of an existing server.
@type server_id: String
@param network_id: The ID of a network.
@type network_id: String
@return: Response code and the Addresses by network.
@rtype: Integer(Response code) and Object(Addresses)
"""
self.server_id = server_id
self.network_id = network_id
url = '%s/servers/%s/ips/%s' % (self.url, self.server_id,
self.network_id)
server_response = self.request('GET', url,
response_entity_type=Addresses,
requestslib_kwargs=requestslib_kwargs)
return server_response
def change_password(self, server_id, password, requestslib_kwargs=None):
'''
@summary: Changes the root password for the server.
@param server_id: The id of an existing server.
@type server_id: String
@param password: The new password.
@type password: String.
@return: Response Object containing response code and the empty
body on success
@rtype: Response Object
'''
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=ChangePassword(password),
requestslib_kwargs=requestslib_kwargs)
return resp
def reboot(self, server_id, reboot_type, requestslib_kwargs=None):
'''
@summary: Reboots the server - soft/hard based on reboot_type.
@param server_id: The id of an existing server.
@type server_id: String
@param reboot_type: Soft or Hard.
@type reboot_type: String.
@return: Response Object containing response code and the empty body
after the server reboot is applied
@rtype: Response Object
'''
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=Reboot(reboot_type),
requestslib_kwargs=requestslib_kwargs)
return resp
def rebuild(self, server_id, imageRef, name=None,
flavorRef=None, adminPass=None,
diskConfig=None, metadata=None,
personality=None, accessIPv4=None, accessIPv6=None,
requestslib_kwargs=None):
'''
@summary: Rebuilds the server
@param server_id: The id of an existing server.
@type server_id: String
@param name: The new name for the server
@type name: String
@param imageRef:The image ID.
@type imageRef: String
@param flavorRef:The flavor ID.
@type flavorRef: String
@param adminPass:The administrator password
@type adminPass: String
@param diskConfig:The disk configuration value, which is AUTO or MANUAL
@type diskConfig: String(AUTO/MANUAL)
@param metadata:A metadata key and value pair.
@type metadata: Dictionary
@param personality:The file path and file contents
@type personality: String
@param accessIPV4:The IP version 4 address.
@type accessIPV4: String
@param accessIPV6:The IP version 6 address
@type accessIPV6: String
@return: Response Object containing response code and
the server domain object
@rtype: Response Object
'''
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
rebuild_request_object = Rebuild(name=name, imageRef=imageRef,
flavorRef=flavorRef,
adminPass=adminPass,
diskConfig=diskConfig,
metadata=metadata,
personality=personality,
accessIPv4=accessIPv4,
accessIPv6=accessIPv6)
resp = self.request('POST', url,
response_entity_type=Server,
request_entity=rebuild_request_object,
requestslib_kwargs=requestslib_kwargs)
return resp
def resize(self, server_id, flavorRef, diskConfig=None,
requestslib_kwargs=None):
'''
@summary: Resizes the server to specified flavorRef.
@param server_id: The id of an existing server.
@type server_id: String
@param flavorRef: The flavor id.
@type flavorRef: String.
@return: Response Object containing response code and
the empty body after the server resize is applied
@rtype: Response Object
'''
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resize_request_object = Resize(flavorRef, diskConfig)
resp = self.request('POST', url,
request_entity=resize_request_object,
requestslib_kwargs=requestslib_kwargs)
return resp
def confirm_resize(self, server_id, requestslib_kwargs=None):
'''
@summary: Confirms resize of server
@param server_id: The id of an existing server.
@type server_id: String
@return: Response Object containing response code and the empty
body after the server resize is applied
@rtype: Response Object
'''
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
confirm_resize_request_object = ConfirmResize()
resp = self.request('POST', url,
request_entity=confirm_resize_request_object,
requestslib_kwargs=requestslib_kwargs)
return resp
def revert_resize(self, server_id, requestslib_kwargs=None):
'''
@summary: Reverts resize of the server
@param server_id: The id of an existing server.
@type server_id: String
@return: Response Object containing response code and the empty body
after the server resize is applied
@rtype: Response Object
'''
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=RevertResize(),
requestslib_kwargs=requestslib_kwargs)
return resp
def migrate_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=MigrateServer(),
requestslib_kwargs=requestslib_kwargs)
return resp
def live_migrate_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=MigrateServer(),
requestslib_kwargs=requestslib_kwargs)
return resp
def lock_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=Lock(),
requestslib_kwargs=requestslib_kwargs)
return resp
def unlock_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=Unlock(),
requestslib_kwargs=requestslib_kwargs)
return resp
def stop_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=Stop(),
requestslib_kwargs=requestslib_kwargs)
return resp
def start_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=Start(),
requestslib_kwargs=requestslib_kwargs)
return resp
def suspend_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=Suspend(),
requestslib_kwargs=requestslib_kwargs)
return resp
def resume_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=Resume(),
requestslib_kwargs=requestslib_kwargs)
return resp
def pause_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=Pause(),
requestslib_kwargs=requestslib_kwargs)
return resp
def unpause_server(self, server_id, requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=Unpause(),
requestslib_kwargs=requestslib_kwargs)
return resp
def reset_state(self, server_id, reset_state='error',
requestslib_kwargs=None):
self.server_id = server_id
url = '%s/servers/%s/action' % (self.url, self.server_id)
resp = self.request('POST', url,
request_entity=MigrateServer(),
requestslib_kwargs=requestslib_kwargs)
return resp
def create_image(self, server_id, name=None, metadata=None,
requestslib_kwargs=None):
'''
@summary: Creates snapshot of the server
@param server_id: The id of an existing server.
@type server_id: String
@param: metadata: A metadata key and value pair.
@type: Metadata Object
@return: Response Object containing response code and the empty body
after the server resize is applied
@rtype: Response Object
'''
if name is None:
name = 'new_image'
self.server_id = server_id
if name is None:
name = rand_name("TestImage")
url = '%s/servers/%s/action' % (self.url, self.server_id)
create_image_request_object = CreateImage(name, metadata)
resp = self.request('POST', url,
request_entity=create_image_request_object,
requestslib_kwargs=requestslib_kwargs)
return resp
def list_server_metadata(self, server_id, requestslib_kwargs=None):
'''
@summary: Returns metadata associated with an server
@param server_id: server ID
@type server_id:String
@return: Metadata associated with an server on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/servers/%s/metadata' % (self.url, server_id)
server_response = self.request('GET', url,
response_entity_type=Metadata,
requestslib_kwargs=requestslib_kwargs)
return server_response
def set_server_metadata(self, server_id, metadata,
requestslib_kwargs=None):
'''
@summary: Sets metadata for the specified server
@param server_id: server ID
@type server_id:String
@param metadata: Metadata to be set for an server
@type metadata: dictionary
@return: Metadata associated with an server on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/servers/%s/metadata' % (self.url, server_id)
request_metadata_object = Metadata(metadata)
server_response = self.request('PUT', url,
response_entity_type=Metadata,
request_entity=request_metadata_object,
requestslib_kwargs=requestslib_kwargs)
return server_response
def update_server_metadata(self, server_id, metadata,
requestslib_kwargs=None):
'''
@summary: Updates metadata items for the specified server
@param server_id: server ID
@type server_id:String
@param metadata: Metadata to be updated for an server
@type metadata: dictionary
@return: Metadata associated with an server on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/servers/%s/metadata' % (self.url, server_id)
request_metadata_object = Metadata(metadata)
resp = self.request('POST', url,
response_entity_type=Metadata,
request_entity=request_metadata_object,
requestslib_kwargs=requestslib_kwargs)
return resp
def get_server_metadata_item(self, server_id, key,
requestslib_kwargs=None):
'''
@summary: Retrieves a single metadata item by key
@param server_id: server ID
@type server_id:String
@param key: Key for which metadata item needs to be retrieved
@type key: String
@return: Metadata Item for a key on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/servers/%s/metadata/%s' % (self.url, server_id, key)
server_response = self.request('GET', url,
response_entity_type=MetadataItem,
requestslib_kwargs=requestslib_kwargs)
return server_response
def set_server_metadata_item(self, server_id, key, value,
requestslib_kwargs=None):
'''
@summary: Sets a metadata item for a specified server
@param server_id: server ID
@type server_id:String
@param key: Key for which metadata item needs to be set
@type key: String
@return: Metadata Item for the key on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/servers/%s/metadata/%s' % (self.url, server_id, key)
request = MetadataItem({key: value})
server_response = self.request('PUT', url,
response_entity_type=MetadataItem,
request_entity=request,
requestslib_kwargs=requestslib_kwargs)
return server_response
def delete_server_metadata_item(self, server_id, key,
requestslib_kwargs=None):
'''
@summary: Sets a metadata item for a specified server
@param server_id: server ID
@type server_id:String
@param key: Key for which metadata item needs to be set
@type key: String
@return: Metadata Item for the key on success
@rtype: Response object with metadata dictionary as entity
'''
url = '%s/servers/%s/metadata/%s' % (self.url, server_id, key)
server_response = self.request('DELETE', url,
requestslib_kwargs=requestslib_kwargs)
return server_response

View File

@ -0,0 +1,64 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cloudcafe.common.models.configuration import ConfigSectionInterface
class ServersConfig(ConfigSectionInterface):
SECTION_NAME = 'servers'
@property
def server_status_interval(self):
"""Amount of time to wait between polling the status of a server"""
return int(self.get("server_status_interval"))
@property
def server_build_timeout(self):
"""
Length of time to wait before timing out on a server reaching
the ACTIVE state
"""
return int(self.get("server_build_timeout"))
@property
def server_resize_timeout(self):
"""
Length of time to wait before timing out on a server reaching
the VERIFY_RESIZE state
"""
return int(self.get("server_resize_timeout"))
@property
def network_for_ssh(self):
"""
Name of network to be used for remote connections
(ie. public, private)
"""
return self.get("network_for_ssh")
@property
def ip_address_version_for_ssh(self):
"""
IP address version to be used for remote connections
(ie. 4, 6)
"""
return self.get("ip_address_version_for_ssh")
@property
def instance_disk_path(self):
"""Primary disk path of instances under test"""
return self.get("instance_disk_path")

View File

@ -0,0 +1,15 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,596 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
import xml.etree.ElementTree as ET
from cafe.engine.models.base import AutoMarshallingModel
from cloudcafe.compute.common.constants import Constants
from cloudcafe.compute.common.models.metadata import Metadata
class CreateServer(AutoMarshallingModel):
ROOT_TAG = 'server'
def __init__(self, name, imageRef, flavorRef, adminPass=None,
diskConfig=None, metadata=None, personality=None,
accessIPv4=None, accessIPv6=None, networks=None):
super(CreateServer, self).__init__()
self.name = name
self.imageRef = imageRef
self.flavorRef = flavorRef
self.diskConfig = diskConfig
self.adminPass = adminPass
self.metadata = metadata
self.personality = personality
self.accessIPv4 = accessIPv4
self.accessIPv6 = accessIPv6
self.networks = networks
def _obj_to_json(self):
body = {}
body['name'] = self.name
body['imageRef'] = self.imageRef
body['flavorRef'] = self.flavorRef
if self.diskConfig is not None:
body['OS-DCF:diskConfig'] = self.diskConfig
if self.adminPass is not None:
body['adminPass'] = self.adminPass
if self.metadata is not None:
body['metadata'] = self.metadata
if self.accessIPv4 is not None:
body['accessIPv4'] = self.accessIPv4
if self.accessIPv6 is not None:
body['accessIPv6'] = self.accessIPv6
if self.personality is not None:
body['personality'] = self.personality
if self.networks is not None:
body['networks'] = self.networks
return json.dumps({self.ROOT_TAG: body})
def _obj_to_xml(self):
element = ET.Element(self.ROOT_TAG)
xml = Constants.XML_HEADER
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('name', self.name)
element.set('imageRef', self.imageRef)
element.set('flavorRef', self.flavorRef)
if self.adminPass is not None:
element.set('adminPass', self.adminPass)
if self.diskConfig is not None:
element.set('xmlns:OS-DCF',
Constants.XML_API_DISK_CONFIG_NAMESPACE)
element.set('OS-DCF:diskConfig', self.diskConfig)
if self.metadata is not None:
meta_ele = ET.Element('metadata')
for key, value in self.metadata.items():
meta_ele.append(Metadata._dict_to_xml(key, value))
element.append(meta_ele)
if self.networks is not None:
networks_ele = ET.Element('networks')
for network_id in self.networks:
network = ET.Element('network')
network.set('uuid', network_id['uuid'])
networks_ele.append(network)
element.append(networks_ele)
if self.personality is not None:
personality_ele = ET.Element('personality')
personality_ele.append(Personality._obj_to_xml(self.personality))
element.append(personality_ele)
if self.accessIPv4 is not None:
element.set('accessIPv4', self.accessIPv4)
if self.accessIPv6 is not None:
element.set('accessIPv6', self.accessIPv6)
xml += ET.tostring(element)
return xml
class UpdateServer(AutoMarshallingModel):
ROOT_TAG = 'server'
def __init__(self, name=None, metadata=None,
accessIPv4=None, accessIPv6=None):
self.name = name
self.metadata = metadata
self.accessIPv4 = accessIPv4
self.accessIPv6 = accessIPv6
def _obj_to_json(self):
return json.dumps(self._auto_to_dict())
def _obj_to_xml(self):
element = ET.Element(self.ROOT_TAG)
xml = Constants.XML_HEADER
element.set('xmlns', Constants.XML_API_NAMESPACE)
if self.name is not None:
element.set('name', self.name)
if self.metadata is not None:
meta_ele = ET.Element('metadata')
for key, value in self.metadata.items():
meta_ele.append(Metadata._dict_to_xml(key, value))
element.append(meta_ele)
if self.accessIPv4 is not None:
element.set('accessIPv4', self.accessIPv4)
if self.accessIPv6 is not None:
element.set('accessIPv6', self.accessIPv6)
xml += ET.tostring(element)
return xml
class Reboot(AutoMarshallingModel):
ROOT_TAG = 'reboot'
def __init__(self, reboot_type):
self.type = reboot_type
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element(self.ROOT_TAG)
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('type', self.type)
xml += ET.tostring(element)
return xml
class Personality(AutoMarshallingModel):
'''
@summary: Personality Request Object for Server
'''
ROOT_TAG = 'personality'
def __init__(self, type):
self.type = type
@classmethod
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
@classmethod
def _obj_to_xml(self, list_dicts):
for pers_dict in list_dicts:
pers_element = ET.Element('file')
pers_element.set('path', pers_dict.get('path'))
pers_element.text = pers_dict.get('contents')
return pers_element
class Rebuild(CreateServer):
'''
@summary: Rebuild Request Object for Server
'''
ROOT_TAG = 'rebuild'
def __init__(self, name, imageRef, flavorRef, adminPass, diskConfig=None,
metadata=None, personality=None, accessIPv4=None,
accessIPv6=None):
super(Rebuild, self).__init__(name=name, imageRef=imageRef,
flavorRef=flavorRef, adminPass=adminPass,
diskConfig=diskConfig, metadata=metadata,
personality=personality,
accessIPv4=accessIPv4,
accessIPv6=accessIPv6)
class Resize(AutoMarshallingModel):
'''
@summary: Resize Request Object for Server
'''
ROOT_TAG = 'resize'
def __init__(self, flavorRef, diskConfig=None):
self.flavorRef = flavorRef
self.diskConfig = diskConfig
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element(self.ROOT_TAG)
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('flavorRef', self.flavorRef)
if self.diskConfig is not None:
element.set('xmlns:OS-DCF', Constants.XML_API_ATOM_NAMESPACE)
element.set('OS-DCF:diskConfig', self.diskConfig)
xml += ET.tostring(element)
return xml
class ResetState(AutoMarshallingModel):
'''
@summary: Reset State Request Object for Server
'''
ROOT_TAG = 'os-resetState'
def __init__(self, state):
self.state = state
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element(self.ROOT_TAG)
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('state', self.state)
xml += ET.tostring(element)
return xml
class ConfirmResize(AutoMarshallingModel):
'''
@summary: Confirm Resize Request Object for Server
'''
ROOT_TAG = 'confirmResize'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
# element = self._auto_to_xml()
element = ET.Element(self.ROOT_TAG)
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class RevertResize(AutoMarshallingModel):
'''
@summary: Revert Resize Request Object for Server
'''
ROOT_TAG = 'revertResize'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element(self.ROOT_TAG)
# element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class MigrateServer(AutoMarshallingModel):
'''
@summary: Migrate Server Request Object
'''
ROOT_TAG = 'migrate'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class ConfirmServerMigration(AutoMarshallingModel):
'''
@summary: Confirm Server Migration Request Object
'''
ROOT_TAG = 'confirmResize'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class Lock(AutoMarshallingModel):
'''
@summary: Lock Server Request Object
'''
ROOT_TAG = 'lock'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class Unlock(AutoMarshallingModel):
'''
@summary: Unlock Server Request Object
'''
ROOT_TAG = 'unlock'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class Start(AutoMarshallingModel):
'''
@summary: Start Server Request Object
'''
ROOT_TAG = 'os-start'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class Stop(AutoMarshallingModel):
'''
@summary: Stop Server Request Object
'''
ROOT_TAG = 'os-stop'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class Suspend(AutoMarshallingModel):
'''
@summary: Suspend Server Request Object
'''
ROOT_TAG = 'suspend'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class Resume(AutoMarshallingModel):
'''
@summary: Resume Server Request Object
'''
ROOT_TAG = 'resume'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class Pause(AutoMarshallingModel):
'''
@summary: Pause Server Request Object
'''
ROOT_TAG = 'pause'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class Unpause(AutoMarshallingModel):
'''
@summary: Unpause Server Request Object
'''
ROOT_TAG = 'unpause'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = self._auto_to_xml()
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
xml += ET.tostring(element)
return xml
class RescueMode(AutoMarshallingModel):
'''
Rescue Server Action Request Object
'''
ROOT_TAG = 'rescue'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element(self.ROOT_TAG)
element.set('xmlns', Constants.XML_API_RESCUE)
xml += ET.tostring(element)
return xml
class ExitRescueMode(AutoMarshallingModel):
'''
Exit Rescue Action Request Object
'''
ROOT_TAG = 'unrescue'
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element(self.ROOT_TAG)
element.set('xmlns', Constants.XML_API_UNRESCUE)
xml += ET.tostring(element)
return xml
class CreateImage(AutoMarshallingModel):
'''
Create Image Server Action Request Object
'''
ROOT_TAG = 'createImage'
def __init__(self, name, metadata=None):
self.name = name
self.metadata = metadata
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element(self.ROOT_TAG)
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('xmlns:atom', Constants.XML_API_ATOM_NAMESPACE)
element.set('name', self.name)
if self.metadata is not None:
meta_ele = ET.Element('metadata')
for key, value in self.metadata.items():
meta_ele.append(Metadata._dict_to_xml(key, value))
element.append(meta_ele)
xml += ET.tostring(element)
return xml
class ChangePassword(AutoMarshallingModel):
ROOT_TAG = 'changePassword'
def __init__(self, adminPassword):
self.adminPass = adminPassword
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
xml = Constants.XML_HEADER
element = ET.Element(self.ROOT_TAG)
element.set('xmlns', Constants.XML_API_NAMESPACE)
element.set('adminPass', self.adminPass)
xml += ET.tostring(element)
return xml
class AddFixedIP(AutoMarshallingModel):
'''
Add Fixed IP Action Request Object
'''
ROOT_TAG = 'addFixedIp'
def __init__(self, networkId):
self.networkId = networkId
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
#TODO: Implement when xml is known
raise NotImplementedError
class RemoveFixedIP(AutoMarshallingModel):
'''
Remove Fixed IP Action Request Object
'''
ROOT_TAG = 'removeFixedIp'
def __init__(self, networkId):
self.networkId = networkId
def _obj_to_json(self):
ret = self._auto_to_dict()
return json.dumps(ret)
def _obj_to_xml(self):
#TODO: Implement when xml is known
raise NotImplementedError

View File

@ -0,0 +1,407 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
import re
import xml.etree.ElementTree as ET
from cafe.engine.models.base import BaseModel
from cafe.engine.models.base import AutoMarshallingModel
from cloudcafe.compute.common.models.link import Links
from cloudcafe.compute.flavors_api.models.flavor import Flavor, FlavorMin
from cloudcafe.compute.images_api.models.image import Image, ImageMin
from cloudcafe.compute.common.equality_tools import EqualityTools
from cloudcafe.compute.common.constants import Constants
from cloudcafe.compute.common.models.metadata import Metadata
class Server(AutoMarshallingModel):
ROOT_TAG = 'server'
def __init__(self, id, diskConfig, power_state, progress, task_state,
vm_state, name, tenantId, status, updated, created, hostId,
user_id, accessIPv4, accessIPv6, addresses, flavor, image,
links, metadata, admin_pass):
self.diskConfig = diskConfig
try:
self.power_state = int(power_state)
except TypeError:
self.power_state = 0
self.progress = progress
self.task_state = task_state
self.vm_state = vm_state
self.name = name
self.id = id
self.tenant_id = tenantId
self.status = status
self.updated = updated
self.created = created
self.host_id = hostId
self.user_id = user_id
self.accessIPv4 = accessIPv4
self.accessIPv6 = accessIPv6
self.addresses = addresses
self.flavor = flavor
self.image = image
self.links = links
self.metadata = metadata
self.admin_pass = admin_pass
@classmethod
def _json_to_obj(cls, serialized_str):
'''
Returns an instance of a Server based on the json serialized_str
passed in
'''
ret = None
json_dict = json.loads(serialized_str)
if 'server' in json_dict.keys():
ret = cls._dict_to_obj(json_dict['server'])
if 'servers' in json_dict.keys():
ret = []
for server in json_dict['servers']:
s = cls._dict_to_obj(server)
ret.append(s)
return ret
@classmethod
def _xml_to_obj(cls, serialized_str):
'''
Returns an instance of a Server based on the xml serialized_str
passed in
'''
element = ET.fromstring(serialized_str)
cls._remove_xml_etree_namespace(
element, Constants.XML_API_NAMESPACE)
cls._remove_xml_etree_namespace(
element, Constants.XML_API_EXTENDED_STATUS_NAMESPACE)
cls._remove_xml_etree_namespace(
element, Constants.XML_API_DISK_CONFIG_NAMESPACE)
cls._remove_xml_etree_namespace(
element, Constants.XML_API_ATOM_NAMESPACE)
if element.tag == 'server':
ret = cls._xml_ele_to_obj(element)
if element.tag == 'servers':
ret = []
for server in element.findall('server'):
s = cls._xml_ele_to_obj(server)
ret.append(s)
return ret
@classmethod
def _xml_ele_to_obj(cls, element):
'''Helper method to turn ElementTree instance to Server instance.'''
server_dict = element.attrib
addresses = None
flavor = None
image = None
links = None
metadata = None
links = Links._xml_ele_to_obj(element)
if element.find('addresses') is not None:
addresses = Addresses._xml_ele_to_obj(element.find('addresses'))
if element.find('flavor') is not None:
flavor = Flavor._xml_ele_to_obj(element.find('flavor'))
if element.find('image') is not None:
image = Image._xml_ele_to_obj(element.find('image'))
if element.find('metadata') is not None:
metadata = Metadata._xml_ele_to_obj(element)
if 'progress' in server_dict:
progress = server_dict.get('progress') \
and int(server_dict.get('progress'))
if 'tenantId' in server_dict:
tenant_id = server_dict.get('tenantId')
if 'userId' in server_dict:
user_id = server_dict.get('userId')
server = Server(server_dict['id'], server_dict['diskConfig'],
server_dict['power_state'], progress,
server_dict['task_state'],
server_dict['vm_state'],
server_dict['name'], tenant_id,
server_dict['status'], server_dict['updated'],
server_dict['created'], server_dict['hostId'],
user_id, server_dict['accessIPv4'],
server_dict['accessIPv6'], addresses, flavor,
image, links, metadata)
return server
@classmethod
def _dict_to_obj(cls, server_dict):
'''Helper method to turn dictionary into Server instance.'''
addresses = None
flavor = None
image = None
links = None
metadata = None
if 'links' in server_dict:
links = Links._dict_to_obj(server_dict['links'])
if 'addresses' in server_dict:
addresses = Addresses._dict_to_obj(server_dict['addresses'])
if 'flavor' in server_dict:
flavor = FlavorMin._dict_to_obj(server_dict['flavor'])
if 'image' in server_dict:
image = ImageMin._dict_to_obj(server_dict['image'])
if 'metadata' in server_dict:
metadata = Metadata._dict_to_obj(server_dict['metadata'])
server = Server(
server_dict['id'], server_dict.get('OS-DCF:diskConfig'),
server_dict.get('OS-EXT-STS:power_state'),
server_dict.get('progress', 0),
server_dict.get('OS-EXT-STS:task_state'),
server_dict.get('OS-EXT-STS:vm_state'),
server_dict.get('name'), server_dict.get('tenant_id'),
server_dict.get('status'), server_dict.get('updated'),
server_dict.get('created'), server_dict.get('hostId'),
server_dict.get('user_id'), server_dict.get('accessIPv4'),
server_dict.get('accessIPv6'), addresses, flavor,
image, links, metadata, server_dict.get('adminPass'))
for each in server_dict:
if each.startswith("{"):
newkey = re.split("}", each)[1]
setattr(server, newkey, server_dict[each])
return server
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: Server object to compare with
@type other: Server
@return: True if Server objects are equal, False otherwise
@rtype: bool
"""
return EqualityTools.are_objects_equal(self, other,
['adminPass', 'updated',
'progress'])
def __ne__(self, other):
"""
@summary: Overrides the default not-equals
@param other: Server object to compare with
@type other: Server
@return: True if Server objects are not equal, False otherwise
@rtype: bool
"""
return not self == other
def min_details(self):
"""
@summary: Get the Minimum details of server
@return: Minimum details of server
@rtype: ServerMin
"""
return ServerMin(name=self.name, id=self.id, links=self.links)
class ServerMin(Server):
"""
@summary: Represents minimum details of a server
"""
def __init__(self, **kwargs):
for keys, values in kwargs.items():
setattr(self, keys, values)
def __eq__(self, other):
"""
@summary: Overrides the default equals
@param other: ServerMin object to compare with
@type other: ServerMin
@return: True if ServerMin objects are equal, False otherwise
@rtype: bool
"""
return EqualityTools.are_objects_equal(self, other)
def __ne__(self, other):
"""
@summary: Overrides the default equals
@param other: ServerMin object to compare with
@type other: ServerMin
@return: True if ServerMin objects are not equal, False otherwise
@rtype: bool
"""
return not self == other
@classmethod
def _xml_ele_to_obj(cls, element):
'''Helper method to turn ElementTree instance to Server instance.'''
if element.find('server') is not None:
element = element.find('server')
server_dict = element.attrib
servermin = ServerMin(**server_dict)
servermin.links = Links._xml_ele_to_obj(element)
return servermin
@classmethod
def _dict_to_obj(cls, server_dict):
'''Helper method to turn dictionary into Server instance.'''
servermin = ServerMin(**server_dict)
if hasattr(servermin, 'links'):
servermin.links = Links._dict_to_obj(servermin.links)
'''
Parse for those keys which have the namespace prefixed,
strip the namespace out
and take only the actual values such as diskConfig,
power_state and assign to server obj
'''
for each in server_dict:
if each.startswith("{"):
newkey = re.split("}", each)[1]
setattr(servermin, newkey, server_dict[each])
return servermin
#New Version
class Addresses(AutoMarshallingModel):
ROOT_TAG = 'addresses'
class _NetworkAddressesList(BaseModel):
def __init__(self):
super(Addresses._NetworkAddressesList, self).__init__()
self.addresses = []
def __repr__(self):
ret = ''
for a in self.addresses:
ret = ret + 'Address:\n\t%s' % str(a)
return ret
def append(self, addr_obj):
self.addresses.append(addr_obj)
@property
def ipv4(self):
for addr in self.addresses:
if str(addr.version) == '4':
return str(addr.addr)
return None
@property
def ipv6(self):
for addr in self.addresses:
if str(addr.version) == '6':
return str(addr.addr)
return None
@property
def count(self):
return len(self.addresses)
class _AddrObj(BaseModel):
def __init__(self, version=None, addr=None):
super(Addresses._AddrObj, self).__init__()
self.version = version
self.addr = addr
def __repr__(self):
ret = ''
ret = ret + 'version: %s' % str(self.version)
ret = ret + 'addr: %s' % str(self.addr)
return ret
def __init__(self, addr_dict):
super(Addresses, self).__init__()
#Preset properties that should be expected, if not always populated
self.public = None
self.private = None
if len(addr_dict) > 1:
''' adddress_type is PUBLIC/PRIVATE '''
for address_type in addr_dict:
''' address_list is list of address dictionaries'''
address_list = addr_dict[address_type]
''' init a network object with empty addresses list '''
network = self._NetworkAddressesList()
for address in address_list:
addrobj = self._AddrObj(
version=int(address.get('version')),
addr=address.get('addr'))
network.addresses.append(addrobj)
setattr(self, address_type, network)
# Validation in case we have nested addresses in addresses
else:
big_addr_dict = addr_dict
if big_addr_dict.get('addresses') is not None:
addr_dict = big_addr_dict.get('addresses')
for address_type in addr_dict:
''' address_list is list of address dictionaries'''
address_list = addr_dict[address_type]
''' init a network object with empty addresses list '''
network = self._NetworkAddressesList()
for address in address_list:
addrobj = self._AddrObj(version=address.get('version'),
addr=address.get('addr'))
network.addresses.append(addrobj)
setattr(self, address_type, network)
def get_by_name(self, label):
try:
ret = getattr(self, label)
except AttributeError:
ret = None
return ret
def __repr__(self):
ret = '\n'
ret = ret + '\npublic:\n\t\t%s' % str(self.public)
ret = ret + '\nprivate:\n\t\t%s' % str(self.private)
return ret
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
return Addresses(json_dict)
@classmethod
def _dict_to_obj(cls, serialized_str):
return Addresses(serialized_str)
@classmethod
def _xml_to_obj(cls, serialized_str):
element = ET.fromstring(serialized_str)
cls._remove_xml_etree_namespace(element, Constants.XML_API_NAMESPACE)
return cls._xml_ele_to_obj(element)
@classmethod
def _xml_ele_to_obj(cls, element):
addresses = {}
if element.tag != 'network':
networks = element.findall('network')
for network in networks:
network_id = network.attrib.get('id')
addresses[network_id] = []
for ip in network:
addresses[network_id].append(ip.attrib)
else:
networks = element
network_id = networks.attrib.get('id')
addresses[network_id] = []
for ip in networks:
addresses[network_id].append(ip.attrib)
return Addresses(addresses)

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,38 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
from cafe.engine.models.base import AutoMarshallingModel
class VolumeAttachmentRequest(AutoMarshallingModel):
def __init__(self, volume_id=None, device=None):
self.id = None
self.server_id = None
self.volume_id = volume_id
self.device = device
def _obj_to_json(self):
return json.dumps(self._obj_to_json_ele())
def _obj_to_json_ele(self):
sub_body = {"volumeId": self.volume_id}
sub_body["device"] = self.device
sub_body = self._set_clean_json_dict_attrs(sub_body)
body = {"volumeAttachment": sub_body}
body = self._set_clean_json_dict_attrs(body)
return json.dumps(body)

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,75 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
from cafe.engine.models.base import \
AutoMarshallingModel, AutoMarshallingListModel
class VolumeAttachment(AutoMarshallingModel):
def __init__(self, id_=None, volume_id=None, server_id=None, device=None):
self.id_ = None
self.server_id = None
self.volume_id = volume_id
self.device = device
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
return VolumeAttachment(
id_=json_dict.get('id'),
volume_id=json_dict.get('volumeId'),
server_id=json_dict.get('serverId'),
device=json_dict.get('device'))
class VolumeAttachmentListResponse(AutoMarshallingListModel):
@classmethod
def _json_to_obj(cls, serialized_str):
'''
Handles both the single and list version of the Volume
call, obviating the need for separate domain objects for "Volumes"
and "Lists of Volumes" responses.
Returns a list-like VolumeAttachmentListResponse
of VolumeAttachment objects, even if there is only one volume
attachment present.
'''
json_dict = json.loads(serialized_str)
is_list = True if json_dict.get('volumeAttachments', None) else False
va_list = VolumeAttachmentListResponse()
if is_list:
for volume_attachment in json_dict.get('volumeAttachments'):
va = VolumeAttachment(
id_=volume_attachment.get('id'),
volume_id=volume_attachment.get('volumeId'),
server_id=volume_attachment.get('serverId'),
device=volume_attachment.get('device'))
va_list.append(va)
else:
volume_attachment = json_dict.get('volumeAttachment')
va_list.append(
VolumeAttachment(
id_=volume_attachment.get('id'),
volume_id=volume_attachment.get('volumeId'),
server_id=volume_attachment.get('serverId'),
device=volume_attachment.get('device')))
va_list.append(va)
return va_list

View File

@ -0,0 +1,96 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.compute.volume_attachments_api.models.requests.volume_attachments \
import VolumeAttachmentRequest
from cloudcafe.compute.volume_attachments_api.models.responses.volume_attachments \
import VolumeAttachmentListResponse
class VolumeAttachmentsAPIClient(AutoMarshallingRestClient):
def __init__(self, url, auth_token, tenant_id, serialize_format=None,
deserialize_format=None):
super(VolumeAttachmentsAPIClient, self).__init__(
serialize_format, deserialize_format)
self.url = url
self.auth_token = auth_token
self.tenant_id = tenant_id
self.default_headers['X-Auth-Token'] = auth_token
self.default_headers['Content-Type'] = 'application/{0}'.format(
self.serialize_format)
self.default_headers['Accept'] = 'application/{0}'.format(
self.deserialize_format)
def attach_volume(self, server_id, volume_id, device=None,
requestslib_kwargs=None):
'''POST
v2/{tenant_id}/servers/{server_id}/os-volume_attachments
'''
url = '{0}/servers/{1}/os-volume_attachments'.format(
self.url, server_id)
va = VolumeAttachmentRequest(volume_id, device)
return self.request(
'POST', url, response_entity_type=VolumeAttachmentListResponse,
request_entity=va, requestslib_kwargs=requestslib_kwargs)
def delete_volume_attachment(self, attachment_id, server_id,
requestslib_kwargs=None):
'''DELETE
v2/servers/{server_id}/os-volume_attachments/{attachment_id}
'''
url = '{0}/servers/{1}/os-volume_attachments/{2}'.format(
self.url, server_id, attachment_id)
params = {
'tenant_id': self.tenant_id, 'server_id': server_id,
'attachment_id': attachment_id}
return self.request(
'DELETE', url, params=params,
requestslib_kwargs=requestslib_kwargs)
def get_server_volume_attachments(self, server_id,
requestslib_kwargs=None):
'''GET
v2/servers/{server_id}/os-volume_attachments/
'''
url = '{0}/servers/{1}/os-volume_attachments'.format(
self.url, server_id)
params = {'tenant_id': self.tenant_id, 'server_id': server_id}
return self.request(
'GET', url, params=params, requestslib_kwargs=requestslib_kwargs)
def get_volume_attachment_details(self, attachment_id, server_id,
requestslib_kwargs=None):
url = '{0}/servers/{1}/os-volume_attachments/{2}'.format(
self.url, server_id, attachment_id)
params = {'tenant_id': self.tenant_id,
'server_id': server_id,
'attachment_id': attachment_id}
return self.request(
'GET', url, params=params, requestslib_kwargs=requestslib_kwargs)

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,22 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
'''
@summary: Classes and Utilities used to manage common Domain Level Objects
@note: Primarily used to pass data between L{ccengine.providers}, L{ccengine.clients} and L{testrepo} Tests
@note: Also used to perform JSON or XML Marshaling and Validation
@note: Consumed by L{ccengine.providers}, L{ccengine.clients}, L{ccengine.common.connectors} and L{testrepo} Tests
'''

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,43 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.behaviors import BaseBehavior, behavior
from cloudcafe.identity.v2_0.tokens_api.client import TokenAPI_Client
from cloudcafe.identity.v2_0.tokens_api.config import TokenAPI_Config
class TokenAPI_Behaviors(BaseBehavior):
def __init__(self, identity_user_api_client=None):
self._client = identity_user_api_client
self.config = TokenAPI_Config()
@behavior(TokenAPI_Client)
def get_access_data(self, username=None, password=None,
tenant_name=None):
username = username or self.config.username
password = password or self.config.password
tenant_name = tenant_name or self.config.tenant_name
access_data = None
if username is not None and password is not None:
response = self._client.authenticate(
username=username, password=password,
tenant_name=tenant_name)
access_data = response.entity
return access_data

View File

@ -0,0 +1,91 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.clients.rest import AutoMarshallingRestClient
from cloudcafe.identity.v2_0.tokens_api.models.requests.auth import \
Auth as AuthRequest
from cloudcafe.identity.v2_0.tokens_api.models.responses.access import \
Access as AuthResponse
from cloudcafe.identity.v2_0.tokens_api.models.requests.credentials import \
PasswordCredentials
_version = 'v2.0'
_tokens = 'tokens'
class BaseTokenAPI_Client(AutoMarshallingRestClient):
def __init__(self, serialize_format, deserialize_format=None):
super(BaseTokenAPI_Client, self).__init__(serialize_format,
deserialize_format)
@property
def token(self):
return self.default_headers.get('X-Auth-Token')
@token.setter
def token(self, token):
self.default_headers['X-Auth-Token'] = token
@token.deleter
def token(self):
del self.default_headers['X-Auth-Token']
class TokenAPI_Client(BaseTokenAPI_Client):
def __init__(self, url, serialize_format, deserialize_format=None,
auth_token=None):
super(TokenAPI_Client, self).__init__(
serialize_format, deserialize_format)
self.base_url = '{0}/{1}'.format(url, _version)
self.default_headers['Content-Type'] = 'application/{0}'.format(
serialize_format)
self.default_headers['Accept'] = 'application/{0}'.format(
serialize_format)
if auth_token is not None:
self.default_headers['X-Auth-Token'] = auth_token
def authenticate(self, username, password, tenant_name,
requestslib_kwargs=None):
'''
@summary: Creates authentication using Username and password.
@param username: The username of the customer.
@type name: String
@param password: The user password.
@type password: String
@return: Response Object containing auth response
@rtype: Response Object
'''
'''
POST
v2.0/tokens
'''
credentials = PasswordCredentials(
username=username,
password=password)
auth_request_entity = AuthRequest(credentials=credentials,
tenant_name=tenant_name)
url = '{0}/{1}'.format(self.base_url, _tokens)
response = self.post(url, response_entity_type=AuthResponse,
request_entity=auth_request_entity,
requestslib_kwargs=requestslib_kwargs)
return response

View File

@ -0,0 +1,46 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cloudcafe.common.models.configuration import ConfigSectionInterface
class TokenAPI_Config(ConfigSectionInterface):
SECTION_NAME = 'token_api'
@property
def serialize_format(self):
return self.get("serialize_format")
@property
def deserialize_format(self):
return self.get("deserialize_format")
@property
def authentication_endpoint(self):
return self.get("authentication_endpoint")
@property
def username(self):
return self.get("username")
@property
def password(self):
return self.get_raw("password")
@property
def tenant_name(self):
return self.get("tenant_name")

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,42 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cafe.engine.models.base import \
AutoMarshallingModel, AutoMarshallingListModel
from cloudcafe.identity.v2_0.tokens_api.models.constants import V2_0Constants
class BaseIdentityModel(AutoMarshallingModel):
@classmethod
def _remove_identity_xml_namespaces(cls, element):
cls._remove_namespace(element, V2_0Constants.XML_NS)
cls._remove_namespace(element, V2_0Constants.XML_NS_OS_KSADM)
cls._remove_namespace(element, V2_0Constants.XML_NS_RAX_KSKEY)
cls._remove_namespace(element, V2_0Constants.XML_NS_OS_KSEC2)
cls._remove_namespace(element, V2_0Constants.XML_NS_RAX_KSQA)
cls._remove_namespace(element, V2_0Constants.XML_NS_RAX_AUTH)
cls._remove_namespace(element, V2_0Constants.XML_NS_RAX_KSGRP)
cls._remove_namespace(element, V2_0Constants.XML_NS_OPENSTACK_COMMON)
cls._remove_namespace(element, V2_0Constants.XML_NS_ATOM)
class BaseIdentityListModel(AutoMarshallingListModel):
@classmethod
def _remove_identity_xml_namespaces(cls, element):
BaseIdentityListModel._remove_identity_xml_namespaces(element)

View File

@ -0,0 +1,33 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
class V2_0Constants(object):
XML_NS = 'http://docs.openstack.org/identity/api/v2.0'
XML_NS_OPENSTACK_COMMON = 'http://docs.openstack.org/common/api/v1.0'
XML_NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
XML_NS_OS_KSADM = \
'http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0'
XML_NS_OS_KSEC2 = \
'http://docs.openstack.org/identity/api/ext/OS-KSEC2/v1.0'
XML_NS_RAX_KSQA = \
'http://docs.rackspace.com/identity/api/ext/RAX-KSQA/v1.0'
XML_NS_RAX_KSKEY = \
'http://docs.rackspace.com/identity/api/ext/RAX-KSKEY/v1.0'
XML_NS_RAX_AUTH = \
'http://docs.rackspace.com/identity/api/ext/RAX-AUTH/v1.0'
XML_NS_RAX_KSGRP = \
'http://docs.rackspace.com/identity/api/ext/RAX-KSGRP/v1.0'
XML_NS_ATOM = 'http://www.w3.org/2005/Atom'

View File

@ -0,0 +1,16 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,84 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
from xml.etree import ElementTree
from cloudcafe.identity.v2_0.tokens_api.models.base import BaseIdentityModel
from cloudcafe.identity.v2_0.tokens_api.models.requests.credentials import \
PasswordCredentials
class Auth(BaseIdentityModel):
ROOT_TAG = 'auth'
def __init__(self, credentials=None, tenant_name=None, token=None):
self.passwordCredentials = credentials
self.token = token
self.tenant_name = tenant_name
def _obj_to_json(self):
ret = {}
if self.passwordCredentials is not None:
ret[PasswordCredentials.ROOT_TAG] = \
self.passwordCredentials._obj_to_dict()
if self.token is not None:
ret[Token.ROOT_TAG] = self.token._obj_to_dict()
if self.tenant_name is not None:
ret['tenantName'] = self.tenant_name
ret = {self.ROOT_TAG: ret}
return json.dumps(ret)
def _obj_to_xml(self):
ele = self._obj_to_xml_ele()
#ele.set('xmlns:xsi', V2_0Constants.XML_NS_XSI)
#ele.set('xmlns', V2_0Constants.XML_NS)
return ElementTree.tostring(ele)
def _obj_to_xml_ele(self):
element = ElementTree.Element(self.ROOT_TAG)
if self.passwordCredentials is not None:
element.append(self.passwordCredentials._obj_to_xml_ele())
if self.token is not None:
element.append(self.token._obj_to_xml_ele())
if self.tenant_name is not None:
element.set('tenantName', self.tenant_name)
return element
class Token(BaseIdentityModel):
ROOT_TAG = 'token'
def __init__(self, id=None):
super(Token, self).__init__()
self.id = id
def _obj_to_dict(self):
ret = {}
if self.id is not None:
ret['id'] = self.id
return ret
def _obj_to_xml(self):
return ElementTree.tostring(self._obj_to_xml_ele())
def _obj_to_xml_ele(self):
element = ElementTree.Element(self.ROOT_TAG)
if self.id is not None:
element.set('id', self.id)
return element

View File

@ -0,0 +1,59 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
from xml.etree import ElementTree
from cloudcafe.identity.v2_0.tokens_api.models.base import \
BaseIdentityModel, V2_0Constants
class PasswordCredentials(BaseIdentityModel):
ROOT_TAG = 'passwordCredentials'
def __init__(self, username=None, password=None):
super(PasswordCredentials, self).__init__()
self.username = username
self.password = password
def _obj_to_json(self):
ret = {self.ROOT_TAG: self._obj_to_dict()}
return json.dumps(ret)
def _obj_to_dict(self):
ret = {}
if self.username is not None:
ret['username'] = self.username
if self.password is not None:
ret['password'] = self.password
return ret
def _obj_to_xml(self):
element = self._obj_to_xml_ele()
element.set('xmlns', V2_0Constants.XML_NS)
element.set('xmlns:xsi', V2_0Constants.XML_NS_XSI)
return ElementTree.tostring(element)
def _obj_to_xml_ele(self):
element = ElementTree.Element(self.ROOT_TAG)
if self.username is not None:
element.set('username', self.username)
if self.password is not None:
element.set('password', self.password)
return element

View File

@ -0,0 +1,100 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
from xml.etree import ElementTree
from cloudcafe.identity.v2_0.tokens_api.models.base import \
BaseIdentityModel, BaseIdentityListModel
class Roles(BaseIdentityListModel):
ROOT_TAG = 'roles'
def __init__(self, roles=None):
'''An object that represents an users response object.
Keyword arguments:
'''
super(Roles, self).__init__()
self.extend(roles)
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
return cls._list_to_obj(json_dict.get(cls.ROOT_TAG))
@classmethod
def _list_to_obj(cls, list_):
ret = {cls.ROOT_TAG: [Role(**role) for role in list_]}
return Roles(**ret)
@classmethod
def _xml_to_obj(cls, serialized_str):
element = ElementTree.fromstring(serialized_str)
cls._remove_identity_xml_namespaces(element)
if element.tag != cls.ROOT_TAG:
return None
return cls._xml_list_to_obj(element.findall(Role.ROOT_TAG))
@classmethod
def _xml_list_to_obj(cls, xml_list):
kwargs = {cls.ROOT_TAG: [Role._xml_ele_to_obj(role)
for role in xml_list]}
return Roles(**kwargs)
class Role(BaseIdentityModel):
ROOT_TAG = 'role'
def __init__(self, id=None, name=None, description=None, serviceId=None,
tenantId=None, propagate=None, weight=None):
super(Role, self).__init__()
self.id = id
self.name = name
self.description = description
self.serviceId = serviceId
self.tenantId = tenantId
self.weight = weight
self.propagate = propagate
@classmethod
def _json_to_obj(cls, serialized_str):
json_dict = json.loads(serialized_str)
json_dict['role']['propagate'] = json_dict['role'].pop('RAX-AUTH:propagate')
json_dict['role']['weight'] = json_dict['role'].pop('RAX-AUTH:Weight')
return Role(**json_dict.get(cls.ROOT_TAG))
@classmethod
def _xml_to_obj(cls, serialized_str):
element = ElementTree.fromstring(serialized_str)
cls._remove_identity_xml_namespaces(element)
if element.tag != cls.ROOT_TAG:
return None
return cls._xml_ele_to_obj(element)
@classmethod
def _xml_ele_to_obj(cls, xml_ele):
kwargs = {'name': xml_ele.get('name'),
'description': xml_ele.get('description'),
'serviceId': xml_ele.get('serviceId'),
'tenantId': xml_ele.get('tenantId')}
try:
kwargs['id'] = int(xml_ele.get('id'))
except (ValueError, TypeError):
kwargs['id'] = xml_ele.get('id')
return Role(**kwargs)

View File

@ -0,0 +1,22 @@
"""
Copyright 2013 Rackspace
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
'''
@summary: Classes and Utilities used to manage common Domain Level Objects
@note: Primarily used to pass data between L{ccengine.providers}, L{ccengine.clients} and L{testrepo} Tests
@note: Also used to perform JSON or XML Marshaling and Validation
@note: Consumed by L{ccengine.providers}, L{ccengine.clients}, L{ccengine.common.connectors} and L{testrepo} Tests
'''

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