blob: 16b67ea850846081edd0099a22e2e5eaac87ce3d [file] [log] [blame]
Wei-Han Chene97d3532016-03-31 19:22:01 +08001#!/usr/bin/env python
2# -*- coding: UTF-8 -*-
3# Copyright 2016 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"""Trainsition to release state directly without reboot."""
8
9import logging
Wei-Han Chene97d3532016-03-31 19:22:01 +080010import os
11import resource
12import shutil
13import signal
14import tempfile
15import textwrap
16import time
17
18import factory_common # pylint: disable=unused-import
19from cros.factory.gooftool import chroot
Wei-Han Chen0a3320e2016-04-23 01:32:07 +080020from cros.factory.gooftool.common import ExecFactoryPar
Wei-Han Chen9adf9de2016-04-01 19:35:41 +080021from cros.factory.gooftool.common import Util
Wei-Han Chen0a3320e2016-04-23 01:32:07 +080022from cros.factory.test.env import paths
Wei-Han Chene97d3532016-03-31 19:22:01 +080023from cros.factory.utils import process_utils
24from cros.factory.utils import sync_utils
25from cros.factory.utils import sys_utils
26
27
Wei-Han Chen9adf9de2016-04-01 19:35:41 +080028"""Directory of scripts for battery cut-off"""
29SCRIPT_DIR = '/usr/local/factory/sh'
30
Wei-Han Chene97d3532016-03-31 19:22:01 +080031
32def OnError(state_dev, logfile):
33 with sys_utils.MountPartition(state_dev,
34 rw=True,
35 fstype='ext4') as mount_point:
36 shutil.copyfile(logfile,
37 os.path.join(mount_point, os.path.basename(logfile)))
38
39
40def Daemonize(logfile=None):
41 """Starts a daemon process and terminates current process.
42
43 A daemon process will be started, and continue excuting the following codes.
44 The original process that calls this function will be terminated.
45
46 Example::
47
48 def DaemonFunc():
49 Daemonize()
50 # the process calling DaemonFunc is terminated.
51 # the following codes will be executed in a daemon process
52 ...
53
54 If you would like to keep the original process alive, you could fork a child
55 process and let child process start the daemon.
56 """
57 # fork from parent process
58 if os.fork():
59 # stop parent process
60 os._exit(0) # pylint: disable=protected-access
61
62 # decouple from parent process
63 os.chdir('/')
64 os.umask(0)
65 os.setsid()
66
67 # fork again
68 if os.fork():
69 os._exit(0) # pylint: disable=protected-access
70
71 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
72 if maxfd == resource.RLIM_INFINITY:
73 maxfd = 1024
74
75 for fd in xrange(maxfd):
76 try:
77 os.close(fd)
78 except OSError:
79 pass
80
81 # Reopen fd 0 (stdin), 1 (stdout), 2 (stderr) to prevent errors from reading
82 # or writing to these files.
83 # Since we have closed all file descriptors, os.open should open a file with
84 # file descriptor equals to 0
85 os.open('/dev/null', os.O_RDWR)
86 if logfile is None:
87 os.dup2(0, 1) # stdout
88 os.dup2(0, 2) # stderr
89 else:
90 os.open(logfile, os.O_RDWR | os.O_CREAT)
91 os.dup2(1, 2) # stderr
92
93
94def ResetLog(logfile=None):
95 if len(logging.getLogger().handlers) > 0:
96 for handler in logging.getLogger().handlers:
97 logging.getLogger().removeHandler(handler)
98 logging.basicConfig(filename=logfile, level=logging.NOTSET)
99
100
101def WipeInTmpFs(is_fast=None, cutoff_args=None, shopfloor_url=None):
102 """prepare to wipe by pivot root to tmpfs and unmount statefull partition.
103
104 Args:
105 is_fast: whether or not to apply fast wipe.
106 cutoff_args: arguments to be passed to battery_cutoff.sh after wiping.
107 shopfloor_url: for inform_shopfloor.sh
108 """
109
110 logfile = '/tmp/wipe_in_tmpfs.log'
111 Daemonize()
112
113 ResetLog(logfile)
114
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800115 factory_par = paths.GetFactoryPythonArchivePath()
Wei-Han Chene97d3532016-03-31 19:22:01 +0800116
117 new_root = tempfile.mkdtemp(prefix='tmpfs.')
118 binary_deps = [
119 'activate_date', 'backlight_tool', 'busybox', 'cgpt', 'cgpt.bin',
120 'clobber-log', 'clobber-state', 'coreutils', 'crossystem', 'dd',
121 'display_boot_message', 'dumpe2fs', 'ectool', 'flashrom', 'halt',
122 'initctl', 'mkfs.ext4', 'mktemp', 'mosys', 'mount', 'mount-encrypted',
123 'od', 'pango-view', 'pkill', 'pv', 'python', 'reboot', 'setterm', 'sh',
124 'shutdown', 'stop', 'umount', 'vpd', 'wget', 'lsof', ]
125 if os.path.exists('/sbin/frecon'):
126 binary_deps.append('/sbin/frecon')
127 else:
128 binary_deps.append('/usr/bin/ply-image')
129
130 etc_issue = textwrap.dedent("""
131 You are now in tmp file system created for in-place wiping.
132
133 For debugging wiping fails, see log files under
134 /tmp
135 /mnt/stateful_partition/unencrypted
136
137 The log file name should be
138 - wipe_in_tmpfs.log
139 - wipe_init.log
140
141 You can also run scripts under /usr/local/factory/sh for wiping process.
142 """)
143
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800144 util = Util()
145
146 root_disk = util.GetPrimaryDevicePath()
147 release_rootfs = util.GetReleaseRootPartitionPath()
148 state_dev = util.GetPrimaryDevicePath(1)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800149 wipe_args = 'factory' + (' fast' if is_fast else '')
150
151 logging.debug('state_dev: %s', state_dev)
152 logging.debug('factory_par: %s', factory_par)
153
154 old_root = 'old_root'
155
156 try:
157 with chroot.TmpChroot(
158 new_root,
159 file_dir_list=[
160 '/bin', '/etc', '/lib', '/lib64', '/opt', '/root', '/sbin',
161 '/usr/share/fonts/notocjk',
162 '/usr/share/cache/fontconfig',
163 '/usr/share/chromeos-assets/images',
164 '/usr/share/chromeos-assets/text/boot_messages',
165 '/usr/share/misc/chromeos-common.sh',
166 '/usr/local/factory/sh',
167 factory_par],
168 binary_list=binary_deps, etc_issue=etc_issue).PivotRoot(old_root):
169 logging.debug(
170 'lsof: %s',
171 process_utils.SpawnOutput('lsof -p %d' % os.getpid(), shell=True))
172
Wei-Han Chene97d3532016-03-31 19:22:01 +0800173 process_utils.Spawn(['sync'], call=True)
174 time.sleep(3)
175
176 # Restart gooftool under new root. Since current gooftool might be using
177 # some resource under stateful partition, restarting gooftool ensures that
178 # everything new gooftool is using comes from tmpfs and we can safely
179 # unmount stateful partition.
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800180 ExecFactoryPar('gooftool', 'wipe_init',
181 '--wipe_args', wipe_args,
182 '--cutoff_args', cutoff_args,
183 '--shopfloor_url', shopfloor_url,
184 '--state_dev', state_dev,
185 '--release_rootfs', release_rootfs,
186 '--root_disk', root_disk,
187 '--old_root', old_root)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800188 raise RuntimeError('Should not reach here')
189 except: # pylint: disable=bare-except
190 logging.exception('wipe_in_place failed')
191 OnError(state_dev, logfile)
192 raise
193
194
195def _StopAllUpstartJobs(exclude_list=None):
196 logging.debug('stopping upstart jobs')
197
198 # Try three times to stop running services because some service will respawn
199 # one time after being stopped, e.g. shill_respawn. Two times should be enough
200 # to stop shill. Adding one more try for safety.
201
202 if exclude_list is None:
203 exclude_list = []
204
205 for unused_tries in xrange(3):
206 service_list = process_utils.SpawnOutput(['initctl', 'list']).splitlines()
207 service_list = [
208 line.split()[0] for line in service_list if 'start/running' in line]
209 for service in service_list:
210 if service in exclude_list:
211 continue
212 process_utils.Spawn(['stop', service], call=True)
213
214
215def _UnmountStatefulPartition(root):
216 logging.debug('unmount stateful partition')
217 stateful_partition_path = os.path.join(root, 'mnt/stateful_partition')
218 # mount points that need chromeos_shutdown to umount
219
220 # 1. find mount points on stateful partition
221 mount_point_list = process_utils.Spawn(
222 ['mount', stateful_partition_path], read_stderr=True).stderr_data
223
224 mount_point_list = [line.split()[5] for line in mount_point_list.splitlines()
225 if 'mounted on' in line]
226 # 2. find processes that are using stateful partitions
227
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800228 def _ListProcOpening(path_list):
229 lsof_cmd = ['lsof', '-t'] + path_list
Wei-Han Chene97d3532016-03-31 19:22:01 +0800230 return [int(line)
231 for line in process_utils.SpawnOutput(lsof_cmd).splitlines()]
232
233 proc_list = _ListProcOpening(mount_point_list)
234
235 if os.getpid in proc_list:
236 logging.error('wipe_init itself is using stateful partition')
237 logging.error(
238 'lsof: %s',
239 process_utils.SpawnOutput('lsof -p %d' % os.getpid(), shell=True))
240 raise RuntimeError('using stateful partition')
241
242 def _KillOpeningBySignal(sig):
243 proc_list = _ListProcOpening(mount_point_list)
244 if not proc_list:
245 return True # we are done
246 for pid in proc_list:
247 os.kill(pid, sig)
248 return False # need to check again
249
250 sync_utils.Retry(10, 0.1, None, _KillOpeningBySignal, signal.SIGTERM)
251 sync_utils.Retry(10, 0.1, None, _KillOpeningBySignal, signal.SIGKILL)
252
253 proc_list = _ListProcOpening(mount_point_list)
254 assert not proc_list, "processes using stateful partition: %s" % proc_list
255
256 os.unlink(os.path.join(root, 'var', 'run'))
257 os.unlink(os.path.join(root, 'var', 'lock'))
258
259 if os.path.exists(os.path.join(root, 'dev', 'mapper', 'encstateful')):
260 def _UmountEncrypted():
261 try:
262 process_utils.Spawn(['mount-encrypted', 'umount'], check_call=True)
263 return True
264 except: # pylint: disable=bare-except
265 return False
266 sync_utils.Retry(10, 0.1, None, _UmountEncrypted)
267
268 for mount_point in mount_point_list:
269 process_utils.Spawn(['umount', '-n', '-R', mount_point], call=True)
270 process_utils.Spawn(['sync'], call=True)
271
272
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800273def _StartWipe(release_rootfs, root_disk, wipe_args, cutoff_args,
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800274 shopfloor_url):
275 stateful_partition_path = '/mnt/stateful_partition'
276
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800277 clobber_state_env = os.environ.copy()
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800278 clobber_state_env.update(ROOT_DEV=release_rootfs,
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800279 ROOT_DISK=root_disk,
280 FACTORY_RETURN_AFTER_WIPING='YES')
281 logging.debug('clobber-state: root_dev=%s, root_disk=%s',
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800282 release_rootfs, root_disk)
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800283 process_utils.Spawn(['clobber-state', wipe_args], env=clobber_state_env,
284 call=True)
285
286 shutil.move('/tmp/clobber-state.log', os.path.join(stateful_partition_path,
287 'unencrypted',
288 'clobber-state.log'))
289 # remove developer flag, which is created by clobber-state after wiping.
290 try:
291 os.unlink(os.path.join(stateful_partition_path, '.developer'))
292 except OSError:
293 pass
294
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800295 logging.debug('enable release partition: %s', release_rootfs)
296 Util().EnableReleasePartition(release_rootfs)
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800297
298 if shopfloor_url:
299 logging.debug('inform shopfloor %s', shopfloor_url)
300 proc = process_utils.Spawn([os.path.join(SCRIPT_DIR,
301 'inform_shopfloor.sh'),
302 shopfloor_url, 'factory_wipe'], check_call=True)
303 logging.debug('stdout: %s', proc.stdout_data)
304 logging.debug('stderr: %s', proc.stderr_data)
305
306 if cutoff_args is None:
307 cutoff_args = ''
308 logging.debug('battery_cutoff: args=%s', cutoff_args)
309 battery_cutoff_script = os.path.join(SCRIPT_DIR, 'battery_cutoff.sh')
310 process_utils.Spawn('%s %s' % (battery_cutoff_script, cutoff_args),
311 shell=True, check_call=True)
312 # should not reach here
313 time.sleep(1e8)
314
315
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800316def WipeInit(wipe_args, cutoff_args, shopfloor_url, state_dev, release_rootfs,
317 root_disk, old_root):
Wei-Han Chene97d3532016-03-31 19:22:01 +0800318 logfile = '/tmp/wipe_init.log'
319 ResetLog(logfile)
320
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800321 logging.debug('wipe_args: %s', wipe_args)
322 logging.debug('cutoff_args: %s', cutoff_args)
323 logging.debug('shopfloor_url: %s', shopfloor_url)
324 logging.debug('state_dev: %s', state_dev)
325 logging.debug('release_rootfs: %s', release_rootfs)
326 logging.debug('root_disk: %s', root_disk)
327 logging.debug('old_root: %s', old_root)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800328
Wei-Han Chene97d3532016-03-31 19:22:01 +0800329 try:
330 _StopAllUpstartJobs(exclude_list=['boot-services', 'console-tty2', 'dbus',
331 'factory-wipe', 'shill',
332 'openssh-server'])
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800333 _UnmountStatefulPartition(old_root)
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800334
335 process_utils.Spawn([os.path.join(SCRIPT_DIR, 'display_wipe_message.sh'),
336 'wipe'], call=True)
337
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800338 _StartWipe(release_rootfs, root_disk, wipe_args, cutoff_args, shopfloor_url)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800339 except: # pylint: disable=bare-except
340 logging.exception('wipe_init failed')
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800341 OnError(state_dev, logfile)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800342 raise