Fix typo in create_server -w
[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, func):
50         def _raise(self, *args, **kwargs):
51             try:
52                 return func(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, func):
64         def _raise(self, *args, **kwargs):
65             try:
66                 func(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, func):
106         def _raise(self, *args, **kwargs):
107             try:
108                 r = func(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, func):
116         def _raise(self, *args, **kwargs):
117             r = func(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, func):
139         def _raise(self, *args, **kwargs):
140             try:
141                 return func(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 = func
151         return _raise
152
153
154 class history(object):
155     @classmethod
156     def init(this, func):
157         def _raise(self, *args, **kwargs):
158             r = func(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, func):
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 func(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     net_types = ('CUSTOM', 'MAC_FILTERED', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
187
188     @classmethod
189     def connection(this, func):
190         return generic._connection(func)
191
192     @classmethod
193     def date(this, func):
194         def _raise(self, *args, **kwargs):
195             try:
196                 return func(self, *args, **kwargs)
197             except ClientError as ce:
198                 if ce.status == 400 and 'changes-since' in ('%s' % ce):
199                     raise CLIError(
200                         'Incorrect date format for --since',
201                         details=['Accepted date format: d/m/y'])
202                 raise
203         return _raise
204
205     @classmethod
206     def cluster_size(this, func):
207         def _raise(self, *args, **kwargs):
208             size = kwargs.get('size', None)
209             try:
210                 size = int(size)
211                 assert size > 0, 'Cluster size must be a positive integer'
212                 return func(self, *args, **kwargs)
213             except ValueError as ve:
214                 msg = 'Invalid cluster size value %s' % size
215                 raiseCLIError(ve, msg, importance=1, details=[
216                     'Cluster size must be a positive integer'])
217             except AssertionError as ae:
218                 raiseCLIError(
219                     ae, 'Invalid cluster size %s' % size, importance=1)
220             except ClientError:
221                 raise
222         return _raise
223
224     @classmethod
225     def network_id(this, func):
226         def _raise(self, *args, **kwargs):
227             network_id = kwargs.get('network_id', None)
228             try:
229                 network_id = int(network_id)
230                 return func(self, *args, **kwargs)
231             except ValueError as ve:
232                 msg = 'Invalid network id %s ' % network_id
233                 details = 'network id must be a positive integer'
234                 raiseCLIError(ve, msg, details=details, importance=1)
235             except ClientError as ce:
236                 if network_id and ce.status == 404 and (
237                     'network' in ('%s' % ce).lower()
238                 ):
239                     msg = 'No network with id %s found' % network_id,
240                     raiseCLIError(ce, msg, details=this.about_network_id)
241                 raise
242         return _raise
243
244     @classmethod
245     def network_type(this, func):
246         def _raise(self, *args, **kwargs):
247             network_type = kwargs.get('network_type', None)
248             msg = 'Invalid network type %s.\nValid types: %s' % (
249                 network_type, ' '.join(this.net_types))
250             assert network_type in this.net_types, msg
251             return func(self, *args, **kwargs)
252         return _raise
253
254     @classmethod
255     def network_max(this, func):
256         def _raise(self, *args, **kwargs):
257             try:
258                 return func(self, *args, **kwargs)
259             except ClientError as ce:
260                 if ce.status == 413:
261                     msg = 'Cannot create another network',
262                     details = [
263                         'Maximum number of networks reached',
264                         '* to get a list of networks: /network list',
265                         '* to delete a network: /network delete <net id>']
266                     raiseCLIError(ce, msg, details=details)
267                 raise
268         return _raise
269
270     @classmethod
271     def network_in_use(this, func):
272         def _raise(self, *args, **kwargs):
273             network_id = kwargs.get('network_id', None)
274             try:
275                 return func(self, *args, **kwargs)
276             except ClientError as ce:
277                 if network_id and ce.status in (400, ):
278                     msg = 'Network with id %s does not exist' % network_id,
279                     raiseCLIError(ce, msg, details=this.about_network_id)
280                 elif network_id or ce.status in (421, ):
281                     msg = 'Network with id %s is in use' % network_id,
282                     raiseCLIError(ce, msg, details=[
283                         'Disconnect all nics/servers of this network first',
284                         '* to get nics: /network info %s' % network_id,
285                         '.  (under "attachments" section)',
286                         '* to disconnect: /network disconnect <nic id>'])
287                 raise
288         return _raise
289
290     @classmethod
291     def flavor_id(this, func):
292         def _raise(self, *args, **kwargs):
293             flavor_id = kwargs.get('flavor_id', None)
294             try:
295                 flavor_id = int(flavor_id)
296                 return func(self, *args, **kwargs)
297             except ValueError as ve:
298                 msg = 'Invalid flavor id %s ' % flavor_id,
299                 details = 'Flavor id must be a positive integer'
300                 raiseCLIError(ve, msg, details=details, importance=1)
301             except ClientError as ce:
302                 if flavor_id and ce.status == 404 and (
303                     'flavor' in ('%s' % ce).lower()
304                 ):
305                         msg = 'No flavor with id %s found' % flavor_id,
306                         raiseCLIError(ce, msg, details=this.about_flavor_id)
307                 raise
308         return _raise
309
310     @classmethod
311     def server_id(this, func):
312         def _raise(self, *args, **kwargs):
313             server_id = kwargs.get('server_id', None)
314             try:
315                 server_id = int(server_id)
316                 return func(self, *args, **kwargs)
317             except ValueError as ve:
318                 msg = 'Invalid virtual server id %s' % server_id,
319                 details = 'Server id must be a positive integer'
320                 raiseCLIError(ve, msg, details=details, importance=1)
321             except ClientError as ce:
322                 err_msg = ('%s' % ce).lower()
323                 if (
324                     ce.status == 404 and 'server' in err_msg
325                 ) or (
326                     ce.status == 400 and 'not found' in err_msg
327                 ):
328                     msg = 'virtual server with id %s not found' % server_id,
329                     raiseCLIError(ce, msg, details=[
330                         '* to get ids of all servers: /server list',
331                         '* to get server details: /server info <server id>'])
332                 raise
333         return _raise
334
335     @classmethod
336     def firewall(this, func):
337         def _raise(self, *args, **kwargs):
338             profile = kwargs.get('profile', None)
339             try:
340                 return func(self, *args, **kwargs)
341             except ClientError as ce:
342                 if ce.status == 400 and profile and (
343                     'firewall' in ('%s' % ce).lower()
344                 ):
345                     msg = '%s is an invalid firewall profile term' % profile
346                     raiseCLIError(ce, msg, details=[
347                         'Try one of the following:',
348                         '* DISABLED: Shutdown firewall',
349                         '* ENABLED: Firewall in normal mode',
350                         '* PROTECTED: Firewall in secure mode'])
351                 raise
352         return _raise
353
354     @classmethod
355     def nic_id(this, func):
356         def _raise(self, *args, **kwargs):
357             try:
358                 return func(self, *args, **kwargs)
359             except ClientError as ce:
360                 nic_id = kwargs.get('nic_id', None)
361                 if nic_id and ce.status == 404 and (
362                     'network interface' in ('%s' % ce).lower()
363                 ):
364                     server_id = kwargs.get('server_id', '<no server>')
365                     err_msg = 'No nic %s on virtual server with id %s' % (
366                         nic_id,
367                         server_id)
368                     raiseCLIError(ce, err_msg, details=[
369                         '* check v. server with id %s: /server info %s' % (
370                             server_id,
371                             server_id),
372                         '* list nics for v. server with id %s:' % server_id,
373                         '      /server addr %s' % server_id])
374                 raise
375         return _raise
376
377     @classmethod
378     def nic_format(this, func):
379         def _raise(self, *args, **kwargs):
380             try:
381                 return func(self, *args, **kwargs)
382             except IndexError as ie:
383                 nic_id = kwargs.get('nic_id', None)
384                 msg = 'Invalid format for network interface (nic) %s' % nic_id
385                 raiseCLIError(ie, msg, importance=1, details=[
386                     'nid_id format: nic-<server id>-<nic id>',
387                     '* get nics of a network: /network info <net id>',
388                     '    (listed the "attachments" section)'])
389         return _raise
390
391     @classmethod
392     def metadata(this, func):
393         def _raise(self, *args, **kwargs):
394             key = kwargs.get('key', None)
395             try:
396                 func(self, *args, **kwargs)
397             except ClientError as ce:
398                 if key and ce.status == 404 and (
399                     'metadata' in ('%s' % ce).lower()
400                 ):
401                         raiseCLIError(
402                             ce, 'No virtual server metadata with key %s' % key)
403                 raise
404         return _raise
405
406
407 class plankton(object):
408
409     about_image_id = [
410         'How to pick a suitable image:',
411         '* get a list of image ids: /image list',
412         '* details of image: /image meta <image id>']
413
414     @classmethod
415     def connection(this, func):
416         return generic._connection(func)
417
418     @classmethod
419     def id(this, func):
420         def _raise(self, *args, **kwargs):
421             image_id = kwargs.get('image_id', None)
422             try:
423                 func(self, *args, **kwargs)
424             except ClientError as ce:
425                 if image_id and (
426                     ce.status == 404
427                     or (
428                         ce.status == 400
429                         and 'image not found' in ('%s' % ce).lower())
430                     or ce.status == 411
431                 ):
432                         msg = 'No image with id %s found' % image_id
433                         raiseCLIError(ce, msg, details=this.about_image_id)
434                 raise
435         return _raise
436
437     @classmethod
438     def metadata(this, func):
439         def _raise(self, *args, **kwargs):
440             key = kwargs.get('key', None)
441             try:
442                 return func(self, *args, **kwargs)
443             except ClientError as ce:
444                 ce_msg = ('%s' % ce).lower()
445                 if ce.status == 404 or (
446                         ce.status == 400 and 'metadata' in ce_msg):
447                     msg = 'No properties with key %s in this image' % key
448                     raiseCLIError(ce, msg)
449                 raise
450         return _raise
451
452
453 class pithos(object):
454     container_howto = [
455         'To specify a container:',
456         '  1. --container=<container> (temporary, overrides all)',
457         '  2. Use the container:path format (temporary, overrides 3)',
458         '  3. Set pithos_container variable (permanent)',
459         '     /config set pithos_container <container>',
460         'For a list of containers: /file list']
461
462     @classmethod
463     def connection(this, func):
464         return generic._connection(func)
465
466     @classmethod
467     def account(this, func):
468         def _raise(self, *args, **kwargs):
469             try:
470                 return func(self, *args, **kwargs)
471             except ClientError as ce:
472                 if ce.status == 403:
473                     raiseCLIError(
474                         ce,
475                         'Invalid account credentials for this operation',
476                         details=['Check user account settings'])
477                 raise
478         return _raise
479
480     @classmethod
481     def quota(this, func):
482         def _raise(self, *args, **kwargs):
483             try:
484                 return func(self, *args, **kwargs)
485             except ClientError as ce:
486                 if ce.status == 413:
487                     raiseCLIError(ce, 'User quota exceeded', details=[
488                         '* get quotas:',
489                         '  * upper total limit:      /file quota',
490                         '  * container limit:',
491                         '    /file containerlimit get <container>',
492                         '* set a higher container limit:',
493                         '    /file containerlimit set <limit> <container>'])
494                 raise
495         return _raise
496
497     @classmethod
498     def container(this, func):
499         def _raise(self, *args, **kwargs):
500             dst_cont = kwargs.get('dst_cont', None)
501             try:
502                 return func(self, *args, **kwargs)
503             except ClientError as ce:
504                 if ce.status == 404 and 'container' in ('%s' % ce).lower():
505                         cont = ('%s or %s' % (
506                             self.container,
507                             dst_cont)) if dst_cont else self.container
508                         msg = 'Is container %s in current account?' % (cont),
509                         raiseCLIError(ce, msg, details=this.container_howto)
510                 raise
511         return _raise
512
513     @classmethod
514     def local_path_download(this, func):
515         def _raise(self, *args, **kwargs):
516             try:
517                 return func(self, *args, **kwargs)
518             except IOError as ioe:
519                 msg = 'Failed to access a local file',
520                 raiseCLIError(ioe, msg, importance=2, details=[
521                     'Check if the file exists. Also check if the remote',
522                     'directories exist. All directories in a remote path',
523                     'must exist to succesfully download a container or a',
524                     'directory.',
525                     'To create a remote directory:',
526                     '  [kamaki] file mkdir REMOTE_DIRECTORY_PATH'])
527         return _raise
528
529     @classmethod
530     def local_path(this, func):
531         def _raise(self, *args, **kwargs):
532             local_path = kwargs.get('local_path', None)
533             try:
534                 return func(self, *args, **kwargs)
535             except IOError as ioe:
536                 msg = 'Failed to access file %s' % local_path,
537                 raiseCLIError(ioe, msg, importance=2)
538         return _raise
539
540     @classmethod
541     def object_path(this, func):
542         def _raise(self, *args, **kwargs):
543             try:
544                 return func(self, *args, **kwargs)
545             except ClientError as ce:
546                 err_msg = ('%s' % ce).lower()
547                 if (
548                     ce.status == 404 or ce.status == 500
549                 ) and 'object' in err_msg and 'not' in err_msg:
550                     msg = 'No object %s in container %s' % (
551                         self.path,
552                         self.container)
553                     raiseCLIError(ce, msg, details=this.container_howto)
554                 raise
555         return _raise
556
557     @classmethod
558     def object_size(this, func):
559         def _raise(self, *args, **kwargs):
560             size = kwargs.get('size', None)
561             start = kwargs.get('start', 0)
562             end = kwargs.get('end', 0)
563             if size:
564                 try:
565                     size = int(size)
566                 except ValueError as ve:
567                     msg = 'Invalid file size %s ' % size
568                     details = ['size must be a positive integer']
569                     raiseCLIError(ve, msg, details=details, importance=1)
570             else:
571                 try:
572                     start = int(start)
573                 except ValueError as e:
574                     msg = 'Invalid start value %s in range' % start,
575                     details = ['size must be a positive integer'],
576                     raiseCLIError(e, msg, details=details, importance=1)
577                 try:
578                     end = int(end)
579                 except ValueError as e:
580                     msg = 'Invalid end value %s in range' % end
581                     details = ['size must be a positive integer']
582                     raiseCLIError(e, msg, details=details, importance=1)
583                 if start > end:
584                     raiseCLIError(
585                         'Invalid range %s-%s' % (start, end),
586                         details=['size must be a positive integer'],
587                         importance=1)
588                 size = end - start
589             try:
590                 return func(self, *args, **kwargs)
591             except ClientError as ce:
592                 err_msg = ('%s' % ce).lower()
593                 expected = 'object length is smaller than range length'
594                 if size and (
595                     ce.status == 416 or (
596                         ce.status == 400 and expected in err_msg)):
597                     raiseCLIError(ce, 'Remote object %s:%s <= %s %s' % (
598                         self.container, self.path, format_size(size),
599                         ('(%sB)' % size) if size >= 1024 else ''))
600                 raise
601         return _raise