df88e80393a978ce7a027e1ff187ce4a35c8cc4b
[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                 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: /astakos authenticate',
73                         '  to set token: /config set [.server.]token <token>',
74                         '  to get current token: /config get [server.]token'])
75                 elif ce.status in range(-12, 200) + [302, 401, 403, 500]:
76                     raiseCLIError(ce, importance=3, details=[
77                         'Check if service is up or set to url %s' % base_url,
78                         '  to get url: /config get %s' % base_url,
79                         '  to set url: /config set %s <URL>' % base_url])
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                         'Please, check if service url is correctly set',
88                         '* to get current url: /config get compute.url',
89                         '* to set url: /config set compute.url <URL>'])
90                 raise
91         return _raise
92
93
94 class astakos(object):
95
96     _token_details = [
97         'To check default token: /config get token',
98         'If set/update a token:',
99         '*  (permanent):    /config set token <token>',
100         '*  (temporary):    re-run with <token> parameter']
101
102     @classmethod
103     def load(this, foo):
104         def _raise(self, *args, **kwargs):
105             r = foo(self, *args, **kwargs)
106             try:
107                 client = getattr(self, 'client')
108             except AttributeError as ae:
109                 raiseCLIError(ae, 'Client setup failure', importance=3)
110             if not getattr(client, 'token', False):
111                 kloger.warning(
112                     'No permanent token (try: kamaki config set token <tkn>)')
113             if not getattr(client, 'base_url', False):
114                 msg = 'Missing astakos server URL'
115                 raise CLIError(msg, importance=3, details=[
116                     'Check if astakos.url is set correctly',
117                     'To get astakos url:   /config get astakos.url',
118                     'To set astakos url:   /config set astakos.url <URL>'])
119             return r
120         return _raise
121
122     @classmethod
123     def authenticate(this, foo):
124         def _raise(self, *args, **kwargs):
125             try:
126                 return foo(self, *args, **kwargs)
127             except ClientError as ce:
128                 if ce.status == 401:
129                     token = kwargs.get('custom_token', 0) or self.client.token
130                     msg = (
131                         'Authorization failed for token %s' % token
132                     ) if token else 'No token provided',
133                     details = [] if token else this._token_details
134                     raiseCLIError(ce, msg, details=details)
135             self._raise = foo
136         return _raise
137
138
139 class history(object):
140     @classmethod
141     def init(this, foo):
142         def _raise(self, *args, **kwargs):
143             r = foo(self, *args, **kwargs)
144             if not hasattr(self, 'history'):
145                 raise CLIError('Failed to load history', importance=2)
146             return r
147         return _raise
148
149     @classmethod
150     def _get_cmd_ids(this, foo):
151         def _raise(self, cmd_ids, *args, **kwargs):
152             if not cmd_ids:
153                 raise CLISyntaxError(
154                     '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                 msg = 'Invalid network id %s ' % network_id
197                 details = ['network id must be a positive integer']
198                 raiseCLIError(ve, msg, details=details, importance=1)
199             except ClientError as ce:
200                 if network_id and ce.status == 404 and (
201                     'network' in ('%s' % ce).lower()
202                 ):
203                     msg = 'No network with id %s found' % network_id,
204                     raiseCLIError(ce, msg, 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                     msg = 'Cannot create another network',
216                     details = [
217                         'Maximum number of networks reached',
218                         '* to get a list of networks: /network list',
219                         '* to delete a network: /network delete <net id>']
220                     raiseCLIError(ce, msg, details=details)
221                 raise
222         return _raise
223
224     @classmethod
225     def network_in_use(this, foo):
226         def _raise(self, *args, **kwargs):
227             network_id = kwargs.get('network_id', None)
228             try:
229                 return foo(self, *args, **kwargs)
230             except ClientError as ce:
231                 if network_id and ce.status == 400:
232                     msg = 'Network with id %s does not exist' % network_id,
233                     raiseCLIError(ce, msg, details=self.about_network_id)
234                 elif network_id or ce.status == 421:
235                     msg = 'Network with id %s is in use' % network_id,
236                     raiseCLIError(ce, msg, details=[
237                         'Disconnect all nics/VMs of this network first',
238                         '* to get nics: /network info %s' % network_id,
239                         '.  (under "attachments" section)',
240                         '* to disconnect: /network disconnect <nic id>'])
241                 raise
242         return _raise
243
244     @classmethod
245     def flavor_id(this, foo):
246         def _raise(self, *args, **kwargs):
247             flavor_id = kwargs.get('flavor_id', None)
248             try:
249                 flavor_id = int(flavor_id)
250                 return foo(self, *args, **kwargs)
251             except ValueError as ve:
252                 msg = 'Invalid flavor id %s ' % flavor_id,
253                 details = 'Flavor id must be a positive integer',
254                 raiseCLIError(ve, msg, details=details, importance=1)
255             except ClientError as ce:
256                 if flavor_id and ce.status == 404 and (
257                     'flavor' in ('%s' % ce).lower()
258                 ):
259                         msg = 'No flavor with id %s found' % flavor_id,
260                         raiseCLIError(ce, msg, details=this.about_flavor_id)
261                 raise
262         return _raise
263
264     @classmethod
265     def server_id(this, foo):
266         def _raise(self, *args, **kwargs):
267             server_id = kwargs.get('server_id', None)
268             try:
269                 server_id = int(server_id)
270                 return foo(self, *args, **kwargs)
271             except ValueError as ve:
272                 msg = 'Invalid server(VM) id %s' % server_id,
273                 details = ['id must be a positive integer'],
274                 raiseCLIError(ve, msg, details=details, importance=1)
275             except ClientError as ce:
276                 err_msg = ('%s' % ce).lower()
277                 if (
278                     ce.status == 404 and 'server' in err_msg
279                 ) or (
280                     ce.status == 400 and 'not found' in err_msg
281                 ):
282                     msg = 'server(VM) with id %s not found' % server_id,
283                     raiseCLIError(ce, msg, 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 and (
297                     'firewall' in ('%s' % ce).lower()
298                 ):
299                     msg = '%s is an invalid firewall profile term' % profile
300                     raiseCLIError(ce, msg, details=[
301                         'Try one of the following:',
302                         '* DISABLED: Shutdown firewall',
303                         '* ENABLED: Firewall in normal mode',
304                         '* PROTECTED: Firewall in secure mode'])
305                 raise
306         return _raise
307
308     @classmethod
309     def nic_id(this, foo):
310         def _raise(self, *args, **kwargs):
311             try:
312                 return foo(self, *args, **kwargs)
313             except ClientError as ce:
314                 nic_id = kwargs.get('nic_id', None)
315                 if nic_id and ce.status == 404 and (
316                     'network interface' in ('%s' % ce).lower()
317                 ):
318                     server_id = kwargs.get('server_id', '<no server>')
319                     err_msg = 'No nic %s on server(VM) with id %s' % (
320                         nic_id,
321                         server_id)
322                     raiseCLIError(ce, err_msg, details=[
323                         '* check server(VM) with id %s: /server info %s' % (
324                             server_id,
325                             server_id),
326                         '* list nics for server(VM) with id %s:' % server_id,
327                         '      /server addr %s' % server_id])
328                 raise
329         return _raise
330
331     @classmethod
332     def nic_format(this, foo):
333         def _raise(self, *args, **kwargs):
334             try:
335                 return foo(self, *args, **kwargs)
336             except IndexError as ie:
337                 nic_id = kwargs.get('nic_id', None)
338                 msg = 'Invalid format for network interface (nic) %s' % nic_id
339                 raiseCLIError(ie, msg, importance=1, 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 and (
353                     'metadata' in ('%s' % ce).lower()
354                 ):
355                         raiseCLIError(ce, 'No VM metadata with key %s' % key)
356                 raise
357         return _raise
358
359
360 class plankton(object):
361
362     about_image_id = [
363         'How to pick a suitable image:',
364         '* get a list of image ids: /image list',
365         '* details of image: /flavor info <image id>']
366
367     @classmethod
368     def connection(this, foo):
369         return generic._connection(foo, 'image.url')
370
371     @classmethod
372     def id(this, foo):
373         def _raise(self, *args, **kwargs):
374             image_id = kwargs.get('image_id', None)
375             try:
376                 foo(self, *args, **kwargs)
377             except ClientError as ce:
378                 if image_id and (
379                     ce.status == 404
380                     or (
381                         ce.status == 400
382                         and 'image not found' in ('%s' % ce).lower())
383                     or ce.status == 411
384                 ):
385                         msg = 'No image with id %s found' % image_id
386                         raiseCLIError(ce, msg, details=this.about_image_id)
387                 raise
388         return _raise
389
390     @classmethod
391     def metadata(this, foo):
392         def _raise(self, *args, **kwargs):
393             key = kwargs.get('key', None)
394             try:
395                 foo(self, *args, **kwargs)
396             except ClientError as ce:
397                 ce_msg = ('%s' % ce).lower()
398                 if ce.status == 404 or (
399                         ce.status == 400 and 'metadata' in ce_msg):
400                     msg = 'No properties with key %s in this image' % key
401                     raiseCLIError(ce, msg)
402                 raise
403         return _raise
404
405
406 class pithos(object):
407     container_howto = [
408         'To specify a container:',
409         '  1. Set store.container variable (permanent)',
410         '     /config set store.container <container>',
411         '  2. --container=<container> (temporary, overrides 1)',
412         '  3. Use the container:path format (temporary, overrides all)',
413         'For a list of containers: /store list']
414
415     @classmethod
416     def connection(this, foo):
417         return generic._connection(foo, 'store.url')
418
419     @classmethod
420     def quota(this, foo):
421         def _raise(self, *args, **kwargs):
422             try:
423                 return foo(self, *args, **kwargs)
424             except ClientError as ce:
425                 if ce.status == 413:
426                     raiseCLIError(ce, 'User quota exceeded', details=[
427                         '* get quotas:',
428                         '  * upper total limit:      /store quota',
429                         '  * container limit:  /store quota <container>',
430                         '* set a higher quota (if permitted):',
431                         '    /store setquota <quota>[unit] <container>'
432                         '    as long as <container quota> <= <total quota>'])
433                 raise
434         return _raise
435
436     @classmethod
437     def container(this, foo):
438         def _raise(self, *args, **kwargs):
439             dst_cont = kwargs.get('dst_cont', None)
440             try:
441                 return foo(self, *args, **kwargs)
442             except ClientError as ce:
443                 if ce.status == 404 and 'container' in ('%s' % ce).lower():
444                         cont = ('%s or %s' % (
445                             self.container,
446                             dst_cont)) if dst_cont else self.container
447                         msg = 'Is container %s in current account?' % (cont),
448                         raiseCLIError(ce, msg, details=this.container_howto)
449                 raise
450         return _raise
451
452     @classmethod
453     def local_path(this, foo):
454         def _raise(self, *args, **kwargs):
455             local_path = kwargs.get('local_path', '<None>')
456             try:
457                 return foo(self, *args, **kwargs)
458             except IOError as ioe:
459                 msg = 'Failed to access file %s' % local_path,
460                 raiseCLIError(ioe, msg, importance=2)
461         return _raise
462
463     @classmethod
464     def object_path(this, foo):
465         def _raise(self, *args, **kwargs):
466             try:
467                 return foo(self, *args, **kwargs)
468             except ClientError as ce:
469                 err_msg = ('%s' % ce).lower()
470                 if (
471                     ce.status == 404 or ce.status == 500
472                 ) and 'object' in err_msg and 'not' in err_msg:
473                     msg = 'No object %s in container %s' % (
474                         self.path,
475                         self.container)
476                     raiseCLIError(ce, msg, details=this.container_howto)
477                 raise
478         return _raise
479
480     @classmethod
481     def object_size(this, foo):
482         def _raise(self, *args, **kwargs):
483             size = kwargs.get('size', None)
484             start = kwargs.get('start', 0)
485             end = kwargs.get('end', 0)
486             if size:
487                 try:
488                     size = int(size)
489                 except ValueError as ve:
490                     msg = 'Invalid file size %s ' % size
491                     details = ['size must be a positive integer']
492                     raiseCLIError(ve, msg, details=details, importance=1)
493             else:
494                 try:
495                     start = int(start)
496                 except ValueError as e:
497                     msg = 'Invalid start value %s in range' % start,
498                     details = ['size must be a positive integer'],
499                     raiseCLIError(e, msg, details=details, importance=1)
500                 try:
501                     end = int(end)
502                 except ValueError as e:
503                     msg = 'Invalid end value %s in range' % end
504                     details = ['size must be a positive integer']
505                     raiseCLIError(e, msg, details=details, importance=1)
506                 if start > end:
507                     raiseCLIError(
508                         'Invalid range %s-%s' % (start, end),
509                         details=['size must be a positive integer'],
510                         importance=1)
511                 size = end - start
512             try:
513                 return foo(self, *args, **kwargs)
514             except ClientError as ce:
515                 err_msg = ('%s' % ce).lower()
516                 expected = 'object length is smaller than range length'
517                 if size and (
518                     ce.status == 416 or (
519                         ce.status == 400 and expected in err_msg)):
520                     raiseCLIError(ce, 'Remote object %s:%s <= %s %s' % (
521                         self.container, self.path, format_size(size),
522                         ('(%sB)' % size) if size >= 1024 else ''))
523                 raise
524         return _raise