Complete clients.cyclades unittests
[kamaki] / kamaki / cli / commands / errors.py
1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.command
33
34 from traceback import print_stack, print_exc
35 import logging
36
37 from kamaki.clients import ClientError
38 from kamaki.cli.errors import CLIError, raiseCLIError, CLISyntaxError
39 from kamaki.cli import _debug, kloger
40 from kamaki.cli.utils import format_size
41
42 sendlog = logging.getLogger('clients.send')
43 datasendlog = logging.getLogger('data.send')
44 recvlog = logging.getLogger('clients.recv')
45 datarecvlog = logging.getLogger('data.recv')
46
47
48 class generic(object):
49
50     @classmethod
51     def all(this, foo):
52         def _raise(self, *args, **kwargs):
53             try:
54                 return foo(self, *args, **kwargs)
55             except Exception as e:
56                 if _debug:
57                     print_stack()
58                     print_exc(e)
59                 raiseCLIError(e)
60         return _raise
61
62     @classmethod
63     def _connection(this, foo, base_url):
64         def _raise(self, *args, **kwargs):
65             try:
66                 foo(self, *args, **kwargs)
67             except ClientError as ce:
68                 if ce.status == 401:
69                     raiseCLIError(ce, 'Authorization failed', details=[
70                         'Make sure a valid token is provided:',
71                         '  to check if token is valid: /astakos authenticate',
72                         '  to set token: /config set [.server.]token <token>',
73                         '  to get current token: /config get [server.]token'])
74                 elif ce.status in range(-12, 200) + [302, 401, 403, 500]:
75                     raiseCLIError(ce, importance=3, details=[
76                         'Check if service is up or set to url %s' % base_url,
77                         '  to get url: /config get %s' % base_url,
78                         '  to set url: /config set %s <URL>' % base_url])
79                 elif ce.status == 404\
80                 and 'kamakihttpresponse' in ('%s' % ce).lower():
81                     client = getattr(self, 'client', None)
82                     if not client:
83                         raise
84                     url = getattr(client, 'base_url', '<empty>')
85                     raiseCLIError(ce,
86                         'Invalid service url %s' % url,
87                         details=[
88                         'Please, check if service url is correctly set',
89                         '* to get current url: /config get compute.url',
90                         '* to set url: /config set compute.url <URL>'])
91                 raise
92         return _raise
93
94
95 class astakos(object):
96
97     _token_details = [
98         'To check default token: /config get token',
99         'If set/update a token:',
100         '*  (permanent):    /config set token <token>',
101         '*  (temporary):    re-run with <token> parameter']
102
103     @classmethod
104     def load(this, foo):
105         def _raise(self, *args, **kwargs):
106             r = foo(self, *args, **kwargs)
107             try:
108                 client = getattr(self, 'client')
109             except AttributeError as ae:
110                 raiseCLIError(ae, 'Client setup failure', importance=3)
111             if not getattr(client, 'token', False):
112                 kloger.warning(
113                     'No permanent token (try: kamaki config set token <tkn>)')
114             if not getattr(client, 'base_url', False):
115                 raise CLIError('Missing astakos server URL',
116                     importance=3,
117                     details=['Check if astakos.url is set correctly',
118                     'To get astakos url:   /config get astakos.url',
119                     'To set astakos url:   /config set astakos.url <URL>'])
120             return r
121         return _raise
122
123     @classmethod
124     def authenticate(this, foo):
125         def _raise(self, *args, **kwargs):
126             try:
127                 r = foo(self, *args, **kwargs)
128             except ClientError as ce:
129                 if ce.status == 401:
130                     token = kwargs.get('custom_token', 0) or self.client.token
131                     raiseCLIError(ce,
132                         'Authorization failed for token %s' % token if token\
133                             else 'No token provided',
134                         details=[] if token else this._token_details)
135             self._raise = foo
136             return r
137         return _raise
138
139
140 class history(object):
141     @classmethod
142     def init(this, foo):
143         def _raise(self, *args, **kwargs):
144             r = foo(self, *args, **kwargs)
145             if not hasattr(self, 'history'):
146                 raise CLIError('Failed to load history', importance=2)
147             return r
148         return _raise
149
150     @classmethod
151     def _get_cmd_ids(this, foo):
152         def _raise(self, cmd_ids, *args, **kwargs):
153             if not cmd_ids:
154                 raise CLISyntaxError('Usage: <id1|id1-id2> [id3|id3-id4] ...',
155                     details=self.__doc__.split('\n'))
156             return foo(self, cmd_ids, *args, **kwargs)
157         return _raise
158
159
160 class cyclades(object):
161     about_flavor_id = [
162         'How to pick a valid flavor id:',
163         '* get a list of flavor ids: /flavor list',
164         '* details of flavor: /flavor info <flavor id>']
165
166     about_network_id = [
167         'How to pick a valid network id:',
168         '* get a list of network ids: /network list',
169         '* details of network: /network info <network id>']
170
171     @classmethod
172     def connection(this, foo):
173         return generic._connection(foo, 'compute.url')
174
175     @classmethod
176     def date(this, foo):
177         def _raise(self, *args, **kwargs):
178             try:
179                 return foo(self, *args, **kwargs)
180             except ClientError as ce:
181                 if ce.status == 400 and 'changes-since' in ('%s' % ce):
182                     raise CLIError(
183                         'Incorrect date format for --since',
184                         details=['Accepted date format: d/m/y'])
185                 raise
186         return _raise
187
188     @classmethod
189     def network_id(this, foo):
190         def _raise(self, *args, **kwargs):
191             network_id = kwargs.get('network_id', None)
192             try:
193                 network_id = int(network_id)
194                 return foo(self, *args, **kwargs)
195             except ValueError as ve:
196                 raiseCLIError(ve, 'Invalid network id %s ' % network_id,
197                     details='network id must be a positive integer',
198                     importance=1)
199             except ClientError as ce:
200                 if network_id and ce.status == 404 and\
201                     'network' in ('%s' % ce).lower():
202                         raiseCLIError(ce,
203                             'No network with id %s found' % network_id,
204                             details=this.about_network_id)
205                 raise
206         return _raise
207
208     @classmethod
209     def network_max(this, foo):
210         def _raise(self, *args, **kwargs):
211             try:
212                 return foo(self, *args, **kwargs)
213             except ClientError as ce:
214                 if ce.status == 413:
215                     raiseCLIError(ce,
216                         'Cannot create another network',
217                         details=['Maximum number of networks reached',
218                             '* to get a list of networks: /network list',
219                             '* to delete a network: /network delete <net id>'])
220                 raise
221         return _raise
222
223     @classmethod
224     def network_in_use(this, foo):
225         def _raise(self, *args, **kwargs):
226             network_id = kwargs.get('network_id', None)
227             try:
228                 return foo(self, *args, **kwargs)
229             except ClientError as ce:
230                 if network_id and ce.status == 400:
231                     raiseCLIError(ce,
232                         'Network with id %s does not exist' % network_id,
233                         details=self.about_network_id)
234                 elif network_id or ce.status == 421:
235                     raiseCLIError(ce,
236                         'Network with id %s is in use' % network_id,
237                         details=[
238                             'Disconnect all nics/VMs of this network first',
239                             '* to get nics: /network info %s' % network_id,
240                             '.  (under "attachments" section)',
241                             '* to disconnect: /network disconnect <nic id>'])
242                 raise
243         return _raise
244
245     @classmethod
246     def flavor_id(this, foo):
247         def _raise(self, *args, **kwargs):
248             flavor_id = kwargs.get('flavor_id', None)
249             try:
250                 flavor_id = int(flavor_id)
251                 return foo(self, *args, **kwargs)
252             except ValueError as ve:
253                 raiseCLIError(ve, 'Invalid flavor id %s ' % flavor_id,
254                     details='Flavor id must be a positive integer',
255                     importance=1)
256             except ClientError as ce:
257                 if flavor_id and ce.status == 404 and\
258                     'flavor' in ('%s' % ce).lower():
259                         raiseCLIError(ce,
260                             'No flavor with id %s found' % flavor_id,
261                             details=this.about_flavor_id)
262                 raise
263         return _raise
264
265     @classmethod
266     def server_id(this, foo):
267         def _raise(self, *args, **kwargs):
268             server_id = kwargs.get('server_id', None)
269             try:
270                 server_id = int(server_id)
271                 return foo(self, *args, **kwargs)
272             except ValueError as ve:
273                 raiseCLIError(ve,
274                     'Invalid server(VM) id %s' % server_id,
275                     details=['id must be a positive integer'],
276                     importance=1)
277             except ClientError as ce:
278                 err_msg = ('%s' % ce).lower()
279                 if (ce.status == 404 and 'server' in err_msg)\
280                 or (ce.status == 400 and 'not found' in err_msg):
281                     raiseCLIError(ce,
282                         'server(VM) with id %s not found' % server_id,
283                         details=[
284                             '* to get existing VM ids: /server list',
285                             '* to get VM details: /server info <VM id>'])
286                 raise
287         return _raise
288
289     @classmethod
290     def firewall(this, foo):
291         def _raise(self, *args, **kwargs):
292             profile = kwargs.get('profile', None)
293             try:
294                 return foo(self, *args, **kwargs)
295             except ClientError as ce:
296                 if ce.status == 400 and profile\
297                 and 'firewall' in ('%s' % ce).lower():
298                     raiseCLIError(ce,
299                         '%s is an invalid firewall profile term' % profile,
300                         details=['Try one of the following:',
301                             '* DISABLED: Shutdown firewall',
302                             '* ENABLED: Firewall in normal mode',
303                             '* PROTECTED: Firewall in secure mode'])
304                 raise
305         return _raise
306
307     @classmethod
308     def nic_id(this, foo):
309         def _raise(self, *args, **kwargs):
310             try:
311                 return foo(self, *args, **kwargs)
312             except ClientError as ce:
313                 nic_id = kwargs.get('nic_id', None)
314                 if nic_id and ce.status == 404\
315                 and 'network interface' in ('%s' % ce).lower():
316                     server_id = kwargs.get('server_id', '<no server>')
317                     err_msg = 'No nic %s on server(VM) with id %s' % (
318                         nic_id,
319                         server_id)
320                     raiseCLIError(ce, err_msg, details=[
321                         '* check server(VM) with id %s: /server info %s' % (
322                             server_id,
323                             server_id),
324                         '* list nics for server(VM) with id %s:' % server_id,
325                         '      /server addr %s' % server_id])
326                 raise
327         return _raise
328
329     @classmethod
330     def nic_format(this, foo):
331         def _raise(self, *args, **kwargs):
332             try:
333                 return foo(self, *args, **kwargs)
334             except IndexError as ie:
335                 nic_id = kwargs.get('nic_id', None)
336                 raiseCLIError(ie,
337                     'Invalid format for network interface (nic) %s' % nic_id,
338                     importance=1,
339                     details=[
340                         'nid_id format: nic-<server id>-<nic id>',
341                         '* get nics of a network: /network info <net id>',
342                         '    (listed the "attachments" section)'])
343         return _raise
344
345     @classmethod
346     def metadata(this, foo):
347         def _raise(self, *args, **kwargs):
348             key = kwargs.get('key', None)
349             try:
350                 foo(self, *args, **kwargs)
351             except ClientError as ce:
352                 if key and ce.status == 404\
353                     and 'metadata' in ('%s' % ce).lower():
354                         raiseCLIError(ce, 'No VM metadata with key %s' % key)
355                 raise
356         return _raise
357
358
359 class plankton(object):
360
361     about_image_id = ['How to pick a suitable image:',
362         '* get a list of image ids: /image list',
363         '* details of image: /flavor info <image id>']
364
365     @classmethod
366     def connection(this, foo):
367         return generic._connection(foo, 'image.url')
368
369     @classmethod
370     def id(this, foo):
371         def _raise(self, *args, **kwargs):
372             image_id = kwargs.get('image_id', None)
373             try:
374                 foo(self, *args, **kwargs)
375             except ClientError as ce:
376                 if image_id and (ce.status == 404\
377                     or (ce.status == 400 and
378                         'image not found' in ('%s' % ce).lower())\
379                     or ce.status == 411):
380                         raiseCLIError(ce,
381                             'No image with id %s found' % image_id,
382                             details=this.about_image_id)
383                 raise
384         return _raise
385
386     @classmethod
387     def metadata(this, foo):
388         def _raise(self, *args, **kwargs):
389             key = kwargs.get('key', None)
390             try:
391                 foo(self, *args, **kwargs)
392             except ClientError as ce:
393                 if ce.status == 404 or ((ce.status == 400\
394                     and 'metadata' in ('%s' % ce).lower())):
395                         raiseCLIError(ce,
396                             'No properties with key %s in this image' % key)
397                 raise
398         return _raise
399
400
401 class pithos(object):
402     container_howto = ['To specify a container:',
403     '  1. Set store.container variable (permanent)',
404     '     /config set store.container <container>',
405     '  2. --container=<container> (temporary, overrides 1)',
406     '  3. Use the container:path format (temporary, overrides all)',
407     'For a list of containers: /store list']
408
409     @classmethod
410     def connection(this, foo):
411         return generic._connection(foo, 'store.url')
412
413     @classmethod
414     def quota(this, foo):
415         def _raise(self, *args, **kwargs):
416             try:
417                 return foo(self, *args, **kwargs)
418             except ClientError as ce:
419                 if ce.status == 413:
420                     raiseCLIError(ce, 'User quota exceeded', details=[
421                         '* get quotas:',
422                         '  * upper total limit:      /store quota',
423                         '  * container limit:  /store quota <container>',
424                         '* set a higher quota (if permitted):',
425                         '    /store setquota <quota>[unit] <container>'
426                         '    as long as <container quota> <= <total quota>'])
427                 raise
428         return _raise
429
430     @classmethod
431     def container(this, foo):
432         def _raise(self, *args, **kwargs):
433             dst_cont = kwargs.get('dst_cont', None)
434             try:
435                 return foo(self, *args, **kwargs)
436             except ClientError as ce:
437                 if ce.status == 404 and 'container' in ('%s' % ce).lower():
438                         cont = '%s or %s' % (self.container, dst_cont)\
439                         if dst_cont else self.container
440                         raiseCLIError(ce,
441                             'Is container %s in account %s ?' % (
442                                 cont,
443                                 self.account),
444                             details=this.container_howto)
445                 raise
446         return _raise
447
448     @classmethod
449     def local_path(this, foo):
450         def _raise(self, *args, **kwargs):
451             local_path = kwargs.get('local_path', '<None>')
452             try:
453                 return foo(self, *args, **kwargs)
454             except IOError as ioe:
455                 raiseCLIError(ioe,
456                     'Failed to access file %s' % local_path,
457                     importance=2)
458         return _raise
459
460     @classmethod
461     def object_path(this, foo):
462         def _raise(self, *args, **kwargs):
463             try:
464                 return foo(self, *args, **kwargs)
465             except ClientError as ce:
466                 err_msg = ('%s' % ce).lower()
467                 if (ce.status == 404 or ce.status == 500)\
468                 and 'object' in err_msg and 'not' in err_msg:
469                     raiseCLIError(ce,
470                         'No object %s in %s\'s container %s'\
471                         % (self.path, self.account, self.container),
472                         details=this.container_howto)
473                 raise
474         return _raise
475
476     @classmethod
477     def object_size(this, foo):
478         def _raise(self, *args, **kwargs):
479             size = kwargs.get('size', None)
480             start = kwargs.get('start', 0)
481             end = kwargs.get('end', 0)
482             if size:
483                 try:
484                     size = int(size)
485                 except ValueError as ve:
486                     raiseCLIError(ve,
487                         'Invalid file size %s ' % size,
488                         details=['size must be a positive integer'],
489                         importance=1)
490             else:
491                 try:
492                     start = int(start)
493                 except ValueError as e:
494                     raiseCLIError(e,
495                         'Invalid start value %s in range' % start,
496                         details=['size must be a positive integer'],
497                         importance=1)
498                 try:
499                     end = int(end)
500                 except ValueError as e:
501                     raiseCLIError(e,
502                         'Invalid end value %s in range' % end,
503                         details=['size must be a positive integer'],
504                         importance=1)
505                 if start > end:
506                     raiseCLIError(
507                         'Invalid range %s-%s' % (start, end),
508                         details=['size must be a positive integer'],
509                         importance=1)
510                 size = end - start
511             try:
512                 return foo(self, *args, **kwargs)
513             except ClientError as ce:
514                 err_msg = ('%s' % ce).lower()
515                 if size and (ce.status == 416 or
516                 (ce.status == 400 and\
517                     'object length is smaller than range length' in err_msg)):
518                     raiseCLIError(ce,
519                         'Remote object %s:%s <= %s %s' % (
520                             self.container,
521                             self.path,
522                             format_size(size),
523                             ('(%sB)' % size) if size >= 1024 else ''))
524                 raise
525         return _raise