pax_global_header00006660000000000000000000000064133166164710014522gustar00rootroot0000000000000052 comment=9d8e8003cf2fcd5478d3365593367634933120eb cs-2.3.1/000077500000000000000000000000001331661647100121325ustar00rootroot00000000000000cs-2.3.1/.gitignore000066400000000000000000000000421331661647100141160ustar00rootroot00000000000000cs.egg-info dist .tox build *.pyc cs-2.3.1/.travis.yml000066400000000000000000000002411331661647100142400ustar00rootroot00000000000000dist: trusty sudo: false language: python python: - 2.7 - 3.4 - 3.5 - 3.6 - 3.6-dev - 3.7-dev install: - pip install tox-travis script: - tox cs-2.3.1/LICENSE000066400000000000000000000027351331661647100131460ustar00rootroot00000000000000Copyright (c) 2014, Bruno Renié and contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. cs-2.3.1/MANIFEST.in000066400000000000000000000000521331661647100136650ustar00rootroot00000000000000recursive-include cs *.py include LICENSE cs-2.3.1/README.rst000066400000000000000000000145471331661647100136340ustar00rootroot00000000000000CS == .. image:: https://travis-ci.org/exoscale/cs.svg?branch=master :alt: Build Status :target: https://travis-ci.org/exoscale/cs .. image:: https://img.shields.io/pypi/l/cs.svg :alt: License :target: https://pypi.org/project/cs/ .. image:: https://img.shields.io/pypi/pyversions/cs.svg :alt: Python versions :target: https://pypi.org/project/cs/ A simple, yet powerful CloudStack API client for python and the command-line. * Python 2.7+ and 3.3+ support. * Async support for Python 3.5+. * All present and future CloudStack API calls and parameters are supported. * Syntax highlight in the command-line client if Pygments is installed. * BSD license. Installation ------------ :: pip install cs # with the colored output pip install cs[highlight] # with the async support (Python 3.5+) pip install cs[async] # with both pip install cs[async,highlight] Usage ----- In Python: .. code-block:: python from cs import CloudStack cs = CloudStack(endpoint='https://api.exoscale.ch/compute', key='cloudstack api key', secret='cloudstack api secret') vms = cs.listVirtualMachines() cs.createSecurityGroup(name='web', description='HTTP traffic') From the command-line, this requires some configuration: .. code-block:: console cat $HOME/.cloudstack.ini .. code-block:: ini [cloudstack] endpoint = https://api.exoscale.ch/compute key = cloudstack api key secret = cloudstack api secret # Optional ca authority certificate verify = /path/to/certs/exoscale_ca.crt # Optional client PEM certificate cert = /path/to/client_exoscale.pem Then: .. code-block:: console $ cs listVirtualMachines .. code-block:: json { "count": 1, "virtualmachine": [ { "account": "...", ... } ] } .. code-block:: console $ cs authorizeSecurityGroupIngress \ cidrlist="0.0.0.0/0" endport=443 startport=443 \ securitygroupname="blah blah" protocol=tcp The command-line client polls when async results are returned. To disable polling, use the ``--async`` flag. To find the list CloudStack API calls go to http://cloudstack.apache.org/api.html Configuration ------------- Configuration is read from several locations, in the following order: * The ``CLOUDSTACK_ENDPOINT``, ``CLOUDSTACK_KEY``, ``CLOUDSTACK_SECRET`` and ``CLOUDSTACK_METHOD`` environment variables, * A ``CLOUDSTACK_CONFIG`` environment variable pointing to an ``.ini`` file, * A ``CLOUDSTACK_VERIFY`` (optional) environment variable pointing to a CA authority cert file, * A ``CLOUDSTACK_CERT`` (optional) environment variable pointing to a client PEM cert file, * A ``cloudstack.ini`` file in the current working directory, * A ``.cloudstack.ini`` file in the home directory. To use that configuration scheme from your Python code: .. code-block:: python from cs import CloudStack, read_config cs = CloudStack(**read_config()) Note that ``read_config()`` can raise ``SystemExit`` if no configuration is found. ``CLOUDSTACK_METHOD`` or the ``method`` entry in the configuration file can be used to change the HTTP verb used to make CloudStack requests. By default, requests are made with the GET method but CloudStack supports POST requests. POST can be useful to overcome some length limits in the CloudStack API. ``CLOUDSTACK_TIMEOUT`` or the ``timeout`` entry in the configuration file can be used to change the HTTP timeout when making CloudStack requests (in seconds). The default value is 10. ``CLOUDSTACK_RETRY`` or the ``retry`` entry in the configuration file (integer) can be used to retry ``list`` and ``queryAsync`` requests on failure. The default value is 0, meaning no retry. Multiple credentials can be set in ``.cloudstack.ini``. This allows selecting the credentials or endpoint to use with a command-line flag. .. code-block:: ini [cloudstack] endpoint = https://some-host/api/compute key = api key secret = api secret [exoscale] endpoint = https://api.exoscale.ch/compute key = api key secret = api secret Usage:: $ cs listVirtualMachines --region=exoscale Optionally ``CLOUDSTACK_REGION`` can be used to overwrite the default region ``cloudstack``. Pagination ---------- CloudStack paginates requests. ``cs`` is able to abstract away the pagination logic to allow fetching large result sets in one go. This is done with the ``fetch_list`` parameter:: $ cs listVirtualMachines fetch_list=true Or in Python:: cs.listVirtualMachines(fetch_list=True) Async client ------------ ``cs`` provides the ``AIOCloudStack`` class for async/await calls in Python 3.5+. .. code-block:: python from cs import AIOCloudStack, read_config cs = AIOCloudStack(**read_config()) vms = await cs.listVirtualMachines() By default, this client polls CloudStack's async jobs to return actual results for commands that result in an async job being created. You can customize this behavior with ``job_timeout`` (default: None -- wait indefinitely) and ``poll_interval`` (default: 2s). .. code-block:: python cs = AIOCloudStack(**read_config(), job_timeout=300, poll_interval=5) Async deployment of multiple vms ________________________________ .. code-block:: python import asyncio from cs import AIOCloudStack, read_config cs = AIOCloudStack(**read_config()) tasks = [asyncio.ensure_future(cs.deployVirtualMachine(zoneid='', serviceofferingid='', templateid='')) for _ in range(5)] results = [] done, pending = await asyncio.wait(tasks) exceptions = 0 last_exception = None for t in done: if t.exception(): exceptions += 1 last_exception = t.exception() elif t.result(): results.append(t.result()) if exceptions: print(f"{exceptions} deployment(s) failed") raise last_exception # Destroy all of them, but skip waiting on the job results tasks = [cs.destroyVirtualMachine(id=vm['id'], fetch_result=False) for vm in results] await asyncio.wait(tasks) Links ----- * CloudStack API: http://cloudstack.apache.org/api.html * Example of use: `Get Started with the exoscale API client `_ cs-2.3.1/cs/000077500000000000000000000000001331661647100125375ustar00rootroot00000000000000cs-2.3.1/cs/__init__.py000066400000000000000000000104731331661647100146550ustar00rootroot00000000000000import argparse import json import os import sys import time from collections import defaultdict try: from configparser import NoSectionError except ImportError: # python 2 from ConfigParser import NoSectionError try: import pygments from pygments.lexers import JsonLexer from pygments.styles import get_style_by_name from pygments.formatters import Terminal256Formatter except ImportError: pygments = None from .client import read_config, CloudStack, CloudStackException # noqa __all__ = ['read_config', 'CloudStack', 'CloudStackException'] if sys.version_info >= (3, 5): try: import aiohttp # noqa except ImportError: pass else: from ._async import AIOCloudStack # noqa __all__.append('AIOCloudStack') def _format_json(data, theme): """Pretty print a dict as a JSON, with colors if pygments is present.""" output = json.dumps(data, indent=2, sort_keys=True) if pygments and sys.stdout.isatty(): style = get_style_by_name(theme) formatter = Terminal256Formatter(style=style) return pygments.highlight(output, JsonLexer(), formatter) return output def main(): parser = argparse.ArgumentParser(description='Cloustack client.') parser.add_argument('--region', metavar='REGION', help='Cloudstack region in ~/.cloudstack.ini', default=os.environ.get('CLOUDSTACK_REGION', 'cloudstack')) parser.add_argument('--theme', metavar='THEME', help='Pygments style', default=os.environ.get('CLOUDSTACK_THEME', 'default')) parser.add_argument('--post', action='store_true', default=False, help='use POST instead of GET') parser.add_argument('--async', action='store_true', default=False, help='do not wait for async result') parser.add_argument('--quiet', '-q', action='store_true', default=False, help='do not display additional status messages') parser.add_argument('command', metavar="COMMAND", help='Cloudstack API command to execute') def parse_option(x): if '=' not in x: raise ValueError("{!r} is not a correctly formatted " "option".format(x)) return x.split('=', 1) parser.add_argument('arguments', metavar="OPTION=VALUE", nargs='*', type=parse_option, help='Cloudstack API argument') options = parser.parse_args() command = options.command kwargs = defaultdict(set) for arg in options.arguments: key, value = arg kwargs[key].add(value.strip(" \"'")) try: config = read_config(ini_group=options.region) except NoSectionError: raise SystemExit("Error: region '%s' not in config" % options.region) theme = config.pop('theme', 'default') if options.post: config['method'] = 'post' cs = CloudStack(**config) ok = True try: response = getattr(cs, command)(**kwargs) except CloudStackException as e: response = e.args[1] if not options.quiet: sys.stderr.write("Cloudstack error: HTTP response " "{0}\n".format(response.status_code)) try: response = json.loads(response.text) except ValueError: sys.stderr.write(response.text) sys.stderr.write("\n") sys.exit(1) if 'Async' not in command and 'jobid' in response and not getattr( options, 'async'): if not options.quiet: sys.stderr.write("Polling result... ^C to abort\n") while True: try: res = cs.queryAsyncJobResult(**response) if res['jobstatus'] != 0: response = res if res['jobresultcode'] != 0: ok = False break time.sleep(3) except KeyboardInterrupt: if not options.quiet: sys.stderr.write("Result not ready yet.\n") break sys.stdout.write(_format_json(response, theme=theme)) sys.stdout.write('\n') sys.exit(int(not ok)) cs-2.3.1/cs/__main__.py000066400000000000000000000000731331661647100146310ustar00rootroot00000000000000from . import main if __name__ == '__main__': main() cs-2.3.1/cs/_async.py000066400000000000000000000103711331661647100143670ustar00rootroot00000000000000import asyncio import ssl import aiohttp from . import CloudStack, CloudStackException from .client import transform class AIOCloudStack(CloudStack): def __init__(self, job_timeout=None, poll_interval=2.0, *args, **kwargs): super().__init__(*args, **kwargs) self.job_timeout = job_timeout self.poll_interval = poll_interval def __getattr__(self, command): async def handler(**kwargs): return (await self._request(command, **kwargs)) return handler async def _request(self, command, json=True, opcode_name='command', fetch_list=False, fetch_result=True, **kwargs): kwarg, kwargs = self._prepare_request(command, json, opcode_name, fetch_list, **kwargs) ssl_context = None if self.cert: ssl_context = ssl.create_default_context(cafile=self.cert) connector = aiohttp.TCPConnector(verify_ssl=self.verify, ssl_context=ssl_context) async with aiohttp.ClientSession(read_timeout=self.timeout, conn_timeout=self.timeout, connector=connector) as session: handler = getattr(session, self.method) done = False final_data = [] page = 1 while not done: if fetch_list: kwargs['page'] = page kwargs = transform(kwargs) kwargs.pop('signature', None) kwargs['signature'] = self._sign(kwargs) response = await handler(self.endpoint, **{kwarg: kwargs}) ctype = response.headers['content-type'].split(';')[0] try: data = await response.json(content_type=ctype) except ValueError as e: msg = "Make sure endpoint URL {!r} is correct.".format( self.endpoint) raise CloudStackException( "HTTP {0} response from CloudStack".format( response.status), response, "{}. {}".format(e, msg)) [key] = data.keys() data = data[key] if response.status != 200: raise CloudStackException( "HTTP {0} response from CloudStack".format( response.status), response, data) if fetch_list: try: [key] = [k for k in data.keys() if k != 'count'] except ValueError: done = True else: final_data.extend(data[key]) page += 1 if fetch_result and 'jobid' in data: try: final_data = await asyncio.wait_for( self._jobresult(data['jobid']), self.job_timeout) except asyncio.TimeoutError: raise CloudStackException( "Timeout waiting for async job result", data['jobid']) done = True else: final_data = data done = True return final_data async def _jobresult(self, jobid): failures = 0 while True: try: j = await self.queryAsyncJobResult(jobid=jobid, fetch_result=False) failures = 0 if j['jobstatus'] != 0: if j['jobresultcode'] != 0 or j['jobstatus'] != 1: raise CloudStackException("Job failure", j) if 'jobresult' not in j: raise CloudStackException("Unkonwn job result", j) return j['jobresult'] except CloudStackException: raise except Exception: failures += 1 if failures > 10: raise await asyncio.sleep(self.poll_interval) cs-2.3.1/cs/client.py000066400000000000000000000201641331661647100143720ustar00rootroot00000000000000#! /usr/bin/env python import base64 import hashlib import hmac import os import sys try: from configparser import ConfigParser except ImportError: # python 2 from ConfigParser import ConfigParser try: from urllib.parse import quote except ImportError: # python 2 from urllib import quote import requests from requests.structures import CaseInsensitiveDict PY2 = sys.version_info < (3, 0) if PY2: text_type = unicode # noqa string_type = basestring # noqa integer_types = int, long # noqa binary_type = str else: text_type = str string_type = str integer_types = int binary_type = bytes if sys.version_info >= (3, 5): try: from . import AIOCloudStack # noqa except ImportError: pass PAGE_SIZE = 500 def cs_encode(value): """ Try to behave like cloudstack, which uses java.net.URLEncoder.encode(stuff).replace('+', '%20'). """ if isinstance(value, int): value = str(value) elif PY2 and isinstance(value, text_type): value = value.encode('utf-8') return quote(value, safe=".-*_") def transform(params): for key, value in list(params.items()): if value is None: params.pop(key) continue if isinstance(value, (string_type, binary_type)): continue elif isinstance(value, integer_types): params[key] = text_type(value) elif isinstance(value, (list, tuple, set, dict)): if not value: params.pop(key) else: if isinstance(value, dict): value = [value] if isinstance(value, set): value = list(value) if not isinstance(value[0], dict): params[key] = ",".join(value) else: params.pop(key) for index, val in enumerate(value): for name, v in val.items(): k = "%s[%d].%s" % (key, index, name) params[k] = text_type(v) else: raise ValueError(type(value)) return params class CloudStackException(Exception): pass class Unauthorized(CloudStackException): pass class CloudStack(object): def __init__(self, endpoint, key, secret, timeout=10, method='get', verify=True, cert=None, name=None, retry=0): self.endpoint = endpoint self.key = key self.secret = secret self.timeout = int(timeout) self.method = method.lower() self.verify = verify self.cert = cert self.name = name self.retry = int(retry) def __repr__(self): return ''.format(self.name or self.endpoint) def __getattr__(self, command): def handler(**kwargs): return self._request(command, **kwargs) return handler def _prepare_request(self, command, json, opcode_name, fetch_list, **kwargs): kwargs = CaseInsensitiveDict(kwargs) kwargs.update({ 'apiKey': self.key, opcode_name: command, }) if json: kwargs['response'] = 'json' if 'page' in kwargs or fetch_list: kwargs.setdefault('pagesize', PAGE_SIZE) kwarg = 'params' if self.method == 'get' else 'data' return kwarg, dict(kwargs._store.values()) def _request(self, command, json=True, opcode_name='command', fetch_list=False, headers=None, **params): kind, params = self._prepare_request(command, json, opcode_name, fetch_list, **params) done = False max_retry = self.retry final_data = [] page = 1 while not done: if fetch_list: params['page'] = page params = transform(params) params.pop('signature', None) params['signature'] = self._sign(params) try: response = getattr(requests, self.method)(self.endpoint, headers=headers, timeout=self.timeout, verify=self.verify, cert=self.cert, **{kind: params}) except requests.exceptions.ConnectionError: max_retry -= 1 if ( max_retry < 0 or not command.startswith(('list', 'queryAsync')) ): raise continue max_retry = self.retry try: data = response.json() except ValueError as e: msg = "Make sure endpoint URL '%s' is correct." % self.endpoint raise CloudStackException( "HTTP {0} response from CloudStack".format( response.status_code), response, "%s. " % str(e) + msg) [key] = data.keys() data = data[key] if response.status_code != 200: raise CloudStackException( "HTTP {0} response from CloudStack".format( response.status_code), response, data) if fetch_list: try: [key] = [k for k in data.keys() if k != 'count'] except ValueError: done = True else: final_data.extend(data[key]) page += 1 if len(final_data) >= data.get('count', PAGE_SIZE): done = True else: final_data = data done = True return final_data def _sign(self, data): """ Computes a signature string according to the CloudStack signature method (hmac/sha1). """ params = "&".join(sorted([ "=".join((key, cs_encode(value))) for key, value in data.items() ])).lower() digest = hmac.new( self.secret.encode('utf-8'), msg=params.encode('utf-8'), digestmod=hashlib.sha1).digest() return base64.b64encode(digest).decode('utf-8').strip() def read_config(ini_group=None): if not ini_group: ini_group = os.environ.get('CLOUDSTACK_REGION', 'cloudstack') # Try env vars first os.environ.setdefault('CLOUDSTACK_METHOD', 'get') os.environ.setdefault('CLOUDSTACK_TIMEOUT', '10') keys = ['endpoint', 'key', 'secret', 'method', 'timeout'] env_conf = {} for key in keys: if 'CLOUDSTACK_{0}'.format(key.upper()) not in os.environ: break else: env_conf[key] = os.environ['CLOUDSTACK_{0}'.format(key.upper())] else: env_conf['verify'] = os.environ.get('CLOUDSTACK_VERIFY', True) env_conf['cert'] = os.environ.get('CLOUDSTACK_CERT', None) env_conf['name'] = None env_conf['retry'] = os.environ.get('CLOUDSTACK_RETRY', 0) return env_conf # Config file: $PWD/cloudstack.ini or $HOME/.cloudstack.ini # Last read wins in configparser paths = ( os.path.join(os.path.expanduser('~'), '.cloudstack.ini'), os.path.join(os.getcwd(), 'cloudstack.ini'), ) # Look at CLOUDSTACK_CONFIG first if present if 'CLOUDSTACK_CONFIG' in os.environ: paths += (os.path.expanduser(os.environ['CLOUDSTACK_CONFIG']),) if not any([os.path.exists(c) for c in paths]): raise SystemExit("Config file not found. Tried {0}".format( ", ".join(paths))) conf = ConfigParser() conf.read(paths) try: cs_conf = conf[ini_group] except AttributeError: # python 2 cs_conf = dict(conf.items(ini_group)) cs_conf['name'] = ini_group allowed_keys = ('endpoint', 'key', 'secret', 'timeout', 'method', 'verify', 'cert', 'name', 'retry', 'theme') return dict(((k, v) for k, v in cs_conf.items() if k in allowed_keys)) cs-2.3.1/setup.cfg000066400000000000000000000001121331661647100137450ustar00rootroot00000000000000[wheel] universal = 1 [check-manifest] ignore = tox.ini tests.py cs-2.3.1/setup.py000066400000000000000000000033101331661647100136410ustar00rootroot00000000000000# coding: utf-8 """ A simple yet powerful CloudStack API client for Python and the command-line. """ from __future__ import unicode_literals import sys from codecs import open from setuptools import find_packages, setup with open('README.rst', 'r', encoding='utf-8') as f: long_description = f.read() install_requires = ['requests'] extras_require = { 'highlight': ['pygments'], } tests_require = [] if sys.version_info < (3, 0): tests_require.append("mock") elif sys.version_info >= (3, 5): extras_require["async"] = ["aiohttp"] tests_require.append("aiohttp") setup( name='cs', version='2.3.1', url='https://github.com/exoscale/cs', license='BSD', author='Bruno Renié', description=__doc__.strip(), long_description=long_description, packages=find_packages(exclude=['tests']), zip_safe=False, include_package_data=True, platforms='any', classifiers=( 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ), install_requires=install_requires, extras_require=extras_require, tests_require=tests_require, test_suite='tests', entry_points={ 'console_scripts': [ 'cs = cs:main', ], }, ) cs-2.3.1/tests.py000066400000000000000000000243321331661647100136520ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals import os import sys from contextlib import contextmanager from functools import partial from unittest import TestCase try: from unittest.mock import patch, call except ImportError: from mock import patch, call from cs import CloudStack, CloudStackException, read_config @contextmanager def env(**kwargs): old_env = {} for key in kwargs: if key in os.environ: old_env[key] = os.environ[key] os.environ.update(kwargs) try: yield finally: for key in kwargs: if key in old_env: os.environ[key] = old_env[key] else: del os.environ[key] @contextmanager def cwd(path): initial = os.getcwd() os.chdir(path) try: yield finally: os.chdir(initial) class ConfigTest(TestCase): if sys.version_info < (2, 7): def setUp(self): super(ConfigTest, self).setUp() self._cleanups = [] def addCleanup(self, fn, *args, **kwargs): self._cleanups.append((fn, args, kwargs)) def tearDown(self): super(ConfigTest, self).tearDown() for fn, args, kwargs in self._cleanups: fn(*args, **kwargs) def test_env_vars(self): with env(CLOUDSTACK_KEY='test key from env', CLOUDSTACK_SECRET='test secret from env', CLOUDSTACK_ENDPOINT='https://api.example.com/from-env'): conf = read_config() self.assertEqual(conf, { 'key': 'test key from env', 'secret': 'test secret from env', 'endpoint': 'https://api.example.com/from-env', 'method': 'get', 'timeout': '10', 'verify': True, 'cert': None, 'name': None, 'retry': 0, }) with env(CLOUDSTACK_KEY='test key from env', CLOUDSTACK_SECRET='test secret from env', CLOUDSTACK_ENDPOINT='https://api.example.com/from-env', CLOUDSTACK_METHOD='post', CLOUDSTACK_TIMEOUT='99', CLOUDSTACK_RETRY='5', CLOUDSTACK_VERIFY='/path/to/ca.pem', CLOUDSTACK_CERT='/path/to/cert.pem'): conf = read_config() self.assertEqual(conf, { 'key': 'test key from env', 'secret': 'test secret from env', 'endpoint': 'https://api.example.com/from-env', 'method': 'post', 'timeout': '99', 'verify': '/path/to/ca.pem', 'cert': '/path/to/cert.pem', 'name': None, 'retry': '5', }) def test_current_dir_config(self): with open('/tmp/cloudstack.ini', 'w') as f: f.write('[cloudstack]\n' 'endpoint = https://api.example.com/from-file\n' 'key = test key from file\n' 'secret = test secret from file\n' 'theme = monokai\n' 'other = please ignore me\n' 'timeout = 50') self.addCleanup(partial(os.remove, '/tmp/cloudstack.ini')) with cwd('/tmp'): conf = read_config() self.assertEqual(dict(conf), { 'endpoint': 'https://api.example.com/from-file', 'key': 'test key from file', 'secret': 'test secret from file', 'theme': 'monokai', 'timeout': '50', 'name': 'cloudstack', }) class RequestTest(TestCase): @patch('requests.get') def test_request_params(self, get): cs = CloudStack(endpoint='localhost', key='foo', secret='bar', timeout=20) get.return_value.status_code = 200 get.return_value.json.return_value = { 'listvirtualmachinesresponse': {}, } machines = cs.listVirtualMachines(listall='true', headers={'Accept-Encoding': 'br'}) self.assertEqual(machines, {}) get.assert_called_once_with( 'localhost', timeout=20, verify=True, cert=None, headers={ 'Accept-Encoding': 'br', }, params={ 'apiKey': 'foo', 'response': 'json', 'command': 'listVirtualMachines', 'listall': 'true', 'signature': 'B0d6hBsZTcFVCiioSxzwKA9Pke8=', }, ) @patch('requests.get') def test_request_params_casing(self, get): cs = CloudStack(endpoint='localhost', key='foo', secret='bar', timeout=20) get.return_value.status_code = 200 get.return_value.json.return_value = { 'listvirtualmachinesresponse': {}, } machines = cs.listVirtualMachines(zoneId=2, templateId='3', temPlateidd='4', pageSize='10', fetch_list=True) self.assertEqual(machines, []) get.assert_called_once_with( 'localhost', timeout=20, verify=True, cert=None, headers=None, params={ 'apiKey': 'foo', 'response': 'json', 'command': 'listVirtualMachines', 'signature': 'mMS7XALuGkCXk7kj5SywySku0Z0=', 'templateId': '3', 'temPlateidd': '4', 'zoneId': '2', 'page': '1', 'pageSize': '10', }, ) @patch('requests.get') def test_encoding(self, get): cs = CloudStack(endpoint='localhost', key='foo', secret='bar') get.return_value.status_code = 200 get.return_value.json.return_value = { 'listvirtualmachinesresponse': {}, } cs.listVirtualMachines(listall=1, unicode_param=u'éèààû') get.assert_called_once_with( 'localhost', timeout=10, verify=True, cert=None, headers=None, params={ 'apiKey': 'foo', 'response': 'json', 'command': 'listVirtualMachines', 'listall': '1', 'unicode_param': u'éèààû', 'signature': 'gABU/KFJKD3FLAgKDuxQoryu4sA=', }, ) @patch("requests.get") def test_transform(self, get): cs = CloudStack(endpoint='localhost', key='foo', secret='bar') get.return_value.status_code = 200 get.return_value.json.return_value = { 'listvirtualmachinesresponse': {}, } cs.listVirtualMachines(foo=["foo", "bar"], bar=[{'baz': 'blah', 'foo': 1000}], bytes_param=b'blah') get.assert_called_once_with( 'localhost', timeout=10, cert=None, verify=True, headers=None, params={ 'command': 'listVirtualMachines', 'response': 'json', 'bar[0].foo': '1000', 'bar[0].baz': 'blah', 'foo': 'foo,bar', 'bytes_param': b'blah', 'apiKey': 'foo', 'signature': 'ImJ/5F0P2RDL7yn4LdLnGcEx5WE=', }, ) @patch("requests.get") def test_transform_dict(self, get): cs = CloudStack(endpoint='localhost', key='foo', secret='bar') get.return_value.status_code = 200 get.return_value.json.return_value = { 'scalevirtualmachineresponse': {}, } cs.scaleVirtualMachine(id='a', details={'cpunumber': 1000, 'memory': '640k'}) get.assert_called_once_with( 'localhost', timeout=10, cert=None, verify=True, headers=None, params={ 'command': 'scaleVirtualMachine', 'response': 'json', 'id': 'a', 'details[0].cpunumber': '1000', 'details[0].memory': '640k', 'apiKey': 'foo', 'signature': 'ZNl66z3gFhnsx2Eo3vvCIM0kAgI=', }, ) @patch("requests.get") def test_transform_empty(self, get): cs = CloudStack(endpoint='localhost', key='foo', secret='bar') get.return_value.status_code = 200 get.return_value.json.return_value = { 'createnetworkresponse': {}, } cs.createNetwork(name="", display_text="") get.assert_called_once_with( 'localhost', timeout=10, cert=None, verify=True, headers=None, params={ 'command': 'createNetwork', 'response': 'json', 'name': '', 'display_text': '', 'apiKey': 'foo', 'signature': 'CistTEiPt/4Rv1v4qSyILvPbhmg=', }, ) @patch("requests.post") @patch("requests.get") def test_method(self, get, post): cs = CloudStack(endpoint='localhost', key='foo', secret='bar', method='post') post.return_value.status_code = 200 post.return_value.json.return_value = { 'listvirtualmachinesresponse': {}, } cs.listVirtualMachines(blah='brah') self.assertEqual(get.call_args_list, []) self.assertEqual(post.call_args_list, [ call( 'localhost', timeout=10, verify=True, cert=None, headers=None, data={ 'command': 'listVirtualMachines', 'blah': 'brah', 'apiKey': 'foo', 'response': 'json', 'signature': '58VvLSaVUqHnG9DhXNOAiDFwBoA=', } )] ) @patch("requests.get") def test_error(self, get): get.return_value.status_code = 530 get.return_value.json.return_value = { 'listvirtualmachinesresponse': {'errorcode': 530, 'uuidList': [], 'cserrorcode': 9999, 'errortext': 'Fail'}} cs = CloudStack(endpoint='localhost', key='foo', secret='bar') self.assertRaises(CloudStackException, cs.listVirtualMachines) cs-2.3.1/tox.ini000066400000000000000000000003361331661647100134470ustar00rootroot00000000000000[tox] envlist = py{27,34,35,36,37}-test lint [travis] python = 3.6: py36, lint [testenv] commands = test: python setup.py test lint: pip install flake8 check-manifest lint: flake8 cs tests.py lint: check-manifest