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