From 481102076cc471a6d3c100296f0e4b30738cf565 Mon Sep 17 00:00:00 2001 From: samu4924 Date: Sat, 30 Mar 2013 14:48:37 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 30 + HISTORY.rst | 0 LICENSE | 13 + README.md | 79 +++ __init__.py | 16 + cloudcafe/__init__.py | 22 + cloudcafe/blockstorage/__init__.py | 16 + cloudcafe/blockstorage/config.py | 30 + .../integration/compute_behaviors.py | 26 + cloudcafe/blockstorage/provider.py | 37 + .../blockstorage/volumes_api/__init__.py | 16 + .../blockstorage/volumes_api/behaviors.py | 374 +++++++++++ cloudcafe/blockstorage/volumes_api/client.py | 201 ++++++ cloudcafe/blockstorage/volumes_api/config.py | 75 +++ .../volumes_api/models/__init__.py | 16 + .../volumes_api/models/requests/__init__.py | 16 + .../models/requests/volumes_api.py | 121 ++++ .../volumes_api/models/responses/__init__.py | 16 + .../models/responses/volumes_api.py | 265 ++++++++ .../blockstorage/volumes_api/provider.py | 35 + cloudcafe/common/__init__.py | 16 + cloudcafe/common/constants.py | 25 + cloudcafe/common/generators/__init__.py | 5 + .../common/generators/identity/__init__.py | 0 .../common/generators/identity/password.py | 19 + cloudcafe/common/models/__init__.py | 16 + cloudcafe/common/models/configuration.py | 48 ++ cloudcafe/common/resources.py | 44 ++ cloudcafe/compute/__init__.py | 16 + cloudcafe/compute/behaviors.py | 16 + cloudcafe/compute/common/__init__.py | 16 + cloudcafe/compute/common/constants.py | 41 ++ cloudcafe/compute/common/datagen.py | 133 ++++ cloudcafe/compute/common/equality_tools.py | 73 ++ cloudcafe/compute/common/exception_handler.py | 121 ++++ cloudcafe/compute/common/exceptions.py | 184 +++++ cloudcafe/compute/common/models/__init__.py | 16 + .../compute/common/models/file_details.py | 29 + cloudcafe/compute/common/models/link.py | 127 ++++ cloudcafe/compute/common/models/metadata.py | 214 ++++++ cloudcafe/compute/common/models/partition.py | 96 +++ cloudcafe/compute/common/types.py | 95 +++ cloudcafe/compute/config.py | 30 + cloudcafe/compute/extensions/__init__.py | 16 + .../extensions/extensions_api/__init__.py | 16 + .../extensions_api/clients/__init__.py | 16 + .../clients/extensions_client.py | 16 + .../extensions_api/models/__init__.py | 16 + .../extensions_api/models/extension.py | 16 + .../extensions/floating_ips_api/__init__.py | 16 + .../extensions/keypairs_api/__init__.py | 16 + .../compute/extensions/rescue_api/__init__.py | 16 + .../security_groups_api/__init__.py | 16 + .../extensions/volumes_api/__init__.py | 16 + cloudcafe/compute/flavors_api/__init__.py | 15 + cloudcafe/compute/flavors_api/client.py | 115 ++++ cloudcafe/compute/flavors_api/config.py | 32 + .../compute/flavors_api/models/__init__.py | 15 + .../compute/flavors_api/models/flavor.py | 185 +++++ cloudcafe/compute/images_api/__init__.py | 15 + cloudcafe/compute/images_api/behaviors.py | 88 +++ cloudcafe/compute/images_api/client.py | 255 +++++++ cloudcafe/compute/images_api/config.py | 42 ++ .../compute/images_api/models/__init__.py | 15 + cloudcafe/compute/images_api/models/image.py | 203 ++++++ cloudcafe/compute/limits_api/__init__.py | 15 + cloudcafe/compute/limits_api/client.py | 133 ++++ .../compute/limits_api/models/__init__.py | 15 + cloudcafe/compute/limits_api/models/limit.py | 132 ++++ cloudcafe/compute/servers_api/__init__.py | 15 + cloudcafe/compute/servers_api/behaviors.py | 203 ++++++ cloudcafe/compute/servers_api/client.py | 634 ++++++++++++++++++ cloudcafe/compute/servers_api/config.py | 64 ++ .../compute/servers_api/models/__init__.py | 15 + .../compute/servers_api/models/requests.py | 596 ++++++++++++++++ .../compute/servers_api/models/servers.py | 407 +++++++++++ .../volume_attachments_api/__init__.py | 16 + .../volume_attachments_api/models/__init__.py | 16 + .../models/requests/__init__.py | 16 + .../models/requests/volume_attachments.py | 38 ++ .../models/responses/__init__.py | 16 + .../models/responses/volume_attachments.py | 75 +++ .../volume_attachments_client.py | 96 +++ cloudcafe/identity/__init__.py | 16 + cloudcafe/identity/v2_0/__init__.py | 22 + cloudcafe/identity/v2_0/provider.py | 16 + .../identity/v2_0/tenants_api/__init__.py | 16 + .../v2_0/tenants_api/models/__init__.py | 16 + .../identity/v2_0/tokens_api/__init__.py | 16 + .../identity/v2_0/tokens_api/behaviors.py | 43 ++ cloudcafe/identity/v2_0/tokens_api/client.py | 91 +++ cloudcafe/identity/v2_0/tokens_api/config.py | 46 ++ .../v2_0/tokens_api/models/__init__.py | 16 + .../identity/v2_0/tokens_api/models/base.py | 42 ++ .../v2_0/tokens_api/models/constants.py | 33 + .../tokens_api/models/requests/__init__.py | 16 + .../v2_0/tokens_api/models/requests/auth.py | 84 +++ .../tokens_api/models/requests/credentials.py | 59 ++ .../v2_0/tokens_api/models/requests/role.py | 100 +++ .../tokens_api/models/responses/__init__.py | 22 + .../tokens_api/models/responses/access.py | 208 ++++++ .../tokens_api/models/responses/endpoint.py | 130 ++++ .../v2_0/tokens_api/models/responses/role.py | 99 +++ .../tokens_api/models/responses/tenant.py | 101 +++ .../v2_0/tokens_api/models/responses/user.py | 149 ++++ .../identity/v2_0/tokens_api/provider.py | 33 + cloudcafe/identity/v2_0/users_api/__init__.py | 16 + .../v2_0/users_api/models/__init__.py | 16 + cloudcafe/objectstorage/__init__.py | 16 + cloudcafe/objectstorage/config.py | 30 + .../objectstorage_api/behaviors.py | 16 + .../objectstorage/objectstorage_api/client.py | 179 +++++ .../objectstorage/objectstorage_api/config.py | 106 +++ cloudcafe/provider.py | 19 + configs/blockstorage/reference.json.config | 21 + configs/blockstorage/reference.xml.config | 21 + configs/compute/devstack.json.config | 47 ++ configs/compute/reference.json.config | 22 + configs/compute/reference.xml.config | 23 + configs/identity/reference.json.config | 23 + configs/identity/reference.xml.config | 23 + configs/objectstorage/reference.json.config | 23 + configs/objectstorage/reference.xml.config | 23 + pip-requires | 0 setup.py | 120 ++++ 125 files changed, 8560 insertions(+) create mode 100644 .gitignore create mode 100644 HISTORY.rst create mode 100644 LICENSE create mode 100644 README.md create mode 100644 __init__.py create mode 100644 cloudcafe/__init__.py create mode 100644 cloudcafe/blockstorage/__init__.py create mode 100644 cloudcafe/blockstorage/config.py create mode 100644 cloudcafe/blockstorage/integration/compute_behaviors.py create mode 100644 cloudcafe/blockstorage/provider.py create mode 100644 cloudcafe/blockstorage/volumes_api/__init__.py create mode 100644 cloudcafe/blockstorage/volumes_api/behaviors.py create mode 100644 cloudcafe/blockstorage/volumes_api/client.py create mode 100644 cloudcafe/blockstorage/volumes_api/config.py create mode 100644 cloudcafe/blockstorage/volumes_api/models/__init__.py create mode 100644 cloudcafe/blockstorage/volumes_api/models/requests/__init__.py create mode 100644 cloudcafe/blockstorage/volumes_api/models/requests/volumes_api.py create mode 100644 cloudcafe/blockstorage/volumes_api/models/responses/__init__.py create mode 100644 cloudcafe/blockstorage/volumes_api/models/responses/volumes_api.py create mode 100644 cloudcafe/blockstorage/volumes_api/provider.py create mode 100644 cloudcafe/common/__init__.py create mode 100644 cloudcafe/common/constants.py create mode 100644 cloudcafe/common/generators/__init__.py create mode 100644 cloudcafe/common/generators/identity/__init__.py create mode 100644 cloudcafe/common/generators/identity/password.py create mode 100644 cloudcafe/common/models/__init__.py create mode 100644 cloudcafe/common/models/configuration.py create mode 100644 cloudcafe/common/resources.py create mode 100644 cloudcafe/compute/__init__.py create mode 100644 cloudcafe/compute/behaviors.py create mode 100644 cloudcafe/compute/common/__init__.py create mode 100644 cloudcafe/compute/common/constants.py create mode 100644 cloudcafe/compute/common/datagen.py create mode 100644 cloudcafe/compute/common/equality_tools.py create mode 100644 cloudcafe/compute/common/exception_handler.py create mode 100644 cloudcafe/compute/common/exceptions.py create mode 100644 cloudcafe/compute/common/models/__init__.py create mode 100644 cloudcafe/compute/common/models/file_details.py create mode 100644 cloudcafe/compute/common/models/link.py create mode 100644 cloudcafe/compute/common/models/metadata.py create mode 100644 cloudcafe/compute/common/models/partition.py create mode 100644 cloudcafe/compute/common/types.py create mode 100644 cloudcafe/compute/config.py create mode 100644 cloudcafe/compute/extensions/__init__.py create mode 100644 cloudcafe/compute/extensions/extensions_api/__init__.py create mode 100644 cloudcafe/compute/extensions/extensions_api/clients/__init__.py create mode 100644 cloudcafe/compute/extensions/extensions_api/clients/extensions_client.py create mode 100644 cloudcafe/compute/extensions/extensions_api/models/__init__.py create mode 100644 cloudcafe/compute/extensions/extensions_api/models/extension.py create mode 100644 cloudcafe/compute/extensions/floating_ips_api/__init__.py create mode 100644 cloudcafe/compute/extensions/keypairs_api/__init__.py create mode 100644 cloudcafe/compute/extensions/rescue_api/__init__.py create mode 100644 cloudcafe/compute/extensions/security_groups_api/__init__.py create mode 100644 cloudcafe/compute/extensions/volumes_api/__init__.py create mode 100644 cloudcafe/compute/flavors_api/__init__.py create mode 100644 cloudcafe/compute/flavors_api/client.py create mode 100644 cloudcafe/compute/flavors_api/config.py create mode 100644 cloudcafe/compute/flavors_api/models/__init__.py create mode 100644 cloudcafe/compute/flavors_api/models/flavor.py create mode 100644 cloudcafe/compute/images_api/__init__.py create mode 100644 cloudcafe/compute/images_api/behaviors.py create mode 100644 cloudcafe/compute/images_api/client.py create mode 100644 cloudcafe/compute/images_api/config.py create mode 100644 cloudcafe/compute/images_api/models/__init__.py create mode 100644 cloudcafe/compute/images_api/models/image.py create mode 100644 cloudcafe/compute/limits_api/__init__.py create mode 100644 cloudcafe/compute/limits_api/client.py create mode 100644 cloudcafe/compute/limits_api/models/__init__.py create mode 100644 cloudcafe/compute/limits_api/models/limit.py create mode 100644 cloudcafe/compute/servers_api/__init__.py create mode 100644 cloudcafe/compute/servers_api/behaviors.py create mode 100644 cloudcafe/compute/servers_api/client.py create mode 100644 cloudcafe/compute/servers_api/config.py create mode 100644 cloudcafe/compute/servers_api/models/__init__.py create mode 100644 cloudcafe/compute/servers_api/models/requests.py create mode 100644 cloudcafe/compute/servers_api/models/servers.py create mode 100644 cloudcafe/compute/volume_attachments_api/__init__.py create mode 100644 cloudcafe/compute/volume_attachments_api/models/__init__.py create mode 100644 cloudcafe/compute/volume_attachments_api/models/requests/__init__.py create mode 100644 cloudcafe/compute/volume_attachments_api/models/requests/volume_attachments.py create mode 100644 cloudcafe/compute/volume_attachments_api/models/responses/__init__.py create mode 100644 cloudcafe/compute/volume_attachments_api/models/responses/volume_attachments.py create mode 100644 cloudcafe/compute/volume_attachments_api/volume_attachments_client.py create mode 100644 cloudcafe/identity/__init__.py create mode 100644 cloudcafe/identity/v2_0/__init__.py create mode 100644 cloudcafe/identity/v2_0/provider.py create mode 100644 cloudcafe/identity/v2_0/tenants_api/__init__.py create mode 100644 cloudcafe/identity/v2_0/tenants_api/models/__init__.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/__init__.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/behaviors.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/client.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/config.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/__init__.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/base.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/constants.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/requests/__init__.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/requests/auth.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/requests/credentials.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/requests/role.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/responses/__init__.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/responses/access.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/responses/endpoint.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/responses/role.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/responses/tenant.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/models/responses/user.py create mode 100644 cloudcafe/identity/v2_0/tokens_api/provider.py create mode 100644 cloudcafe/identity/v2_0/users_api/__init__.py create mode 100644 cloudcafe/identity/v2_0/users_api/models/__init__.py create mode 100644 cloudcafe/objectstorage/__init__.py create mode 100644 cloudcafe/objectstorage/config.py create mode 100644 cloudcafe/objectstorage/objectstorage_api/behaviors.py create mode 100644 cloudcafe/objectstorage/objectstorage_api/client.py create mode 100644 cloudcafe/objectstorage/objectstorage_api/config.py create mode 100644 cloudcafe/provider.py create mode 100644 configs/blockstorage/reference.json.config create mode 100644 configs/blockstorage/reference.xml.config create mode 100644 configs/compute/devstack.json.config create mode 100644 configs/compute/reference.json.config create mode 100644 configs/compute/reference.xml.config create mode 100644 configs/identity/reference.json.config create mode 100644 configs/identity/reference.xml.config create mode 100644 configs/objectstorage/reference.json.config create mode 100644 configs/objectstorage/reference.xml.config create mode 100644 pip-requires create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6f5013c2 --- /dev/null +++ b/.gitignore @@ -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* diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 00000000..e69de29b diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0d72c9b2 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..8a7c5603 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +CloudCAFE, An CAFE Implementation for OpenStack +================================ +
+   _ _ _
+  ( `   )_ 
+ (    )   `)  _
+(____(__.___`)__)
+
+    ( (
+       ) )
+    .........    
+    |       |___ 
+    |       |_  |
+    |  :-)  |_| |
+    |       |___|
+    |_______|
+=== CloudCAFE ===
+= An Open CAFE Implementation =
+
+ +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: /.cloudcafe/configs/ + +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 /.cloudcafe/logs///. In addition, tests built from the built-in CAFE unittest driver will generate +csv statistics files in /.cloudcafe/logs///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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/__init__.py @@ -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. +""" + diff --git a/cloudcafe/__init__.py b/cloudcafe/__init__.py new file mode 100644 index 00000000..8bc91168 --- /dev/null +++ b/cloudcafe/__init__.py @@ -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.' diff --git a/cloudcafe/blockstorage/__init__.py b/cloudcafe/blockstorage/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/blockstorage/__init__.py @@ -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. +""" + diff --git a/cloudcafe/blockstorage/config.py b/cloudcafe/blockstorage/config.py new file mode 100644 index 00000000..ba78f5e8 --- /dev/null +++ b/cloudcafe/blockstorage/config.py @@ -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') diff --git a/cloudcafe/blockstorage/integration/compute_behaviors.py b/cloudcafe/blockstorage/integration/compute_behaviors.py new file mode 100644 index 00000000..412a09f1 --- /dev/null +++ b/cloudcafe/blockstorage/integration/compute_behaviors.py @@ -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 diff --git a/cloudcafe/blockstorage/provider.py b/cloudcafe/blockstorage/provider.py new file mode 100644 index 00000000..13ebcea5 --- /dev/null +++ b/cloudcafe/blockstorage/provider.py @@ -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) + + diff --git a/cloudcafe/blockstorage/volumes_api/__init__.py b/cloudcafe/blockstorage/volumes_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/blockstorage/volumes_api/behaviors.py b/cloudcafe/blockstorage/volumes_api/behaviors.py new file mode 100644 index 00000000..a9c31aa0 --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/behaviors.py @@ -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 diff --git a/cloudcafe/blockstorage/volumes_api/client.py b/cloudcafe/blockstorage/volumes_api/client.py new file mode 100644 index 00000000..b325b6ed --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/client.py @@ -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) + diff --git a/cloudcafe/blockstorage/volumes_api/config.py b/cloudcafe/blockstorage/volumes_api/config.py new file mode 100644 index 00000000..31d75b36 --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/config.py @@ -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) diff --git a/cloudcafe/blockstorage/volumes_api/models/__init__.py b/cloudcafe/blockstorage/volumes_api/models/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/models/__init__.py @@ -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. +""" + diff --git a/cloudcafe/blockstorage/volumes_api/models/requests/__init__.py b/cloudcafe/blockstorage/volumes_api/models/requests/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/models/requests/__init__.py @@ -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. +""" + diff --git a/cloudcafe/blockstorage/volumes_api/models/requests/volumes_api.py b/cloudcafe/blockstorage/volumes_api/models/requests/volumes_api.py new file mode 100644 index 00000000..6af987b1 --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/models/requests/volumes_api.py @@ -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 diff --git a/cloudcafe/blockstorage/volumes_api/models/responses/__init__.py b/cloudcafe/blockstorage/volumes_api/models/responses/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/models/responses/__init__.py @@ -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. +""" + diff --git a/cloudcafe/blockstorage/volumes_api/models/responses/volumes_api.py b/cloudcafe/blockstorage/volumes_api/models/responses/volumes_api.py new file mode 100644 index 00000000..f921b60d --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/models/responses/volumes_api.py @@ -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 diff --git a/cloudcafe/blockstorage/volumes_api/provider.py b/cloudcafe/blockstorage/volumes_api/provider.py new file mode 100644 index 00000000..409ad813 --- /dev/null +++ b/cloudcafe/blockstorage/volumes_api/provider.py @@ -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) diff --git a/cloudcafe/common/__init__.py b/cloudcafe/common/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/common/__init__.py @@ -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. +""" + diff --git a/cloudcafe/common/constants.py b/cloudcafe/common/constants.py new file mode 100644 index 00000000..5430fc36 --- /dev/null +++ b/cloudcafe/common/constants.py @@ -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' diff --git a/cloudcafe/common/generators/__init__.py b/cloudcafe/common/generators/__init__.py new file mode 100644 index 00000000..ac5ad4ee --- /dev/null +++ b/cloudcafe/common/generators/__init__.py @@ -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 +''' \ No newline at end of file diff --git a/cloudcafe/common/generators/identity/__init__.py b/cloudcafe/common/generators/identity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cloudcafe/common/generators/identity/password.py b/cloudcafe/common/generators/identity/password.py new file mode 100644 index 00000000..63498e43 --- /dev/null +++ b/cloudcafe/common/generators/identity/password.py @@ -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!'}) diff --git a/cloudcafe/common/models/__init__.py b/cloudcafe/common/models/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/common/models/__init__.py @@ -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. +""" + diff --git a/cloudcafe/common/models/configuration.py b/cloudcafe/common/models/configuration.py new file mode 100644 index 00000000..0065c0ef --- /dev/null +++ b/cloudcafe/common/models/configuration.py @@ -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 diff --git a/cloudcafe/common/resources.py b/cloudcafe/common/resources.py new file mode 100644 index 00000000..1a112893 --- /dev/null +++ b/cloudcafe/common/resources.py @@ -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 \ No newline at end of file diff --git a/cloudcafe/compute/__init__.py b/cloudcafe/compute/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/behaviors.py b/cloudcafe/compute/behaviors.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/behaviors.py @@ -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. +""" + diff --git a/cloudcafe/compute/common/__init__.py b/cloudcafe/compute/common/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/common/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/common/constants.py b/cloudcafe/compute/common/constants.py new file mode 100644 index 00000000..daadb713 --- /dev/null +++ b/cloudcafe/compute/common/constants.py @@ -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 = "" + SERVICE_TYPE = 'cloudServersOpenStack' + + +class HTTPResponseCodes(object): + NOT_FOUND = 404 + SERVER_ERROR = 500 diff --git a/cloudcafe/compute/common/datagen.py b/cloudcafe/compute/common/datagen.py new file mode 100644 index 00000000..202afd5a --- /dev/null +++ b/cloudcafe/compute/common/datagen.py @@ -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 diff --git a/cloudcafe/compute/common/equality_tools.py b/cloudcafe/compute/common/equality_tools.py new file mode 100644 index 00000000..c6417f4d --- /dev/null +++ b/cloudcafe/compute/common/equality_tools.py @@ -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 diff --git a/cloudcafe/compute/common/exception_handler.py b/cloudcafe/compute/common/exception_handler.py new file mode 100644 index 00000000..7f6a5e7d --- /dev/null +++ b/cloudcafe/compute/common/exception_handler.py @@ -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 diff --git a/cloudcafe/compute/common/exceptions.py b/cloudcafe/compute/common/exceptions.py new file mode 100644 index 00000000..56d600f4 --- /dev/null +++ b/cloudcafe/compute/common/exceptions.py @@ -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) diff --git a/cloudcafe/compute/common/models/__init__.py b/cloudcafe/compute/common/models/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/common/models/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/common/models/file_details.py b/cloudcafe/compute/common/models/file_details.py new file mode 100644 index 00000000..c802f2e2 --- /dev/null +++ b/cloudcafe/compute/common/models/file_details.py @@ -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) diff --git a/cloudcafe/compute/common/models/link.py b/cloudcafe/compute/common/models/link.py new file mode 100644 index 00000000..e8fb03c0 --- /dev/null +++ b/cloudcafe/compute/common/models/link.py @@ -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 for the links which is the parent to the element. + Hence we need to loop twice to get to the dictionary of 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 diff --git a/cloudcafe/compute/common/models/metadata.py b/cloudcafe/compute/common/models/metadata.py new file mode 100644 index 00000000..6aaf0afc --- /dev/null +++ b/cloudcafe/compute/common/models/metadata.py @@ -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 diff --git a/cloudcafe/compute/common/models/partition.py b/cloudcafe/compute/common/models/partition.py new file mode 100644 index 00000000..f8b6665f --- /dev/null +++ b/cloudcafe/compute/common/models/partition.py @@ -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) diff --git a/cloudcafe/compute/common/types.py b/cloudcafe/compute/common/types.py new file mode 100644 index 00000000..9a3b2183 --- /dev/null +++ b/cloudcafe/compute/common/types.py @@ -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" diff --git a/cloudcafe/compute/config.py b/cloudcafe/compute/config.py new file mode 100644 index 00000000..dd58a323 --- /dev/null +++ b/cloudcafe/compute/config.py @@ -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") diff --git a/cloudcafe/compute/extensions/__init__.py b/cloudcafe/compute/extensions/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/extensions_api/__init__.py b/cloudcafe/compute/extensions/extensions_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/extensions_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/extensions_api/clients/__init__.py b/cloudcafe/compute/extensions/extensions_api/clients/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/extensions_api/clients/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/extensions_api/clients/extensions_client.py b/cloudcafe/compute/extensions/extensions_api/clients/extensions_client.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/extensions_api/clients/extensions_client.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/extensions_api/models/__init__.py b/cloudcafe/compute/extensions/extensions_api/models/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/extensions_api/models/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/extensions_api/models/extension.py b/cloudcafe/compute/extensions/extensions_api/models/extension.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/extensions_api/models/extension.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/floating_ips_api/__init__.py b/cloudcafe/compute/extensions/floating_ips_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/floating_ips_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/keypairs_api/__init__.py b/cloudcafe/compute/extensions/keypairs_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/keypairs_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/rescue_api/__init__.py b/cloudcafe/compute/extensions/rescue_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/rescue_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/security_groups_api/__init__.py b/cloudcafe/compute/extensions/security_groups_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/security_groups_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/extensions/volumes_api/__init__.py b/cloudcafe/compute/extensions/volumes_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/extensions/volumes_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/flavors_api/__init__.py b/cloudcafe/compute/flavors_api/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/compute/flavors_api/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/flavors_api/client.py b/cloudcafe/compute/flavors_api/client.py new file mode 100644 index 00000000..1dc18281 --- /dev/null +++ b/cloudcafe/compute/flavors_api/client.py @@ -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 diff --git a/cloudcafe/compute/flavors_api/config.py b/cloudcafe/compute/flavors_api/config.py new file mode 100644 index 00000000..610be6ff --- /dev/null +++ b/cloudcafe/compute/flavors_api/config.py @@ -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") diff --git a/cloudcafe/compute/flavors_api/models/__init__.py b/cloudcafe/compute/flavors_api/models/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/compute/flavors_api/models/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/flavors_api/models/flavor.py b/cloudcafe/compute/flavors_api/models/flavor.py new file mode 100644 index 00000000..12f399ef --- /dev/null +++ b/cloudcafe/compute/flavors_api/models/flavor.py @@ -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 diff --git a/cloudcafe/compute/images_api/__init__.py b/cloudcafe/compute/images_api/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/compute/images_api/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/images_api/behaviors.py b/cloudcafe/compute/images_api/behaviors.py new file mode 100644 index 00000000..8f0ac068 --- /dev/null +++ b/cloudcafe/compute/images_api/behaviors.py @@ -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 \ No newline at end of file diff --git a/cloudcafe/compute/images_api/client.py b/cloudcafe/compute/images_api/client.py new file mode 100644 index 00000000..b02eb7e8 --- /dev/null +++ b/cloudcafe/compute/images_api/client.py @@ -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 diff --git a/cloudcafe/compute/images_api/config.py b/cloudcafe/compute/images_api/config.py new file mode 100644 index 00000000..b519ac57 --- /dev/null +++ b/cloudcafe/compute/images_api/config.py @@ -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")) diff --git a/cloudcafe/compute/images_api/models/__init__.py b/cloudcafe/compute/images_api/models/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/compute/images_api/models/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/images_api/models/image.py b/cloudcafe/compute/images_api/models/image.py new file mode 100644 index 00000000..95dc4540 --- /dev/null +++ b/cloudcafe/compute/images_api/models/image.py @@ -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 diff --git a/cloudcafe/compute/limits_api/__init__.py b/cloudcafe/compute/limits_api/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/compute/limits_api/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/limits_api/client.py b/cloudcafe/compute/limits_api/client.py new file mode 100644 index 00000000..601dff45 --- /dev/null +++ b/cloudcafe/compute/limits_api/client.py @@ -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') diff --git a/cloudcafe/compute/limits_api/models/__init__.py b/cloudcafe/compute/limits_api/models/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/compute/limits_api/models/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/limits_api/models/limit.py b/cloudcafe/compute/limits_api/models/limit.py new file mode 100644 index 00000000..8bf69b74 --- /dev/null +++ b/cloudcafe/compute/limits_api/models/limit.py @@ -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 diff --git a/cloudcafe/compute/servers_api/__init__.py b/cloudcafe/compute/servers_api/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/compute/servers_api/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/servers_api/behaviors.py b/cloudcafe/compute/servers_api/behaviors.py new file mode 100644 index 00000000..f296a92b --- /dev/null +++ b/cloudcafe/compute/servers_api/behaviors.py @@ -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) diff --git a/cloudcafe/compute/servers_api/client.py b/cloudcafe/compute/servers_api/client.py new file mode 100644 index 00000000..1c32f52d --- /dev/null +++ b/cloudcafe/compute/servers_api/client.py @@ -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 diff --git a/cloudcafe/compute/servers_api/config.py b/cloudcafe/compute/servers_api/config.py new file mode 100644 index 00000000..51f114e2 --- /dev/null +++ b/cloudcafe/compute/servers_api/config.py @@ -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") diff --git a/cloudcafe/compute/servers_api/models/__init__.py b/cloudcafe/compute/servers_api/models/__init__.py new file mode 100644 index 00000000..59ab77fa --- /dev/null +++ b/cloudcafe/compute/servers_api/models/__init__.py @@ -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. +""" diff --git a/cloudcafe/compute/servers_api/models/requests.py b/cloudcafe/compute/servers_api/models/requests.py new file mode 100644 index 00000000..0eaadf9a --- /dev/null +++ b/cloudcafe/compute/servers_api/models/requests.py @@ -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 diff --git a/cloudcafe/compute/servers_api/models/servers.py b/cloudcafe/compute/servers_api/models/servers.py new file mode 100644 index 00000000..63297c1e --- /dev/null +++ b/cloudcafe/compute/servers_api/models/servers.py @@ -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) diff --git a/cloudcafe/compute/volume_attachments_api/__init__.py b/cloudcafe/compute/volume_attachments_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/volume_attachments_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/volume_attachments_api/models/__init__.py b/cloudcafe/compute/volume_attachments_api/models/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/volume_attachments_api/models/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/volume_attachments_api/models/requests/__init__.py b/cloudcafe/compute/volume_attachments_api/models/requests/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/volume_attachments_api/models/requests/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/volume_attachments_api/models/requests/volume_attachments.py b/cloudcafe/compute/volume_attachments_api/models/requests/volume_attachments.py new file mode 100644 index 00000000..40b2d6ce --- /dev/null +++ b/cloudcafe/compute/volume_attachments_api/models/requests/volume_attachments.py @@ -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) diff --git a/cloudcafe/compute/volume_attachments_api/models/responses/__init__.py b/cloudcafe/compute/volume_attachments_api/models/responses/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/compute/volume_attachments_api/models/responses/__init__.py @@ -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. +""" + diff --git a/cloudcafe/compute/volume_attachments_api/models/responses/volume_attachments.py b/cloudcafe/compute/volume_attachments_api/models/responses/volume_attachments.py new file mode 100644 index 00000000..7603202d --- /dev/null +++ b/cloudcafe/compute/volume_attachments_api/models/responses/volume_attachments.py @@ -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 + diff --git a/cloudcafe/compute/volume_attachments_api/volume_attachments_client.py b/cloudcafe/compute/volume_attachments_api/volume_attachments_client.py new file mode 100644 index 00000000..76ee0f4d --- /dev/null +++ b/cloudcafe/compute/volume_attachments_api/volume_attachments_client.py @@ -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) diff --git a/cloudcafe/identity/__init__.py b/cloudcafe/identity/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/identity/__init__.py @@ -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. +""" + diff --git a/cloudcafe/identity/v2_0/__init__.py b/cloudcafe/identity/v2_0/__init__.py new file mode 100644 index 00000000..21d6ed89 --- /dev/null +++ b/cloudcafe/identity/v2_0/__init__.py @@ -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 +''' diff --git a/cloudcafe/identity/v2_0/provider.py b/cloudcafe/identity/v2_0/provider.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/identity/v2_0/provider.py @@ -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. +""" + diff --git a/cloudcafe/identity/v2_0/tenants_api/__init__.py b/cloudcafe/identity/v2_0/tenants_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/identity/v2_0/tenants_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/identity/v2_0/tenants_api/models/__init__.py b/cloudcafe/identity/v2_0/tenants_api/models/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/identity/v2_0/tenants_api/models/__init__.py @@ -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. +""" + diff --git a/cloudcafe/identity/v2_0/tokens_api/__init__.py b/cloudcafe/identity/v2_0/tokens_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/identity/v2_0/tokens_api/behaviors.py b/cloudcafe/identity/v2_0/tokens_api/behaviors.py new file mode 100644 index 00000000..bf55f9f8 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/behaviors.py @@ -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 diff --git a/cloudcafe/identity/v2_0/tokens_api/client.py b/cloudcafe/identity/v2_0/tokens_api/client.py new file mode 100644 index 00000000..5a893eb7 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/client.py @@ -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 diff --git a/cloudcafe/identity/v2_0/tokens_api/config.py b/cloudcafe/identity/v2_0/tokens_api/config.py new file mode 100644 index 00000000..50e0acb3 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/config.py @@ -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") diff --git a/cloudcafe/identity/v2_0/tokens_api/models/__init__.py b/cloudcafe/identity/v2_0/tokens_api/models/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/__init__.py @@ -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. +""" + diff --git a/cloudcafe/identity/v2_0/tokens_api/models/base.py b/cloudcafe/identity/v2_0/tokens_api/models/base.py new file mode 100644 index 00000000..afb1e7c8 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/base.py @@ -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) diff --git a/cloudcafe/identity/v2_0/tokens_api/models/constants.py b/cloudcafe/identity/v2_0/tokens_api/models/constants.py new file mode 100644 index 00000000..4a756e39 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/constants.py @@ -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' diff --git a/cloudcafe/identity/v2_0/tokens_api/models/requests/__init__.py b/cloudcafe/identity/v2_0/tokens_api/models/requests/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/requests/__init__.py @@ -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. +""" + diff --git a/cloudcafe/identity/v2_0/tokens_api/models/requests/auth.py b/cloudcafe/identity/v2_0/tokens_api/models/requests/auth.py new file mode 100644 index 00000000..b35b950c --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/requests/auth.py @@ -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 diff --git a/cloudcafe/identity/v2_0/tokens_api/models/requests/credentials.py b/cloudcafe/identity/v2_0/tokens_api/models/requests/credentials.py new file mode 100644 index 00000000..71751ad5 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/requests/credentials.py @@ -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 + diff --git a/cloudcafe/identity/v2_0/tokens_api/models/requests/role.py b/cloudcafe/identity/v2_0/tokens_api/models/requests/role.py new file mode 100644 index 00000000..43bb5ef1 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/requests/role.py @@ -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) diff --git a/cloudcafe/identity/v2_0/tokens_api/models/responses/__init__.py b/cloudcafe/identity/v2_0/tokens_api/models/responses/__init__.py new file mode 100644 index 00000000..21d6ed89 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/responses/__init__.py @@ -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 +''' diff --git a/cloudcafe/identity/v2_0/tokens_api/models/responses/access.py b/cloudcafe/identity/v2_0/tokens_api/models/responses/access.py new file mode 100644 index 00000000..2f664ec8 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/responses/access.py @@ -0,0 +1,208 @@ +""" +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 cloudcafe.identity.v2_0.tokens_api.models.base import \ + BaseIdentityModel, BaseIdentityListModel + + +class Access(BaseIdentityModel): + + TAG = 'access' + + def __init__(self): + self.metadata = {} + self.service_catalog = ServiceCatalog() + self.user = User() + self.token = Token() + + def get_service(self, name): + for service in self.service_catalog: + if service.name == name: + return service + return None + + @classmethod + def _json_to_obj(cls, serialized_str): + json_dict = json.loads(serialized_str) + return cls._dict_to_obj(json_dict.get(cls.TAG)) + + @classmethod + def _dict_to_obj(cls, json_dict): + + access = Access() + access.metadata = json_dict.get('metadata') + access.service_catalog = ServiceCatalog._list_to_obj( + json_dict.get(ServiceCatalog.TAG)) + access.user = User._dict_to_obj(json_dict.get(User.TAG)) + access.token = Token._dict_to_obj(json_dict.get(Token.TAG)) + return access + + +class ServiceCatalog(BaseIdentityListModel): + TAG = 'serviceCatalog' + + @classmethod + def _list_to_obj(cls, service_dict_list): + service_catalog = ServiceCatalog() + for service_dict in service_dict_list: + service = Service._dict_to_obj(service_dict) + service_catalog.append(service) + + return service_catalog + + +class Service(BaseIdentityModel): + + def __init__(self): + self.endpoints = EndpointList() + self.endpoint_links = [] + self.name = None + self.type = None + + def get_endpoint(self, region): + '''Returns the endpoint that matches the provided region, + or None if such an endpoint is not found + ''' + for ep in self.endpoints: + if getattr(ep, 'region'): + if str(ep.region).lower() == str(region).lower(): + return ep + + @classmethod + def _dict_to_obj(cls, json_dict): + service = Service() + service.endpoints = EndpointList._list_to_obj( + json_dict.get(EndpointList.TAG)) + service.endpoint_links = json_dict.get('endpoints_links') + service.name = json_dict.get('name') + service.type = json_dict.get('type') + + return service + + +class EndpointList(BaseIdentityListModel): + TAG = 'endpoints' + + @classmethod + def _list_to_obj(cls, endpoint_dict_list): + endpoint_list = EndpointList() + for endpoint_dict in endpoint_dict_list: + endpoint = Endpoint._dict_to_obj(endpoint_dict) + endpoint_list.append(endpoint) + + return endpoint_list + + +class Endpoint(BaseIdentityModel): + + def __init__(self, admin_url, internal_url, public_url, + region, id): + self.admin_url = admin_url + self.internal_url = internal_url + self.public_url = public_url + self.region = region + self.id_ = id + + @classmethod + def _dict_to_obj(cls, json_dict): + endpoint = Endpoint(json_dict.get('adminURL'), + json_dict.get('internalURL'), + json_dict.get('publicURL'), + json_dict.get('region'), + json_dict.get('id')) + return endpoint + + +class Token(BaseIdentityModel): + TAG = 'token' + + def __init__(self): + self.expires = None + self.issued_at = None + self.id_ = None + self.tenant = Tenant() + + @classmethod + def _dict_to_obj(cls, json_dict): + token_model = Token() + token_model.tenant = Tenant._dict_to_obj(json_dict.get('tenant')) + token_model.expires = json_dict.get('expires') + token_model.issued_at = json_dict.get('issued_at') + token_model.id_ = json_dict.get('id') + + return token_model + + +class Tenant(BaseIdentityModel): + TAG = 'tenant' + + def __init__(self): + self.description = None + self.enabled = None + self.id_ = None + self.name = None + + @classmethod + def _dict_to_obj(cls, json_dict): + tenant = Tenant() + tenant.description = json_dict.get('description') + tenant.enabled = json_dict.get('enabled') + tenant.id_ = json_dict.get('id') + tenant.name = json_dict.get('name') + + return tenant + + +class User(BaseIdentityModel): + TAG = 'user' + + def __init__(self): + self.id_ = None + self.name = None + self.roles = RoleList() + self.role_links = [] + self.username = None + + @classmethod + def _dict_to_obj(cls, json_dict): + user = User() + user.id_ = json_dict.get('id') + user.name = json_dict.get('name') + user.roles = RoleList._list_to_obj(json_dict.get(RoleList.TAG)) + user.role_links = json_dict.get('role_links') + user.username = json_dict.get('username') + + return user + + +class RoleList(BaseIdentityListModel): + TAG = 'roles' + + @classmethod + def _list_to_obj(cls, role_dict_list): + role_list = RoleList() + for role_dict in role_dict_list: + role = Role(name=role_dict.get('name')) + role_list.append(role) + + return role_list + + +class Role(BaseIdentityListModel): + + def __init__(self, name=None): + self.name = name diff --git a/cloudcafe/identity/v2_0/tokens_api/models/responses/endpoint.py b/cloudcafe/identity/v2_0/tokens_api/models/responses/endpoint.py new file mode 100644 index 00000000..4d04e213 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/responses/endpoint.py @@ -0,0 +1,130 @@ +""" +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 Endpoints(BaseIdentityListModel): + + ROOT_TAG = 'endpoints' + + def __init__(self, endpoints=None): + super(Endpoints, self).__init__() + self.extend(endpoints) + + @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: [Endpoint(**endpoint) for endpoint in list_]} + return Endpoints(**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(Endpoint.ROOT_TAG)) + + @classmethod + def _xml_list_to_obj(cls, xml_list): + kwargs = {cls.ROOT_TAG: [Endpoint._xml_ele_to_obj(endpoint) + for endpoint in xml_list]} + return Endpoints(**kwargs) + + +class Endpoint(BaseIdentityModel): + + ROOT_TAG = 'endpoint' + + def __init__(self, tenantId=None, region=None, id=None, publicURL=None, + name=None, adminURL=None, type=None, internalURL=None, + versionId=None, versionInfo=None, versionList=None): +# version=None + super(Endpoint, self).__init__() + self.tenantId = tenantId + self.region = region + self.id = id + self.publicURL = publicURL + self.name = name + self.adminURL = adminURL + self.type = type + self.internalURL = internalURL + self.versionId = versionId + self.versionInfo = versionInfo + self.versionList = versionList + #currently json has version attributes as part of the Endpoint + #xml has it as a seprate element. +# self.version = version + + @classmethod + def _json_to_obj(cls, serialized_str): + json_dict = json.loads(serialized_str) + return cls._dict_to_obj(json_dict.get(cls.ROOT_TAG)) + + @classmethod + def _dict_to_obj(cls, dic): + if Version.ROOT_TAG in dic: + dic[Version.ROOT_TAG] = Version(**dic[Version.ROOT_TAG]) + return Endpoint(**dic) + + @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 = {'tenantId': xml_ele.get('tenantId'), + 'region': xml_ele.get('region'), + 'publicURL': xml_ele.get('publicURL'), + 'name': xml_ele.get('name'), + 'adminURL': xml_ele.get('adminURL'), + 'type': xml_ele.get('type'), + 'internalURL': xml_ele.get('internalURL')} + try: + kwargs['id'] = int(xml_ele.get('id')) + except (ValueError, TypeError): + kwargs['id'] = xml_ele.get('id') + version = xml_ele.find(Version.ROOT_TAG) + if version is not None: + kwargs['versionId'] = version.get('id') + kwargs['versionInfo'] = version.get('info') + kwargs['versionList'] = version.get('list') + return Endpoint(**kwargs) + + +class Version(BaseIdentityModel): + + ROOT_TAG = 'version' + + def __init__(self, id=None, info=None, list=None): + self.id = id + self.info = info + self.list = list + + + diff --git a/cloudcafe/identity/v2_0/tokens_api/models/responses/role.py b/cloudcafe/identity/v2_0/tokens_api/models/responses/role.py new file mode 100644 index 00000000..aeba1ae1 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/responses/role.py @@ -0,0 +1,99 @@ +""" +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) diff --git a/cloudcafe/identity/v2_0/tokens_api/models/responses/tenant.py b/cloudcafe/identity/v2_0/tokens_api/models/responses/tenant.py new file mode 100644 index 00000000..e1a1b0cd --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/responses/tenant.py @@ -0,0 +1,101 @@ +""" +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 Tenants(BaseIdentityListModel): + + ROOT_TAG = 'tenants' + + def __init__(self, tenants=None): + '''An object that represents an tenants response object. + ''' + super(Tenants, self).__init__() + self.extend(tenants) + + @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: [Tenant(**tenant) for tenant in list_]} + return Tenants(**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(Tenant.ROOT_TAG)) + + @classmethod + def _xml_list_to_obj(cls, xml_list): + kwargs = {cls.ROOT_TAG: [Tenant._xml_ele_to_obj(ele) + for ele in xml_list]} + return Tenants(**kwargs) + + +class Tenant(BaseIdentityModel): + + ROOT_TAG = 'tenant' + + def __init__(self, id=None, name=None, description=None, enabled=None, + created=None): + '''An object that represents an tenants response object. + ''' + super(Tenant, self).__init__() + self.id = id + self.name = name + self.description = description + self.enabled = enabled + self.created = created + + @classmethod + def _json_to_obj(cls, serialized_str): + json_dict = json.loads(serialized_str) + return cls._dict_to_obj(json_dict.get(cls.ROOT_TAG)) + + @classmethod + def _dict_to_obj(cls, dic): + return Tenant(**dic) + + @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'), + 'created': xml_ele.get('created')} + try: + kwargs['id'] = int(xml_ele.get('id')) + except (ValueError, TypeError): + kwargs['id'] = xml_ele.get('id') + if xml_ele.get('enabled') is not None: + kwargs['enabled'] = json.loads(xml_ele.get('enabled').lower()) + return Tenant(**kwargs) diff --git a/cloudcafe/identity/v2_0/tokens_api/models/responses/user.py b/cloudcafe/identity/v2_0/tokens_api/models/responses/user.py new file mode 100644 index 00000000..79abdc78 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/models/responses/user.py @@ -0,0 +1,149 @@ +""" +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 +from cloudcafe.identity.v2_0.tokens_api.models.responses.role import Roles, Role + + +class Users(BaseIdentityListModel): + + ROOT_TAG = 'users' + + def __init__(self, users=None): + super(Users, self).__init__() + self.extend(users) + + @classmethod + def _json_to_obj(cls, serialized_str): + ret = json.loads(serialized_str) + ret[cls.ROOT_TAG] = [User._dict_to_obj(user) + for user in ret.get(cls.ROOT_TAG)] + return Users(**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(User.ROOT_TAG)) + + @classmethod + def _xml_list_to_obj(cls, xml_list): + kwargs = {cls.ROOT_TAG: [User._xml_ele_to_obj(ele) + for ele in xml_list]} + return Users(**kwargs) + + +class User(BaseIdentityModel): + + ROOT_TAG = 'user' + + def __init__(self, id=None, enabled=None, username=None, updated=None, + created=None, email=None, domainId=None, defaultRegion=None, + password=None, roles=None, name=None, display_name=None): + '''An object that represents an users response object. + Keyword arguments: + ''' + super(User, self).__init__() + self.id = id + self.enabled = enabled + self.username = username + self.updated = updated + self.created = created + self.email = email + self.domainId = domainId + self.defaultRegion = defaultRegion + self.password = password + self.roles = roles + self.name = name + self.display_name = display_name + + def get_role(self, id=None, name=None): + '''Returns the role object if it matches all provided criteria''' + for role in self.roles: + if id and not name: + if role.id == id: + return role + if name and not id: + if role.name == name: + return role + if name and id: + if (role.name == name) and (role.id == id): + return role + + @classmethod + def _json_to_obj(cls, serialized_str): + json_dict = json.loads(serialized_str) + user = cls._dict_to_obj(json_dict.get(cls.ROOT_TAG)) + return user + + @classmethod + def _dict_to_obj(cls, json_dict): + if 'RAX-AUTH:defaultRegion' in json_dict: + json_dict['defaultRegion'] = \ + json_dict['RAX-AUTH:defaultRegion'] + del json_dict['RAX-AUTH:defaultRegion'] + if 'RAX-AUTH:domainId' in json_dict: + json_dict['domainId'] = json_dict['RAX-AUTH:domainId'] + del json_dict['RAX-AUTH:domainId'] + if 'OS-KSADM:password' in json_dict: + json_dict['password'] = json_dict['OS-KSADM:password'] + del json_dict['OS-KSADM:password'] + if 'display-name' in json_dict: + json_dict['display_name'] = json_dict['display-name'] + del json_dict['display-name'] + if Roles.ROOT_TAG in json_dict: + json_dict[Roles.ROOT_TAG] = Roles.\ + _list_to_obj(json_dict[Roles.ROOT_TAG]) + return User(**json_dict) + + @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 = {'username': xml_ele.get('username'), + 'updated': xml_ele.get('updated'), + 'created': xml_ele.get('created'), + 'email': xml_ele.get('email'), + 'domainId': xml_ele.get('domainId'), + 'defaultRegion': xml_ele.get('defaultRegion'), + 'password': xml_ele.get('password'), + 'name': xml_ele.get('name'), + 'display_name': xml_ele.get('display-name')} + try: + kwargs['id'] = int(xml_ele.get('id')) + except (ValueError, TypeError): + kwargs['id'] = xml_ele.get('id') + if xml_ele.get('enabled') is not None: + kwargs['enabled'] = json.loads(xml_ele.get('enabled').lower()) + roles = xml_ele.find(Roles.ROOT_TAG) + if roles is not None: + #if roles is not a list it is a single element with a list of + #role elements + roles = roles.findall(Role.ROOT_TAG) + if roles is not None: + kwargs['roles'] = Roles._xml_list_to_obj(roles) + return User(**kwargs) diff --git a/cloudcafe/identity/v2_0/tokens_api/provider.py b/cloudcafe/identity/v2_0/tokens_api/provider.py new file mode 100644 index 00000000..d6be98a6 --- /dev/null +++ b/cloudcafe/identity/v2_0/tokens_api/provider.py @@ -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. +""" + +''' +@summary: Provider Module for the AUTH API +@note: Should be the primary interface to a test case or external tool. +''' +from cafe.engine.provider import BaseProvider +from cloudcafe.identity.v2_0.tokens_api.client import TokenAPI_Client +from cloudcafe.identity.v2_0.tokens_api.behaviors import TokenAPI_Behaviors +from cloudcafe.identity.v2_0.tokens_api.config import TokenAPI_Config + + +class TokenAPI_Provider(BaseProvider): + + def __init__(self): + self.config = TokenAPI_Config() + url = self.config.authentication_endpoint + self.client = TokenAPI_Client(url, 'json', 'json') + self.behaviors = TokenAPI_Behaviors(self.client) diff --git a/cloudcafe/identity/v2_0/users_api/__init__.py b/cloudcafe/identity/v2_0/users_api/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/identity/v2_0/users_api/__init__.py @@ -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. +""" + diff --git a/cloudcafe/identity/v2_0/users_api/models/__init__.py b/cloudcafe/identity/v2_0/users_api/models/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/identity/v2_0/users_api/models/__init__.py @@ -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. +""" + diff --git a/cloudcafe/objectstorage/__init__.py b/cloudcafe/objectstorage/__init__.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/objectstorage/__init__.py @@ -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. +""" + diff --git a/cloudcafe/objectstorage/config.py b/cloudcafe/objectstorage/config.py new file mode 100644 index 00000000..33d59bfe --- /dev/null +++ b/cloudcafe/objectstorage/config.py @@ -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 ObjectStorageConfig(ConfigSectionInterface): + + SECTION_NAME = 'objectstorage' + + @property + def identity_service_name(self): + return self.get('identity_service_name') + + @property + def region(self): + return self.get('region') diff --git a/cloudcafe/objectstorage/objectstorage_api/behaviors.py b/cloudcafe/objectstorage/objectstorage_api/behaviors.py new file mode 100644 index 00000000..dd8b1e4e --- /dev/null +++ b/cloudcafe/objectstorage/objectstorage_api/behaviors.py @@ -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. +""" + diff --git a/cloudcafe/objectstorage/objectstorage_api/client.py b/cloudcafe/objectstorage/objectstorage_api/client.py new file mode 100644 index 00000000..95c2ec14 --- /dev/null +++ b/cloudcafe/objectstorage/objectstorage_api/client.py @@ -0,0 +1,179 @@ +""" +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 cStringIO +import datetime +import json +import tarfile +import time +import urllib + +from cafe.engine.clients.rest import RestClient + + +class ObjectStorageClient(RestClient): + + def __init__(self, storage_url, auth_token, base_container_name=None, + base_object_name=None): + super(ObjectStorageClient, self).__init__() + self.storage_url = storage_url + self.auth_token = auth_token + self.base_container_name = base_container_name + self.base_object_name = base_object_name + self.default_headers['X-Auth-Token'] = self.auth_token + + def __add_object_metadata_to_headers(self, metadata=None, headers=None): + """ + Call to __build_metadata specifically for object headers + """ + + return self.__build_metadata('X-Object-Meta-', metadata, headers) + + def __add_container_metadata_to_headers(self, metadata=None, headers=None): + """ + Call to __build_metadata specifically for container headers + """ + + return self.__build_metadata('X-Container-Meta-', metadata, headers) + + def __add_account_metadata_to_headers(self, metadata=None, headers=None): + """ + Call to __build_metadata specifically for account headers + """ + return self.__build_metadata('X-Account-Meta-', metadata, headers) + + def __build_metadata(self, prefix, metadata, headers): + """ + Prepends the prefix to all keys in metadata dict, and then joins + the metadata and header dictionaries together. When a conflict + arises between two header keys, the key in headers wins over the + key in metadata. + + Returns a dict composed of the provided headers and the new + prefixed-metadata headers. + + @param prefix: Appended to all keys in metadata dict + @type prefix: String + @param metadata: Expects a dict with strings as keys and values + @type metadata: Dict + @rtype: Dict + """ + if metadata is None: + return headers + + headers = headers if headers is not None else {} + metadata = metadata if metadata is not None else {} + metadata_headers = {} + + for key in metadata: + try: + meta_key = ''.join([prefix, key]) + except TypeError as e: + self.client_log.error( + 'Non-string prefix OR metadata dict value was passed ' + 'to __build_metadata() in object_storage_client.py') + self.client_log.exception(e) + raise + except: + raise + metadata_headers[meta_key] = metadata[key] + + return dict(metadata_headers, **headers) + + def create_container(self, container_name, metadata=None, headers=None, + requestslib_kwargs=None): + + headers = self.__add_container_metadata_to_headers(metadata, headers) + + url = '{0}/{1}'.format(self.storage_url, container_name) + + response = self.request( + 'PUT', + url, + headers=headers, + requestslib_kwargs=requestslib_kwargs) + + return response + + def create_storage_object(self, container_name, object_name, data=None, + metadata=None, headers=None, + requestslib_kwargs=None): + """ + Creates a storage object in a container via PUT + Optionally adds 'X-Object-Metadata-' prefix to any key in the + metadata dictionary, and then adds that metadata to the headers + dictionary. + """ + headers = self.__add_object_metadata_to_headers(metadata, headers) + + url = '{0}/{1}/{2}'.format( + self.storage_url, + container_name, + object_name) + + response = self.request( + 'PUT', + url, + headers=headers, + data=data, + requestslib_kwargs=requestslib_kwargs) + + return response + + def delete_container(self, container_name, headers=None, + requestslib_kwargs=None): + + url = '{0}/{1}'.format(self.storage_url, container_name) + + response = self.request( + 'DELETE', + url, + headers=headers, + requestslib_kwargs=requestslib_kwargs) + + return response + + def delete_storage_object(self, container_name, object_name, headers=None, + requestslib_kwargs=None): + + url = '{0}/{1}/{2}'.format( + self.storage_url, + container_name, + object_name) + + response = self.request( + 'DELETE', + url, + headers=headers, + requestslib_kwargs=requestslib_kwargs) + + return response + + def _purge_container(self, container_name): + params = {'format': 'json'} + r = self.list_objects(container_name, params=params) + try: + json_data = json.loads(r.content) + for entry in json_data: + self.delete_storage_object(container_name, entry['name']) + except Exception: + pass + + return self.delete_container(container_name) + + def force_delete_containers(self, container_list): + for container_name in container_list: + return self._purge_container(container_name) diff --git a/cloudcafe/objectstorage/objectstorage_api/config.py b/cloudcafe/objectstorage/objectstorage_api/config.py new file mode 100644 index 00000000..55cf7b3d --- /dev/null +++ b/cloudcafe/objectstorage/objectstorage_api/config.py @@ -0,0 +1,106 @@ +""" +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 ObjectStorageAPIConfig(ConfigSectionInterface): + + SECTION_NAME = 'objectstorage_api' + + @property + def default_content_length(self): + return self.get('default_content_length') + + @property + def base_container_name(self): + return self.get('base_container_name') + + @property + def base_object_name(self): + return self.get('base_object_name') + + @property + def http_headers_per_request_count(self): + return self.get('http_headers_per_request_count') + + @property + def http_headers_combined_max_len(self): + return self.get('http_headers_combined_max_len') + + @property + def http_request_line_max_len(self): + return self.get('http_request_line_max_len') + + @property + def http_request_max_content_len(self): + return self.get('http_request_max_content_len') + + @property + def containers_name_max_len(self): + return self.get('containers_name_max_len') + + @property + def containers_list_default_count(self): + return self.get('containers_list_default_count') + + @property + def containers_list_default_max_count(self): + return self.get('containers_list_default_max_count') + + @property + def containers_max_count(self): + return self.get('containers_max_count') + + @property + def object_name_max_len(self): + return self.get('object_name_max_len') + + @property + def object_max_size(self): + return self.get('object_max_size') + + @property + def object_metadata_max_count(self): + return self.get('object_metadata_max_count') + + @property + def object_metadata_combined_byte_len(self): + return self.get('object_metadata_combined_byte_len') + + @property + def object_list_default_count(self): + return self.get('object_list_default_count') + + @property + def object_list_default_max_count(self): + return self.get('object_list_default_max_count') + + @property + def metadata_name_max_len(self): + return self.get('metadata_name_max_len') + + @property + def metadata_value_max_len(self): + return self.get('metadata_value_max_len') + + @property + def tempurl_key_cache_time(self): + return self.get('tempurl_key_cache_time') + + @property + def formpost_key_cache_time(self): + return self.get('formpost_key_cache_time') diff --git a/cloudcafe/provider.py b/cloudcafe/provider.py new file mode 100644 index 00000000..eb582b93 --- /dev/null +++ b/cloudcafe/provider.py @@ -0,0 +1,19 @@ +""" +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 import provider as Compute +from cloudcafe.identity import provider as Identity +from cloudcafe.blockstorage import provider as BlockStorage diff --git a/configs/blockstorage/reference.json.config b/configs/blockstorage/reference.json.config new file mode 100644 index 00000000..1b88c514 --- /dev/null +++ b/configs/blockstorage/reference.json.config @@ -0,0 +1,21 @@ +# ====================================================== +# reference.json.config +# ------------------------------------------------------ +# This configuration is specifically a reference +# implementation for a configuration file. +# You must create a proper configuration file and supply +# the correct values for your Environment(s) +# +# For multiple environments it is suggested that you +# generate specific configurations and name the files +# ..config +# ====================================================== + +[token_api] +serialize_format=json +deserialize_format=json +version=v2.0 +username=admin +tenant_name=admin +password=password +authentication_endpoint=http://198.101.247.226:5000 diff --git a/configs/blockstorage/reference.xml.config b/configs/blockstorage/reference.xml.config new file mode 100644 index 00000000..7730654a --- /dev/null +++ b/configs/blockstorage/reference.xml.config @@ -0,0 +1,21 @@ +# ====================================================== +# reference.xml.config +# ------------------------------------------------------ +# This configuration is specifically a reference +# implementation for a configuration file. +# You must create a proper configuration file and supply +# the correct values for your Environment(s) +# +# For multiple environments it is suggested that you +# generate specific configurations and name the files +# ..config +# ====================================================== + +[token_api] +serialize_format=xml +deserialize_format=xml +version=v2.0 +username=admin +tenant_name=admin +password=password +authentication_endpoint=http://198.101.247.226:5000 diff --git a/configs/compute/devstack.json.config b/configs/compute/devstack.json.config new file mode 100644 index 00000000..9f73eac6 --- /dev/null +++ b/configs/compute/devstack.json.config @@ -0,0 +1,47 @@ +# ====================================================== +# devstack.json.config +# ------------------------------------------------------ +# This configuration is specifically a reference +# implementation for a devstack configuration file. +# You must create a proper configuration file and supply +# the correct values for your Environment(s) +# +# For multiple environments it is suggested that you +# generate specific configurations and name the files +# along the lines of +# ..config +# ====================================================== +[marshalling] +serializer=json +deserializer=json + +[token_api] +authentication_endpoint=http://198.101.247.226:5000 +username=admin +password=password +tenant_name=admin +version=v2.0 + +[flavors] +primary_flavor=42 +secondary_flavor=84 + +[images] +primary_image=23363db8-c2ea-484d-81d5-9cc20a46be47 +secondary_image=23363db8-c2ea-484d-81d5-9cc20a46be47 +image_status_interval=15 +snapshot_timeout=900 + +[servers] +server_status_interval=15 +server_build_timeout=600 +server_resize_timeout=1800 +network_for_ssh=public +ip_address_version_for_ssh=4 +instance_disk_path=/dev/xvda + +[compute] +region=RegionOne +compute_endpoint_name=nova + + diff --git a/configs/compute/reference.json.config b/configs/compute/reference.json.config new file mode 100644 index 00000000..34b4f2c4 --- /dev/null +++ b/configs/compute/reference.json.config @@ -0,0 +1,22 @@ +# ====================================================== +# reference.json.config +# ------------------------------------------------------ +# This configuration is specifically a reference +# implementation for a configuration file. +# You must create a proper configuration file and supply +# the correct values for your Environment(s) +# +# For multiple environments it is suggested that you +# generate specific configurations and name the files +# ..config +# ====================================================== +[marshalling] +serialize_format=json +deserialize_format=json + +[token_api] +version=v2.0 +username=admin +tenant_name=admin +password=password +authentication_endpoint=http://198.101.247.226:5000 diff --git a/configs/compute/reference.xml.config b/configs/compute/reference.xml.config new file mode 100644 index 00000000..570ce9fd --- /dev/null +++ b/configs/compute/reference.xml.config @@ -0,0 +1,23 @@ +# ====================================================== +# reference.xml.config +# ------------------------------------------------------ +# This configuration is specifically a reference +# implementation for a configuration file. +# You must create a proper configuration file and supply +# the correct values for your Environment(s) +# +# For multiple environments it is suggested that you +# generate specific configurations and name the files +# along the lines of +# ..config +# ====================================================== +[marshalling] +serialize_format=xml +deserialize_format=xml + +[token_api] +version=v2.0 +username=admin +tenant_name=admin +password=password +authentication_endpoint=http://198.101.247.226:5000 diff --git a/configs/identity/reference.json.config b/configs/identity/reference.json.config new file mode 100644 index 00000000..ca7f0d5b --- /dev/null +++ b/configs/identity/reference.json.config @@ -0,0 +1,23 @@ +# ====================================================== +# reference.json.config +# ------------------------------------------------------ +# This configuration is specifically a reference +# implementation for a configuration file. +# You must create a proper configuration file and supply +# the correct values for your Environment(s) +# +# For multiple environments it is suggested that you +# generate specific configurations and name the files +# along the lines of +# ..config +# ====================================================== +[marshalling] +serialize_format=json +deserialize_format=json + +[token_api] +version=v2.0 +username=admin +tenant_name=admin +password=password +authentication_endpoint=http://198.101.247.226:5000 diff --git a/configs/identity/reference.xml.config b/configs/identity/reference.xml.config new file mode 100644 index 00000000..570ce9fd --- /dev/null +++ b/configs/identity/reference.xml.config @@ -0,0 +1,23 @@ +# ====================================================== +# reference.xml.config +# ------------------------------------------------------ +# This configuration is specifically a reference +# implementation for a configuration file. +# You must create a proper configuration file and supply +# the correct values for your Environment(s) +# +# For multiple environments it is suggested that you +# generate specific configurations and name the files +# along the lines of +# ..config +# ====================================================== +[marshalling] +serialize_format=xml +deserialize_format=xml + +[token_api] +version=v2.0 +username=admin +tenant_name=admin +password=password +authentication_endpoint=http://198.101.247.226:5000 diff --git a/configs/objectstorage/reference.json.config b/configs/objectstorage/reference.json.config new file mode 100644 index 00000000..ca7f0d5b --- /dev/null +++ b/configs/objectstorage/reference.json.config @@ -0,0 +1,23 @@ +# ====================================================== +# reference.json.config +# ------------------------------------------------------ +# This configuration is specifically a reference +# implementation for a configuration file. +# You must create a proper configuration file and supply +# the correct values for your Environment(s) +# +# For multiple environments it is suggested that you +# generate specific configurations and name the files +# along the lines of +# ..config +# ====================================================== +[marshalling] +serialize_format=json +deserialize_format=json + +[token_api] +version=v2.0 +username=admin +tenant_name=admin +password=password +authentication_endpoint=http://198.101.247.226:5000 diff --git a/configs/objectstorage/reference.xml.config b/configs/objectstorage/reference.xml.config new file mode 100644 index 00000000..570ce9fd --- /dev/null +++ b/configs/objectstorage/reference.xml.config @@ -0,0 +1,23 @@ +# ====================================================== +# reference.xml.config +# ------------------------------------------------------ +# This configuration is specifically a reference +# implementation for a configuration file. +# You must create a proper configuration file and supply +# the correct values for your Environment(s) +# +# For multiple environments it is suggested that you +# generate specific configurations and name the files +# along the lines of +# ..config +# ====================================================== +[marshalling] +serialize_format=xml +deserialize_format=xml + +[token_api] +version=v2.0 +username=admin +tenant_name=admin +password=password +authentication_endpoint=http://198.101.247.226:5000 diff --git a/pip-requires b/pip-requires new file mode 100644 index 00000000..e69de29b diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..c85efc56 --- /dev/null +++ b/setup.py @@ -0,0 +1,120 @@ +""" +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 +import sys +import pwd +import grp +import cloudcafe +import shutil + +try: + from setuptools import setup, find_packages +except ImportError: + from distutils.core import setup, find_packages + +if sys.argv[-1] == 'publish': + os.system('python setup.py sdist upload') + sys.exit() + +requires = open('pip-requires').readlines() + +setup( + name='cloudcafe', + version=cloudcafe.__version__, + description='CloudCAFE is an implementation of the Open CAFE Framework specifically designed to test deployed versions of OpenStack', + long_description='{0}\n\n{1}'.format( + open('README.md').read(), + open('HISTORY.rst').read()), + author='Rackspace Cloud QE', + author_email='cloud-cafe@lists.rackspace.com', + url='http://rackspace.com', + packages=find_packages(exclude=[]), + package_data={'': ['LICENSE', 'NOTICE']}, + package_dir={'cloudcafe': 'cloudcafe'}, + include_package_data=True, + install_requires=requires, + license=open('LICENSE').read(), + zip_safe=False, + #https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=( + 'Development Status :: 1 - Planning', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: Other/Proprietary License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + #'Programming Language :: Python :: 3', + #'Programming Language :: Python :: 3.0', + #'Programming Language :: Python :: 3.1', + #'Programming Language :: Python :: 3.2', + #'Programming Language :: Python :: 3.3', + ) +) + +''' @todo: need to clean this up or do it with puppet/chef ''' +# Default Config Options +root_dir = "{0}/.cloudcafe".format(os.path.expanduser("~")) +config_dir = "{0}/configs".format(root_dir) + +# Build Default directories +if(os.path.exists("{0}/engine.config".format(config_dir)) == False): + raise Exception("Core CAFE Engine configuration not found") +else: + # Copy over the default configurations + if(os.path.exists("~install")): + os.remove("~install") + # Report + print('\n'.join(["\t\t _ _ _", + "\t\t ( ` )_ ", + "\t\t ( ) `) _", + "\t\t(____(__.___`)__)", + "\t\t", + "\t\t ( (", + "\t\t ) )", + "\t\t ......... ", + "\t\t | |___ ", + "\t\t | |_ |", + "\t\t | :-) |_| |", + "\t\t | |___|", + "\t\t |_______|", + "\t\t=== CloudCAFE ==="])) + print("========================================================") + print("CloudCAFE Framework installed") + print("========================================================") + else: + # State file + temp = open("~install", "w") + temp.close() + + ''' todo: This is MAC/Linux Only ''' + # get who really executed this + sudo_user = os.getenv("SUDO_USER") + uid = pwd.getpwnam(sudo_user).pw_uid + gid = pwd.getpwnam(sudo_user).pw_gid + + config_dirs = os.listdir("configs") + for dir in config_dirs: + if not os.path.exists("{0}/{1}".format(config_dir, dir)): + print("Installing configurations for: {0}".format("{0}/{1}".format(config_dir, dir))) + os.makedirs("{0}/{1}".format(config_dir, dir)) + os.chown("{0}/{1}".format(config_dir, dir), uid, gid) + for file in os.listdir("configs/{0}".format(dir)): + print("Installing {0}/{1}/{2}".format(config_dir, dir, file)) + shutil.copy2("configs/{0}/{1}".format(dir, file), "{0}/{1}/{2}".format(config_dir, dir, file)) + os.chown("{0}/{1}/{2}".format(config_dir, dir, file), uid, gid)