Statistics
| Branch: | Tag: | Revision:

root / snf-webproject / synnefo / webproject / management / commands / __init__.py @ b0e7f310

History | View | Annotate | Download (10.4 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 synnefo.webproject.management import util
40
from synnefo.management.common import UserCache
41

    
42

    
43
class ListCommand(BaseCommand):
44
    """Generic *-list management command.
45

46
    Management command to handle common tasks when implementing a -list
47
    management command. This class handles the following tasks:
48

49
    * Retrieving objects from database.
50

51
    The DB model class is declared in ``object_class`` class attribute. Also,
52
    results can be filter using either the ``filters`` and ``excludes``
53
    attribute or the "--filter-by" option.
54

55
    * Display specific fields of the database objects.
56

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

64
    The fields that will be displayed be default is contained in the ``fields``
65
    class attribute. The user can specify different fields using the "--fields"
66
    option.
67

68
    * Handling of user UUIDs and names.
69

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

75
    * Pretty printing output to a nice table.
76

77
    """
78

    
79
    # The following fields must be handled in the ListCommand subclasses!
80

    
81
    # The django DB model
82
    object_class = None
83
    # The name of the field containg the user ID of the user, if any.
84
    user_uuid_field = None
85
    # The name of the field containg the deleted flag, if any.
86
    deleted_field = None
87
    # Dictionary with all available fields
88
    FIELDS = {}
89
    # List of fields to display by default
90
    fields = []
91
    # Default filters and excludes
92
    filters = {}
93
    excludes = {}
94

    
95
    help = "Generic List Command"
96
    option_list = BaseCommand.option_list + (
97
        make_option(
98
            "-o", "--output",
99
            dest="fields",
100
            help="Comma-separated list of output fields"),
101
        make_option(
102
            "--list-fields",
103
            dest="list_fields",
104
            action="store_true",
105
            default=False,
106
            help="List available output fields"),
107
        make_option(
108
            "--filter-by",
109
            dest="filter_by",
110
            metavar="FILTERS",
111
            help="Filter results. Comma separated list of key `cond` val pairs"
112
                 " that displayed entries must satisfy. e.g."
113
                 " --filter-by \"deleted=False,id>=22\"."),
114
        make_option(
115
            "--list-filters",
116
            dest="list_filters",
117
            action="store_true",
118
            default=False,
119
            help="List available filters"),
120
        make_option(
121
            "--no-headers",
122
            dest="headers",
123
            action="store_false",
124
            default=True,
125
            help="Do not display headers"),
126
        make_option(
127
            "--output-format",
128
            dest="output_format",
129
            metavar="[pretty, csv, json]",
130
            default="pretty",
131
            choices=["pretty", "csv", "json"],
132
            help="Select the output format: pretty [the default], tabs"
133
                 " [tab-separated output], csv [comma-separated output]"),
134
    )
135

    
136
    def __init__(self, *args, **kwargs):
137
        if self.user_uuid_field:
138
            self.option_list += (
139
                make_option(
140
                    "-u", "--user",
141
                    dest="user",
142
                    metavar="USER",
143
                    help="List items only for this user."
144
                         " 'USER' can be either a user UUID or a display"
145
                         " name"),
146
                make_option(
147
                    "--display-mails",
148
                    dest="display_mails",
149
                    action="store_true",
150
                    default=False,
151
                    help="Include the user's email"),
152
            )
153

    
154
        if self.deleted_field:
155
            self.option_list += (
156
                make_option(
157
                    "-d", "--deleted",
158
                    dest="deleted",
159
                    action="store_true",
160
                    help="Display only deleted items"),
161
            )
162
        super(ListCommand, self).__init__(*args, **kwargs)
163

    
164
    def handle(self, *args, **options):
165
        if len(args) > 0:
166
            raise CommandError("List commands do not accept any argument")
167

    
168
        assert(self.object_class), "object_class variable must be declared"
169

    
170
        if options["list_fields"]:
171
            self.display_fields()
172
            return
173

    
174
        if options["list_filters"]:
175
            self.display_filters()
176
            return
177

    
178
        # --output option
179
        if options["fields"]:
180
            fields = options["fields"]
181
            fields = fields.split(",")
182
            self.validate_fields(fields)
183
            self.fields = options["fields"].split(",")
184

    
185
        # --filter-by option
186
        if options["filter_by"]:
187
            filters, excludes = util.parse_filters(options["filter_by"])
188
        else:
189
            filters, excludes = ({}, {})
190

    
191
        self.filters.update(filters)
192
        self.excludes.update(excludes)
193

    
194
        # --user option
195
        user = options.get("user")
196
        if user:
197
            if "@" in user:
198
                user = UserCache().get_uuid(user)
199
            self.filters[self.user_uuid_field] = user
200

    
201
        # --deleted option
202
        if self.deleted_field:
203
            deleted = options.get("deleted")
204
            if deleted:
205
                self.filters[self.deleted_field] = True
206
            else:
207
                self.filters[self.deleted_field] = False
208

    
209
        # Special handling of arguments
210
        self.handle_args(self, *args, **options)
211

    
212
        objects = self.object_class.objects
213
        try:
214
            objects = objects.filter(**self.filters)
215
            objects = objects.exclude(**self.excludes)
216
        except FieldError as e:
217
            raise CommandError(e)
218
        except Exception as e:
219
            raise CommandError("Can not filter results: %s" % e)
220

    
221
        # --display-mails option
222
        display_mails = options.get("display_mails")
223
        if display_mails:
224
            if 'user_mail' in self.object_class._meta.get_all_field_names():
225
                raise RuntimeError("%s has already a 'user_mail' attribute")
226

    
227
            self.fields.append("user.email")
228
            self.FIELDS["user.email"] =\
229
                ("user_email", "The email of the owner.")
230
            uuids = [getattr(obj, self.user_uuid_field) for obj in objects]
231
            ucache = UserCache()
232
            ucache.fetch_names(list(set(uuids)))
233
            for obj in objects:
234
                uuid = getattr(obj, self.user_uuid_field)
235
                obj.user_email = ucache.get_name(uuid)
236

    
237
        # Special handling of DB results
238
        self.handle_db_objects(objects)
239

    
240
        headers = self.fields
241
        columns = [self.FIELDS[key][0] for key in headers]
242

    
243
        table = []
244
        for obj in objects:
245
            row = []
246
            for attr in columns:
247
                if callable(attr):
248
                    row.append(attr(obj))
249
                else:
250
                    item = obj
251
                    attrs = attr.split(".")
252
                    for attr in attrs:
253
                        item = getattr(item, attr)
254
                    row.append(item)
255
            table.append(row)
256

    
257
        # Special handle of output
258
        self.handle_output(table, headers)
259

    
260
        # Print output
261
        output_format = options["output_format"]
262
        headers = headers if options["headers"] else None
263
        util.pprint_table(self.stdout, table, headers, output_format)
264

    
265
    def handle_args(self, *args, **kwargs):
266
        pass
267

    
268
    def handle_db_objects(self, objects):
269
        pass
270

    
271
    def handle_output(self, table, headers):
272
        pass
273

    
274
    def display_fields(self):
275
        headers = ["Field", "Description"]
276
        table = []
277
        for field, (_, help_msg) in self.FIELDS.items():
278
            table.append((field, help_msg))
279
        util.pprint_table(self.stdout, table, headers)
280

    
281
    def validate_fields(self, fields):
282
        for f in fields:
283
            if f not in self.FIELDS.keys():
284
                raise CommandError("Unknown field '%s'. 'Use --list-fields"
285
                                   " option to find out available fields."
286
                                   % f)
287

    
288
    def display_filters(self):
289
        headers = ["Filter", "Description", "Help"]
290
        table = []
291
        for field in self.object_class._meta.fields:
292
            table.append((field.name, field.verbose_name, field.help_text))
293
        util.pprint_table(self.stdout, table, headers)