1# pylint: disable-msg=C0111
2#
3# Copyright 2008 Google Inc. All Rights Reserved.
4
5"""This module contains the common behavior of some actions
6
7Operations on ACLs or labels are very similar, so are creations and
8deletions. The following classes provide the common handling.
9
10In these case, the class inheritance is, taking the command
11'atest label create' as an example:
12
13                  atest
14                 /     \
15                /       \
16               /         \
17         atest_create   label
18               \         /
19                \       /
20                 \     /
21               label_create
22
23
24For 'atest label add':
25
26                  atest
27                 /     \
28                /       \
29               /         \
30               |       label
31               |         |
32               |         |
33               |         |
34         atest_add   label_add_or_remove
35               \         /
36                \       /
37                 \     /
38               label_add
39
40
41
42"""
43
44import types
45from autotest_lib.cli import topic_common
46
47
48#
49# List action
50#
51class atest_list(topic_common.atest):
52    """atest <topic> list"""
53    usage_action = 'list'
54
55
56    def _convert_wildcard(self, old_key, new_key,
57                          value, filters, check_results):
58        filters[new_key] = value.rstrip('*')
59        check_results[new_key] = None
60        del filters[old_key]
61        del check_results[old_key]
62
63
64    def _convert_name_wildcard(self, key, value, filters, check_results):
65        if value.endswith('*'):
66            # Could be __name, __login, __hostname
67            new_key = key + '__startswith'
68            self._convert_wildcard(key, new_key, value, filters, check_results)
69
70
71    def _convert_in_wildcard(self, key, value, filters, check_results):
72        if value.endswith('*'):
73            assert key.endswith('__in'), 'Key %s does not end with __in' % key
74            new_key = key.replace('__in', '__startswith', 1)
75            self._convert_wildcard(key, new_key, value, filters, check_results)
76
77
78    def check_for_wildcard(self, filters, check_results):
79        """Check if there is a wilcard (only * for the moment)
80        and replace the request appropriately"""
81        for (key, values) in filters.iteritems():
82            if isinstance(values, types.StringTypes):
83                self._convert_name_wildcard(key, values,
84                                            filters, check_results)
85                continue
86
87            if isinstance(values, types.ListType):
88                if len(values) == 1:
89                    self._convert_in_wildcard(key, values[0],
90                                              filters, check_results)
91                    continue
92
93                for value in values:
94                    if value.endswith('*'):
95                        # Can only be a wildcard if it is by itelf
96                        self.invalid_syntax('Cannot mix wilcards and items')
97
98
99    def execute(self, op, filters={}, check_results={}):
100        """Generic list execute:
101        If no filters where specified, list all the items.  If
102        some specific items where asked for, filter on those:
103        check_results has the same keys than filters.  If only
104        one filter is set, we use the key from check_result to
105        print the error"""
106        self.check_for_wildcard(filters, check_results)
107
108        results = self.execute_rpc(op, **filters)
109
110        for dbkey in filters.keys():
111            if not check_results.get(dbkey, None):
112                # Don't want to check the results
113                # for this key
114                continue
115
116            if len(results) >= len(filters[dbkey]):
117                continue
118
119            # Some bad items
120            field = check_results[dbkey]
121            # The filtering for the job is on the ID which is an int.
122            # Convert it as the jobids from the CLI args are strings.
123            good = set(str(result[field]) for result in results)
124            self.invalid_arg('Unknown %s(s): \n' % self.msg_topic,
125                             ', '.join(set(filters[dbkey]) - good))
126        return results
127
128
129    def output(self, results, keys, sublist_keys=[]):
130        self.print_table(results, keys, sublist_keys)
131
132
133#
134# Creation & Deletion of a topic (ACL, label, user)
135#
136class atest_create_or_delete(topic_common.atest):
137    """atest <topic> [create|delete]
138    To subclass this, you must define:
139                         Example          Comment
140    self.topic           'acl_group'
141    self.op_action       'delete'        Action to remove a 'topic'
142    self.data            {}              Additional args for the topic
143                                         creation/deletion
144    self.msg_topic:      'ACL'           The printable version of the topic.
145    self.msg_done:       'Deleted'       The printable version of the action.
146    """
147    def execute(self):
148        handled = []
149
150        if (self.op_action == 'delete' and not self.no_confirmation and
151            not self.prompt_confirmation()):
152            return
153
154        # Create or Delete the <topic> altogether
155        op = '%s_%s' % (self.op_action, self.topic)
156        for item in self.get_items():
157            try:
158                self.data[self.data_item_key] = item
159                new_id = self.execute_rpc(op, item=item, **self.data)
160                handled.append(item)
161            except topic_common.CliError:
162                pass
163        return handled
164
165
166    def output(self, results):
167        if results:
168            results = ["'%s'" % r for r in results]
169            self.print_wrapped("%s %s" % (self.msg_done, self.msg_topic),
170                               results)
171
172
173class atest_create(atest_create_or_delete):
174    usage_action = 'create'
175    op_action = 'add'
176    msg_done = 'Created'
177
178
179class atest_delete(atest_create_or_delete):
180    data_item_key = 'id'
181    usage_action = op_action = 'delete'
182    msg_done = 'Deleted'
183
184
185#
186# Adding or Removing things (users, hosts or labels) from a topic
187# (ACL, Label or AtomicGroup)
188#
189class atest_add_or_remove(topic_common.atest):
190    """atest <topic> [add|remove]
191    To subclass this, you must define these attributes:
192                       Example             Comment
193    topic              'acl_group'
194    op_action          'remove'            Action for adding users/hosts
195    add_remove_things  {'users': 'user'}   Dict of things to try add/removing.
196                                           Keys are the attribute names.  Values
197                                           are the word to print for an
198                                           individual item of such a value.
199    """
200
201    add_remove_things = {'users': 'user', 'hosts': 'host'}  # Original behavior
202
203
204    def _add_remove_uh_to_topic(self, item, what):
205        """Adds the 'what' (such as users or hosts) to the 'item'"""
206        uhs = getattr(self, what)
207        if len(uhs) == 0:
208            # To skip the try/else
209            raise AttributeError
210        op = '%s_%s_%s' % (self.topic, self.op_action, what)
211        try:
212            self.execute_rpc(op=op,                       # The opcode
213                             **{'id': item, what: uhs})   # The data
214            setattr(self, 'good_%s' % what, uhs)
215        except topic_common.CliError, full_error:
216            bad_uhs = self.parse_json_exception(full_error)
217            good_uhs = list(set(uhs) - set(bad_uhs))
218            if bad_uhs and good_uhs:
219                self.execute_rpc(op=op,
220                                 **{'id': item, what: good_uhs})
221                setattr(self, 'good_%s' % what, good_uhs)
222            else:
223                raise
224
225
226    def execute(self):
227        """Adds or removes things (users, hosts, etc.) from a topic, e.g.:
228
229        Add hosts to labels:
230          self.topic = 'label'
231          self.op_action = 'add'
232          self.add_remove_things = {'users': 'user', 'hosts': 'host'}
233          self.get_items() = The labels/ACLs that the hosts
234                             should be added to.
235
236        Returns:
237          A dictionary of lists of things added successfully using the same
238          keys as self.add_remove_things.
239        """
240        oks = {}
241        for item in self.get_items():
242            # FIXME(gps):
243            # This reverse sorting is only here to avoid breaking many
244            # existing extremely fragile unittests which depend on the
245            # exact order of the calls made below.  'users' must be run
246            # before 'hosts'.
247            plurals = reversed(sorted(self.add_remove_things.keys()))
248            for what in plurals:
249                try:
250                    self._add_remove_uh_to_topic(item, what)
251                except AttributeError:
252                    pass
253                except topic_common.CliError, err:
254                    # The error was already logged by
255                    # self.failure()
256                    pass
257                else:
258                    oks.setdefault(item, []).append(what)
259
260        results = {}
261        for thing in self.add_remove_things:
262            things_ok = [item for item, what in oks.items() if thing in what]
263            results[thing] = things_ok
264
265        return results
266
267
268    def output(self, results):
269        for thing, single_thing in self.add_remove_things.iteritems():
270            # Enclose each of the elements in a single quote.
271            things_ok = ["'%s'" % t for t in results[thing]]
272            if things_ok:
273                self.print_wrapped("%s %s %s %s" % (self.msg_done,
274                                                    self.msg_topic,
275                                                    ', '.join(things_ok),
276                                                    single_thing),
277                                   getattr(self, 'good_%s' % thing))
278
279
280class atest_add(atest_add_or_remove):
281    usage_action = op_action = 'add'
282    msg_done = 'Added to'
283    usage_words = ('Add', 'to')
284
285
286class atest_remove(atest_add_or_remove):
287    usage_action = op_action = 'remove'
288    msg_done = 'Removed from'
289    usage_words = ('Remove', 'from')
290