blob: f1b509163ecf6b63ae80d8a38d2f548a1cdb91ec [file] [log] [blame]
Dale Curtis8adf7892011-09-08 16:13:36 -07001import email, os, re, smtplib
mbligh2c6e6932006-10-07 22:08:47 +00002
mblighb62f7242009-07-29 14:34:30 +00003from autotest_lib.server import frontend
mbligh2c6e6932006-10-07 22:08:47 +00004
mblighb62f7242009-07-29 14:34:30 +00005class trigger(object):
6 """
7 Base trigger class. You are allowed to derive from it and
8 override functions to suit your needs in the configuration file.
9 """
jadmanski0afbb632008-06-06 21:10:57 +000010 def __init__(self):
11 self.__actions = []
mbligh31a85202007-09-26 17:18:45 +000012
mbligh2c6e6932006-10-07 22:08:47 +000013
mblighb62f7242009-07-29 14:34:30 +000014 def run(self, files):
jadmanski0afbb632008-06-06 21:10:57 +000015 # Call each of the actions and pass in the kernel list
16 for action in self.__actions:
mblighb62f7242009-07-29 14:34:30 +000017 action(files)
mbligh31a85202007-09-26 17:18:45 +000018
19
jadmanski0afbb632008-06-06 21:10:57 +000020 def add_action(self, func):
21 self.__actions.append(func)
mblighb62f7242009-07-29 14:34:30 +000022
23
24class base_action(object):
25 """
26 Base class for functor actions. Since actions can also be simple functions
27 all action classes need to override __call__ to be callable.
28 """
29 def __call__(self, kernel_list):
30 """
31 Perform the action for that given kernel filenames list.
32
33 @param kernel_list: a sequence of kernel filenames (strings)
34 """
Dale Curtis8adf7892011-09-08 16:13:36 -070035 raise NotImplementedError('__call__ not implemented')
mblighb62f7242009-07-29 14:34:30 +000036
37
38class map_action(base_action):
39 """
40 Action that uses a map between machines and their associated control
41 files and kernel configuration files and it schedules them using
42 the AFE.
43 """
44
mblighb62f7242009-07-29 14:34:30 +000045 _encode_sep = re.compile('(\D+)')
46
47 class machine_info(object):
48 """
49 Class to organize the machine associated information for this action.
50 """
mblighb8f5b702009-11-06 03:09:03 +000051 def __init__(self, tests, kernel_configs):
mblighb62f7242009-07-29 14:34:30 +000052 """
53 Instantiate a machine_info object.
54
mblighb8f5b702009-11-06 03:09:03 +000055 @param tests: a sequence of test names (as named in the frontend
56 database) to run for this host
mblighb62f7242009-07-29 14:34:30 +000057 @param kernel_configs: a dictionary of
58 kernel_version -> config_filename associating kernel
59 versions with corresponding kernel configuration files
60 ("~" inside the filename will be expanded)
61 """
mblighb8f5b702009-11-06 03:09:03 +000062 self.tests = tests
mblighb62f7242009-07-29 14:34:30 +000063 self.kernel_configs = kernel_configs
64
65
mblighfdec32c2009-11-21 01:44:17 +000066 def __init__(self, tests_map, jobname_pattern, job_owner='autotest',
67 upload_kernel_config=False):
mblighb62f7242009-07-29 14:34:30 +000068 """
69 Instantiate a map_action.
70
mblighb8f5b702009-11-06 03:09:03 +000071 @param tests_map: a dictionary of hostname -> machine_info
mblighfdec32c2009-11-21 01:44:17 +000072 @param jobname_pattern: a string pattern used to make the job name
73 containing a single "%s" that will be replaced with the kernel
74 version
75 @param job_owner: the user used to talk with the RPC server
76 @param upload_kernel_config: specify if the generate control file
77 should contain code that downloads and sends to the client the
78 kernel config file (in case it is an URL); this requires that
79 the tests_map refers only to server side tests
mblighb62f7242009-07-29 14:34:30 +000080 """
mblighb8f5b702009-11-06 03:09:03 +000081 self._tests_map = tests_map
mblighb62f7242009-07-29 14:34:30 +000082 self._jobname_pattern = jobname_pattern
mblighb8f5b702009-11-06 03:09:03 +000083 self._afe = frontend.AFE(user=job_owner)
mblighfdec32c2009-11-21 01:44:17 +000084 self._upload_kernel_config = upload_kernel_config
mblighb62f7242009-07-29 14:34:30 +000085
86
87 def __call__(self, kernel_list):
88 """
89 Schedule jobs to run on the given list of kernel versions using
mblighb8f5b702009-11-06 03:09:03 +000090 the configured machines -> machine_info mapping for test name
mblighb62f7242009-07-29 14:34:30 +000091 selection and kernel config file selection.
92 """
93 for kernel in kernel_list:
94 # Get a list of all the machines available for testing
95 # and the tests that each one is to execute and group them by
mblighb8f5b702009-11-06 03:09:03 +000096 # test/kernel-config so we can run a single job for the same
mblighb62f7242009-07-29 14:34:30 +000097 # group
98
mblighb8f5b702009-11-06 03:09:03 +000099 # dictionary of (test-name,kernel-config)-><list-of-machines>
mblighb62f7242009-07-29 14:34:30 +0000100 jobs = {}
mblighb8f5b702009-11-06 03:09:03 +0000101 for machine, info in self._tests_map.iteritems():
mblighb62f7242009-07-29 14:34:30 +0000102 config_paths = info.kernel_configs
103 kernel_config = '/boot/config'
104
105 if config_paths:
106 kvers = config_paths.keys()
107 close = self._closest_kver_leq(kvers, kernel)
108 kernel_config = config_paths[close]
109
mblighb8f5b702009-11-06 03:09:03 +0000110 for test in info.tests:
111 jobs.setdefault((test, kernel_config), [])
112 jobs[(test, kernel_config)].append(machine)
mblighb62f7242009-07-29 14:34:30 +0000113
mblighb8f5b702009-11-06 03:09:03 +0000114 for (test, kernel_config), hosts in jobs.iteritems():
115 c = self._generate_control(test, kernel, kernel_config)
116 self._schedule_job(self._jobname_pattern % kernel, c, hosts)
mblighb62f7242009-07-29 14:34:30 +0000117
118
119 @classmethod
120 def _kver_encode(cls, version):
121 """
122 Encode the various kernel version strings (ex 2.6.20, 2.6.21-rc1,
123 2.7.30-rc2-git3, etc) in a way that makes them easily comparable using
124 lexicographic ordering.
125
126 @param version: kernel version string to encode
127
128 @return processed kernel version string that can be compared using
129 lexicographic comparison
130 """
131 # if it's not a "rc" release, add a -rc99 so it orders at the end of
132 # all other rc releases for the same base kernel version
133 if 'rc' not in version:
134 version += '-rc99'
135
136 # if it's not a git snapshot add a -git99 so it orders at the end of
137 # all other git snapshots for the same base kernel version
138 if 'git' not in version:
139 version += '-git99'
140
141 # make all number sequences to be at least 2 in size (with a leading 0
142 # if necessary)
143 bits = cls._encode_sep.split(version)
144 for n in range(0, len(bits), 2):
145 if len(bits[n]) < 2:
146 bits[n] = '0' + bits[n]
147 return ''.join(bits)
148
149
150 @classmethod
151 def _kver_cmp(cls, a, b):
152 """
153 Compare 2 kernel versions.
154
155 @param a, b: kernel version strings to compare
156
157 @return True if 'a' is less than 'b' or False otherwise
158 """
159 a, b = cls._kver_encode(a), cls._kver_encode(b)
160 return cmp(a, b)
161
162
163 @classmethod
164 def _closest_kver_leq(cls, klist, kver):
165 """
166 Return the closest kernel ver in the list that is <= kver unless
167 kver is the lowest, in which case return the lowest in klist.
168 """
169 if kver in klist:
170 return kver
171 l = list(klist)
172 l.append(kver)
173 l.sort(cmp=cls._kver_cmp)
174 i = l.index(kver)
175 if i == 0:
176 return l[1]
177 return l[i - 1]
178
179
mblighb8f5b702009-11-06 03:09:03 +0000180 def _generate_control(self, test, kernel, kernel_config):
mblighb62f7242009-07-29 14:34:30 +0000181 """
mblighb8f5b702009-11-06 03:09:03 +0000182 Uses generate_control_file RPC to generate a control file given
183 a test name and kernel information.
mblighb62f7242009-07-29 14:34:30 +0000184
mblighb8f5b702009-11-06 03:09:03 +0000185 @param test: The test name string as it's named in the frontend
186 database.
mblighb62f7242009-07-29 14:34:30 +0000187 @param kernel: A str of the kernel version (i.e. x.x.xx)
188 @param kernel_config: A str filename to the kernel config on the
189 client
190
mblighb8f5b702009-11-06 03:09:03 +0000191 @returns a dict representing a control file as described by
192 frontend.afe.rpc_interface.generate_control_file
mblighb62f7242009-07-29 14:34:30 +0000193 """
mblighb8f5b702009-11-06 03:09:03 +0000194 kernel_info = dict(version=kernel,
195 config_file=os.path.expanduser(kernel_config))
mblighfdec32c2009-11-21 01:44:17 +0000196 return self._afe.generate_control_file(
197 tests=[test], kernel=[kernel_info],
198 upload_kernel_config=self._upload_kernel_config)
mblighb62f7242009-07-29 14:34:30 +0000199
200
mblighb8f5b702009-11-06 03:09:03 +0000201 def _schedule_job(self, jobname, control, hosts):
mblighf0c5bbf2009-11-21 01:44:59 +0000202 control_type = ('Client', 'Server')[control.is_server]
mblighb62f7242009-07-29 14:34:30 +0000203
mblighf0c5bbf2009-11-21 01:44:59 +0000204 self._afe.create_job(control.control_file, jobname,
mblighb8f5b702009-11-06 03:09:03 +0000205 control_type=control_type, hosts=hosts)
mblighb62f7242009-07-29 14:34:30 +0000206
207
208class email_action(base_action):
209 """
210 An action object to send emails about found new kernel versions.
211 """
212 _MAIL = 'sendmail'
213
214 def __init__(self, dest_addr, from_addr='autotest-server@localhost'):
215 """
216 Create an email_action instance.
217
218 @param dest_addr: a string or a list of strings with the destination
219 email address(es)
220 @param from_addr: optional source email address for the sent mails
221 (default 'autotest-server@localhost')
222 """
223 # if passed a string for the dest_addr convert it to a tuple
224 if type(dest_addr) is str:
225 self._dest_addr = (dest_addr,)
226 else:
227 self._dest_addr = dest_addr
228
229 self._from_addr = from_addr
230
231
232 def __call__(self, kernel_list):
233 if not kernel_list:
234 return
235
236 message = '\n'.join(kernel_list)
237 message = 'Testing new kernel releases:\n%s' % message
238
239 self._mail('autotest new kernel notification', message)
240
241
242 def _mail(self, subject, message_text):
243 message = email.Message.Message()
244 message['To'] = ', '.join(self._dest_addr)
245 message['From'] = self._from_addr
246 message['Subject'] = subject
247 message.set_payload(message_text)
248
249 if self._sendmail(message.as_string()):
250 server = smtplib.SMTP('localhost')
251 try:
252 server.sendmail(self._from_addr, self._dest_addr,
253 message.as_string())
254 finally:
255 server.quit()
256
257
258 @classmethod
259 def _sendmail(cls, message):
260 """
261 Send an email using the sendmail command.
262 """
263 # open a pipe to the mail program and
264 # write the data to the pipe
265 p = os.popen('%s -t' % cls._MAIL, 'w')
266 p.write(message)
267 return p.close()