New upstream release.
Vincent Bernat
6 years ago
7 | 7 | A simple, yet powerful CloudStack API client for python and the command-line. |
8 | 8 | |
9 | 9 | * Python 2.6+ and 3.3+ support. |
10 | * Async support for Python 3.5+ | |
10 | 11 | * All present and future CloudStack API calls and parameters are supported. |
11 | 12 | * Syntax highlight in the command-line client if Pygments is installed. |
12 | 13 | * BSD license. |
21 | 22 | Usage |
22 | 23 | ----- |
23 | 24 | |
24 | In Python:: | |
25 | In Python: | |
26 | ||
27 | .. code-block:: python | |
25 | 28 | |
26 | 29 | from cs import CloudStack |
27 | 30 | |
81 | 84 | * A ``cloudstack.ini`` file in the current working directory, |
82 | 85 | * A ``.cloudstack.ini`` file in the home directory. |
83 | 86 | |
84 | To use that configuration scheme from your Python code:: | |
87 | To use that configuration scheme from your Python code: | |
88 | ||
89 | .. code-block:: python | |
85 | 90 | |
86 | 91 | from cs import CloudStack, read_config |
87 | 92 | |
132 | 137 | |
133 | 138 | cs.listVirtualMachines(fetch_list=True) |
134 | 139 | |
140 | Async client | |
141 | ------------ | |
142 | ||
143 | ``cs`` provides the ``AIOCloudStack`` class for async/await calls in Python | |
144 | 3.5+. | |
145 | ||
146 | .. code-block:: python | |
147 | ||
148 | from cs import AIOCloudStack, read_config | |
149 | ||
150 | cs = AIOCloudStack(**read_config()) | |
151 | vms = await cs.listVirtualMachines() | |
152 | ||
153 | By default, this client polls CloudStack's async jobs to return actual results | |
154 | for commands that result in an async job being created. You can customize this | |
155 | behavior with ``job_timeout`` (default: None -- wait indefinitely) and | |
156 | ``poll_interval`` (default: 2s). | |
157 | ||
158 | .. code-block:: python | |
159 | ||
160 | cs = AIOCloudStack(**read_config(), job_timeout=300, poll_interval=5) | |
161 | ||
162 | Async deployment of multiple vms | |
163 | ________________________________ | |
164 | ||
165 | .. code-block:: python | |
166 | ||
167 | import asyncio | |
168 | from cs import AIOCloudStack, read_config | |
169 | ||
170 | cs = AIOCloudStack(**read_config()) | |
171 | tasks = [asyncio.ensure_future(cs.deployVirtualMachine(zoneid='', | |
172 | serviceofferingid='', | |
173 | templateid='')) for _ in range(5)] | |
174 | results = [] | |
175 | done, pending = await asyncio.wait(tasks) | |
176 | exceptions = 0 | |
177 | last_exception = None | |
178 | for t in done: | |
179 | if t.exception(): | |
180 | exceptions += 1 | |
181 | last_exception = t.exception() | |
182 | elif t.result(): | |
183 | results.append(t.result()) | |
184 | if exceptions: | |
185 | print(f"{exceptions} deployment(s) failed") | |
186 | raise last_exception | |
187 | ||
188 | # Destroy all of them, but skip waiting on the job results | |
189 | tasks = [cs.destroyVirtualMachine(id=vm['id'], fetch_result=False) | |
190 | for vm in results] | |
191 | await asyncio.wait(tasks) | |
192 | ||
135 | 193 | Links |
136 | 194 | ----- |
137 | 195 |
0 | import argparse | |
1 | import json | |
2 | import os | |
3 | import sys | |
4 | import time | |
5 | from collections import defaultdict | |
6 | ||
7 | try: | |
8 | from configparser import NoSectionError | |
9 | except ImportError: # python 2 | |
10 | from ConfigParser import NoSectionError | |
11 | ||
12 | try: | |
13 | import pygments | |
14 | from pygments.lexers import JsonLexer | |
15 | from pygments.formatters import TerminalFormatter | |
16 | except ImportError: | |
17 | pygments = None | |
18 | ||
19 | from .client import read_config, CloudStack, CloudStackException # noqa | |
20 | ||
21 | ||
22 | __all__ = ['read_config', 'CloudStack', 'CloudStackException'] | |
23 | ||
24 | if sys.version_info >= (3, 5): | |
25 | try: | |
26 | import aiohttp # noqa | |
27 | except ImportError: | |
28 | pass | |
29 | else: | |
30 | from ._async import AIOCloudStack # noqa | |
31 | __all__.append('AIOCloudStack') | |
32 | ||
33 | ||
34 | def main(): | |
35 | parser = argparse.ArgumentParser(description='Cloustack client.') | |
36 | parser.add_argument('--region', metavar='REGION', | |
37 | help='Cloudstack region in ~/.cloudstack.ini', | |
38 | default=os.environ.get('CLOUDSTACK_REGION', | |
39 | 'cloudstack')) | |
40 | parser.add_argument('--post', action='store_true', default=False, | |
41 | help='use POST instead of GET') | |
42 | parser.add_argument('--async', action='store_true', default=False, | |
43 | help='do not wait for async result') | |
44 | parser.add_argument('--quiet', '-q', action='store_true', default=False, | |
45 | help='do not display additional status messages') | |
46 | parser.add_argument('command', metavar="COMMAND", | |
47 | help='Cloudstack API command to execute') | |
48 | ||
49 | def parse_option(x): | |
50 | if '=' not in x: | |
51 | raise ValueError("{!r} is not a correctly formatted " | |
52 | "option".format(x)) | |
53 | return x.split('=', 1) | |
54 | ||
55 | parser.add_argument('arguments', metavar="OPTION=VALUE", | |
56 | nargs='*', type=parse_option, | |
57 | help='Cloudstack API argument') | |
58 | ||
59 | options = parser.parse_args() | |
60 | command = options.command | |
61 | kwargs = defaultdict(set) | |
62 | for arg in options.arguments: | |
63 | key, value = arg | |
64 | kwargs[key].add(value.strip(" \"'")) | |
65 | ||
66 | try: | |
67 | config = read_config(ini_group=options.region) | |
68 | except NoSectionError: | |
69 | raise SystemExit("Error: region '%s' not in config" % options.region) | |
70 | ||
71 | if options.post: | |
72 | config['method'] = 'post' | |
73 | cs = CloudStack(**config) | |
74 | ok = True | |
75 | try: | |
76 | response = getattr(cs, command)(**kwargs) | |
77 | except CloudStackException as e: | |
78 | response = e.args[1] | |
79 | if not options.quiet: | |
80 | sys.stderr.write("Cloudstack error: HTTP response " | |
81 | "{0}\n".format(response.status_code)) | |
82 | sys.stderr.write(response.text) | |
83 | sys.exit(1) | |
84 | ||
85 | if 'Async' not in command and 'jobid' in response and not options.async: | |
86 | if not options.quiet: | |
87 | sys.stderr.write("Polling result... ^C to abort\n") | |
88 | while True: | |
89 | try: | |
90 | res = cs.queryAsyncJobResult(**response) | |
91 | if res['jobstatus'] != 0: | |
92 | response = res | |
93 | if res['jobresultcode'] != 0: | |
94 | ok = False | |
95 | break | |
96 | time.sleep(3) | |
97 | except KeyboardInterrupt: | |
98 | if not options.quiet: | |
99 | sys.stderr.write("Result not ready yet.\n") | |
100 | break | |
101 | ||
102 | data = json.dumps(response, indent=2, sort_keys=True) | |
103 | ||
104 | if pygments and sys.stdout.isatty(): | |
105 | data = pygments.highlight(data, JsonLexer(), TerminalFormatter()) | |
106 | sys.stdout.write(data) | |
107 | sys.stdout.write('\n') | |
108 | sys.exit(int(not ok)) |
0 | import asyncio | |
1 | import ssl | |
2 | ||
3 | import aiohttp | |
4 | ||
5 | from . import CloudStack, CloudStackException | |
6 | from .client import transform | |
7 | ||
8 | ||
9 | class AIOCloudStack(CloudStack): | |
10 | def __init__(self, job_timeout=None, poll_interval=2.0, | |
11 | *args, **kwargs): | |
12 | super().__init__(*args, **kwargs) | |
13 | self.job_timeout = job_timeout | |
14 | self.poll_interval = poll_interval | |
15 | ||
16 | def __getattr__(self, command): | |
17 | async def handler(**kwargs): | |
18 | return (await self._request(command, **kwargs)) | |
19 | return handler | |
20 | ||
21 | async def _request(self, command, json=True, opcode_name='command', | |
22 | fetch_list=False, fetch_result=True, **kwargs): | |
23 | kwarg, kwargs = self._prepare_request(command, json, opcode_name, | |
24 | fetch_list, **kwargs) | |
25 | ||
26 | ssl_context = None | |
27 | if self.cert: | |
28 | ssl_context = ssl.create_default_context(cafile=self.cert) | |
29 | connector = aiohttp.TCPConnector(verify_ssl=self.verify, | |
30 | ssl_context=ssl_context) | |
31 | ||
32 | async with aiohttp.ClientSession(read_timeout=self.timeout, | |
33 | conn_timeout=self.timeout, | |
34 | connector=connector) as session: | |
35 | handler = getattr(session, self.method) | |
36 | ||
37 | done = False | |
38 | final_data = [] | |
39 | page = 1 | |
40 | while not done: | |
41 | if fetch_list: | |
42 | kwargs['page'] = page | |
43 | ||
44 | kwargs = transform(kwargs) | |
45 | kwargs.pop('signature', None) | |
46 | kwargs['signature'] = self._sign(kwargs) | |
47 | response = await handler(self.endpoint, **{kwarg: kwargs}) | |
48 | ||
49 | ctype = response.headers['content-type'].split(';')[0] | |
50 | try: | |
51 | data = await response.json(content_type=ctype) | |
52 | except ValueError as e: | |
53 | msg = "Make sure endpoint URL {!r} is correct.".format( | |
54 | self.endpoint) | |
55 | raise CloudStackException( | |
56 | "HTTP {0} response from CloudStack".format( | |
57 | response.status), | |
58 | response, | |
59 | "{}. {}".format(e, msg)) | |
60 | ||
61 | [key] = data.keys() | |
62 | data = data[key] | |
63 | if response.status != 200: | |
64 | raise CloudStackException( | |
65 | "HTTP {0} response from CloudStack".format( | |
66 | response.status), response, data) | |
67 | if fetch_list: | |
68 | try: | |
69 | [key] = [k for k in data.keys() if k != 'count'] | |
70 | except ValueError: | |
71 | done = True | |
72 | else: | |
73 | final_data.extend(data[key]) | |
74 | page += 1 | |
75 | if fetch_result and 'jobid' in data: | |
76 | try: | |
77 | final_data = await asyncio.wait_for( | |
78 | self._jobresult(data['jobid']), | |
79 | self.job_timeout) | |
80 | except asyncio.TimeoutError: | |
81 | raise CloudStackException( | |
82 | "Timeout waiting for async job result", | |
83 | data['jobid']) | |
84 | done = True | |
85 | else: | |
86 | final_data = data | |
87 | done = True | |
88 | return final_data | |
89 | ||
90 | async def _jobresult(self, jobid): | |
91 | failures = 0 | |
92 | while True: | |
93 | try: | |
94 | j = await self.queryAsyncJobResult(jobid=jobid, | |
95 | fetch_result=False) | |
96 | failures = 0 | |
97 | if j['jobstatus'] != 0: | |
98 | if j['jobresultcode'] != 0 or j['jobstatus'] != 1: | |
99 | raise CloudStackException("Job failure", j) | |
100 | if 'jobresult' not in j: | |
101 | raise CloudStackException("Unkonwn job result", j) | |
102 | return j['jobresult'] | |
103 | ||
104 | except CloudStackException: | |
105 | raise | |
106 | ||
107 | except Exception: | |
108 | failures += 1 | |
109 | if failures > 10: | |
110 | raise | |
111 | ||
112 | await asyncio.sleep(self.poll_interval) |
0 | #! /usr/bin/env python | |
1 | ||
2 | import base64 | |
3 | import hashlib | |
4 | import hmac | |
5 | import os | |
6 | import sys | |
7 | ||
8 | try: | |
9 | from configparser import ConfigParser | |
10 | except ImportError: # python 2 | |
11 | from ConfigParser import ConfigParser | |
12 | ||
13 | try: | |
14 | from urllib.parse import quote | |
15 | except ImportError: # python 2 | |
16 | from urllib import quote | |
17 | ||
18 | import requests | |
19 | ||
20 | PY2 = sys.version_info < (3, 0) | |
21 | ||
22 | if PY2: | |
23 | text_type = unicode # noqa | |
24 | string_type = basestring # noqa | |
25 | integer_types = int, long # noqa | |
26 | else: | |
27 | text_type = str | |
28 | string_type = str | |
29 | integer_types = int | |
30 | ||
31 | if sys.version_info >= (3, 5): | |
32 | try: | |
33 | from cs.async import AIOCloudStack # noqa | |
34 | except ImportError: | |
35 | pass | |
36 | ||
37 | ||
38 | def cs_encode(value): | |
39 | """ | |
40 | Try to behave like cloudstack, which uses | |
41 | java.net.URLEncoder.encode(stuff).replace('+', '%20'). | |
42 | """ | |
43 | if isinstance(value, int): | |
44 | value = str(value) | |
45 | elif PY2 and isinstance(value, text_type): | |
46 | value = value.encode('utf-8') | |
47 | return quote(value, safe=".-*_") | |
48 | ||
49 | ||
50 | def transform(params): | |
51 | for key, value in list(params.items()): | |
52 | if value is None or value == "": | |
53 | params.pop(key) | |
54 | continue | |
55 | if isinstance(value, string_type): | |
56 | continue | |
57 | elif isinstance(value, integer_types): | |
58 | params[key] = text_type(value) | |
59 | elif isinstance(value, (list, tuple, set)): | |
60 | if not value: | |
61 | params.pop(key) | |
62 | else: | |
63 | if isinstance(value, set): | |
64 | value = list(value) | |
65 | if not isinstance(value[0], dict): | |
66 | params[key] = ",".join(value) | |
67 | else: | |
68 | params.pop(key) | |
69 | for index, val in enumerate(value): | |
70 | for k, v in val.items(): | |
71 | params["%s[%d].%s" % (key, index, k)] = v | |
72 | else: | |
73 | raise ValueError(type(value)) | |
74 | return params | |
75 | ||
76 | ||
77 | class CloudStackException(Exception): | |
78 | pass | |
79 | ||
80 | ||
81 | class Unauthorized(CloudStackException): | |
82 | pass | |
83 | ||
84 | ||
85 | class CloudStack(object): | |
86 | def __init__(self, endpoint, key, secret, timeout=10, method='get', | |
87 | verify=True, cert=None, name=None): | |
88 | self.endpoint = endpoint | |
89 | self.key = key | |
90 | self.secret = secret | |
91 | self.timeout = int(timeout) | |
92 | self.method = method.lower() | |
93 | self.verify = verify | |
94 | self.cert = cert | |
95 | self.name = name | |
96 | ||
97 | def __repr__(self): | |
98 | return '<CloudStack: {0}>'.format(self.name or self.endpoint) | |
99 | ||
100 | def __getattr__(self, command): | |
101 | def handler(**kwargs): | |
102 | return self._request(command, **kwargs) | |
103 | return handler | |
104 | ||
105 | def _prepare_request(self, command, json, opcode_name, fetch_list, | |
106 | **kwargs): | |
107 | kwargs.update({ | |
108 | 'apiKey': self.key, | |
109 | opcode_name: command, | |
110 | }) | |
111 | if json: | |
112 | kwargs['response'] = 'json' | |
113 | if 'page' in kwargs or fetch_list: | |
114 | kwargs.setdefault('pagesize', 500) | |
115 | ||
116 | kwarg = 'params' if self.method == 'get' else 'data' | |
117 | return kwarg, kwargs | |
118 | ||
119 | def _request(self, command, json=True, opcode_name='command', | |
120 | fetch_list=False, **kwargs): | |
121 | kwarg, kwargs = self._prepare_request(command, json, opcode_name, | |
122 | fetch_list, **kwargs) | |
123 | ||
124 | done = False | |
125 | final_data = [] | |
126 | page = 1 | |
127 | while not done: | |
128 | if fetch_list: | |
129 | kwargs['page'] = page | |
130 | ||
131 | kwargs = transform(kwargs) | |
132 | kwargs.pop('signature', None) | |
133 | kwargs['signature'] = self._sign(kwargs) | |
134 | ||
135 | response = getattr(requests, self.method)(self.endpoint, | |
136 | timeout=self.timeout, | |
137 | verify=self.verify, | |
138 | cert=self.cert, | |
139 | **{kwarg: kwargs}) | |
140 | ||
141 | try: | |
142 | data = response.json() | |
143 | except ValueError as e: | |
144 | msg = "Make sure endpoint URL '%s' is correct." % self.endpoint | |
145 | raise CloudStackException( | |
146 | "HTTP {0} response from CloudStack".format( | |
147 | response.status_code), response, "%s. " % str(e) + msg) | |
148 | ||
149 | [key] = data.keys() | |
150 | data = data[key] | |
151 | if response.status_code != 200: | |
152 | raise CloudStackException( | |
153 | "HTTP {0} response from CloudStack".format( | |
154 | response.status_code), response, data) | |
155 | if fetch_list: | |
156 | try: | |
157 | [key] = [k for k in data.keys() if k != 'count'] | |
158 | except ValueError: | |
159 | done = True | |
160 | else: | |
161 | final_data.extend(data[key]) | |
162 | page += 1 | |
163 | else: | |
164 | final_data = data | |
165 | done = True | |
166 | return final_data | |
167 | ||
168 | def _sign(self, data): | |
169 | """ | |
170 | Computes a signature string according to the CloudStack | |
171 | signature method (hmac/sha1). | |
172 | """ | |
173 | params = "&".join(sorted([ | |
174 | "=".join((key, cs_encode(value))) | |
175 | for key, value in data.items() | |
176 | ])).lower() | |
177 | digest = hmac.new( | |
178 | self.secret.encode('utf-8'), | |
179 | msg=params.encode('utf-8'), | |
180 | digestmod=hashlib.sha1).digest() | |
181 | return base64.b64encode(digest).decode('utf-8').strip() | |
182 | ||
183 | ||
184 | def read_config(ini_group=None): | |
185 | if not ini_group: | |
186 | ini_group = os.environ.get('CLOUDSTACK_REGION', 'cloudstack') | |
187 | # Try env vars first | |
188 | os.environ.setdefault('CLOUDSTACK_METHOD', 'get') | |
189 | os.environ.setdefault('CLOUDSTACK_TIMEOUT', '10') | |
190 | keys = ['endpoint', 'key', 'secret', 'method', 'timeout'] | |
191 | env_conf = {} | |
192 | for key in keys: | |
193 | if 'CLOUDSTACK_{0}'.format(key.upper()) not in os.environ: | |
194 | break | |
195 | else: | |
196 | env_conf[key] = os.environ['CLOUDSTACK_{0}'.format(key.upper())] | |
197 | else: | |
198 | env_conf['verify'] = os.environ.get('CLOUDSTACK_VERIFY', True) | |
199 | env_conf['cert'] = os.environ.get('CLOUDSTACK_CERT', None) | |
200 | env_conf['name'] = None | |
201 | return env_conf | |
202 | ||
203 | # Config file: $PWD/cloudstack.ini or $HOME/.cloudstack.ini | |
204 | # Last read wins in configparser | |
205 | paths = ( | |
206 | os.path.join(os.path.expanduser('~'), '.cloudstack.ini'), | |
207 | os.path.join(os.getcwd(), 'cloudstack.ini'), | |
208 | ) | |
209 | # Look at CLOUDSTACK_CONFIG first if present | |
210 | if 'CLOUDSTACK_CONFIG' in os.environ: | |
211 | paths += (os.path.expanduser(os.environ['CLOUDSTACK_CONFIG']),) | |
212 | if not any([os.path.exists(c) for c in paths]): | |
213 | raise SystemExit("Config file not found. Tried {0}".format( | |
214 | ", ".join(paths))) | |
215 | conf = ConfigParser() | |
216 | conf.read(paths) | |
217 | try: | |
218 | cs_conf = conf[ini_group] | |
219 | except AttributeError: # python 2 | |
220 | cs_conf = dict(conf.items(ini_group)) | |
221 | cs_conf['name'] = ini_group | |
222 | return cs_conf |
0 | #! /usr/bin/env python | |
1 | ||
2 | import argparse | |
3 | import base64 | |
4 | import hashlib | |
5 | import hmac | |
6 | import json | |
7 | import os | |
8 | import sys | |
9 | import time | |
10 | ||
11 | from collections import defaultdict | |
12 | ||
13 | try: | |
14 | from configparser import ConfigParser, NoSectionError | |
15 | except ImportError: # python 2 | |
16 | from ConfigParser import ConfigParser, NoSectionError | |
17 | ||
18 | try: | |
19 | from urllib.parse import quote | |
20 | except ImportError: # python 2 | |
21 | from urllib import quote | |
22 | ||
23 | try: | |
24 | import pygments | |
25 | from pygments.lexers import JsonLexer | |
26 | from pygments.formatters import TerminalFormatter | |
27 | except ImportError: | |
28 | pygments = None | |
29 | ||
30 | import requests | |
31 | ||
32 | ||
33 | PY2 = sys.version_info < (3, 0) | |
34 | ||
35 | if PY2: | |
36 | text_type = unicode # noqa | |
37 | string_type = basestring # noqa | |
38 | integer_types = int, long # noqa | |
39 | else: | |
40 | text_type = str | |
41 | string_type = str | |
42 | integer_types = int | |
43 | ||
44 | ||
45 | def cs_encode(value): | |
46 | """ | |
47 | Try to behave like cloudstack, which uses | |
48 | java.net.URLEncoder.encode(stuff).replace('+', '%20'). | |
49 | """ | |
50 | if isinstance(value, int): | |
51 | value = str(value) | |
52 | elif PY2 and isinstance(value, text_type): | |
53 | value = value.encode('utf-8') | |
54 | return quote(value, safe=".-*_") | |
55 | ||
56 | ||
57 | def transform(params): | |
58 | for key, value in list(params.items()): | |
59 | if value is None or value == "": | |
60 | params.pop(key) | |
61 | continue | |
62 | if isinstance(value, string_type): | |
63 | continue | |
64 | elif isinstance(value, integer_types): | |
65 | params[key] = text_type(value) | |
66 | elif isinstance(value, (list, tuple, set)): | |
67 | if not value: | |
68 | params.pop(key) | |
69 | else: | |
70 | if isinstance(value, set): | |
71 | value = list(value) | |
72 | if not isinstance(value[0], dict): | |
73 | params[key] = ",".join(value) | |
74 | else: | |
75 | params.pop(key) | |
76 | for index, val in enumerate(value): | |
77 | for k, v in val.items(): | |
78 | params["%s[%d].%s" % (key, index, k)] = v | |
79 | else: | |
80 | raise ValueError(type(value)) | |
81 | return params | |
82 | ||
83 | ||
84 | class CloudStackException(Exception): | |
85 | pass | |
86 | ||
87 | ||
88 | class Unauthorized(CloudStackException): | |
89 | pass | |
90 | ||
91 | ||
92 | class CloudStack(object): | |
93 | def __init__(self, endpoint, key, secret, timeout=10, method='get', | |
94 | verify=True, cert=None, name=None): | |
95 | self.endpoint = endpoint | |
96 | self.key = key | |
97 | self.secret = secret | |
98 | self.timeout = int(timeout) | |
99 | self.method = method.lower() | |
100 | self.verify = verify | |
101 | self.cert = cert | |
102 | self.name = name | |
103 | ||
104 | def __repr__(self): | |
105 | return '<CloudStack: {0}>'.format(self.name or self.endpoint) | |
106 | ||
107 | def __getattr__(self, command): | |
108 | def handler(**kwargs): | |
109 | return self._request(command, **kwargs) | |
110 | return handler | |
111 | ||
112 | def _request(self, command, json=True, opcode_name='command', | |
113 | fetch_list=False, **kwargs): | |
114 | kwargs.update({ | |
115 | 'apiKey': self.key, | |
116 | opcode_name: command, | |
117 | }) | |
118 | if json: | |
119 | kwargs['response'] = 'json' | |
120 | if 'page' in kwargs or fetch_list: | |
121 | kwargs.setdefault('pagesize', 500) | |
122 | ||
123 | kwarg = 'params' if self.method == 'get' else 'data' | |
124 | ||
125 | done = False | |
126 | final_data = [] | |
127 | page = 1 | |
128 | while not done: | |
129 | if fetch_list: | |
130 | kwargs['page'] = page | |
131 | ||
132 | kwargs = transform(kwargs) | |
133 | kwargs.pop('signature', None) | |
134 | kwargs['signature'] = self._sign(kwargs) | |
135 | ||
136 | response = getattr(requests, self.method)(self.endpoint, | |
137 | timeout=self.timeout, | |
138 | verify=self.verify, | |
139 | cert=self.cert, | |
140 | **{kwarg: kwargs}) | |
141 | ||
142 | try: | |
143 | data = response.json() | |
144 | except ValueError as e: | |
145 | msg = "Make sure endpoint URL '%s' is correct." % self.endpoint | |
146 | raise CloudStackException( | |
147 | "HTTP {0} response from CloudStack".format( | |
148 | response.status_code), response, "%s. " % str(e) + msg) | |
149 | ||
150 | [key] = data.keys() | |
151 | data = data[key] | |
152 | if response.status_code != 200: | |
153 | raise CloudStackException( | |
154 | "HTTP {0} response from CloudStack".format( | |
155 | response.status_code), response, data) | |
156 | if fetch_list: | |
157 | try: | |
158 | [key] = [k for k in data.keys() if k != 'count'] | |
159 | except ValueError: | |
160 | done = True | |
161 | else: | |
162 | final_data.extend(data[key]) | |
163 | page += 1 | |
164 | else: | |
165 | final_data = data | |
166 | done = True | |
167 | return final_data | |
168 | ||
169 | def _sign(self, data): | |
170 | """ | |
171 | Computes a signature string according to the CloudStack | |
172 | signature method (hmac/sha1). | |
173 | """ | |
174 | params = "&".join(sorted([ | |
175 | "=".join((key, cs_encode(value))) | |
176 | for key, value in data.items() | |
177 | ])).lower() | |
178 | digest = hmac.new( | |
179 | self.secret.encode('utf-8'), | |
180 | msg=params.encode('utf-8'), | |
181 | digestmod=hashlib.sha1).digest() | |
182 | return base64.b64encode(digest).decode('utf-8').strip() | |
183 | ||
184 | ||
185 | def read_config(ini_group=None): | |
186 | if not ini_group: | |
187 | ini_group = os.environ.get('CLOUDSTACK_REGION', 'cloudstack') | |
188 | # Try env vars first | |
189 | os.environ.setdefault('CLOUDSTACK_METHOD', 'get') | |
190 | os.environ.setdefault('CLOUDSTACK_TIMEOUT', '10') | |
191 | keys = ['endpoint', 'key', 'secret', 'method', 'timeout'] | |
192 | env_conf = {} | |
193 | for key in keys: | |
194 | if 'CLOUDSTACK_{0}'.format(key.upper()) not in os.environ: | |
195 | break | |
196 | else: | |
197 | env_conf[key] = os.environ['CLOUDSTACK_{0}'.format(key.upper())] | |
198 | else: | |
199 | env_conf['verify'] = os.environ.get('CLOUDSTACK_VERIFY', True) | |
200 | env_conf['cert'] = os.environ.get('CLOUDSTACK_CERT', None) | |
201 | env_conf['name'] = None | |
202 | return env_conf | |
203 | ||
204 | # Config file: $PWD/cloudstack.ini or $HOME/.cloudstack.ini | |
205 | # Last read wins in configparser | |
206 | paths = ( | |
207 | os.path.join(os.path.expanduser('~'), '.cloudstack.ini'), | |
208 | os.path.join(os.getcwd(), 'cloudstack.ini'), | |
209 | ) | |
210 | # Look at CLOUDSTACK_CONFIG first if present | |
211 | if 'CLOUDSTACK_CONFIG' in os.environ: | |
212 | paths += (os.path.expanduser(os.environ['CLOUDSTACK_CONFIG']),) | |
213 | if not any([os.path.exists(c) for c in paths]): | |
214 | raise SystemExit("Config file not found. Tried {0}".format( | |
215 | ", ".join(paths))) | |
216 | conf = ConfigParser() | |
217 | conf.read(paths) | |
218 | try: | |
219 | cs_conf = conf[ini_group] | |
220 | except AttributeError: # python 2 | |
221 | cs_conf = dict(conf.items(ini_group)) | |
222 | cs_conf['name'] = ini_group | |
223 | return cs_conf | |
224 | ||
225 | ||
226 | def main(): | |
227 | parser = argparse.ArgumentParser(description='Cloustack client.') | |
228 | parser.add_argument('--region', metavar='REGION', | |
229 | help='Cloudstack region in ~/.cloudstack.ini', | |
230 | default=os.environ.get('CLOUDSTACK_REGION', | |
231 | 'cloudstack')) | |
232 | parser.add_argument('--post', action='store_true', default=False, | |
233 | help='use POST instead of GET') | |
234 | parser.add_argument('--async', action='store_true', default=False, | |
235 | help='do not wait for async result') | |
236 | parser.add_argument('--quiet', '-q', action='store_true', default=False, | |
237 | help='do not display additional status messages') | |
238 | parser.add_argument('command', metavar="COMMAND", | |
239 | help='Cloudstack API command to execute') | |
240 | ||
241 | def parse_option(x): | |
242 | if '=' not in x: | |
243 | raise ValueError("{!r} is not a correctly formatted " | |
244 | "option".format(x)) | |
245 | return x.split('=', 1) | |
246 | ||
247 | parser.add_argument('arguments', metavar="OPTION=VALUE", | |
248 | nargs='*', type=parse_option, | |
249 | help='Cloudstack API argument') | |
250 | ||
251 | options = parser.parse_args() | |
252 | command = options.command | |
253 | kwargs = defaultdict(set) | |
254 | for arg in options.arguments: | |
255 | key, value = arg | |
256 | kwargs[key].add(value.strip(" \"'")) | |
257 | ||
258 | try: | |
259 | config = read_config(ini_group=options.region) | |
260 | except NoSectionError: | |
261 | raise SystemExit("Error: region '%s' not in config" % options.region) | |
262 | ||
263 | if options.post: | |
264 | config['method'] = 'post' | |
265 | cs = CloudStack(**config) | |
266 | ok = True | |
267 | try: | |
268 | response = getattr(cs, command)(**kwargs) | |
269 | except CloudStackException as e: | |
270 | response = e.args[1] | |
271 | if not options.quiet: | |
272 | sys.stderr.write("Cloudstack error: HTTP response " | |
273 | "{0}\n".format(response.status_code)) | |
274 | sys.stderr.write(response.text) | |
275 | sys.exit(1) | |
276 | ||
277 | if 'Async' not in command and 'jobid' in response and not options.async: | |
278 | if not options.quiet: | |
279 | sys.stderr.write("Polling result... ^C to abort\n") | |
280 | while True: | |
281 | try: | |
282 | res = cs.queryAsyncJobResult(**response) | |
283 | if res['jobstatus'] != 0: | |
284 | response = res | |
285 | if res['jobresultcode'] != 0: | |
286 | ok = False | |
287 | break | |
288 | time.sleep(3) | |
289 | except KeyboardInterrupt: | |
290 | if not options.quiet: | |
291 | sys.stderr.write("Result not ready yet.\n") | |
292 | break | |
293 | ||
294 | data = json.dumps(response, indent=2, sort_keys=True) | |
295 | ||
296 | if pygments and sys.stdout.isatty(): | |
297 | data = pygments.highlight(data, JsonLexer(), TerminalFormatter()) | |
298 | sys.stdout.write(data) | |
299 | sys.stdout.write('\n') | |
300 | sys.exit(int(not ok)) | |
301 | ||
302 | ||
303 | if __name__ == '__main__': | |
304 | main() |
0 | 0 | # coding: utf-8 |
1 | from setuptools import setup | |
1 | import sys | |
2 | import setuptools | |
3 | from setuptools import find_packages, setup | |
2 | 4 | |
3 | 5 | with open('README.rst', 'r') as f: |
4 | 6 | long_description = f.read() |
5 | 7 | |
8 | install_requires = ['requests'] | |
9 | extras_require = { | |
10 | 'highlight': ['pygments'], | |
11 | } | |
12 | ||
13 | if int(setuptools.__version__.split(".", 1)[0]) < 18: | |
14 | if sys.version_info[0:2] >= (3, 5): | |
15 | install_requires.append("aiohttp") | |
16 | else: | |
17 | extras_require[":python_version>='3.5'"] = ["aiohttp"] | |
18 | ||
6 | 19 | setup( |
7 | 20 | name='cs', |
8 | version='1.1.1', | |
21 | version='2.0.0', | |
9 | 22 | url='https://github.com/exoscale/cs', |
10 | 23 | license='BSD', |
11 | 24 | author=u'Bruno ReniƩ', |
12 | 25 | description=('A simple yet powerful CloudStack API client for ' |
13 | 26 | 'Python and the command-line.'), |
14 | 27 | long_description=long_description, |
15 | py_modules=('cs',), | |
28 | packages=find_packages(exclude=['tests']), | |
16 | 29 | zip_safe=False, |
17 | 30 | include_package_data=True, |
18 | 31 | platforms='any', |
25 | 38 | 'Programming Language :: Python :: 2', |
26 | 39 | 'Programming Language :: Python :: 3', |
27 | 40 | ), |
28 | install_requires=( | |
29 | 'requests', | |
30 | ), | |
31 | extras_require={ | |
32 | 'highlight': ['pygments'], | |
33 | }, | |
41 | install_requires=install_requires, | |
42 | extras_require=extras_require, | |
34 | 43 | test_suite='tests', |
35 | 44 | entry_points={ |
36 | 45 | 'console_scripts': [ |