__init__.py 14.3 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
from kamaki.cli.cmds import errors
44
from kamaki.clients.utils import escape_ctrl_chars
45

46

47
log = get_logger(__name__)
48

Stavros Sachtouris's avatar
Stavros Sachtouris committed
49

50
def dont_raise(*errs):
51 52 53 54
    def decorator(func):
        def wrap(*args, **kwargs):
            try:
                return func(*args, **kwargs)
55
            except errs as e:
56 57 58 59 60 61 62
                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
63 64


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


74
def fall_back(func):
75 76 77 78 79 80 81 82 83 84 85
    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


86
class CommandInit(object):
87

88 89 90 91 92 93
    # 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

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

121 122 123 124
    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]):
125
            astakos = getattr(self, 'astakos', None)
126 127 128 129 130 131 132 133 134
            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)

135 136 137 138
    @errors.Astakos.project_id
    def _project_id_exists(self, project_id):
        self.astakos.get_client().get_project(project_id)

139
    @dont_raise(UnicodeError)
140
    def write(self, s):
141
        self._out.write(s.encode(pref_enc, errors='replace'))
142 143 144
        self._out.flush()

    def writeln(self, s=''):
145
        self.write('%s\n' % s)
146 147

    def error(self, s=''):
148 149
        esc_s = escape_ctrl_chars(s)
        self._err.write(('%s\n' % esc_s).encode(pref_enc, errors='replace'))
150 151
        self._err.flush()

152 153
    def print_list(self, *args, **kwargs):
        kwargs.setdefault('out', self._out)
154
        return print_list(*args, **kwargs)
155 156

    def print_dict(self, *args, **kwargs):
157
        kwargs.setdefault('out', self)
158
        return print_dict(*args, **kwargs)
159 160

    def print_json(self, *args, **kwargs):
161
        kwargs.setdefault('out', self)
162
        return print_json(*args, **kwargs)
163 164

    def print_items(self, *args, **kwargs):
165
        kwargs.setdefault('out', self)
166
        return print_items(*args, **kwargs)
167 168 169

    def ask_user(self, *args, **kwargs):
        kwargs.setdefault('user_in', self._in)
170
        kwargs.setdefault('out', self)
171
        return ask_user(*args, **kwargs)
172

173
    @dont_raise(KeyError)
174
    def _custom_url(self, service):
175
        return self.config.get_cloud(self.cloud, '%s_url' % service)
176

177
    @dont_raise(KeyError)
178
    def _custom_token(self, service):
179
        return self.config.get_cloud(self.cloud, '%s_token' % service)
180

181
    @dont_raise(KeyError)
182
    def _custom_type(self, service):
183
        return self.config.get_cloud(self.cloud, '%s_type' % service)
184

185
    @dont_raise(KeyError)
186
    def _custom_version(self, service):
187
        return self.config.get_cloud(self.cloud, '%s_version' % service)
188

189
    def _uuids2usernames(self, uuids):
190
        return self.astakos.post_user_catalogs(uuids)
191 192

    def _usernames2uuids(self, username):
193
        return self.astakos.post_user_catalogs(displaynames=username)
194

195
    def _uuid2username(self, uuid):
196
        return self._uuids2usernames([uuid]).get(uuid, None)
197 198

    def _username2uuid(self, username):
199
        return self._usernames2uuids([username]).get(username, None)
200

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

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

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

241 242 243 244
    def __getitem__(self, argterm):
        """
        :param argterm: (str) the name/label of an argument in self.arguments

245 246
        :returns: the value of the corresponding Argument (not the argument
            object)
247 248 249

        :raises KeyError: if argterm not in self.arguments of this object
        """
250
        return self.arguments[argterm].value
251 252 253 254 255 256 257 258 259 260 261 262 263

    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

264 265 266 267 268 269 270 271 272 273
    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]

274
    def get_argument(self, argterm):
275 276 277 278 279 280 281
        """
        :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
        """
282
        return self[argterm]
283 284


285 286 287
#  feature classes - inherit them to get special features for your commands


288 289 290
class OutputFormatArgument(ValueArgument):
    """Accepted output formats: json (default)"""

291
    formats = dict(json=print_json)
292 293 294

    def ___init__(self, *args, **kwargs):
        super(OutputFormatArgument, self).___init__(*args, **kwargs)
295
        self.value = None
296 297 298

    def value(self, newvalue):
        if not newvalue:
299
            return
300
        elif newvalue.lower() in self.formats:
301
            self.value = newvalue.lower()
302 303
        else:
            raise CLIInvalidArgument(
304
                'Invalid value %s for argument %s' % (newvalue, self.lvalue),
305 306 307
                details=['Valid output formats: %s' % ', '.join(self.formats)])


308
class OptionalOutput(object):
309 310

    oo_arguments = dict(
311 312 313 314
        output_format=OutputFormatArgument(
            'Show output in chosen output format (%s)' % ', '.join(
                OutputFormatArgument.formats),
            '--output-format'),
315 316
    )

317
    def print_(self, output, print_method=print_items, **print_method_kwargs):
318 319 320
        if self['output_format']:
            func = OutputFormatArgument.formats[self['output_format']]
            func(output, out=self)
321
        else:
322
            print_method_kwargs.setdefault('out', self)
323
            print_method(output, **print_method_kwargs)
324 325


326
class NameFilter(object):
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341

    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 (
342 343 344 345 346
            (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())]
347 348

    def _exact_name_filter(self, items):
349
        return filter_dicts_by_dict(items, dict(name=self['name'] or '')) if (
350 351 352 353 354 355
            self['name']) else items

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


356
class IDFilter(object):
357 358 359 360 361 362 363 364

    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(
365
            'print only if id contains this (case insensitive)', '--id-like')
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
    )

    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))
382 383 384 385 386 387 388 389


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

390
    def wait(
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
            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)