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