blob: 778f888d1ed94d921266c21eca783657582f01de [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
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080024from cros.factory.tools.mount_partition import MountPartition
25from cros.factory.utils.process_utils import Spawn
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080026
27
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +080028INSTALLER_PATH = 'usr/local/factory/py/toolkit/installer.py'
29
Jon Salz4f3ade52014-02-20 17:55:09 +080030# Short and sweet help header for the executable generated by makeself.
31HELP_HEADER = """
32Installs the factory toolkit, transforming a test image into a factory test
33image. You can:
34
35- Install the factory toolkit on a CrOS device that is running a test
36 image. To do this, copy install_factory_toolkit.run to the device and
37 run it. The factory tests will then come up on the next boot.
38
39 rsync -a install_factory_toolkit.run crosdevice:/tmp
40 ssh crosdevice '/tmp/install_factory_toolkit.run && sync && reboot'
41
42- Modify a test image, turning it into a factory test image. When you
43 use the image on a device, the factory tests will come up.
44
45 install_factory_toolkit.run chromiumos_test_image.bin
46"""
47
48HELP_HEADER_ADVANCED = """
49- (advanced) Modify a mounted stateful partition, turning it into a factory
50 test image. This is equivalent to the previous command:
51
52 mount_partition -rw chromiumos_test_image.bin 1 /mnt/stateful
53 install_factory_toolkit.run /mnt/stateful
54 umount /mnt/stateful
55
56- (advanced) Unpack the factory toolkit, modify a file, and then repack it.
57
58 # Unpack but don't actually install
59 install_factory_toolkit.run --target /tmp/toolkit --noexec
60 # Edit some files in /tmp/toolkit
61 emacs /tmp/toolkit/whatever
62 # Repack
63 install_factory_toolkit.run -- --repack /tmp/toolkit \\
64 --pack-into /path/to/new/install_factory_toolkit.run
65"""
66
67# The makeself-generated header comes next. This is a little confusing,
68# so explain.
69HELP_HEADER_MAKESELF = """
70For complete usage information and advanced operations, run
71"install_factory_toolkit.run -- --help" (note the extra "--").
72
73Following is the help message from makeself, which was used to create
74this self-extracting archive.
75
76-----
77"""
78
79
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080080class FactoryToolkitInstaller():
81 """Factory toolkit installer.
82
83 Args:
84 src: Source path containing usr/ and var/.
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080085 dest: Installation destination path. Set this to the mount point of the
86 stateful partition if patching a test image.
87 no_enable: True to not install the tag file.
88 system_root: The path to the root of the file system. This must be left
89 as its default value except for unit testing.
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +080090 """
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080091
Jon Salzb7e44262014-05-07 15:53:37 +080092 # Whether to sudo when rsyncing; set to False for testing.
93 _sudo = True
94
Vic Yang7039f422014-07-07 15:38:13 -070095 def __init__(self, src, dest, no_enable, enable_host,
96 enable_device, system_root='/'):
Jon Salz4f3ade52014-02-20 17:55:09 +080097 self._src = src
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080098 self._system_root = system_root
99 if dest == self._system_root:
100 self._usr_local_dest = os.path.join(dest, 'usr', 'local')
101 self._var_dest = os.path.join(dest, 'var')
Jon Salz4f3ade52014-02-20 17:55:09 +0800102
103 # Make sure we're on a CrOS device.
104 lsb_release = self._ReadLSBRelease()
105 is_cros = (
106 lsb_release and
107 re.match('^CHROMEOS_RELEASE', lsb_release, re.MULTILINE) is not None)
108
109 if not is_cros:
110 sys.stderr.write(
111 "ERROR: You're not on a CrOS device (/etc/lsb-release does not\n"
112 "contain CHROMEOS_RELEASE), so you must specify a test image or a\n"
113 "mounted stateful partition on which to install the factory\n"
114 "toolkit. Please run\n"
115 "\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
Vic Yang7039f422014-07-07 15:38:13 -0700137 self._enable_host = enable_host
Ricky Liangd7716912014-07-10 11:52:24 +0800138 self._host_tag_file = os.path.join(self._usr_local_dest, 'factory',
139 'init', 'run_goofy_host')
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
Jon Salz4f3ade52014-02-20 17:55:09 +0800150 @staticmethod
151 def _ReadLSBRelease():
152 """Returns the contents of /etc/lsb-release, or None if it does not
153 exist."""
154 if os.path.exists('/etc/lsb-release'):
155 with open('/etc/lsb-release') as f:
156 return f.read()
157 return None
158
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800159 def WarningMessage(self, target_test_image=None):
Jon Salz4f3ade52014-02-20 17:55:09 +0800160 with open(os.path.join(self._src, 'VERSION')) as f:
161 ret = f.read()
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800162 if target_test_image:
Jon Salz4f3ade52014-02-20 17:55:09 +0800163 ret += (
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800164 '\n'
165 '\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800166 '*** You are about to patch the factory toolkit into:\n'
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800167 '*** %s\n'
168 '***' % target_test_image)
169 else:
Jon Salz4f3ade52014-02-20 17:55:09 +0800170 ret += (
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800171 '\n'
172 '\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800173 '*** You are about to install the factory toolkit to:\n'
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800174 '*** %s\n'
175 '***' % self._dest)
176 if self._dest == self._system_root:
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800177 if self._no_enable:
178 ret += ('\n'
179 '*** Factory tests will be disabled after this process is done, but\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800180 '*** you can enable them by creating the factory enabled tag:\n'
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800181 '*** %s\n'
182 '***' % self._tag_file)
183 else:
184 ret += ('\n'
185 '*** After this process is done, your device will start factory\n'
186 '*** tests on the next reboot.\n'
187 '***\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800188 '*** Factory tests can be disabled by deleting the factory enabled\n'
189 '*** tag:\n'
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800190 '*** %s\n'
191 '***' % self._tag_file)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800192 return ret
193
Vic Yang7039f422014-07-07 15:38:13 -0700194 def _SetTagFile(self, name, path, enabled):
195 """Install or remove a tag file."""
196 if enabled:
197 print '*** Installing %s enabled tag...' % name
198 Spawn(['touch', path], sudo=True, log=True, check_call=True)
Ricky Liang8c88a122014-07-11 21:21:22 +0800199 Spawn(['chmod', 'go+r', path], sudo=True, log=True, check_call=True)
Vic Yang7039f422014-07-07 15:38:13 -0700200 else:
201 print '*** Removing %s enabled tag...' % name
202 Spawn(['rm', '-f', path], sudo=True, log=True, check_call=True)
203
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800204 def Install(self):
205 print '*** Installing factory toolkit...'
Jon Salzb7e44262014-05-07 15:53:37 +0800206 for src, dest in ((self._usr_local_src, self._usr_local_dest),
207 (self._var_src, self._var_dest)):
208 # Change the source directory to root, and add group/world read
209 # permissions. This is necessary because when the toolkit was
210 # unpacked, the user may not have been root so the permessions
211 # may be hosed. This is skipped for testing.
Peter Ammon5ac58422014-06-09 14:45:50 -0700212 # --force is necessary to allow goofy directory from prior
213 # toolkit installations to be overwritten by the goofy symlink.
Ricky Liang5e95be22014-07-09 12:52:07 +0800214 try:
215 if self._sudo:
216 Spawn(['chown', '-R', 'root', src],
217 sudo=True, log=True, check_call=True)
218 Spawn(['chmod', '-R', 'go+rX', src],
219 sudo=True, log=True, check_call=True)
220 print '*** %s -> %s' % (src, dest)
221 Spawn(['rsync', '-a', '--force', src + '/', dest],
222 sudo=self._sudo, log=True, check_output=True)
223 finally:
224 # Need to change the source directory back to the original user, or the
225 # script in makeself will fail to remove the temporary source directory.
226 if self._sudo:
227 myuser = os.environ.get('USER')
228 Spawn(['chown', '-R', myuser, src],
229 sudo=True, log=True, check_call=True)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800230
Vic Yang7039f422014-07-07 15:38:13 -0700231 self._SetTagFile('factory', self._tag_file, not self._no_enable)
232 self._SetTagFile('host', self._host_tag_file, self._enable_host)
233 self._SetTagFile('device', self._device_tag_file, self._enable_device)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800234
235 print '*** Installation completed.'
236
237
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800238@contextmanager
239def DummyContext(arg):
240 """A context manager that simply yields its argument."""
241 yield arg
242
243
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800244def PrintBuildInfo(src_root):
245 """Print build information."""
246 info_file = os.path.join(src_root, 'REPO_STATUS')
247 if not os.path.exists(info_file):
248 raise OSError('Build info file not found!')
249 with open(info_file, 'r') as f:
250 print f.read()
251
252
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800253def PackFactoryToolkit(src_root, output_path):
254 """Packs the files containing this script into a factory toolkit."""
255 with open(os.path.join(src_root, 'VERSION'), 'r') as f:
256 version = f.read().strip()
Jon Salz4f3ade52014-02-20 17:55:09 +0800257 with tempfile.NamedTemporaryFile() as help_header:
258 help_header.write(version + "\n" + HELP_HEADER + HELP_HEADER_MAKESELF)
259 help_header.flush()
260 Spawn([os.path.join(src_root, 'makeself.sh'), '--bzip2', '--nox11',
261 '--help-header', help_header.name,
262 src_root, output_path, version, INSTALLER_PATH, '--in-exe'],
263 check_call=True, log=True)
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800264 print ('\n'
265 ' Factory toolkit generated at %s.\n'
266 '\n'
267 ' To install factory toolkit on a live device running a test image,\n'
268 ' copy this to the device and execute it as root.\n'
269 '\n'
270 ' Alternatively, the factory toolkit can be used to patch a test\n'
271 ' image. For more information, run:\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800272 ' %s --help\n'
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800273 '\n' % (output_path, output_path))
274
275
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800276def main():
Jon Salz4f3ade52014-02-20 17:55:09 +0800277 import logging
278 logging.basicConfig(level=logging.INFO)
279
280 # In order to determine which usage message to show, first determine
281 # whether we're in the self-extracting archive. Do this first
282 # because we need it to even parse the arguments.
283 if '--in-exe' in sys.argv:
284 sys.argv = [x for x in sys.argv if x != '--in-exe']
285 in_archive = True
286 else:
287 in_archive = False
288
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800289 parser = argparse.ArgumentParser(
Jon Salz4f3ade52014-02-20 17:55:09 +0800290 description=HELP_HEADER + HELP_HEADER_ADVANCED,
291 usage=('install_factory_toolkit.run -- [options]' if in_archive
292 else None),
293 formatter_class=argparse.RawDescriptionHelpFormatter)
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800294 parser.add_argument('dest', nargs='?', default='/',
295 help='A test image or the mount point of the stateful partition. '
296 "If omitted, install to live system, i.e. '/'.")
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800297 parser.add_argument('--no-enable', '-n', action='store_true',
298 help="Don't enable factory tests after installing")
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800299 parser.add_argument('--yes', '-y', action='store_true',
300 help="Don't ask for confirmation")
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800301 parser.add_argument('--build-info', action='store_true',
302 help="Print build information and exit")
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800303 parser.add_argument('--pack-into', metavar='NEW_TOOLKIT',
304 help="Pack the files into a new factory toolkit")
305 parser.add_argument('--repack', metavar='UNPACKED_TOOLKIT',
306 help="Repack from previously unpacked toolkit")
Vic Yang7039f422014-07-07 15:38:13 -0700307
308 parser.add_argument('--enable-host', dest='enable_host',
309 action='store_true',
310 help="Run goofy host on startup")
311 parser.add_argument('--no-enable-host', dest='enable_host',
312 action='store_false', help=argparse.SUPPRESS)
313 parser.set_defaults(enable_host=True)
314
315 parser.add_argument('--enable-device', dest='enable_device',
316 action='store_true',
317 help="Run goofy_device on startup")
318 parser.add_argument('--no-enable-device', dest='enable_device',
319 action='store_false', help=argparse.SUPPRESS)
320 parser.set_defaults(enable_device=False)
321
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800322 args = parser.parse_args()
323
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800324 src_root = factory.FACTORY_PATH
325 for _ in xrange(3):
326 src_root = os.path.dirname(src_root)
327
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800328 # --pack-into may be called directly so this must be done before changing
329 # working directory to OLDPWD.
330 if args.pack_into and args.repack is None:
331 PackFactoryToolkit(src_root, args.pack_into)
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800332 return
333
Jon Salz4f3ade52014-02-20 17:55:09 +0800334 if not in_archive:
335 # If you're not in the self-extracting archive, you're not allowed to
336 # do anything except the above --pack-into call.
337 parser.error('Not running from install_factory_toolkit.run; '
338 'only --pack-into (without --repack) is allowed')
339
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800340 # Change to original working directory in case the user specifies
341 # a relative path.
342 # TODO: Use USER_PWD instead when makeself is upgraded
343 os.chdir(os.environ['OLDPWD'])
344
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800345 if args.repack:
346 if args.pack_into is None:
347 parser.error('Must specify --pack-into when using --repack.')
348 Spawn([os.path.join(args.repack, INSTALLER_PATH),
349 '--pack-into', args.pack_into], check_call=True, log=True)
350 return
351
352 if args.build_info:
353 PrintBuildInfo(src_root)
354 return
355
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800356 if not os.path.exists(args.dest):
357 parser.error('Destination %s does not exist!' % args.dest)
358
359 patch_test_image = os.path.isfile(args.dest)
360
361 with (MountPartition(args.dest, 1, rw=True) if patch_test_image
362 else DummyContext(args.dest)) as dest:
Vic Yang7039f422014-07-07 15:38:13 -0700363 installer = FactoryToolkitInstaller(
364 src_root, dest, args.no_enable, args.enable_host, args.enable_device)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800365
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800366 print installer.WarningMessage(args.dest if patch_test_image else None)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800367
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800368 if not args.yes:
369 answer = raw_input('*** Continue? [y/N] ')
370 if not answer or answer[0] not in 'yY':
371 sys.exit('Aborting.')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800372
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800373 installer.Install()
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800374
375if __name__ == '__main__':
376 main()