Statistics
| Branch: | Tag: | Revision:

root / snf-django-lib / snf_django / management / commands / __init__.py @ 1e285210

History | View | Annotate | Download (13.5 kB)

1
# Copyright 2012-2014 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.
33

    
34
from optparse import make_option
35
from django.core.management.base import (BaseCommand,
36
                                         handle_default_options)
37
from django.core.management.base import CommandError as DjangoCommandError
38
from django.core.exceptions import FieldError
39

    
40
from snf_django.management import utils
41
from snf_django.lib.astakos import UserCache
42

    
43
import distutils
44

    
45
USER_EMAIL_FIELD = "user.email"
46

    
47

    
48
class CommandError(DjangoCommandError):
49
    def __str__(self):
50
        return utils.smart_locale_str(self.message)
51

    
52

    
53
class SynnefoCommand(BaseCommand):
54
    option_list = BaseCommand.option_list + (
55
        make_option(
56
            "--output-format",
57
            dest="output_format",
58
            metavar="[pretty, csv, json]",
59
            default="pretty",
60
            choices=["pretty", "csv", "json"],
61
            help="Select the output format: pretty [the default], json, "
62
                 "csv [comma-separated output]"),
63
    )
64

    
65
    def run_from_argv(self, argv):
66
        """Override BaseCommand.run_from_argv to decode arguments and options
67

68
        Convert command line arguments and options to unicode objects using
69
        user's preferred encoding.
70

71
        """
72
        argv = [utils.smart_locale_unicode(a) for a in argv]
73
        parser = self.create_parser(argv[0], argv[1])
74
        options, args = parser.parse_args(argv[2:])
75
        handle_default_options(options)
76
        self.execute(*args, **options.__dict__)
77

    
78

    
79
class ListCommand(SynnefoCommand):
80
    """Generic *-list management command.
81

82
    Management command to handle common tasks when implementing a -list
83
    management command. This class handles the following tasks:
84

85
    * Retrieving objects from database.
86

87
    The DB model class is declared in ``object_class`` class attribute. Also,
88
    results can be filter using either the ``filters`` and ``excludes``
89
    attribute or the "--filter-by" option.
90

91
    * Display specific fields of the database objects.
92

93
    List of available fields is defined in the ``FIELDS`` class attribute,
94
    which is a dictionary mapping from field names to tuples containing the
95
    way the field is retrieved and a text help message to display. The first
96
    field of the tuple is either a string containing a chain of attribute
97
    accesses (e.g. "machine.flavor.cpu") either a callable function, taking
98
    as argument the DB object and returning a single value.
99

100
    The fields that will be displayed be default is contained in the ``fields``
101
    class attribute. The user can specify different fields using the "--fields"
102
    option.
103

104
    * Handling of user UUIDs and names.
105

106
    If the ``user_uuid_field`` is declared, then "--user" and "--display-mails"
107
    options will become available. The first one allows filtering via either
108
    a user's UUID or display name. The "--displayname" option will append
109
    the displayname of ther user with "user_uuid_field" to the output.
110

111
    * Pretty printing output to a nice table.
112

113
    """
114

    
115
    # The following fields must be handled in the ListCommand subclasses!
116

    
117
    # The django DB model
118
    object_class = None
119
    # The name of the field containg the user ID of the user, if any.
120
    user_uuid_field = None
121
    # The name of the field containg the deleted flag, if any.
122
    deleted_field = None
123
    # Dictionary with all available fields
124
    FIELDS = {}
125
    # List of fields to display by default
126
    fields = []
127
    # Default filters and excludes
128
    filters = {}
129
    excludes = {}
130
    # Order results
131
    order_by = None
132

    
133
    # Fields used only with user_user_field
134
    astakos_auth_url = None
135
    astakos_token = None
136

    
137
    # Optimize DB queries
138
    prefetch_related = []
139
    select_related = []
140

    
141
    help = "Generic List Command"
142
    option_list = SynnefoCommand.option_list + (
143
        make_option(
144
            "-o", "--output",
145
            dest="fields",
146
            help="Comma-separated list of output fields"),
147
        make_option(
148
            "--list-fields",
149
            dest="list_fields",
150
            action="store_true",
151
            default=False,
152
            help="List available output fields"),
153
        make_option(
154
            "--filter-by",
155
            dest="filter_by",
156
            metavar="FILTERS",
157
            help="Filter results. Comma separated list of key `cond` val pairs"
158
                 " that displayed entries must satisfy. e.g."
159
                 " --filter-by \"deleted=False,id>=22\"."),
160
        make_option(
161
            "--list-filters",
162
            dest="list_filters",
163
            action="store_true",
164
            default=False,
165
            help="List available filters"),
166
        make_option(
167
            "--no-headers",
168
            dest="headers",
169
            action="store_false",
170
            default=True,
171
            help="Do not display headers"),
172
    )
173

    
174
    def __init__(self, *args, **kwargs):
175
        if self.user_uuid_field:
176
            assert(self.astakos_auth_url), "astakos_auth_url attribute is "\
177
                                           "needed when user_uuid_field "\
178
                                           "is declared"
179
            assert(self.astakos_token), "astakos_token attribute is needed"\
180
                                        " when user_uuid_field is declared"
181
            self.option_list += (
182
                make_option(
183
                    "-u", "--user",
184
                    dest="user",
185
                    metavar="USER",
186
                    help="List items only for this user."
187
                         " 'USER' can be either a user UUID or a display"
188
                         " name"),
189
                make_option(
190
                    "--display-mails",
191
                    dest="display_mails",
192
                    action="store_true",
193
                    default=False,
194
                    help="Include the user's email"),
195
            )
196

    
197
        if self.deleted_field:
198
            self.option_list += (
199
                make_option(
200
                    "-d", "--deleted",
201
                    dest="deleted",
202
                    action="store_true",
203
                    help="Display only deleted items"),
204
            )
205
        super(ListCommand, self).__init__(*args, **kwargs)
206

    
207
    def handle(self, *args, **options):
208
        if len(args) > 0:
209
            raise CommandError("List commands do not accept any argument")
210

    
211
        assert(self.object_class), "object_class variable must be declared"
212

    
213
        # If an user field is declared, include the USER_EMAIL_FIELD in the
214
        # available fields
215
        if self.user_uuid_field is not None:
216
            self.FIELDS[USER_EMAIL_FIELD] =\
217
                ("_user_email", "The email of the owner")
218

    
219
        if options["list_fields"]:
220
            self.display_fields()
221
            return
222

    
223
        if options["list_filters"]:
224
            self.display_filters()
225
            return
226

    
227
        # --output option
228
        if options["fields"]:
229
            fields = options["fields"]
230
            fields = fields.split(",")
231
            self.validate_fields(fields)
232
            self.fields = options["fields"].split(",")
233

    
234
        # --display-mails option
235
        if options.get("display_mails"):
236
            self.fields.append(USER_EMAIL_FIELD)
237

    
238
        # --filter-by option
239
        if options["filter_by"]:
240
            filters, excludes = \
241
                utils.parse_queryset_filters(options["filter_by"])
242
        else:
243
            filters, excludes = ({}, {})
244

    
245
        self.filters.update(filters)
246
        self.excludes.update(excludes)
247

    
248
        # --user option
249
        user = options.get("user")
250
        if user:
251
            if "@" in user:
252
                ucache = UserCache(self.astakos_auth_url, self.astakos_token)
253
                user = ucache.get_uuid(user)
254
            self.filters[self.user_uuid_field] = user
255

    
256
        # --deleted option
257
        if self.deleted_field:
258
            deleted = options.get("deleted")
259
            if deleted:
260
                self.filters[self.deleted_field] = True
261
            else:
262
                self.filters[self.deleted_field] = False
263

    
264
        # Special handling of arguments
265
        self.handle_args(self, *args, **options)
266

    
267
        select_related = getattr(self, "select_related", [])
268
        prefetch_related = getattr(self, "prefetch_related", [])
269

    
270
        objects = self.object_class.objects
271
        try:
272
            if select_related:
273
                objects = objects.select_related(*select_related)
274
            if prefetch_related:
275
                objects = objects.prefetch_related(*prefetch_related)
276
            objects = objects.filter(**self.filters)
277
            for key, value in self.excludes.iteritems():
278
                objects = objects.exclude(**{key: value})
279
        except FieldError as e:
280
            raise CommandError(e)
281
        except Exception as e:
282
            raise CommandError("Can not filter results: %s" % e)
283

    
284
        order_key = self.order_by if self.order_by is not None else 'pk'
285
        objects = objects.order_by(order_key)
286

    
287
        if USER_EMAIL_FIELD in self.fields:
288
            if '_user_email' in self.object_class._meta.get_all_field_names():
289
                raise RuntimeError("%s has already a 'user_mail' attribute")
290
            uuids = [getattr(obj, self.user_uuid_field) for obj in objects]
291
            ucache = UserCache(self.astakos_auth_url, self.astakos_token)
292
            ucache.fetch_names(list(set(uuids)))
293
            for obj in objects:
294
                uuid = getattr(obj, self.user_uuid_field)
295
                obj._user_email = ucache.get_name(uuid)
296

    
297
        # Special handling of DB results
298
        objects = list(objects)
299
        self.handle_db_objects(objects, **options)
300

    
301
        headers = self.fields
302
        columns = [self.FIELDS[key][0] for key in headers]
303

    
304
        table = []
305
        for obj in objects:
306
            row = []
307
            for attr in columns:
308
                if callable(attr):
309
                    row.append(attr(obj))
310
                else:
311
                    item = obj
312
                    attrs = attr.split(".")
313
                    for attr in attrs:
314
                        item = getattr(item, attr)
315
                    row.append(item)
316
            table.append(row)
317

    
318
        # Special handle of output
319
        self.handle_output(table, headers)
320

    
321
        # Print output
322
        output_format = options["output_format"]
323
        if output_format != "json" and not options["headers"]:
324
            headers = None
325
        utils.pprint_table(self.stdout, table, headers, output_format)
326

    
327
    def handle_args(self, *args, **kwargs):
328
        pass
329

    
330
    def handle_db_objects(self, objects, **options):
331
        pass
332

    
333
    def handle_output(self, table, headers):
334
        pass
335

    
336
    def display_fields(self):
337
        headers = ["Field", "Description"]
338
        table = []
339
        for field, (_, help_msg) in self.FIELDS.items():
340
            table.append((field, help_msg))
341
        utils.pprint_table(self.stdout, table, headers)
342

    
343
    def validate_fields(self, fields):
344
        for f in fields:
345
            if f not in self.FIELDS.keys():
346
                raise CommandError("Unknown field '%s'. 'Use --list-fields"
347
                                   " option to find out available fields."
348
                                   % f)
349

    
350
    def display_filters(self):
351
        headers = ["Filter", "Description", "Help"]
352
        table = []
353
        for field in self.object_class._meta.fields:
354
            table.append((field.name, field.verbose_name, field.help_text))
355
        utils.pprint_table(self.stdout, table, headers)
356

    
357

    
358
class RemoveCommand(BaseCommand):
359
    help = "Generic remove command"
360
    option_list = BaseCommand.option_list + (
361
        make_option(
362
            "-f", "--force",
363
            dest="force",
364
            action="store_true",
365
            default=False,
366
            help="Do not prompt for confirmation"),
367
    )
368

    
369
    def confirm_deletion(self, force, resource='', args=''):
370
        if force is True:
371
            return True
372

    
373
        ids = ', '.join(args)
374
        self.stdout.write("Are you sure you want to delete %s %s?"
375
                          " [Y/N] " % (resource, ids))
376
        try:
377
            answer = distutils.util.strtobool(raw_input())
378
            if answer != 1:
379
                raise CommandError("Aborting deletion")
380
        except ValueError:
381
            raise CommandError("Unaccepted input value. Please choose yes/no"
382
                               " (y/n).")