blob: 166c4274a122d6080db6ac893962379c4fed61d0 [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 (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080095 def __init__(self, src, dest, no_enable, system_root='/'):
Jon Salz4f3ade52014-02-20 17:55:09 +080096 self._src = src
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080097 self._system_root = system_root
98 if dest == self._system_root:
99 self._usr_local_dest = os.path.join(dest, 'usr', 'local')
100 self._var_dest = os.path.join(dest, 'var')
Jon Salz4f3ade52014-02-20 17:55:09 +0800101
102 # Make sure we're on a CrOS device.
103 lsb_release = self._ReadLSBRelease()
104 is_cros = (
105 lsb_release and
106 re.match('^CHROMEOS_RELEASE', lsb_release, re.MULTILINE) is not None)
107
108 if not is_cros:
109 sys.stderr.write(
110 "ERROR: You're not on a CrOS device (/etc/lsb-release does not\n"
111 "contain CHROMEOS_RELEASE), so you must specify a test image or a\n"
112 "mounted stateful partition on which to install the factory\n"
113 "toolkit. Please run\n"
114 "\n"
115 " install_factory_toolkit.run -- --help\n"
116 "\n"
117 "for help.\n")
118 sys.exit(1)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800119 if os.getuid() != 0:
Jon Salz4f3ade52014-02-20 17:55:09 +0800120 raise Exception('You must be root to install the factory toolkit on a '
121 'CrOS device.')
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800122 else:
123 self._usr_local_dest = os.path.join(dest, 'dev_image')
124 self._var_dest = os.path.join(dest, 'var_overlay')
125 if (not os.path.exists(self._usr_local_dest) or
126 not os.path.exists(self._var_dest)):
127 raise Exception(
128 'The destination path %s is not a stateful partition!' % dest)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800129
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800130 self._dest = dest
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800131 self._usr_local_src = os.path.join(src, 'usr', 'local')
132 self._var_src = os.path.join(src, 'var')
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800133 self._no_enable = no_enable
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800134 self._tag_file = os.path.join(self._usr_local_dest, 'factory', 'enabled')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800135
136 if (not os.path.exists(self._usr_local_src) or
137 not os.path.exists(self._var_src)):
138 raise Exception(
139 'This installer must be run from within the factory toolkit!')
140
Jon Salz4f3ade52014-02-20 17:55:09 +0800141 @staticmethod
142 def _ReadLSBRelease():
143 """Returns the contents of /etc/lsb-release, or None if it does not
144 exist."""
145 if os.path.exists('/etc/lsb-release'):
146 with open('/etc/lsb-release') as f:
147 return f.read()
148 return None
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 (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800185 def Install(self):
186 print '*** Installing factory toolkit...'
Jon Salzb7e44262014-05-07 15:53:37 +0800187 for src, dest in ((self._usr_local_src, self._usr_local_dest),
188 (self._var_src, self._var_dest)):
189 # Change the source directory to root, and add group/world read
190 # permissions. This is necessary because when the toolkit was
191 # unpacked, the user may not have been root so the permessions
192 # may be hosed. This is skipped for testing.
Peter Ammon5ac58422014-06-09 14:45:50 -0700193 # --force is necessary to allow goofy directory from prior
194 # toolkit installations to be overwritten by the goofy symlink.
Jon Salzb7e44262014-05-07 15:53:37 +0800195 if self._sudo:
196 Spawn(['chown', '-R', 'root', src],
197 sudo=True, log=True, check_call=True)
198 Spawn(['chmod', '-R', 'go+rX', src],
199 sudo=True, log=True, check_call=True)
200 print '*** %s -> %s' % (src, dest)
Peter Ammon5ac58422014-06-09 14:45:50 -0700201 Spawn(['rsync', '-a', '--force', src + '/', dest],
Jon Salzb7e44262014-05-07 15:53:37 +0800202 sudo=self._sudo, log=True, check_output=True)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800203
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800204 if self._no_enable:
205 print '*** Removing factory enabled tag...'
Jon Salzb7e44262014-05-07 15:53:37 +0800206 Spawn(['rm', '-f', self._tag_file], sudo=True, log=True, check_call=True)
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800207 else:
208 print '*** Installing factory enabled tag...'
Jon Salzb7e44262014-05-07 15:53:37 +0800209 Spawn(['touch', self._tag_file], sudo=True, log=True, check_call=True)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800210
211 print '*** Installation completed.'
212
213
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800214@contextmanager
215def DummyContext(arg):
216 """A context manager that simply yields its argument."""
217 yield arg
218
219
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800220def PrintBuildInfo(src_root):
221 """Print build information."""
222 info_file = os.path.join(src_root, 'REPO_STATUS')
223 if not os.path.exists(info_file):
224 raise OSError('Build info file not found!')
225 with open(info_file, 'r') as f:
226 print f.read()
227
228
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800229def PackFactoryToolkit(src_root, output_path):
230 """Packs the files containing this script into a factory toolkit."""
231 with open(os.path.join(src_root, 'VERSION'), 'r') as f:
232 version = f.read().strip()
Jon Salz4f3ade52014-02-20 17:55:09 +0800233 with tempfile.NamedTemporaryFile() as help_header:
234 help_header.write(version + "\n" + HELP_HEADER + HELP_HEADER_MAKESELF)
235 help_header.flush()
236 Spawn([os.path.join(src_root, 'makeself.sh'), '--bzip2', '--nox11',
237 '--help-header', help_header.name,
238 src_root, output_path, version, INSTALLER_PATH, '--in-exe'],
239 check_call=True, log=True)
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800240 print ('\n'
241 ' Factory toolkit generated at %s.\n'
242 '\n'
243 ' To install factory toolkit on a live device running a test image,\n'
244 ' copy this to the device and execute it as root.\n'
245 '\n'
246 ' Alternatively, the factory toolkit can be used to patch a test\n'
247 ' image. For more information, run:\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800248 ' %s --help\n'
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800249 '\n' % (output_path, output_path))
250
251
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800252def main():
Jon Salz4f3ade52014-02-20 17:55:09 +0800253 import logging
254 logging.basicConfig(level=logging.INFO)
255
256 # In order to determine which usage message to show, first determine
257 # whether we're in the self-extracting archive. Do this first
258 # because we need it to even parse the arguments.
259 if '--in-exe' in sys.argv:
260 sys.argv = [x for x in sys.argv if x != '--in-exe']
261 in_archive = True
262 else:
263 in_archive = False
264
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800265 parser = argparse.ArgumentParser(
Jon Salz4f3ade52014-02-20 17:55:09 +0800266 description=HELP_HEADER + HELP_HEADER_ADVANCED,
267 usage=('install_factory_toolkit.run -- [options]' if in_archive
268 else None),
269 formatter_class=argparse.RawDescriptionHelpFormatter)
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800270 parser.add_argument('dest', nargs='?', default='/',
271 help='A test image or the mount point of the stateful partition. '
272 "If omitted, install to live system, i.e. '/'.")
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800273 parser.add_argument('--no-enable', '-n', action='store_true',
274 help="Don't enable factory tests after installing")
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800275 parser.add_argument('--yes', '-y', action='store_true',
276 help="Don't ask for confirmation")
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800277 parser.add_argument('--build-info', action='store_true',
278 help="Print build information and exit")
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800279 parser.add_argument('--pack-into', metavar='NEW_TOOLKIT',
280 help="Pack the files into a new factory toolkit")
281 parser.add_argument('--repack', metavar='UNPACKED_TOOLKIT',
282 help="Repack from previously unpacked toolkit")
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800283 args = parser.parse_args()
284
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800285 src_root = factory.FACTORY_PATH
286 for _ in xrange(3):
287 src_root = os.path.dirname(src_root)
288
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800289 # --pack-into may be called directly so this must be done before changing
290 # working directory to OLDPWD.
291 if args.pack_into and args.repack is None:
292 PackFactoryToolkit(src_root, args.pack_into)
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800293 return
294
Jon Salz4f3ade52014-02-20 17:55:09 +0800295 if not in_archive:
296 # If you're not in the self-extracting archive, you're not allowed to
297 # do anything except the above --pack-into call.
298 parser.error('Not running from install_factory_toolkit.run; '
299 'only --pack-into (without --repack) is allowed')
300
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800301 # Change to original working directory in case the user specifies
302 # a relative path.
303 # TODO: Use USER_PWD instead when makeself is upgraded
304 os.chdir(os.environ['OLDPWD'])
305
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800306 if args.repack:
307 if args.pack_into is None:
308 parser.error('Must specify --pack-into when using --repack.')
309 Spawn([os.path.join(args.repack, INSTALLER_PATH),
310 '--pack-into', args.pack_into], check_call=True, log=True)
311 return
312
313 if args.build_info:
314 PrintBuildInfo(src_root)
315 return
316
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800317 if not os.path.exists(args.dest):
318 parser.error('Destination %s does not exist!' % args.dest)
319
320 patch_test_image = os.path.isfile(args.dest)
321
322 with (MountPartition(args.dest, 1, rw=True) if patch_test_image
323 else DummyContext(args.dest)) as dest:
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800324 installer = FactoryToolkitInstaller(src_root, dest, args.no_enable)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800325
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800326 print installer.WarningMessage(args.dest if patch_test_image else None)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800327
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800328 if not args.yes:
329 answer = raw_input('*** Continue? [y/N] ')
330 if not answer or answer[0] not in 'yY':
331 sys.exit('Aborting.')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800332
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800333 installer.Install()
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800334
335if __name__ == '__main__':
336 main()