blob: a723fd3a8d35495f2ed1e24ba08b54619b782af4 [file] [log] [blame]
Don Garrett8db752c2014-10-17 16:56:55 -07001#!/usr/bin/python
2# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Runs on autotest servers from a cron job to self update them.
7
8This script is designed to run on all autotest servers to allow them to
9automatically self-update based on the manifests used to create their (existing)
10repos.
11"""
12
13from __future__ import print_function
14
15import ConfigParser
Don Garrett03432d62014-11-19 18:18:35 -080016import argparse
Don Garrett8db752c2014-10-17 16:56:55 -070017import os
Don Garrett699b4b32014-12-11 13:10:15 -080018import re
Don Garrett8db752c2014-10-17 16:56:55 -070019import subprocess
20import sys
21import time
22
23import common
24
25from autotest_lib.client.common_lib import global_config
26
Don Garrettd0321722014-11-18 16:03:33 -080027# How long after restarting a service do we watch it to see if it's stable.
28SERVICE_STABILITY_TIMER = 60
29
Don Garrett8db752c2014-10-17 16:56:55 -070030
31class DirtyTreeException(Exception):
Don Garrettd0321722014-11-18 16:03:33 -080032 """Raised when the tree has been modified in an unexpected way."""
Don Garrett8db752c2014-10-17 16:56:55 -070033
34
35class UnknownCommandException(Exception):
Don Garrettd0321722014-11-18 16:03:33 -080036 """Raised when we try to run a command name with no associated command."""
Don Garrett8db752c2014-10-17 16:56:55 -070037
38
39class UnstableServices(Exception):
Don Garrettd0321722014-11-18 16:03:33 -080040 """Raised if a service appears unstable after restart."""
Don Garrett8db752c2014-10-17 16:56:55 -070041
42
43def verify_repo_clean():
44 """This function verifies that the current repo is valid, and clean.
45
46 @raises DirtyTreeException if the repo is not clean.
47 @raises subprocess.CalledProcessError on a repo command failure.
48 """
Don Garrett8db752c2014-10-17 16:56:55 -070049 out = subprocess.check_output(['repo', 'status'], stderr=subprocess.STDOUT)
Don Garrett699b4b32014-12-11 13:10:15 -080050 out = out.strip()
51
52 # We're clean, with no branches.
53 CLEAN_STATUS_OUTPUT = 'nothing to commit (working directory clean)'
54 if out == CLEAN_STATUS_OUTPUT:
55 return
56
57 # We're clean, but the branch 'prod' exists in the project autotest.
58 # We use wildcards to skip over the text format characters repo uses.
59 if re.match(r'^.*project autotest/.*branch prod.*$\Z', out):
60 return
61
62 raise DirtyTreeException(out)
Don Garrett8db752c2014-10-17 16:56:55 -070063
Don Garrett8db752c2014-10-17 16:56:55 -070064
65def repo_versions():
66 """This function collects the versions of all git repos in the general repo.
67
Don Garrettfa2c1c42014-12-11 12:11:49 -080068 @returns A dictionary mapping project names to git hashes for HEAD.
Don Garrett8db752c2014-10-17 16:56:55 -070069 @raises subprocess.CalledProcessError on a repo command failure.
70 """
Don Garrettfa2c1c42014-12-11 12:11:49 -080071 cmd = ['repo', 'forall', '-p', '-c', 'pwd && git log -1 --format=%h']
72 output = subprocess.check_output(cmd)
73
74 # The expected output format is:
75
76 # project chrome_build/
77 # /dir/holding/chrome_build
78 # 73dee9d
79 #
80 # project chrome_release/
81 # /dir/holding/chrome_release
82 # 9f3a5d8
83
84 lines = output.splitlines()
85
86 PROJECT_PREFIX = 'project '
87
88 project_heads = {}
89 for n in range(0, len(lines), 4):
90 project_line = lines[n]
91 project_dir = lines[n+1]
92 project_hash = lines[n+2]
93 # lines[n+3] is a blank line, but doesn't exist for the final block.
94
95 # Convert 'project chrome_build/' -> 'chrome_build'
96 assert project_line.startswith(PROJECT_PREFIX)
97 name = project_line[len(PROJECT_PREFIX):].rstrip('/')
98
99 project_heads[name] = (project_dir, project_hash)
100
101 return project_heads
Don Garrett8db752c2014-10-17 16:56:55 -0700102
103
104def repo_sync():
105 """Perform a repo sync.
106
107 @raises subprocess.CalledProcessError on a repo command failure.
108 """
Don Garrettd0321722014-11-18 16:03:33 -0800109 subprocess.check_output(['repo', 'sync'])
Don Garrett8db752c2014-10-17 16:56:55 -0700110
111
Don Garrettd0321722014-11-18 16:03:33 -0800112def discover_update_commands():
113 """Lookup the commands to run on this server.
Don Garrett8db752c2014-10-17 16:56:55 -0700114
Don Garrettd0321722014-11-18 16:03:33 -0800115 These commonly come from shadow_config.ini, since they vary by server type.
Don Garrett8db752c2014-10-17 16:56:55 -0700116
Don Garrettd0321722014-11-18 16:03:33 -0800117 @returns List of command names in string format.
Don Garrett8db752c2014-10-17 16:56:55 -0700118 """
Don Garrett8db752c2014-10-17 16:56:55 -0700119 try:
Don Garrettd0321722014-11-18 16:03:33 -0800120 return global_config.global_config.get_config_value(
Don Garrett8db752c2014-10-17 16:56:55 -0700121 'UPDATE', 'commands', type=list)
122
123 except (ConfigParser.NoSectionError, global_config.ConfigError):
Don Garrettd0321722014-11-18 16:03:33 -0800124 return []
Don Garrett8db752c2014-10-17 16:56:55 -0700125
Don Garrettd0321722014-11-18 16:03:33 -0800126
127def discover_restart_services():
128 """Find the services that need restarting on the current server.
129
130 These commonly come from shadow_config.ini, since they vary by server type.
131
132 @returns List of service names in string format.
133 """
Don Garrett8db752c2014-10-17 16:56:55 -0700134 try:
135 # From shadow_config.ini, lookup which services to restart.
Don Garrettd0321722014-11-18 16:03:33 -0800136 return global_config.global_config.get_config_value(
Don Garrett8db752c2014-10-17 16:56:55 -0700137 'UPDATE', 'services', type=list)
138
139 except (ConfigParser.NoSectionError, global_config.ConfigError):
Don Garrettd0321722014-11-18 16:03:33 -0800140 return []
Don Garrett8db752c2014-10-17 16:56:55 -0700141
Don Garrett8db752c2014-10-17 16:56:55 -0700142
Don Garrett03432d62014-11-19 18:18:35 -0800143def update_command(cmd_tag, dryrun=False):
Don Garrettd0321722014-11-18 16:03:33 -0800144 """Restart a command.
Don Garrett8db752c2014-10-17 16:56:55 -0700145
Don Garrettd0321722014-11-18 16:03:33 -0800146 The command name is looked up in global_config.ini to find the full command
147 to run, then it's executed.
Don Garrett8db752c2014-10-17 16:56:55 -0700148
Don Garrettd0321722014-11-18 16:03:33 -0800149 @param cmd_tag: Which command to restart.
Don Garrett03432d62014-11-19 18:18:35 -0800150 @param dryrun: If true print the command that would have been run.
Don Garrett8db752c2014-10-17 16:56:55 -0700151
Don Garrettd0321722014-11-18 16:03:33 -0800152 @raises UnknownCommandException If cmd_tag can't be looked up.
153 @raises subprocess.CalledProcessError on a command failure.
154 """
155 # Lookup the list of commands to consider. They are intended to be
156 # in global_config.ini so that they can be shared everywhere.
157 cmds = dict(global_config.global_config.config.items(
158 'UPDATE_COMMANDS'))
Don Garrett8db752c2014-10-17 16:56:55 -0700159
Don Garrettd0321722014-11-18 16:03:33 -0800160 if cmd_tag not in cmds:
161 raise UnknownCommandException(cmd_tag, cmds)
Don Garrett8db752c2014-10-17 16:56:55 -0700162
Don Garrettd0321722014-11-18 16:03:33 -0800163 expanded_command = cmds[cmd_tag].replace('AUTOTEST_REPO',
164 common.autotest_dir)
Don Garrett8db752c2014-10-17 16:56:55 -0700165
Don Garrett699b4b32014-12-11 13:10:15 -0800166 print('Running: %s: %s' % (cmd_tag, expanded_command))
Don Garrett03432d62014-11-19 18:18:35 -0800167 if dryrun:
Don Garrett699b4b32014-12-11 13:10:15 -0800168 print('Skip: %s' % expanded_command)
Don Garrett03432d62014-11-19 18:18:35 -0800169 else:
Don Garrett03432d62014-11-19 18:18:35 -0800170 subprocess.check_call(expanded_command, shell=True)
Don Garrett8db752c2014-10-17 16:56:55 -0700171
Don Garrett8db752c2014-10-17 16:56:55 -0700172
Don Garrett03432d62014-11-19 18:18:35 -0800173def restart_service(service_name, dryrun=False):
Don Garrettd0321722014-11-18 16:03:33 -0800174 """Restart a service.
175
176 Restarts the standard service with "service <name> restart".
177
178 @param service_name: The name of the service to restart.
Don Garrett03432d62014-11-19 18:18:35 -0800179 @param dryrun: Don't really run anything, just print out the command.
Don Garrettd0321722014-11-18 16:03:33 -0800180
181 @raises subprocess.CalledProcessError on a command failure.
182 """
Don Garrett03432d62014-11-19 18:18:35 -0800183 cmd = ['sudo', 'service', service_name, 'restart']
Don Garrett699b4b32014-12-11 13:10:15 -0800184 print('Restarting: %s' % service_name)
Don Garrett03432d62014-11-19 18:18:35 -0800185 if dryrun:
Don Garrett699b4b32014-12-11 13:10:15 -0800186 print('Skip: %s' % ' '.join(cmd))
Don Garrett03432d62014-11-19 18:18:35 -0800187 else:
Don Garrett03432d62014-11-19 18:18:35 -0800188 subprocess.check_call(cmd)
Don Garrettd0321722014-11-18 16:03:33 -0800189
190
191def service_status(service_name):
192 """Return the results "status <name>" for a given service.
193
194 This string is expected to contain the pid, and so to change is the service
195 is shutdown or restarted for any reason.
196
197 @param service_name: The name of the service to check on.
Don Garrett03432d62014-11-19 18:18:35 -0800198
Don Garrettd0321722014-11-18 16:03:33 -0800199 @returns The output of the external command.
200 Ex: autofs start/running, process 1931
201
202 @raises subprocess.CalledProcessError on a command failure.
203 """
204 return subprocess.check_output(['sudo', 'status', service_name])
205
206
Don Garrett03432d62014-11-19 18:18:35 -0800207def restart_services(service_names, dryrun=False):
Don Garrettd0321722014-11-18 16:03:33 -0800208 """Restart services as needed for the current server type.
209
210 Restart the listed set of services, and watch to see if they are stable for
211 at least SERVICE_STABILITY_TIMER. It restarts all services quickly,
212 waits for that delay, then verifies the status of all of them.
213
214 @param service_names: The list of service to restart and monitor.
Don Garrett03432d62014-11-19 18:18:35 -0800215 @param dryrun: Don't really restart the service, just print out the command.
Don Garrettd0321722014-11-18 16:03:33 -0800216
217 @raises subprocess.CalledProcessError on a command failure.
Don Garrett03432d62014-11-19 18:18:35 -0800218 @raises UnstableServices if any services are unstable after restart.
Don Garrettd0321722014-11-18 16:03:33 -0800219 """
220 service_statuses = {}
221
Don Garrett03432d62014-11-19 18:18:35 -0800222 if dryrun:
223 for name in service_names:
224 restart_service(name, dryrun=True)
225 return
226
Don Garrettd0321722014-11-18 16:03:33 -0800227 # Restart each, and record the status (including pid).
228 for name in service_names:
229 restart_service(name)
230 service_statuses[name] = service_status(name)
231
232 # Wait for a while to let the services settle.
233 time.sleep(SERVICE_STABILITY_TIMER)
234
235 # Look for any services that changed status.
236 unstable_services = [n for n in service_names
237 if service_status(n) != service_statuses[n]]
238
239 # Report any services having issues.
240 if unstable_services:
241 raise UnstableServices(unstable_services)
Don Garrett8db752c2014-10-17 16:56:55 -0700242
243
Don Garrett03432d62014-11-19 18:18:35 -0800244def run_deploy_actions(dryrun=False):
Don Garrettfa2c1c42014-12-11 12:11:49 -0800245 """Run arbitrary update commands specified in global.ini.
Don Garrett8db752c2014-10-17 16:56:55 -0700246
Don Garrett03432d62014-11-19 18:18:35 -0800247 @param dryrun: Don't really restart the service, just print out the command.
Don Garrett8db752c2014-10-17 16:56:55 -0700248
Don Garrett03432d62014-11-19 18:18:35 -0800249 @raises subprocess.CalledProcessError on a command failure.
250 @raises UnstableServices if any services are unstable after restart.
251 """
Don Garrettd0321722014-11-18 16:03:33 -0800252 cmds = discover_update_commands()
253 if cmds:
254 print('Running update commands:', ', '.join(cmds))
255 for cmd in cmds:
Don Garrett03432d62014-11-19 18:18:35 -0800256 update_command(cmd, dryrun=dryrun)
Don Garrettd0321722014-11-18 16:03:33 -0800257
258 services = discover_restart_services()
259 if services:
Don Garrett03432d62014-11-19 18:18:35 -0800260 print('Restarting Services:', ', '.join(services))
261 restart_services(services, dryrun=dryrun)
262
263
Don Garrettfa2c1c42014-12-11 12:11:49 -0800264def report_changes(versions_before, versions_after):
265 """Produce a report describing what changed in all repos.
266
267 @param versions_before: Results of repo_versions() from before the update.
268 @param versions_after: Results of repo_versions() from after the update.
269
270 @returns string containing a human friendly changes report.
271 """
272 result = []
273
274 for project in sorted(set(versions_before.keys() + versions_after.keys())):
275 result.append('%s:' % project)
276
277 _, before_hash = versions_before.get(project, (None, None))
278 after_dir, after_hash = versions_after.get(project, (None, None))
279
280 if project not in versions_before:
281 result.append('Added.')
282
283 elif project not in versions_after:
284 result.append('Removed.')
285
286 elif before_hash == after_hash:
287 result.append('No Change.')
288
289 else:
290 hashes = '%s..%s' % (before_hash, after_hash)
291 cmd = ['git', 'log', hashes, '--oneline']
292 out = subprocess.check_output(cmd, cwd=after_dir,
293 stderr=subprocess.STDOUT)
294 result.append(out.strip())
295
296 result.append('')
297
298 return '\n'.join(result)
299
300
Don Garrett03432d62014-11-19 18:18:35 -0800301def parse_arguments(args):
302 """Parse command line arguments.
303
304 @param args: The command line arguments to parse. (ususally sys.argsv[1:])
305
Don Garrett40036362014-12-08 15:52:44 -0800306 @returns An argparse.Namespace populated with argument values.
Don Garrett03432d62014-11-19 18:18:35 -0800307 """
308 parser = argparse.ArgumentParser(
309 description='Command to update an autotest server.')
310 parser.add_argument('--skip-verify', action='store_false',
311 dest='verify', default=True,
312 help='Disable verification of a clean repository.')
313 parser.add_argument('--skip-update', action='store_false',
314 dest='update', default=True,
315 help='Skip the repository source code update.')
316 parser.add_argument('--skip-actions', action='store_false',
317 dest='actions', default=True,
318 help='Skip the post update actions.')
319 parser.add_argument('--skip-report', action='store_false',
320 dest='report', default=True,
321 help='Skip the git version report.')
Don Garrette3718912014-12-05 13:11:44 -0800322 parser.add_argument('--actions-only', action='store_true',
323 help='Run the post update actions (restart services).')
Don Garrett03432d62014-11-19 18:18:35 -0800324 parser.add_argument('--dryrun', action='store_true',
325 help='Don\'t actually run any commands, just log.')
326
327 results = parser.parse_args(args)
328
Don Garrette3718912014-12-05 13:11:44 -0800329 if results.actions_only:
330 results.verify = False
331 results.update = False
332 results.report = False
333
Don Garrett03432d62014-11-19 18:18:35 -0800334 # TODO(dgarrett): Make these behaviors support dryrun.
335 if results.dryrun:
336 results.verify = False
337 results.update = False
338
339 return results
340
341
342def main(args):
343 """Main method."""
344 os.chdir(common.autotest_dir)
345 global_config.global_config.parse_config_file()
346
347 behaviors = parse_arguments(args)
348
349 if behaviors.verify:
Don Garrettd0321722014-11-18 16:03:33 -0800350 try:
Don Garrett03432d62014-11-19 18:18:35 -0800351 print('Checking tree status:')
352 verify_repo_clean()
353 print('Clean.')
354 except DirtyTreeException as e:
355 print('Local tree is dirty, can\'t perform update safely.')
356 print()
357 print('repo status:')
Don Garrettd0321722014-11-18 16:03:33 -0800358 print(e.args[0])
359 return 1
Don Garrett8db752c2014-10-17 16:56:55 -0700360
Don Garrettfa2c1c42014-12-11 12:11:49 -0800361 versions_before = versions_after = {}
362
Don Garrett03432d62014-11-19 18:18:35 -0800363 if behaviors.update:
364 print('Checking repository versions.')
365 versions_before = repo_versions()
366
367 print('Updating Repo.')
368 repo_sync()
369
370 print('Checking repository versions after update.')
Don Garrettfa2c1c42014-12-11 12:11:49 -0800371 versions_after = repo_versions()
372 if versions_before == versions_after:
Don Garrett03432d62014-11-19 18:18:35 -0800373 print('No change found.')
374 return
375
376 if behaviors.actions:
377 try:
378 run_deploy_actions(dryrun=behaviors.dryrun)
379 except UnstableServices as e:
380 print('The following services were not stable after '
381 'the update:')
382 print(e.args[0])
383 return 1
384
Don Garrettfa2c1c42014-12-11 12:11:49 -0800385 if behaviors.report and versions_before and versions_after:
386 print('Changes:')
387 print(report_changes(versions_before, versions_after))
Don Garrett8db752c2014-10-17 16:56:55 -0700388
389
390if __name__ == '__main__':
Don Garrett03432d62014-11-19 18:18:35 -0800391 sys.exit(main(sys.argv[1:]))