blob: 17e701415499f0d5e95d99b2df3530aff0ab65a3 [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
92 def __init__(self, src, dest, no_enable, system_root='/'):
Jon Salz4f3ade52014-02-20 17:55:09 +080093 self._src = src
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +080094 self._system_root = system_root
95 if dest == self._system_root:
96 self._usr_local_dest = os.path.join(dest, 'usr', 'local')
97 self._var_dest = os.path.join(dest, 'var')
Jon Salz4f3ade52014-02-20 17:55:09 +080098
99 # Make sure we're on a CrOS device.
100 lsb_release = self._ReadLSBRelease()
101 is_cros = (
102 lsb_release and
103 re.match('^CHROMEOS_RELEASE', lsb_release, re.MULTILINE) is not None)
104
105 if not is_cros:
106 sys.stderr.write(
107 "ERROR: You're not on a CrOS device (/etc/lsb-release does not\n"
108 "contain CHROMEOS_RELEASE), so you must specify a test image or a\n"
109 "mounted stateful partition on which to install the factory\n"
110 "toolkit. Please run\n"
111 "\n"
112 " install_factory_toolkit.run -- --help\n"
113 "\n"
114 "for help.\n")
115 sys.exit(1)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800116 if os.getuid() != 0:
Jon Salz4f3ade52014-02-20 17:55:09 +0800117 raise Exception('You must be root to install the factory toolkit on a '
118 'CrOS device.')
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800119 else:
120 self._usr_local_dest = os.path.join(dest, 'dev_image')
121 self._var_dest = os.path.join(dest, 'var_overlay')
122 if (not os.path.exists(self._usr_local_dest) or
123 not os.path.exists(self._var_dest)):
124 raise Exception(
125 'The destination path %s is not a stateful partition!' % dest)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800126
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800127 self._dest = dest
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800128 self._usr_local_src = os.path.join(src, 'usr', 'local')
129 self._var_src = os.path.join(src, 'var')
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800130 self._no_enable = no_enable
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800131 self._tag_file = os.path.join(self._usr_local_dest, 'factory', 'enabled')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800132
133 if (not os.path.exists(self._usr_local_src) or
134 not os.path.exists(self._var_src)):
135 raise Exception(
136 'This installer must be run from within the factory toolkit!')
137
Jon Salz4f3ade52014-02-20 17:55:09 +0800138 @staticmethod
139 def _ReadLSBRelease():
140 """Returns the contents of /etc/lsb-release, or None if it does not
141 exist."""
142 if os.path.exists('/etc/lsb-release'):
143 with open('/etc/lsb-release') as f:
144 return f.read()
145 return None
146
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800147 def WarningMessage(self, target_test_image=None):
Jon Salz4f3ade52014-02-20 17:55:09 +0800148 with open(os.path.join(self._src, 'VERSION')) as f:
149 ret = f.read()
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800150 if target_test_image:
Jon Salz4f3ade52014-02-20 17:55:09 +0800151 ret += (
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800152 '\n'
153 '\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800154 '*** You are about to patch the factory toolkit into:\n'
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800155 '*** %s\n'
156 '***' % target_test_image)
157 else:
Jon Salz4f3ade52014-02-20 17:55:09 +0800158 ret += (
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800159 '\n'
160 '\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800161 '*** You are about to install the factory toolkit to:\n'
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800162 '*** %s\n'
163 '***' % self._dest)
164 if self._dest == self._system_root:
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800165 if self._no_enable:
166 ret += ('\n'
167 '*** Factory tests will be disabled after this process is done, but\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800168 '*** you can enable them by creating the factory enabled tag:\n'
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800169 '*** %s\n'
170 '***' % self._tag_file)
171 else:
172 ret += ('\n'
173 '*** After this process is done, your device will start factory\n'
174 '*** tests on the next reboot.\n'
175 '***\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800176 '*** Factory tests can be disabled by deleting the factory enabled\n'
177 '*** tag:\n'
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800178 '*** %s\n'
179 '***' % self._tag_file)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800180 return ret
181
182 def _Rsync(self, src, dest):
183 print '*** %s -> %s' % (src, dest)
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800184 Spawn(['rsync', '-a', src + '/', dest],
185 sudo=True, log=True, check_output=True)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800186
187 def Install(self):
188 print '*** Installing factory toolkit...'
189 self._Rsync(self._usr_local_src, self._usr_local_dest)
190 self._Rsync(self._var_src, self._var_dest)
191
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800192 if self._no_enable:
193 print '*** Removing factory enabled tag...'
194 try:
195 os.unlink(self._tag_file)
196 except OSError:
197 pass
198 else:
199 print '*** Installing factory enabled tag...'
200 open(self._tag_file, 'w').close()
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800201
202 print '*** Installation completed.'
203
204
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800205@contextmanager
206def DummyContext(arg):
207 """A context manager that simply yields its argument."""
208 yield arg
209
210
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800211def PrintBuildInfo(src_root):
212 """Print build information."""
213 info_file = os.path.join(src_root, 'REPO_STATUS')
214 if not os.path.exists(info_file):
215 raise OSError('Build info file not found!')
216 with open(info_file, 'r') as f:
217 print f.read()
218
219
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800220def PackFactoryToolkit(src_root, output_path):
221 """Packs the files containing this script into a factory toolkit."""
222 with open(os.path.join(src_root, 'VERSION'), 'r') as f:
223 version = f.read().strip()
Jon Salz4f3ade52014-02-20 17:55:09 +0800224 with tempfile.NamedTemporaryFile() as help_header:
225 help_header.write(version + "\n" + HELP_HEADER + HELP_HEADER_MAKESELF)
226 help_header.flush()
227 Spawn([os.path.join(src_root, 'makeself.sh'), '--bzip2', '--nox11',
228 '--help-header', help_header.name,
229 src_root, output_path, version, INSTALLER_PATH, '--in-exe'],
230 check_call=True, log=True)
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800231 print ('\n'
232 ' Factory toolkit generated at %s.\n'
233 '\n'
234 ' To install factory toolkit on a live device running a test image,\n'
235 ' copy this to the device and execute it as root.\n'
236 '\n'
237 ' Alternatively, the factory toolkit can be used to patch a test\n'
238 ' image. For more information, run:\n'
Jon Salz4f3ade52014-02-20 17:55:09 +0800239 ' %s --help\n'
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800240 '\n' % (output_path, output_path))
241
242
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800243def main():
Jon Salz4f3ade52014-02-20 17:55:09 +0800244 import logging
245 logging.basicConfig(level=logging.INFO)
246
247 # In order to determine which usage message to show, first determine
248 # whether we're in the self-extracting archive. Do this first
249 # because we need it to even parse the arguments.
250 if '--in-exe' in sys.argv:
251 sys.argv = [x for x in sys.argv if x != '--in-exe']
252 in_archive = True
253 else:
254 in_archive = False
255
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800256 parser = argparse.ArgumentParser(
Jon Salz4f3ade52014-02-20 17:55:09 +0800257 description=HELP_HEADER + HELP_HEADER_ADVANCED,
258 usage=('install_factory_toolkit.run -- [options]' if in_archive
259 else None),
260 formatter_class=argparse.RawDescriptionHelpFormatter)
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800261 parser.add_argument('dest', nargs='?', default='/',
262 help='A test image or the mount point of the stateful partition. '
263 "If omitted, install to live system, i.e. '/'.")
Vic (Chun-Ju) Yang7cc3e672014-01-20 14:06:39 +0800264 parser.add_argument('--no-enable', '-n', action='store_true',
265 help="Don't enable factory tests after installing")
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800266 parser.add_argument('--yes', '-y', action='store_true',
267 help="Don't ask for confirmation")
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800268 parser.add_argument('--build-info', action='store_true',
269 help="Print build information and exit")
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800270 parser.add_argument('--pack-into', metavar='NEW_TOOLKIT',
271 help="Pack the files into a new factory toolkit")
272 parser.add_argument('--repack', metavar='UNPACKED_TOOLKIT',
273 help="Repack from previously unpacked toolkit")
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800274 args = parser.parse_args()
275
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800276 src_root = factory.FACTORY_PATH
277 for _ in xrange(3):
278 src_root = os.path.dirname(src_root)
279
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800280 # --pack-into may be called directly so this must be done before changing
281 # working directory to OLDPWD.
282 if args.pack_into and args.repack is None:
283 PackFactoryToolkit(src_root, args.pack_into)
Vic (Chun-Ju) Yang98b4fbc2014-02-18 19:32:32 +0800284 return
285
Jon Salz4f3ade52014-02-20 17:55:09 +0800286 if not in_archive:
287 # If you're not in the self-extracting archive, you're not allowed to
288 # do anything except the above --pack-into call.
289 parser.error('Not running from install_factory_toolkit.run; '
290 'only --pack-into (without --repack) is allowed')
291
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800292 # Change to original working directory in case the user specifies
293 # a relative path.
294 # TODO: Use USER_PWD instead when makeself is upgraded
295 os.chdir(os.environ['OLDPWD'])
296
Vic (Chun-Ju) Yangb7388f72014-02-19 15:22:58 +0800297 if args.repack:
298 if args.pack_into is None:
299 parser.error('Must specify --pack-into when using --repack.')
300 Spawn([os.path.join(args.repack, INSTALLER_PATH),
301 '--pack-into', args.pack_into], check_call=True, log=True)
302 return
303
304 if args.build_info:
305 PrintBuildInfo(src_root)
306 return
307
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800308 if not os.path.exists(args.dest):
309 parser.error('Destination %s does not exist!' % args.dest)
310
311 patch_test_image = os.path.isfile(args.dest)
312
313 with (MountPartition(args.dest, 1, rw=True) if patch_test_image
314 else DummyContext(args.dest)) as dest:
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800315 installer = FactoryToolkitInstaller(src_root, dest, args.no_enable)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800316
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800317 print installer.WarningMessage(args.dest if patch_test_image else None)
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800318
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800319 if not args.yes:
320 answer = raw_input('*** Continue? [y/N] ')
321 if not answer or answer[0] not in 'yY':
322 sys.exit('Aborting.')
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800323
Vic (Chun-Ju) Yang469592b2014-02-18 19:15:41 +0800324 installer.Install()
Vic (Chun-Ju) Yang296871a2014-01-13 12:05:18 +0800325
326if __name__ == '__main__':
327 main()