blob: f3180e29144a0151266e93162eea98cfcb97b13c [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
Dan Shicf278042016-04-06 21:16:34 -070019import socket
Don Garrett8db752c2014-10-17 16:56:55 -070020import subprocess
21import sys
22import time
23
24import common
25
26from autotest_lib.client.common_lib import global_config
Dan Shicf278042016-04-06 21:16:34 -070027from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Don Garrett8db752c2014-10-17 16:56:55 -070028
Don Garrettd0321722014-11-18 16:03:33 -080029# How long after restarting a service do we watch it to see if it's stable.
Don Garrett6073ba92015-07-23 15:01:39 -070030SERVICE_STABILITY_TIMER = 120
Don Garrettd0321722014-11-18 16:03:33 -080031
Dan Shicf278042016-04-06 21:16:34 -070032# A list of commands that only applies to primary server. For example,
33# test_importer should only be run in primary master scheduler. If two servers
34# are both running test_importer, there is a chance to fail as both try to
35# update the same table.
36PRIMARY_ONLY_COMMANDS = ['test_importer']
37
38AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
Don Garrett8db752c2014-10-17 16:56:55 -070039
40class DirtyTreeException(Exception):
Don Garrettd0321722014-11-18 16:03:33 -080041 """Raised when the tree has been modified in an unexpected way."""
Don Garrett8db752c2014-10-17 16:56:55 -070042
43
44class UnknownCommandException(Exception):
Don Garrettd0321722014-11-18 16:03:33 -080045 """Raised when we try to run a command name with no associated command."""
Don Garrett8db752c2014-10-17 16:56:55 -070046
47
48class UnstableServices(Exception):
Don Garrettd0321722014-11-18 16:03:33 -080049 """Raised if a service appears unstable after restart."""
Don Garrett8db752c2014-10-17 16:56:55 -070050
51
Don Garrett35711212014-12-18 14:33:41 -080052def strip_terminal_codes(text):
53 """This function removes all terminal formatting codes from a string.
54
55 @param text: String of text to cleanup.
56 @returns String with format codes removed.
57 """
58 ESC = '\x1b'
59 return re.sub(ESC+r'\[[^m]*m', '', text)
60
61
Don Garrett8db752c2014-10-17 16:56:55 -070062def verify_repo_clean():
63 """This function verifies that the current repo is valid, and clean.
64
65 @raises DirtyTreeException if the repo is not clean.
66 @raises subprocess.CalledProcessError on a repo command failure.
67 """
Don Garrett8db752c2014-10-17 16:56:55 -070068 out = subprocess.check_output(['repo', 'status'], stderr=subprocess.STDOUT)
Don Garrett35711212014-12-18 14:33:41 -080069 out = strip_terminal_codes(out).strip()
Don Garrett699b4b32014-12-11 13:10:15 -080070
Don Garrett699b4b32014-12-11 13:10:15 -080071 CLEAN_STATUS_OUTPUT = 'nothing to commit (working directory clean)'
Prathmesh Prabhuda286992015-04-07 13:20:08 -070072 if out != CLEAN_STATUS_OUTPUT:
Dan Shicf278042016-04-06 21:16:34 -070073 raise DirtyTreeException(out)
Don Garrett8db752c2014-10-17 16:56:55 -070074
Don Garrett8db752c2014-10-17 16:56:55 -070075
76def repo_versions():
77 """This function collects the versions of all git repos in the general repo.
78
Don Garrettfa2c1c42014-12-11 12:11:49 -080079 @returns A dictionary mapping project names to git hashes for HEAD.
Don Garrett8db752c2014-10-17 16:56:55 -070080 @raises subprocess.CalledProcessError on a repo command failure.
81 """
Don Garrettfa2c1c42014-12-11 12:11:49 -080082 cmd = ['repo', 'forall', '-p', '-c', 'pwd && git log -1 --format=%h']
Don Garrett35711212014-12-18 14:33:41 -080083 output = strip_terminal_codes(subprocess.check_output(cmd))
Don Garrettfa2c1c42014-12-11 12:11:49 -080084
85 # The expected output format is:
86
87 # project chrome_build/
88 # /dir/holding/chrome_build
89 # 73dee9d
90 #
91 # project chrome_release/
92 # /dir/holding/chrome_release
93 # 9f3a5d8
94
95 lines = output.splitlines()
96
97 PROJECT_PREFIX = 'project '
98
99 project_heads = {}
100 for n in range(0, len(lines), 4):
101 project_line = lines[n]
102 project_dir = lines[n+1]
103 project_hash = lines[n+2]
104 # lines[n+3] is a blank line, but doesn't exist for the final block.
105
106 # Convert 'project chrome_build/' -> 'chrome_build'
107 assert project_line.startswith(PROJECT_PREFIX)
108 name = project_line[len(PROJECT_PREFIX):].rstrip('/')
109
110 project_heads[name] = (project_dir, project_hash)
111
112 return project_heads
Don Garrett8db752c2014-10-17 16:56:55 -0700113
114
115def repo_sync():
116 """Perform a repo sync.
117
118 @raises subprocess.CalledProcessError on a repo command failure.
119 """
Don Garrettd0321722014-11-18 16:03:33 -0800120 subprocess.check_output(['repo', 'sync'])
Don Garrett8db752c2014-10-17 16:56:55 -0700121
122
Don Garrettd0321722014-11-18 16:03:33 -0800123def discover_update_commands():
124 """Lookup the commands to run on this server.
Don Garrett8db752c2014-10-17 16:56:55 -0700125
Don Garrettd0321722014-11-18 16:03:33 -0800126 These commonly come from shadow_config.ini, since they vary by server type.
Don Garrett8db752c2014-10-17 16:56:55 -0700127
Don Garrettd0321722014-11-18 16:03:33 -0800128 @returns List of command names in string format.
Don Garrett8db752c2014-10-17 16:56:55 -0700129 """
Don Garrett8db752c2014-10-17 16:56:55 -0700130 try:
Don Garrettd0321722014-11-18 16:03:33 -0800131 return global_config.global_config.get_config_value(
Don Garrett8db752c2014-10-17 16:56:55 -0700132 'UPDATE', 'commands', type=list)
133
134 except (ConfigParser.NoSectionError, global_config.ConfigError):
Don Garrettd0321722014-11-18 16:03:33 -0800135 return []
Don Garrett8db752c2014-10-17 16:56:55 -0700136
Don Garrettd0321722014-11-18 16:03:33 -0800137
138def discover_restart_services():
139 """Find the services that need restarting on the current server.
140
141 These commonly come from shadow_config.ini, since they vary by server type.
142
143 @returns List of service names in string format.
144 """
Don Garrett8db752c2014-10-17 16:56:55 -0700145 try:
146 # From shadow_config.ini, lookup which services to restart.
Don Garrettd0321722014-11-18 16:03:33 -0800147 return global_config.global_config.get_config_value(
Don Garrett8db752c2014-10-17 16:56:55 -0700148 'UPDATE', 'services', type=list)
149
150 except (ConfigParser.NoSectionError, global_config.ConfigError):
Don Garrettd0321722014-11-18 16:03:33 -0800151 return []
Don Garrett8db752c2014-10-17 16:56:55 -0700152
Don Garrett8db752c2014-10-17 16:56:55 -0700153
Don Garrett03432d62014-11-19 18:18:35 -0800154def update_command(cmd_tag, dryrun=False):
Don Garrettd0321722014-11-18 16:03:33 -0800155 """Restart a command.
Don Garrett8db752c2014-10-17 16:56:55 -0700156
Don Garrettd0321722014-11-18 16:03:33 -0800157 The command name is looked up in global_config.ini to find the full command
158 to run, then it's executed.
Don Garrett8db752c2014-10-17 16:56:55 -0700159
Don Garrettd0321722014-11-18 16:03:33 -0800160 @param cmd_tag: Which command to restart.
Don Garrett03432d62014-11-19 18:18:35 -0800161 @param dryrun: If true print the command that would have been run.
Don Garrett8db752c2014-10-17 16:56:55 -0700162
Don Garrettd0321722014-11-18 16:03:33 -0800163 @raises UnknownCommandException If cmd_tag can't be looked up.
164 @raises subprocess.CalledProcessError on a command failure.
165 """
166 # Lookup the list of commands to consider. They are intended to be
167 # in global_config.ini so that they can be shared everywhere.
168 cmds = dict(global_config.global_config.config.items(
169 'UPDATE_COMMANDS'))
Don Garrett8db752c2014-10-17 16:56:55 -0700170
Don Garrettd0321722014-11-18 16:03:33 -0800171 if cmd_tag not in cmds:
172 raise UnknownCommandException(cmd_tag, cmds)
Don Garrett8db752c2014-10-17 16:56:55 -0700173
Don Garrettd0321722014-11-18 16:03:33 -0800174 expanded_command = cmds[cmd_tag].replace('AUTOTEST_REPO',
175 common.autotest_dir)
Don Garrett8db752c2014-10-17 16:56:55 -0700176
Don Garrett699b4b32014-12-11 13:10:15 -0800177 print('Running: %s: %s' % (cmd_tag, expanded_command))
Don Garrett03432d62014-11-19 18:18:35 -0800178 if dryrun:
Don Garrett699b4b32014-12-11 13:10:15 -0800179 print('Skip: %s' % expanded_command)
Don Garrett03432d62014-11-19 18:18:35 -0800180 else:
Don Garrett4769c902015-01-05 15:58:56 -0800181 try:
182 subprocess.check_output(expanded_command, shell=True,
183 stderr=subprocess.STDOUT)
184 except subprocess.CalledProcessError as e:
185 print('FAILED:')
186 print(e.output)
187 raise
Don Garrett8db752c2014-10-17 16:56:55 -0700188
Don Garrett8db752c2014-10-17 16:56:55 -0700189
Don Garrett03432d62014-11-19 18:18:35 -0800190def restart_service(service_name, dryrun=False):
Don Garrettd0321722014-11-18 16:03:33 -0800191 """Restart a service.
192
193 Restarts the standard service with "service <name> restart".
194
195 @param service_name: The name of the service to restart.
Don Garrett03432d62014-11-19 18:18:35 -0800196 @param dryrun: Don't really run anything, just print out the command.
Don Garrettd0321722014-11-18 16:03:33 -0800197
198 @raises subprocess.CalledProcessError on a command failure.
199 """
Don Garrett03432d62014-11-19 18:18:35 -0800200 cmd = ['sudo', 'service', service_name, 'restart']
Don Garrett699b4b32014-12-11 13:10:15 -0800201 print('Restarting: %s' % service_name)
Don Garrett03432d62014-11-19 18:18:35 -0800202 if dryrun:
Don Garrett699b4b32014-12-11 13:10:15 -0800203 print('Skip: %s' % ' '.join(cmd))
Don Garrett03432d62014-11-19 18:18:35 -0800204 else:
Don Garrett03432d62014-11-19 18:18:35 -0800205 subprocess.check_call(cmd)
Don Garrettd0321722014-11-18 16:03:33 -0800206
207
208def service_status(service_name):
209 """Return the results "status <name>" for a given service.
210
211 This string is expected to contain the pid, and so to change is the service
212 is shutdown or restarted for any reason.
213
214 @param service_name: The name of the service to check on.
Don Garrett03432d62014-11-19 18:18:35 -0800215
Don Garrettd0321722014-11-18 16:03:33 -0800216 @returns The output of the external command.
217 Ex: autofs start/running, process 1931
218
219 @raises subprocess.CalledProcessError on a command failure.
220 """
221 return subprocess.check_output(['sudo', 'status', service_name])
222
223
Dan Shi57d4c732015-01-22 18:38:50 -0800224def restart_services(service_names, dryrun=False, skip_service_status=False):
Don Garrettd0321722014-11-18 16:03:33 -0800225 """Restart services as needed for the current server type.
226
227 Restart the listed set of services, and watch to see if they are stable for
228 at least SERVICE_STABILITY_TIMER. It restarts all services quickly,
229 waits for that delay, then verifies the status of all of them.
230
231 @param service_names: The list of service to restart and monitor.
Don Garrett03432d62014-11-19 18:18:35 -0800232 @param dryrun: Don't really restart the service, just print out the command.
Dan Shi57d4c732015-01-22 18:38:50 -0800233 @param skip_service_status: Set to True to skip service status check.
234 Default is False.
Don Garrettd0321722014-11-18 16:03:33 -0800235
236 @raises subprocess.CalledProcessError on a command failure.
Don Garrett03432d62014-11-19 18:18:35 -0800237 @raises UnstableServices if any services are unstable after restart.
Don Garrettd0321722014-11-18 16:03:33 -0800238 """
239 service_statuses = {}
240
Don Garrett03432d62014-11-19 18:18:35 -0800241 if dryrun:
242 for name in service_names:
243 restart_service(name, dryrun=True)
244 return
245
Don Garrettd0321722014-11-18 16:03:33 -0800246 # Restart each, and record the status (including pid).
247 for name in service_names:
248 restart_service(name)
249 service_statuses[name] = service_status(name)
250
Dan Shi57d4c732015-01-22 18:38:50 -0800251 # Skip service status check if --skip-service-status is specified. Used for
252 # servers in backup status.
253 if skip_service_status:
254 print('--skip-service-status is specified, skip checking services.')
255 return
256
Don Garrettd0321722014-11-18 16:03:33 -0800257 # Wait for a while to let the services settle.
258 time.sleep(SERVICE_STABILITY_TIMER)
259
260 # Look for any services that changed status.
261 unstable_services = [n for n in service_names
262 if service_status(n) != service_statuses[n]]
263
264 # Report any services having issues.
265 if unstable_services:
266 raise UnstableServices(unstable_services)
Don Garrett8db752c2014-10-17 16:56:55 -0700267
268
Dan Shi57d4c732015-01-22 18:38:50 -0800269def run_deploy_actions(dryrun=False, skip_service_status=False):
Don Garrettfa2c1c42014-12-11 12:11:49 -0800270 """Run arbitrary update commands specified in global.ini.
Don Garrett8db752c2014-10-17 16:56:55 -0700271
Don Garrett03432d62014-11-19 18:18:35 -0800272 @param dryrun: Don't really restart the service, just print out the command.
Dan Shi57d4c732015-01-22 18:38:50 -0800273 @param skip_service_status: Set to True to skip service status check.
274 Default is False.
Don Garrett8db752c2014-10-17 16:56:55 -0700275
Don Garrett03432d62014-11-19 18:18:35 -0800276 @raises subprocess.CalledProcessError on a command failure.
277 @raises UnstableServices if any services are unstable after restart.
278 """
Don Garrettd0321722014-11-18 16:03:33 -0800279 cmds = discover_update_commands()
280 if cmds:
281 print('Running update commands:', ', '.join(cmds))
282 for cmd in cmds:
Dan Shicf278042016-04-06 21:16:34 -0700283 if (cmd in PRIMARY_ONLY_COMMANDS and
284 not AFE.run('get_servers', hostname=socket.getfqdn(),
285 status='primary')):
286 print('Command %s is only applicable to primary servers.' % cmd)
287 continue
Don Garrett03432d62014-11-19 18:18:35 -0800288 update_command(cmd, dryrun=dryrun)
Don Garrettd0321722014-11-18 16:03:33 -0800289
290 services = discover_restart_services()
291 if services:
Don Garrett03432d62014-11-19 18:18:35 -0800292 print('Restarting Services:', ', '.join(services))
Dan Shi57d4c732015-01-22 18:38:50 -0800293 restart_services(services, dryrun=dryrun,
294 skip_service_status=skip_service_status)
Don Garrett03432d62014-11-19 18:18:35 -0800295
296
Don Garrettfa2c1c42014-12-11 12:11:49 -0800297def report_changes(versions_before, versions_after):
298 """Produce a report describing what changed in all repos.
299
300 @param versions_before: Results of repo_versions() from before the update.
301 @param versions_after: Results of repo_versions() from after the update.
302
303 @returns string containing a human friendly changes report.
304 """
305 result = []
306
Don Garrett35711212014-12-18 14:33:41 -0800307 if versions_after:
308 for project in sorted(set(versions_before.keys() + versions_after.keys())):
309 result.append('%s:' % project)
Don Garrettfa2c1c42014-12-11 12:11:49 -0800310
Don Garrett35711212014-12-18 14:33:41 -0800311 _, before_hash = versions_before.get(project, (None, None))
312 after_dir, after_hash = versions_after.get(project, (None, None))
Don Garrettfa2c1c42014-12-11 12:11:49 -0800313
Don Garrett35711212014-12-18 14:33:41 -0800314 if project not in versions_before:
315 result.append('Added.')
Don Garrettfa2c1c42014-12-11 12:11:49 -0800316
Don Garrett35711212014-12-18 14:33:41 -0800317 elif project not in versions_after:
318 result.append('Removed.')
Don Garrettfa2c1c42014-12-11 12:11:49 -0800319
Don Garrett35711212014-12-18 14:33:41 -0800320 elif before_hash == after_hash:
321 result.append('No Change.')
Don Garrettfa2c1c42014-12-11 12:11:49 -0800322
Don Garrett35711212014-12-18 14:33:41 -0800323 else:
324 hashes = '%s..%s' % (before_hash, after_hash)
325 cmd = ['git', 'log', hashes, '--oneline']
326 out = subprocess.check_output(cmd, cwd=after_dir,
327 stderr=subprocess.STDOUT)
328 result.append(out.strip())
Don Garrettfa2c1c42014-12-11 12:11:49 -0800329
Don Garrett35711212014-12-18 14:33:41 -0800330 result.append('')
331 else:
332 for project in sorted(versions_before.keys()):
333 _, before_hash = versions_before[project]
334 result.append('%s: %s' % (project, before_hash))
Don Garrettfa2c1c42014-12-11 12:11:49 -0800335 result.append('')
336
337 return '\n'.join(result)
338
339
Don Garrett03432d62014-11-19 18:18:35 -0800340def parse_arguments(args):
341 """Parse command line arguments.
342
343 @param args: The command line arguments to parse. (ususally sys.argsv[1:])
344
Don Garrett40036362014-12-08 15:52:44 -0800345 @returns An argparse.Namespace populated with argument values.
Don Garrett03432d62014-11-19 18:18:35 -0800346 """
347 parser = argparse.ArgumentParser(
348 description='Command to update an autotest server.')
349 parser.add_argument('--skip-verify', action='store_false',
350 dest='verify', default=True,
351 help='Disable verification of a clean repository.')
352 parser.add_argument('--skip-update', action='store_false',
353 dest='update', default=True,
354 help='Skip the repository source code update.')
355 parser.add_argument('--skip-actions', action='store_false',
356 dest='actions', default=True,
357 help='Skip the post update actions.')
358 parser.add_argument('--skip-report', action='store_false',
359 dest='report', default=True,
360 help='Skip the git version report.')
Don Garrette3718912014-12-05 13:11:44 -0800361 parser.add_argument('--actions-only', action='store_true',
362 help='Run the post update actions (restart services).')
Don Garrett03432d62014-11-19 18:18:35 -0800363 parser.add_argument('--dryrun', action='store_true',
364 help='Don\'t actually run any commands, just log.')
Dan Shi57d4c732015-01-22 18:38:50 -0800365 parser.add_argument('--skip-service-status', action='store_true',
366 help='Skip checking the service status.')
Don Garrett03432d62014-11-19 18:18:35 -0800367
368 results = parser.parse_args(args)
369
Don Garrette3718912014-12-05 13:11:44 -0800370 if results.actions_only:
371 results.verify = False
372 results.update = False
373 results.report = False
374
Don Garrett03432d62014-11-19 18:18:35 -0800375 # TODO(dgarrett): Make these behaviors support dryrun.
376 if results.dryrun:
377 results.verify = False
378 results.update = False
379
380 return results
381
382
383def main(args):
384 """Main method."""
385 os.chdir(common.autotest_dir)
386 global_config.global_config.parse_config_file()
387
388 behaviors = parse_arguments(args)
389
390 if behaviors.verify:
Don Garrettd0321722014-11-18 16:03:33 -0800391 try:
Don Garrett03432d62014-11-19 18:18:35 -0800392 print('Checking tree status:')
393 verify_repo_clean()
394 print('Clean.')
395 except DirtyTreeException as e:
396 print('Local tree is dirty, can\'t perform update safely.')
397 print()
398 print('repo status:')
Don Garrettd0321722014-11-18 16:03:33 -0800399 print(e.args[0])
400 return 1
Don Garrett8db752c2014-10-17 16:56:55 -0700401
Don Garrett35711212014-12-18 14:33:41 -0800402 versions_before = repo_versions()
403 versions_after = {}
Don Garrettfa2c1c42014-12-11 12:11:49 -0800404
Don Garrett03432d62014-11-19 18:18:35 -0800405 if behaviors.update:
Don Garrett03432d62014-11-19 18:18:35 -0800406 print('Updating Repo.')
407 repo_sync()
Don Garrettfa2c1c42014-12-11 12:11:49 -0800408 versions_after = repo_versions()
Don Garrett03432d62014-11-19 18:18:35 -0800409
410 if behaviors.actions:
411 try:
Dan Shi57d4c732015-01-22 18:38:50 -0800412 run_deploy_actions(
413 dryrun=behaviors.dryrun,
414 skip_service_status=behaviors.skip_service_status)
Don Garrett03432d62014-11-19 18:18:35 -0800415 except UnstableServices as e:
416 print('The following services were not stable after '
417 'the update:')
418 print(e.args[0])
419 return 1
420
Don Garrett35711212014-12-18 14:33:41 -0800421 if behaviors.report:
Don Garrettfa2c1c42014-12-11 12:11:49 -0800422 print('Changes:')
423 print(report_changes(versions_before, versions_after))
Don Garrett8db752c2014-10-17 16:56:55 -0700424
425
426if __name__ == '__main__':
Don Garrett03432d62014-11-19 18:18:35 -0800427 sys.exit(main(sys.argv[1:]))