action_common.py revision fb8f0ab0c168fb477b5eb756d041d06e3491bbde
1#
2# Copyright 2008 Google Inc. All Rights Reserved.
3
4"""This module contains the common behavior of some actions
5
6Operations on ACLs or labels are very similar, so are creations and
7deletions. The following classes provide the common handling.
8
9In these case, the class inheritance is, taking the command
10'atest label create' as an example:
11
12                  atest
13                 /     \
14                /       \
15               /         \
16         atest_create   label
17               \         /
18                \       /
19                 \     /
20               label_create
21
22
23For 'atest label add':
24
25                  atest
26                 /     \
27                /       \
28               /         \
29               |       label
30               |         |
31               |         |
32               |         |
33         atest_add   label_add_or_remove
34               \         /
35                \       /
36                 \     /
37               label_add
38
39
40
41"""
42
43import re, socket, types
44from autotest_lib.cli import topic_common
45
46
47#
48# List action
49#
50class atest_list(topic_common.atest):
51    """atest <topic> list"""
52    usage_action = 'list'
53
54
55    def _convert_wildcard(self, old_key, new_key,
56                          value, filters, check_results):
57        filters[new_key] = value.rstrip('*')
58        check_results[new_key] = None
59        del filters[old_key]
60        del check_results[old_key]
61
62
63    def _convert_name_wildcard(self, key, value, filters, check_results):
64        if value.endswith('*'):
65            # Could be __name, __login, __hostname
66            new_key = key + '__startswith'
67            self._convert_wildcard(key, new_key, value, filters, check_results)
68
69
70    def _convert_in_wildcard(self, key, value, filters, check_results):
71        if value.endswith('*'):
72            assert(key.endswith('__in'))
73            new_key = key.replace('__in', '__startswith', 1)
74            self._convert_wildcard(key, new_key, value, filters, check_results)
75
76
77    def check_for_wildcard(self, filters, check_results):
78        """Check if there is a wilcard (only * for the moment)
79        and replace the request appropriately"""
80        for (key, values) in filters.iteritems():
81            if isinstance(values, types.StringTypes):
82                self._convert_name_wildcard(key, values,
83                                            filters, check_results)
84                continue
85
86            if isinstance(values, types.ListType):
87                if len(values) == 1:
88                    self._convert_in_wildcard(key, values[0],
89                                              filters, check_results)
90                    continue
91
92                for value in values:
93                    if value.endswith('*'):
94                        # Can only be a wildcard if it is by itelf
95                        self.invalid_syntax('Cannot mix wilcards and items')
96
97
98    def execute(self, op, filters={}, check_results={}):
99        """Generic list execute:
100        If no filters where specified, list all the items.  If
101        some specific items where asked for, filter on those:
102        check_results has the same keys than filters.  If only
103        one filter is set, we use the key from check_result to
104        print the error"""
105        self.check_for_wildcard(filters, check_results)
106
107        socket.setdefaulttimeout(topic_common.LIST_SOCKET_TIMEOUT)
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        # Create or Delete the <topic> altogether
151        op = '%s_%s' % (self.op_action, self.topic)
152        for item in self.get_items():
153            try:
154                self.data[self.data_item_key] = item
155                new_id = self.execute_rpc(op, item=item, **self.data)
156                handled.append(item)
157            except topic_common.CliError:
158                pass
159        return handled
160
161
162    def output(self, results):
163        if results:
164            self.print_wrapped ("%s %s" % (self.msg_done, self.msg_topic),
165                                results)
166
167
168class atest_create(atest_create_or_delete):
169    usage_action = 'create'
170    op_action = 'add'
171    msg_done = 'Created'
172
173    def parse_hosts(self, args):
174        """ Parses the arguments to generate a list of hosts and meta_hosts
175        A host is a regular name, a meta_host is n*label or *label.
176        These can be mixed on the CLI, and separated by either commas or
177        spaces, e.g.: 5*Machine_Label host0 5*Machine_Label2,host2 """
178
179        hosts = []
180        meta_hosts = []
181
182        for arg in args:
183            for host in arg.split(','):
184                if re.match('^[0-9]+[*]', host):
185                    num, host = host.split('*', 1)
186                    meta_hosts += int(num) * [host]
187                elif re.match('^[*](\w*)', host):
188                    meta_hosts += [re.match('^[*](\w*)', host).group(1)]
189                elif host != '':
190                    # Real hostname
191                    hosts.append(host)
192
193        return (hosts, meta_hosts)
194
195
196class atest_delete(atest_create_or_delete):
197    data_item_key = 'id'
198    usage_action = op_action = 'delete'
199    msg_done = 'Deleted'
200
201
202#
203# Adding or Removing users or hosts from a topic (ACL or label)
204#
205class atest_add_or_remove(topic_common.atest):
206    """atest <topic> [add|remove]
207    To subclass this, you must define:
208                       Example          Comment
209    self.topic         'acl_group'
210    self.op_action     'remove'         Action for adding users/hosts
211    """
212
213    def _add_remove_uh_to_topic(self, item, what):
214        """Adds the 'what' (users or hosts) to the 'item'"""
215        uhs = getattr(self, what)
216        if len(uhs) == 0:
217            # To skip the try/else
218            raise AttributeError
219        op = '%s_%s_%s' % (self.topic, self.op_action, what)
220        self.execute_rpc(op=op,                                 # The opcode
221                         item='%s (%s)' %(item, ','.join(uhs)), # The error
222                         **{'id': item, what: uhs})             # The data
223
224
225    def execute(self):
226        """Adds or removes users or hosts from a topic, e.g.:
227        add hosts to labels:
228          self.topic = 'label'
229          self.op_action = 'add'
230          self.get_items() = the labels that the hosts
231                             should be added to"""
232        oks = {}
233        for item in self.get_items():
234            for what in ['users', 'hosts']:
235                try:
236                    self._add_remove_uh_to_topic(item, what)
237                except AttributeError:
238                    pass
239                except topic_common.CliError, err:
240                    # The error was already logged by
241                    # self.failure()
242                    pass
243                else:
244                    oks.setdefault(item, []).append(what)
245
246        users_ok = [item for (item, what) in oks.items() if 'users' in what]
247        hosts_ok = [item for (item, what) in oks.items() if 'hosts' in what]
248
249        return (users_ok, hosts_ok)
250
251
252    def output(self, results):
253        (users_ok, hosts_ok) = results
254        if users_ok:
255            self.print_wrapped("%s %s %s user" %
256                               (self.msg_done,
257                                self.msg_topic,
258                                ', '.join(users_ok)),
259                               self.users)
260
261        if hosts_ok:
262            self.print_wrapped("%s %s %s host" %
263                               (self.msg_done,
264                                self.msg_topic,
265                                ', '.join(hosts_ok)),
266                               self.hosts)
267
268
269class atest_add(atest_add_or_remove):
270    usage_action = op_action = 'add'
271    msg_done = 'Added to'
272    usage_words = ('Add', 'to')
273
274
275class atest_remove(atest_add_or_remove):
276    usage_action = op_action = 'remove'
277    msg_done = 'Removed from'
278    usage_words = ('Remove', 'from')
279