blob: 7fe9385a42a9d0fd1459647a8d52be5f0cd824f0 [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
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07004from autotest_lib.client.common_lib import control_data
mbligh2c6e6932006-10-07 22:08:47 +00005
mblighb62f7242009-07-29 14:34:30 +00006class trigger(object):
7 """
8 Base trigger class. You are allowed to derive from it and
9 override functions to suit your needs in the configuration file.
10 """
jadmanski0afbb632008-06-06 21:10:57 +000011 def __init__(self):
12 self.__actions = []
mbligh31a85202007-09-26 17:18:45 +000013
mbligh2c6e6932006-10-07 22:08:47 +000014
mblighb62f7242009-07-29 14:34:30 +000015 def run(self, files):
jadmanski0afbb632008-06-06 21:10:57 +000016 # Call each of the actions and pass in the kernel list
17 for action in self.__actions:
mblighb62f7242009-07-29 14:34:30 +000018 action(files)
mbligh31a85202007-09-26 17:18:45 +000019
20
jadmanski0afbb632008-06-06 21:10:57 +000021 def add_action(self, func):
22 self.__actions.append(func)
mblighb62f7242009-07-29 14:34:30 +000023
24
25class base_action(object):
26 """
27 Base class for functor actions. Since actions can also be simple functions
28 all action classes need to override __call__ to be callable.
29 """
30 def __call__(self, kernel_list):
31 """
32 Perform the action for that given kernel filenames list.
33
34 @param kernel_list: a sequence of kernel filenames (strings)
35 """
Dale Curtis8adf7892011-09-08 16:13:36 -070036 raise NotImplementedError('__call__ not implemented')
mblighb62f7242009-07-29 14:34:30 +000037
38
39class map_action(base_action):
40 """
41 Action that uses a map between machines and their associated control
42 files and kernel configuration files and it schedules them using
43 the AFE.
44 """
45
mblighb62f7242009-07-29 14:34:30 +000046 _encode_sep = re.compile('(\D+)')
47
48 class machine_info(object):
49 """
50 Class to organize the machine associated information for this action.
51 """
mblighb8f5b702009-11-06 03:09:03 +000052 def __init__(self, tests, kernel_configs):
mblighb62f7242009-07-29 14:34:30 +000053 """
54 Instantiate a machine_info object.
55
mblighb8f5b702009-11-06 03:09:03 +000056 @param tests: a sequence of test names (as named in the frontend
57 database) to run for this host
mblighb62f7242009-07-29 14:34:30 +000058 @param kernel_configs: a dictionary of
59 kernel_version -> config_filename associating kernel
60 versions with corresponding kernel configuration files
61 ("~" inside the filename will be expanded)
62 """
mblighb8f5b702009-11-06 03:09:03 +000063 self.tests = tests
mblighb62f7242009-07-29 14:34:30 +000064 self.kernel_configs = kernel_configs
65
66
mblighfdec32c2009-11-21 01:44:17 +000067 def __init__(self, tests_map, jobname_pattern, job_owner='autotest',
68 upload_kernel_config=False):
mblighb62f7242009-07-29 14:34:30 +000069 """
70 Instantiate a map_action.
71
mblighb8f5b702009-11-06 03:09:03 +000072 @param tests_map: a dictionary of hostname -> machine_info
mblighfdec32c2009-11-21 01:44:17 +000073 @param jobname_pattern: a string pattern used to make the job name
74 containing a single "%s" that will be replaced with the kernel
75 version
76 @param job_owner: the user used to talk with the RPC server
77 @param upload_kernel_config: specify if the generate control file
78 should contain code that downloads and sends to the client the
79 kernel config file (in case it is an URL); this requires that
80 the tests_map refers only to server side tests
mblighb62f7242009-07-29 14:34:30 +000081 """
mblighb8f5b702009-11-06 03:09:03 +000082 self._tests_map = tests_map
mblighb62f7242009-07-29 14:34:30 +000083 self._jobname_pattern = jobname_pattern
mblighb8f5b702009-11-06 03:09:03 +000084 self._afe = frontend.AFE(user=job_owner)
mblighfdec32c2009-11-21 01:44:17 +000085 self._upload_kernel_config = upload_kernel_config
mblighb62f7242009-07-29 14:34:30 +000086
87
88 def __call__(self, kernel_list):
89 """
90 Schedule jobs to run on the given list of kernel versions using
mblighb8f5b702009-11-06 03:09:03 +000091 the configured machines -> machine_info mapping for test name
mblighb62f7242009-07-29 14:34:30 +000092 selection and kernel config file selection.
93 """
94 for kernel in kernel_list:
95 # Get a list of all the machines available for testing
96 # and the tests that each one is to execute and group them by
mblighb8f5b702009-11-06 03:09:03 +000097 # test/kernel-config so we can run a single job for the same
mblighb62f7242009-07-29 14:34:30 +000098 # group
99
mblighb8f5b702009-11-06 03:09:03 +0000100 # dictionary of (test-name,kernel-config)-><list-of-machines>
mblighb62f7242009-07-29 14:34:30 +0000101 jobs = {}
mblighb8f5b702009-11-06 03:09:03 +0000102 for machine, info in self._tests_map.iteritems():
mblighb62f7242009-07-29 14:34:30 +0000103 config_paths = info.kernel_configs
104 kernel_config = '/boot/config'
105
106 if config_paths:
107 kvers = config_paths.keys()
108 close = self._closest_kver_leq(kvers, kernel)
109 kernel_config = config_paths[close]
110
mblighb8f5b702009-11-06 03:09:03 +0000111 for test in info.tests:
112 jobs.setdefault((test, kernel_config), [])
113 jobs[(test, kernel_config)].append(machine)
mblighb62f7242009-07-29 14:34:30 +0000114
mblighb8f5b702009-11-06 03:09:03 +0000115 for (test, kernel_config), hosts in jobs.iteritems():
116 c = self._generate_control(test, kernel, kernel_config)
117 self._schedule_job(self._jobname_pattern % kernel, c, hosts)
mblighb62f7242009-07-29 14:34:30 +0000118
119
120 @classmethod
121 def _kver_encode(cls, version):
122 """
123 Encode the various kernel version strings (ex 2.6.20, 2.6.21-rc1,
124 2.7.30-rc2-git3, etc) in a way that makes them easily comparable using
125 lexicographic ordering.
126
127 @param version: kernel version string to encode
128
129 @return processed kernel version string that can be compared using
130 lexicographic comparison
131 """
132 # if it's not a "rc" release, add a -rc99 so it orders at the end of
133 # all other rc releases for the same base kernel version
134 if 'rc' not in version:
135 version += '-rc99'
136
137 # if it's not a git snapshot add a -git99 so it orders at the end of
138 # all other git snapshots for the same base kernel version
139 if 'git' not in version:
140 version += '-git99'
141
142 # make all number sequences to be at least 2 in size (with a leading 0
143 # if necessary)
144 bits = cls._encode_sep.split(version)
145 for n in range(0, len(bits), 2):
146 if len(bits[n]) < 2:
147 bits[n] = '0' + bits[n]
148 return ''.join(bits)
149
150
151 @classmethod
152 def _kver_cmp(cls, a, b):
153 """
154 Compare 2 kernel versions.
155
156 @param a, b: kernel version strings to compare
157
158 @return True if 'a' is less than 'b' or False otherwise
159 """
160 a, b = cls._kver_encode(a), cls._kver_encode(b)
161 return cmp(a, b)
162
163
164 @classmethod
165 def _closest_kver_leq(cls, klist, kver):
166 """
167 Return the closest kernel ver in the list that is <= kver unless
168 kver is the lowest, in which case return the lowest in klist.
169 """
170 if kver in klist:
171 return kver
172 l = list(klist)
173 l.append(kver)
174 l.sort(cmp=cls._kver_cmp)
175 i = l.index(kver)
176 if i == 0:
177 return l[1]
178 return l[i - 1]
179
180
mblighb8f5b702009-11-06 03:09:03 +0000181 def _generate_control(self, test, kernel, kernel_config):
mblighb62f7242009-07-29 14:34:30 +0000182 """
mblighb8f5b702009-11-06 03:09:03 +0000183 Uses generate_control_file RPC to generate a control file given
184 a test name and kernel information.
mblighb62f7242009-07-29 14:34:30 +0000185
mblighb8f5b702009-11-06 03:09:03 +0000186 @param test: The test name string as it's named in the frontend
187 database.
mblighb62f7242009-07-29 14:34:30 +0000188 @param kernel: A str of the kernel version (i.e. x.x.xx)
189 @param kernel_config: A str filename to the kernel config on the
190 client
191
mblighb8f5b702009-11-06 03:09:03 +0000192 @returns a dict representing a control file as described by
193 frontend.afe.rpc_interface.generate_control_file
mblighb62f7242009-07-29 14:34:30 +0000194 """
mblighb8f5b702009-11-06 03:09:03 +0000195 kernel_info = dict(version=kernel,
196 config_file=os.path.expanduser(kernel_config))
mblighfdec32c2009-11-21 01:44:17 +0000197 return self._afe.generate_control_file(
198 tests=[test], kernel=[kernel_info],
199 upload_kernel_config=self._upload_kernel_config)
mblighb62f7242009-07-29 14:34:30 +0000200
201
mblighb8f5b702009-11-06 03:09:03 +0000202 def _schedule_job(self, jobname, control, hosts):
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700203 if control.is_server:
204 control_type = control_data.CONTROL_TYPE_NAMES.SERVER
205 else:
206 control_type = control_data.CONTROL_TYPE_NAMES.CLIENT
mblighb62f7242009-07-29 14:34:30 +0000207
mblighf0c5bbf2009-11-21 01:44:59 +0000208 self._afe.create_job(control.control_file, jobname,
mblighb8f5b702009-11-06 03:09:03 +0000209 control_type=control_type, hosts=hosts)
mblighb62f7242009-07-29 14:34:30 +0000210
211
212class email_action(base_action):
213 """
214 An action object to send emails about found new kernel versions.
215 """
216 _MAIL = 'sendmail'
217
218 def __init__(self, dest_addr, from_addr='autotest-server@localhost'):
219 """
220 Create an email_action instance.
221
222 @param dest_addr: a string or a list of strings with the destination
223 email address(es)
224 @param from_addr: optional source email address for the sent mails
225 (default 'autotest-server@localhost')
226 """
227 # if passed a string for the dest_addr convert it to a tuple
228 if type(dest_addr) is str:
229 self._dest_addr = (dest_addr,)
230 else:
231 self._dest_addr = dest_addr
232
233 self._from_addr = from_addr
234
235
236 def __call__(self, kernel_list):
237 if not kernel_list:
238 return
239
240 message = '\n'.join(kernel_list)
241 message = 'Testing new kernel releases:\n%s' % message
242
243 self._mail('autotest new kernel notification', message)
244
245
246 def _mail(self, subject, message_text):
247 message = email.Message.Message()
248 message['To'] = ', '.join(self._dest_addr)
249 message['From'] = self._from_addr
250 message['Subject'] = subject
251 message.set_payload(message_text)
252
253 if self._sendmail(message.as_string()):
254 server = smtplib.SMTP('localhost')
255 try:
256 server.sendmail(self._from_addr, self._dest_addr,
257 message.as_string())
258 finally:
259 server.quit()
260
261
262 @classmethod
263 def _sendmail(cls, message):
264 """
265 Send an email using the sendmail command.
266 """
267 # open a pipe to the mail program and
268 # write the data to the pipe
269 p = os.popen('%s -t' % cls._MAIL, 'w')
270 p.write(message)
271 return p.close()