Statistics
| Branch: | Tag: | Revision:

root / snf-django-lib / snf_django / management / commands / __init__.py @ 07661d4d

History | View | Annotate | Download (14.4 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, OptionParser, OptionGroup,
35
                      TitledHelpFormatter)
36

    
37
from django.core.management.base import BaseCommand, CommandError
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 SynnefoCommandFormatter(TitledHelpFormatter):
49
    def format_heading(self, heading):
50
        if heading == "Options":
51
            return ""
52
        return "%s\n%s\n" % (heading, "=-"[self.level] * len(heading))
53

    
54

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

    
67
    def create_parser(self, prog_name, subcommand):
68
        parser = OptionParser(prog=prog_name, add_help_option=False,
69
                              formatter=SynnefoCommandFormatter())
70

    
71
        parser.set_usage(self.usage(subcommand))
72
        parser.version = self.get_version()
73

    
74
        # Handle Django's and common options
75
        common_options = OptionGroup(parser, "Common Options")
76
        common_options.add_option("-h", "--help", action="help",
77
                                  help="show this help message and exit")
78

    
79
        common_options.add_option("--version", action="version",
80
                                  help="show program's version number and"
81
                                       "  exit")
82
        [common_options.add_option(o) for o in self.option_list]
83
        if common_options.option_list:
84
            parser.add_option_group(common_options)
85

    
86
        # Handle command specific options
87
        command_options = OptionGroup(parser, "Command Specific Options")
88
        [command_options.add_option(o)
89
         for o in getattr(self, "command_option_list", ())]
90
        if command_options.option_list:
91
            parser.add_option_group(command_options)
92

    
93
        return parser
94

    
95
    def pprint_table(self, *args, **kwargs):
96
        utils.pprint_table(self.stdout, *args, **kwargs)
97

    
98

    
99
class ListCommand(SynnefoCommand):
100
    """Generic *-list management command.
101

102
    Management command to handle common tasks when implementing a -list
103
    management command. This class handles the following tasks:
104

105
    * Retrieving objects from database.
106

107
    The DB model class is declared in ``object_class`` class attribute. Also,
108
    results can be filter using either the ``filters`` and ``excludes``
109
    attribute or the "--filter-by" option.
110

111
    * Display specific fields of the database objects.
112

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

120
    The fields that will be displayed be default is contained in the ``fields``
121
    class attribute. The user can specify different fields using the "--fields"
122
    option.
123

124
    * Handling of user UUIDs and names.
125

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

131
    * Pretty printing output to a nice table.
132

133
    """
134

    
135
    # The following fields must be handled in the ListCommand subclasses!
136

    
137
    # The django DB model
138
    object_class = None
139
    # The name of the field containg the user ID of the user, if any.
140
    user_uuid_field = None
141
    # The name of the field containg the deleted flag, if any.
142
    deleted_field = None
143
    # Dictionary with all available fields
144
    FIELDS = {}
145
    # List of fields to display by default
146
    fields = []
147
    # Default filters and excludes
148
    filters = {}
149
    excludes = {}
150
    # Order results
151
    order_by = None
152

    
153
    # Fields used only with user_user_field
154
    astakos_auth_url = None
155
    astakos_token = None
156

    
157
    # Optimize DB queries
158
    prefetch_related = []
159
    select_related = []
160

    
161
    help = "Generic List Command"
162
    option_list = SynnefoCommand.option_list + (
163
        make_option(
164
            "-o", "--output",
165
            dest="fields",
166
            help="Comma-separated list of output fields"),
167
        make_option(
168
            "--list-fields",
169
            dest="list_fields",
170
            action="store_true",
171
            default=False,
172
            help="List available output fields"),
173
        make_option(
174
            "--filter-by",
175
            dest="filter_by",
176
            metavar="FILTERS",
177
            help="Filter results. Comma separated list of key `cond` val pairs"
178
                 " that displayed entries must satisfy. e.g."
179
                 " --filter-by \"deleted=False,id>=22\"."),
180
        make_option(
181
            "--list-filters",
182
            dest="list_filters",
183
            action="store_true",
184
            default=False,
185
            help="List available filters"),
186
        make_option(
187
            "--no-headers",
188
            dest="headers",
189
            action="store_false",
190
            default=True,
191
            help="Do not display headers"),
192
    )
193

    
194
    def __init__(self, *args, **kwargs):
195
        if self.user_uuid_field:
196
            assert(self.astakos_auth_url), "astakos_auth_url attribute is "\
197
                                           "needed when user_uuid_field "\
198
                                           "is declared"
199
            assert(self.astakos_token), "astakos_token attribute is needed"\
200
                                        " when user_uuid_field is declared"
201
            self.option_list += (
202
                make_option(
203
                    "-u", "--user",
204
                    dest="user",
205
                    metavar="USER",
206
                    help="List items only for this user."
207
                         " 'USER' can be either a user UUID or a display"
208
                         " name"),
209
                make_option(
210
                    "--display-mails",
211
                    dest="display_mails",
212
                    action="store_true",
213
                    default=False,
214
                    help="Include the user's email"),
215
            )
216

    
217
        if self.deleted_field:
218
            self.option_list += (
219
                make_option(
220
                    "-d", "--deleted",
221
                    dest="deleted",
222
                    action="store_true",
223
                    help="Display only deleted items"),
224
            )
225
        super(ListCommand, self).__init__(*args, **kwargs)
226

    
227
    def handle(self, *args, **options):
228
        if len(args) > 0:
229
            raise CommandError("List commands do not accept any argument")
230

    
231
        assert(self.object_class), "object_class variable must be declared"
232

    
233
        # If an user field is declared, include the USER_EMAIL_FIELD in the
234
        # available fields
235
        if self.user_uuid_field is not None:
236
            self.FIELDS[USER_EMAIL_FIELD] =\
237
                ("_user_email", "The email of the owner")
238

    
239
        if options["list_fields"]:
240
            self.display_fields()
241
            return
242

    
243
        if options["list_filters"]:
244
            self.display_filters()
245
            return
246

    
247
        # --output option
248
        if options["fields"]:
249
            fields = options["fields"]
250
            fields = fields.split(",")
251
            self.validate_fields(fields)
252
            self.fields = options["fields"].split(",")
253

    
254
        # --display-mails option
255
        if options.get("display_mails"):
256
            self.fields.append(USER_EMAIL_FIELD)
257

    
258
        # --filter-by option
259
        if options["filter_by"]:
260
            filters, excludes = \
261
                utils.parse_queryset_filters(options["filter_by"])
262
        else:
263
            filters, excludes = ({}, {})
264

    
265
        self.filters.update(filters)
266
        self.excludes.update(excludes)
267

    
268
        # --user option
269
        user = options.get("user")
270
        if user:
271
            if "@" in user:
272
                ucache = UserCache(self.astakos_auth_url, self.astakos_token)
273
                user = ucache.get_uuid(user)
274
            self.filters[self.user_uuid_field] = user
275

    
276
        # --deleted option
277
        if self.deleted_field:
278
            deleted = options.get("deleted")
279
            if deleted:
280
                self.filters[self.deleted_field] = True
281
            else:
282
                self.filters[self.deleted_field] = False
283

    
284
        # Special handling of arguments
285
        self.handle_args(self, *args, **options)
286

    
287
        select_related = getattr(self, "select_related", [])
288
        prefetch_related = getattr(self, "prefetch_related", [])
289

    
290
        objects = self.object_class.objects
291
        try:
292
            if select_related:
293
                objects = objects.select_related(*select_related)
294
            if prefetch_related:
295
                objects = objects.prefetch_related(*prefetch_related)
296
            objects = objects.filter(**self.filters)
297
            for key, value in self.excludes.iteritems():
298
                objects = objects.exclude(**{key:value})
299
        except FieldError as e:
300
            raise CommandError(e)
301
        except Exception as e:
302
            raise CommandError("Can not filter results: %s" % e)
303

    
304
        order_key = self.order_by if self.order_by is not None else 'pk'
305
        objects = objects.order_by(order_key)
306

    
307
        if USER_EMAIL_FIELD in self.fields:
308
            if '_user_email' in self.object_class._meta.get_all_field_names():
309
                raise RuntimeError("%s has already a 'user_mail' attribute")
310
            uuids = [getattr(obj, self.user_uuid_field) for obj in objects]
311
            ucache = UserCache(self.astakos_auth_url, self.astakos_token)
312
            ucache.fetch_names(list(set(uuids)))
313
            for obj in objects:
314
                uuid = getattr(obj, self.user_uuid_field)
315
                obj._user_email = ucache.get_name(uuid)
316

    
317
        # Special handling of DB results
318
        objects = list(objects)
319
        self.handle_db_objects(objects, **options)
320

    
321
        headers = self.fields
322
        columns = [self.FIELDS[key][0] for key in headers]
323

    
324
        table = []
325
        for obj in objects:
326
            row = []
327
            for attr in columns:
328
                if callable(attr):
329
                    row.append(attr(obj))
330
                else:
331
                    item = obj
332
                    attrs = attr.split(".")
333
                    for attr in attrs:
334
                        item = getattr(item, attr)
335
                    row.append(item)
336
            table.append(row)
337

    
338
        # Special handle of output
339
        self.handle_output(table, headers)
340

    
341
        # Print output
342
        output_format = options["output_format"]
343
        if output_format != "json" and not options["headers"]:
344
            headers = None
345
        utils.pprint_table(self.stdout, table, headers, output_format)
346

    
347
    def handle_args(self, *args, **kwargs):
348
        pass
349

    
350
    def handle_db_objects(self, objects, **options):
351
        pass
352

    
353
    def handle_output(self, table, headers):
354
        pass
355

    
356
    def display_fields(self):
357
        headers = ["Field", "Description"]
358
        table = []
359
        for field, (_, help_msg) in self.FIELDS.items():
360
            table.append((field, help_msg))
361
        utils.pprint_table(self.stdout, table, headers)
362

    
363
    def validate_fields(self, fields):
364
        for f in fields:
365
            if f not in self.FIELDS.keys():
366
                raise CommandError("Unknown field '%s'. 'Use --list-fields"
367
                                   " option to find out available fields."
368
                                   % f)
369

    
370
    def display_filters(self):
371
        headers = ["Filter", "Description", "Help"]
372
        table = []
373
        for field in self.object_class._meta.fields:
374
            table.append((field.name, field.verbose_name, field.help_text))
375
        utils.pprint_table(self.stdout, table, headers)
376

    
377

    
378
class RemoveCommand(BaseCommand):
379
    help = "Generic remove command"
380
    option_list = BaseCommand.option_list + (
381
        make_option(
382
            "-f", "--force",
383
            dest="force",
384
            action="store_true",
385
            default=False,
386
            help="Do not prompt for confirmation"),
387
    )
388

    
389
    def confirm_deletion(self, force, resource='', args=''):
390
        if force is True:
391
            return True
392

    
393
        ids = ', '.join(args)
394
        self.stdout.write("Are you sure you want to delete %s %s?"
395
                          " [Y/N] " % (resource, ids))
396
        try:
397
            answer = distutils.util.strtobool(raw_input())
398
            if answer != 1:
399
                raise CommandError("Aborting deletion")
400
        except ValueError:
401
            raise CommandError("Unaccepted input value. Please choose yes/no"
402
                               " (y/n).")