blob: 9edb70b33c6ec4539b03896cce1a6c488fa6954b [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
Jon Salz4f3ade52014-02-20 17:55:09 +080018import re
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080019import sys
Jon Salz4f3ade52014-02-20 17:55:09 +080020import tempfile
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080021
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080022import factory_common # pylint: disable=W0611
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080023from cros.factory.test import factory
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
80
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080081class FactoryToolkitInstaller():
82 """Factory toolkit installer.
83
84 Args:
85 src: Source path containing usr/ and var/.
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080086 dest: Installation destination path. Set this to the mount point of the
87 stateful partition if patching a test image.
88 no_enable: True to not install the tag file.
89 system_root: The path to the root of the file system. This must be left
90 as its default value except for unit testing.
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080091 """
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080092
Jon Salzb7e44262014-05-07 15:53:37 +080093 # Whether to sudo when rsyncing; set to False for testing.
94 _sudo = True
95
Peter Ammon948b7172014-07-15 12:43:06 -070096 def __init__(self, src, dest, no_enable, enable_presenter,
Vic Yang7039f422014-07-07 15:38:13 -070097 enable_device, system_root='/'):
Jon Salz4f3ade52014-02-20 17:55:09 +080098 self._src = src
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080099 self._system_root = system_root
100 if dest == self._system_root:
101 self._usr_local_dest = os.path.join(dest, 'usr', 'local')
102 self._var_dest = os.path.join(dest, 'var')
Jon Salz4f3ade52014-02-20 17:55:09 +0800103
104 # Make sure we're on a CrOS device.
105 lsb_release = self._ReadLSBRelease()
106 is_cros = (
107 lsb_release and
108 re.match('^CHROMEOS_RELEASE', lsb_release, re.MULTILINE) is not None)
109
110 if not is_cros:
111 sys.stderr.write(
112 "ERROR: You're not on a CrOS device (/etc/lsb-release does not\n"
113 "contain CHROMEOS_RELEASE), so you must specify a test image or a\n"
114 "mounted stateful partition on which to install the factory\n"
115 "toolkit. Please run\n"
116 "\n"
117 " install_factory_toolkit.run -- --help\n"
118 "\n"
119 "for help.\n")
120 sys.exit(1)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800121 if os.getuid() != 0:
Jon Salz4f3ade52014-02-20 17:55:09 +0800122 raise Exception('You must be root to install the factory toolkit on a '
123 'CrOS device.')
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800124 else:
125 self._usr_local_dest = os.path.join(dest, 'dev_image')
126 self._var_dest = os.path.join(dest, 'var_overlay')
127 if (not os.path.exists(self._usr_local_dest) or
128 not os.path.exists(self._var_dest)):
129 raise Exception(
130 'The destination path %s is not a stateful partition!' % dest)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800131
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800132 self._dest = dest
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800133 self._usr_local_src = os.path.join(src, 'usr', 'local')
134 self._var_src = os.path.join(src, 'var')
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800135 self._no_enable = no_enable
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800136 self._tag_file = os.path.join(self._usr_local_dest, 'factory', 'enabled')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800137
Peter Ammon948b7172014-07-15 12:43:06 -0700138 self._enable_presenter = enable_presenter
139 self._presenter_tag_file = os.path.join(self._usr_local_dest, 'factory',
140 'init', 'run_goofy_presenter')
Vic Yang7039f422014-07-07 15:38:13 -0700141
142 self._enable_device = enable_device
Ricky Liangd7716912014-07-10 11:52:24 +0800143 self._device_tag_file = os.path.join(self._usr_local_dest, 'factory',
144 'init', 'run_goofy_device')
Vic Yang7039f422014-07-07 15:38:13 -0700145
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800146 if (not os.path.exists(self._usr_local_src) or
147 not os.path.exists(self._var_src)):
148 raise Exception(
149 'This installer must be run from within the factory toolkit!')
150
Jon Salz4f3ade52014-02-20 17:55:09 +0800151 @staticmethod
152 def _ReadLSBRelease():
153 """Returns the contents of /etc/lsb-release, or None if it does not
154 exist."""
155 if os.path.exists('/etc/lsb-release'):
156 with open('/etc/lsb-release') as f:
157 return f.read()
158 return None
159
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800160 def WarningMessage(self, target_test_image=None):
Jon Salz4f3ade52014-02-20 17:55:09 +0800161 with open(os.path.join(self._src, 'VERSION')) as f:
162 ret = f.read()
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800163 if target_test_image:
Jon Salz4f3ade52014-02-20 17:55:09 +0800164 ret += (
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800165 '\n'
166 '\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800167 '*** You are about to patch the factory toolkit into:\n'
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800168 '*** %s\n'
169 '***' % target_test_image)
170 else:
Jon Salz4f3ade52014-02-20 17:55:09 +0800171 ret += (
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800172 '\n'
173 '\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800174 '*** You are about to install the factory toolkit to:\n'
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800175 '*** %s\n'
176 '***' % self._dest)
177 if self._dest == self._system_root:
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800178 if self._no_enable:
179 ret += ('\n'
180 '*** Factory tests will be disabled after this process is done, but\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800181 '*** you can enable them by creating the factory enabled tag:\n'
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800182 '*** %s\n'
183 '***' % self._tag_file)
184 else:
185 ret += ('\n'
186 '*** After this process is done, your device will start factory\n'
187 '*** tests on the next reboot.\n'
188 '***\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800189 '*** Factory tests can be disabled by deleting the factory enabled\n'
190 '*** tag:\n'
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800191 '*** %s\n'
192 '***' % self._tag_file)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800193 return ret
194
Vic Yang7039f422014-07-07 15:38:13 -0700195 def _SetTagFile(self, name, path, enabled):
196 """Install or remove a tag file."""
197 if enabled:
198 print '*** Installing %s enabled tag...' % name
199 Spawn(['touch', path], sudo=True, log=True, check_call=True)
Ricky Liang8c88a122014-07-11 21:21:22 +0800200 Spawn(['chmod', 'go+r', path], sudo=True, log=True, check_call=True)
Vic Yang7039f422014-07-07 15:38:13 -0700201 else:
202 print '*** Removing %s enabled tag...' % name
203 Spawn(['rm', '-f', path], sudo=True, log=True, check_call=True)
204
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800205 def Install(self):
206 print '*** Installing factory toolkit...'
Jon Salzb7e44262014-05-07 15:53:37 +0800207 for src, dest in ((self._usr_local_src, self._usr_local_dest),
208 (self._var_src, self._var_dest)):
209 # Change the source directory to root, and add group/world read
210 # permissions. This is necessary because when the toolkit was
211 # unpacked, the user may not have been root so the permessions
212 # may be hosed. This is skipped for testing.
Peter Ammon5ac58422014-06-09 14:45:50 -0700213 # --force is necessary to allow goofy directory from prior
214 # toolkit installations to be overwritten by the goofy symlink.
Ricky Liang5e95be22014-07-09 12:52:07 +0800215 try:
216 if self._sudo:
217 Spawn(['chown', '-R', 'root', src],
218 sudo=True, log=True, check_call=True)
219 Spawn(['chmod', '-R', 'go+rX', src],
220 sudo=True, log=True, check_call=True)
221 print '*** %s -> %s' % (src, dest)
222 Spawn(['rsync', '-a', '--force', src + '/', dest],
223 sudo=self._sudo, log=True, check_output=True)
224 finally:
225 # Need to change the source directory back to the original user, or the
226 # script in makeself will fail to remove the temporary source directory.
227 if self._sudo:
228 myuser = os.environ.get('USER')
229 Spawn(['chown', '-R', myuser, src],
230 sudo=True, log=True, check_call=True)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800231
Jon Salz25590302014-07-11 16:07:20 +0800232 print '*** Installing symlinks...'
233 install_symlinks.InstallSymlinks(
234 '../factory/bin',
235 os.path.join(self._usr_local_dest, 'bin'),
236 install_symlinks.MODE_FULL,
237 sudo=self._sudo)
238
239 print '*** Removing factory-mini...'
240 Spawn(['rm', '-rf', os.path.join(self._usr_local_dest, 'factory-mini')],
241 sudo=self._sudo, log=True, check_call=True)
242
Vic Yang7039f422014-07-07 15:38:13 -0700243 self._SetTagFile('factory', self._tag_file, not self._no_enable)
Peter Ammon948b7172014-07-15 12:43:06 -0700244 self._SetTagFile('presenter', self._presenter_tag_file,
245 self._enable_presenter)
Vic Yang7039f422014-07-07 15:38:13 -0700246 self._SetTagFile('device', self._device_tag_file, self._enable_device)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800247
248 print '*** Installation completed.'
249
250
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800251@contextmanager
252def DummyContext(arg):
253 """A context manager that simply yields its argument."""
254 yield arg
255
256
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800257def PrintBuildInfo(src_root):
258 """Print build information."""
259 info_file = os.path.join(src_root, 'REPO_STATUS')
260 if not os.path.exists(info_file):
261 raise OSError('Build info file not found!')
262 with open(info_file, 'r') as f:
263 print f.read()
264
265
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800266def PackFactoryToolkit(src_root, output_path):
267 """Packs the files containing this script into a factory toolkit."""
268 with open(os.path.join(src_root, 'VERSION'), 'r') as f:
269 version = f.read().strip()
Jon Salz4f3ade52014-02-20 17:55:09 +0800270 with tempfile.NamedTemporaryFile() as help_header:
271 help_header.write(version + "\n" + HELP_HEADER + HELP_HEADER_MAKESELF)
272 help_header.flush()
273 Spawn([os.path.join(src_root, 'makeself.sh'), '--bzip2', '--nox11',
274 '--help-header', help_header.name,
275 src_root, output_path, version, INSTALLER_PATH, '--in-exe'],
276 check_call=True, log=True)
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800277 print ('\n'
278 ' Factory toolkit generated at %s.\n'
279 '\n'
280 ' To install factory toolkit on a live device running a test image,\n'
281 ' copy this to the device and execute it as root.\n'
282 '\n'
283 ' Alternatively, the factory toolkit can be used to patch a test\n'
284 ' image. For more information, run:\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800285 ' %s --help\n'
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800286 '\n' % (output_path, output_path))
287
288
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800289def main():
Jon Salz4f3ade52014-02-20 17:55:09 +0800290 import logging
291 logging.basicConfig(level=logging.INFO)
292
293 # In order to determine which usage message to show, first determine
294 # whether we're in the self-extracting archive. Do this first
295 # because we need it to even parse the arguments.
296 if '--in-exe' in sys.argv:
297 sys.argv = [x for x in sys.argv if x != '--in-exe']
298 in_archive = True
299 else:
300 in_archive = False
301
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800302 parser = argparse.ArgumentParser(
Jon Salz4f3ade52014-02-20 17:55:09 +0800303 description=HELP_HEADER + HELP_HEADER_ADVANCED,
304 usage=('install_factory_toolkit.run -- [options]' if in_archive
305 else None),
306 formatter_class=argparse.RawDescriptionHelpFormatter)
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800307 parser.add_argument('dest', nargs='?', default='/',
308 help='A test image or the mount point of the stateful partition. '
309 "If omitted, install to live system, i.e. '/'.")
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800310 parser.add_argument('--no-enable', '-n', action='store_true',
311 help="Don't enable factory tests after installing")
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800312 parser.add_argument('--yes', '-y', action='store_true',
313 help="Don't ask for confirmation")
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800314 parser.add_argument('--build-info', action='store_true',
315 help="Print build information and exit")
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800316 parser.add_argument('--pack-into', metavar='NEW_TOOLKIT',
317 help="Pack the files into a new factory toolkit")
318 parser.add_argument('--repack', metavar='UNPACKED_TOOLKIT',
319 help="Repack from previously unpacked toolkit")
Vic Yang7039f422014-07-07 15:38:13 -0700320
Peter Ammon948b7172014-07-15 12:43:06 -0700321 parser.add_argument('--enable-presenter', dest='enable_presenter',
Vic Yang7039f422014-07-07 15:38:13 -0700322 action='store_true',
Peter Ammon948b7172014-07-15 12:43:06 -0700323 help="Run goofy in presenter mode on startup")
324 parser.add_argument('--no-enable-presenter', dest='enable_presenter',
Vic Yang7039f422014-07-07 15:38:13 -0700325 action='store_false', help=argparse.SUPPRESS)
Peter Ammon948b7172014-07-15 12:43:06 -0700326 parser.set_defaults(enable_presenter=False)
Vic Yang7039f422014-07-07 15:38:13 -0700327
328 parser.add_argument('--enable-device', dest='enable_device',
329 action='store_true',
Peter Ammon948b7172014-07-15 12:43:06 -0700330 help="Run goofy in device mode on startup")
Vic Yang7039f422014-07-07 15:38:13 -0700331 parser.add_argument('--no-enable-device', dest='enable_device',
332 action='store_false', help=argparse.SUPPRESS)
Peter Ammon948b7172014-07-15 12:43:06 -0700333 parser.set_defaults(enable_device=True)
Vic Yang7039f422014-07-07 15:38:13 -0700334
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800335 args = parser.parse_args()
336
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800337 src_root = factory.FACTORY_PATH
338 for _ in xrange(3):
339 src_root = os.path.dirname(src_root)
340
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800341 # --pack-into may be called directly so this must be done before changing
342 # working directory to OLDPWD.
343 if args.pack_into and args.repack is None:
344 PackFactoryToolkit(src_root, args.pack_into)
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800345 return
346
Jon Salz4f3ade52014-02-20 17:55:09 +0800347 if not in_archive:
348 # If you're not in the self-extracting archive, you're not allowed to
349 # do anything except the above --pack-into call.
350 parser.error('Not running from install_factory_toolkit.run; '
351 'only --pack-into (without --repack) is allowed')
352
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800353 # Change to original working directory in case the user specifies
354 # a relative path.
355 # TODO: Use USER_PWD instead when makeself is upgraded
356 os.chdir(os.environ['OLDPWD'])
357
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800358 if args.repack:
359 if args.pack_into is None:
360 parser.error('Must specify --pack-into when using --repack.')
361 Spawn([os.path.join(args.repack, INSTALLER_PATH),
362 '--pack-into', args.pack_into], check_call=True, log=True)
363 return
364
365 if args.build_info:
366 PrintBuildInfo(src_root)
367 return
368
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800369 if not os.path.exists(args.dest):
370 parser.error('Destination %s does not exist!' % args.dest)
371
372 patch_test_image = os.path.isfile(args.dest)
373
374 with (MountPartition(args.dest, 1, rw=True) if patch_test_image
375 else DummyContext(args.dest)) as dest:
Peter Ammon948b7172014-07-15 12:43:06 -0700376 installer = FactoryToolkitInstaller(src_root, dest, args.no_enable,
377 args.enable_presenter, args.enable_device)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800378
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800379 print installer.WarningMessage(args.dest if patch_test_image else None)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800380
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800381 if not args.yes:
382 answer = raw_input('*** Continue? [y/N] ')
383 if not answer or answer[0] not in 'yY':
384 sys.exit('Aborting.')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800385
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800386 installer.Install()
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800387
388if __name__ == '__main__':
389 main()