pax_global_header 0000666 0000000 0000000 00000000064 13165700337 0014517 g ustar 00root root 0000000 0000000 52 comment=790d0bc640d4cd7d649531fb208e0b6cf9d6cc1f
cs-2.0.0/ 0000775 0000000 0000000 00000000000 13165700337 0012123 5 ustar 00root root 0000000 0000000 cs-2.0.0/.gitignore 0000664 0000000 0000000 00000000042 13165700337 0014107 0 ustar 00root root 0000000 0000000 cs.egg-info
dist
.tox
build
*.pyc
cs-2.0.0/.travis.yml 0000664 0000000 0000000 00000000311 13165700337 0014227 0 ustar 00root root 0000000 0000000 language: python
python: 3.5
sudo: false
env:
- TOXENV=py26
- TOXENV=py27
- TOXENV=py33
- TOXENV=py34
- TOXENV=py35
- TOXENV=lint
install:
- pip install tox
script:
- tox -e $TOXENV
cs-2.0.0/LICENSE 0000664 0000000 0000000 00000002735 13165700337 0013137 0 ustar 00root root 0000000 0000000 Copyright (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.0.0/README.rst 0000664 0000000 0000000 00000013231 13165700337 0013612 0 ustar 00root root 0000000 0000000 CS
==
.. image:: https://travis-ci.org/exoscale/cs.svg?branch=master
:alt: Build Status
:target: https://travis-ci.org/exoscale/cs
A simple, yet powerful CloudStack API client for python and the command-line.
* Python 2.6+ 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
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::
cat $HOME/.cloudstack.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::
$ cs listVirtualMachines
{
"count": 1,
"virtualmachine": [
{
"account": "...",
...
}
]
}
$ 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.
Multiple credentials can be set in ``.cloudstack.ini``. This allows selecting
the credentials or endpoint to use with a command-line flag::
cat $HOME/.cloudstack.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.0.0/cs/ 0000775 0000000 0000000 00000000000 13165700337 0012530 5 ustar 00root root 0000000 0000000 cs-2.0.0/cs/__init__.py 0000664 0000000 0000000 00000007156 13165700337 0014652 0 ustar 00root root 0000000 0000000 import 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.formatters import TerminalFormatter
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 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('--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)
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))
sys.stderr.write(response.text)
sys.exit(1)
if 'Async' not in command and 'jobid' in response and not 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
data = json.dumps(response, indent=2, sort_keys=True)
if pygments and sys.stdout.isatty():
data = pygments.highlight(data, JsonLexer(), TerminalFormatter())
sys.stdout.write(data)
sys.stdout.write('\n')
sys.exit(int(not ok))
cs-2.0.0/cs/__main__.py 0000664 0000000 0000000 00000000073 13165700337 0014622 0 ustar 00root root 0000000 0000000 from . import main
if __name__ == '__main__':
main()
cs-2.0.0/cs/_async.py 0000664 0000000 0000000 00000010371 13165700337 0014360 0 ustar 00root root 0000000 0000000 import 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.0.0/cs/client.py 0000664 0000000 0000000 00000015647 13165700337 0014375 0 ustar 00root root 0000000 0000000 #! /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
PY2 = sys.version_info < (3, 0)
if PY2:
text_type = unicode # noqa
string_type = basestring # noqa
integer_types = int, long # noqa
else:
text_type = str
string_type = str
integer_types = int
if sys.version_info >= (3, 5):
try:
from cs.async import AIOCloudStack # noqa
except ImportError:
pass
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 or value == "":
params.pop(key)
continue
if isinstance(value, string_type):
continue
elif isinstance(value, integer_types):
params[key] = text_type(value)
elif isinstance(value, (list, tuple, set)):
if not value:
params.pop(key)
else:
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 k, v in val.items():
params["%s[%d].%s" % (key, index, k)] = 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):
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
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.update({
'apiKey': self.key,
opcode_name: command,
})
if json:
kwargs['response'] = 'json'
if 'page' in kwargs or fetch_list:
kwargs.setdefault('pagesize', 500)
kwarg = 'params' if self.method == 'get' else 'data'
return kwarg, kwargs
def _request(self, command, json=True, opcode_name='command',
fetch_list=False, **kwargs):
kwarg, kwargs = self._prepare_request(command, json, opcode_name,
fetch_list, **kwargs)
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 = getattr(requests, self.method)(self.endpoint,
timeout=self.timeout,
verify=self.verify,
cert=self.cert,
**{kwarg: kwargs})
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
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
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
return cs_conf
cs-2.0.0/setup.cfg 0000664 0000000 0000000 00000000026 13165700337 0013742 0 ustar 00root root 0000000 0000000 [wheel]
universal = 1
cs-2.0.0/setup.py 0000664 0000000 0000000 00000002572 13165700337 0013643 0 ustar 00root root 0000000 0000000 # coding: utf-8
import sys
import setuptools
from setuptools import find_packages, setup
with open('README.rst', 'r') as f:
long_description = f.read()
install_requires = ['requests']
extras_require = {
'highlight': ['pygments'],
}
if int(setuptools.__version__.split(".", 1)[0]) < 18:
if sys.version_info[0:2] >= (3, 5):
install_requires.append("aiohttp")
else:
extras_require[":python_version>='3.5'"] = ["aiohttp"]
setup(
name='cs',
version='2.0.0',
url='https://github.com/exoscale/cs',
license='BSD',
author=u'Bruno Renié',
description=('A simple yet powerful CloudStack API client for '
'Python and the command-line.'),
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 :: 3',
),
install_requires=install_requires,
extras_require=extras_require,
test_suite='tests',
entry_points={
'console_scripts': [
'cs = cs:main',
],
},
)
cs-2.0.0/tests.py 0000664 0000000 0000000 00000017565 13165700337 0013655 0 ustar 00root root 0000000 0000000 # coding: utf-8
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,
})
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_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,
})
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'
'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',
'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')
self.assertEqual(machines, {})
get.assert_called_once_with(
'localhost', timeout=20, verify=True, cert=None, 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')
self.assertEqual(machines, {})
get.assert_called_once_with(
'localhost', timeout=20, verify=True, cert=None, params={
'apiKey': 'foo',
'response': 'json',
'command': 'listVirtualMachines',
'signature': 'dMRxAZcs2OPK15WUulzUtnrLWD0=',
'templateId': '3',
'temPlateidd': '4',
'zoneId': '2'
},
)
@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, params={
'apiKey': 'foo',
'response': 'json',
'command': 'listVirtualMachines',
'listall': '1',
'unicode_param': u'éèààû',
'signature': 'gABU/KFJKD3FLAgKDuxQoryu4sA=',
},
)
@patch("requests.get")
def test_transformt(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': 'meh'}])
get.assert_called_once_with(
'localhost', timeout=10, cert=None, verify=True, params={
'command': 'listVirtualMachines',
'response': 'json',
'bar[0].foo': 'meh',
'bar[0].baz': 'blah',
'foo': 'foo,bar',
'apiKey': 'foo',
'signature': 'UGUVEfCOfGfOlqoTj1D2m5adr2g=',
},
)
@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, 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.0.0/tox.ini 0000664 0000000 0000000 00000000433 13165700337 0013436 0 ustar 00root root 0000000 0000000 [tox]
envlist =
py26,
py27,
py33,
py34,
py35,
lint
[testenv]
commands =
python setup.py test
deps =
requests
[testenv:py26]
deps =
{[testenv]deps}
mock
[testenv:py27]
deps =
{[testenv]deps}
mock
[testenv:lint]
deps =
flake8
commands =
flake8 cs.py
flake8 tests.py