22d381f3dbe49e9cdf8ae71e7c6feb0f3f85662e
[kamaki] / kamaki / cli / commands / errors.py
1 # Copyright 2011-2013 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 from astakosclient import AstakosClientException
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 CLOUDNAME = [
43     'Note: If you use a named cloud, use its name instead of "default"']
44
45
46 class generic(object):
47
48     @classmethod
49     def all(this, foo):
50         def _raise(self, *args, **kwargs):
51             try:
52                 return foo(self, *args, **kwargs)
53             except Exception as e:
54                 if _debug:
55                     print_stack()
56                     print_exc(e)
57                 if isinstance(e, CLIError) or isinstance(e, ClientError):
58                     raiseCLIError(e)
59                 raiseCLIError(e, details=['%s, -d for debug info' % type(e)])
60         return _raise
61
62     @classmethod
63     def _connection(this, foo):
64         def _raise(self, *args, **kwargs):
65             try:
66                 foo(self, *args, **kwargs)
67             except ClientError as ce:
68                 ce_msg = ('%s' % ce).lower()
69                 if ce.status == 401:
70                     raiseCLIError(ce, 'Authorization failed', details=[
71                         'Make sure a valid token is provided:',
72                         '  to check if token is valid: /user authenticate',
73                         '  to set token:',
74                         '    /config set cloud.default.token <token>',
75                         '  to get current token:',
76                         '    /config get cloud.default.token'] + CLOUDNAME)
77                 elif ce.status in range(-12, 200) + [302, 401, 403, 500]:
78                     raiseCLIError(ce, importance=3, details=[
79                         'Check if service is up'])
80                 elif ce.status == 404 and 'kamakihttpresponse' in ce_msg:
81                     client = getattr(self, 'client', None)
82                     if not client:
83                         raise
84                     url = getattr(client, 'base_url', '<empty>')
85                     msg = 'Invalid service URL %s' % url
86                     raiseCLIError(ce, msg, details=[
87                         'Check if authentication URL is correct',
88                         '  check current URL:',
89                         '    /config get cloud.default.url',
90                         '  set new authentication URL:',
91                         '    /config set cloud.default.url'] + CLOUDNAME)
92                 raise
93         return _raise
94
95
96 class user(object):
97
98     _token_details = [
99         'To check default token: /config get cloud.default.token',
100         'If set/update a token:',
101         '*  (permanent):  /config set cloud.default.token <token>',
102         '*  (temporary):  re-run with <token> parameter'] + CLOUDNAME
103
104     @classmethod
105     def astakosclient(this, foo):
106         def _raise(self, *args, **kwargs):
107             try:
108                 r = foo(self, *args, **kwargs)
109             except AstakosClientException as ace:
110                 raiseCLIError(ace, 'Error in synnefo-AstakosClient')
111             return r
112         return _raise
113
114     @classmethod
115     def load(this, foo):
116         def _raise(self, *args, **kwargs):
117             r = foo(self, *args, **kwargs)
118             try:
119                 client = getattr(self, 'client')
120             except AttributeError as ae:
121                 raiseCLIError(ae, 'Client setup failure', importance=3)
122             if not getattr(client, 'token', False):
123                 kloger.warning(
124                     'No permanent token (try:'
125                     ' kamaki config set cloud.default.token <tkn>)')
126             if not getattr(client, 'astakos_base_url', False):
127                 msg = 'Missing synnefo authentication URL'
128                 raise CLIError(msg, importance=3, details=[
129                     'Check if authentication URL is correct',
130                         '  check current URL:',
131                         '    /config get cloud.default.url',
132                         '  set new auth. URL:',
133                         '    /config set cloud.default.url'] + CLOUDNAME)
134             return r
135         return _raise
136
137     @classmethod
138     def authenticate(this, foo):
139         def _raise(self, *args, **kwargs):
140             try:
141                 return foo(self, *args, **kwargs)
142             except (ClientError, AstakosClientException) as ce:
143                 if ce.status == 401:
144                     token = kwargs.get('custom_token', 0) or self.client.token
145                     msg = ('Authorization failed for token %s' % token) if (
146                         token) else 'No token provided',
147                     details = [] if token else this._token_details
148                     raiseCLIError(ce, msg, details=details)
149                 raise ce
150             self._raise = foo
151         return _raise
152
153
154 class history(object):
155     @classmethod
156     def init(this, foo):
157         def _raise(self, *args, **kwargs):
158             r = foo(self, *args, **kwargs)
159             if not hasattr(self, 'history'):
160                 raise CLIError('Failed to load history', importance=2)
161             return r
162         return _raise
163
164     @classmethod
165     def _get_cmd_ids(this, foo):
166         def _raise(self, cmd_ids, *args, **kwargs):
167             if not cmd_ids:
168                 raise CLISyntaxError(
169                     'Usage: <id1|id1-id2> [id3|id3-id4] ...',
170                     details=self.__doc__.split('\n'))
171             return foo(self, cmd_ids, *args, **kwargs)
172         return _raise
173
174
175 class cyclades(object):
176     about_flavor_id = [
177         'How to pick a valid flavor id:',
178         '* get a list of flavor ids: /flavor list',
179         '* details of flavor: /flavor info <flavor id>']
180
181     about_network_id = [
182         'How to pick a valid network id:',
183         '* get a list of network ids: /network list',
184         '* details of network: /network info <network id>']
185
186     @classmethod
187     def connection(this, foo):
188         return generic._connection(foo)
189
190     @classmethod
191     def date(this, foo):
192         def _raise(self, *args, **kwargs):
193             try:
194                 return foo(self, *args, **kwargs)
195             except ClientError as ce:
196                 if ce.status == 400 and 'changes-since' in ('%s' % ce):
197                     raise CLIError(
198                         'Incorrect date format for --since',
199                         details=['Accepted date format: d/m/y'])
200                 raise
201         return _raise
202
203     @classmethod
204     def cluster_size(this, foo):
205         def _raise(self, *args, **kwargs):
206             size = kwargs.get('size', None)
207             try:
208                 size = int(size)
209                 assert size > 0, 'Cluster size must be a positive integer'
210                 return foo(self, *args, **kwargs)
211             except ValueError as ve:
212                 msg = 'Invalid cluster size value %s' % size
213                 raiseCLIError(ve, msg, importance=1, details=[
214                     'Cluster size must be a positive integer'])
215             except AssertionError as ae:
216                 raiseCLIError(
217                     ae, 'Invalid cluster size %s' % size, importance=1)
218             except ClientError:
219                 raise
220         return _raise
221
222     @classmethod
223     def network_id(this, foo):
224         def _raise(self, *args, **kwargs):
225             network_id = kwargs.get('network_id', None)
226             try:
227                 network_id = int(network_id)
228                 return foo(self, *args, **kwargs)
229             except ValueError as ve:
230                 msg = 'Invalid network id %s ' % network_id
231                 details = 'network id must be a positive integer'
232                 raiseCLIError(ve, msg, details=details, importance=1)
233             except ClientError as ce:
234                 if network_id and ce.status == 404 and (
235                     'network' in ('%s' % ce).lower()
236                 ):
237                     msg = 'No network with id %s found' % network_id,
238                     raiseCLIError(ce, msg, details=this.about_network_id)
239                 raise
240         return _raise
241
242     @classmethod
243     def network_max(this, foo):
244         def _raise(self, *args, **kwargs):
245             try:
246                 return foo(self, *args, **kwargs)
247             except ClientError as ce:
248                 if ce.status == 413:
249                     msg = 'Cannot create another network',
250                     details = [
251                         'Maximum number of networks reached',
252                         '* to get a list of networks: /network list',
253                         '* to delete a network: /network delete <net id>']
254                     raiseCLIError(ce, msg, details=details)
255                 raise
256         return _raise
257
258     @classmethod
259     def network_in_use(this, foo):
260         def _raise(self, *args, **kwargs):
261             network_id = kwargs.get('network_id', None)
262             try:
263                 return foo(self, *args, **kwargs)
264             except ClientError as ce:
265                 if network_id and ce.status in (400, ):
266                     msg = 'Network with id %s does not exist' % network_id,
267                     raiseCLIError(ce, msg, details=this.about_network_id)
268                 elif network_id or ce.status in (421, ):
269                     msg = 'Network with id %s is in use' % network_id,
270                     raiseCLIError(ce, msg, details=[
271                         'Disconnect all nics/servers of this network first',
272                         '* to get nics: /network info %s' % network_id,
273                         '.  (under "attachments" section)',
274                         '* to disconnect: /network disconnect <nic id>'])
275                 raise
276         return _raise
277
278     @classmethod
279     def flavor_id(this, foo):
280         def _raise(self, *args, **kwargs):
281             flavor_id = kwargs.get('flavor_id', None)
282             try:
283                 flavor_id = int(flavor_id)
284                 return foo(self, *args, **kwargs)
285             except ValueError as ve:
286                 msg = 'Invalid flavor id %s ' % flavor_id,
287                 details = 'Flavor id must be a positive integer'
288                 raiseCLIError(ve, msg, details=details, importance=1)
289             except ClientError as ce:
290                 if flavor_id and ce.status == 404 and (
291                     'flavor' in ('%s' % ce).lower()
292                 ):
293                         msg = 'No flavor with id %s found' % flavor_id,
294                         raiseCLIError(ce, msg, details=this.about_flavor_id)
295                 raise
296         return _raise
297
298     @classmethod
299     def server_id(this, foo):
300         def _raise(self, *args, **kwargs):
301             server_id = kwargs.get('server_id', None)
302             try:
303                 server_id = int(server_id)
304                 return foo(self, *args, **kwargs)
305             except ValueError as ve:
306                 msg = 'Invalid virtual server id %s' % server_id,
307                 details = 'Server id must be a positive integer'
308                 raiseCLIError(ve, msg, details=details, importance=1)
309             except ClientError as ce:
310                 err_msg = ('%s' % ce).lower()
311                 if (
312                     ce.status == 404 and 'server' in err_msg
313                 ) or (
314                     ce.status == 400 and 'not found' in err_msg
315                 ):
316                     msg = 'virtual server with id %s not found' % server_id,
317                     raiseCLIError(ce, msg, details=[
318                         '* to get ids of all servers: /server list',
319                         '* to get server details: /server info <server id>'])
320                 raise
321         return _raise
322
323     @classmethod
324     def firewall(this, foo):
325         def _raise(self, *args, **kwargs):
326             profile = kwargs.get('profile', None)
327             try:
328                 return foo(self, *args, **kwargs)
329             except ClientError as ce:
330                 if ce.status == 400 and profile and (
331                     'firewall' in ('%s' % ce).lower()
332                 ):
333                     msg = '%s is an invalid firewall profile term' % profile
334                     raiseCLIError(ce, msg, details=[
335                         'Try one of the following:',
336                         '* DISABLED: Shutdown firewall',
337                         '* ENABLED: Firewall in normal mode',
338                         '* PROTECTED: Firewall in secure mode'])
339                 raise
340         return _raise
341
342     @classmethod
343     def nic_id(this, foo):
344         def _raise(self, *args, **kwargs):
345             try:
346                 return foo(self, *args, **kwargs)
347             except ClientError as ce:
348                 nic_id = kwargs.get('nic_id', None)
349                 if nic_id and ce.status == 404 and (
350                     'network interface' in ('%s' % ce).lower()
351                 ):
352                     server_id = kwargs.get('server_id', '<no server>')
353                     err_msg = 'No nic %s on virtual server with id %s' % (
354                         nic_id,
355                         server_id)
356                     raiseCLIError(ce, err_msg, details=[
357                         '* check v. server with id %s: /server info %s' % (
358                             server_id,
359                             server_id),
360                         '* list nics for v. server with id %s:' % server_id,
361                         '      /server addr %s' % server_id])
362                 raise
363         return _raise
364
365     @classmethod
366     def nic_format(this, foo):
367         def _raise(self, *args, **kwargs):
368             try:
369                 return foo(self, *args, **kwargs)
370             except IndexError as ie:
371                 nic_id = kwargs.get('nic_id', None)
372                 msg = 'Invalid format for network interface (nic) %s' % nic_id
373                 raiseCLIError(ie, msg, importance=1, details=[
374                     'nid_id format: nic-<server id>-<nic id>',
375                     '* get nics of a network: /network info <net id>',
376                     '    (listed the "attachments" section)'])
377         return _raise
378
379     @classmethod
380     def metadata(this, foo):
381         def _raise(self, *args, **kwargs):
382             key = kwargs.get('key', None)
383             try:
384                 foo(self, *args, **kwargs)
385             except ClientError as ce:
386                 if key and ce.status == 404 and (
387                     'metadata' in ('%s' % ce).lower()
388                 ):
389                         raiseCLIError(
390                             ce, 'No v. server metadata with key %s' % key)
391                 raise
392         return _raise
393
394
395 class plankton(object):
396
397     about_image_id = [
398         'How to pick a suitable image:',
399         '* get a list of image ids: /image list',
400         '* details of image: /image meta <image id>']
401
402     @classmethod
403     def connection(this, foo):
404         return generic._connection(foo)
405
406     @classmethod
407     def id(this, foo):
408         def _raise(self, *args, **kwargs):
409             image_id = kwargs.get('image_id', None)
410             try:
411                 foo(self, *args, **kwargs)
412             except ClientError as ce:
413                 if image_id and (
414                     ce.status == 404
415                     or (
416                         ce.status == 400
417                         and 'image not found' in ('%s' % ce).lower())
418                     or ce.status == 411
419                 ):
420                         msg = 'No image with id %s found' % image_id
421                         raiseCLIError(ce, msg, details=this.about_image_id)
422                 raise
423         return _raise
424
425     @classmethod
426     def metadata(this, foo):
427         def _raise(self, *args, **kwargs):
428             key = kwargs.get('key', None)
429             try:
430                 return foo(self, *args, **kwargs)
431             except ClientError as ce:
432                 ce_msg = ('%s' % ce).lower()
433                 if ce.status == 404 or (
434                         ce.status == 400 and 'metadata' in ce_msg):
435                     msg = 'No properties with key %s in this image' % key
436                     raiseCLIError(ce, msg)
437                 raise
438         return _raise
439
440
441 class pithos(object):
442     container_howto = [
443         'To specify a container:',
444         '  1. --container=<container> (temporary, overrides all)',
445         '  2. Use the container:path format (temporary, overrides 3)',
446         '  3. Set pithos_container variable (permanent)',
447         '     /config set pithos_container <container>',
448         'For a list of containers: /file list']
449
450     @classmethod
451     def connection(this, foo):
452         return generic._connection(foo)
453
454     @classmethod
455     def account(this, foo):
456         def _raise(self, *args, **kwargs):
457             try:
458                 return foo(self, *args, **kwargs)
459             except ClientError as ce:
460                 if ce.status == 403:
461                     raiseCLIError(
462                         ce,
463                         'Invalid account credentials for this operation',
464                         details=['Check user account settings'])
465                 raise
466         return _raise
467
468     @classmethod
469     def quota(this, foo):
470         def _raise(self, *args, **kwargs):
471             try:
472                 return foo(self, *args, **kwargs)
473             except ClientError as ce:
474                 if ce.status == 413:
475                     raiseCLIError(ce, 'User quota exceeded', details=[
476                         '* get quotas:',
477                         '  * upper total limit:      /file quota',
478                         '  * container limit:',
479                         '    /file containerlimit get <container>',
480                         '* set a higher container limit:',
481                         '    /file containerlimit set <limit> <container>'])
482                 raise
483         return _raise
484
485     @classmethod
486     def container(this, foo):
487         def _raise(self, *args, **kwargs):
488             dst_cont = kwargs.get('dst_cont', None)
489             try:
490                 return foo(self, *args, **kwargs)
491             except ClientError as ce:
492                 if ce.status == 404 and 'container' in ('%s' % ce).lower():
493                         cont = ('%s or %s' % (
494                             self.container,
495                             dst_cont)) if dst_cont else self.container
496                         msg = 'Is container %s in current account?' % (cont),
497                         raiseCLIError(ce, msg, details=this.container_howto)
498                 raise
499         return _raise
500
501     @classmethod
502     def local_path(this, foo):
503         def _raise(self, *args, **kwargs):
504             local_path = kwargs.get('local_path', '<None>')
505             try:
506                 return foo(self, *args, **kwargs)
507             except IOError as ioe:
508                 msg = 'Failed to access file %s' % local_path,
509                 raiseCLIError(ioe, msg, importance=2)
510         return _raise
511
512     @classmethod
513     def object_path(this, foo):
514         def _raise(self, *args, **kwargs):
515             try:
516                 return foo(self, *args, **kwargs)
517             except ClientError as ce:
518                 err_msg = ('%s' % ce).lower()
519                 if (
520                     ce.status == 404 or ce.status == 500
521                 ) and 'object' in err_msg and 'not' in err_msg:
522                     msg = 'No object %s in container %s' % (
523                         self.path,
524                         self.container)
525                     raiseCLIError(ce, msg, details=this.container_howto)
526                 raise
527         return _raise
528
529     @classmethod
530     def object_size(this, foo):
531         def _raise(self, *args, **kwargs):
532             size = kwargs.get('size', None)
533             start = kwargs.get('start', 0)
534             end = kwargs.get('end', 0)
535             if size:
536                 try:
537                     size = int(size)
538                 except ValueError as ve:
539                     msg = 'Invalid file size %s ' % size
540                     details = ['size must be a positive integer']
541                     raiseCLIError(ve, msg, details=details, importance=1)
542             else:
543                 try:
544                     start = int(start)
545                 except ValueError as e:
546                     msg = 'Invalid start value %s in range' % start,
547                     details = ['size must be a positive integer'],
548                     raiseCLIError(e, msg, details=details, importance=1)
549                 try:
550                     end = int(end)
551                 except ValueError as e:
552                     msg = 'Invalid end value %s in range' % end
553                     details = ['size must be a positive integer']
554                     raiseCLIError(e, msg, details=details, importance=1)
555                 if start > end:
556                     raiseCLIError(
557                         'Invalid range %s-%s' % (start, end),
558                         details=['size must be a positive integer'],
559                         importance=1)
560                 size = end - start
561             try:
562                 return foo(self, *args, **kwargs)
563             except ClientError as ce:
564                 err_msg = ('%s' % ce).lower()
565                 expected = 'object length is smaller than range length'
566                 if size and (
567                     ce.status == 416 or (
568                         ce.status == 400 and expected in err_msg)):
569                     raiseCLIError(ce, 'Remote object %s:%s <= %s %s' % (
570                         self.container, self.path, format_size(size),
571                         ('(%sB)' % size) if size >= 1024 else ''))
572                 raise
573         return _raise