Revision 7cfc0cef snf-webproject/synnefo/webproject/management/commands/__init__.py

b/snf-webproject/synnefo/webproject/management/commands/__init__.py
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 synnefo.webproject.management import utils
40
from snf_django.lib.astakos import UserCache
41

  
42

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

  
55

  
56
class ListCommand(BaseCommand):
57
    """Generic *-list management command.
58

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

  
62
    * Retrieving objects from database.
63

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

  
68
    * Display specific fields of the database objects.
69

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

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

  
81
    * Handling of user UUIDs and names.
82

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

  
88
    * Pretty printing output to a nice table.
89

  
90
    """
91

  
92
    # The following fields must be handled in the ListCommand subclasses!
93

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

  
110
    # Fields used only with user_user_field
111
    astakos_url = None
112
    astakos_token = None
113

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

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

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

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

  
191
        assert(self.object_class), "object_class variable must be declared"
192

  
193
        if options["list_fields"]:
194
            self.display_fields()
195
            return
196

  
197
        if options["list_filters"]:
198
            self.display_filters()
199
            return
200

  
201
        # --output option
202
        if options["fields"]:
203
            fields = options["fields"]
204
            fields = fields.split(",")
205
            self.validate_fields(fields)
206
            self.fields = options["fields"].split(",")
207

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

  
214
        self.filters.update(filters)
215
        self.excludes.update(excludes)
216

  
217
        # --user option
218
        user = options.get("user")
219
        if user:
220
            if "@" in user:
221
                ucache = UserCache(self.astakos_url, self.astakos_token)
222
                user = ucache.get_uuid(user)
223
            self.filters[self.user_uuid_field] = user
224

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

  
233
        # Special handling of arguments
234
        self.handle_args(self, *args, **options)
235

  
236
        objects = self.object_class.objects
237
        try:
238
            objects = objects.filter(**self.filters)
239
            objects = objects.exclude(**self.excludes)
240
        except FieldError as e:
241
            raise CommandError(e)
242
        except Exception as e:
243
            raise CommandError("Can not filter results: %s" % e)
244

  
245
        order_key = self.order_by if self.order_by is not None else 'pk'
246
        objects = objects.order_by(order_key)
247

  
248
        # --display-mails option
249
        display_mails = options.get("display_mails")
250
        if display_mails:
251
            if 'user_mail' in self.object_class._meta.get_all_field_names():
252
                raise RuntimeError("%s has already a 'user_mail' attribute")
253

  
254
            self.fields.append("user.email")
255
            self.FIELDS["user.email"] =\
256
                ("user_email", "The email of the owner.")
257
            uuids = [getattr(obj, self.user_uuid_field) for obj in objects]
258
            ucache = UserCache(self.astakos_url, self.astakos_token)
259
            ucache.fetch_names(list(set(uuids)))
260
            for obj in objects:
261
                uuid = getattr(obj, self.user_uuid_field)
262
                obj.user_email = ucache.get_name(uuid)
263

  
264
        # Special handling of DB results
265
        objects = list(objects)
266
        self.handle_db_objects(objects, **options)
267

  
268
        headers = self.fields
269
        columns = [self.FIELDS[key][0] for key in headers]
270

  
271
        table = []
272
        for obj in objects:
273
            row = []
274
            for attr in columns:
275
                if callable(attr):
276
                    row.append(attr(obj))
277
                else:
278
                    item = obj
279
                    attrs = attr.split(".")
280
                    for attr in attrs:
281
                        item = getattr(item, attr)
282
                    row.append(item)
283
            table.append(row)
284

  
285
        # Special handle of output
286
        self.handle_output(table, headers)
287

  
288
        # Print output
289
        output_format = options["output_format"]
290
        if output_format != "json" and not options["headers"]:
291
            headers = None
292
        utils.pprint_table(self.stdout, table, headers, output_format)
293

  
294
    def handle_args(self, *args, **kwargs):
295
        pass
296

  
297
    def handle_db_objects(self, objects, **options):
298
        pass
299

  
300
    def handle_output(self, table, headers):
301
        pass
302

  
303
    def display_fields(self):
304
        headers = ["Field", "Description"]
305
        table = []
306
        for field, (_, help_msg) in self.FIELDS.items():
307
            table.append((field, help_msg))
308
        utils.pprint_table(self.stdout, table, headers)
309

  
310
    def validate_fields(self, fields):
311
        for f in fields:
312
            if f not in self.FIELDS.keys():
313
                raise CommandError("Unknown field '%s'. 'Use --list-fields"
314
                                   " option to find out available fields."
315
                                   % f)
316

  
317
    def display_filters(self):
318
        headers = ["Filter", "Description", "Help"]
319
        table = []
320
        for field in self.object_class._meta.fields:
321
            table.append((field.name, field.verbose_name, field.help_text))
322
        utils.pprint_table(self.stdout, table, headers)

Also available in: Unified diff