blob: 01d01e37e01bb7819ab0f59bb20ea9ae139f0670 [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.
193 if self._sudo:
194 Spawn(['chown', '-R', 'root', src],
195 sudo=True, log=True, check_call=True)
196 Spawn(['chmod', '-R', 'go+rX', src],
197 sudo=True, log=True, check_call=True)
198 print '*** %s -> %s' % (src, dest)
199 Spawn(['rsync', '-a', src + '/', dest],
200 sudo=self._sudo, log=True, check_output=True)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800201
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800202 if self._no_enable:
203 print '*** Removing factory enabled tag...'
Jon Salzb7e44262014-05-07 15:53:37 +0800204 Spawn(['rm', '-f', self._tag_file], sudo=True, log=True, check_call=True)
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800205 else:
206 print '*** Installing factory enabled tag...'
Jon Salzb7e44262014-05-07 15:53:37 +0800207 Spawn(['touch', self._tag_file], sudo=True, log=True, check_call=True)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800208
209 print '*** Installation completed.'
210
211
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800212@contextmanager
213def DummyContext(arg):
214 """A context manager that simply yields its argument."""
215 yield arg
216
217
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800218def PrintBuildInfo(src_root):
219 """Print build information."""
220 info_file = os.path.join(src_root, 'REPO_STATUS')
221 if not os.path.exists(info_file):
222 raise OSError('Build info file not found!')
223 with open(info_file, 'r') as f:
224 print f.read()
225
226
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800227def PackFactoryToolkit(src_root, output_path):
228 """Packs the files containing this script into a factory toolkit."""
229 with open(os.path.join(src_root, 'VERSION'), 'r') as f:
230 version = f.read().strip()
Jon Salz4f3ade52014-02-20 17:55:09 +0800231 with tempfile.NamedTemporaryFile() as help_header:
232 help_header.write(version + "\n" + HELP_HEADER + HELP_HEADER_MAKESELF)
233 help_header.flush()
234 Spawn([os.path.join(src_root, 'makeself.sh'), '--bzip2', '--nox11',
235 '--help-header', help_header.name,
236 src_root, output_path, version, INSTALLER_PATH, '--in-exe'],
237 check_call=True, log=True)
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800238 print ('\n'
239 ' Factory toolkit generated at %s.\n'
240 '\n'
241 ' To install factory toolkit on a live device running a test image,\n'
242 ' copy this to the device and execute it as root.\n'
243 '\n'
244 ' Alternatively, the factory toolkit can be used to patch a test\n'
245 ' image. For more information, run:\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800246 ' %s --help\n'
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800247 '\n' % (output_path, output_path))
248
249
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800250def main():
Jon Salz4f3ade52014-02-20 17:55:09 +0800251 import logging
252 logging.basicConfig(level=logging.INFO)
253
254 # In order to determine which usage message to show, first determine
255 # whether we're in the self-extracting archive. Do this first
256 # because we need it to even parse the arguments.
257 if '--in-exe' in sys.argv:
258 sys.argv = [x for x in sys.argv if x != '--in-exe']
259 in_archive = True
260 else:
261 in_archive = False
262
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800263 parser = argparse.ArgumentParser(
Jon Salz4f3ade52014-02-20 17:55:09 +0800264 description=HELP_HEADER + HELP_HEADER_ADVANCED,
265 usage=('install_factory_toolkit.run -- [options]' if in_archive
266 else None),
267 formatter_class=argparse.RawDescriptionHelpFormatter)
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800268 parser.add_argument('dest', nargs='?', default='/',
269 help='A test image or the mount point of the stateful partition. '
270 "If omitted, install to live system, i.e. '/'.")
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800271 parser.add_argument('--no-enable', '-n', action='store_true',
272 help="Don't enable factory tests after installing")
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800273 parser.add_argument('--yes', '-y', action='store_true',
274 help="Don't ask for confirmation")
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800275 parser.add_argument('--build-info', action='store_true',
276 help="Print build information and exit")
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800277 parser.add_argument('--pack-into', metavar='NEW_TOOLKIT',
278 help="Pack the files into a new factory toolkit")
279 parser.add_argument('--repack', metavar='UNPACKED_TOOLKIT',
280 help="Repack from previously unpacked toolkit")
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800281 args = parser.parse_args()
282
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800283 src_root = factory.FACTORY_PATH
284 for _ in xrange(3):
285 src_root = os.path.dirname(src_root)
286
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800287 # --pack-into may be called directly so this must be done before changing
288 # working directory to OLDPWD.
289 if args.pack_into and args.repack is None:
290 PackFactoryToolkit(src_root, args.pack_into)
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800291 return
292
Jon Salz4f3ade52014-02-20 17:55:09 +0800293 if not in_archive:
294 # If you're not in the self-extracting archive, you're not allowed to
295 # do anything except the above --pack-into call.
296 parser.error('Not running from install_factory_toolkit.run; '
297 'only --pack-into (without --repack) is allowed')
298
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800299 # Change to original working directory in case the user specifies
300 # a relative path.
301 # TODO: Use USER_PWD instead when makeself is upgraded
302 os.chdir(os.environ['OLDPWD'])
303
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800304 if args.repack:
305 if args.pack_into is None:
306 parser.error('Must specify --pack-into when using --repack.')
307 Spawn([os.path.join(args.repack, INSTALLER_PATH),
308 '--pack-into', args.pack_into], check_call=True, log=True)
309 return
310
311 if args.build_info:
312 PrintBuildInfo(src_root)
313 return
314
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800315 if not os.path.exists(args.dest):
316 parser.error('Destination %s does not exist!' % args.dest)
317
318 patch_test_image = os.path.isfile(args.dest)
319
320 with (MountPartition(args.dest, 1, rw=True) if patch_test_image
321 else DummyContext(args.dest)) as dest:
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800322 installer = FactoryToolkitInstaller(src_root, dest, args.no_enable)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800323
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800324 print installer.WarningMessage(args.dest if patch_test_image else None)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800325
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800326 if not args.yes:
327 answer = raw_input('*** Continue? [y/N] ')
328 if not answer or answer[0] not in 'yY':
329 sys.exit('Aborting.')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800330
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800331 installer.Install()
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800332
333if __name__ == '__main__':
334 main()