__init__.py 14 KB
Newer Older
1
# Copyright 2011-2014 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.command

34
35
36
from sys import stdin, stdout, stderr
from traceback import format_exc

37
from kamaki.cli.logger import get_logger
38
from kamaki.cli.utils import (
39
40
    print_list, print_dict, print_json, print_items, ask_user, pref_enc,
    filter_dicts_by_dict)
41
from kamaki.cli.argument import ValueArgument, ProgressBarArgument
42
from kamaki.cli.errors import CLIInvalidArgument, CLIBaseUrlError
43

44

45
log = get_logger(__name__)
46

Stavros Sachtouris's avatar
Stavros Sachtouris committed
47

48
49
50
51
52
53
54
55
56
57
58
59
60
def dont_raise(*errors):
    def decorator(func):
        def wrap(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except errors as e:
                log.debug('Suppressed error %s while calling %s(%s)' % (
                    e, func.__name__, ','.join(['%s' % i for i in args] + [
                        ('%s=%s' % items) for items in kwargs.items()])))
                log.debug(format_exc(e))
                return None
        return wrap
    return decorator
61
62


63
def client_log(func):
64
65
    def wrap(self, *args, **kwargs):
        try:
66
            return func(self, *args, **kwargs)
67
68
69
70
71
        finally:
            self._set_log_params()
    return wrap


72
def fall_back(func):
73
74
75
76
77
78
79
80
81
82
83
    def wrap(self, inp):
        try:
            inp = func(self, inp)
        except Exception as e:
            log.warning('WARNING: Error while running %s: %s' % (func, e))
            log.warning('\tWARNING: Kamaki will use original data to go on')
        finally:
            return inp
    return wrap


84
class CommandInit(object):
85

86
87
88
89
90
91
    # self.arguments (dict) contains all non-positional arguments
    # self.required (list or tuple) contains required argument keys
    #     if it is a list, at least one of these arguments is required
    #     if it is a tuple, all arguments are required
    #     Lists and tuples can nest other lists and/or tuples

92
93
    def __init__(
            self,
94
            arguments={}, astakos=None, cloud=None,
95
96
97
            _in=None, _out=None, _err=None):
        self._in, self._out, self._err = (
            _in or stdin, _out or stdout, _err or stderr)
98
        self.required = getattr(self, 'required', None)
99
100
        if hasattr(self, 'arguments'):
            arguments.update(self.arguments)
101
        if isinstance(self, OptionalOutput):
102
            arguments.update(self.oo_arguments)
103
        if isinstance(self, NameFilter):
104
            arguments.update(self.nf_arguments)
105
        if isinstance(self, IDFilter):
106
            arguments.update(self.if_arguments)
107
108
109
110
        try:
            arguments.update(self.wait_arguments)
        except AttributeError:
            pass
111
        self.arguments = dict(arguments)
112
        try:
113
            self.config = self['config']
114
115
        except KeyError:
            pass
116
        self.astakos = astakos or getattr(self, 'astakos', None)
117
        self.cloud = cloud or getattr(self, 'cloud', None)
118

119
120
121
122
    def get_client(self, cls, service):
        self.cloud = getattr(self, 'cloud', 'default')
        URL, TOKEN = self._custom_url(service), self._custom_token(service)
        if not all([URL, TOKEN]):
123
            astakos = getattr(self, 'astakos', None)
124
125
126
127
128
129
130
131
132
            if astakos:
                URL = URL or astakos.get_endpoint_url(
                    self._custom_type(service) or cls.service_type,
                    self._custom_version(service))
                TOKEN = TOKEN or astakos.token
            else:
                raise CLIBaseUrlError(service=service)
        return cls(URL, TOKEN)

133
    @dont_raise(UnicodeError)
134
    def write(self, s):
135
        self._out.write(s.encode(pref_enc, errors='replace'))
136
137
138
        self._out.flush()

    def writeln(self, s=''):
139
        self.write('%s\n' % s)
140
141

    def error(self, s=''):
142
        self._err.write(('%s\n' % s).encode(pref_enc, errors='replace'))
143
144
        self._err.flush()

145
146
    def print_list(self, *args, **kwargs):
        kwargs.setdefault('out', self._out)
147
        return print_list(*args, **kwargs)
148
149

    def print_dict(self, *args, **kwargs):
150
        kwargs.setdefault('out', self)
151
        return print_dict(*args, **kwargs)
152
153

    def print_json(self, *args, **kwargs):
154
        kwargs.setdefault('out', self)
155
        return print_json(*args, **kwargs)
156
157

    def print_items(self, *args, **kwargs):
158
        kwargs.setdefault('out', self)
159
        return print_items(*args, **kwargs)
160
161
162

    def ask_user(self, *args, **kwargs):
        kwargs.setdefault('user_in', self._in)
163
        kwargs.setdefault('out', self)
164
        return ask_user(*args, **kwargs)
165

166
    @dont_raise(KeyError)
167
    def _custom_url(self, service):
168
        return self.config.get_cloud(self.cloud, '%s_url' % service)
169

170
    @dont_raise(KeyError)
171
    def _custom_token(self, service):
172
        return self.config.get_cloud(self.cloud, '%s_token' % service)
173

174
    @dont_raise(KeyError)
175
    def _custom_type(self, service):
176
        return self.config.get_cloud(self.cloud, '%s_type' % service)
177

178
    @dont_raise(KeyError)
179
    def _custom_version(self, service):
180
        return self.config.get_cloud(self.cloud, '%s_version' % service)
181

182
    def _uuids2usernames(self, uuids):
183
        return self.astakos.post_user_catalogs(uuids)
184
185

    def _usernames2uuids(self, username):
186
        return self.astakos.post_user_catalogs(displaynames=username)
187

188
    def _uuid2username(self, uuid):
189
        return self._uuids2usernames([uuid]).get(uuid, None)
190
191

    def _username2uuid(self, username):
192
        return self._usernames2uuids([username]).get(username, None)
193

194
    def _set_log_params(self):
195
196
        if not self.client:
            return
197
        try:
198
            self.client.LOG_TOKEN = (
199
                self['config'].get('global', 'log_token').lower() == 'on')
200
201
202
203
204
        except Exception as e:
            log.debug('Failed to read custom log_token setting:'
                '%s\n default for log_token is off' % e)
        try:
            self.client.LOG_DATA = (
205
                self['config'].get('global', 'log_data').lower() == 'on')
206
        except Exception as e:
207
208
209
210
            log.debug('Failed to read custom log_data setting:'
                '%s\n default for log_data is off' % e)
        try:
            self.client.LOG_PID = (
211
                self['config'].get('global', 'log_pid').lower() == 'on')
212
213
214
        except Exception as e:
            log.debug('Failed to read custom log_pid setting:'
                '%s\n default for log_pid is off' % e)
215

216
217
    def _safe_progress_bar(
            self, msg, arg='progress_bar', countdown=False, timeout=100):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
218
219
220
        """Try to get a progress bar, but do not raise errors"""
        try:
            progress_bar = self.arguments[arg]
221
            progress_bar.file = self._err
222
223
            gen = progress_bar.get_generator(
                msg, countdown=countdown, timeout=timeout)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
224
225
226
227
228
229
230
231
232
233
        except Exception:
            return (None, None)
        return (progress_bar, gen)

    def _safe_progress_bar_finish(self, progress_bar):
        try:
            progress_bar.finish()
        except Exception:
            pass

234
235
236
237
    def __getitem__(self, argterm):
        """
        :param argterm: (str) the name/label of an argument in self.arguments

238
239
        :returns: the value of the corresponding Argument (not the argument
            object)
240
241
242

        :raises KeyError: if argterm not in self.arguments of this object
        """
243
        return self.arguments[argterm].value
244
245
246
247
248
249
250
251
252
253
254
255
256

    def __setitem__(self, argterm, arg):
        """Install an argument as argterm
        If argterm points to another argument, the other argument is lost

        :param argterm: (str)

        :param arg: (Argument)
        """
        if not hasattr(self, 'arguments'):
            self.arguments = {}
        self.arguments[argterm] = arg

257
258
259
260
261
262
263
264
265
266
    def get_argument_object(self, argterm):
        """
        :param argterm: (str) the name/label of an argument in self.arguments

        :returns: the arument object

        :raises KeyError: if argterm not in self.arguments of this object
        """
        return self.arguments[argterm]

267
    def get_argument(self, argterm):
268
269
270
271
272
273
274
        """
        :param argterm: (str) the name/label of an argument in self.arguments

        :returns: the value of the arument object

        :raises KeyError: if argterm not in self.arguments of this object
        """
275
        return self[argterm]
276
277


278
279
280
#  feature classes - inherit them to get special features for your commands


281
282
283
class OutputFormatArgument(ValueArgument):
    """Accepted output formats: json (default)"""

284
    formats = dict(json=print_json)
285
286
287

    def ___init__(self, *args, **kwargs):
        super(OutputFormatArgument, self).___init__(*args, **kwargs)
288
        self.value = None
289
290
291

    def value(self, newvalue):
        if not newvalue:
292
            return
293
        elif newvalue.lower() in self.formats:
294
            self.value = newvalue.lower()
295
296
        else:
            raise CLIInvalidArgument(
297
                'Invalid value %s for argument %s' % (newvalue, self.lvalue),
298
299
300
                details=['Valid output formats: %s' % ', '.join(self.formats)])


301
class OptionalOutput(object):
302
303

    oo_arguments = dict(
304
305
306
307
        output_format=OutputFormatArgument(
            'Show output in chosen output format (%s)' % ', '.join(
                OutputFormatArgument.formats),
            '--output-format'),
308
309
    )

310
    def print_(self, output, print_method=print_items, **print_method_kwargs):
311
312
313
        if self['output_format']:
            func = OutputFormatArgument.formats[self['output_format']]
            func(output, out=self)
314
        else:
315
            print_method_kwargs.setdefault('out', self)
316
            print_method(output, **print_method_kwargs)
317
318


319
class NameFilter(object):
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334

    nf_arguments = dict(
        name=ValueArgument('filter by name', '--name'),
        name_pref=ValueArgument(
            'filter by name prefix (case insensitive)', '--name-prefix'),
        name_suff=ValueArgument(
            'filter by name suffix (case insensitive)', '--name-suffix'),
        name_like=ValueArgument(
            'print only if name contains this (case insensitive)',
            '--name-like')
    )

    def _non_exact_name_filter(self, items):
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
        return [item for item in items if (
335
336
337
338
339
            (not np) or (item['name'] or '').lower().startswith(
                np.lower())) and (
            (not ns) or (item['name'] or '').lower().endswith(
                ns.lower())) and (
            (not nl) or nl.lower() in (item['name'] or '').lower())]
340
341

    def _exact_name_filter(self, items):
342
        return filter_dicts_by_dict(items, dict(name=self['name'] or '')) if (
343
344
345
346
347
348
            self['name']) else items

    def _filter_by_name(self, items):
        return self._non_exact_name_filter(self._exact_name_filter(items))


349
class IDFilter(object):
350
351
352
353
354
355
356
357

    if_arguments = dict(
        id=ValueArgument('filter by id', '--id'),
        id_pref=ValueArgument(
            'filter by id prefix (case insensitive)', '--id-prefix'),
        id_suff=ValueArgument(
            'filter by id suffix (case insensitive)', '--id-suffix'),
        id_like=ValueArgument(
358
            'print only if id contains this (case insensitive)', '--id-like')
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
    )

    def _non_exact_id_filter(self, items):
        np, ns, nl = self['id_pref'], self['id_suff'], self['id_like']
        return [item for item in items if (
            (not np) or (
                '%s' % item['id']).lower().startswith(np.lower())) and (
            (not ns) or ('%s' % item['id']).lower().endswith(ns.lower())) and (
            (not nl) or nl.lower() in ('%s' % item['id']).lower())]

    def _exact_id_filter(self, items):
        return filter_dicts_by_dict(items, dict(id=self['id'])) if (
            self['id']) else items

    def _filter_by_id(self, items):
        return self._non_exact_id_filter(self._exact_id_filter(items))
375
376
377
378
379
380
381
382


class Wait(object):
    wait_arguments = dict(
        progress_bar=ProgressBarArgument(
            'do not show progress bar', ('-N', '--no-progress-bar'), False)
    )

383
    def wait(
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
            self, service, service_id, status_method, current_status,
            countdown=True, timeout=60):
        (progress_bar, wait_cb) = self._safe_progress_bar(
            '%s %s: status is still %s' % (
                service, service_id, current_status),
            countdown=countdown, timeout=timeout)
        try:
            new_mode = status_method(
                service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
            if new_mode:
                self.error('%s %s: status is now %s' % (
                    service, service_id, new_mode))
            else:
                self.error('%s %s: status is still %s' % (
                    service, service_id, current_status))
        except KeyboardInterrupt:
            self.error('\n- canceled')
        finally:
            self._safe_progress_bar_finish(progress_bar)