Dale Curtis | 8adf789 | 2011-09-08 16:13:36 -0700 | [diff] [blame] | 1 | import email, os, re, smtplib |
mbligh | 2c6e693 | 2006-10-07 22:08:47 +0000 | [diff] [blame] | 2 | |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 3 | from autotest_lib.server import frontend |
Aviv Keshet | 3dd8beb | 2013-05-13 17:36:04 -0700 | [diff] [blame] | 4 | from autotest_lib.client.common_lib import control_data |
mbligh | 2c6e693 | 2006-10-07 22:08:47 +0000 | [diff] [blame] | 5 | |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 6 | class 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 | """ |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 11 | def __init__(self): |
| 12 | self.__actions = [] |
mbligh | 31a8520 | 2007-09-26 17:18:45 +0000 | [diff] [blame] | 13 | |
mbligh | 2c6e693 | 2006-10-07 22:08:47 +0000 | [diff] [blame] | 14 | |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 15 | def run(self, files): |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 16 | # Call each of the actions and pass in the kernel list |
| 17 | for action in self.__actions: |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 18 | action(files) |
mbligh | 31a8520 | 2007-09-26 17:18:45 +0000 | [diff] [blame] | 19 | |
| 20 | |
jadmanski | 0afbb63 | 2008-06-06 21:10:57 +0000 | [diff] [blame] | 21 | def add_action(self, func): |
| 22 | self.__actions.append(func) |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 23 | |
| 24 | |
| 25 | class 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 Curtis | 8adf789 | 2011-09-08 16:13:36 -0700 | [diff] [blame] | 36 | raise NotImplementedError('__call__ not implemented') |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 37 | |
| 38 | |
| 39 | class 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 | |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 46 | _encode_sep = re.compile('(\D+)') |
| 47 | |
| 48 | class machine_info(object): |
| 49 | """ |
| 50 | Class to organize the machine associated information for this action. |
| 51 | """ |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 52 | def __init__(self, tests, kernel_configs): |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 53 | """ |
| 54 | Instantiate a machine_info object. |
| 55 | |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 56 | @param tests: a sequence of test names (as named in the frontend |
| 57 | database) to run for this host |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 58 | @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 | """ |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 63 | self.tests = tests |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 64 | self.kernel_configs = kernel_configs |
| 65 | |
| 66 | |
mbligh | fdec32c | 2009-11-21 01:44:17 +0000 | [diff] [blame] | 67 | def __init__(self, tests_map, jobname_pattern, job_owner='autotest', |
| 68 | upload_kernel_config=False): |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 69 | """ |
| 70 | Instantiate a map_action. |
| 71 | |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 72 | @param tests_map: a dictionary of hostname -> machine_info |
mbligh | fdec32c | 2009-11-21 01:44:17 +0000 | [diff] [blame] | 73 | @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 |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 81 | """ |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 82 | self._tests_map = tests_map |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 83 | self._jobname_pattern = jobname_pattern |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 84 | self._afe = frontend.AFE(user=job_owner) |
mbligh | fdec32c | 2009-11-21 01:44:17 +0000 | [diff] [blame] | 85 | self._upload_kernel_config = upload_kernel_config |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 86 | |
| 87 | |
| 88 | def __call__(self, kernel_list): |
| 89 | """ |
| 90 | Schedule jobs to run on the given list of kernel versions using |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 91 | the configured machines -> machine_info mapping for test name |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 92 | 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 |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 97 | # test/kernel-config so we can run a single job for the same |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 98 | # group |
| 99 | |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 100 | # dictionary of (test-name,kernel-config)-><list-of-machines> |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 101 | jobs = {} |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 102 | for machine, info in self._tests_map.iteritems(): |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 103 | 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 | |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 111 | for test in info.tests: |
| 112 | jobs.setdefault((test, kernel_config), []) |
| 113 | jobs[(test, kernel_config)].append(machine) |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 114 | |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 115 | 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) |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 118 | |
| 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 | |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 181 | def _generate_control(self, test, kernel, kernel_config): |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 182 | """ |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 183 | Uses generate_control_file RPC to generate a control file given |
| 184 | a test name and kernel information. |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 185 | |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 186 | @param test: The test name string as it's named in the frontend |
| 187 | database. |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 188 | @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 | |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 192 | @returns a dict representing a control file as described by |
| 193 | frontend.afe.rpc_interface.generate_control_file |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 194 | """ |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 195 | kernel_info = dict(version=kernel, |
| 196 | config_file=os.path.expanduser(kernel_config)) |
mbligh | fdec32c | 2009-11-21 01:44:17 +0000 | [diff] [blame] | 197 | return self._afe.generate_control_file( |
| 198 | tests=[test], kernel=[kernel_info], |
| 199 | upload_kernel_config=self._upload_kernel_config) |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 200 | |
| 201 | |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 202 | def _schedule_job(self, jobname, control, hosts): |
Aviv Keshet | 3dd8beb | 2013-05-13 17:36:04 -0700 | [diff] [blame] | 203 | if control.is_server: |
| 204 | control_type = control_data.CONTROL_TYPE_NAMES.SERVER |
| 205 | else: |
| 206 | control_type = control_data.CONTROL_TYPE_NAMES.CLIENT |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 207 | |
mbligh | f0c5bbf | 2009-11-21 01:44:59 +0000 | [diff] [blame] | 208 | self._afe.create_job(control.control_file, jobname, |
mbligh | b8f5b70 | 2009-11-06 03:09:03 +0000 | [diff] [blame] | 209 | control_type=control_type, hosts=hosts) |
mbligh | b62f724 | 2009-07-29 14:34:30 +0000 | [diff] [blame] | 210 | |
| 211 | |
| 212 | class 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() |