blob: e9eb632e17960522a4be034cae8c169f111e90b8 [file] [log] [blame]
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +08001#!/usr/bin/python -Bu
2#
3# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Factory toolkit installer.
8
9The factory toolkit is a self-extracting shellball containing factory test
10related files and this installer. This installer is invoked when the toolkit
11is deployed and is responsible for installing files.
12"""
13
14
15import argparse
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080016from contextlib import contextmanager
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080017import os
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080018import sys
Jon Salz4f3ade52014-02-20 17:55:09 +080019import tempfile
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080020
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080021import factory_common # pylint: disable=W0611
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080022from cros.factory.test import factory
Vic Yang196d5242014-08-05 13:51:35 +080023from cros.factory.test import utils
Jon Salz25590302014-07-11 16:07:20 +080024from cros.factory.tools import install_symlinks
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080025from cros.factory.tools.mount_partition import MountPartition
26from cros.factory.utils.process_utils import Spawn
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080027
28
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +080029INSTALLER_PATH = 'usr/local/factory/py/toolkit/installer.py'
30
Jon Salz4f3ade52014-02-20 17:55:09 +080031# Short and sweet help header for the executable generated by makeself.
32HELP_HEADER = """
33Installs the factory toolkit, transforming a test image into a factory test
34image. You can:
35
36- Install the factory toolkit on a CrOS device that is running a test
37 image. To do this, copy install_factory_toolkit.run to the device and
38 run it. The factory tests will then come up on the next boot.
39
40 rsync -a install_factory_toolkit.run crosdevice:/tmp
41 ssh crosdevice '/tmp/install_factory_toolkit.run && sync && reboot'
42
43- Modify a test image, turning it into a factory test image. When you
44 use the image on a device, the factory tests will come up.
45
46 install_factory_toolkit.run chromiumos_test_image.bin
47"""
48
49HELP_HEADER_ADVANCED = """
50- (advanced) Modify a mounted stateful partition, turning it into a factory
51 test image. This is equivalent to the previous command:
52
53 mount_partition -rw chromiumos_test_image.bin 1 /mnt/stateful
54 install_factory_toolkit.run /mnt/stateful
55 umount /mnt/stateful
56
57- (advanced) Unpack the factory toolkit, modify a file, and then repack it.
58
59 # Unpack but don't actually install
60 install_factory_toolkit.run --target /tmp/toolkit --noexec
61 # Edit some files in /tmp/toolkit
62 emacs /tmp/toolkit/whatever
63 # Repack
64 install_factory_toolkit.run -- --repack /tmp/toolkit \\
65 --pack-into /path/to/new/install_factory_toolkit.run
66"""
67
68# The makeself-generated header comes next. This is a little confusing,
69# so explain.
70HELP_HEADER_MAKESELF = """
71For complete usage information and advanced operations, run
72"install_factory_toolkit.run -- --help" (note the extra "--").
73
74Following is the help message from makeself, which was used to create
75this self-extracting archive.
76
77-----
78"""
79
Vic Yang196d5242014-08-05 13:51:35 +080080# The method to determine whether running on Chrome OS device or not.
81# Override this for unit testing.
82_in_cros_device = utils.in_cros_device
83
Jon Salz4f3ade52014-02-20 17:55:09 +080084
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080085class FactoryToolkitInstaller():
86 """Factory toolkit installer.
87
88 Args:
89 src: Source path containing usr/ and var/.
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080090 dest: Installation destination path. Set this to the mount point of the
91 stateful partition if patching a test image.
92 no_enable: True to not install the tag file.
93 system_root: The path to the root of the file system. This must be left
94 as its default value except for unit testing.
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080095 """
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080096
Jon Salzb7e44262014-05-07 15:53:37 +080097 # Whether to sudo when rsyncing; set to False for testing.
98 _sudo = True
99
Peter Ammon948b7172014-07-15 12:43:06 -0700100 def __init__(self, src, dest, no_enable, enable_presenter,
Vic Yang7039f422014-07-07 15:38:13 -0700101 enable_device, system_root='/'):
Jon Salz4f3ade52014-02-20 17:55:09 +0800102 self._src = src
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800103 self._system_root = system_root
104 if dest == self._system_root:
105 self._usr_local_dest = os.path.join(dest, 'usr', 'local')
106 self._var_dest = os.path.join(dest, 'var')
Jon Salz4f3ade52014-02-20 17:55:09 +0800107
108 # Make sure we're on a CrOS device.
Vic Yang196d5242014-08-05 13:51:35 +0800109 if not _in_cros_device():
Jon Salz4f3ade52014-02-20 17:55:09 +0800110 sys.stderr.write(
Vic Yang196d5242014-08-05 13:51:35 +0800111 "ERROR: You're not on a CrOS device (for more details, please\n"
112 "check utils.py:in_cros_device), so you must specify a test\n"
113 "image or a mounted stateful partition on which to install the\n"
114 "factory toolkit. Please run\n"
Jon Salz4f3ade52014-02-20 17:55:09 +0800115 "\n"
116 " install_factory_toolkit.run -- --help\n"
117 "\n"
118 "for help.\n")
119 sys.exit(1)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800120 if os.getuid() != 0:
Jon Salz4f3ade52014-02-20 17:55:09 +0800121 raise Exception('You must be root to install the factory toolkit on a '
122 'CrOS device.')
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800123 else:
124 self._usr_local_dest = os.path.join(dest, 'dev_image')
125 self._var_dest = os.path.join(dest, 'var_overlay')
126 if (not os.path.exists(self._usr_local_dest) or
127 not os.path.exists(self._var_dest)):
128 raise Exception(
129 'The destination path %s is not a stateful partition!' % dest)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800130
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800131 self._dest = dest
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800132 self._usr_local_src = os.path.join(src, 'usr', 'local')
133 self._var_src = os.path.join(src, 'var')
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800134 self._no_enable = no_enable
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800135 self._tag_file = os.path.join(self._usr_local_dest, 'factory', 'enabled')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800136
Peter Ammon948b7172014-07-15 12:43:06 -0700137 self._enable_presenter = enable_presenter
138 self._presenter_tag_file = os.path.join(self._usr_local_dest, 'factory',
139 'init', 'run_goofy_presenter')
Vic Yang7039f422014-07-07 15:38:13 -0700140
141 self._enable_device = enable_device
Ricky Liangd7716912014-07-10 11:52:24 +0800142 self._device_tag_file = os.path.join(self._usr_local_dest, 'factory',
143 'init', 'run_goofy_device')
Vic Yang7039f422014-07-07 15:38:13 -0700144
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800145 if (not os.path.exists(self._usr_local_src) or
146 not os.path.exists(self._var_src)):
147 raise Exception(
148 'This installer must be run from within the factory toolkit!')
149
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800150 def WarningMessage(self, target_test_image=None):
Jon Salz4f3ade52014-02-20 17:55:09 +0800151 with open(os.path.join(self._src, 'VERSION')) as f:
152 ret = f.read()
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800153 if target_test_image:
Jon Salz4f3ade52014-02-20 17:55:09 +0800154 ret += (
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800155 '\n'
156 '\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800157 '*** You are about to patch the factory toolkit into:\n'
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800158 '*** %s\n'
159 '***' % target_test_image)
160 else:
Jon Salz4f3ade52014-02-20 17:55:09 +0800161 ret += (
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800162 '\n'
163 '\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800164 '*** You are about to install the factory toolkit to:\n'
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800165 '*** %s\n'
166 '***' % self._dest)
167 if self._dest == self._system_root:
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800168 if self._no_enable:
169 ret += ('\n'
170 '*** Factory tests will be disabled after this process is done, but\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800171 '*** you can enable them by creating the factory enabled tag:\n'
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800172 '*** %s\n'
173 '***' % self._tag_file)
174 else:
175 ret += ('\n'
176 '*** After this process is done, your device will start factory\n'
177 '*** tests on the next reboot.\n'
178 '***\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800179 '*** Factory tests can be disabled by deleting the factory enabled\n'
180 '*** tag:\n'
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800181 '*** %s\n'
182 '***' % self._tag_file)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800183 return ret
184
Vic Yang7039f422014-07-07 15:38:13 -0700185 def _SetTagFile(self, name, path, enabled):
186 """Install or remove a tag file."""
187 if enabled:
188 print '*** Installing %s enabled tag...' % name
189 Spawn(['touch', path], sudo=True, log=True, check_call=True)
Ricky Liang8c88a122014-07-11 21:21:22 +0800190 Spawn(['chmod', 'go+r', path], sudo=True, log=True, check_call=True)
Vic Yang7039f422014-07-07 15:38:13 -0700191 else:
192 print '*** Removing %s enabled tag...' % name
193 Spawn(['rm', '-f', path], sudo=True, log=True, check_call=True)
194
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800195 def Install(self):
196 print '*** Installing factory toolkit...'
Jon Salzb7e44262014-05-07 15:53:37 +0800197 for src, dest in ((self._usr_local_src, self._usr_local_dest),
198 (self._var_src, self._var_dest)):
199 # Change the source directory to root, and add group/world read
200 # permissions. This is necessary because when the toolkit was
201 # unpacked, the user may not have been root so the permessions
202 # may be hosed. This is skipped for testing.
Peter Ammon5ac58422014-06-09 14:45:50 -0700203 # --force is necessary to allow goofy directory from prior
204 # toolkit installations to be overwritten by the goofy symlink.
Ricky Liang5e95be22014-07-09 12:52:07 +0800205 try:
206 if self._sudo:
207 Spawn(['chown', '-R', 'root', src],
208 sudo=True, log=True, check_call=True)
209 Spawn(['chmod', '-R', 'go+rX', src],
210 sudo=True, log=True, check_call=True)
211 print '*** %s -> %s' % (src, dest)
212 Spawn(['rsync', '-a', '--force', src + '/', dest],
213 sudo=self._sudo, log=True, check_output=True)
214 finally:
215 # Need to change the source directory back to the original user, or the
216 # script in makeself will fail to remove the temporary source directory.
217 if self._sudo:
218 myuser = os.environ.get('USER')
219 Spawn(['chown', '-R', myuser, src],
220 sudo=True, log=True, check_call=True)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800221
Jon Salz25590302014-07-11 16:07:20 +0800222 print '*** Installing symlinks...'
223 install_symlinks.InstallSymlinks(
224 '../factory/bin',
225 os.path.join(self._usr_local_dest, 'bin'),
226 install_symlinks.MODE_FULL,
227 sudo=self._sudo)
228
229 print '*** Removing factory-mini...'
230 Spawn(['rm', '-rf', os.path.join(self._usr_local_dest, 'factory-mini')],
231 sudo=self._sudo, log=True, check_call=True)
232
Vic Yang7039f422014-07-07 15:38:13 -0700233 self._SetTagFile('factory', self._tag_file, not self._no_enable)
Peter Ammon948b7172014-07-15 12:43:06 -0700234 self._SetTagFile('presenter', self._presenter_tag_file,
235 self._enable_presenter)
Vic Yang7039f422014-07-07 15:38:13 -0700236 self._SetTagFile('device', self._device_tag_file, self._enable_device)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800237
238 print '*** Installation completed.'
239
240
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800241@contextmanager
242def DummyContext(arg):
243 """A context manager that simply yields its argument."""
244 yield arg
245
246
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800247def PrintBuildInfo(src_root):
248 """Print build information."""
249 info_file = os.path.join(src_root, 'REPO_STATUS')
250 if not os.path.exists(info_file):
251 raise OSError('Build info file not found!')
252 with open(info_file, 'r') as f:
253 print f.read()
254
255
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800256def PackFactoryToolkit(src_root, output_path):
257 """Packs the files containing this script into a factory toolkit."""
258 with open(os.path.join(src_root, 'VERSION'), 'r') as f:
259 version = f.read().strip()
Jon Salz4f3ade52014-02-20 17:55:09 +0800260 with tempfile.NamedTemporaryFile() as help_header:
261 help_header.write(version + "\n" + HELP_HEADER + HELP_HEADER_MAKESELF)
262 help_header.flush()
263 Spawn([os.path.join(src_root, 'makeself.sh'), '--bzip2', '--nox11',
264 '--help-header', help_header.name,
265 src_root, output_path, version, INSTALLER_PATH, '--in-exe'],
266 check_call=True, log=True)
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800267 print ('\n'
268 ' Factory toolkit generated at %s.\n'
269 '\n'
270 ' To install factory toolkit on a live device running a test image,\n'
271 ' copy this to the device and execute it as root.\n'
272 '\n'
273 ' Alternatively, the factory toolkit can be used to patch a test\n'
274 ' image. For more information, run:\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800275 ' %s --help\n'
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800276 '\n' % (output_path, output_path))
277
278
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800279def main():
Jon Salz4f3ade52014-02-20 17:55:09 +0800280 import logging
281 logging.basicConfig(level=logging.INFO)
282
283 # In order to determine which usage message to show, first determine
284 # whether we're in the self-extracting archive. Do this first
285 # because we need it to even parse the arguments.
286 if '--in-exe' in sys.argv:
287 sys.argv = [x for x in sys.argv if x != '--in-exe']
288 in_archive = True
289 else:
290 in_archive = False
291
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800292 parser = argparse.ArgumentParser(
Jon Salz4f3ade52014-02-20 17:55:09 +0800293 description=HELP_HEADER + HELP_HEADER_ADVANCED,
294 usage=('install_factory_toolkit.run -- [options]' if in_archive
295 else None),
296 formatter_class=argparse.RawDescriptionHelpFormatter)
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800297 parser.add_argument('dest', nargs='?', default='/',
298 help='A test image or the mount point of the stateful partition. '
299 "If omitted, install to live system, i.e. '/'.")
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800300 parser.add_argument('--no-enable', '-n', action='store_true',
301 help="Don't enable factory tests after installing")
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800302 parser.add_argument('--yes', '-y', action='store_true',
303 help="Don't ask for confirmation")
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800304 parser.add_argument('--build-info', action='store_true',
305 help="Print build information and exit")
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800306 parser.add_argument('--pack-into', metavar='NEW_TOOLKIT',
307 help="Pack the files into a new factory toolkit")
308 parser.add_argument('--repack', metavar='UNPACKED_TOOLKIT',
309 help="Repack from previously unpacked toolkit")
Vic Yang7039f422014-07-07 15:38:13 -0700310
Peter Ammon948b7172014-07-15 12:43:06 -0700311 parser.add_argument('--enable-presenter', dest='enable_presenter',
Vic Yang7039f422014-07-07 15:38:13 -0700312 action='store_true',
Peter Ammon948b7172014-07-15 12:43:06 -0700313 help="Run goofy in presenter mode on startup")
314 parser.add_argument('--no-enable-presenter', dest='enable_presenter',
Vic Yang7039f422014-07-07 15:38:13 -0700315 action='store_false', help=argparse.SUPPRESS)
Peter Ammon948b7172014-07-15 12:43:06 -0700316 parser.set_defaults(enable_presenter=False)
Vic Yang7039f422014-07-07 15:38:13 -0700317
318 parser.add_argument('--enable-device', dest='enable_device',
319 action='store_true',
Peter Ammon948b7172014-07-15 12:43:06 -0700320 help="Run goofy in device mode on startup")
Vic Yang7039f422014-07-07 15:38:13 -0700321 parser.add_argument('--no-enable-device', dest='enable_device',
322 action='store_false', help=argparse.SUPPRESS)
Peter Ammon948b7172014-07-15 12:43:06 -0700323 parser.set_defaults(enable_device=True)
Vic Yang7039f422014-07-07 15:38:13 -0700324
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800325 args = parser.parse_args()
326
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800327 src_root = factory.FACTORY_PATH
328 for _ in xrange(3):
329 src_root = os.path.dirname(src_root)
330
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800331 # --pack-into may be called directly so this must be done before changing
332 # working directory to OLDPWD.
333 if args.pack_into and args.repack is None:
334 PackFactoryToolkit(src_root, args.pack_into)
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800335 return
336
Jon Salz4f3ade52014-02-20 17:55:09 +0800337 if not in_archive:
338 # If you're not in the self-extracting archive, you're not allowed to
339 # do anything except the above --pack-into call.
340 parser.error('Not running from install_factory_toolkit.run; '
341 'only --pack-into (without --repack) is allowed')
342
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800343 # Change to original working directory in case the user specifies
344 # a relative path.
345 # TODO: Use USER_PWD instead when makeself is upgraded
346 os.chdir(os.environ['OLDPWD'])
347
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800348 if args.repack:
349 if args.pack_into is None:
350 parser.error('Must specify --pack-into when using --repack.')
351 Spawn([os.path.join(args.repack, INSTALLER_PATH),
352 '--pack-into', args.pack_into], check_call=True, log=True)
353 return
354
355 if args.build_info:
356 PrintBuildInfo(src_root)
357 return
358
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800359 if not os.path.exists(args.dest):
360 parser.error('Destination %s does not exist!' % args.dest)
361
362 patch_test_image = os.path.isfile(args.dest)
363
364 with (MountPartition(args.dest, 1, rw=True) if patch_test_image
365 else DummyContext(args.dest)) as dest:
Peter Ammon948b7172014-07-15 12:43:06 -0700366 installer = FactoryToolkitInstaller(src_root, dest, args.no_enable,
367 args.enable_presenter, args.enable_device)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800368
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800369 print installer.WarningMessage(args.dest if patch_test_image else None)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800370
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800371 if not args.yes:
372 answer = raw_input('*** Continue? [y/N] ')
373 if not answer or answer[0] not in 'yY':
374 sys.exit('Aborting.')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800375
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800376 installer.Install()
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800377
378if __name__ == '__main__':
379 main()