diff --git a/.gitignore b/.gitignore index 1399c98..b597ded 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.py[cod] +.venv # C extensions *.so @@ -23,6 +24,7 @@ pip-log.txt # Unit test / coverage reports .coverage +cover .tox nosetests.xml .testrepository @@ -48,4 +50,6 @@ ChangeLog # Editors *~ -.*.swp \ No newline at end of file +.*.swp +.idea +.vscode diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..271b4fd --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,28 @@ +When developping on flameclient, do not forget to check code quality with the +`./checkcode` command, and then check you did not brake anything by running +`python -m unittest discover -v`. + +To create flame modules you need to create a module with a class which +inheritates from `flameclient.resources.ResourceManager` + +You need to implement the `api_resources` property and the `get_hot_resources()` +method (read their docstring for more information). +If you want you can also implement the `add_arguments(parser=None)` method to +add your own module's command line arguments. +You can also implement the `post_process()`, +`post_process_hot_resources(resources)`, `post_process_heat_template(template)` and/or +`post_process_adoption_data(adoption_data)` methods to perform +post processing after the generator's `extract_data` method was called +(read their docstring for more information). This allows you to modify results +before rendering the template. +These post processing methods are not threaded and are executed in order of the +managers' `post_priority` attribute (defaults to 100). + +Then, you need to add in your package's `setup.py` or `setup.cfg` an +'openstack_flame' entry point pointing to the module file where your subclass +of `flameclient.resources.ResourceManager` is defined. + +Once your package installed, flame will automatically discover all +'openstack_flame' entry points to load the corresponding modules, and all +loaded modules having a `flameclient.resources.ResourceManager` subclass will +have this subclass detected and added tho the list of managers. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index ce92823..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -This software is released under the MIT License. - -Copyright (c) 2014 Cloudwatt - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSE b/LICENSE new file mode 120000 index 0000000..75b02ab --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +flameclient/LICENSE.txt \ No newline at end of file diff --git a/README.rst b/README.rst index f11ebce..aa796a7 100644 --- a/README.rst +++ b/README.rst @@ -32,64 +32,154 @@ Then just run: Usage ----- - usage: flame [-h] [--username USERNAME] [--password PASSWORD] - [--project PROJECT] [--region REGION] [--auth_url AUTH_URL] - [--os-auth-token OS_AUTH_TOKEN] [--insecure] - [--os-cert ] [--os-key ] - [--endpoint_type ENDPOINT_TYPE] [--exclude-servers] - [--exclude-volumes] [--exclude-keypairs] [--generate-stack-data] - [--extract-ports] [--exclude-secgroup] +To use the CLI of flame:: + + usage: flame [-h] [--debug] [--generate-stack-data] [--include-constraints] + [--no-threads] [--prefetch] [--exclude-keypairs] + [--extract-ports] [--exclude-secgroups] [--exclude-servers] + [--exclude-volumes] [--os-cloud ] [--os-auth-type ] + [--os-auth-url OS_AUTH_URL] [--os-system-scope OS_SYSTEM_SCOPE] + [--os-domain-id OS_DOMAIN_ID] [--os-domain-name OS_DOMAIN_NAME] + [--os-project-id OS_PROJECT_ID] + [--os-project-name OS_PROJECT_NAME] + [--os-project-domain-id OS_PROJECT_DOMAIN_ID] + [--os-project-domain-name OS_PROJECT_DOMAIN_NAME] + [--os-trust-id OS_TRUST_ID] + [--os-default-domain-id OS_DEFAULT_DOMAIN_ID] + [--os-default-domain-name OS_DEFAULT_DOMAIN_NAME] + [--os-user-id OS_USER_ID] [--os-username OS_USERNAME] + [--os-user-domain-id OS_USER_DOMAIN_ID] + [--os-user-domain-name OS_USER_DOMAIN_NAME] + [--os-password OS_PASSWORD] [--insecure] + [--os-cacert ] [--os-cert ] + [--os-key ] [--timeout ] [--collect-timing] + [--os-service-type ] [--os-service-name ] + [--os-interface ] [--os-region-name ] + [--os-endpoint-override ] [--os-api-version ] Heat template and data file generator optional arguments: -h, --help show this help message and exit - --username USERNAME A user name with access to the project. Defaults to - env[OS_USERNAME] - --password PASSWORD The user's password. Defaults to env[OS_PASSWORD] - --project PROJECT Name of project. Defaults to env[OS_TENANT_NAME] - --region REGION Name of region. Defaults to env[OS_REGION_NAME] - --auth_url AUTH_URL Authentication URL. Defaults to env[OS_AUTH_URL]. - --os-auth-token OS_AUTH_TOKEN - User's auth token. Defaults to env[OS_AUTH_TOKEN]. - --os-cert - Path to user's certificate needed to establish - two-way SSL connection with the identity service. - Defaults to env[OS_CERT]. - --os-key Path to the user's certificate private key. - Defaults to env[OS_KEY]. - --insecure Explicitly allow clients to perform"insecure" SSL - (https) requests. The server's certificate will not be - verified against any certificate authorities. This - option should be used with caution. - --endpoint_type ENDPOINT_TYPE - Defaults to env[OS_ENDPOINT_TYPE] or publicURL - --exclude-servers Do not export in template server resources - --exclude-volumes Do not export in template volume resources - --exclude-keypairs Do not export in template key pair resources + --debug set debuging log level --generate-stack-data In addition to template, generate Heat stack data file. + --include-constraints + Export in template custom constraints + --no-threads Deactivate threads for api calls, (usefull for (i)pdb + debugging. + --prefetch Prefetch all API calls (works only without --no- + threads + --exclude-keypairs Do not export in template key pair resources --extract-ports Export the tenant network ports --exclude-secgroups Do not export in template security group resources + --exclude-servers Do not export in template server resources + --exclude-volumes Do not export in template volume resources + --os-cloud Named cloud to connect to + --os-auth-type , --os-auth-plugin + Authentication type to use + + Authentication Options: + Options specific to the password plugin. + + --os-auth-url OS_AUTH_URL + Authentication URL + --os-system-scope OS_SYSTEM_SCOPE + Scope for system operations + --os-domain-id OS_DOMAIN_ID + Domain ID to scope to + --os-domain-name OS_DOMAIN_NAME + Domain name to scope to + --os-project-id OS_PROJECT_ID, --os-tenant-id OS_PROJECT_ID + Project ID to scope to + --os-project-name OS_PROJECT_NAME, --os-tenant-name OS_PROJECT_NAME + Project name to scope to + --os-project-domain-id OS_PROJECT_DOMAIN_ID + Domain ID containing project + --os-project-domain-name OS_PROJECT_DOMAIN_NAME + Domain name containing project + --os-trust-id OS_TRUST_ID + Trust ID + --os-default-domain-id OS_DEFAULT_DOMAIN_ID + Optional domain ID to use with v3 and v2 parameters. + It will be used for both the user and project domain + in v3 and ignored in v2 authentication. + --os-default-domain-name OS_DEFAULT_DOMAIN_NAME + Optional domain name to use with v3 API and v2 + parameters. It will be used for both the user and + project domain in v3 and ignored in v2 authentication. + --os-user-id OS_USER_ID + User id + --os-username OS_USERNAME, --os-user-name OS_USERNAME + Username + --os-user-domain-id OS_USER_DOMAIN_ID + User's domain id + --os-user-domain-name OS_USER_DOMAIN_NAME + User's domain name + --os-password OS_PASSWORD + User's password + + API Connection Options: + Options controlling the HTTP API Connections + + --insecure Explicitly allow client to perform "insecure" TLS + (https) requests. The server's certificate will not be + verified against any certificate authorities. This + option should be used with caution. + --os-cacert + Specify a CA bundle file to use in verifying a TLS + (https) server certificate. Defaults to + env[OS_CACERT]. + --os-cert + Defaults to env[OS_CERT]. + --os-key Defaults to env[OS_KEY]. + --timeout Set request timeout (in seconds). + --collect-timing Collect per-API call timing information. + + Service Options: + Options controlling the specialization of the API Connection from + information found in the catalog + + --os-service-type + Service type to request from the catalog + --os-service-name + Service name to request from the catalog + --os-interface + API Interface to use [public, internal, admin] + --os-region-name + Region of the cloud to use + --os-endpoint-override + Endpoint to use instead of the endpoint in the catalog + --os-api-version + Which version of the service API to use Usage example ------------- -To use Flame you can provide yours OpenStack credentials as arguments : - - $ flame --username user --password password --project project - --auth_url http://:5000/v2.0 +To use Flame you can provide yours OpenStack credentials as arguments :: + $ flame --os-username 'user_name' \ + --os-password 'password' \ + --os-project-name 'project_name' \ + --os-auth-url 'http://:5000/v2.0' Or you can source your OpenStack RC file and use Flame without arguments. -To establish a two-way SSL connection with the identity service : +To establish a two-way SSL connection with the identity service :: - $flame --username arezmerita --os-auth-token keystonetoken \ - --project project-arezmerita --auth_url http://:5000/v2.0 - --os-cert --os-key + $flame --os-username 'user_name' \ + --os-password 'password' \ + --os-project-name 'project_name' \ + --os-auth_url http://:5000/v2.0 \ + --os-cert \ + --os-key Flame can be used with either a login and password pair or a keystone -token by exporting the OS_AUTH_TOKEN variable (the token is obtained -with keystone token-get). +token by exporting the OS_AUTH_TOKEN variable and the `--os-auth-type 'token'` +parameter (the token is obtained with keystone token-get ):: + + $ flame --os-auth-type 'token' \ + --os-token 'token_id' \ + --os-auth-url 'http://:5000/v2.0' + diff --git a/check_code b/check_code new file mode 100755 index 0000000..bf4a519 --- /dev/null +++ b/check_code @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +CURDIR=$(pwd) +SELFDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +cd $SELFDIR +python -m flake8 flameclient +if [[ $1 = '-v' ]]; then + python -m pylint --rcfile=pylintrc --reports=yes flameclient +else + python -m pylint --rcfile=pylintrc flameclient +fi +cd $CURDIR diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..55f1dea --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,4 @@ +bpython==0.17.1 +ipython==5.8.0 +ipdb==0.11 +q==2.6 diff --git a/doc/source/conf.py b/doc/source/conf.py index d75a2a9..6b42f88 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -72,4 +72,4 @@ latex_documents = [ ] # Example configuration for intersphinx: refer to the Python standard library. -#intersphinx_mapping = {'http://docs.python.org/': None} \ No newline at end of file +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/installation.rst b/doc/source/installation.rst index 6682d54..1084311 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -9,4 +9,5 @@ At the command line:: Or, if you have virtualenvwrapper installed:: $ mkvirtualenv python-flameclient - $ pip install python-flameclient \ No newline at end of file + $ pip install python-flameclient + diff --git a/doc/source/limitations.rst b/doc/source/limitations.rst index 333682d..963af4b 100644 --- a/doc/source/limitations.rst +++ b/doc/source/limitations.rst @@ -14,5 +14,3 @@ free IP addresses of a pool, for example, with a pool starting at 10.0.0.2 : When this stack is imported in Heat, the DHCP server IP is set to the lowest free IP address of its pool. Depending on the VM creation order, the DHCP address can either collide with vm1's or vm2's IP. - - diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 15f480d..d4d49e1 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -8,45 +8,125 @@ To use install flame in a project:: To use the CLI of flame:: - usage: flame [-h] [--username USERNAME] [--password PASSWORD] - [--project PROJECT] [--region REGION] [--auth_url AUTH_URL] - [--os-auth-token OS_AUTH_TOKEN] [--insecure] - [--os-cert ] [--os-key ] - [--endpoint_type ENDPOINT_TYPE] [--exclude-servers] - [--exclude-volumes] [--exclude-keypairs] [--generate-stack-data] - [--extract-ports] + usage: flame [-h] [--debug] [--generate-stack-data] [--include-constraints] + [--no-threads] [--prefetch] [--exclude-keypairs] + [--extract-ports] [--exclude-secgroups] [--exclude-servers] + [--exclude-volumes] [--os-cloud ] [--os-auth-type ] + [--os-auth-url OS_AUTH_URL] [--os-system-scope OS_SYSTEM_SCOPE] + [--os-domain-id OS_DOMAIN_ID] [--os-domain-name OS_DOMAIN_NAME] + [--os-project-id OS_PROJECT_ID] + [--os-project-name OS_PROJECT_NAME] + [--os-project-domain-id OS_PROJECT_DOMAIN_ID] + [--os-project-domain-name OS_PROJECT_DOMAIN_NAME] + [--os-trust-id OS_TRUST_ID] + [--os-default-domain-id OS_DEFAULT_DOMAIN_ID] + [--os-default-domain-name OS_DEFAULT_DOMAIN_NAME] + [--os-user-id OS_USER_ID] [--os-username OS_USERNAME] + [--os-user-domain-id OS_USER_DOMAIN_ID] + [--os-user-domain-name OS_USER_DOMAIN_NAME] + [--os-password OS_PASSWORD] [--insecure] + [--os-cacert ] [--os-cert ] + [--os-key ] [--timeout ] [--collect-timing] + [--os-service-type ] [--os-service-name ] + [--os-interface ] [--os-region-name ] + [--os-endpoint-override ] [--os-api-version ] Heat template and data file generator optional arguments: -h, --help show this help message and exit - --username USERNAME A user name with access to the project. Defaults to - env[OS_USERNAME] - --password PASSWORD The user's password. Defaults to env[OS_PASSWORD] - --project PROJECT Name of project. Defaults to env[OS_TENANT_NAME] - --region REGION Name of region. Defaults to env[OS_REGION_NAME] - --auth_url AUTH_URL Authentication URL. Defaults to env[OS_AUTH_URL]. - --os-auth-token OS_AUTH_TOKEN - User's auth token. Defaults to env[OS_AUTH_TOKEN]. - --os-cert - Path to user's certificate needed to establish - two-way SSL connection with the identity service. - Defaults to env[OS_CERT]. - --os-key Path to the user's certificate private key. - Defaults to env[OS_KEY]. - --insecure Explicitly allow clients to perform"insecure" SSL - (https) requests. The server's certificate will not be - verified against any certificate authorities. This - option should be used with caution. - --endpoint_type ENDPOINT_TYPE - Defaults to env[OS_ENDPOINT_TYPE] or publicURL - --exclude-servers Do not export in template server resources - --exclude-volumes Do not export in template volume resources - --exclude-keypairs Do not export in template key pair resources + --debug set debuging log level --generate-stack-data In addition to template, generate Heat stack data file. + --include-constraints + Export in template custom constraints + --no-threads Deactivate threads for api calls, (usefull for (i)pdb + debugging. + --prefetch Prefetch all API calls (works only without --no- + threads + --exclude-keypairs Do not export in template key pair resources --extract-ports Export the tenant network ports + --exclude-secgroups Do not export in template security group resources + --exclude-servers Do not export in template server resources + --exclude-volumes Do not export in template volume resources + --os-cloud Named cloud to connect to + --os-auth-type , --os-auth-plugin + Authentication type to use + + Authentication Options: + Options specific to the password plugin. + + --os-auth-url OS_AUTH_URL + Authentication URL + --os-system-scope OS_SYSTEM_SCOPE + Scope for system operations + --os-domain-id OS_DOMAIN_ID + Domain ID to scope to + --os-domain-name OS_DOMAIN_NAME + Domain name to scope to + --os-project-id OS_PROJECT_ID, --os-tenant-id OS_PROJECT_ID + Project ID to scope to + --os-project-name OS_PROJECT_NAME, --os-tenant-name OS_PROJECT_NAME + Project name to scope to + --os-project-domain-id OS_PROJECT_DOMAIN_ID + Domain ID containing project + --os-project-domain-name OS_PROJECT_DOMAIN_NAME + Domain name containing project + --os-trust-id OS_TRUST_ID + Trust ID + --os-default-domain-id OS_DEFAULT_DOMAIN_ID + Optional domain ID to use with v3 and v2 parameters. + It will be used for both the user and project domain + in v3 and ignored in v2 authentication. + --os-default-domain-name OS_DEFAULT_DOMAIN_NAME + Optional domain name to use with v3 API and v2 + parameters. It will be used for both the user and + project domain in v3 and ignored in v2 authentication. + --os-user-id OS_USER_ID + User id + --os-username OS_USERNAME, --os-user-name OS_USERNAME + Username + --os-user-domain-id OS_USER_DOMAIN_ID + User's domain id + --os-user-domain-name OS_USER_DOMAIN_NAME + User's domain name + --os-password OS_PASSWORD + User's password + + API Connection Options: + Options controlling the HTTP API Connections + + --insecure Explicitly allow client to perform "insecure" TLS + (https) requests. The server's certificate will not be + verified against any certificate authorities. This + option should be used with caution. + --os-cacert + Specify a CA bundle file to use in verifying a TLS + (https) server certificate. Defaults to + env[OS_CACERT]. + --os-cert + Defaults to env[OS_CERT]. + --os-key Defaults to env[OS_KEY]. + --timeout Set request timeout (in seconds). + --collect-timing Collect per-API call timing information. + + Service Options: + Options controlling the specialization of the API Connection from + information found in the catalog + + --os-service-type + Service type to request from the catalog + --os-service-name + Service name to request from the catalog + --os-interface + API Interface to use [public, internal, admin] + --os-region-name + Region of the cloud to use + --os-endpoint-override + Endpoint to use instead of the endpoint in the catalog + --os-api-version + Which version of the service API to use Example @@ -54,21 +134,28 @@ Example To use Flame you can provide yours OpenStack credentials as arguments:: - $ flame --username arezmerita --password password \ - --project project-arezmerita --auth_url https://example.com/v2.0/ + $ flame --os-username 'user_name' \ + --os-password 'password' \ + --os-project-name 'project_name' \ + --os-auth-url 'http://:5000/v2.0' Or a token and a tenant:: - $ flame --username arezmerita --os-auth-token keystonetoken \ - --project project-arezmerita --auth_url https://example.com/v2.0/ + $ flame --os-auth-type 'token' \ + --os-token 'token_id' \ + --os-auth-url 'http://:5000/v2.0' To establish a two-way SSL connection with the identity service :: - $flame --username arezmerita --os-auth-token keystonetoken \ - --project project-arezmerita --auth_url https://example.com/v2.0/ - --os-cert --os-key + $flame --os-username 'user_name' \ + --os-password 'password' \ + --os-project-name 'project_name' \ + --os-auth_url http://:5000/v2.0 \ + --os-cert \ + --os-key Or you can source your OpenStack RC file and use Flame without arguments:: $ source credential.rc $ flame + diff --git a/flameclient/LICENSE.txt b/flameclient/LICENSE.txt new file mode 100644 index 0000000..3a5cc0d --- /dev/null +++ b/flameclient/LICENSE.txt @@ -0,0 +1,21 @@ +This software is released under the MIT License. + +Copyright (c) 2018 Orange Cloud for Business / Cloudwatt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flameclient/__init__.py b/flameclient/__init__.py index b304948..c737e78 100644 --- a/flameclient/__init__.py +++ b/flameclient/__init__.py @@ -1,20 +1,30 @@ # -*- coding: utf-8 -*- -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# This software is released under the MIT License. # -# http://www.apache.org/licenses/LICENSE-2.0 +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. import pbr.version try: __version__ = pbr.version.VersionInfo('flameclient').version_string() -except Exception: +except Exception: # pylint: disable=W0703 __version__ = 'unknown' diff --git a/flameclient/client.py b/flameclient/client.py deleted file mode 100644 index d342519..0000000 --- a/flameclient/client.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- - -# This software is released under the MIT License. -# -# Copyright (c) 2014 Cloudwatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from flameclient.flame import TemplateGenerator # noqa - - -class Client(object): - def __init__(self, username, password, tenant_name, auth_url, auth_token, - **kwargs): - self.template_generator = TemplateGenerator(username, password, - tenant_name, auth_url, - auth_token, - **kwargs) - - def generate(self, exclude_servers, exclude_volumes, exclude_keypairs, - generate_stack_data, extract_ports=False, - exclude_secgroups=False): - self.template_generator.extract_vm_details(exclude_servers, - exclude_volumes, - exclude_keypairs, - generate_stack_data, - extract_ports, - exclude_secgroups - ) - self.template_generator.extract_data() - return self.template_generator.heat_template_and_data() diff --git a/flameclient/cmd.py b/flameclient/cmd.py index 4f4c230..7f7da3a 100644 --- a/flameclient/cmd.py +++ b/flameclient/cmd.py @@ -2,7 +2,7 @@ # This software is released under the MIT License. # -# Copyright (c) 2014 Cloudwatt +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,95 +22,59 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import print_function - -import argparse -import os - -from flameclient import client +from flameclient import flame -def main(args=None): - desc = "Heat template and data file generator" - parser = argparse.ArgumentParser(description=desc) - parser.add_argument("--username", type=str, - default=os.environ.get("OS_USERNAME"), - help="A user name with access to the project. " - "Defaults to env[OS_USERNAME]") - parser.add_argument("--password", type=str, - default=os.environ.get("OS_PASSWORD"), - help="The user's password. " - "Defaults to env[OS_PASSWORD]") - parser.add_argument("--project", type=str, - default=os.environ.get("OS_TENANT_NAME"), - help="Name of project. " - "Defaults to env[OS_TENANT_NAME]") - parser.add_argument("--region", - default=os.environ.get("OS_REGION_NAME"), - help="Name of region. " - "Defaults to env[OS_REGION_NAME]") - parser.add_argument("--auth_url", type=str, - default=os.environ.get("OS_AUTH_URL"), - help="Authentication URL. " - "Defaults to env[OS_AUTH_URL].") - parser.add_argument("--os-auth-token", type=str, - default=os.environ.get("OS_AUTH_TOKEN"), - help="User's auth token. " - "Defaults to env[OS_AUTH_TOKEN].") - parser.add_argument('--insecure', action='store_true', default=False, - help="Explicitly allow clients to perform" - "\"insecure\" SSL (https) requests. The " - "server's certificate will not be verified " - "against any certificate authorities. This " - "option should be used with caution.") - parser.add_argument("--endpoint_type", type=str, - default=os.environ.get("OS_ENDPOINT_TYPE", - "publicURL"), - help="Defaults to env[OS_ENDPOINT_TYPE] or publicURL") - parser.add_argument("--os-cert", type=str, metavar='', - default=os.environ.get("OS_CERT"), - help="User's certificate. " - "Defaults to env[OS_CERT].") - parser.add_argument("--os-key", type=str, metavar='', - default=os.environ.get("OS_KEY"), - help="User's key. " - "Defaults to env[OS_KEY].") - parser.add_argument('--exclude-servers', action='store_true', - default=False, - help="Do not export in template server resources") - parser.add_argument('--exclude-volumes', action='store_true', - default=False, - help="Do not export in template volume resources") - parser.add_argument('--exclude-keypairs', action='store_true', - default=False, - help="Do not export in template key pair resources") - parser.add_argument('--generate-stack-data', action='store_true', - default=False, - help="In addition to template, generate Heat " - "stack data file.") - parser.add_argument('--extract-ports', action='store_true', - default=False, - help="Export the tenant network ports") - parser.add_argument('--exclude-secgroups', action='store_true', - default=False, - help="Do not export in template " - "security group resources") +def main(): + """Flame heat template generation - args = parser.parse_args() - flame = client.Client(args.username, args.password, - args.project, args.auth_url, - args.os_auth_token, - cert=args.os_cert, key=args.os_key, - region_name=args.region, - endpoint_type=args.endpoint_type, - insecure=args.insecure) - template = flame.template_generator - template.extract_vm_details(args.exclude_servers, - args.exclude_volumes, - args.exclude_keypairs, - args.generate_stack_data, - args.extract_ports, - args.exclude_secgroups) - template.extract_data() - print("### Heat Template ###") - print(template.heat_template_and_data()) + Flame can be used with a shade or openstack sdk instance, and options can + be passed as option kwargs. + + ex: + + from flameclient.session import get_shade + from flameclient import flame + + auth_kwargs = { + 'auth_type': u'password', + 'auth_url': 'https://your_cloud_identity_url/v2.0', + 'interface': 'public', + 'password': 'YourPassword', + 'project_id': 'YourProjectID', + 'project_name': 'YourProjectName', + 'region_name': 'region_one', + 'username': 'YourUserName' + } + + cloud = get_shade(**auth_kwargs) + + Or instead of kwargs if you want to use environment variables: + + cloud = get_shade(load_envvars=True) + + Then: + + generator = flame.TemplateGenerator( + connection=cloud, + options=dict( + include_constraint=True, + extract_ports=True, + generate_adoption_data=True, + no_threads=True) + ) + + generator.extract_data() + print(generator.heat_template_and_data()) + + Passing a shade or openstack sdk instance and option kwargs allows you to + integrate shade in other projects. + + """ + template_generator = flame.TemplateGenerator() + template_generator.extract_data() + template_generator.output_template_and_data() + + +if __name__ == '__main__': + main() diff --git a/flameclient/collections_abc.py b/flameclient/collections_abc.py new file mode 100644 index 0000000..387ad1b --- /dev/null +++ b/flameclient/collections_abc.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +try: + # This is not handled by six.moves yet: + # https://github.com/benjaminp/six/issues/155 + from collections.abc import * # noqa pylint: disable=W0401,W0614 + from collections.abc import __all__ # noqa +except ImportError: + from _abcoll import * # noqa pylint: disable=W0401,W0614 + from _abcoll import __all__ # noqa diff --git a/flameclient/flame.py b/flameclient/flame.py index 8a45349..3449eed 100644 --- a/flameclient/flame.py +++ b/flameclient/flame.py @@ -2,7 +2,7 @@ # This software is released under the MIT License. # -# Copyright (c) 2014 Cloudwatt +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,17 +22,33 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from __future__ import print_function + +import argparse +import collections import logging +import threading import concurrent.futures -import netaddr + import six + import yaml -from flameclient import managers +from keystoneauth1.exceptions.auth_plugins import OptionError +from openstack.connection import Connection # noqa +from shade.openstackcloud import OpenStackCloud # noqa +from flameclient import resources as base_resources +from flameclient import session +from flameclient import utils + +# We want logging to be configured with the configuration, so the following +# import is to import logging configuration, therefore we silence out pep8 +# and pylint about this unused import: +from flameclient import logs # noqa # pylint: disable=W0611 +LOG = logging.getLogger(__name__) -logging.basicConfig(level=logging.ERROR) template_skeleton = ''' heat_template_version: 2013-05-23 @@ -41,718 +57,344 @@ parameters: resources: ''' -stack_data_skeleton = ''' +adoption_data_skeleton = ''' status: 'COMPLETE' action: 'CREATE' resources: ''' -class Resource(object): - """Describes an OpenStack resource.""" - - def __init__(self, name, type, id=None, properties=None): - self.name = name - self.type = type - self.id = id - self.status = 'COMPLETE' - self.properties = properties or {} - self.parameters = {} - - def add_parameter(self, name, description, parameter_type='string', - constraints=None, default=None): - data = { - 'type': parameter_type, - 'description': description, - } - - # (arezmerita) disable cause heat bug #1314240 - # if constraints: - # data['constraints'] = constraints - if default: - data['default'] = default - - self.parameters[name] = data - - @property - def template_resource(self): - return { - self.name: { - 'type': self.type, - 'properties': self.properties - } - } - - @property - def template_parameter(self): - return self.parameters - - @property - def stack_resource(self): - if self.id is None: - return {} - return { - self.name: { - 'status': self.status, - 'name': self.name, - 'resource_data': {}, - 'resource_id': self.id, - 'action': 'CREATE', - 'type': self.type, - 'metadata': {} - } - } +def add_arguments(parser=None, managers=None): + if parser is None: + desc = "Heat template and data file generator" + parser = argparse.ArgumentParser(description=desc) + # argparse does not support `allow_abbrev=False` before python 3.5 + # e.g.: `argparse.ArgumentParser(description=desc, allow_abbrev=False)` + # will not work in python 2 to 3.4 so we do not use it yet. + # More info: + # https://stackoverflow.com/questions/10750802/disable-abbreviation-in-argparse" # noqa + parser.add_argument('-v', '--debug', action='store_true', + default=False, + help="Activate debug verbose output (set log level to " + "debug).") + parser.add_argument('-f', '--file', metavar='FILE_NAME', + help="Send results to FILE_NAME instead of standard " + "output.") + parser.add_argument('--generate-adoption-data', action='store_true', + default=False, + help="Generate Heat adoption data.") + parser.add_argument('--include-constraints', action='store_true', + default=False, + help="Export in template custom constraints.") + parser.add_argument('--no-threads', action='store_true', + default=False, + help='Deactivate threads for api calls, (usefull for ' + '(i)pdb debugging.') + parser.add_argument('--prefetch', action='store_true', + default=False, + help='Prefetch all API calls (works only without ' + '--no-threads).') + if managers is not None: + for manager in managers: + manager.add_arguments(parser) + return parser class TemplateGenerator(object): + """Generate a heat template from existing openstack resources. + + :param openstack.connection.Connection connection: + An `openstack.connection.Connection` instance (`openstacksdk`). + Since `shade.openstackcloud.OpenStackCloud` (`shade`) is a subclass it, + you can also use shade instances. + If you do not pass this parameter, environment variables and CLI args + will be parsed. + + :param list managers: + A list of `flameclient.resources.ResourceManager` instances or + subclasses + We will get them automatically if not set. + If we receive subclasses, we instantiate them. + + :param argparse.Namespace options: + argparse options. It allows you to pass the ordinary command line + arguments. + :param dict options: + You can also pass an ordinary dictionary and it will + be converted to an `argparse.Namespace` instance. + Passing an ordinary dictionary is useful when importing + TemplateGenerator from other modules or projects than flame. + + args and kwargs are extra parameters which each + `flameclient.resources.ResourceManager` will have access to. + + `self.cache_dict` is a dict to store data on the template generator. + Usefull to share cache data between several objects on a single + TemplateGenerator instance. + """ + + _heat_routers = None + _heat_router_interfaces = None + _locks = None + _memoize_dict = None + _routers = None + adoption_data = None + api = base_resources.DirectManagerApiResourceAccess() + args = () + cloud = None + connection = None + hot_resources = None + managers = None + options = None + parser = None + template = None + + def __init__( + self, connection=None, options=None, managers=None, *args, **kwargs + ): + + self.args = args + self.kwargs = kwargs or {} + self._memoize_dict = {} + self._locks = collections.defaultdict(threading.Lock) + + if managers is None: + self.managers = base_resources.ManagerList( + manager_class(self) + for manager_class in base_resources.get_manager_classes() + ) + self.parser = add_arguments(managers=self.managers) + else: + self.parser = add_arguments() + self.managers = base_resources.ManagerList() + for manager in managers: + self.add_manager(manager) + self.add_manager_args(manager) + + # We want to have all option defaults without parsing CLI + # argument yet, so we parse an empty list. + # We parse CLI args only if connection is None, when we set a + # connection. + self.options = self.parser.parse_args([]) + if options: + self.update_options(options) + + self.set_connection(connection) - def __init__(self, username, password, tenant_name, auth_url, - auth_token=None, cert=None, key=None, insecure=False, - endpoint_type='publicURL', region_name=None): - self.thread_pool = concurrent.futures.ThreadPoolExecutor(10) - self.generate_data = False self._setup_templates() - self._setup_managers(username, password, tenant_name, auth_url, cert, - key, insecure, endpoint_type, region_name, - auth_token) - def _setup_templates(self): - self.template = yaml.load(template_skeleton) - self.template['resources'] = {} - self.template['parameters'] = {} + def update_options(self, options): + all_options = vars(self.options) + if isinstance(options, argparse.Namespace): + options = vars(options) + if isinstance(options, dict): + all_options.update(options) + self.options = utils.dict_to_options(all_options) + else: + raise TypeError( + "'options' has to be dict or argparse.Namespace but " + "received %s" % type(options) + ) + if self.options.debug: + LOG.setLevel(logging.DEBUG) - self.stack_data = yaml.load(stack_data_skeleton) - self.stack_data['resources'] = {} + def add_manager(self, manager): + """Add a manager class or instance. - def _setup_managers(self, username, password, tenant_name, auth_url, - insecure, endpoint_type, cert=None, key=None, - region_name=None, auth_token=None): - self.keystone = managers.KeystoneManager( - username, password, - tenant_name, - auth_url, cert, key, insecure, - endpoint_type, - region_name=region_name, - auth_token=auth_token - ) - self.keystone.authenticate() - self.neutron = managers.NeutronManager(self.keystone) - self.nova = managers.NovaManager(self.keystone) - self.cinder = managers.CinderManager(self.keystone) + :param flameclient.resources.ResourceManager manager: + a `flameclient.resources.ResourceManager` class or instance - def async_fetch_data(self, fetch_map): - """Call the methods in fetch_map in parallel and filter the results. - - fetch_map is a dict of the form : - {resource_type, (fetch_method, filter_method), ...} - - This method returns an iterator on the filtered results. - The iterator items are of the form (res_type, result) + You may want (or not) to add the manager's arguments afterwards with + the add_manager_args method. """ - - future_res = {} - with self.thread_pool as tp: - for res_type, (method, data_filter) in six.iteritems(fetch_map): - future_res[tp.submit(method)] = res_type - for res_available in concurrent.futures.as_completed(future_res): - res = res_available.result() - res_type = future_res[res_available] - yield res_type, fetch_map[res_type][1](res) - - def order_ports(self): - for i, port in self.ports.values(): - for fixed_ip in port['fixed_ips']: - ip_subnet = self.subnets[fixed_ip['subnet_id']][1] - pools = ip_subnet.get('allocation_pools') - if pools: - pools_starts = [pool['start'] for pool in pools] - if fixed_ip['ip_address'] in pools_starts: - # Its the first port of the subnet - ip_subnet['first_port'] = port - - def extract_vm_details(self, exclude_servers, exclude_volumes, - exclude_keypairs, generate_data, - extract_ports=False, exclude_secgroups=False): - self.exclude_servers = exclude_servers - self.exclude_volumes = exclude_volumes - self.exclude_keypairs = exclude_keypairs - self.generate_data = generate_data - self.extract_ports = extract_ports - self.exclude_secgroups = exclude_secgroups - self.external_networks = [] - fetch_map = { - 'subnets': (self.neutron.subnet_list, self.build_data), - 'networks': (self.neutron.network_list, self.build_data), - 'routers': (self.neutron.router_list, lambda x: x), - 'servergroups': (self.nova.servergroup_list, self.build_data), - 'floatingips': (self.neutron.floatingip_list, lambda x: x), - 'ports': (self.neutron.port_list, self.build_data), - } - - if not exclude_secgroups: - fetch_map['secgroups'] = ( - self.neutron.secgroup_list, self.build_data + if isinstance(manager, base_resources.ResourceManager): + if manager.generator is not self: + LOG.debug( + "Setting `%s.generator = %s`", manager, self + ) + manager.generator = self + self.managers.append(manager) + elif issubclass(manager, base_resources.ResourceManager): + self.managers.append(manager(self)) + else: + raise TypeError( + "managers need to be instances or subclasses of " + "%s but received %s (%s instance)." % ( + base_resources.ResourceManager, manager, type(manager) + ) ) - if not exclude_keypairs: - fetch_map['keys'] = (self.nova.keypair_list, - lambda l: {key.name: (index, key) for - index, key in enumerate(l)}) + def add_manager_args(self, manager): + """Add a manager's arguments to the argparse.ArgumentParser. - if not exclude_servers: - fetch_map['flavors'] = (self.nova.flavor_list, self.build_data) - fetch_map['servers'] = (self.nova.server_list, self.build_data) + :param flameclient.resources.ResourceManager manager: + a `flameclient.resources.ResourceManager` class or instance - if (not exclude_volumes or - (exclude_volumes and not exclude_servers)): - fetch_map['volumes'] = (self.cinder.volume_list, self.build_data) - for res_type, result in self.async_fetch_data(fetch_map): - self.__setattr__(res_type, result) - self.order_ports() + You may want (or not) to update the options (parsed args) list + afterwards with the update_options method. + """ + manager.add_arguments(self.parser) - def build_data(self, data): - if not data: - return {} + def set_connection(self, connection): + """Set the connection - if isinstance(data[0], dict): - return dict((element['id'], (index, element)) - for index, element in enumerate(data)) - else: - return dict((element.id, (index, element)) - for index, element in enumerate(data)) + :param openstack.connection.Connection connection: + An `openstack.connection.Connection` instance (`openstacksdk`). + Since `shade.openstackcloud.OpenStackCloud` (`shade`) is a subclass + you can also use shade instances. + If you do not pass this parameter, environment variables and CLI + args will be parsed to set a connection. + """ + if not connection: + known_args, unknown_args = session.get_openstack_cli_arguments( + self.parser, renamed_args=True + ) - @staticmethod - def format_template(filename): - return yaml.safe_dump(filename, default_flow_style=False) + if unknown_args: + msg = 'unrecognized arguments: %s' + self.parser.error(msg % ' '.join(unknown_args)) - def _extract_router_gateway(self, router_resource_name, router): - router_external_network_name = ("%s_external_network" % - router_resource_name) - external_network = router['external_gateway_info']['network_id'] - properties = { - 'router_id': {'get_resource': router_resource_name}, - 'network_id': {'get_param': router_external_network_name} + self.update_options(known_args) + LOG.debug("Setting Openstack connection from envvars and args") + try: + connection = session.get_shade( + parser_or_options=self.options, load_envvars=True, + load_yaml_config=True + ) + except OptionError as e: + self.parser.error(str(e)) + + if isinstance(connection, Connection): + self.connection = connection + if isinstance(connection, OpenStackCloud): + # OpenStackCloud is a subclass of Connection so when we have an + # OpenStackCloud instance, both self.connection and self.cloud will + # be available. We want this because we want to be able to call + # self.connection methods indifferently even if we have an + # OpenStackCloud instance. + self.cloud = connection + if not (self.cloud or self.connection): + raise TypeError( + "`conn` has to be either an " + "openstack.connection.Connection or " + "shade.openstackcloud.OpenStackCloud instance" + ) + + @classmethod + def get_new_template(cls): + template = yaml.load(template_skeleton) + template['resources'] = {} + template['parameters'] = {} + return template + + @classmethod + def get_new_adoption_data(cls): + adoption_data = yaml.load(adoption_data_skeleton) + adoption_data['resources'] = {} + return adoption_data + + def _setup_templates(self): + self.template = self.get_new_template() + self.adoption_data = self.get_new_adoption_data() + + @property + def conn(self): + return self.connection + + @property + def api_resource_getters(self): + return { + manager.__module__: manager.get_api_resources + for manager in self.managers } - resource = Resource("%s_gateway" % router_resource_name, - 'OS::Neutron::RouterGateway', - "%s:%s" % (router['id'], external_network), - properties) - description = "Router external network" - constraints = [{'custom_constraint': "neutron.network"}] - resource.add_parameter(router_external_network_name, description, - constraints=constraints, - default=external_network) + @property + def hot_resource_getters(self): + return { + manager.__module__: manager.get_hot_resources + for manager in self.managers + } - return resource + def prefetch_api_resources(self): + """Fetch all api resources in parallel calls""" + if self.options.prefetch and not self.options.no_threads: + futures = {} + with concurrent.futures.ThreadPoolExecutor(10) as tp: + for name, getter in six.iteritems(self.api_resource_getters): + futures[tp.submit(getter)] = name + for res in concurrent.futures.as_completed(futures): + name = futures[res] + LOG.debug("Getting api resources from %s", name) - def _extract_router_interfaces(self, router_resource_name, ports): - resources = [] - for n, port in enumerate(ports): - if port['device_owner'] != "network:router_interface": - continue + def get_hot_resources(self): + resources = self.hot_resources or [] - resource_name = "%s_interface_%d" % (router_resource_name, n) - subnet_resource_name = self.get_subnet_resource_name( - port['fixed_ips'][0]['subnet_id']) - resource_id = ("%s:subnet_id=%s" % - (port['device_id'], - port['fixed_ips'][0]['subnet_id'])) - properties = { - 'subnet_id': {'get_resource': subnet_resource_name}, - 'router_id': {'get_resource': router_resource_name} - } - resource = Resource(resource_name, 'OS::Neutron::RouterInterface', - resource_id, properties) - resources.append(resource) + self.prefetch_api_resources() + + if self.options.no_threads: + for name, getter in six.iteritems(self.hot_resource_getters): + LOG.debug("Getting resources from %s", name) + resources.extend(getter()) + else: + futures = {} + with concurrent.futures.ThreadPoolExecutor(10) as tp: + for name, getter in six.iteritems(self.hot_resource_getters): + futures[tp.submit(getter)] = name + for res in concurrent.futures.as_completed(futures): + name = futures[res] + LOG.debug("Getting resources from %s", name) + resources.extend(res.result()) + + self.hot_resources = resources return resources - def _extract_routers(self): - resources = [] - for n, router in enumerate(self.routers): - name = "router_%d" % n - properties = { - 'name': router['name'], - 'admin_state_up': router['admin_state_up'], - } - resource = Resource(name, 'OS::Neutron::Router', - router['id'], properties) - resources.append(resource) + def get_managers_by_post_priority(self): + # Copy the list of managers: + managers = [manager for manager in self.managers] + # And sort + managers.sort(key=lambda manager: manager.post_priority) + return managers - ports = self.neutron.router_interfaces_list(router) - resources += self._extract_router_interfaces(name, ports) + def call_managers_post_process(self): + for manager in self.get_managers_by_post_priority(): + manager.post_process() - if router['external_gateway_info']: - resources.append(self._extract_router_gateway(name, router)) - return resources + def call_managers_post_process_hot_resources(self): + for manager in self.get_managers_by_post_priority(): + self.hot_resources = manager.post_process_hot_resources( + self.hot_resources + ) - def _extract_networks(self): - resources = [] - for n, network in self.networks.values(): - if network['router:external']: - self.external_networks.append(network['id']) - continue + def call_managers_post_process_heat_template(self): + for manager in self.get_managers_by_post_priority(): + self.template = manager.post_process_heat_template(self.template) - properties = { - 'name': network['name'], - 'admin_state_up': network['admin_state_up'], - 'shared': network['shared'] - } - resource = Resource("network_%d" % n, 'OS::Neutron::Net', - network['id'], properties) - resources.append(resource) - return resources - - def get_network_resource_name(self, network_id): - return "network_%d" % self.networks[network_id][0] - - def get_subnet_resource_name(self, subnet_id): - return "subnet_%d" % self.subnets[subnet_id][0] - - def get_server_resource_name(self, device_id): - return "server_%d" % self.servers[device_id][0] - - def get_router_resource_name(self, device_id): - return "router_%d" % self.routers[device_id][0] - - def get_secgroup_resource_name(self, secgroup_id): - return "security_group_%d" % self.secgroups[secgroup_id][0] - - def get_ports_for_server(self, server_id): - ports = [] - for n, port in self.ports.values(): - if port['device_id'] == server_id: - ports.append("port_%d" % n) - return ports - - def _extract_subnets(self): - resources = [] - for n, subnet in self.subnets.values(): - if subnet['network_id'] in self.external_networks: - continue - - net_name = self.get_network_resource_name(subnet['network_id']) - properties = { - 'name': subnet['name'], - 'allocation_pools': subnet['allocation_pools'], - 'cidr': subnet['cidr'], - 'dns_nameservers': subnet['dns_nameservers'], - 'enable_dhcp': subnet['enable_dhcp'], - 'host_routes': subnet['host_routes'], - 'ip_version': subnet['ip_version'], - 'network_id': {'get_resource': net_name} - } - resource = Resource("subnet_%d" % n, 'OS::Neutron::Subnet', - subnet['id'], properties) - resources.append(resource) - return resources - - def _extract_ports(self): - resources = [] - resources_dict = {} - self.dhcp_fixed_ips = {} - for n, port in self.ports.values(): - fixed_ips = [] - for fixed_ip_dict in port['fixed_ips']: - subnet_id = fixed_ip_dict['subnet_id'] - subnet_name = self.get_subnet_resource_name(subnet_id) - fixed_ip_resource = {u'subnet_id': - {'get_resource': subnet_name}, - u'ip_address': fixed_ip_dict['ip_address'] - } - fixed_ips.append(fixed_ip_resource) - - if port['device_owner'] == 'network:dhcp': - # Add the fixed ip to the dhcp_fixed_ips list - dhcp_ips = self.dhcp_fixed_ips.setdefault(subnet_name, []) - dhcp_ips.append(fixed_ip_dict['ip_address']) - if not port['device_owner'].startswith('compute:'): - # It's not a server, skip it! - continue - net_name = self.get_network_resource_name(port['network_id']) - properties = { - 'network_id': {'get_resource': net_name}, - 'admin_state_up': port['admin_state_up'], - 'fixed_ips': fixed_ips, - 'mac_address': port['mac_address'], - 'device_owner': port['device_owner'], - } - if port['name'] != '': - # This port has a name - properties['name'] = port['name'] - resource = Resource("port_%d" % n, 'OS::Neutron::Port', - port['id'], properties) - if not self.exclude_secgroups: - security_groups = self.build_port_secgroups(resource, port) - properties['security_groups'] = security_groups - - resources.append(resource) - resources_dict[port['id']] = resource - - return resources - - def _build_rules(self, rules): - brules = [] - for rule in rules: - if rule['protocol'] == 'any': - del rule['protocol'] - del rule['port_range_min'] - del rule['port_range_max'] - rg_id = rule['remote_group_id'] - if rg_id is not None: - rule['remote_mode'] = "remote_group_id" - resource_name = "security_group_%d" % self.secgroups[rg_id][0] - if rg_id == rule['security_group_id']: - del rule['remote_group_id'] - else: - rule['remote_group_id'] = {'get_resource': resource_name} - del rule['tenant_id'] - del rule['id'] - del rule['security_group_id'] - rule = dict((k, v) for k, v in rule.items() if v is not None) - brules.append(rule) - return brules - - def _extract_secgroups(self): - resources = [] - for n, secgroup in self.secgroups.values(): - if secgroup['name'] == 'default' and self.generate_data: - continue - - if secgroup['name'] == "default": - secgroup['name'] = "_default" - - rules = self._build_rules(secgroup['security_group_rules']) - properties = { - 'description': secgroup['description'], - 'name': secgroup['name'], - 'rules': rules, - } - resource = Resource("security_group_%d" % n, - 'OS::Neutron::SecurityGroup', - secgroup['id'], - properties) - resources.append(resource) - return resources - - def _extract_servergroups(self): - resources = [] - for n, servergroup in self.servergroups.values(): - properties = {'name': servergroup.name, - 'policies': servergroup.policies} - resource = Resource("servergroup_%d" % n, 'OS::Nova::ServerGroup', - servergroup.id, properties) - resources.append(resource) - return resources - - def _extract_keys(self): - resources = [] - for n, key in self.keys.values(): - properties = {'name': key.name, 'public_key': key.public_key} - resource = Resource("key_%d" % n, 'OS::Nova::KeyPair', - key.id, properties) - resources.append(resource) - return resources - - def build_secgroups(self, resource, server): - security_groups = [] - server_secgroups = set(self.nova.server_security_group_list(server)) - - secgroup_default_parameter = None - for secgr in server_secgroups: - if secgr.name == 'default' and self.generate_data: - if not secgroup_default_parameter: - server_res_name = 'server_%d' % self.servers[server.id][0] - param_name = "%s_default_security_group" % server_res_name - description = ("Default security group for server %s" % - server.name) - default = secgr.id - resource.add_parameter(param_name, description, - default=default) - secgroup_default_parameter = {'get_param': param_name} - security_groups.append(secgroup_default_parameter) - else: - resource_name = ("security_group_%d" % - self.secgroups[secgr.id][0]) - security_groups.append({'get_resource': resource_name}) - - return security_groups - - def build_port_secgroups(self, resource, port): - security_groups = [] - port_secgroups = [self.secgroups[sgid][1] - for sgid in port['security_groups']] - - secgroup_default_parameter = None - for secgr in port_secgroups: - if secgr['name'] == 'default' and self.generate_data: - if not secgroup_default_parameter: - port_res_name = 'port_%d' % self.ports[port['id']][0] - param_name = "%s_default_security_group" % port_res_name - description = ("Default security group for port %s" % - port['name']) - default = secgr['id'] - resource.add_parameter(param_name, description, - default=default) - secgroup_default_parameter = {'get_param': param_name} - security_groups.append(secgroup_default_parameter) - else: - resource_name = ("security_group_%d" % - self.secgroups[secgr['id']][0]) - security_groups.append({'get_resource': resource_name}) - - return security_groups - - def build_networks(self, addresses): - networks = [] - for net_name in addresses: - ip = addresses[net_name][0]['addr'] - for s, subnet in self.subnets.values(): - if netaddr.IPAddress(ip) in netaddr.IPNetwork(subnet['cidr']): - for n, network in self.networks.values(): - if (network['name'] == net_name and - network['id'] == subnet['network_id']): - net = self.get_network_resource_name( - subnet['network_id']) - networks.append({'network': {'get_resource': net}}) - return networks - - def get_servergroup_resource_name(self, servergroup_id): - return "servergroup_%d" % self.servergroups[servergroup_id][0] - - def _extract_servers(self): - resources = [] - for n, server in self.servers.values(): - resource_name = "server_%d" % n - properties = { - 'name': server.name, - 'diskConfig': getattr(server, 'OS-DCF:diskConfig') - } - resource = Resource(resource_name, 'OS::Nova::Server', - server.id, properties) - - if server.config_drive: - properties['config_drive'] = server.config_drive - - # Flavor - flavor_parameter_name = "%s_flavor" % resource_name - description = "Flavor to use for server %s" % resource_name - default = self.flavors[server.flavor['id']][1].name - resource.add_parameter(flavor_parameter_name, description, - default=default) - properties['flavor'] = {'get_param': flavor_parameter_name} - - # Image - if server.image: - image_parameter_name = "%s_image" % resource_name - description = ( - "Image to use to boot server %s" % resource_name) - constraints = [{'custom_constraint': "glance.image"}] - resource.add_parameter(image_parameter_name, description, - default=server.image['id'], - constraints=constraints) - properties['image'] = {'get_param': image_parameter_name} - - # Keypair - if server.key_name: - if self.exclude_keypairs or server.key_name not in self.keys: - key_parameter_name = "%s_key" % resource_name - description = ("Key for server %s" % resource_name) - constraints = [{'custom_constraint': "nova.keypair"}] - resource.add_parameter(key_parameter_name, description, - default=server.key_name, - constraints=constraints) - properties['key_name'] = {'get_param': key_parameter_name} - else: - resource_key = "key_%d" % self.keys[server.key_name][0] - properties['key_name'] = {'get_resource': resource_key} - - if self.extract_ports: - ports = [{"port": {"get_resource": port}} - for port in self.get_ports_for_server(server.id)] - if ports: - properties['networks'] = ports - else: - if not self.exclude_secgroups: - security_groups = self.build_secgroups(resource, server) - if security_groups: - properties['security_groups'] = security_groups - - networks = self.build_networks(server.addresses) - if networks: - properties['networks'] = networks - - if server.metadata: - properties['metadata'] = server.metadata - - server_volumes = [] - key = 'os-extended-volumes:volumes_attached' - for server_volume in getattr(server, key): - volume = self.volumes[server_volume['id']] - volume_resource_name = "volume_%d" % volume[0] - device = volume[1].attachments[0]['device'] - if not self.exclude_volumes: - server_volumes.append( - {'volume_id': {'get_resource': volume_resource_name}, - 'device_name': device}) - else: - volume_parameter_name = ("volume_%s_%d" % - (server.name, volume[0])) - description = ("Volume for server %s, device %s" % - (server.name, device)) - server_volumes.append( - {'volume_id': {'get_param': volume_parameter_name}, - 'device_name': device}) - resource.add_parameter(volume_parameter_name, description, - default=server_volume['id']) - if server_volumes: - # block_device_mapping_v2 is the new way of associating - # block devices to an instance - properties['block_device_mapping_v2'] = server_volumes - - # server-group - for servergroup in self.servergroups.values(): - if server.id in servergroup[1].members: - hint = {'group': {'get_resource': - self.get_servergroup_resource_name - (servergroup[1].id)}} - properties['scheduler_hints'] = \ - hint - - resources.append(resource) - return resources - - def _extract_floating(self): - resources = [] - for n, ip in enumerate(self.floatingips): - ip_resource_name = "floatingip_%d" % n - net_param_name = "external_network_for_floating_ip_%d" % n - ip_properties = { - 'floating_network_id': {'get_param': net_param_name}} - resource = Resource(ip_resource_name, 'OS::Neutron::FloatingIP', - ip['id'], ip_properties) - - description = "Network to allocate floating IP from" - constraints = [{'custom_constraint': "neutron.network"}] - default = ip['floating_network_id'] - resource.add_parameter(net_param_name, description, - constraints=constraints, - default=default) - resources.append(resource) - - if self.extract_ports and ip['port_id']: - port_number = self.ports[ip['port_id']][0] - port_resource_name = "port_%d" % port_number - properties = { - 'floatingip_id': {'get_resource': ip_resource_name}, - 'port_id': {'get_resource': port_resource_name} - } - resource_id = ("%s:%s" % - (ip['id'], - ip['port_id'])) - resource = Resource("floatingip_association_%d" % n, - 'OS::Neutron::FloatingIPAssociation', - resource_id, - properties) - resources.append(resource) - else: - if not self.exclude_servers and ip['port_id']: - device = self.ports[ip['port_id']][1]['device_id'] - if device and self.servers.get(device): - server = self.servers[device] - server_resource_name = "server_%d" % server[0] - properties = { - 'floating_ip': {'get_resource': ip_resource_name}, - 'server_id': {'get_resource': server_resource_name} - } - resource = Resource("floatingip_association_%d" % n, - 'OS::Nova::FloatingIPAssociation', - None, - properties) - resources.append(resource) - return resources - - def _extract_volumes(self): - resources = [] - for n, volume in self.volumes.values(): - resource_name = "volume_%d" % n - properties = {'size': volume.size} - resource = Resource(resource_name, 'OS::Cinder::Volume', - volume.id, properties) - if volume.source_volid: - if volume.source_volid in self.volumes: - key = "volume_%d" % self.volumes[volume.source_volid][0] - properties['source_volid'] = {'get_resource': key} - else: - key = "%s_source_volid" % resource_name - description = ( - "Volume to create volume %s from" % resource_name) - resource.add_parameter(key, description) - properties['source_volid'] = {'get_param': key} - if volume.bootable == 'true' and not volume.snapshot_id: - key = "%s_image" % resource_name - description = "Image to create volume %s from" % resource_name - constraints = [{'custom_constraint': "glance.image"}] - default = volume.volume_image_metadata['image_id'] - resource.add_parameter(key, description, - constraints=constraints, - default=default) - properties['image'] = {'get_param': key} - if volume.snapshot_id: - key = "%s_snapshot_id" % resource_name - properties['snapshot_id'] = {'get_param': key} - description = ( - "Snapshot to create volume %s from" % resource_name) - resource.add_parameter(key, description, - default=volume.snapshot_id) - if hasattr(volume, 'display_name') and volume.display_name: - properties['name'] = volume.display_name - if (hasattr(volume, 'display_description') and - volume.display_description): - properties['description'] = volume.display_description - if volume.volume_type and volume.volume_type != 'None': - key = "%s_volume_type" % resource_name - description = ( - "Volume type for volume %s" % resource_name) - default = volume.volume_type - resource.add_parameter(key, description, default=default) - properties['volume_type'] = {'get_param': key} - if volume.metadata: - properties['metadata'] = volume.metadata - - resources.append(resource) - return resources + def call_managers_post_process_adoption_data(self): + for manager in self.get_managers_by_post_priority(): + self.adoption_data = manager.post_process_adoption_data( + self.adoption_data + ) def extract_data(self): - resources = self._extract_routers() - resources += self._extract_networks() - if self.extract_ports: - resources += self._extract_ports() - - resources += self._extract_subnets() - resources += self._extract_floating() - resources += self._extract_servergroups() - - if not self.exclude_secgroups: - resources += self._extract_secgroups() - - if not self.exclude_keypairs: - resources += self._extract_keys() - if not self.exclude_servers: - resources += self._extract_servers() - if not self.exclude_volumes: - resources += self._extract_volumes() - - for resource in resources: + for resource in self.get_hot_resources(): self.template['resources'].update(resource.template_resource) self.template['parameters'].update(resource.template_parameter) - if self.generate_data: - self.stack_data['resources'].update(resource.stack_resource) + if self.options.generate_adoption_data: + self.adoption_data['resources'].update(resource.stack_resource) + + self.call_managers_post_process() + self.call_managers_post_process_hot_resources() + self.call_managers_post_process_heat_template() + self.call_managers_post_process_adoption_data() + + @staticmethod + def format_template(data): + return yaml.safe_dump(data, default_flow_style=False) def heat_template_and_data(self): - if self.generate_data: - out = self.stack_data.copy() + if self.options.generate_adoption_data: + out = self.adoption_data.copy() out['template'] = self.template out['environment'] = {"parameter_defaults": {}, "parameters": {}} @@ -760,3 +402,41 @@ class TemplateGenerator(object): return self.format_template(out) else: return self.format_template(self.template) + + def output_template_and_data(self): + output_data = self.heat_template_and_data() + if self.options.file: + with open(self.options.file, 'w') as fp: + fp.write(output_data) + else: + print(output_data) + + def generator_memoize(self, method, *args, **kwargs): + """Memoize method calls on this instance + + Utility to memoize API calls on a TemplateGenerator instance + """ + try: + key = utils.hash_func_call(method, *args, **kwargs) + with self._locks[key]: + if key not in self._memoize_dict: + result = method(*args, **kwargs) + if isinstance(result, collections.Iterator): + # We can not memoize an iterator, so we store it in a + # tuple. The choice for tuples is because tuples take + # less memory than lists: + # https://stackoverflow.com/questions/46664007/why-do-tuples-take-less-space-in-memory-than-lists # noqa + result = tuple(result) + self._memoize_dict[key] = result + return self._memoize_dict[key] + except TypeError: + LOG.error( + "Could not hash %s call with args %s and kwargs %s", + method.__name__, args, kwargs, exc_info=True + ) + return method(*args, **kwargs) + + def get_resource_name(self, manager_name, resource_id): + for manager in self.managers: + if manager.name == manager_name: + return manager.get_resource_name(resource_id) diff --git a/flameclient/logs.py b/flameclient/logs.py new file mode 100644 index 0000000..847b414 --- /dev/null +++ b/flameclient/logs.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import logging +from logging.config import dictConfig +import sys + + +COLOR_LOGS = sys.stdout.isatty() + + +class CWColorFormatter(logging.Formatter): + """Special Formatter adding color to logs. + + color is not added if settings.DEBUG is True to prevent syslog cluttering + when in production. + + This Formatter is just Candy for developers. + """ + LEVEL_COLORS = { + logging.NOTSET: '\033[01;0m', # Reset color + logging.DEBUG: '\033[00;32m', # GREEN + logging.INFO: '\033[00;36m', # CYAN + # Where did this one go?: + # logging.AUDIT: '\033[01;36m', # BOLD CYAN + logging.WARN: '\033[01;33m', # BOLD YELLOW + logging.ERROR: '\033[01;31m', # BOLD RED + logging.CRITICAL: '\033[01;31m', # BOLD RED + } + + reset_color = '\033[01;0m' + + def format(self, record): + if COLOR_LOGS: + record.reset_color = self.reset_color + record.color = self.LEVEL_COLORS[record.levelno] + else: + # We do not want colors in production because syslog does not + # handle them. + record.reset_color = '' + record.color = '' + return super(CWColorFormatter, self).format(record) + + +LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + '()': CWColorFormatter, + 'format': '%(color)s%(levelname)s: %(pathname)s %(funcName)s %(lineno)d:%(reset_color)s %(message)s' # noqa + }, + }, + 'handlers': { + 'default': { + 'level': 'DEBUG', + 'formatter': 'standard', + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + '': { + 'handlers': ['default'], + 'level': 'INFO', + 'propagate': True + }, + 'flameclient': { + 'handlers': ['default'], + # This can be overriden in the config's logging section + 'level': 'INFO', + 'propagate': False + }, + } +} + + +dictConfig(LOGGING_CONFIG) diff --git a/flameclient/managers.py b/flameclient/managers.py deleted file mode 100644 index c17a479..0000000 --- a/flameclient/managers.py +++ /dev/null @@ -1,228 +0,0 @@ -# -*- coding: utf-8 -*- - -# This software is released under the MIT License. -# -# Copyright (c) 2014 Cloudwatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from cinderclient.v1 import client as cinder_client -from keystoneclient.v2_0 import client as keystone_client -from neutronclient.v2_0 import client as neutron_client -from novaclient import client as nova_client - - -class KeystoneManager(object): - """Manages Keystone queries.""" - _client = None - - def __init__(self, username, password, project, auth_url, insecure, - endpoint_type='publicURL', cert=None, key=None, - region_name=None, auth_token=None): - self.username = username - self.password = password - self.project = project - self.auth_url = auth_url - self.cert = cert - self.key = key - self.insecure = insecure - self.region_name = region_name - self.endpoint_type = endpoint_type - self.auth_token = auth_token - - def authenticate(self): - self.client().authenticate() - self.auth_token = self.client().auth_token - - def client(self): - if not self._client: - self._client = keystone_client.Client( - username=self.username, - password=self.password, - tenant_name=self.project, - auth_url=self.auth_url, - cert=self.cert, - key=self.key, - region_name=self.region_name, - insecure=self.insecure, - endpoint_type=self.endpoint_type, - token=self.auth_token) - return self._client - - def set_client(self, client): - self._client = client - - def get_token(self): - return self.client().auth_token - - def get_endpoint(self, service_type, endpoint_type="publicURL"): - catalog = self.client().service_catalog.get_endpoints() - return catalog[service_type][0][endpoint_type] - - def get_project_id(self): - return self.client().project_id - - -class NeutronManager(object): - _client = None - _project_id = None - - def __init__(self, keystone_mgr): - self.keystone_mgr = keystone_mgr - - def client(self): - if not self._client: - # Create the client - self._client = neutron_client.Client( - auth_url=self.keystone_mgr.auth_url, - insecure=self.keystone_mgr.insecure, - endpoint_url=self.keystone_mgr.get_endpoint('network'), - token=self.keystone_mgr.auth_token) - if not self._project_id: - self._project_id = self.keystone_mgr.get_project_id() - return self._client - - def set_client(self, client): - self._client = client - - def set_project_id(self, project_id): - self._project_id = project_id - - def router_list(self): - return filter(self._owned_resource, - self.client().list_routers()['routers']) - - def router_interfaces_list(self, router): - return self._client.list_ports(device_id=router['id'])['ports'] - - def port_list(self): - return self.client().list_ports()['ports'] - - def network_list(self): - return filter(self._owned_resource, - self.client().list_networks()['networks']) - - def secgroup_list(self): - return filter(self._owned_resource, - self.client().list_security_groups()['security_groups']) - - def floatingip_list(self): - return filter(self._owned_resource, - self.client().list_floatingips()['floatingips']) - - def subnet_list(self): - return filter(self._owned_resource, - self.client().list_subnets()['subnets']) - - def _owned_resource(self, res): - # Only considering resources owned by project - return res['tenant_id'] == self._project_id - - -class NovaManager(object): - """Manage nova resources.""" - _client = None - - def __init__(self, keystone_mgr): - self.keystone_mgr = keystone_mgr - - def client(self): - if not self._client: - self._client = nova_client.Client( - '2', - self.keystone_mgr.username, - self.keystone_mgr.auth_token, - self.keystone_mgr.project, - self.keystone_mgr.auth_url, - region_name=self.keystone_mgr.region_name, - insecure=self.keystone_mgr.insecure, - endpoint_type=self.keystone_mgr.endpoint_type, - auth_token=self.keystone_mgr.auth_token - ) - return self._client - - def set_client(self, client): - self._client = client - - def server_list(self): - return self.client().servers.list() - - def floating_ip_list(self): - return self.client().floating_ips.list() - - def flavor_list(self): - return self.client().flavors.list() - - def flavor_get(self, id): - return self.client().flavors.get(id) - - def keypair_list(self): - return self.client().keypairs.list() - - def keypair_show(self, keypair): - return self.client().keypairs.get(keypair) - - def server_security_group_list(self, server): - return self.client().servers.list_security_group(server) - - def servergroup_list(self): - return self.client().server_groups.list(True) - - -class CinderManager(object): - """Manage Cinder resources.""" - _client = None - - def __init__(self, keystone_mgr): - self.keystone_mgr = keystone_mgr - self.defined = True - - def client(self): - if self.defined and not self._client: - try: - cinder_url = self.keystone_mgr.get_endpoint("volumev2") - except KeyError: - cinder_url = self.keystone_mgr.get_endpoint("volume") - client = cinder_client.Client( - self.keystone_mgr.username, - self.keystone_mgr.auth_token, - project_id=self.keystone_mgr.project, - auth_url=cinder_url, - http_log_debug=True, - insecure=self.keystone_mgr.insecure - ) - client.client.auth_token = self.keystone_mgr.auth_token - client.client.management_url = cinder_url - self._client = client - return self._client - - def set_client(self, client): - self._client = client - - def volume_list(self): - volumes = [] - client = self.client() - if client: - for vol in client.volumes.list(): - volumes.append(client.volumes.get(vol.id)) - return volumes - - def snapshot_list(self): - client = self.client() - return client.volume_snapshots.list() if client else [] diff --git a/flameclient/resources/__init__.py b/flameclient/resources/__init__.py new file mode 100644 index 0000000..8993008 --- /dev/null +++ b/flameclient/resources/__init__.py @@ -0,0 +1,574 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import abc +import logging + +import six +from six.moves import UserDict + +from flameclient.utils import camel_to_snake +from flameclient.utils import ClassProperty +from flameclient.utils import clean_dict +from flameclient.utils import load_resource_entry_points +from flameclient.utils import load_resource_modules +from flameclient.utils import munchify + + +LOG = logging.getLogger(__name__) + + +class BaseHotResource(object): + """Describes a heat resource from parameters + + :param ResourceManager manager: + A ResourceManager instance. + This instance is necessary to have access to the manager's + openstack.connection.Connection instance + + :param string type: The name of the openstack resource type. + e.g.: "OS::Nova::Server" etc... + + :param string id: The openstack resource id. + + :param dict properties: The openstack resource properties. + + """ + + id = None + manager = None + parameters = None + properties = None + type = None + + def __init__(self, manager, name, type=None, id=None, properties=None): # noqa pylint: disable=W0622 + if not isinstance(manager, ResourceManager): + raise TypeError( + "manager needs to be a flameclient.resources.ResourceManager " + "instance. Received '%s' (%s) instead" % ( + manager, self.__type(manager) + ) + ) + self.manager = manager + self.name = name + if type: + self.type = type + self.id = id + self.status = 'COMPLETE' + self.properties = properties or {} + self.parameters = {} + + @staticmethod + def __type(*args, **kwargs): + """Needed to call the builtin type function when overriden""" + return type(*args, **kwargs) + + @property + def connection(self): + return self.manager.connection + + @property + def conn(self): + return self.connection + + @property + def cloud(self): + return self.manager.cloud + + @property + def generator(self): + return self.manager.generator + + @property + def managers(self): + return self.generator.managers + + @property + def api(self): + return self.generator.api + + @property + def options(self): + return self.generator.options + + @property + def kwargs(self): + return self.manager.kwargs + + def add_parameter( + self, name, description, parameter_type='string', constraints=None, + default=None + ): + data = { + 'type': parameter_type, + 'description': description, + } + + if constraints and self.options.include_constraints: + data['constraints'] = constraints + if default: + data['default'] = default + + self.parameters[name] = data + + @property + def clean_properties(self): + return clean_dict(self.properties) + + @property + def template_resource(self): + return { + self.name: { + 'type': self.type, + 'properties': self.clean_properties + } + } + + @property + def clean_parameters(self): + return clean_dict(self.parameters) + + @property + def template_parameter(self): + return self.parameters + + @property + def stack_resource(self): + if self.id is None: + return {} + return { + self.name: { + 'status': self.status, + 'name': self.name, + 'resource_data': {}, + 'resource_id': self.id, + 'action': 'CREATE', + 'type': self.type, + 'metadata': {} + } + } + + def generator_memoize(self, method, *args, **kwargs): + return self.manager.generator_memoize(method, *args, **kwargs) + + +@six.add_metaclass(abc.ABCMeta) +class TypedHotResource(BaseHotResource): + """Describes a heat resource from parameters with predefined type + + :param ResourceManager manager: + A ResourceManager instance. + This instance is necessary to have access to the manager's + openstack.connection.Connection instance + + :param string id: The openstack resource id. + + :param dict properties: The openstack resource properties. + + """ + + def __init__(self, manager, name, id=None, properties=None): # noqa pylint: disable=W0622 + super(TypedHotResource, self).__init__( + manager, name, type=None, id=id, properties=properties + ) + + @abc.abstractproperty + def type(self): # pylint: disable=E0202 + """The resource type + + You need to set this property on subclasses. + """ + raise NotImplementedError + + +@six.add_metaclass(abc.ABCMeta) +class AdvancedHotResource(TypedHotResource, UserDict): + """Describes a heat resource from openstack.resource.Resource instance + + These advanced resources mays also have resources. + + property_keys are properties to automatically extract from the resource. + + :param ResourceManager manager: + A ResourceManager instance. + This instance is necessary to have access to the manager's + openstack.connection.Connection instance + + :param string name: The name of the openstack resource. + e.g.: "My_server". + + :param openstack.resource.Resource data: + An `openstack.resource.Resource` instance or a munch.Munch instance + representing the openstack resource returned by an openstack client or + returned by an `openstack.connection.Connection` instance's API call + method, or returned by a `shade.openstackcloud.OpenStackCloud` + instance's API call method. + + :param dict properties: The openstack resource properties. + + When subclassing this class, the `property_keys` tuple represents + property keys which will be automatically extracted from the resource, + allowing you to not pass any properties. + + Since we inheritate from UserDict, we have a direct access to `self.data`. + More info here: + https://docs.python.org/3/library/collections.html#userdict-objects + The only difference is that we use a munch instead of dict for `self.data` + This means we can access `self.data[key]` with `self[key]` (UserDict) and + via `self.data.key` (munch). + """ + + property_keys = () + data = None + + def __init__(self, manager, name, data, properties=None): + + data = munchify(data) + + final_properties = {} + # Automatically get properties from self.property_keys + for key in self.property_keys: + final_properties[key] = data[key] + # Override properties: + if properties: + final_properties.update(properties) + + super(AdvancedHotResource, self).__init__( + manager, name, id=data.get('id'), + properties=final_properties + ) + self.data = data + + +@six.add_metaclass(abc.ABCMeta) +class ResourceManager(object): + """Resource manager to list specific openstack resources + + :param flameclient.flame.TemplateGenerator generator: + A `flameclient.flame.TemplateGenerator` instance. + + :param argparse.Namespace options: + argparse options. + + args and kwargs are extra parameters which each resource can access in case + of developer needs. + + This class needs to be subclassed. Every subclass which is imported will be + discovered by `resources.get_manager_classes()` + """ + + generator = None + args = () + kwargs = None + + def __init__(self, generator, *args, **kwargs): + self.generator = generator + self.args = args + self.kwargs = kwargs or {} + + @ClassProperty + def name(cls): + """The resource manager name + + You need to set this property on subclasses if the automatic naming + does not work. + + ex.: If this class is calles FooBarsManager, this property will return + 'foo_bars' + """ + return camel_to_snake(cls.__name__).rstrip('_manager') + + @ClassProperty + def singular_name(cls): + """The resource manager singular name + + You need to set this property on subclasses if the automatic naming + does not work. + + Override this if the singular of self.name does not consist in + in removing the trailing 's'. + """ + return cls.name.rstrip('s') + + @property + def options(self): + return self.generator.options + + @property + def connection(self): + return self.generator.connection + + @property + def conn(self): + """Give a short name access""" + return self.connection + + @property + def cloud(self): + return self.generator.cloud + + @property + def managers(self): + return self.generator.managers + + @classmethod + def add_arguments(cls, parser): + """Add parser argparse.ArgumentParser arguments + + Use this method in your subclasses if you want to add any specific + argparse arguments. These arguments will then be available on + `self.options`. + + :param argparse.ArgumentParser parser: An argparse.ArgumentParser + instance + + :returns: An argparse.ArgumentParser parser instance + """ + return parser + + @abc.abstractproperty + def api_resources(self): + """Implement this property to return api resources + + This property will be automatically set as an attribute on + `generator.api` and `self.api` with `self.name`. + + e.g.: if this class has `self.name` set to 'routers', the + `self.generator.api.routers` attribute and `self.api.routers` + attribute will return the results of this property. + + Consider using the `utils.memoized_property` decorator on the + property. This is because this property should be computed only once + It's the reason why it's a property and not a method. + Also consider using self.generator_memoize on API calls. + + This property should return a Munch instance generated by + `utils.data_list_to_dict` on a list of `openstack.resource.Resource` + instances in order to be consistent with all managers + the result has to be in the following form: + + Munch( + { + 'some_id': Munch({}), + 'other_id': Munch{}, + } + ) + + The `get_resource_name` expects to find a `.enum` attribute on + each munch value. You will need to override `get_resource_name` if + you have a different data format. + """ + raise NotImplementedError + + def get_api_resources(self): + return self.api_resources + + @property + def api(self): + """Direct access to all managers' api resources + + e.g.: if there is a FooManager (with FooManager.name = 'foo'), + `self.api.foo` will give access to `FooManager.api_resources` + if the current instance and a FooManager instance are in a ManagerList + on a TemplateGenerator. + + This allows access to each manager's api resources from any other + manager. + + For this to work it implies that TemplateGenerator.api is a + DirectManagerApiResourceAccess instance. + """ + return self.generator.api + + @abc.abstractmethod + def get_hot_resources(self): + """Implement this method to return resources + + You need to take into account `self.options.no_threads` and not use + threads (e.g.: `concurrent.futures`, threading module, etc...) when it + is True. + """ + raise NotImplementedError + + def get_resource_num(self, resource_id): + return self.api_resources[resource_id].enum + + def get_resource_name(self, resource_id): + return '%s_%d' % ( + self.singular_name, self.get_resource_num(resource_id) + ) + + def generator_memoize(self, method, *args, **kwargs): + return self.generator.generator_memoize(method, *args, **kwargs) + + @ClassProperty + def post_priority(cls): + return 100 + + def post_process(self): + """This method is called after get_hot_resources of all managers + + Use this method to perform actions after processing. + + This method will be called on all managers sorted by the order of + their self.post_priority number. + """ + pass + + def post_process_hot_resources(self, resources): # pylint: disable=R0201 + """This method is called after get_hot_resources of all managers + + Use this method to perform post processing resources modifications. + + :param list resources: the self.generator.resources attribute after + all managers' `get_hot_resources` methods have + been called. + :returns: A modified resources list (or a new resources list). + A resources list is a list containing resources returned by + each manager's `get_hot_resources` method. + + This method will be called on all managers sorted by the order of + their self.post_priority number. + This allows you to modify the list aof resources returned by managers. + """ + return resources + + def post_process_heat_template(self, template): # pylint: disable=R0201 + """This method is called after get_hot_resources of all managers + + Use this method to perform post processing template modifications. + + :param dict template: the self.generator.template attribute after + all managers' `get_hot_resources` methods have + been called. + :returns: The template dictionary which the generator will use to + render the flame template. If ou need to create a blank + template use `self.generator.get_new_template()` to + initialise it. + + + You need to return a heat template (in python dictionary format) + This method will be called on all managers sorted by the order of + their self.post_priority number. + """ + return template + + def post_process_adoption_data(self, adoption_data): # noqa pylint: disable=R0201 + """This method is called after get_hot_resources of all managers + + Use this method to perform post processing adoption_data modifications. + + :param dict adoption_data: the self.generator.adoption_data attribute + after all managers' `get_hot_resources` + methods have been called. + :returns: The adoption_data dictionary which the generator will use to + render the flame adoption data. If ou need to create a blank + template use `self.generator.get_new_adoption_data()` to + initialise it. + + This method will be called on all managers sorted by the order of + their self.post_priority number. + """ + return adoption_data + + +class ManagerList(list): + + def __getattr__(self, name): + for manager in self: + if manager.name == name: + return manager + raise AttributeError( + "'%s' has no '%s' manager" % ( + self.__name__, name + ) + ) + + def __setattr__(self, name, value): + if isinstance(value, ResourceManager): + if name != value.name: + raise ValueError( + "'%s'.name != '%s'" % (value, name) + ) + self.append(value) + raise TypeError( + "'%s' '%s' has to be a ResourceManager instance instead of %s" % ( + value, name, type(value) + ) + ) + + def __delattr__(self, name): + for num, manager in enumerate(self): + if manager.name == name: + self.pop(num) + raise AttributeError( + "'%s' has no '%s' manager" % ( + self.__name__, name + ) + ) + + +class DirectManagerApiResourceAccess(object): + """Give direct access to a manager's api_resources attribute + + + This needs to be set as the 'api' attribute on the TemplateGenerator + class. + """ + + instance = None + owner = None + + def __get__(self, instance, owner): + if instance: + self.instance = instance + self.owner = owner + return self + + def __getattr__(self, name): + if self.instance: + for manager in self.instance.managers: + if manager.name == name: + return manager.api_resources + raise AttributeError( + "'%s' has no '%s' manager in self.managers" % ( + self.owner.__name__, name + ) + ) + + +def get_manager_classes(): + """List all subclasses of ResourceManager + + if any loaded python module contains a ResourceManager subclass, it will be + discovered. + Make sure to load any module containing the subclass if you want it to be + discoverable. + You can use `utils.load_resource_modules(dirname) or + `utils.load_resource_entry_points(name='openstack_flame')` to load modules + containing ResourceManager subclasses before using this method. + """ + load_resource_modules(__file__) + load_resource_entry_points() + return ResourceManager.__subclasses__() diff --git a/flameclient/resources/flavors.py b/flameclient/resources/flavors.py new file mode 100644 index 0000000..730a6e8 --- /dev/null +++ b/flameclient/resources/flavors.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class FlavorsManager(base_resources.ResourceManager): + """Used only to provide api resources. Heat does not create flavors.""" + + @staticmethod + def add_resource_flavor(resource): + data = resource.data + manager = resource.manager + flavor_parameter_name = "%s_flavor" % resource.name + description = "Flavor to use for %s %s" % ( + manager.singular_name, resource.name + ) + default = data.flavor['id'] + resource.add_parameter( + flavor_parameter_name, description, default=default + ) + resource.properties['flavor'] = {'get_param': flavor_parameter_name} + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.compute.flavors) + ) + + def get_hot_resources(self): + return [] diff --git a/flameclient/resources/floatingips.py b/flameclient/resources/floatingips.py new file mode 100644 index 0000000..c39f865 --- /dev/null +++ b/flameclient/resources/floatingips.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class FloatingIP(base_resources.AdvancedHotResource): + type = 'OS::Neutron::FloatingIP' + + def __init__(self, manager, name, floating_ip, properties=None): + super(FloatingIP, self).__init__( + manager, name, floating_ip, properties + ) + net_param_name = "external_network_for_%s" % self.name + self.properties['floating_network_id'] = {'get_param': net_param_name} + description = "Network to allocate floating IP from" + constraints = [{'custom_constraint': "neutron.network"}] + default = self.data['floating_network_id'] + self.add_parameter(net_param_name, description, + constraints=constraints, + default=default) + + @property + def association(self): + return self.managers.ports.get_fip_association(self) \ + or self.managers.servers.get_fip_association(self) + + +class FloatingIpsManager(base_resources.ResourceManager): + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.network.ips) + ) + + def get_hot_resources(self): + floating_ips = [ + FloatingIP(self, self.get_resource_name(fip_id), floating_ip) + for fip_id, floating_ip in six.iteritems(self.api.floating_ips) + ] + floating_ip_associations = [ + fip.association for fip in floating_ips + if fip.association + ] + return floating_ips + floating_ip_associations diff --git a/flameclient/resources/images.py b/flameclient/resources/images.py new file mode 100644 index 0000000..61c54bc --- /dev/null +++ b/flameclient/resources/images.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class ImagesManager(base_resources.ResourceManager): + """Used only to provide api resources. Heat does not create images.""" + + @staticmethod + def add_resource_image(resource): + data = resource.data + manager = resource.manager + if data.image: + image_parameter_name = "%s_image" % resource.name + description = ( + "Image to use for %s %s" % ( + manager.singular_name, resource.name + ) + ) + constraints = [{'custom_constraint': "glance.image"}] + resource.add_parameter(image_parameter_name, description, + default=data.image['id'], + constraints=constraints) + resource.properties['image'] = {'get_param': image_parameter_name} + + @memoized_property + def api_resources(self): + return data_list_to_dict( + # self.conn.compute.images gives detailed images + self.generator_memoize(self.conn.image.images) + ) + + def get_hot_resources(self): + return [] diff --git a/flameclient/resources/key_pairs.py b/flameclient/resources/key_pairs.py new file mode 100644 index 0000000..f58613d --- /dev/null +++ b/flameclient/resources/key_pairs.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class KeyPair(base_resources.AdvancedHotResource): + type = 'OS::Nova::KeyPair' + property_keys = ('name', 'public_key',) + + +class KeypairsManager(base_resources.ResourceManager): + + def add_resource_keypair(self, resource): + data = resource.data + manager = resource.manager + + if data.key_name: + if ( + self.options.exclude_keypairs or + data.key_name not in self.api.keypairs + ): + key_parameter_name = "%s_key" % resource.name + description = ( + "Key for %s %s" % (manager.singular_name, resource.name) + + ) + constraints = [{'custom_constraint': "nova.keypair"}] + resource.add_parameter(key_parameter_name, description, + default=data.key_name, + constraints=constraints) + resource.properties['key_name'] = { + 'get_param': key_parameter_name + } + else: + resource.properties['key_name'] = { + 'get_resource': self.get_resource_name(data.key_name) + } + + @classmethod + def add_arguments(cls, parser): + parser.add_argument('--exclude-keypairs', action='store_true', + default=False, + help="Do not export key pair resources." + ) + return parser + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.compute.keypairs) + ) + + def get_hot_resources(self): + if not self.options.exclude_keypairs: + return [ + KeyPair(self, self.get_resource_name(resource.id), resource) + for resource in six.itervalues(self.api.keypairs) + ] + return [] diff --git a/flameclient/resources/networks.py b/flameclient/resources/networks.py new file mode 100644 index 0000000..7b5914f --- /dev/null +++ b/flameclient/resources/networks.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +import netaddr + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class Network(base_resources.AdvancedHotResource): + type = 'OS::Neutron::Net' + property_keys = ('name', 'admin_state_up', 'shared') + + +class NetworksManager(base_resources.ResourceManager): + + def add_resource_networks(self, resource): + addresses = resource.data.addresses + networks = [] + for net_name in addresses: + ip = addresses[net_name][0]['addr'] + for subnet in six.itervalues(self.api.subnets): + if netaddr.IPAddress(ip) in netaddr.IPNetwork(subnet['cidr']): + for network in six.itervalues(self.api.networks): + if (network['name'] == net_name and + network['id'] == subnet['network_id']): + net = self.get_resource_name(subnet['network_id']) + networks.append({'network': {'get_resource': net}}) + if networks: + resource.properties['networks'] = networks + + @memoized_property + def all_networks(self): + return data_list_to_dict( + self.generator_memoize(self.conn.network.networks) + ) + + @property + def api_resources(self): + return self.all_networks + + @memoized_property + def external_networks(self): + return data_list_to_dict( + ( + network for network in six.itervalues(self.all_networks) + if network['router:external'] + ), + enum=False # Do not overwrite enumeration done in all_networks + ) + + @memoized_property + def internal_networks(self): + return data_list_to_dict( + ( + network for network in six.itervalues(self.all_networks) + if network.id not in self.external_networks + ), + enum=False # Do not overwrite enumeration done in all_networks + ) + + def get_hot_resources(self): + return [ + Network(self, self.get_resource_name(network.id), network) + for network in six.itervalues(self.internal_networks) + ] diff --git a/flameclient/resources/ports.py b/flameclient/resources/ports.py new file mode 100644 index 0000000..423d326 --- /dev/null +++ b/flameclient/resources/ports.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class Port(base_resources.AdvancedHotResource): + type = 'OS::Neutron::Port' + property_keys = ('admin_state_up', 'mac_address', 'device_owner') + + def __init__(self, manager, name, port, properties=None): + super(Port, self).__init__(manager, name, port, properties) + + fixed_ips = [] + + for fixed_ip_dict in port['fixed_ips']: + subnet_id = fixed_ip_dict['subnet_id'] + subnet_resource_name = self.managers.subnets.get_resource_name( + subnet_id + ) + fixed_ip_resource = { + u'subnet_id': {'get_resource': subnet_resource_name}, + u'ip_address': fixed_ip_dict['ip_address'] + } + fixed_ips.append(fixed_ip_resource) + + net_resource_name = self.managers.networks.get_resource_name( + port['network_id'] + ) + self.properties.update( + {'network_id': {'get_resource': net_resource_name}, + 'fixed_ips': fixed_ips} + ) + if port['name'] != '': + # This port has a name + self.properties['name'] = port['name'] + + self.managers.security_groups.add_resource_secgrp_props_and_params( + self + ) + + +class NeutronFloatingIpAssociation(base_resources.TypedHotResource): + type = 'OS::Neutron::FloatingIPAssociation' + + +class PortsManager(base_resources.ResourceManager): + + def get_fip_association(self, resource): + ip = resource.data + manager = resource.manager + if ip['port_id'] and self.options.extract_ports: + port_resource_name = self.get_resource_name(ip['port_id']) + properties = { + 'floatingip_id': {'get_resource': resource.name}, + 'port_id': {'get_resource': port_resource_name} + } + resource_num = manager.get_resource_num(resource.id) + fip_assoc_id = ("%s:%s" % (ip['id'], ip['port_id'])) + return NeutronFloatingIpAssociation( + manager, + "floatingip_association_%d" % resource_num, + fip_assoc_id, + properties + ) + + def get_ports_for_resource(self, resource): + ports = [] + for port in six.itervalues(self.api.ports): + if port['device_id'] == resource.data.id: + ports.append(self.get_resource_name(port.id)) + return ports + + def add_resource_ports_or_secgroups_and_networks(self, resource): + if self.options.extract_ports: + ports = [{"port": {"get_resource": port}} + for port in self.get_ports_for_resource(resource)] + if ports: + resource.properties['networks'] = ports + else: + self.managers.security_groups \ + .add_resource_secgrp_props_and_params(resource) + + self.managers.networks.add_resource_networks(resource) + + @classmethod + def add_arguments(cls, parser): + parser.add_argument('--extract-ports', action='store_true', + default=False, + help="Export the tenant network ports.") + return parser + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.network.ports) + ) + + @memoized_property + def port_resources(self): + return [ + Port(self, self.get_resource_name(port.id), port) + for port in six.itervalues(self.api.ports) + ] + + def get_hot_resources(self): + if self.options.extract_ports: + return [ + port for port in self.port_resources + if port['device_owner'].startswith('compute:') + ] + return [] diff --git a/flameclient/resources/routers.py b/flameclient/resources/routers.py new file mode 100644 index 0000000..881ee45 --- /dev/null +++ b/flameclient/resources/routers.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property +from flameclient.utils import munchify + + +class RouterInterface(base_resources.TypedHotResource): + type = 'OS::Neutron::RouterInterface' + + +class RouterGateway(base_resources.TypedHotResource): + type = 'OS::Neutron::RouterGateway' + + +class Router(base_resources.AdvancedHotResource): + type = 'OS::Neutron::Router' + property_keys = ('name', 'admin_state_up') + + @memoized_property + def os_ports(self): + return munchify(self.generator_memoize( + self.conn.network.ports, device_id=self['id'] + )) + + @memoized_property + def router_interfaces(self): + router_interfaces = [] + for n, port in enumerate(self.os_ports): + if port['device_owner'] != "network:router_interface": + continue + resource_name = "%s_interface_%d" % (self.name, n) + subnet_resource_name = self.managers.subnets.get_resource_name( + port['fixed_ips'][0]['subnet_id'] + ) + resource_id = ("%s:subnet_id=%s" % + (port['device_id'], + port['fixed_ips'][0]['subnet_id'])) + properties = { + 'subnet_id': {'get_resource': subnet_resource_name}, + 'router_id': {'get_resource': self.name} + } + router_interfaces.append( + RouterInterface( + self.manager, resource_name, resource_id, properties) + ) + return router_interfaces + + @memoized_property + def router_gateway(self): + if self['external_gateway_info']: + router_external_network_name = ("%s_external_network" % self.name) + external_network = self['external_gateway_info']['network_id'] + properties = { + 'router_id': {'get_resource': self.name}, + 'network_id': {'get_param': router_external_network_name} + } + gateway = RouterGateway(self.manager, "%s_gateway" % self.name, + "%s:%s" % (self['id'], external_network), + properties) + description = "Router external network" + constraints = [{'custom_constraint': "neutron.network"}] + gateway.add_parameter(router_external_network_name, description, + constraints=constraints, + default=external_network) + return gateway + + +class RoutersManager(base_resources.ResourceManager): + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.network.routers) + ) + + routers = api_resources + + def get_hot_resources(self): + resources = [] + for rid, router in six.iteritems(self.api.routers): + resource = Router(self, self.get_resource_name(rid), router) + resources.append(resource) + resources.extend(resource.router_interfaces) + if resource.router_gateway: + resources.append(resource.router_gateway) + return resources diff --git a/flameclient/resources/security_groups.py b/flameclient/resources/security_groups.py new file mode 100644 index 0000000..d4f9773 --- /dev/null +++ b/flameclient/resources/security_groups.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from copy import deepcopy +import six + +from flameclient import collections_abc +from flameclient import resources as base_resources +from flameclient.utils import clean_dict +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class SecurityGroup(base_resources.AdvancedHotResource): + type = 'OS::Neutron::SecurityGroup' + property_keys = ('description',) + + def __init__(self, manager, name, data, properties=None): + super(SecurityGroup, self).__init__( + manager, name, data, properties + ) + if data['name'] == 'default': + self.properties['name'] = '_default' + else: + self.properties['name'] = data['name'] + + self.properties['rules'] = self._build_rules( + data['security_group_rules'] + ) + + def _build_rules(self, original_rules): + final_rules = [] + for rule in original_rules: + new_rule = deepcopy(rule) + if new_rule['protocol'] == 'any': + del new_rule['protocol'] + del new_rule['port_range_min'] + del new_rule['port_range_max'] + rg_id = new_rule['remote_group_id'] + if rg_id is not None: + new_rule['remote_mode'] = "remote_group_id" + resource_name = self.manager.get_resource_name(rg_id) + if rg_id == new_rule['security_group_id']: + del new_rule['remote_group_id'] + else: + new_rule['remote_group_id'] = { + 'get_resource': resource_name + } + del new_rule['tenant_id'] + del new_rule['id'] + del new_rule['security_group_id'] + final_rule = clean_dict(new_rule, clean_list=False) + final_rules.append(final_rule) + return final_rules + + +class SecurityGroupsManager(base_resources.ResourceManager): + + @classmethod + def add_arguments(cls, parser): + parser.add_argument('--exclude-secgroups', action='store_true', + default=False, + help="Do not export the security group resources.") + return parser + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.network.security_groups) + ) + + def get_security_group(self, name_or_id): + if isinstance(name_or_id, six.string_types): + try: + return self.api_resources[name_or_id] + except KeyError: + for sg in six.itervalues(self.api_resources): + if name_or_id == sg.name: + return sg + raise ValueError( + "No security group with '%s' id or name found" + ) + elif isinstance(name_or_id, collections_abc.Mapping): + if 'id' in name_or_id: + return self.get_security_group(name_or_id['id']) + elif 'name' in name_or_id: + return self.get_security_group(name_or_id['name']) + else: + raise KeyError( + "%s has no 'id' or 'name' key" % name_or_id + ) + else: + raise ValueError( + "%s has to be a string or dict with 'id' or 'name' key." + ) + + def get_resource_secgroups(self, resource): + return [ + self.get_security_group(sg) + for sg in resource.data.security_groups + ] + + def add_resource_secgrp_props_and_params(self, resource): + """Add security group properties and parameters to a resource + + :param BaseHotResource resource: a BaseHotResource (or subclass) + instance + + """ + if not self.options.exclude_secgroups: + + manager = resource.manager + + data = resource.data + security_groups = [] + + secgroup_default_parameter = None + for secgr in self.get_resource_secgroups(resource): + if secgr['name'] == 'default' and \ + self.options.generate_adoption_data: + if not secgroup_default_parameter: + res_name = manager.get_resource_name(data['id']) + param_name = "%s_default_security_group" % res_name + description = ( + "Default security group for %s %s" % ( + manager.singular_name, resource['name'] + ) + ) + default = secgr['id'] + resource.add_parameter( + param_name, description, default=default + ) + secgroup_default_parameter = {'get_param': param_name} + security_groups.append(secgroup_default_parameter) + else: + resource_name = self.get_resource_name(secgr['id']) + security_groups.append({'get_resource': resource_name}) + + if security_groups: + resource.properties['security_groups'] = security_groups + + def get_hot_resources(self): + resources = [] + if not self.options.exclude_secgroups: + for secgroup in six.itervalues(self.api.security_groups): + if secgroup['name'] == 'default' \ + and self.options.generate_adoption_data: + continue + resources.append( + SecurityGroup( + self, self.get_resource_name(secgroup.id), secgroup + ) + ) + return resources diff --git a/flameclient/resources/server_groups.py b/flameclient/resources/server_groups.py new file mode 100644 index 0000000..94f812a --- /dev/null +++ b/flameclient/resources/server_groups.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class ServerGroup(base_resources.AdvancedHotResource): + type = 'OS::Nova::ServerGroup' + property_keys = ('name', 'policies') + + +class ServerGroupsManager(base_resources.ResourceManager): + + def add_resource_server_groups(self, resource): + server = resource.data + for servergroup in self.api_resources.values(): + if server.id in servergroup.members: + hint = { + 'group': { + 'get_resource': + self.get_resource_name(servergroup.id) + } + } + resource.properties['scheduler_hints'] = hint + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.compute.server_groups) + ) + + def get_hot_resources(self): + return [ + ServerGroup(self, self.get_resource_name(sg.id), sg) + for sg in six.itervalues(self.api_resources) + ] diff --git a/flameclient/resources/servers.py b/flameclient/resources/servers.py new file mode 100644 index 0000000..0c7850a --- /dev/null +++ b/flameclient/resources/servers.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class Server(base_resources.AdvancedHotResource): + type = 'OS::Nova::Server' + property_keys = ('name',) + + def __init__(self, manager, name, server, properties=None): + super(Server, self).__init__(manager, name, server, properties) + for property_name in ('config_drive', 'metadata'): + if server[property_name]: + self.properties[property_name] = server[property_name] + self.managers.flavors.add_resource_flavor(self) + self.managers.images.add_resource_image(self) + self.managers.keypairs.add_resource_keypair(self) + self.managers.ports.add_resource_ports_or_secgroups_and_networks(self) + self.managers.volumes.add_resource_attached_volumes(self) + self.managers.server_groups.add_resource_server_groups(self) + + +class NovaFloatingIpAssociation(base_resources.TypedHotResource): + type = 'OS::Nova::FloatingIPAssociation' + + +class ServersManager(base_resources.ResourceManager): + + def get_fip_association(self, resource): + ip = resource.data + manager = resource.manager + if not self.options.exclude_servers and ip['port_id']: + server_id = self.api.ports[ip['port_id']]['device_id'] + if server_id and server_id in self.api.servers: + server_resource_name = self.get_resource_name(server_id) + resource_num = manager.get_resource_num(resource.id) + properties = { + 'floating_ip': {'get_resource': resource.name}, + 'server_id': {'get_resource': server_resource_name} + } + return NovaFloatingIpAssociation( + manager, + "floatingip_association_%d" % resource_num, + None, + properties + ) + + @classmethod + def add_arguments(cls, parser): + parser.add_argument('--exclude-servers', action='store_true', + default=False, + help="Do not export in template server resources.") + return parser + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.compute.servers) + ) + + def get_hot_resources(self): + if not self.options.exclude_servers: + return [ + Server(self, self.get_resource_name(server.id), server) + for server in six.itervalues(self.api.servers) + ] + return [] diff --git a/flameclient/resources/subnets.py b/flameclient/resources/subnets.py new file mode 100644 index 0000000..f569c52 --- /dev/null +++ b/flameclient/resources/subnets.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class Subnet(base_resources.AdvancedHotResource): + type = 'OS::Neutron::Subnet' + property_keys = ( + 'name', + 'allocation_pools', + 'cidr', + 'dns_nameservers', + 'enable_dhcp', + 'host_routes', + 'ip_version', + ) + + def __init__(self, manager, name, data, properties=None): + super(Subnet, self).__init__(manager, name, data, properties) + net_name = self.managers.networks.get_resource_name(self['network_id']) + self.properties['network_id'] = {'get_resource': net_name} + + +class SubnetsManager(base_resources.ResourceManager): + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.network.subnets) + ) + + def get_hot_resources(self): + return [ + Subnet(self, self.get_resource_name(subnet.id), subnet) + for subnet in six.itervalues(self.api.subnets) + if subnet['network_id'] in self.managers.networks.internal_networks + ] diff --git a/flameclient/resources/volumes.py b/flameclient/resources/volumes.py new file mode 100644 index 0000000..4c4631f --- /dev/null +++ b/flameclient/resources/volumes.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +from flameclient import resources as base_resources +from flameclient.utils import data_list_to_dict +from flameclient.utils import memoized_property + + +class Volume(base_resources.AdvancedHotResource): + type = 'OS::Cinder::Volume' + property_keys = ('size', 'name') + + def _add_source_volume(self): + volume = self.data + if volume.source_volid: + if volume.source_volid in self.api.volumes: + key = self.manager.get_resource_name(volume.source_volid) + self.properties['source_volid'] = {'get_resource': key} + else: + key = "%s_source_volid" % self.name + description = ( + "Volume to create volume %s from" % self.name) + self.add_parameter(key, description) + self.properties['source_volid'] = {'get_param': key} + + def _add_image(self): + volume = self.data + if volume.bootable and not volume.snapshot_id: + key = "%s_image" % self.name + description = "Image to create volume %s from" % self.name + constraints = [{'custom_constraint': "glance.image"}] + default = volume.volume_image_metadata['image_id'] + self.add_parameter(key, description, + constraints=constraints, + default=default) + self.properties['image'] = {'get_param': key} + + def _add_snapshot(self): + volume = self.data + if volume.snapshot_id: + key = "%s_snapshot_id" % self.name + self.properties['snapshot_id'] = {'get_param': key} + description = ( + "Snapshot to create volume %s from" % self.name) + self.add_parameter(key, description, + default=volume.snapshot_id) + + def _add_display_name(self): + volume = self.data + if hasattr(volume, 'display_name') and volume.display_name: + self.properties['name'] = volume.display_name + + def _add_display_description(self): + volume = self.data + if ( + hasattr(volume, 'display_description') and + volume.display_description + ): + self.properties['description'] = volume.display_description + + def _add_volume_type(self): + volume = self.data + if volume.volume_type and volume.volume_type != 'None': + key = "%s_volume_type" % self.name + description = ( + "Volume type for volume %s" % self.name) + default = volume.volume_type + self.add_parameter(key, description, default=default) + self.properties['volume_type'] = {'get_param': key} + + def _add_metadata(self): + volume = self.data + if volume.metadata: + self.properties['metadata'] = volume.metadata + + def __init__(self, manager, name, volume, properties=None): + super(Volume, self).__init__(manager, name, volume, properties) + self._add_source_volume() + self._add_image() + self._add_display_name() + self._add_display_description() + self._add_volume_type() + self._add_metadata() + + +class VolumesManager(base_resources.ResourceManager): + + def add_resource_attached_volumes(self, resource): + server = resource.data + manager = resource.manager + server_volumes = [] + att_key = 'os-extended-volumes:volumes_attached' + for server_volume in server[att_key]: + volume = self.api.volumes[server_volume['id']] + volume_resource_name = self.get_resource_name(server_volume['id']) + device = volume.attachments[0]['device'] + if not self.options.exclude_volumes: + server_volumes.append( + {'volume_id': {'get_resource': volume_resource_name}, + 'device_name': device}) + else: + volume_parameter_name = ("volume_%s_%d" % + (server.name, volume.enum)) + description = ("Volume for %s %s, device %s" % + (manager.singular_name, server.name, device)) + server_volumes.append( + {'volume_id': {'get_param': volume_parameter_name}, + 'device_name': device}) + resource.add_parameter(volume_parameter_name, description, + default=server_volume['id']) + if server_volumes: + # block_device_mapping_v2 is the new way of associating + # block devices to an instance + resource.properties['block_device_mapping_v2'] = server_volumes + + @classmethod + def add_arguments(cls, parser): + parser.add_argument('--exclude-volumes', action='store_true', + default=False, + help="Do not export volume resources.") + return parser + + @memoized_property + def api_resources(self): + return data_list_to_dict( + self.generator_memoize(self.conn.volume.volumes) + ) + + def get_hot_resources(self): + if not self.options.exclude_volumes: + return [ + Volume(self, self.get_resource_name(volume.id), volume) + for volume in six.itervalues(self.api.volumes) + ] + return [] diff --git a/flameclient/session.py b/flameclient/session.py new file mode 100644 index 0000000..48f6aa1 --- /dev/null +++ b/flameclient/session.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import argparse +import logging +import sys + +from keystoneauth1 import loading +from keystoneauth1 import session as keystone_session +from openstack import connection as os_connection +# We get a subclass of openstack.config.loader.OpenStackConfig which is more +# complete: +from os_client_config.config import OpenStackConfig # noqa +from shade.openstackcloud import OpenStackCloud # noqa + +from flameclient import utils + +LOG = logging.getLogger(__name__) + + +def list_auth_types(): + return [loader for loader in loading.get_available_plugin_loaders()] + + +def get_loader(auth_type): + if auth_type not in list_auth_types(): + raise ValueError( + "'auth_type' has to be one of %s (received %s)" % ( + list_auth_types(), auth_type + ) + ) + return loading.get_plugin_loader(auth_type) + + +def extract_loader_kwargs(loader, **kwargs): + """Get keystoneauth1.loading.get_plugin_loader's auth kwargs + + Return the specific auth kwargs and remaining kwargs + """ + loader_keys = [option.dest for option in loader.get_options()] + loader_kwargs = { + key: kwargs[key] for key in loader_keys if key in kwargs + } + remaining_kwargs = { + key: kwargs[key] for key in kwargs if key not in loader_keys + } + return loader_kwargs, remaining_kwargs + + +def get_openstack_config_with_envvars( + parser, config=None, load_envvars=False, load_yaml_config=False +): + """Add openstack_options to argparse.ArgumentParser + + :param argparse.ArgumentParser parser: argparse.ArgumentParser instance + or None. + + :param bool load_envvars: + Whether or not to load config settings from environment variables. + Defaults to True. + + :returns: OpenStackConfig instance + """ + + if config is None: + # If we use `openstack.config.loader.OpenStackConfig` instead of + # `os_client_config.config.OpenStackConfig` and then instantiate a + # `shade.openstackcloud.OpenStackCloud` instance as `cloud`, when + # trying to access the `cloud.keystone_client` attribute (or any other + # `*_client attribute`), we get a + # "TypeError: 'NoneType' object is not callable" exception + # This is why we use `os_client_config.config.OpenStackConfig`: it's to + # have all `shade.openstackcloud.OpenStackCloud` attributes working + # properly. Whether we use them or not, whether these attributes are + # deprecated or not, we do not want a 'broken' shade instance in case + # third party managers would use them. + config = OpenStackConfig( + load_envvars=load_envvars, load_yaml_config=load_yaml_config + ) + if parser: + if load_envvars or load_yaml_config: + config.register_argparse_arguments(parser, sys.argv) + else: + config.register_argparse_arguments(parser, []) + return config + + +def get_openstack_cli_arguments( + parser=None, load_envvars=True, load_yaml_config=True, + renamed_args=False +): + if parser is None: + parser = argparse.ArgumentParser() + get_openstack_config_with_envvars( + parser, load_envvars=load_envvars, load_yaml_config=load_yaml_config + ) + known_args, unknown_args = parser.parse_known_args() + if renamed_args: + known_args = utils.rename_os_options(known_args, clean=False) + return known_args, unknown_args + + +def get_openstack_envvars_as_kwargs( + with_args=False, parser=None, load_envvars=True, load_yaml_config=True +): + """Get openstack environment variables""" + known_args, unknown_args = get_openstack_cli_arguments( + parser=parser, load_envvars=load_envvars, + load_yaml_config=load_yaml_config + ) + kwargs = utils.rename_os_kwargs(vars(known_args), clean=True) + if with_args: + return known_args, unknown_args, kwargs + return kwargs + + +def get_keystoneauth1_session( + load_envvars=False, load_yaml_config=False, **kwargs +): + if load_envvars: + kwargs.update( + get_openstack_envvars_as_kwargs( + load_envvars=load_envvars, load_yaml_config=load_yaml_config + ) + ) + auth_type = kwargs.get('auth_type', 'password') + loader = get_loader(auth_type) + loader_kwargs, _ = extract_loader_kwargs(loader, **kwargs) + auth = loader.load_from_options(**loader_kwargs) + return keystone_session.Session(auth=auth) + + +get_keystone_session = get_keystoneauth1_session + + +def get_openstack_config( + parser_or_options=None, load_envvars=False, load_yaml_config=False, + **kwargs +): + """Same as os_client_config.get_config with less errors. + + Indeed, if we source OS_* variable environments, and one calls: + + os_client_config.get_config( + load_envvars=False, load_yaml_config=False, **kwargs + ) + + we get this kind of error: + + ConfigException: Region fr1 is not a valid region name for cloud + envvars. Valid choices are fr0. Please note that region names are case + sensitive. + + It seems like os_client_config.get_config fails to NOT handle envvars. + + Also, os_client_config.get_config saves the config in a global variable... + This is absolutely not thread safe... + + """ + parsed_options = None + parser = None + if isinstance(parser_or_options, argparse.ArgumentParser): + parser = parser_or_options + config = get_openstack_config_with_envvars( + parser=parser, load_envvars=load_envvars, + load_yaml_config=load_yaml_config + ) + if parser_or_options is not None: + if isinstance(parser_or_options, argparse.Namespace): + parsed_options = parser_or_options + elif isinstance(parser_or_options, dict): + parsed_options = utils.dict_to_options(parser_or_options) + elif isinstance(parser_or_options, argparse.ArgumentParser): + if load_envvars or load_yaml_config: + parsed_options, _ = parser_or_options.parse_known_args( + sys.argv) + else: + parsed_options, _ = parser_or_options.parse_known_args([]) + else: + raise AttributeError( + "'parser_options' has to be an 'argparse.ArgumentParser' or " + "'argparse.Namespace' instance or dict or None. " + "Received '%s'" % type(parser_or_options) + ) + return config.get_one( + options=parsed_options, + load_yaml_config=load_yaml_config, + load_envvars=load_envvars, + **kwargs + ) + + +def get_openstack_sdk_connection( + parser_or_options=None, load_envvars=False, load_yaml_config=False, + cloud_config=None, session=None, + **kwargs +): + if session is not None: + return os_connection.Connection(session=session, **kwargs) + + if cloud_config is None: + # we could return + # `openstack.connect(load_envvars=load_envvars, load_yaml_config=load_yaml_config, **kwargs)` # noqa + # but by doing so magic things are lacking in the config and we have + # random failing methods on the instance. See comments in + # get_openstack_config_with_envvars for more information. + cloud_config = get_openstack_config( + parser_or_options=parser_or_options, load_envvars=load_envvars, + load_yaml_config=load_yaml_config, + **kwargs + ) + if isinstance(parser_or_options, dict): + parser_or_options = utils.dict_to_options(parser_or_options) + return os_connection.from_config( + cloud_config=cloud_config, options=parser_or_options + ) + + +def get_shade( + parser_or_options=None, cloud_config=None, connection=None, + load_envvars=False, load_yaml_config=False, + **kwargs +): + """Get shade instance + + You can use an `argparse.ArgumentParser` or `argparse.Namespace` instance + with `load_envvars` and/or `load_yaml_config` set to True, + Or you kan use kwargs to authenticate with `load_envvars` AND + `load_yaml_config` set to False: + + cloud = get_shade( + auth_type='password', + auth_url='https://identity.fr1.cloudwatt.com/v2.0', + interface='public', + password='YourPassword', + project_id='Your ProjectID, + project_name='YourProjectName, + region_name='YourRegionName', + username='YourUserName' + ) + + You can also use a token instead of password with kwargs, If so, use + `auth_type='token'`. + + """ + if cloud_config is not None: + return OpenStackCloud(cloud_config=cloud_config, **kwargs) + elif connection is not None: + return OpenStackCloud(cloud_config=connection.config, **kwargs) + else: + return OpenStackCloud( + cloud_config=get_openstack_config( + parser_or_options=parser_or_options, load_envvars=load_envvars, + load_yaml_config=load_yaml_config, + **kwargs + ) + ) diff --git a/flameclient/tests/__init__.py b/flameclient/tests/__init__.py index e69de29..6262fcc 100644 --- a/flameclient/tests/__init__.py +++ b/flameclient/tests/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +try: + from unittest import mock # Python 3.3+ +except ImportError: + import mock # noqa: Python 2.7 + +try: + import unittest2 as unittest # Python 2.7 +except ImportError: + import unittest # noqa diff --git a/flameclient/tests/base.py b/flameclient/tests/base.py deleted file mode 100644 index 6e93268..0000000 --- a/flameclient/tests/base.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2010-2011 OpenStack Foundation -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os - -import fixtures -import testtools - -_TRUE_VALUES = ('True', 'true', '1', 'yes') - - -class TestCase(testtools.TestCase): - - """Test case base class for all unit tests.""" - - def setUp(self): - """Run before each test method to initialize test environment.""" - - super(TestCase, self).setUp() - test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) - try: - test_timeout = int(test_timeout) - except ValueError: - # If timeout value is invalid do not set a timeout. - test_timeout = 0 - if test_timeout > 0: - self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) - - self.useFixture(fixtures.NestedTempfile()) - self.useFixture(fixtures.TempHomeDir()) - - if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: - stdout = self.useFixture(fixtures.StringStream('stdout')).stream - self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) - if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: - stderr = self.useFixture(fixtures.StringStream('stderr')).stream - self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) - - self.log_fixture = self.useFixture(fixtures.FakeLogger()) diff --git a/flameclient/tests/fixtures/__init__.py b/flameclient/tests/fixtures/__init__.py new file mode 100644 index 0000000..830bbe3 --- /dev/null +++ b/flameclient/tests/fixtures/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os + + +FIXTURES_DIR = os.path.realpath( + os.path.dirname(__file__) +) diff --git a/flameclient/tests/fixtures/openstackcloud/__init__.py b/flameclient/tests/fixtures/openstackcloud/__init__.py new file mode 100644 index 0000000..99952aa --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from flameclient.utils import load_resource_modules + + +FIXTURES = load_resource_modules(__file__) diff --git a/flameclient/tests/fixtures/openstackcloud/flavors.py b/flameclient/tests/fixtures/openstackcloud/flavors.py new file mode 100644 index 0000000..258a629 --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/flavors.py @@ -0,0 +1,432 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'compute.flavors' + +FIXTURES = [{'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 20, + 'id': '16', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/16', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/16', + 'rel': 'bookmark'}], + 'name': 't1.cw.tiny', + 'os-flavor-access:is_public': True, + 'ram': 629, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 1}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '17', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/17', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/17', + 'rel': 'bookmark'}], + 'name': 's1.cw.small-1', + 'os-flavor-access:is_public': True, + 'ram': 1792, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 1}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '18', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/18', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/18', + 'rel': 'bookmark'}], + 'name': 'n1.cw.highcpu-2', + 'os-flavor-access:is_public': True, + 'ram': 4000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 2}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '19', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/19', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/19', + 'rel': 'bookmark'}], + 'name': 'n1.cw.highcpu-4', + 'os-flavor-access:is_public': True, + 'ram': 8000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 4}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '20', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/20', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/20', + 'rel': 'bookmark'}], + 'name': 'n1.cw.highcpu-8', + 'os-flavor-access:is_public': True, + 'ram': 16000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 8}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '21', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/21', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/21', + 'rel': 'bookmark'}], + 'name': 'n1.cw.standard-1', + 'os-flavor-access:is_public': True, + 'ram': 4000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 1}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '22', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/22', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/22', + 'rel': 'bookmark'}], + 'name': 'n1.cw.standard-2', + 'os-flavor-access:is_public': True, + 'ram': 8000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 2}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '23', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/23', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/23', + 'rel': 'bookmark'}], + 'name': 'n1.cw.standard-4', + 'os-flavor-access:is_public': True, + 'ram': 16000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 4}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '24', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/24', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/24', + 'rel': 'bookmark'}], + 'name': 'n1.cw.standard-8', + 'os-flavor-access:is_public': True, + 'ram': 32000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 8}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '25', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/25', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/25', + 'rel': 'bookmark'}], + 'name': 'n1.cw.standard-12', + 'os-flavor-access:is_public': True, + 'ram': 48000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 12}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '26', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/26', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/26', + 'rel': 'bookmark'}], + 'name': 'n1.cw.standard-16', + 'os-flavor-access:is_public': True, + 'ram': 64000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 16}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '28', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/28', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/28', + 'rel': 'bookmark'}], + 'name': 'n1.cw.highmem-2', + 'os-flavor-access:is_public': True, + 'ram': 13312, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 2}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '29', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/29', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/29', + 'rel': 'bookmark'}], + 'name': 'n1.cw.highmem-4', + 'os-flavor-access:is_public': True, + 'ram': 26624, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 4}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '30', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/30', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/30', + 'rel': 'bookmark'}], + 'name': 'n1.cw.highmem-8', + 'os-flavor-access:is_public': True, + 'ram': 53248, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 8}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '31', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/31', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/31', + 'rel': 'bookmark'}], + 'name': 'n1.cw.highmem-12', + 'os-flavor-access:is_public': True, + 'ram': 79872, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 12}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '38', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/38', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/38', + 'rel': 'bookmark'}], + 'name': 'n2.cw.highmem-2', + 'os-flavor-access:is_public': True, + 'ram': 13312, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 2}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 50, + 'description': None, + 'disk': 50, + 'id': '39', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/39', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/39', + 'rel': 'bookmark'}], + 'name': 'n2.cw.highmem-4', + 'os-flavor-access:is_public': True, + 'ram': 26624, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 4}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 100, + 'description': None, + 'disk': 50, + 'id': '40', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/40', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/40', + 'rel': 'bookmark'}], + 'name': 'n2.cw.highmem-8', + 'os-flavor-access:is_public': True, + 'ram': 53248, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 8}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 300, + 'description': None, + 'disk': 50, + 'id': '41', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/41', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/41', + 'rel': 'bookmark'}], + 'name': 'n2.cw.highmem-16', + 'os-flavor-access:is_public': True, + 'ram': 104000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 16}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '42', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/42', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/42', + 'rel': 'bookmark'}], + 'name': 'n2.cw.standard-1', + 'os-flavor-access:is_public': True, + 'ram': 4000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 1}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '43', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/43', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/43', + 'rel': 'bookmark'}], + 'name': 'n2.cw.standard-2', + 'os-flavor-access:is_public': True, + 'ram': 8000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 2}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '44', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/44', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/44', + 'rel': 'bookmark'}], + 'name': 'n2.cw.standard-4', + 'os-flavor-access:is_public': True, + 'ram': 16000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 4}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '45', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/45', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/45', + 'rel': 'bookmark'}], + 'name': 'n2.cw.standard-8', + 'os-flavor-access:is_public': True, + 'ram': 32000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 8}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'description': None, + 'disk': 50, + 'id': '46', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/46', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/46', + 'rel': 'bookmark'}], + 'name': 'n2.cw.standard-16', + 'os-flavor-access:is_public': True, + 'ram': 64000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 16}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 400, + 'description': None, + 'disk': 50, + 'id': '53', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/53', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/53', + 'rel': 'bookmark'}], + 'name': 'i2.cw.largessd-4', + 'os-flavor-access:is_public': True, + 'ram': 32000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 4}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 850, + 'description': None, + 'disk': 50, + 'id': '54', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/54', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/54', + 'rel': 'bookmark'}], + 'name': 'i2.cw.largessd-8', + 'os-flavor-access:is_public': True, + 'ram': 64000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 8}, + {'OS-FLV-DISABLED:disabled': False, + 'OS-FLV-EXT-DATA:ephemeral': 1750, + 'description': None, + 'disk': 50, + 'id': '55', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/flavors/55', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/55', + 'rel': 'bookmark'}], + 'name': 'i2.cw.largessd-16', + 'os-flavor-access:is_public': True, + 'ram': 128000, + 'rxtx_factor': 1.0, + 'swap': '', + 'vcpus': 16}] diff --git a/flameclient/tests/fixtures/openstackcloud/floatingips.py b/flameclient/tests/fixtures/openstackcloud/floatingips.py new file mode 100644 index 0000000..8c9894a --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/floatingips.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'network.ips' + +FIXTURES = [{'created_at': None, + 'description': None, + 'dns_domain': None, + 'dns_name': None, + 'fixed_ip_address': '192.168.0.8', + 'floating_ip_address': '84.39.32.171', + 'floating_network_id': '6ea98324-0f14-49f6-97c0-885d1b8dc517', + 'id': 'e6f50641-6c7e-468c-9623-1fd5ee2c1ebc', + 'name': '84.39.32.171', + 'port_details': None, + 'port_id': None, + 'qos_policy_id': None, + 'revision_number': None, + 'router_id': None, + 'status': 'ACTIVE', + 'subnet_id': None, + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'updated_at': None}] diff --git a/flameclient/tests/fixtures/openstackcloud/images.py b/flameclient/tests/fixtures/openstackcloud/images.py new file mode 100644 index 0000000..76ef479 --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/images.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'image.images' + +FIXTURES = [{'architecture': None, + 'auto_disk_config': None, + 'checksum': '57baccdf32b77ff7939d429b842d4440', + 'container_format': 'bare', + 'created_at': '2017-06-20T14:53:41Z', + 'direct_url': None, + 'disk_format': 'qcow2', + 'file': '/v2/images/8254d703-e46f-4101-88ff-6f40bf7df51a/file', + 'hw_boot_menu': None, + 'hw_cpu_cores': None, + 'hw_cpu_sockets': None, + 'hw_cpu_threads': None, + 'hw_disk_bus': None, + 'hw_machine_type': None, + 'hw_qemu_guest_agent': None, + 'hw_rng_model': 'virtio', + 'hw_scsi_model': None, + 'hw_serial_port_count': None, + 'hw_video_model': None, + 'hw_video_ram': None, + 'hw_vif_model': None, + 'hw_vif_multiqueue_enabled': None, + 'hw_watchdog_action': None, + 'hypervisor-type': None, + 'id': '8254d703-e46f-4101-88ff-6f40bf7df51a', + 'img_config_drive': None, + 'instance_type_rxtx_factor': None, + 'instance_uuid': None, + 'kernel_id': None, + 'locations': None, + 'metadata': None, + 'min_disk': 50, + 'min_ram': 2048, + 'name': 'Windows Server 2012 R2 Standard FR', + 'os_admin_user': None, + 'os_command_line': None, + 'os_distro': None, + 'os_require_quiesce': None, + 'os_secure_boot': None, + 'os_type': None, + 'os_version': None, + 'owner': '3ab52a7f1b824983bf418df633d9d88d', + 'path': None, + 'properties': None, + 'protected': False, + 'ramdisk_id': None, + 'size': 7976386560, + 'status': 'active', + 'store': None, + 'tags': [], + 'updated_at': '2017-06-20T14:56:43Z', + 'url': None, + 'value': None, + 'virtual_size': None, + 'visibility': 'public', + 'vm_mode': None, + 'vmware_adaptertype': None, + 'vmware_ostype': None}, + {'architecture': None, + 'auto_disk_config': None, + 'checksum': '8ec802fe753dfe8e226645a2e0106bf7', + 'container_format': 'bare', + 'created_at': '2017-03-23T15:46:41Z', + 'direct_url': None, + 'disk_format': 'qcow2', + 'file': '/v2/images/70a9c910-dd99-4065-bce9-11e89bc479fe/file', + 'hw_boot_menu': None, + 'hw_cpu_cores': None, + 'hw_cpu_sockets': None, + 'hw_cpu_threads': None, + 'hw_disk_bus': None, + 'hw_machine_type': None, + 'hw_qemu_guest_agent': None, + 'hw_rng_model': 'virtio', + 'hw_scsi_model': None, + 'hw_serial_port_count': None, + 'hw_video_model': None, + 'hw_video_ram': None, + 'hw_vif_model': None, + 'hw_vif_multiqueue_enabled': None, + 'hw_watchdog_action': None, + 'hypervisor-type': None, + 'id': '70a9c910-dd99-4065-bce9-11e89bc479fe', + 'img_config_drive': None, + 'instance_type_rxtx_factor': None, + 'instance_uuid': None, + 'kernel_id': None, + 'locations': None, + 'metadata': None, + 'min_disk': 20, + 'min_ram': 0, + 'name': 'Ubuntu 14.04', + 'os_admin_user': None, + 'os_command_line': None, + 'os_distro': None, + 'os_require_quiesce': None, + 'os_secure_boot': None, + 'os_type': None, + 'os_version': None, + 'owner': '3ab52a7f1b824983bf418df633d9d88d', + 'path': None, + 'properties': None, + 'protected': False, + 'ramdisk_id': None, + 'size': 1009057792, + 'status': 'active', + 'store': None, + 'tags': [], + 'updated_at': '2017-12-26T10:33:50Z', + 'url': None, + 'value': None, + 'virtual_size': None, + 'visibility': 'public', + 'vm_mode': None, + 'vmware_adaptertype': None, + 'vmware_ostype': None}, + {'architecture': None, + 'auto_disk_config': None, + 'checksum': '12872d8897d2eabfceac8b4627ff88f0', + 'container_format': 'bare', + 'created_at': '2016-06-07T07:36:26Z', + 'direct_url': None, + 'disk_format': 'qcow2', + 'file': '/v2/images/dd7d4b21-79b8-42f0-8464-0d1a5274c638/file', + 'hw_boot_menu': None, + 'hw_cpu_cores': None, + 'hw_cpu_sockets': None, + 'hw_cpu_threads': None, + 'hw_disk_bus': None, + 'hw_machine_type': None, + 'hw_qemu_guest_agent': None, + 'hw_rng_model': None, + 'hw_scsi_model': None, + 'hw_serial_port_count': None, + 'hw_video_model': None, + 'hw_video_ram': None, + 'hw_vif_model': None, + 'hw_vif_multiqueue_enabled': None, + 'hw_watchdog_action': None, + 'hypervisor-type': None, + 'id': 'dd7d4b21-79b8-42f0-8464-0d1a5274c638', + 'img_config_drive': None, + 'instance_type_rxtx_factor': None, + 'instance_uuid': None, + 'kernel_id': None, + 'locations': None, + 'metadata': None, + 'min_disk': 0, + 'min_ram': 0, + 'name': 'tempest-ubuntu-do-not-erase', + 'os_admin_user': None, + 'os_command_line': None, + 'os_distro': None, + 'os_require_quiesce': None, + 'os_secure_boot': None, + 'os_type': None, + 'os_version': None, + 'owner': '3ab52a7f1b824983bf418df633d9d88d', + 'path': None, + 'properties': None, + 'protected': False, + 'ramdisk_id': None, + 'size': 261488640, + 'status': 'active', + 'store': None, + 'tags': [], + 'updated_at': '2016-06-07T07:36:31Z', + 'url': None, + 'value': None, + 'virtual_size': None, + 'visibility': 'private', + 'vm_mode': None, + 'vmware_adaptertype': None, + 'vmware_ostype': None}] diff --git a/flameclient/tests/fixtures/openstackcloud/key_pairs.py b/flameclient/tests/fixtures/openstackcloud/key_pairs.py new file mode 100644 index 0000000..dbf4c73 --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/key_pairs.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'compute.keypairs' + +FIXTURES = [{'fingerprint': '01:99:63:d2:96:d4:23:25:2f:d5:c9:e4:2f:30:d2:3f', + 'id': 'tellurium-key', + 'name': 'tellurium-key', + 'private_key': None, + 'public_key': 'ssh-rsa ' + 'AAAAB3NzaC1yc2EAAAADAQABAAABAQCwp5kVZ+3baPZllNXZDG2mivd5nJ5wWY6Jj/WV6NlO6cUiaH5om6itU3lyJxtAbLgbbvY2FjMg1PI2JI3EHx0OSPEbDdeNsQGi31qyuiB1S5p6TreI0Dfy0tywJ9G2CURjkuJnC8SvnMfVYLMFBvx7p8RzxSdDm/zrmc4KY3ktdfYQDNtEiH2jucUUiY0ipVkDNhhv03+5C9cnpaIcVDBkddE/KEME8NIIh7s6aYCXJEWJx85nOVVRD5qK7ouV6FcGVn6zqWRD3jn0iSxcFwiKx7p6M77PmJAY4gBIpWQmutok6T4ZrXxa7jE4dybwo5e8dyvGyc7WWGqXcmcinUWr ' + 'Generated-by-Nova'}, + {'fingerprint': '76:e2:94:d2:13:c0:72:49:43:a7:58:f5:92:2f:e9:62', + 'id': 'tellurium-key-phrase', + 'name': 'tellurium-key-phrase', + 'private_key': None, + 'public_key': 'ssh-rsa ' + 'AAAAB3NzaC1yc2EAAAADAQABAAABAQCaYaTC6KCC2GURFMpRQiRq8Og10PcBbVnrQqluzi2E5vAqTHAegzivmGB8h2xrjXUfbKV2bE8kz2ZE66sq1yLO/jZ+rkOA+zLnOxNwkk3gQq57b26ZegPL2tTgottJTPGyPz6v2+LLtDf/+xTJMjkPKcMWylu12Js0XKVkdY35fwN7fRvA4xghtGu1GcmS2XFDMmeLDrG1KNPbBLj5cGoD723Ho7ZhAjLThoY/xMN2OYsSNzrg3S00QngZMzQvJf0ETrB3GtITk5FUs54qMRfiyUC72lw0gLTIJK8rs7fX/yeO+467afRt2xwHN50rEWJPeTVYWdlH/msh7NQ3LZ8/ ' + 'root@9b638ff21113'}] diff --git a/flameclient/tests/fixtures/openstackcloud/networks.py b/flameclient/tests/fixtures/openstackcloud/networks.py new file mode 100644 index 0000000..574fef9 --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/networks.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'network.networks' + +FIXTURES = [{'admin_state_up': True, + 'availability_zone_hints': None, + 'availability_zones': None, + 'created_at': None, + 'description': None, + 'dns_domain': None, + 'id': '6ea98324-0f14-49f6-97c0-885d1b8dc517', + 'ipv4_address_scope': None, + 'ipv6_address_scope': None, + 'is_default': None, + 'mtu': None, + 'name': 'public', + 'port_security_enabled': False, + 'project_id': None, + 'provider:network_type': None, + 'provider:physical_network': None, + 'provider:segmentation_id': None, + 'qos_policy_id': None, + 'revision_number': None, + 'router:external': True, + 'segments': None, + 'shared': False, + 'status': 'ACTIVE', + 'subnets': ['7ce9190e-397e-4523-bb9a-942a338555c8', + '16af2b37-a739-474a-b64d-ad5b515b24d7', + '85bc9804-bde9-4fc3-ac28-138bbed0be47', + '12469c2c-6902-460a-ad54-9252ca791d55', + '84095c22-48af-4cf5-b03d-0c745bfa1586', + '12f473cc-a01e-4f70-86d7-c1ae05058545', + '02efcad7-4744-41c4-8429-332828309d4f', + 'aa8fc58b-5db8-45ba-a354-180e5e2ae84b', + '6ac09891-9788-46b7-8165-9d2c9cefa483', + '38d8fc45-2698-4ffc-9beb-59331e3ef68d', + 'aded311e-5766-4adc-b6d8-64dd5c93a8c3', + 'b7f41f14-35a2-4bf4-a2b3-60c2200c0fb5', + '65d8722e-09f6-4b39-a693-e61018867377', + 'c5387c58-542c-4cfd-8338-afcbd37e4546', + 'baa6798a-0eba-417e-b294-31fbf1c4a7c5', + '3c8fde67-c209-4716-a515-5777667a4835', + 'bf806833-7f33-4254-865f-8a874d36d17c', + '6736cae7-7501-4e10-af94-bb06acaf5de6', + '7b2e3c57-e8cc-47ac-a008-e0ee2db5e567', + '85342bf1-7e69-4468-b269-8435c4991773', + '6e289f4e-ec6a-4fd7-9bbc-94c3f17e57a8', + 'de042fd3-acf3-487f-819f-7b18617728ec', + 'da35a785-99ea-4d54-b056-0c20feca1125'], + 'tags': [], + 'updated_at': None, + 'vlan_transparent': None}, + {'admin_state_up': True, + 'availability_zone_hints': None, + 'availability_zones': None, + 'created_at': None, + 'description': None, + 'dns_domain': None, + 'id': 'f054013d-7052-4708-9c72-2948a329fac3', + 'ipv4_address_scope': None, + 'ipv6_address_scope': None, + 'is_default': None, + 'mtu': None, + 'name': 'tellurium_net', + 'port_security_enabled': False, + 'project_id': None, + 'provider:network_type': None, + 'provider:physical_network': None, + 'provider:segmentation_id': None, + 'qos_policy_id': None, + 'revision_number': None, + 'router:external': False, + 'segments': None, + 'shared': False, + 'status': 'ACTIVE', + 'subnets': ['541f0782-587f-428b-bd79-ca227a66973b'], + 'tags': [], + 'updated_at': None, + 'vlan_transparent': None}] diff --git a/flameclient/tests/fixtures/openstackcloud/ports.py b/flameclient/tests/fixtures/openstackcloud/ports.py new file mode 100644 index 0000000..edfdad2 --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/ports.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'network.ports' + +FIXTURES = [{'admin_state_up': True, + 'allowed_address_pairs': None, + 'binding:host_id': None, + 'binding:profile': None, + 'binding:vif_details': None, + 'binding:vif_type': None, + 'binding:vnic_type': 'normal', + 'created_at': None, + 'data_plane_status': None, + 'description': None, + 'device_id': '2cf8db13-312a-4307-ba38-43b727ebcce6', + 'device_owner': 'compute:None', + 'dns_assignment': None, + 'dns_domain': None, + 'dns_name': None, + 'extra_dhcp_opts': None, + 'fixed_ips': [{'ip_address': '192.168.0.3', + 'subnet_id': '541f0782-587f-428b-bd79-ca227a66973b'}], + 'id': '3decf6f8-2591-4ab0-af19-00f6ce6a0cf5', + 'mac_address': '02:3d:ec:f6:f8:25', + 'name': 'Tellurium_Fixtures-windows_port-hxvnswbzi56x', + 'network_id': 'f054013d-7052-4708-9c72-2948a329fac3', + 'port_security_enabled': False, + 'qos_policy_id': None, + 'revision_number': None, + 'security_groups': ['9ffd2654-7ca6-48ae-852d-6503d5ce4a60'], + 'status': 'ACTIVE', + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'trunk_details': None, + 'updated_at': None}, + {'admin_state_up': True, + 'allowed_address_pairs': None, + 'binding:host_id': None, + 'binding:profile': None, + 'binding:vif_details': None, + 'binding:vif_type': None, + 'binding:vnic_type': 'normal', + 'created_at': None, + 'data_plane_status': None, + 'description': None, + 'device_id': '4add78fa-93df-4550-aaf7-aba2239ba00a', + 'device_owner': 'compute:None', + 'dns_assignment': None, + 'dns_domain': None, + 'dns_name': None, + 'extra_dhcp_opts': None, + 'fixed_ips': [{'ip_address': '192.168.0.4', + 'subnet_id': '541f0782-587f-428b-bd79-ca227a66973b'}], + 'id': '206df716-da1a-4a50-ba1a-74f42a007dd0', + 'mac_address': '02:20:6d:f7:16:da', + 'name': 'Tellurium_Fixtures-instance_port-fmln6fjl4yvo', + 'network_id': 'f054013d-7052-4708-9c72-2948a329fac3', + 'port_security_enabled': False, + 'qos_policy_id': None, + 'revision_number': None, + 'security_groups': ['9ffd2654-7ca6-48ae-852d-6503d5ce4a60'], + 'status': 'ACTIVE', + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'trunk_details': None, + 'updated_at': None}, + {'admin_state_up': True, + 'allowed_address_pairs': None, + 'binding:host_id': None, + 'binding:profile': None, + 'binding:vif_details': None, + 'binding:vif_type': None, + 'binding:vnic_type': 'normal', + 'created_at': None, + 'data_plane_status': None, + 'description': None, + 'device_id': 'b6316031-e629-48b8-aac5-3f1b21ffe0f3', + 'device_owner': 'compute:None', + 'dns_assignment': None, + 'dns_domain': None, + 'dns_name': None, + 'extra_dhcp_opts': None, + 'fixed_ips': [{'ip_address': '192.168.0.5', + 'subnet_id': '541f0782-587f-428b-bd79-ca227a66973b'}], + 'id': 'cb336b60-f298-4a51-9443-89880e3cdb51', + 'mac_address': '02:cb:33:6b:60:f2', + 'name': 'Tellurium_Fixtures-windows_ssh_pass_port-4zlwanbetsg2', + 'network_id': 'f054013d-7052-4708-9c72-2948a329fac3', + 'port_security_enabled': False, + 'qos_policy_id': None, + 'revision_number': None, + 'security_groups': ['9ffd2654-7ca6-48ae-852d-6503d5ce4a60'], + 'status': 'ACTIVE', + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'trunk_details': None, + 'updated_at': None}, + {'admin_state_up': True, + 'allowed_address_pairs': None, + 'binding:host_id': None, + 'binding:profile': None, + 'binding:vif_details': None, + 'binding:vif_type': None, + 'binding:vnic_type': 'normal', + 'created_at': None, + 'data_plane_status': None, + 'description': None, + 'device_id': 'ebc6dd0c-a276-4368-8084-bd37c587cc24', + 'device_owner': 'network:router_interface', + 'dns_assignment': None, + 'dns_domain': None, + 'dns_name': None, + 'extra_dhcp_opts': None, + 'fixed_ips': [{'ip_address': '192.168.0.1', + 'subnet_id': '541f0782-587f-428b-bd79-ca227a66973b'}], + 'id': 'e3b88764-3c46-44aa-ae26-fa6ec2df93bd', + 'mac_address': '02:e3:b8:87:64:3c', + 'name': 'e3b88764-3c46-44aa-ae26-fa6ec2df93bd', + 'network_id': 'f054013d-7052-4708-9c72-2948a329fac3', + 'port_security_enabled': False, + 'qos_policy_id': None, + 'revision_number': None, + 'security_groups': ['9ffd2654-7ca6-48ae-852d-6503d5ce4a60'], + 'status': 'ACTIVE', + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'trunk_details': None, + 'updated_at': None}, + {'admin_state_up': True, + 'allowed_address_pairs': None, + 'binding:host_id': None, + 'binding:profile': None, + 'binding:vif_details': None, + 'binding:vif_type': None, + 'binding:vnic_type': 'normal', + 'created_at': None, + 'data_plane_status': None, + 'description': None, + 'device_id': '99c42e21-5099-4903-9121-063925aad299', + 'device_owner': 'compute:nova', + 'dns_assignment': None, + 'dns_domain': None, + 'dns_name': None, + 'extra_dhcp_opts': None, + 'fixed_ips': [{'ip_address': '192.168.0.6', + 'subnet_id': '541f0782-587f-428b-bd79-ca227a66973b'}], + 'id': '4f413432-b499-4fcc-a81a-1ee0d8bc16b7', + 'mac_address': '02:4f:41:34:32:b4', + 'name': '4f413432-b499-4fcc-a81a-1ee0d8bc16b7', + 'network_id': 'f054013d-7052-4708-9c72-2948a329fac3', + 'port_security_enabled': False, + 'qos_policy_id': None, + 'revision_number': None, + 'security_groups': ['156799a3-565e-48b3-938c-f95f09093c66', + '9ffd2654-7ca6-48ae-852d-6503d5ce4a60'], + 'status': 'ACTIVE', + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'trunk_details': None, + 'updated_at': None}] diff --git a/flameclient/tests/fixtures/openstackcloud/routers.py b/flameclient/tests/fixtures/openstackcloud/routers.py new file mode 100644 index 0000000..d77845e --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/routers.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'network.routers' + +FIXTURES = [{'admin_state_up': True, + 'availability_zone_hints': None, + 'availability_zones': None, + 'created_at': None, + 'description': None, + 'distributed': None, + 'external_gateway_info': {'enable_snat': True, + 'network_id': '6ea98324-0f14-49f6-97c0-885d1b8dc517'}, + 'flavor_id': None, + 'ha': None, + 'id': 'ebc6dd0c-a276-4368-8084-bd37c587cc24', + 'name': 'tellurium_router', + 'revision': None, + 'routes': None, + 'status': 'ACTIVE', + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'updated_at': None}] diff --git a/flameclient/tests/fixtures/openstackcloud/security_groups.py b/flameclient/tests/fixtures/openstackcloud/security_groups.py new file mode 100644 index 0000000..5e0403d --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/security_groups.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'network.security_groups' + +FIXTURES = [{'created_at': None, + 'description': None, + 'id': '9ffd2654-7ca6-48ae-852d-6503d5ce4a60', + 'name': 'default', + 'revision_number': None, + 'security_group_rules': [{'direction': 'ingress', + 'ethertype': 'IPv4', + 'id': 'ec72eaca-22e2-47b6-b1a5-2fe6f8129e47', + 'port_range_max': None, + 'port_range_min': None, + 'protocol': None, + 'remote_group_id': '9ffd2654-7ca6-48ae-852d-6503d5ce4a60', + 'remote_ip_prefix': None, + 'security_group_id': '9ffd2654-7ca6-48ae-852d-6503d5ce4a60', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce'}, + {'direction': 'ingress', + 'ethertype': 'IPv6', + 'id': 'd5720f56-e7ce-4c7f-982d-a70d76c37b11', + 'port_range_max': None, + 'port_range_min': None, + 'protocol': None, + 'remote_group_id': '9ffd2654-7ca6-48ae-852d-6503d5ce4a60', + 'remote_ip_prefix': None, + 'security_group_id': '9ffd2654-7ca6-48ae-852d-6503d5ce4a60', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce'}, + {'direction': 'egress', + 'ethertype': 'IPv4', + 'id': 'd05bc784-b34c-4e0c-a2ea-7b10992caa1e', + 'port_range_max': None, + 'port_range_min': None, + 'protocol': None, + 'remote_group_id': None, + 'remote_ip_prefix': '0.0.0.0/0', + 'security_group_id': '9ffd2654-7ca6-48ae-852d-6503d5ce4a60', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce'}, + {'direction': 'egress', + 'ethertype': 'IPv6', + 'id': 'a6abbc8e-92c2-42f6-9ac9-a04544588739', + 'port_range_max': None, + 'port_range_min': None, + 'protocol': None, + 'remote_group_id': None, + 'remote_ip_prefix': '::/0', + 'security_group_id': '9ffd2654-7ca6-48ae-852d-6503d5ce4a60', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce'}], + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'updated_at': None}, + {'created_at': None, + 'description': '', + 'id': '156799a3-565e-48b3-938c-f95f09093c66', + 'name': 'http', + 'revision_number': None, + 'security_group_rules': [{'direction': 'egress', + 'ethertype': 'IPv4', + 'id': '80508f7d-b893-4bcb-bddc-51a946634492', + 'port_range_max': None, + 'port_range_min': None, + 'protocol': None, + 'remote_group_id': None, + 'remote_ip_prefix': '0.0.0.0/0', + 'security_group_id': '156799a3-565e-48b3-938c-f95f09093c66', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce'}, + {'direction': 'egress', + 'ethertype': 'IPv6', + 'id': 'eab50b80-bc3c-484c-a3c8-fd6cf5ee9c50', + 'port_range_max': None, + 'port_range_min': None, + 'protocol': None, + 'remote_group_id': None, + 'remote_ip_prefix': None, + 'security_group_id': '156799a3-565e-48b3-938c-f95f09093c66', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce'}, + {'direction': 'ingress', + 'ethertype': 'IPv4', + 'id': 'a6ed0dc8-ee29-462e-84a0-961675e08c4a', + 'port_range_max': 80, + 'port_range_min': 80, + 'protocol': 'tcp', + 'remote_group_id': None, + 'remote_ip_prefix': '0.0.0.0/0', + 'security_group_id': '156799a3-565e-48b3-938c-f95f09093c66', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce'}], + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'updated_at': None}] diff --git a/flameclient/tests/fixtures/openstackcloud/server_groups.py b/flameclient/tests/fixtures/openstackcloud/server_groups.py new file mode 100644 index 0000000..6f3942a --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/server_groups.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'compute.server_groups' + +FIXTURES = [] diff --git a/flameclient/tests/fixtures/openstackcloud/servers.py b/flameclient/tests/fixtures/openstackcloud/servers.py new file mode 100644 index 0000000..a4964e5 --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/servers.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'compute.servers' + +FIXTURES = [{'OS-DCF:diskConfig': 'AUTO', + 'OS-EXT-AZ:availability_zone': 'nova', + 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, + 'OS-EXT-SRV-ATTR:instance_name': None, + 'OS-EXT-SRV-ATTR:user_data': None, + 'OS-EXT-STS:power_state': 1, + 'OS-EXT-STS:task_state': None, + 'OS-EXT-STS:vm_state': 'active', + 'OS-SCH-HNT:scheduler_hints': None, + 'OS-SRV-USG:launched_at': '2018-11-09T13:39:42.000000', + 'OS-SRV-USG:terminated_at': None, + 'accessIPv4': '', + 'accessIPv6': '', + 'addresses': {'tellurium_net': [{'OS-EXT-IPS-MAC:mac_addr': '02:4f:41:34:32:b4', + 'OS-EXT-IPS:type': 'fixed', + 'addr': '192.168.0.6', + 'version': 4}]}, + 'adminPass': None, + 'block_device_mapping_v2': None, + 'config_drive': '', + 'created': '2018-11-09T13:39:38Z', + 'flavor': {'id': '17', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/17', + 'rel': 'bookmark'}]}, + 'flavorRef': None, + 'hostId': '84246cf2f8ac920628901e7a84a054b45f246f2d77f6df3f6c30cc43', + 'id': '99c42e21-5099-4903-9121-063925aad299', + 'image': {}, + 'imageRef': None, + 'key_name': 'tellurium-key', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/servers/99c42e21-5099-4903-9121-063925aad299', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/servers/99c42e21-5099-4903-9121-063925aad299', + 'rel': 'bookmark'}], + 'metadata': {}, + 'name': 'Ubuntu 14.04', + 'networks': None, + 'os-extended-volumes:volumes_attached': [{'id': '8ca10346-fe4c-4e68-9a18-98df875d1ecc'}], + 'personality': None, + 'progress': 0, + 'security_groups': [{'name': 'http'}, {'name': 'default'}], + 'status': 'ACTIVE', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'updated': '2018-11-09T13:39:42Z', + 'user_id': '7f71ea7b8abd41e5a1f303f4a1bc16b9'}, + {'OS-DCF:diskConfig': 'MANUAL', + 'OS-EXT-AZ:availability_zone': 'nova', + 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, + 'OS-EXT-SRV-ATTR:instance_name': None, + 'OS-EXT-SRV-ATTR:user_data': None, + 'OS-EXT-STS:power_state': 1, + 'OS-EXT-STS:task_state': None, + 'OS-EXT-STS:vm_state': 'active', + 'OS-SCH-HNT:scheduler_hints': None, + 'OS-SRV-USG:launched_at': '2018-11-09T04:54:35.000000', + 'OS-SRV-USG:terminated_at': None, + 'accessIPv4': '', + 'accessIPv6': '', + 'addresses': {'tellurium_net': [{'OS-EXT-IPS-MAC:mac_addr': '02:3d:ec:f6:f8:25', + 'OS-EXT-IPS:type': 'fixed', + 'addr': '192.168.0.3', + 'version': 4}]}, + 'adminPass': None, + 'block_device_mapping_v2': None, + 'config_drive': '', + 'created': '2018-11-09T04:54:32Z', + 'flavor': {'id': '42', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/42', + 'rel': 'bookmark'}]}, + 'flavorRef': None, + 'hostId': 'dd853263647ff603cfe0726dc78e34e0b9b2fc092bead501af233bcf', + 'id': '2cf8db13-312a-4307-ba38-43b727ebcce6', + 'image': {'id': '8254d703-e46f-4101-88ff-6f40bf7df51a', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/images/8254d703-e46f-4101-88ff-6f40bf7df51a', + 'rel': 'bookmark'}]}, + 'imageRef': None, + 'key_name': 'tellurium-key', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/servers/2cf8db13-312a-4307-ba38-43b727ebcce6', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/servers/2cf8db13-312a-4307-ba38-43b727ebcce6', + 'rel': 'bookmark'}], + 'metadata': {}, + 'name': 'tellurium_win_instance', + 'networks': None, + 'os-extended-volumes:volumes_attached': [], + 'personality': None, + 'progress': 0, + 'security_groups': [{'name': 'default'}], + 'status': 'ACTIVE', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'updated': '2018-11-09T04:55:58Z', + 'user_id': '7f71ea7b8abd41e5a1f303f4a1bc16b9'}, + {'OS-DCF:diskConfig': 'MANUAL', + 'OS-EXT-AZ:availability_zone': 'nova', + 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, + 'OS-EXT-SRV-ATTR:instance_name': None, + 'OS-EXT-SRV-ATTR:user_data': None, + 'OS-EXT-STS:power_state': 1, + 'OS-EXT-STS:task_state': None, + 'OS-EXT-STS:vm_state': 'active', + 'OS-SCH-HNT:scheduler_hints': None, + 'OS-SRV-USG:launched_at': '2018-11-09T04:57:15.000000', + 'OS-SRV-USG:terminated_at': None, + 'accessIPv4': '', + 'accessIPv6': '', + 'addresses': {'tellurium_net': [{'OS-EXT-IPS-MAC:mac_addr': '02:cb:33:6b:60:f2', + 'OS-EXT-IPS:type': 'fixed', + 'addr': '192.168.0.5', + 'version': 4}]}, + 'adminPass': None, + 'block_device_mapping_v2': None, + 'config_drive': '', + 'created': '2018-11-09T04:54:32Z', + 'flavor': {'id': '42', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/42', + 'rel': 'bookmark'}]}, + 'flavorRef': None, + 'hostId': 'b357f8b6b7368a07bb2aed776c6579d132c90f7dcb2f28a2a34c98e8', + 'id': 'b6316031-e629-48b8-aac5-3f1b21ffe0f3', + 'image': {'id': '8254d703-e46f-4101-88ff-6f40bf7df51a', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/images/8254d703-e46f-4101-88ff-6f40bf7df51a', + 'rel': 'bookmark'}]}, + 'imageRef': None, + 'key_name': 'tellurium-key-phrase', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/servers/b6316031-e629-48b8-aac5-3f1b21ffe0f3', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/servers/b6316031-e629-48b8-aac5-3f1b21ffe0f3', + 'rel': 'bookmark'}], + 'metadata': {}, + 'name': 'tellurium_win_instance_ssh_pass', + 'networks': None, + 'os-extended-volumes:volumes_attached': [], + 'personality': None, + 'progress': 0, + 'security_groups': [{'name': 'default'}], + 'status': 'ACTIVE', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'updated': '2018-11-09T04:59:08Z', + 'user_id': '7f71ea7b8abd41e5a1f303f4a1bc16b9'}, + {'OS-DCF:diskConfig': 'MANUAL', + 'OS-EXT-AZ:availability_zone': 'nova', + 'OS-EXT-SRV-ATTR:hypervisor_hostname': None, + 'OS-EXT-SRV-ATTR:instance_name': None, + 'OS-EXT-SRV-ATTR:user_data': None, + 'OS-EXT-STS:power_state': 1, + 'OS-EXT-STS:task_state': None, + 'OS-EXT-STS:vm_state': 'active', + 'OS-SCH-HNT:scheduler_hints': None, + 'OS-SRV-USG:launched_at': '2018-11-09T04:54:36.000000', + 'OS-SRV-USG:terminated_at': None, + 'accessIPv4': '', + 'accessIPv6': '', + 'addresses': {'tellurium_net': [{'OS-EXT-IPS-MAC:mac_addr': '02:20:6d:f7:16:da', + 'OS-EXT-IPS:type': 'fixed', + 'addr': '192.168.0.4', + 'version': 4}]}, + 'adminPass': None, + 'block_device_mapping_v2': None, + 'config_drive': '', + 'created': '2018-11-09T04:54:32Z', + 'flavor': {'id': '16', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/flavors/16', + 'rel': 'bookmark'}]}, + 'flavorRef': None, + 'hostId': 'a5586828459ec792613bc24c0807801ce4341a562d47ff3203d41722', + 'id': '4add78fa-93df-4550-aaf7-aba2239ba00a', + 'image': {'id': 'dd7d4b21-79b8-42f0-8464-0d1a5274c638', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/images/dd7d4b21-79b8-42f0-8464-0d1a5274c638', + 'rel': 'bookmark'}]}, + 'imageRef': None, + 'key_name': 'tellurium-key', + 'links': [{'href': 'https://compute.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/servers/4add78fa-93df-4550-aaf7-aba2239ba00a', + 'rel': 'self'}, + {'href': 'https://compute.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/servers/4add78fa-93df-4550-aaf7-aba2239ba00a', + 'rel': 'bookmark'}], + 'metadata': {}, + 'name': 'tellurium_instance', + 'networks': None, + 'os-extended-volumes:volumes_attached': [], + 'personality': None, + 'progress': 0, + 'security_groups': [{'name': 'default'}], + 'status': 'ACTIVE', + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'updated': '2018-11-09T05:22:45Z', + 'user_id': '7f71ea7b8abd41e5a1f303f4a1bc16b9'}] diff --git a/flameclient/tests/fixtures/openstackcloud/subnets.py b/flameclient/tests/fixtures/openstackcloud/subnets.py new file mode 100644 index 0000000..4a10dc2 --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/subnets.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'network.subnets' + +FIXTURES = [{'allocation_pools': [{'end': '192.168.0.254', 'start': '192.168.0.2'}], + 'cidr': '192.168.0.0/24', + 'created_at': None, + 'description': None, + 'dns_nameservers': ['8.8.8.8'], + 'enable_dhcp': True, + 'gateway_ip': '192.168.0.1', + 'host_routes': [], + 'id': '541f0782-587f-428b-bd79-ca227a66973b', + 'ip_version': 4, + 'ipv6_address_mode': None, + 'ipv6_ra_mode': None, + 'name': 'tellurium_net_subnet', + 'network_id': 'f054013d-7052-4708-9c72-2948a329fac3', + 'revision_number': None, + 'segment_id': None, + 'service_types': None, + 'subnetpool_id': None, + 'tags': [], + 'tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'updated_at': None, + 'use_default_subnetpool': None}] diff --git a/flameclient/tests/fixtures/openstackcloud/volumes.py b/flameclient/tests/fixtures/openstackcloud/volumes.py new file mode 100644 index 0000000..8aec490 --- /dev/null +++ b/flameclient/tests/fixtures/openstackcloud/volumes.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +NAME = 'volume.volumes' + +FIXTURES = [{'attachments': [{'attached_at': '2018-11-09T13:39:41.000000', + 'attachment_id': '7edd34f8-5c1b-4c46-afac-916db0e10726', + 'device': '/dev/vda', + 'host_name': None, + 'id': '8ca10346-fe4c-4e68-9a18-98df875d1ecc', + 'server_id': '99c42e21-5099-4903-9121-063925aad299', + 'volume_id': '8ca10346-fe4c-4e68-9a18-98df875d1ecc'}], + 'availability_zone': 'prd1', + 'bootable': True, + 'consistencygroup_id': None, + 'created_at': '2018-11-09T13:34:08.000000', + 'description': '', + 'encrypted': False, + 'id': '8ca10346-fe4c-4e68-9a18-98df875d1ecc', + 'imageRef': None, + 'links': [{'href': 'https://volume.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/volumes/8ca10346-fe4c-4e68-9a18-98df875d1ecc', + 'rel': 'self'}, + {'href': 'https://volume.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/volumes/8ca10346-fe4c-4e68-9a18-98df875d1ecc', + 'rel': 'bookmark'}], + 'metadata': {'attached_mode': 'rw', 'readonly': 'False'}, + 'name': 'Ubuntu 14.04', + 'os-vol-host-attr:host': None, + 'os-vol-mig-status-attr:migstat': None, + 'os-vol-mig-status-attr:name_id': None, + 'os-vol-tenant-attr:tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'os-volume-replication:driver_data': None, + 'os-volume-replication:extended_status': None, + 'replication_status': 'disabled', + 'size': 20, + 'snapshot_id': None, + 'source_volid': None, + 'status': 'in-use', + 'volume_image_metadata': {'checksum': '8ec802fe753dfe8e226645a2e0106bf7', + 'container_format': 'bare', + 'cw_cat': 'open_source', + 'cw_logo': 'lin-ubuntu.png', + 'cw_origin': 'Cloudwatt', + 'cw_os': 'Ubuntu', + 'disk_format': 'qcow2', + 'hw_cpu_max_sockets': '1', + 'hw_rng_model': 'virtio', + 'image_id': '70a9c910-dd99-4065-bce9-11e89bc479fe', + 'image_name': 'Ubuntu 14.04', + 'min_disk': '20', + 'min_ram': '0', + 'size': '1009057792'}, + 'volume_type': 'standard'}, + {'attachments': [], + 'availability_zone': 'prd1', + 'bootable': False, + 'consistencygroup_id': None, + 'created_at': '2018-11-09T04:54:29.000000', + 'description': None, + 'encrypted': False, + 'id': '34ce951a-f2d9-4bdd-904d-9f70269c680b', + 'imageRef': None, + 'links': [{'href': 'https://volume.fr1.cloudwatt.com/v2/9824a7403a1b411d8d207d26218597ce/volumes/34ce951a-f2d9-4bdd-904d-9f70269c680b', + 'rel': 'self'}, + {'href': 'https://volume.fr1.cloudwatt.com/9824a7403a1b411d8d207d26218597ce/volumes/34ce951a-f2d9-4bdd-904d-9f70269c680b', + 'rel': 'bookmark'}], + 'metadata': {}, + 'name': 'tellurium_volume', + 'os-vol-host-attr:host': None, + 'os-vol-mig-status-attr:migstat': None, + 'os-vol-mig-status-attr:name_id': None, + 'os-vol-tenant-attr:tenant_id': '9824a7403a1b411d8d207d26218597ce', + 'os-volume-replication:driver_data': None, + 'os-volume-replication:extended_status': None, + 'replication_status': 'disabled', + 'size': 5, + 'snapshot_id': None, + 'source_volid': None, + 'status': 'available', + 'volume_image_metadata': {}, + 'volume_type': 'standard'}] diff --git a/flameclient/tests/fixtures/results/__init__.py b/flameclient/tests/fixtures/results/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flameclient/tests/fixtures/results/flame.yaml b/flameclient/tests/fixtures/results/flame.yaml new file mode 100644 index 0000000..29b3293 --- /dev/null +++ b/flameclient/tests/fixtures/results/flame.yaml @@ -0,0 +1,225 @@ +description: Generated template +heat_template_version: 2013-05-23 +parameters: + external_network_for_floating_ip_0: + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Network to allocate floating IP from + type: string + router_0_external_network: + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Router external network + type: string + server_0_flavor: + default: '17' + description: Flavor to use for server server_0 + type: string + server_1_flavor: + default: '42' + description: Flavor to use for server server_1 + type: string + server_1_image: + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_1 + type: string + server_2_flavor: + default: '42' + description: Flavor to use for server server_2 + type: string + server_2_image: + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_2 + type: string + server_3_flavor: + default: '16' + description: Flavor to use for server server_3 + type: string + server_3_image: + default: dd7d4b21-79b8-42f0-8464-0d1a5274c638 + description: Image to use for server server_3 + type: string + volume_0_image: + default: 70a9c910-dd99-4065-bce9-11e89bc479fe + description: Image to create volume volume_0 from + type: string + volume_0_volume_type: + default: standard + description: Volume type for volume volume_0 + type: string + volume_1_volume_type: + default: standard + description: Volume type for volume volume_1 + type: string +resources: + floating_ip_0: + properties: + floating_network_id: + get_param: external_network_for_floating_ip_0 + type: OS::Neutron::FloatingIP + keypair_0: + properties: + name: tellurium-key + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwp5kVZ+3baPZllNXZDG2mivd5nJ5wWY6Jj/WV6NlO6cUiaH5om6itU3lyJxtAbLgbbvY2FjMg1PI2JI3EHx0OSPEbDdeNsQGi31qyuiB1S5p6TreI0Dfy0tywJ9G2CURjkuJnC8SvnMfVYLMFBvx7p8RzxSdDm/zrmc4KY3ktdfYQDNtEiH2jucUUiY0ipVkDNhhv03+5C9cnpaIcVDBkddE/KEME8NIIh7s6aYCXJEWJx85nOVVRD5qK7ouV6FcGVn6zqWRD3jn0iSxcFwiKx7p6M77PmJAY4gBIpWQmutok6T4ZrXxa7jE4dybwo5e8dyvGyc7WWGqXcmcinUWr + Generated-by-Nova + type: OS::Nova::KeyPair + keypair_1: + properties: + name: tellurium-key-phrase + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCaYaTC6KCC2GURFMpRQiRq8Og10PcBbVnrQqluzi2E5vAqTHAegzivmGB8h2xrjXUfbKV2bE8kz2ZE66sq1yLO/jZ+rkOA+zLnOxNwkk3gQq57b26ZegPL2tTgottJTPGyPz6v2+LLtDf/+xTJMjkPKcMWylu12Js0XKVkdY35fwN7fRvA4xghtGu1GcmS2XFDMmeLDrG1KNPbBLj5cGoD723Ho7ZhAjLThoY/xMN2OYsSNzrg3S00QngZMzQvJf0ETrB3GtITk5FUs54qMRfiyUC72lw0gLTIJK8rs7fX/yeO+467afRt2xwHN50rEWJPeTVYWdlH/msh7NQ3LZ8/ + root@9b638ff21113 + type: OS::Nova::KeyPair + network_1: + properties: + admin_state_up: true + name: tellurium_net + shared: false + type: OS::Neutron::Net + router_0: + properties: + admin_state_up: true + name: tellurium_router + type: OS::Neutron::Router + router_0_gateway: + properties: + network_id: + get_param: router_0_external_network + router_id: + get_resource: router_0 + type: OS::Neutron::RouterGateway + router_0_interface_3: + properties: + router_id: + get_resource: router_0 + subnet_id: + get_resource: subnet_0 + type: OS::Neutron::RouterInterface + security_group_0: + properties: + name: _default + rules: + - direction: ingress + ethertype: IPv4 + remote_mode: remote_group_id + - direction: ingress + ethertype: IPv6 + remote_mode: remote_group_id + - direction: egress + ethertype: IPv4 + remote_ip_prefix: 0.0.0.0/0 + - direction: egress + ethertype: IPv6 + remote_ip_prefix: ::/0 + type: OS::Neutron::SecurityGroup + security_group_1: + properties: + description: '' + name: http + rules: + - direction: egress + ethertype: IPv4 + remote_ip_prefix: 0.0.0.0/0 + - direction: egress + ethertype: IPv6 + - direction: ingress + ethertype: IPv4 + port_range_max: 80 + port_range_min: 80 + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + type: OS::Neutron::SecurityGroup + server_0: + properties: + block_device_mapping_v2: + - device_name: /dev/vda + volume_id: + get_resource: volume_0 + flavor: + get_param: server_0_flavor + key_name: + get_resource: keypair_0 + name: Ubuntu 14.04 + networks: + - network: + get_resource: network_1 + security_groups: + - get_resource: security_group_1 + - get_resource: security_group_0 + type: OS::Nova::Server + server_1: + properties: + flavor: + get_param: server_1_flavor + image: + get_param: server_1_image + key_name: + get_resource: keypair_0 + name: tellurium_win_instance + networks: + - network: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Nova::Server + server_2: + properties: + flavor: + get_param: server_2_flavor + image: + get_param: server_2_image + key_name: + get_resource: keypair_1 + name: tellurium_win_instance_ssh_pass + networks: + - network: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Nova::Server + server_3: + properties: + flavor: + get_param: server_3_flavor + image: + get_param: server_3_image + key_name: + get_resource: keypair_0 + name: tellurium_instance + networks: + - network: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Nova::Server + subnet_0: + properties: + allocation_pools: + - end: 192.168.0.254 + start: 192.168.0.2 + cidr: 192.168.0.0/24 + dns_nameservers: + - 8.8.8.8 + enable_dhcp: true + host_routes: [] + ip_version: 4 + name: tellurium_net_subnet + network_id: + get_resource: network_1 + type: OS::Neutron::Subnet + volume_0: + properties: + image: + get_param: volume_0_image + metadata: + attached_mode: rw + readonly: 'False' + name: Ubuntu 14.04 + size: 20 + volume_type: + get_param: volume_0_volume_type + type: OS::Cinder::Volume + volume_1: + properties: + name: tellurium_volume + size: 5 + volume_type: + get_param: volume_1_volume_type + type: OS::Cinder::Volume diff --git a/flameclient/tests/fixtures/results/flame_adoption_data.yaml b/flameclient/tests/fixtures/results/flame_adoption_data.yaml new file mode 100644 index 0000000..202fd69 --- /dev/null +++ b/flameclient/tests/fixtures/results/flame_adoption_data.yaml @@ -0,0 +1,351 @@ +action: CREATE +environment: + parameter_defaults: {} + parameters: {} +resources: + floating_ip_0: + action: CREATE + metadata: {} + name: floating_ip_0 + resource_data: {} + resource_id: e6f50641-6c7e-468c-9623-1fd5ee2c1ebc + status: COMPLETE + type: OS::Neutron::FloatingIP + keypair_0: + action: CREATE + metadata: {} + name: keypair_0 + resource_data: {} + resource_id: tellurium-key + status: COMPLETE + type: OS::Nova::KeyPair + keypair_1: + action: CREATE + metadata: {} + name: keypair_1 + resource_data: {} + resource_id: tellurium-key-phrase + status: COMPLETE + type: OS::Nova::KeyPair + network_1: + action: CREATE + metadata: {} + name: network_1 + resource_data: {} + resource_id: f054013d-7052-4708-9c72-2948a329fac3 + status: COMPLETE + type: OS::Neutron::Net + router_0: + action: CREATE + metadata: {} + name: router_0 + resource_data: {} + resource_id: ebc6dd0c-a276-4368-8084-bd37c587cc24 + status: COMPLETE + type: OS::Neutron::Router + router_0_gateway: + action: CREATE + metadata: {} + name: router_0_gateway + resource_data: {} + resource_id: ebc6dd0c-a276-4368-8084-bd37c587cc24:6ea98324-0f14-49f6-97c0-885d1b8dc517 + status: COMPLETE + type: OS::Neutron::RouterGateway + router_0_interface_3: + action: CREATE + metadata: {} + name: router_0_interface_3 + resource_data: {} + resource_id: ebc6dd0c-a276-4368-8084-bd37c587cc24:subnet_id=541f0782-587f-428b-bd79-ca227a66973b + status: COMPLETE + type: OS::Neutron::RouterInterface + security_group_1: + action: CREATE + metadata: {} + name: security_group_1 + resource_data: {} + resource_id: 156799a3-565e-48b3-938c-f95f09093c66 + status: COMPLETE + type: OS::Neutron::SecurityGroup + server_0: + action: CREATE + metadata: {} + name: server_0 + resource_data: {} + resource_id: 99c42e21-5099-4903-9121-063925aad299 + status: COMPLETE + type: OS::Nova::Server + server_1: + action: CREATE + metadata: {} + name: server_1 + resource_data: {} + resource_id: 2cf8db13-312a-4307-ba38-43b727ebcce6 + status: COMPLETE + type: OS::Nova::Server + server_2: + action: CREATE + metadata: {} + name: server_2 + resource_data: {} + resource_id: b6316031-e629-48b8-aac5-3f1b21ffe0f3 + status: COMPLETE + type: OS::Nova::Server + server_3: + action: CREATE + metadata: {} + name: server_3 + resource_data: {} + resource_id: 4add78fa-93df-4550-aaf7-aba2239ba00a + status: COMPLETE + type: OS::Nova::Server + subnet_0: + action: CREATE + metadata: {} + name: subnet_0 + resource_data: {} + resource_id: 541f0782-587f-428b-bd79-ca227a66973b + status: COMPLETE + type: OS::Neutron::Subnet + volume_0: + action: CREATE + metadata: {} + name: volume_0 + resource_data: {} + resource_id: 8ca10346-fe4c-4e68-9a18-98df875d1ecc + status: COMPLETE + type: OS::Cinder::Volume + volume_1: + action: CREATE + metadata: {} + name: volume_1 + resource_data: {} + resource_id: 34ce951a-f2d9-4bdd-904d-9f70269c680b + status: COMPLETE + type: OS::Cinder::Volume +status: COMPLETE +template: + description: Generated template + heat_template_version: 2013-05-23 + parameters: + external_network_for_floating_ip_0: + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Network to allocate floating IP from + type: string + router_0_external_network: + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Router external network + type: string + server_0_default_security_group: + default: 9ffd2654-7ca6-48ae-852d-6503d5ce4a60 + description: Default security group for server Ubuntu 14.04 + type: string + server_0_flavor: + default: '17' + description: Flavor to use for server server_0 + type: string + server_1_default_security_group: + default: 9ffd2654-7ca6-48ae-852d-6503d5ce4a60 + description: Default security group for server tellurium_win_instance + type: string + server_1_flavor: + default: '42' + description: Flavor to use for server server_1 + type: string + server_1_image: + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_1 + type: string + server_2_default_security_group: + default: 9ffd2654-7ca6-48ae-852d-6503d5ce4a60 + description: Default security group for server tellurium_win_instance_ssh_pass + type: string + server_2_flavor: + default: '42' + description: Flavor to use for server server_2 + type: string + server_2_image: + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_2 + type: string + server_3_default_security_group: + default: 9ffd2654-7ca6-48ae-852d-6503d5ce4a60 + description: Default security group for server tellurium_instance + type: string + server_3_flavor: + default: '16' + description: Flavor to use for server server_3 + type: string + server_3_image: + default: dd7d4b21-79b8-42f0-8464-0d1a5274c638 + description: Image to use for server server_3 + type: string + volume_0_image: + default: 70a9c910-dd99-4065-bce9-11e89bc479fe + description: Image to create volume volume_0 from + type: string + volume_0_volume_type: + default: standard + description: Volume type for volume volume_0 + type: string + volume_1_volume_type: + default: standard + description: Volume type for volume volume_1 + type: string + resources: + floating_ip_0: + properties: + floating_network_id: + get_param: external_network_for_floating_ip_0 + type: OS::Neutron::FloatingIP + keypair_0: + properties: + name: tellurium-key + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwp5kVZ+3baPZllNXZDG2mivd5nJ5wWY6Jj/WV6NlO6cUiaH5om6itU3lyJxtAbLgbbvY2FjMg1PI2JI3EHx0OSPEbDdeNsQGi31qyuiB1S5p6TreI0Dfy0tywJ9G2CURjkuJnC8SvnMfVYLMFBvx7p8RzxSdDm/zrmc4KY3ktdfYQDNtEiH2jucUUiY0ipVkDNhhv03+5C9cnpaIcVDBkddE/KEME8NIIh7s6aYCXJEWJx85nOVVRD5qK7ouV6FcGVn6zqWRD3jn0iSxcFwiKx7p6M77PmJAY4gBIpWQmutok6T4ZrXxa7jE4dybwo5e8dyvGyc7WWGqXcmcinUWr + Generated-by-Nova + type: OS::Nova::KeyPair + keypair_1: + properties: + name: tellurium-key-phrase + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCaYaTC6KCC2GURFMpRQiRq8Og10PcBbVnrQqluzi2E5vAqTHAegzivmGB8h2xrjXUfbKV2bE8kz2ZE66sq1yLO/jZ+rkOA+zLnOxNwkk3gQq57b26ZegPL2tTgottJTPGyPz6v2+LLtDf/+xTJMjkPKcMWylu12Js0XKVkdY35fwN7fRvA4xghtGu1GcmS2XFDMmeLDrG1KNPbBLj5cGoD723Ho7ZhAjLThoY/xMN2OYsSNzrg3S00QngZMzQvJf0ETrB3GtITk5FUs54qMRfiyUC72lw0gLTIJK8rs7fX/yeO+467afRt2xwHN50rEWJPeTVYWdlH/msh7NQ3LZ8/ + root@9b638ff21113 + type: OS::Nova::KeyPair + network_1: + properties: + admin_state_up: true + name: tellurium_net + shared: false + type: OS::Neutron::Net + router_0: + properties: + admin_state_up: true + name: tellurium_router + type: OS::Neutron::Router + router_0_gateway: + properties: + network_id: + get_param: router_0_external_network + router_id: + get_resource: router_0 + type: OS::Neutron::RouterGateway + router_0_interface_3: + properties: + router_id: + get_resource: router_0 + subnet_id: + get_resource: subnet_0 + type: OS::Neutron::RouterInterface + security_group_1: + properties: + description: '' + name: http + rules: + - direction: egress + ethertype: IPv4 + remote_ip_prefix: 0.0.0.0/0 + - direction: egress + ethertype: IPv6 + - direction: ingress + ethertype: IPv4 + port_range_max: 80 + port_range_min: 80 + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + type: OS::Neutron::SecurityGroup + server_0: + properties: + block_device_mapping_v2: + - device_name: /dev/vda + volume_id: + get_resource: volume_0 + flavor: + get_param: server_0_flavor + key_name: + get_resource: keypair_0 + name: Ubuntu 14.04 + networks: + - network: + get_resource: network_1 + security_groups: + - get_resource: security_group_1 + - get_param: server_0_default_security_group + type: OS::Nova::Server + server_1: + properties: + flavor: + get_param: server_1_flavor + image: + get_param: server_1_image + key_name: + get_resource: keypair_0 + name: tellurium_win_instance + networks: + - network: + get_resource: network_1 + security_groups: + - get_param: server_1_default_security_group + type: OS::Nova::Server + server_2: + properties: + flavor: + get_param: server_2_flavor + image: + get_param: server_2_image + key_name: + get_resource: keypair_1 + name: tellurium_win_instance_ssh_pass + networks: + - network: + get_resource: network_1 + security_groups: + - get_param: server_2_default_security_group + type: OS::Nova::Server + server_3: + properties: + flavor: + get_param: server_3_flavor + image: + get_param: server_3_image + key_name: + get_resource: keypair_0 + name: tellurium_instance + networks: + - network: + get_resource: network_1 + security_groups: + - get_param: server_3_default_security_group + type: OS::Nova::Server + subnet_0: + properties: + allocation_pools: + - end: 192.168.0.254 + start: 192.168.0.2 + cidr: 192.168.0.0/24 + dns_nameservers: + - 8.8.8.8 + enable_dhcp: true + host_routes: [] + ip_version: 4 + name: tellurium_net_subnet + network_id: + get_resource: network_1 + type: OS::Neutron::Subnet + volume_0: + properties: + image: + get_param: volume_0_image + metadata: + attached_mode: rw + readonly: 'False' + name: Ubuntu 14.04 + size: 20 + volume_type: + get_param: volume_0_volume_type + type: OS::Cinder::Volume + volume_1: + properties: + name: tellurium_volume + size: 5 + volume_type: + get_param: volume_1_volume_type + type: OS::Cinder::Volume diff --git a/flameclient/tests/fixtures/results/flame_constraints.yaml b/flameclient/tests/fixtures/results/flame_constraints.yaml new file mode 100644 index 0000000..7e938db --- /dev/null +++ b/flameclient/tests/fixtures/results/flame_constraints.yaml @@ -0,0 +1,237 @@ +description: Generated template +heat_template_version: 2013-05-23 +parameters: + external_network_for_floating_ip_0: + constraints: + - custom_constraint: neutron.network + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Network to allocate floating IP from + type: string + router_0_external_network: + constraints: + - custom_constraint: neutron.network + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Router external network + type: string + server_0_flavor: + default: '17' + description: Flavor to use for server server_0 + type: string + server_1_flavor: + default: '42' + description: Flavor to use for server server_1 + type: string + server_1_image: + constraints: + - custom_constraint: glance.image + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_1 + type: string + server_2_flavor: + default: '42' + description: Flavor to use for server server_2 + type: string + server_2_image: + constraints: + - custom_constraint: glance.image + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_2 + type: string + server_3_flavor: + default: '16' + description: Flavor to use for server server_3 + type: string + server_3_image: + constraints: + - custom_constraint: glance.image + default: dd7d4b21-79b8-42f0-8464-0d1a5274c638 + description: Image to use for server server_3 + type: string + volume_0_image: + constraints: + - custom_constraint: glance.image + default: 70a9c910-dd99-4065-bce9-11e89bc479fe + description: Image to create volume volume_0 from + type: string + volume_0_volume_type: + default: standard + description: Volume type for volume volume_0 + type: string + volume_1_volume_type: + default: standard + description: Volume type for volume volume_1 + type: string +resources: + floating_ip_0: + properties: + floating_network_id: + get_param: external_network_for_floating_ip_0 + type: OS::Neutron::FloatingIP + keypair_0: + properties: + name: tellurium-key + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwp5kVZ+3baPZllNXZDG2mivd5nJ5wWY6Jj/WV6NlO6cUiaH5om6itU3lyJxtAbLgbbvY2FjMg1PI2JI3EHx0OSPEbDdeNsQGi31qyuiB1S5p6TreI0Dfy0tywJ9G2CURjkuJnC8SvnMfVYLMFBvx7p8RzxSdDm/zrmc4KY3ktdfYQDNtEiH2jucUUiY0ipVkDNhhv03+5C9cnpaIcVDBkddE/KEME8NIIh7s6aYCXJEWJx85nOVVRD5qK7ouV6FcGVn6zqWRD3jn0iSxcFwiKx7p6M77PmJAY4gBIpWQmutok6T4ZrXxa7jE4dybwo5e8dyvGyc7WWGqXcmcinUWr + Generated-by-Nova + type: OS::Nova::KeyPair + keypair_1: + properties: + name: tellurium-key-phrase + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCaYaTC6KCC2GURFMpRQiRq8Og10PcBbVnrQqluzi2E5vAqTHAegzivmGB8h2xrjXUfbKV2bE8kz2ZE66sq1yLO/jZ+rkOA+zLnOxNwkk3gQq57b26ZegPL2tTgottJTPGyPz6v2+LLtDf/+xTJMjkPKcMWylu12Js0XKVkdY35fwN7fRvA4xghtGu1GcmS2XFDMmeLDrG1KNPbBLj5cGoD723Ho7ZhAjLThoY/xMN2OYsSNzrg3S00QngZMzQvJf0ETrB3GtITk5FUs54qMRfiyUC72lw0gLTIJK8rs7fX/yeO+467afRt2xwHN50rEWJPeTVYWdlH/msh7NQ3LZ8/ + root@9b638ff21113 + type: OS::Nova::KeyPair + network_1: + properties: + admin_state_up: true + name: tellurium_net + shared: false + type: OS::Neutron::Net + router_0: + properties: + admin_state_up: true + name: tellurium_router + type: OS::Neutron::Router + router_0_gateway: + properties: + network_id: + get_param: router_0_external_network + router_id: + get_resource: router_0 + type: OS::Neutron::RouterGateway + router_0_interface_3: + properties: + router_id: + get_resource: router_0 + subnet_id: + get_resource: subnet_0 + type: OS::Neutron::RouterInterface + security_group_0: + properties: + name: _default + rules: + - direction: ingress + ethertype: IPv4 + remote_mode: remote_group_id + - direction: ingress + ethertype: IPv6 + remote_mode: remote_group_id + - direction: egress + ethertype: IPv4 + remote_ip_prefix: 0.0.0.0/0 + - direction: egress + ethertype: IPv6 + remote_ip_prefix: ::/0 + type: OS::Neutron::SecurityGroup + security_group_1: + properties: + description: '' + name: http + rules: + - direction: egress + ethertype: IPv4 + remote_ip_prefix: 0.0.0.0/0 + - direction: egress + ethertype: IPv6 + - direction: ingress + ethertype: IPv4 + port_range_max: 80 + port_range_min: 80 + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + type: OS::Neutron::SecurityGroup + server_0: + properties: + block_device_mapping_v2: + - device_name: /dev/vda + volume_id: + get_resource: volume_0 + flavor: + get_param: server_0_flavor + key_name: + get_resource: keypair_0 + name: Ubuntu 14.04 + networks: + - network: + get_resource: network_1 + security_groups: + - get_resource: security_group_1 + - get_resource: security_group_0 + type: OS::Nova::Server + server_1: + properties: + flavor: + get_param: server_1_flavor + image: + get_param: server_1_image + key_name: + get_resource: keypair_0 + name: tellurium_win_instance + networks: + - network: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Nova::Server + server_2: + properties: + flavor: + get_param: server_2_flavor + image: + get_param: server_2_image + key_name: + get_resource: keypair_1 + name: tellurium_win_instance_ssh_pass + networks: + - network: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Nova::Server + server_3: + properties: + flavor: + get_param: server_3_flavor + image: + get_param: server_3_image + key_name: + get_resource: keypair_0 + name: tellurium_instance + networks: + - network: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Nova::Server + subnet_0: + properties: + allocation_pools: + - end: 192.168.0.254 + start: 192.168.0.2 + cidr: 192.168.0.0/24 + dns_nameservers: + - 8.8.8.8 + enable_dhcp: true + host_routes: [] + ip_version: 4 + name: tellurium_net_subnet + network_id: + get_resource: network_1 + type: OS::Neutron::Subnet + volume_0: + properties: + image: + get_param: volume_0_image + metadata: + attached_mode: rw + readonly: 'False' + name: Ubuntu 14.04 + size: 20 + volume_type: + get_param: volume_0_volume_type + type: OS::Cinder::Volume + volume_1: + properties: + name: tellurium_volume + size: 5 + volume_type: + get_param: volume_1_volume_type + type: OS::Cinder::Volume diff --git a/flameclient/tests/fixtures/results/flame_constraints_extract_ports.yaml b/flameclient/tests/fixtures/results/flame_constraints_extract_ports.yaml new file mode 100644 index 0000000..11b6b8f --- /dev/null +++ b/flameclient/tests/fixtures/results/flame_constraints_extract_ports.yaml @@ -0,0 +1,289 @@ +description: Generated template +heat_template_version: 2013-05-23 +parameters: + external_network_for_floating_ip_0: + constraints: + - custom_constraint: neutron.network + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Network to allocate floating IP from + type: string + router_0_external_network: + constraints: + - custom_constraint: neutron.network + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Router external network + type: string + server_0_flavor: + default: '17' + description: Flavor to use for server server_0 + type: string + server_1_flavor: + default: '42' + description: Flavor to use for server server_1 + type: string + server_1_image: + constraints: + - custom_constraint: glance.image + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_1 + type: string + server_2_flavor: + default: '42' + description: Flavor to use for server server_2 + type: string + server_2_image: + constraints: + - custom_constraint: glance.image + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_2 + type: string + server_3_flavor: + default: '16' + description: Flavor to use for server server_3 + type: string + server_3_image: + constraints: + - custom_constraint: glance.image + default: dd7d4b21-79b8-42f0-8464-0d1a5274c638 + description: Image to use for server server_3 + type: string + volume_0_image: + constraints: + - custom_constraint: glance.image + default: 70a9c910-dd99-4065-bce9-11e89bc479fe + description: Image to create volume volume_0 from + type: string + volume_0_volume_type: + default: standard + description: Volume type for volume volume_0 + type: string + volume_1_volume_type: + default: standard + description: Volume type for volume volume_1 + type: string +resources: + floating_ip_0: + properties: + floating_network_id: + get_param: external_network_for_floating_ip_0 + type: OS::Neutron::FloatingIP + keypair_0: + properties: + name: tellurium-key + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwp5kVZ+3baPZllNXZDG2mivd5nJ5wWY6Jj/WV6NlO6cUiaH5om6itU3lyJxtAbLgbbvY2FjMg1PI2JI3EHx0OSPEbDdeNsQGi31qyuiB1S5p6TreI0Dfy0tywJ9G2CURjkuJnC8SvnMfVYLMFBvx7p8RzxSdDm/zrmc4KY3ktdfYQDNtEiH2jucUUiY0ipVkDNhhv03+5C9cnpaIcVDBkddE/KEME8NIIh7s6aYCXJEWJx85nOVVRD5qK7ouV6FcGVn6zqWRD3jn0iSxcFwiKx7p6M77PmJAY4gBIpWQmutok6T4ZrXxa7jE4dybwo5e8dyvGyc7WWGqXcmcinUWr + Generated-by-Nova + type: OS::Nova::KeyPair + keypair_1: + properties: + name: tellurium-key-phrase + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCaYaTC6KCC2GURFMpRQiRq8Og10PcBbVnrQqluzi2E5vAqTHAegzivmGB8h2xrjXUfbKV2bE8kz2ZE66sq1yLO/jZ+rkOA+zLnOxNwkk3gQq57b26ZegPL2tTgottJTPGyPz6v2+LLtDf/+xTJMjkPKcMWylu12Js0XKVkdY35fwN7fRvA4xghtGu1GcmS2XFDMmeLDrG1KNPbBLj5cGoD723Ho7ZhAjLThoY/xMN2OYsSNzrg3S00QngZMzQvJf0ETrB3GtITk5FUs54qMRfiyUC72lw0gLTIJK8rs7fX/yeO+467afRt2xwHN50rEWJPeTVYWdlH/msh7NQ3LZ8/ + root@9b638ff21113 + type: OS::Nova::KeyPair + network_1: + properties: + admin_state_up: true + name: tellurium_net + shared: false + type: OS::Neutron::Net + port_0: + properties: + admin_state_up: true + device_owner: compute:None + fixed_ips: + - ip_address: 192.168.0.3 + subnet_id: + get_resource: subnet_0 + mac_address: 02:3d:ec:f6:f8:25 + name: Tellurium_Fixtures-windows_port-hxvnswbzi56x + network_id: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Neutron::Port + port_1: + properties: + admin_state_up: true + device_owner: compute:None + fixed_ips: + - ip_address: 192.168.0.4 + subnet_id: + get_resource: subnet_0 + mac_address: 02:20:6d:f7:16:da + name: Tellurium_Fixtures-instance_port-fmln6fjl4yvo + network_id: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Neutron::Port + port_2: + properties: + admin_state_up: true + device_owner: compute:None + fixed_ips: + - ip_address: 192.168.0.5 + subnet_id: + get_resource: subnet_0 + mac_address: 02:cb:33:6b:60:f2 + name: Tellurium_Fixtures-windows_ssh_pass_port-4zlwanbetsg2 + network_id: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Neutron::Port + port_4: + properties: + admin_state_up: true + device_owner: compute:nova + fixed_ips: + - ip_address: 192.168.0.6 + subnet_id: + get_resource: subnet_0 + mac_address: 02:4f:41:34:32:b4 + name: 4f413432-b499-4fcc-a81a-1ee0d8bc16b7 + network_id: + get_resource: network_1 + security_groups: + - get_resource: security_group_1 + - get_resource: security_group_0 + type: OS::Neutron::Port + router_0: + properties: + admin_state_up: true + name: tellurium_router + type: OS::Neutron::Router + router_0_gateway: + properties: + network_id: + get_param: router_0_external_network + router_id: + get_resource: router_0 + type: OS::Neutron::RouterGateway + router_0_interface_3: + properties: + router_id: + get_resource: router_0 + subnet_id: + get_resource: subnet_0 + type: OS::Neutron::RouterInterface + security_group_0: + properties: + name: _default + rules: + - direction: ingress + ethertype: IPv4 + remote_mode: remote_group_id + - direction: ingress + ethertype: IPv6 + remote_mode: remote_group_id + - direction: egress + ethertype: IPv4 + remote_ip_prefix: 0.0.0.0/0 + - direction: egress + ethertype: IPv6 + remote_ip_prefix: ::/0 + type: OS::Neutron::SecurityGroup + security_group_1: + properties: + description: '' + name: http + rules: + - direction: egress + ethertype: IPv4 + remote_ip_prefix: 0.0.0.0/0 + - direction: egress + ethertype: IPv6 + - direction: ingress + ethertype: IPv4 + port_range_max: 80 + port_range_min: 80 + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + type: OS::Neutron::SecurityGroup + server_0: + properties: + block_device_mapping_v2: + - device_name: /dev/vda + volume_id: + get_resource: volume_0 + flavor: + get_param: server_0_flavor + key_name: + get_resource: keypair_0 + name: Ubuntu 14.04 + networks: + - port: + get_resource: port_4 + type: OS::Nova::Server + server_1: + properties: + flavor: + get_param: server_1_flavor + image: + get_param: server_1_image + key_name: + get_resource: keypair_0 + name: tellurium_win_instance + networks: + - port: + get_resource: port_0 + type: OS::Nova::Server + server_2: + properties: + flavor: + get_param: server_2_flavor + image: + get_param: server_2_image + key_name: + get_resource: keypair_1 + name: tellurium_win_instance_ssh_pass + networks: + - port: + get_resource: port_2 + type: OS::Nova::Server + server_3: + properties: + flavor: + get_param: server_3_flavor + image: + get_param: server_3_image + key_name: + get_resource: keypair_0 + name: tellurium_instance + networks: + - port: + get_resource: port_1 + type: OS::Nova::Server + subnet_0: + properties: + allocation_pools: + - end: 192.168.0.254 + start: 192.168.0.2 + cidr: 192.168.0.0/24 + dns_nameservers: + - 8.8.8.8 + enable_dhcp: true + host_routes: [] + ip_version: 4 + name: tellurium_net_subnet + network_id: + get_resource: network_1 + type: OS::Neutron::Subnet + volume_0: + properties: + image: + get_param: volume_0_image + metadata: + attached_mode: rw + readonly: 'False' + name: Ubuntu 14.04 + size: 20 + volume_type: + get_param: volume_0_volume_type + type: OS::Cinder::Volume + volume_1: + properties: + name: tellurium_volume + size: 5 + volume_type: + get_param: volume_1_volume_type + type: OS::Cinder::Volume diff --git a/flameclient/tests/fixtures/results/flame_extract_ports.yaml b/flameclient/tests/fixtures/results/flame_extract_ports.yaml new file mode 100644 index 0000000..0c30b6c --- /dev/null +++ b/flameclient/tests/fixtures/results/flame_extract_ports.yaml @@ -0,0 +1,277 @@ +description: Generated template +heat_template_version: 2013-05-23 +parameters: + external_network_for_floating_ip_0: + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Network to allocate floating IP from + type: string + router_0_external_network: + default: 6ea98324-0f14-49f6-97c0-885d1b8dc517 + description: Router external network + type: string + server_0_flavor: + default: '17' + description: Flavor to use for server server_0 + type: string + server_1_flavor: + default: '42' + description: Flavor to use for server server_1 + type: string + server_1_image: + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_1 + type: string + server_2_flavor: + default: '42' + description: Flavor to use for server server_2 + type: string + server_2_image: + default: 8254d703-e46f-4101-88ff-6f40bf7df51a + description: Image to use for server server_2 + type: string + server_3_flavor: + default: '16' + description: Flavor to use for server server_3 + type: string + server_3_image: + default: dd7d4b21-79b8-42f0-8464-0d1a5274c638 + description: Image to use for server server_3 + type: string + volume_0_image: + default: 70a9c910-dd99-4065-bce9-11e89bc479fe + description: Image to create volume volume_0 from + type: string + volume_0_volume_type: + default: standard + description: Volume type for volume volume_0 + type: string + volume_1_volume_type: + default: standard + description: Volume type for volume volume_1 + type: string +resources: + floating_ip_0: + properties: + floating_network_id: + get_param: external_network_for_floating_ip_0 + type: OS::Neutron::FloatingIP + keypair_0: + properties: + name: tellurium-key + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwp5kVZ+3baPZllNXZDG2mivd5nJ5wWY6Jj/WV6NlO6cUiaH5om6itU3lyJxtAbLgbbvY2FjMg1PI2JI3EHx0OSPEbDdeNsQGi31qyuiB1S5p6TreI0Dfy0tywJ9G2CURjkuJnC8SvnMfVYLMFBvx7p8RzxSdDm/zrmc4KY3ktdfYQDNtEiH2jucUUiY0ipVkDNhhv03+5C9cnpaIcVDBkddE/KEME8NIIh7s6aYCXJEWJx85nOVVRD5qK7ouV6FcGVn6zqWRD3jn0iSxcFwiKx7p6M77PmJAY4gBIpWQmutok6T4ZrXxa7jE4dybwo5e8dyvGyc7WWGqXcmcinUWr + Generated-by-Nova + type: OS::Nova::KeyPair + keypair_1: + properties: + name: tellurium-key-phrase + public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCaYaTC6KCC2GURFMpRQiRq8Og10PcBbVnrQqluzi2E5vAqTHAegzivmGB8h2xrjXUfbKV2bE8kz2ZE66sq1yLO/jZ+rkOA+zLnOxNwkk3gQq57b26ZegPL2tTgottJTPGyPz6v2+LLtDf/+xTJMjkPKcMWylu12Js0XKVkdY35fwN7fRvA4xghtGu1GcmS2XFDMmeLDrG1KNPbBLj5cGoD723Ho7ZhAjLThoY/xMN2OYsSNzrg3S00QngZMzQvJf0ETrB3GtITk5FUs54qMRfiyUC72lw0gLTIJK8rs7fX/yeO+467afRt2xwHN50rEWJPeTVYWdlH/msh7NQ3LZ8/ + root@9b638ff21113 + type: OS::Nova::KeyPair + network_1: + properties: + admin_state_up: true + name: tellurium_net + shared: false + type: OS::Neutron::Net + port_0: + properties: + admin_state_up: true + device_owner: compute:None + fixed_ips: + - ip_address: 192.168.0.3 + subnet_id: + get_resource: subnet_0 + mac_address: 02:3d:ec:f6:f8:25 + name: Tellurium_Fixtures-windows_port-hxvnswbzi56x + network_id: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Neutron::Port + port_1: + properties: + admin_state_up: true + device_owner: compute:None + fixed_ips: + - ip_address: 192.168.0.4 + subnet_id: + get_resource: subnet_0 + mac_address: 02:20:6d:f7:16:da + name: Tellurium_Fixtures-instance_port-fmln6fjl4yvo + network_id: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Neutron::Port + port_2: + properties: + admin_state_up: true + device_owner: compute:None + fixed_ips: + - ip_address: 192.168.0.5 + subnet_id: + get_resource: subnet_0 + mac_address: 02:cb:33:6b:60:f2 + name: Tellurium_Fixtures-windows_ssh_pass_port-4zlwanbetsg2 + network_id: + get_resource: network_1 + security_groups: + - get_resource: security_group_0 + type: OS::Neutron::Port + port_4: + properties: + admin_state_up: true + device_owner: compute:nova + fixed_ips: + - ip_address: 192.168.0.6 + subnet_id: + get_resource: subnet_0 + mac_address: 02:4f:41:34:32:b4 + name: 4f413432-b499-4fcc-a81a-1ee0d8bc16b7 + network_id: + get_resource: network_1 + security_groups: + - get_resource: security_group_1 + - get_resource: security_group_0 + type: OS::Neutron::Port + router_0: + properties: + admin_state_up: true + name: tellurium_router + type: OS::Neutron::Router + router_0_gateway: + properties: + network_id: + get_param: router_0_external_network + router_id: + get_resource: router_0 + type: OS::Neutron::RouterGateway + router_0_interface_3: + properties: + router_id: + get_resource: router_0 + subnet_id: + get_resource: subnet_0 + type: OS::Neutron::RouterInterface + security_group_0: + properties: + name: _default + rules: + - direction: ingress + ethertype: IPv4 + remote_mode: remote_group_id + - direction: ingress + ethertype: IPv6 + remote_mode: remote_group_id + - direction: egress + ethertype: IPv4 + remote_ip_prefix: 0.0.0.0/0 + - direction: egress + ethertype: IPv6 + remote_ip_prefix: ::/0 + type: OS::Neutron::SecurityGroup + security_group_1: + properties: + description: '' + name: http + rules: + - direction: egress + ethertype: IPv4 + remote_ip_prefix: 0.0.0.0/0 + - direction: egress + ethertype: IPv6 + - direction: ingress + ethertype: IPv4 + port_range_max: 80 + port_range_min: 80 + protocol: tcp + remote_ip_prefix: 0.0.0.0/0 + type: OS::Neutron::SecurityGroup + server_0: + properties: + block_device_mapping_v2: + - device_name: /dev/vda + volume_id: + get_resource: volume_0 + flavor: + get_param: server_0_flavor + key_name: + get_resource: keypair_0 + name: Ubuntu 14.04 + networks: + - port: + get_resource: port_4 + type: OS::Nova::Server + server_1: + properties: + flavor: + get_param: server_1_flavor + image: + get_param: server_1_image + key_name: + get_resource: keypair_0 + name: tellurium_win_instance + networks: + - port: + get_resource: port_0 + type: OS::Nova::Server + server_2: + properties: + flavor: + get_param: server_2_flavor + image: + get_param: server_2_image + key_name: + get_resource: keypair_1 + name: tellurium_win_instance_ssh_pass + networks: + - port: + get_resource: port_2 + type: OS::Nova::Server + server_3: + properties: + flavor: + get_param: server_3_flavor + image: + get_param: server_3_image + key_name: + get_resource: keypair_0 + name: tellurium_instance + networks: + - port: + get_resource: port_1 + type: OS::Nova::Server + subnet_0: + properties: + allocation_pools: + - end: 192.168.0.254 + start: 192.168.0.2 + cidr: 192.168.0.0/24 + dns_nameservers: + - 8.8.8.8 + enable_dhcp: true + host_routes: [] + ip_version: 4 + name: tellurium_net_subnet + network_id: + get_resource: network_1 + type: OS::Neutron::Subnet + volume_0: + properties: + image: + get_param: volume_0_image + metadata: + attached_mode: rw + readonly: 'False' + name: Ubuntu 14.04 + size: 20 + volume_type: + get_param: volume_0_volume_type + type: OS::Cinder::Volume + volume_1: + properties: + name: tellurium_volume + size: 5 + volume_type: + get_param: volume_1_volume_type + type: OS::Cinder::Volume diff --git a/flameclient/tests/fixtures/utils.py b/flameclient/tests/fixtures/utils.py new file mode 100644 index 0000000..5218204 --- /dev/null +++ b/flameclient/tests/fixtures/utils.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +from pprint import pformat + +import six + +from flameclient.utils import get_deep_attr +from flameclient.utils import munchify + + +def get_licence(): + import flameclient + licence_filename = os.path.join( + os.path.realpath(os.path.dirname(flameclient.__file__)), + 'LICENSE.txt' + ) + with open(licence_filename) as fp: + return fp.read() + + +def get_python_header(): + top = '# -*- coding: utf-8 -*-' + lines = get_licence().strip().split('\n') + commented_lines = ['# %s' % line.strip() for line in lines] + commented_lines = [line.strip() for line in commented_lines] + commented_licence = '\n'.join(commented_lines) + return '%s\n\n%s\n' % (top, commented_licence) + + +def write_openstackcloud_fixture_data(filename, attr_name, data): + """Write a fixtures file with given data + + :param str filename: filename holding the fixtures. + :param str attr_name: attribute name of the `connection` parameter. + + :param list data: a python list of data which needs to be returned + + """ + fixtures = [vars(munchify(element)) for element in data] + + openstackcloud_dir = os.path.join( + os.path.realpath(os.path.dirname(__file__)), + 'openstackcloud' + ) + + if not filename.startswith('/'): + filename = os.path.join(openstackcloud_dir, filename) + + head = get_python_header() + + file_str = "%s\n\nNAME = '%s'\n\nFIXTURES = %s\n" % ( + head, attr_name, pformat(fixtures, indent=1) + ) + with open(filename, 'w') as fp: + fp.write(file_str) + + +def write_openstackcloud_fixture(filename, attr_name, connection): + """Write a fixtures API call file + + :param str filename: filename holding the fixtures. + :param str attr_name: attribute name of the `connection` parameter. + + :param openstack.connection.Connection connection: + An `openstack.connection.Connection` instance (`openstacksdk`). + Since `shade.openstackcloud.OpenStackCloud` (`shade`) is a subclass + you can also use shade instances. + + ex. to write connection.network.security_groups to a fixtures file: + + write_openstackcloud_fixture( + 'flameclient/tests/fixtures/openstackcloud/security_groups.py', + 'network.security_groups', + connection + ) + + """ + method = get_deep_attr(connection, attr_name) + write_openstackcloud_fixture_data(filename, attr_name, method()) + + +def rewrite_all_openstackcloud_fixtures(connection): + """Rewrite all fixtures in flameclient.tests.fixtures + + This function rewrites all fixture files present in + flameclient.tests.fixtures from an existing openstack session. + + :param openstack.connection.Connection connection: + An `openstack.connection.Connection` instance (`openstacksdk`). + Since `shade.openstackcloud.OpenStackCloud` (`shade`) is a subclass + you can also use shade instances. + + DANGEROUS! You can break everything!!! + """ + from flameclient.tests.fixtures import openstackcloud + for fixture_module in six.itervalues(openstackcloud.FIXTURES): + write_openstackcloud_fixture( + fixture_module.__file__, + fixture_module.NAME, + connection + ) diff --git a/flameclient/tests/test_flame.py b/flameclient/tests/test_flame.py index dbca7b7..ff4195c 100644 --- a/flameclient/tests/test_flame.py +++ b/flameclient/tests/test_flame.py @@ -2,7 +2,7 @@ # This software is released under the MIT License. # -# Copyright (c) 2014 Cloudwatt +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,2902 +22,90 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import os -import mock -import yaml - -from flameclient import client as flame_client from flameclient import flame -from flameclient.tests import base +from flameclient.tests.fixtures import FIXTURES_DIR +from flameclient.tests import unittest +from flameclient.tests.utils import get_mocked_openstackcloud -class FakeBase(object): - - def __init__(self, **kwargs): - for key, value in kwargs.items(): - setattr(self, key, value) - - -class FakeVolume(FakeBase): - id = 1234 - size = 1 - source_volid = None - bootable = 'false' - snapshot_id = None - display_name = 'vol1' - display_description = 'Description' - volume_type = 'fast' - metadata = None - - -class FakeUnnamedVolume(FakeBase): - id = 1234 - size = 1 - source_volid = None - bootable = 'false' - snapshot_id = None - volume_type = 'fast' - metadata = None - - -class FakeServer(FakeBase): - id = '1234' - name = 'server1' - config_drive = None - flavor = {'id': '2'} - image = {'id': '3333', - 'links': [{'href': 'http://p/7777/images/3333', - 'rel': 'bookmark'}]} - key_name = 'testkey' - addresses = [] - metadata = None - - def __init__(self, **kwargs): - kwargs.setdefault('OS-DCF:diskConfig', 'MANUAL') - kwargs.setdefault('os-extended-volumes:volumes_attached', []) - super(FakeServer, self).__init__(**kwargs) - - -class FakeFlavor(FakeBase): - name = 'm1.tiny' - id = '1' - - -class FakeKeypair(FakeBase): - name = 'key' - id = 'key' - public_key = 'ssh-rsa AAAAB3NzaC' - - -class FakeSecurityGroup(FakeBase): - id = '1' - name = 'name' - - -class FakeServerGroup(FakeBase): - name = 'policy_group' - id = '1234' - policies = 'affinity' - members = ['12345'] - - -class FakeNeutronManager(object): - - def __init__(self): - self.groups = [] - self.routers = [{'name': 'myrouter', - 'id': '1234', - 'admin_state_up': 'true', - 'external_gateway_info': None}, ] - self.ports = [] - self.subnets = [] - self.networks = [{'status': 'ACTIVE', - 'subnets': ['1111'], - 'name': 'mynetwork', - 'router:external': False, - 'admin_state_up': True, - 'shared': False, - 'id': '2222'}, ] - self.floatingips = [] - - def subnet_list(self): - return self.subnets - - def network_list(self): - return self.networks - - def port_list(self): - return self.ports - - def router_list(self): - return self.routers - - def router_interfaces_list(self, router): - return self.ports - - def secgroup_list(self): - return self.groups - - def floatingip_list(self): - return self.floatingips - - -class FakeNovaManager(object): - - def __init__(self): - self.servers = [FakeServer()] - self.flavors = [FakeFlavor(id='2', name='m1.small')] - self.groups = {} - self.keypairs = [FakeKeypair(name='testkey', - public_key='ssh-rsa XXXX')] - self.servergroups = [FakeServerGroup()] - - def keypair_list(self): - return self.keypairs - - def flavor_list(self): - return self.flavors - - def server_list(self): - return self.servers - - def server_security_group_list(self, server): - return self.groups.get(server.name, []) - - def servergroup_list(self): - return self.servergroups - - -class FakeCinderManager(object): - - def __init__(self): - self.volumes = [FakeVolume(), ] - - def volume_list(self): - return self.volumes - - -class ResourceTestCase(base.TestCase): - - def test_template_resource(self): - resource = flame.Resource('my-name', - 'my-type', - properties='my-properties') - - expected = { - 'my-name': { - 'type': 'my-type', - 'properties': 'my-properties', - } - } - self.assertEqual(expected, resource.template_resource) - - def test_stack_resource(self): - resource = flame.Resource('my-name', 'my-type', id='my-id') - - expected = { - 'my-name': { - 'status': 'COMPLETE', - 'name': 'my-name', - 'resource_data': {}, - 'resource_id': 'my-id', - 'type': 'my-type', - 'metadata': {}, - 'action': 'CREATE' - } - } - self.assertEqual(expected, resource.stack_resource) - - def test_empty_stack_resource(self): - resource = flame.Resource('my-name', 'my-type') - self.assertEqual({}, resource.stack_resource) - - def test_add_parameter(self): - resource = flame.Resource('my-name', 'my-type') - resource.add_parameter('my-parameter', 'my-description') - - expected = { - 'my-parameter': { - 'type': 'string', - 'description': 'my-description' - } - } - self.assertEqual(expected, resource.template_parameter) - - def test_add_parameter_with_type(self): - resource = flame.Resource('my-name', 'my-type') - resource.add_parameter('my-parameter', 'my-description', - parameter_type='my-type') - - expected = { - 'my-parameter': { - 'type': 'my-type', - 'description': 'my-description' - } - } - self.assertEqual(expected, resource.template_parameter) - - def test_add_parameter_with_default(self): - resource = flame.Resource('my-name', 'my-type') - resource.add_parameter('my-parameter', 'my-description', - default='my-default') - - expected = { - 'my-parameter': { - 'type': 'string', - 'description': 'my-description', - 'default': 'my-default' - } - } - self.assertEqual(expected, resource.template_parameter) - - -class BaseTestCase(base.TestCase): - +class TestFlame(unittest.TestCase): def setUp(self): - super(BaseTestCase, self).setUp() - self.patch_neutron = mock.patch('flameclient.managers.NeutronManager') - self.mock_neutron = self.patch_neutron.start() - self.patch_nova = mock.patch('flameclient.managers.NovaManager') - self.mock_nova = self.patch_nova.start() - self.patch_cinder = mock.patch('flameclient.managers.CinderManager') - self.mock_cinder = self.patch_cinder.start() - self.patch_keystone = mock.patch( - 'flameclient.managers.KeystoneManager' + self.conn = get_mocked_openstackcloud() + + def test_flame(self): + res_file = os.path.join(FIXTURES_DIR, 'results/flame.yaml') + generator = flame.TemplateGenerator( + connection=self.conn, + options=dict( + no_threads=True + ) ) - self.mock_keystone = self.patch_keystone.start() - - def tearDown(self): - self.mock_neutron.stop() - self.mock_nova.stop() - self.mock_cinder.stop() - self.mock_keystone.stop() - super(BaseTestCase, self).tearDown() - - def get_generator(self, exclude_servers, exclude_volumes, - exclude_keypairs, generate_data, - extract_ports, exclude_secgroups=False): - generator = flame.TemplateGenerator('x', 'x', 'x', 'x', True, - 'publicURL') - generator.extract_vm_details(exclude_servers, exclude_volumes, - exclude_keypairs, generate_data, - extract_ports, exclude_secgroups) - return generator - - def check_stackdata(self, resources, expected_resources): - merged_resources = {} - for resource in resources: - merged_resources.update(resource.stack_resource) - - self.assertEqual(expected_resources, merged_resources) - - def check_template(self, resources, expected_resources, - expected_parameters=None): - - expected_parameters = expected_parameters or {} - merged_resources = {} - merged_parameters = {} - for resource in resources: - merged_resources.update(resource.template_resource) - merged_parameters.update(resource.template_parameter) - - self.assertEqual(expected_resources, merged_resources) - self.assertEqual(expected_parameters, merged_parameters) - - -class TemplateGenerationTest(BaseTestCase): - def test_heat_template_and_data_with_data(self): - generator = self.get_generator(False, False, False, True, False) generator.extract_data() - out = yaml.load(generator.heat_template_and_data()) - mandatory_keys = set(('environment', 'template', 'resources', - 'action', 'status')) - self.assertEqual(mandatory_keys, set(out.keys())) - self.assertEqual(generator.template, out['template']) - del out['environment'] - del out['template'] - self.assertEqual(generator.stack_data, out) + with open(res_file) as fp: + result = fp.read() + self.assertEqual(generator.heat_template_and_data(), result) - def test_heat_template_and_data_without_data(self): - generator = self.get_generator(False, False, False, False, False) + def test_flame_adoption_data(self): + res_file = os.path.join( + FIXTURES_DIR, 'results/flame_adoption_data.yaml' + ) + generator = flame.TemplateGenerator( + connection=self.conn, + options=dict( + no_threads=True, + generate_adoption_data=True + ) + ) generator.extract_data() - out = yaml.load(generator.heat_template_and_data()) - mandatory_keys = {'heat_template_version', 'resources', 'description', - 'parameters'} - self.assertEqual(mandatory_keys, set(out.keys())) - self.assertEqual(generator.template, out) - - -class ClientTest(BaseTestCase): - def setUp(self): - super(ClientTest, self).setUp() - self.c = flame_client.Client('username', 'password', 'tenant_name', - 'authUrl', 'auth_token') - - def test_generate(self): - out = self.c.generate(False, False, False, True) - parsed_out = yaml.load(out) - self.assertIsInstance(parsed_out, dict) - - def test_generate_no_stack_data(self): - out = self.c.generate(False, False, False, False) - parsed_out = yaml.load(out) - self.assertIsInstance(parsed_out, dict) - self.assertNotIn('template', parsed_out.keys()) - - def test_generate_contains_extract(self): - out = self.c.generate(False, False, False, True, False) - parsed_out = yaml.load(out) - self.assertIsInstance(parsed_out, dict) - self.assertIn('template', parsed_out.keys()) - - -class StackDataTests(BaseTestCase): - - def test_keypair(self): - self.mock_nova.return_value = FakeNovaManager() - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'key_0': { - 'type': 'OS::Nova::KeyPair', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'key_0', - 'resource_data': {}, - 'resource_id': 'key', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_keys(), expected) - - def test_router(self): - self.mock_neutron.return_value = FakeNeutronManager() - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'router_0': { - 'type': 'OS::Neutron::Router', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_routers(), expected) - - def test_router_with_external_gateway(self): - fake = FakeNeutronManager() - fake.routers = [{'name': 'myrouter', - 'id': '1234', - 'admin_state_up': 'true', - 'external_gateway_info': { - 'network_id': '8765', - 'enable_snat': 'true'}}, ] - # This router has no port - fake.ports = [] - self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'router_0': { - 'type': 'OS::Neutron::Router', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE' - }, - 'router_0_gateway': { - 'type': 'OS::Neutron::RouterGateway', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0_gateway', - 'resource_data': {}, - 'resource_id': '1234:8765', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_routers(), expected) - - def test_router_with_ports(self): - fake = FakeNeutronManager() - fake.ports = [{'status': 'ACTIVE', - 'name': '', - 'allowed_address_pairs': [], - 'admin_state_up': True, - 'network_id': '4444', - 'extra_dhcp_opts': [], - 'binding:vnic_type': 'normal', - 'device_owner': 'network:router_interface', - 'mac_address': 'fa:16:3e:4b:8c:98', - 'fixed_ips': [{'subnet_id': '1111', - 'ip_address': '10.123.2.3'}], - 'id': '1234567', - 'security_groups': [], - 'device_id': '1234'}, ] - fake.subnets = [{'name': 'subnet_1111', - 'enable_dhcp': True, - 'network_id': '1234', - 'dns_nameservers': [], - 'allocation_pools': [{'start': '10.123.2.2', - 'end': '10.123.2.30'}], - 'host_routes': [], - 'ip_version': 4, - 'gateway_ip': '10.123.2.1', - 'cidr': '10.123.2.0/27', - 'id': '1111'}, ] - - self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'router_0': { - 'type': 'OS::Neutron::Router', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE' - }, - 'router_0_interface_0': { - 'type': 'OS::Neutron::RouterInterface', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0_interface_0', - 'resource_data': {}, - 'resource_id': '1234:subnet_id=1111', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_routers(), expected) - - def test_network(self): - self.mock_neutron.return_value = FakeNeutronManager() - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'network_0': { - 'type': 'OS::Neutron::Net', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'network_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_networks(), expected) - - def test_external_network(self): - fake = FakeNeutronManager() - fake.networks[0]['router:external'] = True - self.mock_neutron.return_value = fake - - generator = self.get_generator(False, False, False, True, False) - - self.check_stackdata(generator._extract_networks(), {}) - - def test_subnet(self): - fake = FakeNeutronManager() - fake.subnets = [{'name': 'subnet_1111', - 'enable_dhcp': True, - 'network_id': '2222', - 'dns_nameservers': [], - 'allocation_pools': [{'start': '10.123.2.2', - 'end': '10.123.2.30'}], - 'host_routes': [], - 'ip_version': 4, - 'gateway_ip': '10.123.2.1', - 'cidr': '10.123.2.0/27', - 'id': '1111'}, ] - self.mock_neutron.return_value = fake - - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'subnet_0': { - 'type': 'OS::Neutron::Subnet', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'subnet_0', - 'resource_data': {}, - 'resource_id': '1111', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_subnets(), expected) - - def test_floatingip(self): - fake = FakeNeutronManager() - fake.floatingips = [{'router_id': '1111', - 'status': 'ACTIVE', - 'floating_network_id': '1234', - 'fixed_ip_address': '10.0.48.251', - 'floating_ip_address': '84.39.33.60', - 'port_id': '4321', - 'id': '2222'}, ] - self.mock_neutron.return_value = fake - - generator = self.get_generator(True, False, False, True, False) - - expected = { - 'floatingip_0': { - 'type': 'OS::Neutron::FloatingIP', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'floatingip_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_floating(), expected) - - def test_security_group(self): - rules = [{'remote_group_id': None, - 'direction': 'ingress', - 'remote_ip_prefix': '0.0.0.0/0', - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': 22, - 'port_range_min': 22, - 'id': '8901', - 'security_group_id': '1234'}, ] - fake = FakeNeutronManager() - fake.groups = [{'tenant_id': '7777', - 'name': 'somename', - 'description': 'description', - 'security_group_rules': rules, - 'id': '1234'}, ] - self.mock_neutron.return_value = fake - - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'security_group_0': { - 'type': 'OS::Neutron::SecurityGroup', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'security_group_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_secgroups(), expected) - - def test_default_security_group(self): - rules = [{'remote_group_id': None, - 'direction': 'ingress', - 'remote_ip_prefix': '0.0.0.0/0', - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': 22, - 'port_range_min': 22, - 'id': '8901', - 'security_group_id': '1234'}, ] - fake = FakeNeutronManager() - fake.groups = [{'tenant_id': '7777', - 'name': 'default', - 'description': 'default', - 'security_group_rules': rules, - 'id': '1234'}, ] - self.mock_neutron.return_value = fake - - generator = self.get_generator(False, False, False, True, False) - - self.check_stackdata(generator._extract_secgroups(), {}) - - def test_volume(self): - self.mock_cinder.return_value = FakeCinderManager() - - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'volume_0': { - 'type': 'OS::Cinder::Volume', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'volume_0', - 'resource_data': {}, - 'resource_id': 1234, - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_volumes(), expected) - - def test_server(self): - self.mock_nova.return_value = FakeNovaManager() - - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'server_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_servers(), expected) - - def test_server_with_default_security_group(self): - fake_neutron = FakeNeutronManager() - fake_nova = FakeNovaManager() - fake_neutron.groups = [{"name": "default", - "id": "1", - "security_group_rules": [], - "description": "default"}, ] - fake_nova.groups = {'server1': [FakeSecurityGroup( - name='default', description='default')]} - self.mock_neutron.return_value = fake_neutron - self.mock_nova.return_value = fake_nova - - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'action': 'CREATE', - 'metadata': {}, - 'name': 'server_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE' - } - } - self.check_stackdata(generator._extract_servers(), expected) - - def test_servergroup(self): - self.mock_nova.return_value = FakeNovaManager() - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'servergroup_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'servergroup_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::ServerGroup' - } - } - self.check_stackdata(generator._extract_servergroups(), expected) - - -class NetworkTests(BaseTestCase): - - def test_keypair(self): - self.mock_nova.return_value = FakeNovaManager() - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'key_0': { - 'type': 'OS::Nova::KeyPair', - 'properties': { - 'public_key': 'ssh-rsa XXXX', - 'name': 'testkey' - } - } - } - self.check_template(generator._extract_keys(), expected) - - def test_router(self): - self.mock_neutron.return_value = FakeNeutronManager() - self.mock_neutron.return_value.ports = [] - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'router_0': { - 'type': 'OS::Neutron::Router', - 'properties': { - 'name': 'myrouter', - 'admin_state_up': 'true', - } - } - } - self.check_template(generator._extract_routers(), expected) - - def test_router_with_external_gateway(self): - fake = FakeNeutronManager() - fake.ports = [] - fake.routers = [{'name': 'myrouter', - 'id': '1234', - 'admin_state_up': 'true', - 'external_gateway_info': { - 'network_id': '8765', - 'enable_snat': 'true'}}, ] - self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'router_0_external_network': { - 'default': '8765', - 'type': 'string', - 'description': 'Router external network' - } - } - expected_resources = { - 'router_0': { - 'type': 'OS::Neutron::Router', - 'properties': { - 'name': 'myrouter', - 'admin_state_up': 'true', - } - }, - 'router_0_gateway': { - 'type': 'OS::Neutron::RouterGateway', - 'properties': { - 'router_id': {'get_resource': 'router_0'}, - 'network_id': { - 'get_param': 'router_0_external_network' - } - } - } - } - self.check_template(generator._extract_routers(), expected_resources, - expected_parameters) - - def test_router_with_ports(self): - fake = FakeNeutronManager() - fake.ports = [{'status': 'ACTIVE', - 'name': '', - 'allowed_address_pairs': [], - 'admin_state_up': True, - 'network_id': '4444', - 'extra_dhcp_opts': [], - 'binding:vnic_type': 'normal', - 'device_owner': 'network:router_interface', - 'mac_address': 'fa:16:3e:4b:8c:98', - 'fixed_ips': [{'subnet_id': '1111', - 'ip_address': '10.123.2.3'}], - 'id': '1234567', - 'security_groups': [], - 'device_id': '1234'}, ] - fake.subnets = [{'name': 'subnet_1111', - 'enable_dhcp': True, - 'network_id': '1234', - 'dns_nameservers': [], - 'allocation_pools': [{'start': '10.123.2.2', - 'end': '10.123.2.30'}], - 'host_routes': [], - 'ip_version': 4, - 'gateway_ip': '10.123.2.1', - 'cidr': '10.123.2.0/27', - 'id': '1111'}, ] - - self.mock_neutron.return_value = fake - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'router_0': { - 'type': 'OS::Neutron::Router', - 'properties': { - 'name': 'myrouter', - 'admin_state_up': 'true', - } - }, - 'router_0_interface_0': { - 'type': 'OS::Neutron::RouterInterface', - 'properties': { - 'subnet_id': {'get_resource': 'subnet_0'}, - 'router_id': {'get_resource': 'router_0'} - } - } - } - self.check_template(generator._extract_routers(), expected) - - def test_network(self): - self.mock_neutron.return_value = FakeNeutronManager() - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'network_0': { - 'type': 'OS::Neutron::Net', - 'properties': { - 'shared': False, - 'name': 'mynetwork', - 'admin_state_up': True - } - } - } - self.check_template(generator._extract_networks(), expected) - - def test_external_network(self): - fake = FakeNeutronManager() - fake.networks[0]['router:external'] = True - self.mock_neutron.return_value = fake - - generator = self.get_generator(False, False, False, True, False) - - self.check_template(generator._extract_networks(), {}) - - def test_subnet(self): - fake = FakeNeutronManager() - fake.subnets = [{'name': 'subnet_1111', - 'enable_dhcp': True, - 'network_id': '2222', - 'dns_nameservers': [], - 'allocation_pools': [{'start': '10.123.2.2', - 'end': '10.123.2.30'}], - 'host_routes': [], - 'ip_version': 4, - 'gateway_ip': '10.123.2.1', - 'cidr': '10.123.2.0/27', - 'id': '1111'}, ] - self.mock_neutron.return_value = fake - - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'subnet_0': { - 'type': 'OS::Neutron::Subnet', - 'properties': { - 'network_id': {'get_resource': 'network_0'}, - 'allocation_pools': [{'start': '10.123.2.2', - 'end': '10.123.2.30'}], - 'host_routes': [], - 'name': 'subnet_1111', - 'enable_dhcp': True, - 'ip_version': 4, - 'cidr': '10.123.2.0/27', - 'dns_nameservers': [] - } - } - } - self.check_template(generator._extract_subnets(), expected) - - def test_floatingip(self): - fake = FakeNeutronManager() - fake.floatingips = [{'router_id': '1111', - 'status': 'ACTIVE', - 'floating_network_id': '1234', - 'fixed_ip_address': '10.0.48.251', - 'floating_ip_address': '84.39.33.60', - 'port_id': '4321', - 'id': '2222'}, ] - self.mock_neutron.return_value = fake - - generator = self.get_generator(True, False, False, False, False) - - expected_parameters = { - 'external_network_for_floating_ip_0': { - 'default': '1234', - 'type': 'string', - 'description': 'Network to allocate floating IP from' - } - } - expected_resources = { - 'floatingip_0': { - 'type': 'OS::Neutron::FloatingIP', - 'properties': { - 'floating_network_id': { - 'get_param': 'external_network_for_floating_ip_0' - } - } - } - } - self.check_template(generator._extract_floating(), expected_resources, - expected_parameters) - - def test_security_group(self): - rules = [ - { - 'remote_group_id': '1234', - 'direction': 'ingress', - 'remote_ip_prefix': None, - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': 65535, - 'port_range_min': 1, - 'id': '5678', - 'security_group_id': '1234' - }, - { - 'remote_group_id': None, - 'direction': 'egress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '6789', - 'security_group_id': '1234' - }, - { - 'remote_group_id': None, - 'direction': 'egress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv6', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '7890', - 'security_group_id': '1234' - }, - { - 'remote_group_id': None, - 'direction': 'ingress', - 'remote_ip_prefix': '0.0.0.0/0', - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': 22, - 'port_range_min': 22, - 'id': '8901', - 'security_group_id': '1234' - }, - ] - fake = FakeNeutronManager() - fake.groups = [{'tenant_id': '7777', - 'name': 'toto', - 'description': 'description', - 'security_group_rules': rules, - 'id': '1234'}, ] - self.mock_neutron.return_value = fake - - generator = self.get_generator(False, False, False, False, False) - - expected = { - 'security_group_0': { - 'type': 'OS::Neutron::SecurityGroup', - 'properties': { - 'rules': [ - { - 'direction': 'ingress', - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'port_range_max': 65535, - 'port_range_min': 1, - 'remote_mode': 'remote_group_id' - }, - { - 'ethertype': 'IPv4', - 'direction': 'egress' - }, - { - 'ethertype': 'IPv6', - 'direction': 'egress' - }, - { - 'direction': 'ingress', - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'port_range_max': 22, - 'port_range_min': 22, - 'remote_ip_prefix': '0.0.0.0/0' - } - ], - 'description': 'description', - 'name': 'toto' - } - } - } - self.check_template(generator._extract_secgroups(), expected) - - def test_security_group_default(self): - rules = [ - { - 'remote_group_id': None, - 'direction': 'egress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv6', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '1234', - 'security_group_id': '1111' - }, - { - 'remote_group_id': '1111', - 'direction': 'ingress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv6', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '2345', - 'security_group_id': '1111' - }, - { - 'remote_group_id': None, - 'direction': 'ingress', - 'remote_ip_prefix': '0.0.0.0/0', - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': 22, - 'port_range_min': 22, - 'id': '3456', - 'security_group_id': '1111' - }, - { - 'remote_group_id': None, - 'direction': 'egress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '4567', - 'security_group_id': '1111' - }, - { - 'remote_group_id': '1111', - 'direction': 'ingress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '5678', - 'security_group_id': '1111' - } - ] - fake = FakeNeutronManager() - fake.groups = [{'tenant_id': '7777', - 'name': 'default', - 'description': 'default', - 'security_group_rules': rules, - 'id': '1111'}, ] - self.mock_neutron.return_value = fake - - generator = self.get_generator(False, False, False, False, False) - - expected = { - 'security_group_0': { - 'type': 'OS::Neutron::SecurityGroup', - 'properties': { - 'rules': [ - { - 'ethertype': 'IPv6', - 'direction': 'egress' - }, - { - 'ethertype': 'IPv6', - 'direction': 'ingress', - 'remote_mode': 'remote_group_id' - }, - { - 'direction': 'ingress', - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'port_range_max': 22, - 'port_range_min': 22, - 'remote_ip_prefix': '0.0.0.0/0' - }, - { - 'ethertype': 'IPv4', - 'direction': 'egress' - }, - { - 'ethertype': 'IPv4', - 'direction': 'ingress', - 'remote_mode': 'remote_group_id' - } - ], - 'description': 'default', - 'name': '_default' - } - } - } - self.check_template(generator._extract_secgroups(), expected) - - def test_security_groups(self): - rules1 = [ - { - 'remote_group_id': '2222', - 'direction': 'ingress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '01234', - 'security_group_id': '1111' - }, - { - 'remote_group_id': None, - 'direction': 'egress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv6', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '1234', - 'security_group_id': '1111' - }, - { - 'remote_group_id': None, - 'direction': 'egress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '2345', - 'security_group_id': '1111' - }, - { - 'remote_group_id': '2222', - 'direction': 'ingress', - 'remote_ip_prefix': None, - 'protocol': 'icmp', - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '3456', - 'security_group_id': '1111' - } - ] - - rules2 = [ - { - 'remote_group_id': '1111', - 'direction': 'ingress', - 'remote_ip_prefix': None, - 'protocol': 'udp', - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': 8888, - 'port_range_min': 7777, - 'id': '4567', - 'security_group_id': '2222' - }, - { - 'remote_group_id': None, - 'direction': 'egress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv6', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '5678', - 'security_group_id': '2222' - }, - { - 'remote_group_id': None, - 'direction': 'egress', - 'remote_ip_prefix': None, - 'protocol': None, - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': None, - 'port_range_min': None, - 'id': '6789', - 'security_group_id': '2222' - }, - { - 'remote_group_id': '1111', - 'direction': 'ingress', - 'remote_ip_prefix': None, - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'tenant_id': '7777', - 'port_range_max': 65535, - 'port_range_min': 1, - 'id': '7890', - 'security_group_id': '2222' - } - ] - fake = FakeNeutronManager() - fake.groups = [{'tenant_id': '7777', - 'name': 'security_group_1', - 'description': 'security_group_1', - 'security_group_rules': rules1, - 'id': '1111'}, - {'tenant_id': '7777', - 'name': 'security_group_2', - 'description': 'security_group_2', - 'security_group_rules': rules2, - 'id': '2222'}, ] - self.mock_neutron.return_value = fake - - generator = self.get_generator(False, False, False, True, False) - - expected = { - 'security_group_0': { - 'type': 'OS::Neutron::SecurityGroup', - 'properties': { - 'rules': [ - { - 'remote_group_id': { - 'get_resource': 'security_group_1' - }, - 'direction': 'ingress', - 'ethertype': 'IPv4', - 'remote_mode': 'remote_group_id' - }, - { - 'ethertype': 'IPv6', - 'direction': 'egress' - }, - { - 'ethertype': 'IPv4', - 'direction': 'egress' - }, - { - 'remote_group_id': { - 'get_resource': 'security_group_1' - }, - 'direction': 'ingress', - 'protocol': 'icmp', - 'ethertype': 'IPv4', - 'remote_mode': 'remote_group_id' - } - ], - 'description': 'security_group_1', - 'name': 'security_group_1' - } - }, - 'security_group_1': { - 'type': 'OS::Neutron::SecurityGroup', - 'properties': { - 'rules': [ - { - 'remote_group_id': { - 'get_resource': 'security_group_0' - }, - 'direction': 'ingress', - 'protocol': 'udp', - 'ethertype': 'IPv4', - 'port_range_max': 8888, - 'port_range_min': 7777, - 'remote_mode': 'remote_group_id' - }, - { - 'ethertype': 'IPv6', - 'direction': 'egress' - }, - { - 'ethertype': 'IPv4', - 'direction': 'egress' - }, - { - 'remote_group_id': { - 'get_resource': 'security_group_0' - }, - 'direction': 'ingress', - 'protocol': 'tcp', - 'ethertype': 'IPv4', - 'port_range_max': 65535, - 'port_range_min': 1, - 'remote_mode': 'remote_group_id' - } - ], - 'description': 'security_group_2', - 'name': 'security_group_2' - } - } - } - self.check_template(generator._extract_secgroups(), expected) - - -class VolumeTests(BaseTestCase): - - def setUp(self): - super(VolumeTests, self).setUp() - self.fake = FakeCinderManager() - self.mock_cinder.return_value = self.fake - - def test_basic(self): - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string'} - } - - expected_resources = { - 'volume_0': { - 'type': 'OS::Cinder::Volume', - 'properties': { - 'name': 'vol1', - 'description': 'Description', - 'volume_type': {'get_param': 'volume_0_volume_type'}, - 'size': 1 - } - } - } - self.check_template(generator._extract_volumes(), expected_resources, - expected_parameters) - - def test_basic_unnamed(self): - self.fake.volumes = [FakeUnnamedVolume(), ] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string'} - } - - expected_resources = { - 'volume_0': { - 'type': 'OS::Cinder::Volume', - 'properties': { - 'volume_type': {'get_param': 'volume_0_volume_type'}, - 'size': 1 - } - } - } - self.check_template(generator._extract_volumes(), expected_resources, - expected_parameters) - - def test_source_volid_external(self): - self.fake.volumes = [FakeVolume(source_volid=5678), ] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'volume_0_source_volid': { - 'description': 'Volume to create volume volume_0 from', - 'type': 'string' - }, - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string'} - } - expected_resources = { - 'volume_0': { - 'type': 'OS::Cinder::Volume', - 'properties': { - 'name': 'vol1', - 'description': 'Description', - 'source_volid': {'get_param': 'volume_0_source_volid'}, - 'volume_type': {'get_param': 'volume_0_volume_type'}, - 'size': 1 - } - } - } - self.check_template(generator._extract_volumes(), expected_resources, - expected_parameters) - - def test_source_volid_included(self): - self.fake.volumes = [FakeVolume(source_volid=5678), - FakeVolume(id=5678)] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string' - }, - 'volume_1_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_1', - 'type': 'string' - } - } - - expected_resources = { - 'volume_0': { - 'type': 'OS::Cinder::Volume', - 'properties': { - 'name': 'vol1', - 'description': 'Description', - 'source_volid': {'get_resource': 'volume_1'}, - 'volume_type': {'get_param': 'volume_0_volume_type'}, - 'size': 1 - } - }, - 'volume_1': { - 'type': 'OS::Cinder::Volume', - 'properties': { - 'name': 'vol1', - 'description': 'Description', - 'volume_type': {'get_param': 'volume_1_volume_type'}, - 'size': 1 - } - } - } - self.check_template(generator._extract_volumes(), expected_resources, - expected_parameters) - - def test_image(self): - metadata = { - 'kernel_id': '9817', - 'container_format': 'bare', - 'min_ram': '0', - 'ramdisk_id': '4ec7', - 'disk_format': 'qcow2', - 'image_name': 'cirros-0.3.1-x86_64-uec', - 'image_id': '5c5c', - 'checksum': 'f8a2e', - 'min_disk': '0', - 'size': '25'} - self.fake.volumes = [FakeVolume(bootable='true', - volume_image_metadata=metadata), ] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string' - }, - 'volume_0_image': { - 'default': '5c5c', - 'description': 'Image to create volume volume_0 from', - 'type': 'string' - } - } - expected_resources = { - 'volume_0': { - 'type': 'OS::Cinder::Volume', - 'properties': { - 'name': 'vol1', - 'description': 'Description', - 'image': {'get_param': 'volume_0_image'}, - 'volume_type': {'get_param': 'volume_0_volume_type'}, - 'size': 1 - } - } - } - self.check_template(generator._extract_volumes(), expected_resources, - expected_parameters) - - def test_snapshot_id(self): - self.fake.volumes = [FakeVolume(snapshot_id=5678), ] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'volume_0_snapshot_id': { - 'default': 5678, - 'description': 'Snapshot to create volume volume_0 from', - 'type': 'string' - }, - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string' - } - } - expected_resources = { - 'volume_0': { - 'type': 'OS::Cinder::Volume', - 'properties': { - 'name': 'vol1', - 'description': 'Description', - 'snapshot_id': {'get_param': 'volume_0_snapshot_id'}, - 'volume_type': {'get_param': 'volume_0_volume_type'}, - 'size': 1 - } - } - } - self.check_template(generator._extract_volumes(), expected_resources, - expected_parameters) - - def test_volume_type(self): - self.fake.volumes = [FakeVolume(volume_type='isci'), ] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'volume_0_volume_type': { - 'description': 'Volume type for volume volume_0', - 'default': 'isci', - 'type': 'string' - } - } - expected_resources = { - 'volume_0': { - 'type': 'OS::Cinder::Volume', - 'properties': { - 'name': 'vol1', - 'description': 'Description', - 'volume_type': {'get_param': 'volume_0_volume_type'}, - 'size': 1 - } - } - } - self.check_template(generator._extract_volumes(), expected_resources, - expected_parameters) - - def test_metadata(self): - self.fake.volumes = [FakeVolume(metadata={'key': 'value'}), ] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string' - } - } - expected_resources = { - 'volume_0': { - 'type': 'OS::Cinder::Volume', - 'properties': { - 'name': 'vol1', - 'description': 'Description', - 'metadata': {'key': 'value'}, - 'volume_type': {'get_param': 'volume_0_volume_type'}, - 'size': 1 - } - } - } - self.check_template(generator._extract_volumes(), expected_resources, - expected_parameters) - - -class ServerTests(BaseTestCase): - - def setUp(self): - super(ServerTests, self).setUp() - self.fake = FakeNovaManager() - self.mock_nova.return_value = self.fake - - def test_basic(self): - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'description': 'Image to use to boot server server_0', - 'default': '3333', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'} - } - } - } - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - def test_keypair(self): - self.fake.servers = [FakeServer(key_name='testkey')] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'description': 'Image to use to boot server server_0', - 'default': '3333', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'diskConfig': 'MANUAL', - 'key_name': {'get_resource': 'key_0'}, - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'} - } - } - } - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - def test_boot_from_volume(self): - attachments = [{'device': 'vda', - 'server_id': '777', - 'id': '5678', - 'host_name': None, - 'volume_id': '5678'}, ] - servers_args = {"id": 777, - "image": None, - "os-extended-volumes:volumes_attached": [{'id': 5678}]} - self.fake.servers = [FakeServer(**servers_args), ] - fake_cinder = FakeCinderManager() - fake_cinder.volumes = [FakeVolume(id=5678, - attachments=attachments, - bootable='true')] - self.mock_cinder.return_value = fake_cinder - - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'key_name': {'get_resource': 'key_0'}, - 'block_device_mapping_v2': [{'volume_id': { - 'get_resource': 'volume_0'}, 'device_name': 'vda'}] - } - } - } - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - def test_volume_attached(self): - attachments = [{'device': '/dev/vdb', - 'server_id': '777', - 'id': '5678', - 'host_name': None, - 'volume_id': '5678'}, ] - fake_cinder = FakeCinderManager() - fake_cinder.volumes = [FakeVolume(id=5678, attachments=attachments, - bootable='false'), ] - self.mock_cinder.return_value = fake_cinder - server_args = {"id": 777, - "os-extended-volumes:volumes_attached": [{'id': 5678}]} - self.fake.servers = [FakeServer(**server_args), ] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'description': 'Image to use to boot server server_0', - 'default': '3333', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'}, - 'block_device_mapping_v2': [{'volume_id': { - 'get_resource': 'volume_0'}, 'device_name': - '/dev/vdb'}] - } - }, - } - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - def test_security_groups(self): - fake_neutron = FakeNeutronManager() - fake_neutron.groups = [{"name": "group1", - "id": "1", - "security_group_rules": [], - "description": "Group"}, ] - self.mock_neutron.return_value = fake_neutron - self.fake.groups = {'server1': [FakeSecurityGroup(), ]} - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'description': 'Image to use to boot server server_0', - 'default': '3333', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'diskConfig': 'MANUAL', - 'security_groups': [ - { - 'get_resource': 'security_group_0' - } - ], - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'} - } - } - } - generator._extract_secgroups() - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - def test_config_drive(self): - self.fake.servers = [FakeServer(config_drive="True"), ] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'description': 'Image to use to boot server server_0', - 'default': '3333', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'config_drive': 'True', - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'} - } - } - } - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - def test_metadata(self): - self.fake.servers = [FakeServer(metadata={"key": "value"}), ] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'description': 'Image to use to boot server server_0', - 'default': '3333', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'metadata': {'key': 'value'}, - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'} - } - } - } - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - def test_networks(self): - subnet = { - "id": "4321", - "name": "private_subnet", - "network_id": "1234", - "allocation_pools": {"start": "10.0.0.2", "end": "10.0.0.254"}, - "cidr": "10.0.0.0/24", - "dns_nameservers": [], - "enable_dhcp": True, - "host_routes": [], - "ip_version": 4} - fake_neutron = FakeNeutronManager() - fake_neutron.subnets = [subnet] - fake_neutron.networks = [{"id": "1234", "name": "private"}, ] - self.mock_neutron.return_value = fake_neutron - addresses = {"private": [{"addr": "10.0.0.2"}]} - self.fake.servers = [FakeServer(addresses=addresses)] - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'description': 'Image to use to boot server server_0', - 'default': '3333', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'networks': [ - {'network': {'get_resource': 'network_0'}}], - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'} - } - } - } - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - def test_excluded_volume_attached(self): - attachments = [{'device': '/dev/vdb', - 'server_id': '777', - 'id': '5678', - 'host_name': None, - 'volume_id': '5678'}] - fake_cinder = FakeCinderManager() - fake_cinder.volumes = [FakeVolume(id=5678, attachments=attachments, - bootable='false'), ] - self.mock_cinder.return_value = fake_cinder - server_args = {"id": 777, - "os-extended-volumes:volumes_attached": [{'id': 5678}]} - self.fake.servers = [FakeServer(**server_args), ] - generator = self.get_generator(False, True, False, False, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'description': 'Image to use to boot server server_0', - 'default': '3333', - 'type': 'string' - }, - 'volume_server1_0': { - 'default': 5678, - 'description': 'Volume for server server1, device ' - '/dev/vdb', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'}, - 'block_device_mapping_v2': [{'volume_id': { - 'get_param': 'volume_server1_0'}, 'device_name': - '/dev/vdb'}] - } - } - } - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - def test_servergroup(self): - self.fake.servers = [FakeServer()] - self.fake.servers[0].id = '12345' - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'description': 'Image to use to boot server server_0', - 'default': '3333', - 'type': 'string' - } - } - expected_resources = { - 'server_0': { - 'type': 'OS::Nova::Server', - 'properties': { - 'name': 'server1', - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'}, - 'scheduler_hints': {'group': - {'get_resource': 'servergroup_0'}} - } - } - } - self.check_template(generator._extract_servers(), expected_resources, - expected_parameters) - - -class GenerationTests(BaseTestCase): - - def setUp(self): - super(GenerationTests, self).setUp() - self.mock_neutron.return_value = FakeNeutronManager() - self.mock_nova.return_value = FakeNovaManager() - self.mock_cinder.return_value = FakeCinderManager() - - def test_generation(self): - - generator = self.get_generator(False, False, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'default': '3333', - 'description': 'Image to use to boot server server_0', - 'type': 'string' - }, - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string' - } - } - expected_resources = { - 'key_0': { - 'properties': { - 'name': 'testkey', - 'public_key': 'ssh-rsa XXXX' - }, - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'properties': { - 'admin_state_up': True, - 'name': 'mynetwork', - 'shared': False - }, - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'properties': { - 'admin_state_up': 'true', - 'name': 'myrouter' - }, - 'type': 'OS::Neutron::Router' - }, - 'server_0': { - 'properties': { - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'}, - 'name': 'server1' - }, - 'type': 'OS::Nova::Server' - }, - 'servergroup_0': { - 'properties': { - 'name': 'policy_group', - 'policies': 'affinity'}, - 'type': 'OS::Nova::ServerGroup' - }, - 'volume_0': { - 'properties': { - 'description': 'Description', - 'name': 'vol1', - 'size': 1, - 'volume_type': {'get_param': 'volume_0_volume_type'} - }, - 'type': 'OS::Cinder::Volume' - } - } - - expected_data = { - 'key_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'key_0', - 'resource_data': {}, - 'resource_id': 'key', - 'status': 'COMPLETE', - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'network_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Router' - }, - 'server_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'server_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::Server' - }, - 'servergroup_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'servergroup_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::ServerGroup' - }, - 'volume_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'volume_0', - 'resource_data': {}, - 'resource_id': 1234, - 'status': 'COMPLETE', - 'type': 'OS::Cinder::Volume' - } - } + with open(res_file) as fp: + result = fp.read() + self.assertEqual(generator.heat_template_and_data(), result) + + def test_flame_constraints(self): + res_file = os.path.join(FIXTURES_DIR, 'results/flame_constraints.yaml') + generator = flame.TemplateGenerator( + connection=self.conn, + options=dict( + no_threads=True, + include_constraints=True + ) + ) generator.extract_data() - self.assertEqual(generator.template['resources'], expected_resources) - self.assertEqual(generator.template['parameters'], expected_parameters) - self.assertEqual(generator.stack_data['resources'], expected_data) - - def test_generation_secgroups(self): - self.mock_neutron().groups = [{ - 'id': '4321', - 'name': 'my_sec_group', - 'security_group_rules': [{ - 'protocol': 'TCP', - 'port_range_min': '22', - 'port_range_max': '22', - 'tenant_id': 'tenant', - 'id': '1212', - 'security_group_id': '4444', - 'remote_group_id': None - }], - 'description': 'group description' - }] - - generator = self.get_generator(False, False, False, True, False, False) - - expected_resources = { - "volume_0": { - "properties": { - "name": "vol1", - "volume_type": { - "get_param": "volume_0_volume_type" - }, - "description": "Description", - "size": 1 - }, - "type": "OS::Cinder::Volume" - }, - "key_0": { - "type": "OS::Nova::KeyPair", - "properties": { - "name": "testkey", - "public_key": "ssh-rsa XXXX" - } - }, - "network_0": { - "properties": { - "admin_state_up": True, - "shared": False, - "name": "mynetwork" - }, - "type": "OS::Neutron::Net" - }, - "router_0": { - "properties": { - "admin_state_up": "true", - "name": "myrouter" - }, - "type": "OS::Neutron::Router" - }, - "server_0": { - "properties": { - "image": { - "get_param": "server_0_image" - }, - "flavor": { - "get_param": "server_0_flavor" - }, - "name": "server1", - "diskConfig": "MANUAL", - "key_name": { - "get_resource": "key_0" - } - }, - "type": "OS::Nova::Server" - }, - "security_group_0": { - "type": "OS::Neutron::SecurityGroup", - "properties": { - "rules": [ - { - "port_range_min": "22", - "port_range_max": "22", - "protocol": "TCP" - } - ], - "name": "my_sec_group", - "description": "group description" - } - }, - "servergroup_0": { - "type": "OS::Nova::ServerGroup", - "properties": { - "policies": "affinity", - "name": "policy_group" - } - } - } - - expected_parameters = { - "server_0_image": { - "description": "Image to use to boot server server_0", - "type": "string", - "default": "3333" - }, - "server_0_flavor": { - "description": "Flavor to use for server server_0", - "type": "string", - "default": "m1.small" - }, - "volume_0_volume_type": { - "default": "fast", - "description": "Volume type for volume volume_0", - "type": "string" - } - } - expected_data = { - 'key_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'key_0', - 'resource_data': {}, - 'resource_id': 'key', - 'status': 'COMPLETE', - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'network_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Router' - }, - 'security_group_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'security_group_0', - 'resource_data': {}, - 'resource_id': '4321', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::SecurityGroup' - }, - 'server_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'server_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::Server' - }, - 'servergroup_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'servergroup_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::ServerGroup' - }, - 'volume_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'volume_0', - 'resource_data': {}, - 'resource_id': 1234, - 'status': 'COMPLETE', - 'type': 'OS::Cinder::Volume' - } - } + with open(res_file) as fp: + result = fp.read() + self.assertEqual(generator.heat_template_and_data(), result) + def test_flame_extract_ports(self): + res_file = os.path.join( + FIXTURES_DIR, 'results/flame_extract_ports.yaml' + ) + generator = flame.TemplateGenerator( + connection=self.conn, + options=dict( + no_threads=True, + extract_ports=True + ) + ) generator.extract_data() - self.assertEqual(expected_resources, generator.template['resources']) - self.assertEqual(expected_parameters, generator.template['parameters']) - self.assertEqual(expected_data, generator.stack_data['resources']) - - def test_generation_exclude_secgroups(self): - self.mock_neutron().groups = [{ - 'id': '4321', - 'name': 'my_sec_group', - 'security_group_rules': [{ - 'protocol': 'TCP', - 'port_range_min': '22', - 'port_range_max': '22', - 'tenant_id': 'tenant', - 'id': '1212', - 'security_group_id': '4444', - 'remote_group_id': None - }], - 'description': 'group description' - }] - - generator = self.get_generator(False, False, False, True, False, True) - - expected_resources = { - "volume_0": { - "properties": { - "name": "vol1", - "volume_type": { - "get_param": "volume_0_volume_type" - }, - "description": "Description", - "size": 1 - }, - "type": "OS::Cinder::Volume" - }, - "key_0": { - "type": "OS::Nova::KeyPair", - "properties": { - "name": "testkey", - "public_key": "ssh-rsa XXXX" - } - }, - "network_0": { - "properties": { - "admin_state_up": True, - "shared": False, - "name": "mynetwork" - }, - "type": "OS::Neutron::Net" - }, - "router_0": { - "properties": { - "admin_state_up": "true", - "name": "myrouter" - }, - "type": "OS::Neutron::Router" - }, - "server_0": { - "properties": { - "image": { - "get_param": "server_0_image" - }, - "flavor": { - "get_param": "server_0_flavor" - }, - "name": "server1", - "diskConfig": "MANUAL", - "key_name": { - "get_resource": "key_0" - } - }, - "type": "OS::Nova::Server" - }, - "servergroup_0": { - "type": "OS::Nova::ServerGroup", - "properties": { - "policies": "affinity", - "name": "policy_group" - } - } - } - - expected_parameters = { - "server_0_image": { - "description": "Image to use to boot server server_0", - "type": "string", - "default": "3333" - }, - "server_0_flavor": { - "description": "Flavor to use for server server_0", - "type": "string", - "default": "m1.small" - }, - "volume_0_volume_type": { - "default": "fast", - "description": "Volume type for volume volume_0", - "type": "string" - } - } - expected_data = { - 'key_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'key_0', - 'resource_data': {}, - 'resource_id': 'key', - 'status': 'COMPLETE', - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'network_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Router' - }, - 'server_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'server_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::Server' - }, - 'servergroup_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'servergroup_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::ServerGroup' - }, - 'volume_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'volume_0', - 'resource_data': {}, - 'resource_id': 1234, - 'status': 'COMPLETE', - 'type': 'OS::Cinder::Volume' - } - } + with open(res_file) as fp: + result = fp.read() + self.assertEqual(generator.heat_template_and_data(), result) + def test_flame_constraints_extract_ports(self): + res_file = os.path.join( + FIXTURES_DIR, 'results/flame_constraints_extract_ports.yaml' + ) + generator = flame.TemplateGenerator( + connection=self.conn, + options=dict( + no_threads=True, + extract_ports=True, + include_constraints=True + ) + ) generator.extract_data() - self.assertEqual(expected_resources, generator.template['resources']) - self.assertEqual(expected_parameters, generator.template['parameters']) - self.assertEqual(expected_data, generator.stack_data['resources']) - - def test_generation_exclude_servers(self): - - generator = self.get_generator(True, False, False, True, False) - - expected_parameters = { - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string' - } - } - - expected_resources = { - 'key_0': { - 'properties': { - 'name': 'testkey', - 'public_key': 'ssh-rsa XXXX' - }, - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'properties': { - 'admin_state_up': True, - 'name': 'mynetwork', - 'shared': False - }, - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'properties': { - 'admin_state_up': 'true', - 'name': 'myrouter' - }, - 'type': 'OS::Neutron::Router' - }, - 'servergroup_0': { - 'properties': { - 'name': 'policy_group', - 'policies': 'affinity' - }, - 'type': 'OS::Nova::ServerGroup' - }, - 'volume_0': { - 'properties': { - 'description': 'Description', - 'name': 'vol1', - 'size': 1, - 'volume_type': {'get_param': 'volume_0_volume_type'} - }, - 'type': 'OS::Cinder::Volume' - } - } - - expected_data = { - 'key_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'key_0', - 'resource_data': {}, - 'resource_id': 'key', - 'status': 'COMPLETE', - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'network_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Router' - }, - 'servergroup_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'servergroup_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::ServerGroup' - }, - 'volume_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'volume_0', - 'resource_data': {}, - 'resource_id': 1234, - 'status': 'COMPLETE', - 'type': 'OS::Cinder::Volume' - } - } - - generator.extract_data() - self.assertEqual(generator.template['resources'], expected_resources) - self.assertEqual(generator.template['parameters'], expected_parameters) - self.assertEqual(generator.stack_data['resources'], expected_data) - - def test_generation_exclude_volumes(self): - - generator = self.get_generator(False, True, False, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'default': '3333', - 'description': 'Image to use to boot server server_0', - 'type': 'string' - } - } - expected_resources = { - 'key_0': { - 'properties': { - 'name': 'testkey', - 'public_key': 'ssh-rsa XXXX' - }, - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'properties': { - 'admin_state_up': True, - 'name': 'mynetwork', - 'shared': False - }, - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'properties': { - 'admin_state_up': 'true', - 'name': 'myrouter' - }, - 'type': 'OS::Neutron::Router' - }, - 'server_0': { - 'properties': { - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_resource': 'key_0'}, - 'name': 'server1' - }, - 'type': 'OS::Nova::Server' - }, - 'servergroup_0': { - 'properties': { - 'name': 'policy_group', - 'policies': 'affinity'}, - 'type': 'OS::Nova::ServerGroup' - } - } - - expected_data = { - 'key_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'key_0', - 'resource_data': {}, - 'resource_id': 'key', - 'status': 'COMPLETE', - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'network_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Router' - }, - 'server_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'server_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::Server' - }, - 'servergroup_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'servergroup_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::ServerGroup' - } - } - - generator.extract_data() - self.assertEqual(generator.template['resources'], expected_resources) - self.assertEqual(generator.template['parameters'], expected_parameters) - self.assertEqual(generator.stack_data['resources'], expected_data) - - def test_generation_exclude_keypairs(self): - - generator = self.get_generator(False, False, True, True, False) - - expected_parameters = { - 'server_0_flavor': { - 'default': 'm1.small', - 'description': 'Flavor to use for server server_0', - 'type': 'string' - }, - 'server_0_image': { - 'default': '3333', - 'description': 'Image to use to boot server server_0', - 'type': 'string' - }, - 'server_0_key': { - 'default': 'testkey', - 'description': 'Key for server server_0', - 'type': 'string' - }, - 'volume_0_volume_type': { - 'default': 'fast', - 'description': 'Volume type for volume volume_0', - 'type': 'string' - } - } - expected_resources = { - 'network_0': { - 'properties': { - 'admin_state_up': True, - 'name': 'mynetwork', - 'shared': False - }, - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'properties': { - 'admin_state_up': 'true', - 'name': 'myrouter' - }, - 'type': 'OS::Neutron::Router' - }, - 'server_0': { - 'properties': { - 'diskConfig': 'MANUAL', - 'flavor': {'get_param': 'server_0_flavor'}, - 'image': {'get_param': 'server_0_image'}, - 'key_name': {'get_param': 'server_0_key'}, - 'name': 'server1' - }, - 'type': 'OS::Nova::Server' - }, - 'servergroup_0': { - 'properties': { - 'name': 'policy_group', - 'policies': 'affinity' - }, - 'type': 'OS::Nova::ServerGroup' - }, - 'volume_0': { - 'properties': { - 'description': 'Description', - 'name': 'vol1', - 'size': 1, - 'volume_type': {'get_param': 'volume_0_volume_type'} - }, - 'type': 'OS::Cinder::Volume' - } - } - - expected_data = { - 'network_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'network_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Router' - }, - 'server_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'server_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::Server' - }, - 'servergroup_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'servergroup_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::ServerGroup' - }, - 'volume_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'volume_0', - 'resource_data': {}, - 'resource_id': 1234, - 'status': 'COMPLETE', - 'type': 'OS::Cinder::Volume' - } - } - - generator.extract_data() - self.assertEqual(generator.template['resources'], expected_resources) - self.assertEqual(generator.template['parameters'], expected_parameters) - self.assertEqual(generator.stack_data['resources'], expected_data) - - def test_generation_exclude_servers_and_volumes(self): - - generator = self.get_generator(True, True, False, True, False) - - expected_parameters = {} - expected_resources = { - 'key_0': { - 'properties': { - 'name': 'testkey', - 'public_key': 'ssh-rsa XXXX' - }, - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'properties': { - 'admin_state_up': True, - 'name': 'mynetwork', - 'shared': False - }, - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'properties': { - 'admin_state_up': 'true', - 'name': 'myrouter' - }, - 'type': 'OS::Neutron::Router' - }, - 'servergroup_0': { - 'properties': { - 'name': 'policy_group', - 'policies': 'affinity' - }, - 'type': 'OS::Nova::ServerGroup' - } - } - - expected_data = { - 'key_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'key_0', - 'resource_data': {}, - 'resource_id': 'key', - 'status': 'COMPLETE', - 'type': 'OS::Nova::KeyPair' - }, - 'network_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'network_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Router' - }, - 'servergroup_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'servergroup_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::ServerGroup' - } - } - generator.extract_data() - self.assertEqual(generator.template['resources'], expected_resources) - self.assertEqual(generator.template['parameters'], expected_parameters) - self.assertEqual(generator.stack_data['resources'], expected_data) - - def test_generation_exclude_servers_volumes_keypairs(self): - - generator = self.get_generator(True, True, True, True, False) - - expected_parameters = {} - expected_resources = { - 'network_0': { - 'properties': { - 'admin_state_up': True, - 'name': 'mynetwork', - 'shared': False - }, - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'properties': { - 'admin_state_up': 'true', - 'name': 'myrouter' - }, - 'type': 'OS::Neutron::Router' - }, - 'servergroup_0': { - 'properties': { - 'name': 'policy_group', - 'policies': 'affinity' - }, - 'type': 'OS::Nova::ServerGroup' - } - } - - expected_data = { - 'network_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'network_0', - 'resource_data': {}, - 'resource_id': '2222', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Net' - }, - 'router_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'router_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Router' - }, - 'servergroup_0': { - 'action': 'CREATE', - 'metadata': {}, - 'name': 'servergroup_0', - 'resource_data': {}, - 'resource_id': '1234', - 'status': 'COMPLETE', - 'type': 'OS::Nova::ServerGroup' - } - } - - generator.extract_data() - self.assertEqual(generator.template['resources'], expected_resources) - self.assertEqual(generator.template['parameters'], expected_parameters) - self.assertEqual(generator.stack_data['resources'], expected_data) + with open(res_file) as fp: + result = fp.read() + self.assertEqual(generator.heat_template_and_data(), result) diff --git a/flameclient/tests/test_flameports.py b/flameclient/tests/test_flameports.py deleted file mode 100644 index 00f720b..0000000 --- a/flameclient/tests/test_flameports.py +++ /dev/null @@ -1,640 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2014 Cloudwatt - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT 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 re - -import mock - -from flameclient import flame -from flameclient.tests import base - - -class FakeBase(object): - - def __init__(self, **kwargs): - for key, value in kwargs.items(): - setattr(self, key, value) - - -class FakeVolume(FakeBase): - id = 1234 - size = 1 - source_volid = None - bootable = 'false' - snapshot_id = None - display_name = 'vol1' - display_description = 'Description' - volume_type = 'fast' - metadata = None - - -class FakeServer(FakeBase): - id = '1234' - name = 'server1' - config_drive = None - flavor = {'id': '2'} - image = {'id': '3333', - 'links': [{'href': 'http://p/7777/images/3333', - 'rel': 'bookmark'}]} - key_name = 'testkey' - addresses = [] - metadata = None - - def __init__(self, server_id, **kwargs): - self.id = server_id - kwargs.setdefault('OS-DCF:diskConfig', 'MANUAL') - kwargs.setdefault('os-extended-volumes:volumes_attached', []) - super(FakeServer, self).__init__(**kwargs) - - -class FakeFlavor(FakeBase): - name = 'm1.tiny' - id = '1' - - -class FakeKeypair(FakeBase): - name = 'key' - id = 'key' - public_key = 'ssh-rsa AAAAB3NzaC' - - -class FakeSecurityGroup(FakeBase): - id = '1' - name = 'name' - - -class FakeNeutronManager(object): - - def __init__(self): - self.groups = [{u'description': u'default', - u'id': u'secgorup1', - u'name': u'default', - u'security_group_rules': [ - {u'direction': u'ingress', - u'ethertype': u'IPv4', - u'id': u'secgroup-rule1', - u'port_range_max': 65535, - u'port_range_min': 1, - u'protocol': u'tcp', - u'remote_group_id': None, - u'remote_ip_prefix': u'0.0.0.0/0', - u'security_group_id': u'secgorup1', - u'tenant_id': u'tenant1'}, - ], - u'tenant_id': u'tenant1'}] - - self.routers = [ - {u'admin_state_up': True, - u'external_gateway_info': {u'enable_snat': True, - u'network_id': u'network3'}, - u'id': u'router1', - u'name': u'gw-internal-a', - u'routes': [], - u'status': u'ACTIVE', - u'tenant_id': u'tenant1'}, - ] - - self.ports = [{u'admin_state_up': True, - u'allowed_address_pairs': [], - u'binding:vnic_type': u'normal', - u'device_id': u'router1', - u'device_owner': u'network:router_interface', - u'extra_dhcp_opts': [], - u'fixed_ips': [{u'ip_address': u'192.168.203.1', - u'subnet_id': u'subnet3'}], - u'id': u'port1', - u'mac_address': u'fa:16:3e:fe:c1:b3', - u'name': u'', - u'network_id': u'network1', - u'security_groups': [], - u'status': u'ACTIVE', - u'tenant_id': u'tenant1'}, - {u'admin_state_up': True, - u'allowed_address_pairs': [], - u'binding:vnic_type': u'normal', - u'device_id': u'server3', - u'device_owner': u'compute:nova', - u'extra_dhcp_opts': [], - u'fixed_ips': [{u'ip_address': u'192.168.203.5', - u'subnet_id': u'subnet3'}], - u'id': u'port2', - u'mac_address': u'fa:16:3e:e4:44:7b', - u'name': u'', - u'network_id': u'network1', - u'security_groups': [u'secgorup1'], - u'status': u'ACTIVE', - u'tenant_id': u'tenant1'}, - {u'admin_state_up': True, - u'allowed_address_pairs': [], - u'binding:vnic_type': u'normal', - u'device_id': u'server2', - u'device_owner': u'compute:nova', - u'extra_dhcp_opts': [], - u'fixed_ips': [{u'ip_address': u'192.168.203.4', - u'subnet_id': u'subnet3'}], - u'id': u'port3', - u'mac_address': u'fa:16:3e:e8:e4:e2', - u'name': u'', - u'network_id': u'network1', - u'security_groups': [u'secgorup1'], - u'status': u'ACTIVE', - u'tenant_id': u'tenant1'}, - {u'admin_state_up': True, - u'allowed_address_pairs': [], - u'binding:vnic_type': u'normal', - u'device_id': u'dhcp1-network1', - u'device_owner': u'network:dhcp', - u'extra_dhcp_opts': [], - u'fixed_ips': [{u'ip_address': u'192.168.203.3', - u'subnet_id': u'subnet3'}, - {u'ip_address': u'192.168.204.2', - u'subnet_id': u'subnet4'}], - u'id': u'port4', - u'mac_address': u'fa:16:3e:af:86:30', - u'name': u'', - u'network_id': u'network1', - u'security_groups': [], - u'status': u'ACTIVE', - u'tenant_id': u'tenant1'}, - {u'admin_state_up': True, - u'allowed_address_pairs': [], - u'binding:vnic_type': u'normal', - u'device_id': u'server1', - u'device_owner': u'compute:nova', - u'extra_dhcp_opts': [], - u'fixed_ips': [{u'ip_address': u'192.168.203.2', - u'subnet_id': u'subnet3'}], - u'id': u'port6', - u'mac_address': u'fa:16:3e:b0:9a:e2', - u'name': u'', - u'network_id': u'network1', - u'security_groups': [u'secgorup1'], - u'status': u'ACTIVE', - u'tenant_id': u'tenant1'} - ] - self.subnets = [{u'allocation_pools': [ - {u'end': u'172.19.0.254', u'start': u'172.19.0.2'}], - u'cidr': u'172.19.0.0/24', - u'dns_nameservers': [], - u'enable_dhcp': True, - u'gateway_ip': u'172.19.0.1', - u'host_routes': [], - u'id': u'subnet1', - u'ip_version': 4, - u'name': u'storage', - u'network_id': u'network2', - u'tenant_id': u'tenant1'}, - {u'allocation_pools': [ - {u'end': u'10.8.8.200', - u'start': u'10.8.8.100'}], - u'cidr': u'10.8.8.0/24', - u'dns_nameservers': [], - u'enable_dhcp': False, - u'gateway_ip': u'10.8.8.254', - u'host_routes': [], - u'id': u'subnet2', - u'ip_version': 4, - u'name': u'ext-subnet', - u'network_id': u'network3', - u'tenant_id': u'tenant1'}, - {u'allocation_pools': [{u'end': u'192.168.203.254', - u'start': u'192.168.203.2'}], - u'cidr': u'192.168.203.0/24', - u'dns_nameservers': [], - u'enable_dhcp': True, - u'gateway_ip': u'192.168.203.1', - u'host_routes': [], - u'id': u'subnet3', - u'ip_version': 4, - u'name': u'int-a-1', - u'network_id': u'network1', - u'tenant_id': u'tenant1'}, - {u'allocation_pools': [{u'end': u'192.168.204.254', - u'start': u'192.168.204.2'}], - u'cidr': u'192.168.204.0/24', - u'dns_nameservers': [], - u'enable_dhcp': True, - u'gateway_ip': u'192.168.204.1', - u'host_routes': [], - u'id': u'subnet4', - u'ip_version': 4, - u'name': u'int-a-2', - u'network_id': u'network1', - u'tenant_id': u'tenant1'}] - self.networks = [{u'admin_state_up': True, - u'id': u'network1', - u'name': u'internal', - u'router:external': False, - u'shared': False, - u'status': u'ACTIVE', - u'subnets': [u'subnet3', - u'subnet4'], - u'tenant_id': u'tenant1'}, - {u'admin_state_up': True, - u'id': u'network2', - u'name': u'storage', - u'router:external': False, - u'shared': False, - u'status': u'ACTIVE', - u'subnets': [u'subnet1'], - u'tenant_id': u'tenant1'}, - {u'admin_state_up': True, - u'id': u'network3', - u'name': u'ext-net', - u'router:external': True, - u'shared': True, - u'status': u'ACTIVE', - u'subnets': [u'subnet2'], - u'tenant_id': u'tenant1'}] - - self.floatingips = [{u'fixed_ip_address': None, - u'floating_ip_address': u'10.8.8.102', - u'floating_network_id': u'network3', - u'id': u'floating1', - u'port_id': None, - u'router_id': None, - u'status': u'DOWN', - u'tenant_id': u'tenant1'}, - {u'fixed_ip_address': None, - u'floating_ip_address': u'10.8.8.101', - u'floating_network_id': u'network3', - u'id': u'floating2', - u'port_id': None, - u'router_id': None, - u'status': u'DOWN', - u'tenant_id': u'tenant1'}, - {u'fixed_ip_address': u'192.168.203.4', - u'floating_ip_address': u'10.8.8.168', - u'floating_network_id': u'network3', - u'id': u'floating3', - u'port_id': u'port3', - u'router_id': u'router1', - u'status': u'ACTIVE', - u'tenant_id': u'tenant1'}, - {u'fixed_ip_address': None, - u'floating_ip_address': u'10.8.8.118', - u'floating_network_id': u'network3', - u'id': u'floating4', - u'port_id': None, - u'router_id': None, - u'status': u'DOWN', - u'tenant_id': u'tenant1'}] - - def subnet_list(self): - return self.subnets - - def network_list(self): - return self.networks - - def port_list(self): - return self.ports - - def router_list(self): - return self.routers - - def router_interfaces_list(self, router): - return [port for port in self.ports - if port['device_id'] == router['id']] - - def secgroup_list(self): - return self.groups - - def floatingip_list(self): - return self.floatingips - - -class FakeNovaManager(object): - - def __init__(self): - self.servers = [FakeServer('server1'), - FakeServer('server2'), - FakeServer('server3')] - self.servergroups = [] - self.flavors = [FakeFlavor(id='2', name='m1.small')] - self.groups = {} - self.keypairs = [FakeKeypair(name='testkey', - public_key='ssh-rsa XXXX')] - - def keypair_list(self): - return self.keypairs - - def flavor_list(self): - return self.flavors - - def server_list(self): - return self.servers - - def server_security_group_list(self, server): - return self.groups.get(server.name, []) - - def servergroup_list(self): - return self.servergroups - - -class FakeCinderManager(object): - - def __init__(self): - self.volumes = [FakeVolume(), ] - - def volume_list(self): - return self.volumes - - -class ResourceTestCase(base.TestCase): - - def test_template_resource(self): - resource = flame.Resource('my-name', - 'my-type', - properties='my-properties') - - expected = { - 'my-name': { - 'type': 'my-type', - 'properties': 'my-properties', - } - } - self.assertEqual(expected, resource.template_resource) - - -class BaseTestCase(base.TestCase): - - def setUp(self): - super(BaseTestCase, self).setUp() - self.patch_neutron = mock.patch('flameclient.managers.NeutronManager') - self.mock_neutron = self.patch_neutron.start() - self.patch_nova = mock.patch('flameclient.managers.NovaManager') - self.mock_nova = self.patch_nova.start() - self.patch_cinder = mock.patch('flameclient.managers.CinderManager') - self.mock_cinder = self.patch_cinder.start() - - def tearDown(self): - self.mock_neutron.stop() - self.mock_nova.stop() - self.mock_cinder.stop() - super(BaseTestCase, self).tearDown() - - def get_generator(self, exclude_servers, exclude_volumes, - exclude_keypairs, generate_data, extract_ports): - generator = flame.TemplateGenerator('x', 'x', 'x', 'x', True, - 'publicURL') - generator.extract_vm_details(exclude_servers, exclude_volumes, - exclude_keypairs, generate_data, - extract_ports) - return generator - - def check_stackdata(self, resources, expected_resources): - merged_resources = {} - for resource in resources: - merged_resources.update(resource.stack_resource) - - self.assertEqual(expected_resources, merged_resources) - - def check_template(self, resources, expected_resources, - expected_parameters=None): - - expected_parameters = expected_parameters or {} - merged_resources = {} - merged_parameters = {} - for resource in resources: - merged_resources.update(resource.template_resource) - merged_parameters.update(resource.template_parameter) - - self.assertEqual(expected_resources, merged_resources) - self.assertEqual(expected_parameters, merged_parameters) - - -class StackDataTests(BaseTestCase): - - def setUp(self): - super(StackDataTests, self).setUp() - self.mock_neutron.return_value = FakeNeutronManager() - self.mock_nova.return_value = FakeNovaManager() - self.mock_cinder.return_value = FakeCinderManager() - - def test_routers_presents(self): - generator = self.get_generator(False, False, False, True, True) - extraction = generator._extract_routers() - routers = {r.name: r for r in extraction} - self.assertIn('router_0', routers) - - def test_routers_resource_names(self): - generator = self.get_generator(False, False, False, True, True) - generator_output = generator._extract_routers() - routers = (res for res in generator_output - if res.type == "OS::Neutron::Router") - for n, router in enumerate(routers): - assert(router.name.startswith("router_")) - - def test_ports_presents(self): - generator = self.get_generator(False, False, False, True, True) - extraction = generator._extract_ports() - ports = {r.name: r for r in extraction} - self.assertIn('port_1', ports) - self.assertIn('port_2', ports) - - def test_ports_resource_names_types(self): - generator = self.get_generator(False, False, False, True, True) - extraction = generator._extract_ports() - for n, port in enumerate(extraction): - props = port.properties - assert(extraction[0].name.startswith("port_")) - self.assertEqual("OS::Neutron::Port", port.type) - self.assertIsInstance(props['admin_state_up'], bool) - self.assertIsInstance(props['security_groups'], list) - assert(props['device_owner'].startswith("compute:")) - - def test_port_fixed_ip(self): - reference = [{'ip_address': '192.168.203.2', - 'subnet_id': {'get_resource': 'subnet_2'}}] - generator = self.get_generator(False, False, False, True, True) - extraction = generator._extract_ports() - # Get the right port for the test - port = next((p for p in extraction if - p.properties['mac_address'] == 'fa:16:3e:b0:9a:e2')) - props = port.properties - self.assertIsInstance(props['fixed_ips'], list) - fixed_ips = props['fixed_ips'] - for ref in reference: - self.assertIn(ref, fixed_ips) - - def test_servers_ports_assignations(self): - generator = self.get_generator(False, False, False, True, True) - extraction = generator._extract_servers() - used_ports = [] - for n, server in enumerate(extraction): - props = server.properties - self.assertIsInstance(props['networks'], list) - for network in props['networks']: - port = network['port']['get_resource'] - assert(port.startswith("port_")) - # Port has not been used by another server - self.assertNotIn(port, used_ports) - used_ports.append(port) - - def test_floating_association(self): - generator = self.get_generator(False, False, False, True, True) - extraction = generator._extract_floating() - associations = (res for res in extraction - if res.type == "OS::Neutron::FloatingIPAssociation") - for association in associations: - props = association.properties - assert(props['floatingip_id']['get_resource']. - startswith('floatingip_')) - assert(props['port_id']['get_resource']. - startswith('port_')) - - -class GenerationTests(BaseTestCase): - resource_ref = set(['floatingip_association_2', - 'subnet_2', 'subnet_3', 'subnet_0', - 'port_2', 'port_1', 'port_4', - 'server_2', 'server_1', 'server_0', - 'router_0', - 'router_0_interface_0', - 'router_0_gateway', - 'key_0', - 'network_0', 'network_1', - 'floatingip_0', 'floatingip_1', - 'floatingip_2', 'floatingip_3', - 'volume_0']) - - params_ref = set(['volume_0_volume_type', - 'external_network_for_floating_ip_3', - 'external_network_for_floating_ip_2', - 'external_network_for_floating_ip_1', - 'external_network_for_floating_ip_0', - 'port_4_default_security_group', - 'port_1_default_security_group', - 'port_2_default_security_group', - 'router_0_external_network', - 'server_1_image', - 'server_1_flavor', - 'server_1_key', - 'server_2_image', - 'server_2_flavor', - 'server_2_key', - 'server_0_image', - 'server_0_flavor', - 'server_0_key']) - - data_ref = set(['floatingip_0', 'floatingip_1', 'floatingip_2', - 'floatingip_3', - 'floatingip_association_2', - 'key_0', - 'network_0', 'network_1', - 'port_1', 'port_2', 'port_4', - 'router_0', - 'router_0_gateway', - 'router_0_interface_0', - 'server_0', 'server_1', 'server_2', - 'subnet_0', 'subnet_2', 'subnet_3', - 'volume_0']) - - def filter_set(self, filtered_set, exclude): - excluded_set = set() - for exc in exclude: - excluded_set.update( - set([e for e in filtered_set if re.search(exc, e)]) - ) - return filtered_set.difference(excluded_set) - - def setUp(self): - super(GenerationTests, self).setUp() - self.mock_neutron.return_value = FakeNeutronManager() - self.mock_nova.return_value = FakeNovaManager() - self.mock_cinder.return_value = FakeCinderManager() - - def test_generation(self): - - exclusion_table = [ - {'call_params': (False, False, False, True, True), - 'resource_filter': [], - 'params_filter': ['^server_\d+_key$'], - 'data_filter': []}, - # No server - {'call_params': (True, False, False, True, True), - 'resource_filter': ['^server'], - 'params_filter': ['^server'], - 'data_filter': ['^server']}, - # No volumes - {'call_params': (False, True, False, True, True), - 'resource_filter': ['^volume'], - 'params_filter': [r'^volume_\d+_volume_type$', - '^server_\d+_key$'], - 'data_filter': ['^volume']}, - # No keys - {'call_params': (False, False, True, True, True), - 'resource_filter': ['^key_\d+$'], - 'params_filter': [], - 'data_filter': ['^key', 'server_\d+_key']}, - # No ports - {'call_params': (False, False, False, True, False), - 'resource_filter': ['^port_\d+$'], - 'params_filter': ['^port_\d+_default_security_group$', - 'server_\d+_key$'], - 'data_filter': ['^port_\d+', '^floatingip_association_\d+$']}, - ] - - for exclusion in exclusion_table: - generator = self.get_generator(*exclusion['call_params']) - resource_ref = self.filter_set(self.resource_ref, - exclusion['resource_filter']) - params_ref = self.filter_set(self.params_ref, - exclusion['params_filter']) - data_ref = self.filter_set(self.data_ref, - exclusion['data_filter']) - - generator.extract_data() - # All the resources, params and datas are present - self.assertEqual(resource_ref, - set(generator.template['resources'].keys()), - "Called with : %r" % (exclusion['call_params'],)) - self.assertEqual(params_ref, - set(generator.template['parameters'].keys()), - "Called with : %r" % (exclusion['call_params'],)) - self.assertEqual(data_ref, - set(generator.stack_data['resources'].keys()), - "Called with : %r" % (exclusion['call_params'],)) - - def test_floating_association_data(self): - generator = self.get_generator(False, False, False, True, True) - generator.extract_data() - # Look for floating ips - assoc_name = 'floatingip_association_2' - association_data = generator.stack_data['resources'][assoc_name] - reference = {'action': 'CREATE', - 'metadata': {}, - 'name': 'floatingip_association_2', - 'resource_data': {}, - 'resource_id': u'floating3:port3', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::FloatingIPAssociation'} - self.assertEqual(reference, association_data) - - def test_port_data(self): - generator = self.get_generator(False, False, False, True, True) - generator.extract_data() - # Look for floating ips - assoc_name = 'port_2' - association_data = generator.stack_data['resources'][assoc_name] - reference = {'action': 'CREATE', - 'metadata': {}, - 'name': 'port_2', - 'resource_data': {}, - 'resource_id': u'port3', - 'status': 'COMPLETE', - 'type': 'OS::Neutron::Port'} - self.assertEqual(reference, association_data) diff --git a/flameclient/tests/test_utils.py b/flameclient/tests/test_utils.py new file mode 100644 index 0000000..097399a --- /dev/null +++ b/flameclient/tests/test_utils.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from flameclient.tests import unittest +from flameclient import utils + + +class TestUtils(unittest.TestCase): + def test_camel_to_snake(self): + for inp, outp in ( + ('camelcase', 'camelcase'), + ('Camelcase', 'camelcase'), + ('camelCase', 'camel_case'), + ('CamelCase', 'camel_case'), + ('camelCCase', 'camel_c_case'), + ('CCamelCase', 'c_camel_case'), + ('CCCCamelCase', 'ccc_camel_case'), + ('CCCcamelCase', 'cc_ccamel_case'), + ('Camel123case', 'camel_123case'), + ('Camel123Case', 'camel_123_case'), + ): + self.assertEqual(utils.camel_to_snake(inp), outp) + + def test_format_option(self): + for option, formated in ( + ('--foo-bar', 'foo_bar'), + ('--this-is-a-long-option', 'this_is_a_long_option') + ): + self.assertEqual( + formated, utils.format_option(option) + ) + + def test_format_option_kwargs(self): + self.assertEqual( + {'foo_bar': 'value'}, utils.format_option_kwargs( + {'--foo-bar': 'value'} + ) + ) diff --git a/flameclient/tests/utils.py b/flameclient/tests/utils.py new file mode 100644 index 0000000..98a4427 --- /dev/null +++ b/flameclient/tests/utils.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import six + +import openstack + +from flameclient.tests import mock +from flameclient.utils import get_deep_attr +from flameclient.utils import munchify + + +def get_mocked_openstackcloud(): + from flameclient.tests.fixtures import openstackcloud + + cloud = mock.Mock(spec_set=openstack.connection.Connection) + # for each self.conn.`fixture_module.NAME`() call we will return + # fixture_module.FIXTURES + for fixture_module in six.itervalues(openstackcloud.FIXTURES): + get_deep_attr( + cloud, fixture_module.NAME + ).return_value = munchify(fixture_module.FIXTURES) + return cloud diff --git a/flameclient/utils.py b/flameclient/utils.py new file mode 100644 index 0000000..7260464 --- /dev/null +++ b/flameclient/utils.py @@ -0,0 +1,363 @@ +# -*- coding: utf-8 -*- + +# This software is released under the MIT License. +# +# Copyright (c) 2018 Orange Cloud for Business / Cloudwatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import argparse +import re +from threading import Lock + +import importlib +import os +import pkgutil +import sys + +import munch +import pkg_resources +import six + +from flameclient import collections_abc + + +CS2STR1 = re.compile('(.)([A-Z][a-z]+)') +CS2STR2 = re.compile('(.)([0-9]+)') +CS2STR3 = re.compile('([a-z0-9])([A-Z])') +REPLSTR = r'\1_\2' + + +def camel_to_snake(string): + """Transform camel_case string to snake_case string""" + return CS2STR3.sub( + REPLSTR, CS2STR2.sub(REPLSTR, CS2STR1.sub(REPLSTR, string)) + ).lower() + + +def sys_path_to_module(path): + full_path = os.path.realpath(os.path.abspath(path)) + path = full_path + while True: + if path in sys.path: + break + path = os.path.dirname(path) + if path == '/': + return '' + return path + + +def get_module_name_from_file(filename): + filename = os.path.realpath(os.path.abspath(filename)) + sys_path = '%s/' % sys_path_to_module(filename) + package_path = filename.split(sys_path, 1)[1] + package_path = package_path.rsplit('.py', 1)[0] + return package_path.replace('/', '.') + + +def load_resource_modules(base_file_or_dir, exclude=tuple()): + """Load packages in a directory + + base_file_or_dir: the base file or dir where we will load packages. + prefix: the prefix to add to module names. + """ + modules = {} + if isinstance(exclude, six.string_types): + exclude = (exclude, ) + full_path = os.path.realpath(os.path.abspath(base_file_or_dir)) + if os.path.isdir(full_path): + pkg_dir = full_path + elif os.path.isfile(full_path): + pkg_dir = os.path.dirname(full_path) + else: + raise ImportError('Can not find "%s"' % full_path) + prefix = "%s." % get_module_name_from_file(pkg_dir) + for _, name, ispkg in pkgutil.iter_modules([pkg_dir], prefix): + # `name` has the form "foo.bar.plop" + # We do not want to load "foo.bar.test*" modules to not load unittest + # files. + # We also do not want to load files starting with an underscore. + if ( + not name.split('.')[-1].startswith('test') and + not name.split('.')[-1].startswith('_') and + name not in exclude and + not ispkg + ): + modules[name] = importlib.import_module(name) + return modules + + +def load_resource_entry_points(name='openstack_flame'): + entry_points = {} + for entry_point in pkg_resources.iter_entry_points(name): + entry_points[entry_point.name] = entry_point.load() + return entry_points + + +def munchify(obj, iterator_type=list, remunch=False): + """Transforms an object into a `munch.Munch` object when possible. + + The difference with `munch.munchify` is that when an object has a + `_to_munch` method, we call this method which was designed for it. + + This method is useful when we have `openstack.resource.Resource` objects + because they have a `_to_munch` method. + + The purpose is to have the same type of objects whether we use + `openstack.connection.Connection` methods or + `shade.openstackcloud.OpenStackCloud` specific methods. + + `openstack.connection.Connection` methods return + `openstack.resource.Resource` subclasses and + `shade.openstackcloud.OpenStackCloud` methods return `munch.Munch` objects. + + :param bool remunch: If True we return a deep copy of the object if it's + already a `munch.Munch` instance. By default we + return the `munch.Munch` object as is. + + + :param function iterator_type: If we have an iterator instead of an + iterable object we return a list by default. + You can change this behaviour by passing the + callable to the type you want (ex.: tuple). + This only affects Iterators and not + iterables for which we keep the same type. + You get an iterator when you have an + `openstack.connection.Connection` instance + and do e.g.: `conn.compute.servers()`. + So if you call + `munchify(conn.compute.servers())` you will + get the iterator_type (a list by default). + """ + + # Check + # https://docs.python.org/2/library/collections.html#collections-abstract-base-classes # noqa + # or + # https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes # noqa + # To know in which order to test. + if not remunch and isinstance(obj, munch.Munch): + return obj + if hasattr(obj, '_to_munch'): + return obj._to_munch() + elif isinstance(obj, collections_abc.Mapping): # Test for `dict` likes. + # Mappings (dicts, OrderedDict, etc...) are iterable so it + # needs to be tested before iterable types. + return munch.Munch( + (key, munchify(value)) for key, value in six.iteritems(obj) + ) + elif isinstance(obj, six.string_types): + # Strings are iterable so this needs to be tested before the + # iterable types. + return obj + elif isinstance(obj, collections_abc.Iterator): + # `Iterator` is a subclass of `Iterable` so we need to test it first. + return iterator_type(munchify(elt) for elt in obj) + elif isinstance(obj, collections_abc.Iterable): + # At last, check if we have an iterable object. + return type(obj)(munchify(elt) for elt in obj) + else: + return munch.munchify(obj) + + +def data_list_to_dict(data_list, enum=True): + data_dict = munch.Munch() + for num, data in enumerate(data_list): + data_dict[data.id] = munchify(data) + if enum: + data_dict[data.id].enum = num + return data_dict + + +def clean_dict(obj, clean_list=True, super_clean=False, iterator_type=list): + """Returns a dict copy with only values which are not None + + :param bool clean_list: whether to suppress or not None values from lists + :param bool super_clean: whether to suppress or not values which evaluate + to False (not only None). + :param function iterator_type: cast function for iterator types. + + """ + + if isinstance(obj, six.string_types): + return obj + elif isinstance(obj, collections_abc.Mapping): + if super_clean: + return type(obj)( + (key, clean_dict( + value, clean_list=clean_list, super_clean=super_clean)) + for key, value in six.iteritems(obj) if value + ) + return type(obj)( + (key, clean_dict( + value, clean_list=clean_list, super_clean=super_clean)) + for key, value in six.iteritems(obj)if value is not None + ) + elif isinstance(obj, collections_abc.Iterator): + if super_clean: + return iterator_type( + clean_dict(elt, clean_list=clean_list, super_clean=super_clean) + for elt in obj if elt or not clean_list + ) + return iterator_type( + clean_dict(elt, clean_list=clean_list, super_clean=super_clean) + for elt in obj if elt is not None or not clean_list + ) + elif isinstance(obj, collections_abc.Iterable): + # At last, check if we have an iterable object. + if super_clean: + return type(obj)( + clean_dict(elt, clean_list=clean_list, super_clean=super_clean) + for elt in obj if elt or not clean_list + ) + return type(obj)( + clean_dict(elt, clean_list=clean_list, super_clean=super_clean) + for elt in obj if elt is not None or not clean_list + ) + else: + return obj + + +def format_option(option_str): + option_str = option_str.lstrip('-') + option_str = option_str.replace('-', '_') + return option_str + + +def format_option_kwargs(kwargs): + """Format a dictionary with option names to dict keys. + + This will allow further keyword arguments passing of argparse options. + + example: + change `{'--foo-bar': 'value'}` to `{'foo_bar': 'value'}` etc... + + """ + return { + format_option(key): value for key, value in six.iteritems(kwargs) + } + + +def dict_to_options(kwargs): + if kwargs is not None: + if isinstance(kwargs, argparse.Namespace): + return kwargs + return argparse.Namespace(**format_option_kwargs(kwargs)) + return None + + +def rename_os_kwargs(kwargs, clean=False): + """Clean Openstack kwargs from the 'os_' prefix. + + envvars return 'os_username', 'os_auth_url', etc... variables. + we want to remove the 'os_' prefix + + :param dict kwargs: the dictionary to clean + + :param bool clean: If true, the returned dictionary will not have keys + with empty values. + + """ + new_kwargs = {} + for key, value in six.iteritems(kwargs): + if value or not clean: + if key.startswith('os_'): + new_kwargs[key.split('os_')[1]] = value + else: + new_kwargs[key] = value + return new_kwargs + + +def rename_os_options(options, clean=False): + """Clean Openstack envvars from the 'os_' prefix. + + envvars return 'os_username', 'os_auth_url', etc... variables. + we want to remove the 'os_' prefix + + :param argparse.Namespace options: the options to clean + + :param bool clean: If true, the returned options will not have + attributes with empty values. + """ + kwargs = vars(options) + renamed_kwargs = rename_os_kwargs(kwargs, clean=clean) + return argparse.Namespace(**renamed_kwargs) + + +def hash_func_call(func, *args, **kwargs): + """hash a function call""" + # kwargs is a dict, and dicts are not hashable. + kwargs_tuple = tuple((key, value) for key, value in six.iteritems(kwargs)) + return hash((func, args, kwargs_tuple)) + + +def get_deep_attr(obj, value): + """Get deep attribute + + example, `getattr(some_object, 'attr.subattr')` does not work. + With get_deep_attr it works. + + """ + subelts = value.split('.', 1) + if len(subelts) == 1: + return getattr(obj, value) + else: + return get_deep_attr(getattr(obj, subelts[0]), subelts[1]) + + +def memoized_property(func): + """Decorator to set properties which will be computed only once + + """ + attr_name = '__%s' % func.__name__ + lock = Lock() + + class FixedProperty(object): + def __get__(self, instance, owner): + if instance: + with lock: + try: + return getattr(instance, attr_name) + except AttributeError: + result = func(instance) + setattr(instance, attr_name, result) + return result + + return self + + def __set__(self, instance, value): + with lock: + setattr(instance, attr_name, value) + + def __delete__(self, instance): + with lock: + delattr(instance, attr_name) + + return FixedProperty() + + +class ClassProperty(classmethod): + + def __get__(self, instance, owner): + return self.__func__(owner) + + def __set__(self, instance, value): + setattr(instance, self.__func__.__name__, value) + + def __delete__(self, instance): + delattr(instance, self.__func__) diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..52349ed --- /dev/null +++ b/pylintrc @@ -0,0 +1,110 @@ +[MASTER] +profile=no +persistent=yes +ignore=migrations + + +[MESSAGES CONTROL] +# C0103 Invalid %s name "%s" +# C0111 Missing docstring +# C0302 Too many lines in module +# C0330 Wrong hanging indentation before block (We let pep8 check this) +# E0611 No name %r in module %r +# E1101 %s %r has no %r member +# E1102 %s is not callable +# E1133 Non-iterable value %s is used in an iterating context # (caused by flameclient.utils.memoized_property) +# E1135 Value '%s' doesn't support membership test # (caused by flameclient.utils.memoized_property) +# E1136 %s is unsubscriptable # (caused by flameclient.utils.fixed_property) +# F0401 Unable to import %s +# I0011 Warning locally suppressed using disable-msg +# I0012 Warning locally suppressed using disable-msg +# R0123 Comparison to literal +# R0201 Method could be a function +# R0901 Too many ancestors +# R0902 Too many instance attributes +# R0904 Too many public methods +# R0911 Too many return statements +# R0912 Too many branches +# R0914 Too many local variables +# R0915 Too many statements +# R1705 Unnecessary "else" after "return" +# R1710 inconsistent-return-statements +# W0142 Used * or * magic* Used when a function or method is called using *args or **kwargs to dispatch arguments. +# W0212 Access to a protected member %s of a client class +# W0223 Method %r is abstract in class %r but is not overridden +# W0232 Class has no __init__ method Used when a class has no __init__ method, neither its parent classes. +# W0403 Relative import '%s', should be '%s' +# W0511 Used when a warning note as FIXME or XXX is detected. +# W0613 Unused argument %r Used when a function or method argument is not used. +# W0702 No exception's type specified Used when an except clause doesn't specify exceptions type to catch. +# W0704 Except doesn't do anything Used when an except clause does nothing but "pass" and there is no "else" clause +# W1113 keyword-arg-before-vararg +# example: +# disable=C0111,I0011,I0012,R0201,W0142,W0212,W0232,W0613,W0702,W0704 +# or: +# disable=I0011,I0012,W0142,W0212,W0232 +disable=C0103,C0111,C0302,C0330,E0611,E1101,E1102,E1133,E1135,E1136,I0011,I0012,R0123,R0901,R0902,R0904,R0911,R0912,R0914,R0915,R1705,R1710,W0142,W0212,W0223,W0403,W0511,W0613,W0702,W0704,W1113 + + +[REPORTS] +include-ids=yes +output-format=parseable +#reports=yes + + +[BASIC] +#no-docstring-rgx=__.*__|_.* +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|logger|register|urlpatterns)$ +method-rgx=([a-z_][a-z0-9_]{2,30}|setUp|tearDown|test_[a-z0-9_]{2,60}|assert[a-zA-Z0-9]{2,30})$ +#good-names=_,i,j,k,e,v,db,qs,pk +good-names=_,i,j,k,e,v,by,fp,td,tr + + +[TYPECHECK] +ignore-mixin-members=yes +#ignored-classes=SQLObject,WSGIRequest +#zope=no +#generated-members=objects,DoesNotExist,id,pk,_meta,base_fields,context + + +[VARIABLES] +#init-import=no +#dummy-variables-rgx=_|dummy +#additional-builtins= + + +[SIMILARITIES] +min-similarity-lines=6 +#ignore-comments=yes +#ignore-docstrings=yes +ignore-imports=yes + + +[MISCELLANEOUS] +notes=FIXME,XXX,TODO + + +[FORMAT] +max-line-length=1000 +max-module-lines=500 +indent-string=' ' + + +[CLASSES] +#defining-attr-methods=__init__,__new__,setUp + + +[DESIGN] +max-args=10 +max-locals=15 +max-returns=6 +max-branchs=12 +max-statements=50 +max-parents=7 +max-attributes=7 +min-public-methods=0 +max-public-methods=50 + + +[IMPORTS] +#deprecated-modules=regsub,TERMIOS,Bastion,rexec diff --git a/requirements.txt b/requirements.txt index f21f768..726d296 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,20 @@ -pbr>=1.6 -Babel>=1.3 -futures - -netaddr>=0.7.12,!=0.7.16 -python-keystoneclient -python-neutronclient -python-novaclient -python-cinderclient -PyYAML +certifi==2018.8.24 +futures>=3.1.1,<=3.2.0 +keystoneauth1==3.11.0 +ndg-httpsclient==0.5.1 +netaddr==0.7.19 +openstacksdk==0.17.2 +os-client-config==1.31.2 +pbr==4.2.0 +pyasn1==0.4.4 +pyOpenSSL==18.0.0 +python-cinderclient==4.0.1 +python-glanceclient==2.12.1 +python-heatclient==1.16.1 +python-keystoneclient==3.17.0 +python-neutronclient==6.10.0 +python-novaclient==11.0.0 +python-openstackclient==3.16.1 +python-swiftclient==3.6.0 +PyYAML==3.13 +shade==1.29.0 diff --git a/setup.cfg b/setup.cfg index 1781f26..6c474c4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ name = python-flameclient summary = Automatic Heat template generation description-file = README.rst -author = CloudWatt +author = Orange Cloud for Business / CloudWatt author-email = info@cloudwatt.com home-page = http://www.cloudwatt.com/ classifier = diff --git a/test-requirements.txt b/test-requirements.txt index 0cf0d0a..892103b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,13 +1,17 @@ -hacking>=0.10.2,<0.11 - -futures -coverage>=3.6 -discover -mock>=1.2 -fixtures>=1.3.1 -python-subunit -sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 -oslosphinx -testrepository>=0.0.18 -testscenarios>=0.4 -testtools>=1.4.0 +coverage==4.5.1 +discover==0.4.0 +fixtures==3.0.0 +hacking==1.1.0 +mccabe<0.6,>=0.2.1 +mock==2.0.0 +oslosphinx==4.18.0 +os-testr==1.0.0 +pycodestyle==2.0.0 +pylint==1.9.3 +python-subunit==1.3.0 +sphinx==1.8.0 +stestr==2.1.1 +testrepository==0.0.20 +testscenarios==0.5.0 +testtools==2.3.0 +tox==3.4.0 diff --git a/tox.ini b/tox.ini index 34886c8..2de6d52 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] minversion = 1.6 -envlist = py33,py27,pypy,pep8 +envlist = py37,py36,py35,py34,py27,pypy,pep8,pylint skipsdist = True [testenv] usedevelop = True -install_command = pip install -U {opts} {packages} +install_command = pip install -c {toxinidir}/upper-constraints.txt -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt @@ -13,7 +13,10 @@ deps = -r{toxinidir}/requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:pep8] -commands = flake8 +commands = python -m flake8 flameclient + +[testenv:pylint] +commands = python -m pylint --rcfile=pylintrc --reports=yes flameclient [testenv:venv] commands = {posargs} @@ -30,4 +33,4 @@ commands = python setup.py build_sphinx show-source = True ignore = H803 builtins = _ -exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,flameclient/tests/fixtures/openstackcloud/* diff --git a/upper-constraints.txt b/upper-constraints.txt new file mode 100644 index 0000000..003c6d0 --- /dev/null +++ b/upper-constraints.txt @@ -0,0 +1,552 @@ +ntlm-auth===1.2.0 +voluptuous===0.11.5 +chardet===3.0.4 +rsa===3.4.2 +restructuredtext-lint===1.1.3 +netmiko===2.2.2 +instack-undercloud===9.3.0 +PasteDeploy===1.5.2 +typing===3.6.6 +python-saharaclient===2.0.0 +python-hnvclient===0.1.0 +Routes===2.4.1 +rtslib-fb===2.1.66 +XStatic-Angular-Bootstrap===2.2.0.0 +paunch===3.2.0 +WebOb===1.8.2 +sphinxcontrib-actdiag===0.8.5 +docopt===0.6.2 +pecan===1.3.2 +ryu===4.27 +os-api-ref===1.5.0 +python-ldap===3.1.0 +oslo.concurrency===3.27.0 +websocket-client===0.53.0 +osprofiler===2.3.0 +tabulate===0.8.2 +python-ironic-inspector-client===3.3.0 +lxml===4.2.5 +python-kingbirdclient===0.2.1 +setproctitle===1.1.10 +pytest===3.8.0 +python-etcd===0.4.5 +raven===6.9.0 +cursive===0.2.2 +oslo.service===1.32.0 +django-appconf===1.0.2 +pykerberos===1.2.1 +certifi===2018.8.24 +sphinxcontrib-nwdiag===0.9.5 +requests-aws===0.1.8 +alabaster===0.7.11 +pbr===4.2.0 +munch===2.3.2 +attrs===18.2.0 +microversion-parse===0.2.1 +Pint===0.8.1 +oslo.i18n===3.21.0 +jsonpath-rw-ext===1.1.3 +python-mistralclient===3.7.0 +oslo.context===2.21.0 +python-senlinclient===1.8.0 +rcssmin===1.0.6 +pycadf===2.8.0 +grpcio===1.15.0 +skydive-client===0.4.5 +pysendfile===2.0.1 +fixtures===3.0.0 +neutron-lib===1.18.0 +XStatic-FileSaver===1.3.2.0 +pystache===0.5.4 +XStatic-Font-Awesome===4.7.0.0 +nose===1.3.7 +nosehtmloutput===0.0.5 +waitress===1.1.0 +os-refresh-config===9.1.0 +pysnmp===4.4.6 +sphinxcontrib-websupport===1.1.0 +Mako===1.0.7 +XStatic-angular-ui-router===0.3.1.2 +pyScss===1.3.4 +XStatic-jQuery===1.10.2.1 +jsonmodels===2.3 +ddt===1.2.0 +pyserial===3.4 +ipaddress===1.0.22;python_version=='2.7' +python-freezerclient===1.7.0 +os-xenapi===0.3.4 +python-vitrageclient===2.3.0 +nosexcover===1.0.11 +krest===1.3.1 +psycopg2===2.7.5 +networkx===2.1 +bashate===0.6.0 +XStatic-Angular===1.5.8.0 +pyngus===2.2.4 +Pillow===5.2.0 +zuul-sphinx===0.2.5 +python-mimeparse===1.6.0 +tripleo-common===9.3.0 +Tempita===0.5.2 +ply===3.11 +requests-toolbelt===0.8.0 +simplejson===3.16.0 +suds-jurko===0.6 +python-swiftclient===3.6.0 +pyOpenSSL===18.0.0 +monasca-common===2.11.0 +scipy===1.1.0 +rsd-lib===0.2.2 +XStatic-Jasmine===2.4.1.1 +python-glanceclient===2.12.1 +pyinotify===0.9.6 +debtcollector===1.20.0 +requests-unixsocket===0.1.5 +asn1crypto===0.24.0 +croniter===0.3.25 +python-watcherclient===2.1.0 +MarkupSafe===1.0 +pypowervm===1.1.18 +doc8===0.8.0 +pymongo===3.7.1 +sqlparse===0.2.4 +oslotest===3.6.0 +jsonpointer===2.0 +defusedxml===0.5.0 +netaddr===0.7.19 +pyghmi===1.2.14 +sphinxcontrib-blockdiag===1.5.5 +thrift===0.11.0 +gnocchiclient===7.0.5 +wcwidth===0.1.7 +sphinxcontrib.datatemplates===0.1.0 +jsonpath-rw===1.4.0 +prettytable===0.7.2 +vine===1.1.4 +taskflow===3.2.0 +traceback2===1.4.0 +semantic-version===2.6.0 +virtualbmc===1.4.0 +deprecation===2.0.5 +SQLAlchemy===1.2.11 +pyroute2===0.5.2 +google-auth===1.5.1 +kazoo===2.5.0 +XStatic-roboto-fontface===0.5.0.0 +pyudev===0.21.0 +eventlet===0.24.1 +openstack-doc-tools===1.8.0 +frozendict===1.2 +oslo.messaging===8.1.0 +jira===2.0.0 +extras===1.0.0 +PyJWT===1.6.4 +zVMCloudConnector===1.2.4 +paramiko===2.4.1 +reno===2.11.0 +unicodecsv===0.14.1;python_version=='2.7' +imagesize===1.1.0 +pydot===1.2.4 +pathlib===1.0.1;python_version=='2.7' +urllib3===1.23 +graphviz===0.9 +PyKMIP===0.8.0 +whereto===0.4.0 +python-subunit===1.3.0 +tornado===4.5.3 +pycparser===2.18 +mock===2.0.0 +PyYAML===3.13 +beautifulsoup4===4.6.3 +os-net-config===9.2.0 +ovs===2.9.2 +cryptography===2.3.1 +adal===1.1.0 +backports.ssl-match-hostname===3.5.0.1;python_version=='2.7' +openstack-release-test===1.1.0 +pylxd===2.2.7 +ruamel.ordereddict===0.4.13;python_version=='2.7' +pycryptodomex===3.6.6 +anyjson===0.3.3 +requests-mock===1.5.2 +os-apply-config===9.1.0 +oslosphinx===4.18.0 +mox3===0.26.0 +gunicorn===19.9.0 +textfsm===0.4.1 +unittest2===1.1.0 +django-compressor===2.2 +libvirt-python===4.6.0 +python-zunclient===2.1.0 +asyncio===3.4.3;python_version=='3.4' +asyncio===3.4.3;python_version=='3.5' +asyncio===3.4.3;python_version=='3.6' +tzlocal===1.5.1 +python-novaclient===11.0.0 +bcrypt===3.1.4 +fixtures-git===0.1.0 +os-client-config===1.31.2 +XStatic-Angular-Gettext===2.3.8.0 +XStatic-Hogan===2.0.0.2 +XStatic-objectpath===1.2.1.0 +python-manilaclient===1.24.1 +requests===2.19.1 +snowballstemmer===1.2.1 +Jinja2===2.10 +XStatic-Bootstrap-SCSS===3.3.7.1 +pyzabbix===0.7.4 +ptyprocess===0.6.0 +threadloop===1.0.2 +amqp===2.3.2 +ruamel.yaml===0.15.66 +websockify===0.8.0 +XStatic-JQuery.quicksearch===2.0.3.1 +mpmath===1.0.0 +django-debreach===1.5.2 +sphinx-feature-classification===0.3.0 +XStatic-JQuery-Migrate===1.2.1.1 +appdirs===1.4.3 +tinyrpc===0.9.3 +google-auth-httplib2===0.0.3 +Flask-SQLAlchemy===2.3.2 +daiquiri===1.5.0 +influxdb===5.1.0 +funcparserlib===0.3.6 +passlib===1.7.1 +dib-utils===0.0.11 +cliff===2.13.0 +os-brick===2.5.3 +ansible-runner===1.1.1 +trollius===2.2;python_version=='2.7' +scp===0.11.0 +python-zaqarclient===1.10.0 +funcsigs===1.0.2;python_version=='2.7' +zhmcclient===0.19.0 +lockfile===0.12.2 +dnspython3===1.15.0;python_version=='3.4' +dnspython3===1.15.0;python_version=='3.5' +dnspython3===1.15.0;python_version=='3.6' +ldappool===2.3.0 +termcolor===1.1.0 +hiredis===0.2.0 +google-api-python-client===1.7.4 +castellan===0.19.0 +oslo.versionedobjects===1.33.3 +webcolors===1.8.1 +aodhclient===1.1.1 +autobahn===18.9.2 +SQLAlchemy-Utils===0.33.4 +pluggy===0.7.1 +coverage===4.5.1 +freezegun===0.3.10 +python-pytun===2.2.1 +pyperclip===1.6.4 +cassandra-driver===3.15.1 +mox===0.5.3 +XStatic-Angular-Schema-Form===0.8.13.0 +gabbi===1.44.0 +nwdiag===1.0.4 +XStatic-bootswatch===3.3.7.0 +XStatic-JS-Yaml===3.8.1.0 +XStatic-term.js===0.0.7.0 +oslo.log===3.39.0 +nodeenv===1.3.2 +pylev===1.3.0 +python-searchlightclient===1.3.0 +oslo.middleware===3.36.0 +XStatic-mdi===1.4.57.0 +django-pyscss===2.0.2 +uritemplate===3.0.0 +django-babel===0.6.2 +docutils===0.14 +notifier===1.0.3 +ujson===1.35 +selenium===3.14.0 +python-glareclient===0.5.3 +mypy===0.620;python_version=='3.4' +mypy===0.620;python_version=='3.5' +mypy===0.620;python_version=='3.6' +mistral-lib===1.0.0 +dogtag-pki===10.3.5.1 +XStatic-Angular-UUID===0.0.4.0 +sphinxcontrib-seqdiag===0.8.5 +os-win===4.0.1 +dictdiffer===0.7.1 +retrying===1.3.3 +shade===1.29.0 +pathlib2===2.3.2 +pydotplus===2.0.2 +flask-oslolog===0.1 +jeepney===0.3.1;python_version=='3.4' +jeepney===0.3.1;python_version=='3.5' +jeepney===0.3.1;python_version=='3.6' +stestr===2.1.1 +singledispatch===3.4.0.3;python_version=='2.7' +oslo.serialization===2.27.0 +warlock===1.3.0 +exabgp===4.0.8 +sphinxcontrib-httpdomain===1.7.0 +metalsmith===0.7.0 +thriftpy===0.3.9;python_version=='2.7' +text-unidecode===1.2 +murano-pkg-check===0.3.0 +oslo.vmware===2.31.0 +sqlalchemy-migrate===0.11.0 +python-monascaclient===1.12.1 +ldap3===2.5.1 +requests-ntlm===1.1.0 +python-string-utils===0.6.0 +automaton===1.15.0 +os-service-types===1.3.0 +keyring===15.1.0 +testscenarios===0.5.0 +sphinxcontrib-pecanwsme===0.9.0 +sadisplay===0.4.9 +enum34===1.1.6 +packaging===17.1 +flask-keystone===0.2 +nose-exclude===0.5.0 +psutil===5.4.7 +py===1.6.0 +txaio===18.8.1 +python-qinlingclient===2.0.0 +elasticsearch===2.4.1 +django-nose===1.4.5 +XStatic-JQuery.TableSorter===2.14.5.1 +pifpaf===2.1.1 +pysmi===0.3.1 +blockdiag===1.5.4 +testtools===2.3.0 +Parsley===1.3 +XStatic-tv4===1.2.7.0 +XStatic-JSEncrypt===2.3.1.1 +python-cinderclient===4.0.1 +keystonemiddleware===5.2.0 +django-formtools===2.1 +python-ceilometerclient===2.9.0 +XStatic-Spin===1.2.5.2 +openshift===0.7.1 +tap-as-a-service===3.0.0 +os-traits===0.9.0 +SecretStorage===2.3.1;python_version=='2.7' +SecretStorage===3.1.0;python_version=='3.4' +SecretStorage===3.1.0;python_version=='3.5' +SecretStorage===3.1.0;python_version=='3.6' +opentracing===1.3.0 +XStatic-Rickshaw===1.5.0.0 +iso8601===0.1.12 +tooz===1.62.0 +linecache2===1.0.0 +oauth2client===4.1.3 +idna===2.7 +python-karborclient===1.1.0 +weakrefmethod===1.0.3;python_version=='2.7' +PuLP===1.6.8 +crc16===0.1.1 +protobuf===3.6.1 +os-dpm===1.1.0 +sushy===1.6.0 +python-neutronclient===6.10.0 +pika===0.12.0 +oslo.cache===1.30.1 +WebTest===2.0.30 +openstack.nose-plugin===0.11 +os-collect-config===9.2.0 +python-qpid-proton===0.23.0 +python-octaviaclient===1.6.0 +pysaml2===4.6.2 +requests-oauthlib===1.0.0 +oslo.reports===1.28.0 +ceilometermiddleware===1.3.0 +python-nss===1.0.1 +testrepository===0.0.20 +sympy===1.3 +sphinxmark===0.1.19 +PyNaCl===1.2.1 +osc-lib===1.11.1 +python-consul===1.1.0 +Faker===0.9.1 +more-itertools===4.3.0 +seqdiag===0.9.6 +numpy===1.15.1 +msgpack===0.5.6 +Sphinx===1.8.0 +oslo.config===6.4.0 +tempest===19.0.0 +django-floppyforms===1.7.0 +openstackdocstheme===1.23.2 +osc-placement===1.3.0 +zake===0.2.2 +python-rsdclient===0.1.3 +python-magic===0.4.15 +python-solumclient===2.7.1 +PyMySQL===0.9.2 +kubernetes===7.0.0 +httplib2===0.11.3 +bottle===0.12.13 +betamax===0.8.1 +construct===2.8.22 +pyparsing===2.2.0 +dogpile.cache===0.6.7 +python-barbicanclient===4.7.0 +tricircleclient===0.4.0 +WSME===0.9.3 +proboscis===1.2.6.0 +fortiosclient===0.0.3 +stevedore===1.29.0 +botocore===1.12.4 +xmltodict===0.11.0 +pyasn1===0.4.4 +oslo.rootwrap===5.14.1 +Django===1.11.15;python_version=='2.7' +Django===2.0.8;python_version=='3.4' +Django===2.0.8;python_version=='3.5' +Django===2.0.8;python_version=='3.6' +pexpect===4.6.0 +cmd2===0.8.9;python_version=='2.7' +cmd2===0.9.4;python_version=='3.4' +cmd2===0.9.4;python_version=='3.5' +cmd2===0.9.4;python_version=='3.6' +redis===2.10.6 +jmespath===0.9.3 +click===6.7 +atomicwrites===1.2.1 +docker-pycreds===0.3.0 +XStatic-smart-table===1.4.13.2 +kuryr-lib===0.8.0 +scrypt===0.8.6 +jsonpatch===1.23 +python-daemon===2.2.0 +typed-ast===1.1.0;python_version=='3.4' +typed-ast===1.1.0;python_version=='3.5' +typed-ast===1.1.0;python_version=='3.6' +os-testr===1.0.0 +cotyledon===1.7.1 +stomp.py===4.1.21 +xattr===0.9.6 +systemd-python===234 +python-memcached===1.59 +openstacksdk===0.17.2 +six===1.11.0 +dulwich===0.19.6 +pykafka===2.7.0 +kombu===4.2.1 +distro===1.3.0 +betamax-matchers===0.4.0 +yaql===1.1.3 +requestsexceptions===1.4.0 +testresources===2.0.1 +falcon===1.4.1 +subprocess32===3.5.2;python_version=='2.7' +etcd3gw===0.2.4 +Flask-RESTful===0.3.6 +GitPython===2.1.11 +python-ironicclient===2.5.0 +XStatic===1.0.1 +XStatic-Angular-FileUpload===12.0.4.0 +python-openstackclient===3.16.1 +pyzmq===17.1.2 +oslo.db===4.40.0 +simplegeneric===0.8.1 +python-pcre===0.7 +abclient===0.2.3 +pymemcache===2.0.0 +wrapt===1.10.11 +oslo.privsep===1.29.0 +sphinxcontrib-apidoc===0.2.1 +oslo.policy===1.38.1 +python-muranoclient===1.1.1 +pyeclib===1.5.0 +wsgi-intercept===1.8.0 +ndg-httpsclient===0.5.1;python_version=='2.7' +repoze.lru===0.7 +rfc3986===1.1.0 +tenacity===5.0.2 +python-designateclient===2.10.0 +future===0.16.0 +Paste===2.0.3 +jaeger-client===3.11.0 +XStatic-Json2yaml===0.1.1.0 +boto===2.49.0 +functools32===3.2.3.post2;python_version=='2.7' +os-vif===1.11.1 +python-masakariclient===5.2.0 +Werkzeug===0.14.1 +pyasn1-modules===0.2.2 +entrypoints===0.2.3 +APScheduler===3.5.3 +monotonic===1.5 +python-smaugclient===0.0.8 +python-troveclient===2.16.0 +etcd3===0.8.1 +XStatic-Bootstrap-Datepicker===1.3.1.0 +CouchDB===1.2 +netifaces===0.10.7 +cachetools===2.1.0 +ws4py===0.5.1 +backports-abc===0.5;python_version=='2.7' +keystoneauth1===3.11.0 +statsd===3.3.0 +XenAPI===1.2 +python-keystoneclient===3.17.0 +ceilometer===11.0.0 +demjson===2.2.4 +diskimage-builder===2.17.0 +heat-translator===1.1.0 +python-magnumclient===2.10.0 +docker===3.5.0 +qpid-python===1.36.0.post1;python_version=='2.7' +contextlib2===0.5.5 +XStatic-Angular-lrdragndrop===1.0.2.2 +python-congressclient===1.11.0 +ovsdbapp===0.12.1 +aniso8601===3.0.2 +rjsmin===1.0.12 +icalendar===4.0.2 +configparser===3.5.0;python_version=='2.7' +decorator===4.3.0 +cffi===1.11.5 +futurist===1.7.0 +jsonschema===2.6.0 +python-blazarclient===2.0.1 +alembic===1.0.0 +glance-store===0.26.1 +sphinxcontrib-programoutput===0.11 +sphinx-testing===0.7.2 +dnspython===1.15.0 +oauthlib===2.1.0 +Babel===2.6.0 +logutils===0.3.5 +scandir===1.9.0;python_version=='2.7' +sphinxcontrib-fulltoc===1.2.0 +smmap2===2.0.4 +greenlet===0.4.15 +XStatic-Angular-Vis===4.16.0.0 +confluent-kafka===0.11.5 +xvfbwrapper===0.2.9 +futures===3.2.0;python_version=='2.7' +tosca-parser===1.1.0 +Flask===1.0.2 +happybase===1.1.0;python_version=='2.7' +marathon===0.10.0 +fasteners===0.14.1 +sortedcontainers===2.0.5 +python-tackerclient===0.14.0 +python-heatclient===1.16.1 +kafka-python===1.4.3 +oslo.utils===3.37.0 +python-editor===1.0.3 +gitdb2===2.0.4 +requests-kerberos===0.12.0 +itsdangerous===0.24 +XStatic-jquery-ui===1.12.0.1 +monasca-statsd===1.10.1 +python-dateutil===2.7.3 +virtualenv===16.0.0 +colorama===0.3.9 +ironic-lib===2.14.0 +pytz===2018.5 +XStatic-D3===3.5.17.0 +actdiag===0.5.4 +sysv-ipc===1.0.0 +scikit-learn===0.19.2