Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.8 kB)

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

    
34
from optparse import make_option
35

    
36
from django.core.management.base import BaseCommand, CommandError
37
from django.core.exceptions import FieldError
38

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

    
42
import distutils
43

    
44

    
45
class SynnefoCommand(BaseCommand):
46
    option_list = BaseCommand.option_list + (
47
        make_option(
48
            "--output-format",
49
            dest="output_format",
50
            metavar="[pretty, csv, json]",
51
            default="pretty",
52
            choices=["pretty", "csv", "json"],
53
            help="Select the output format: pretty [the default], tabs"
54
                 " [tab-separated output], csv [comma-separated output]"),
55
    )
56

    
57

    
58
class ListCommand(BaseCommand):
59
    """Generic *-list management command.
60

61
    Management command to handle common tasks when implementing a -list
62
    management command. This class handles the following tasks:
63

64
    * Retrieving objects from database.
65

66
    The DB model class is declared in ``object_class`` class attribute. Also,
67
    results can be filter using either the ``filters`` and ``excludes``
68
    attribute or the "--filter-by" option.
69

70
    * Display specific fields of the database objects.
71

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

79
    The fields that will be displayed be default is contained in the ``fields``
80
    class attribute. The user can specify different fields using the "--fields"
81
    option.
82

83
    * Handling of user UUIDs and names.
84

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

90
    * Pretty printing output to a nice table.
91

92
    """
93

    
94
    # The following fields must be handled in the ListCommand subclasses!
95

    
96
    # The django DB model
97
    object_class = None
98
    # The name of the field containg the user ID of the user, if any.
99
    user_uuid_field = None
100
    # The name of the field containg the deleted flag, if any.
101
    deleted_field = None
102
    # Dictionary with all available fields
103
    FIELDS = {}
104
    # List of fields to display by default
105
    fields = []
106
    # Default filters and excludes
107
    filters = {}
108
    excludes = {}
109
    # Order results
110
    order_by = None
111

    
112
    # Fields used only with user_user_field
113
    astakos_auth_url = None
114
    astakos_token = None
115

    
116
    help = "Generic List Command"
117
    option_list = BaseCommand.option_list + (
118
        make_option(
119
            "-o", "--output",
120
            dest="fields",
121
            help="Comma-separated list of output fields"),
122
        make_option(
123
            "--list-fields",
124
            dest="list_fields",
125
            action="store_true",
126
            default=False,
127
            help="List available output fields"),
128
        make_option(
129
            "--filter-by",
130
            dest="filter_by",
131
            metavar="FILTERS",
132
            help="Filter results. Comma separated list of key `cond` val pairs"
133
                 " that displayed entries must satisfy. e.g."
134
                 " --filter-by \"deleted=False,id>=22\"."),
135
        make_option(
136
            "--list-filters",
137
            dest="list_filters",
138
            action="store_true",
139
            default=False,
140
            help="List available filters"),
141
        make_option(
142
            "--no-headers",
143
            dest="headers",
144
            action="store_false",
145
            default=True,
146
            help="Do not display headers"),
147
        make_option(
148
            "--output-format",
149
            dest="output_format",
150
            metavar="[pretty, csv, json]",
151
            default="pretty",
152
            choices=["pretty", "csv", "json"],
153
            help="Select the output format: pretty [the default], tabs"
154
                 " [tab-separated output], csv [comma-separated output]"),
155
    )
156

    
157
    def __init__(self, *args, **kwargs):
158
        if self.user_uuid_field:
159
            assert(self.astakos_auth_url), "astakos_auth_url attribute is "\
160
                                           "needed when user_uuid_field "\
161
                                           "is declared"
162
            assert(self.astakos_token), "astakos_token attribute is needed"\
163
                                        " when user_uuid_field is declared"
164
            self.option_list += (
165
                make_option(
166
                    "-u", "--user",
167
                    dest="user",
168
                    metavar="USER",
169
                    help="List items only for this user."
170
                         " 'USER' can be either a user UUID or a display"
171
                         " name"),
172
                make_option(
173
                    "--display-mails",
174
                    dest="display_mails",
175
                    action="store_true",
176
                    default=False,
177
                    help="Include the user's email"),
178
            )
179

    
180
        if self.deleted_field:
181
            self.option_list += (
182
                make_option(
183
                    "-d", "--deleted",
184
                    dest="deleted",
185
                    action="store_true",
186
                    help="Display only deleted items"),
187
            )
188
        super(ListCommand, self).__init__(*args, **kwargs)
189

    
190
    def handle(self, *args, **options):
191
        if len(args) > 0:
192
            raise CommandError("List commands do not accept any argument")
193

    
194
        assert(self.object_class), "object_class variable must be declared"
195

    
196
        if options["list_fields"]:
197
            self.display_fields()
198
            return
199

    
200
        if options["list_filters"]:
201
            self.display_filters()
202
            return
203

    
204
        # --output option
205
        if options["fields"]:
206
            fields = options["fields"]
207
            fields = fields.split(",")
208
            self.validate_fields(fields)
209
            self.fields = options["fields"].split(",")
210

    
211
        # --filter-by option
212
        if options["filter_by"]:
213
            filters, excludes = utils.parse_filters(options["filter_by"])
214
        else:
215
            filters, excludes = ({}, {})
216

    
217
        self.filters.update(filters)
218
        self.excludes.update(excludes)
219

    
220
        # --user option
221
        user = options.get("user")
222
        if user:
223
            if "@" in user:
224
                ucache = UserCache(self.astakos_auth_url, self.astakos_token)
225
                user = ucache.get_uuid(user)
226
            self.filters[self.user_uuid_field] = user
227

    
228
        # --deleted option
229
        if self.deleted_field:
230
            deleted = options.get("deleted")
231
            if deleted:
232
                self.filters[self.deleted_field] = True
233
            else:
234
                self.filters[self.deleted_field] = False
235

    
236
        # Special handling of arguments
237
        self.handle_args(self, *args, **options)
238

    
239
        select_related = getattr(self, "select_related", [])
240
        prefetch_related = getattr(self, "prefetch_related", [])
241

    
242
        objects = self.object_class.objects
243
        try:
244
            for sr in select_related:
245
                objects = objects.select_related(sr)
246
            for pr in prefetch_related:
247
                objects = objects.prefetch_related(pr)
248
            objects = objects.filter(**self.filters)
249
            objects = objects.exclude(**self.excludes)
250
        except FieldError as e:
251
            raise CommandError(e)
252
        except Exception as e:
253
            raise CommandError("Can not filter results: %s" % e)
254

    
255
        order_key = self.order_by if self.order_by is not None else 'pk'
256
        objects = objects.order_by(order_key)
257

    
258
        # --display-mails option
259
        display_mails = options.get("display_mails")
260
        if display_mails:
261
            if 'user_mail' in self.object_class._meta.get_all_field_names():
262
                raise RuntimeError("%s has already a 'user_mail' attribute")
263

    
264
            self.fields.append("user.email")
265
            self.FIELDS["user.email"] =\
266
                ("user_email", "The email of the owner.")
267
            uuids = [getattr(obj, self.user_uuid_field) for obj in objects]
268
            ucache = UserCache(self.astakos_auth_url, self.astakos_token)
269
            ucache.fetch_names(list(set(uuids)))
270
            for obj in objects:
271
                uuid = getattr(obj, self.user_uuid_field)
272
                obj.user_email = ucache.get_name(uuid)
273

    
274
        # Special handling of DB results
275
        objects = list(objects)
276
        self.handle_db_objects(objects, **options)
277

    
278
        headers = self.fields
279
        columns = [self.FIELDS[key][0] for key in headers]
280

    
281
        table = []
282
        for obj in objects:
283
            row = []
284
            for attr in columns:
285
                if callable(attr):
286
                    row.append(attr(obj))
287
                else:
288
                    item = obj
289
                    attrs = attr.split(".")
290
                    for attr in attrs:
291
                        item = getattr(item, attr)
292
                    row.append(item)
293
            table.append(row)
294

    
295
        # Special handle of output
296
        self.handle_output(table, headers)
297

    
298
        # Print output
299
        output_format = options["output_format"]
300
        if output_format != "json" and not options["headers"]:
301
            headers = None
302
        utils.pprint_table(self.stdout, table, headers, output_format)
303

    
304
    def handle_args(self, *args, **kwargs):
305
        pass
306

    
307
    def handle_db_objects(self, objects, **options):
308
        pass
309

    
310
    def handle_output(self, table, headers):
311
        pass
312

    
313
    def display_fields(self):
314
        headers = ["Field", "Description"]
315
        table = []
316
        for field, (_, help_msg) in self.FIELDS.items():
317
            table.append((field, help_msg))
318
        utils.pprint_table(self.stdout, table, headers)
319

    
320
    def validate_fields(self, fields):
321
        for f in fields:
322
            if f not in self.FIELDS.keys():
323
                raise CommandError("Unknown field '%s'. 'Use --list-fields"
324
                                   " option to find out available fields."
325
                                   % f)
326

    
327
    def display_filters(self):
328
        headers = ["Filter", "Description", "Help"]
329
        table = []
330
        for field in self.object_class._meta.fields:
331
            table.append((field.name, field.verbose_name, field.help_text))
332
        utils.pprint_table(self.stdout, table, headers)
333

    
334

    
335
class RemoveCommand(BaseCommand):
336
    help = "Generic remove command"
337
    option_list = BaseCommand.option_list + (
338
        make_option(
339
            "-f", "--force",
340
            dest="force",
341
            action="store_true",
342
            default=False,
343
            help="Do not prompt for confirmation"),
344
    )
345

    
346
    def confirm_deletion(self, force, resource='', args=''):
347
        if force is True:
348
            return True
349

    
350
        ids = ', '.join(args)
351
        self.stdout.write("Are you sure you want to delete %s %s?"
352
                          " [Y/N]\n" % (resource, ids))
353
        try:
354
            answer = distutils.util.strtobool(raw_input())
355
            if answer != 1:
356
                raise CommandError("Aborting deletion")
357
        except ValueError:
358
            raise CommandError("Invalid Y/N value, aborting")