blob: c2b0d1cc7f447c433052c4f4c6450f180276fa3a [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
10import json
11import os
12import resource
13import shutil
14import signal
15import tempfile
16import textwrap
17import time
18
19import factory_common # pylint: disable=unused-import
20from cros.factory.gooftool import chroot
21from cros.factory.utils import process_utils
22from cros.factory.utils import sync_utils
23from cros.factory.utils import sys_utils
24
25
26WIPE_ARGS_FILE = '/tmp/factory_wipe_args'
27
28
29def OnError(state_dev, logfile):
30 with sys_utils.MountPartition(state_dev,
31 rw=True,
32 fstype='ext4') as mount_point:
33 shutil.copyfile(logfile,
34 os.path.join(mount_point, os.path.basename(logfile)))
35
36
37def Daemonize(logfile=None):
38 """Starts a daemon process and terminates current process.
39
40 A daemon process will be started, and continue excuting the following codes.
41 The original process that calls this function will be terminated.
42
43 Example::
44
45 def DaemonFunc():
46 Daemonize()
47 # the process calling DaemonFunc is terminated.
48 # the following codes will be executed in a daemon process
49 ...
50
51 If you would like to keep the original process alive, you could fork a child
52 process and let child process start the daemon.
53 """
54 # fork from parent process
55 if os.fork():
56 # stop parent process
57 os._exit(0) # pylint: disable=protected-access
58
59 # decouple from parent process
60 os.chdir('/')
61 os.umask(0)
62 os.setsid()
63
64 # fork again
65 if os.fork():
66 os._exit(0) # pylint: disable=protected-access
67
68 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
69 if maxfd == resource.RLIM_INFINITY:
70 maxfd = 1024
71
72 for fd in xrange(maxfd):
73 try:
74 os.close(fd)
75 except OSError:
76 pass
77
78 # Reopen fd 0 (stdin), 1 (stdout), 2 (stderr) to prevent errors from reading
79 # or writing to these files.
80 # Since we have closed all file descriptors, os.open should open a file with
81 # file descriptor equals to 0
82 os.open('/dev/null', os.O_RDWR)
83 if logfile is None:
84 os.dup2(0, 1) # stdout
85 os.dup2(0, 2) # stderr
86 else:
87 os.open(logfile, os.O_RDWR | os.O_CREAT)
88 os.dup2(1, 2) # stderr
89
90
91def ResetLog(logfile=None):
92 if len(logging.getLogger().handlers) > 0:
93 for handler in logging.getLogger().handlers:
94 logging.getLogger().removeHandler(handler)
95 logging.basicConfig(filename=logfile, level=logging.NOTSET)
96
97
98def WipeInTmpFs(is_fast=None, cutoff_args=None, shopfloor_url=None):
99 """prepare to wipe by pivot root to tmpfs and unmount statefull partition.
100
101 Args:
102 is_fast: whether or not to apply fast wipe.
103 cutoff_args: arguments to be passed to battery_cutoff.sh after wiping.
104 shopfloor_url: for inform_shopfloor.sh
105 """
106
107 logfile = '/tmp/wipe_in_tmpfs.log'
108 Daemonize()
109
110 ResetLog(logfile)
111
112 factory_par = sys_utils.GetRunningFactoryPythonArchivePath()
113 if not factory_par:
114 # try to find factory python archive at default location
115 if os.path.exists('/usr/local/factory/factory-mini.par'):
116 factory_par = '/usr/local/factory/factory-mini.par'
117 elif os.path.exists('/usr/local/factory/factory.par'):
118 factory_par = '/usr/local/factory/factory.par'
119 else:
120 raise RuntimeError('cannot find factory python archive')
121
122 new_root = tempfile.mkdtemp(prefix='tmpfs.')
123 binary_deps = [
124 'activate_date', 'backlight_tool', 'busybox', 'cgpt', 'cgpt.bin',
125 'clobber-log', 'clobber-state', 'coreutils', 'crossystem', 'dd',
126 'display_boot_message', 'dumpe2fs', 'ectool', 'flashrom', 'halt',
127 'initctl', 'mkfs.ext4', 'mktemp', 'mosys', 'mount', 'mount-encrypted',
128 'od', 'pango-view', 'pkill', 'pv', 'python', 'reboot', 'setterm', 'sh',
129 'shutdown', 'stop', 'umount', 'vpd', 'wget', 'lsof', ]
130 if os.path.exists('/sbin/frecon'):
131 binary_deps.append('/sbin/frecon')
132 else:
133 binary_deps.append('/usr/bin/ply-image')
134
135 etc_issue = textwrap.dedent("""
136 You are now in tmp file system created for in-place wiping.
137
138 For debugging wiping fails, see log files under
139 /tmp
140 /mnt/stateful_partition/unencrypted
141
142 The log file name should be
143 - wipe_in_tmpfs.log
144 - wipe_init.log
145
146 You can also run scripts under /usr/local/factory/sh for wiping process.
147 """)
148
149 root_disk = process_utils.SpawnOutput(['rootdev', '-s', '-d']).strip()
150 if root_disk[-1].isdigit():
151 state_dev = root_disk + 'p1'
152 else:
153 state_dev = root_disk + '1'
154 factory_root_dev = process_utils.SpawnOutput(['rootdev', '-s']).strip()
155 wipe_args = 'factory' + (' fast' if is_fast else '')
156
157 logging.debug('state_dev: %s', state_dev)
158 logging.debug('factory_par: %s', factory_par)
159
160 old_root = 'old_root'
161
162 try:
163 with chroot.TmpChroot(
164 new_root,
165 file_dir_list=[
166 '/bin', '/etc', '/lib', '/lib64', '/opt', '/root', '/sbin',
167 '/usr/share/fonts/notocjk',
168 '/usr/share/cache/fontconfig',
169 '/usr/share/chromeos-assets/images',
170 '/usr/share/chromeos-assets/text/boot_messages',
171 '/usr/share/misc/chromeos-common.sh',
172 '/usr/local/factory/sh',
173 factory_par],
174 binary_list=binary_deps, etc_issue=etc_issue).PivotRoot(old_root):
175 logging.debug(
176 'lsof: %s',
177 process_utils.SpawnOutput('lsof -p %d' % os.getpid(), shell=True))
178
179 json.dump(dict(wipe_args=wipe_args,
180 cutoff_args=cutoff_args,
181 shopfloor_url=shopfloor_url,
182 state_dev=state_dev,
183 factory_root_dev=factory_root_dev,
184 root_disk=root_disk,
185 old_root=old_root),
186 open(WIPE_ARGS_FILE, 'w'))
187
188 process_utils.Spawn(['sync'], call=True)
189 time.sleep(3)
190
191 # Restart gooftool under new root. Since current gooftool might be using
192 # some resource under stateful partition, restarting gooftool ensures that
193 # everything new gooftool is using comes from tmpfs and we can safely
194 # unmount stateful partition.
195 # There are two factory_par in the argument because os.execl's function
196 # signature is: os.execl(exec_path, arg0, arg1, ...)
197 os.execl(factory_par, factory_par,
198 'gooftool', 'wipe_init', '--args_file', WIPE_ARGS_FILE)
199 raise RuntimeError('Should not reach here')
200 except: # pylint: disable=bare-except
201 logging.exception('wipe_in_place failed')
202 OnError(state_dev, logfile)
203 raise
204
205
206def _StopAllUpstartJobs(exclude_list=None):
207 logging.debug('stopping upstart jobs')
208
209 # Try three times to stop running services because some service will respawn
210 # one time after being stopped, e.g. shill_respawn. Two times should be enough
211 # to stop shill. Adding one more try for safety.
212
213 if exclude_list is None:
214 exclude_list = []
215
216 for unused_tries in xrange(3):
217 service_list = process_utils.SpawnOutput(['initctl', 'list']).splitlines()
218 service_list = [
219 line.split()[0] for line in service_list if 'start/running' in line]
220 for service in service_list:
221 if service in exclude_list:
222 continue
223 process_utils.Spawn(['stop', service], call=True)
224
225
226def _UnmountStatefulPartition(root):
227 logging.debug('unmount stateful partition')
228 stateful_partition_path = os.path.join(root, 'mnt/stateful_partition')
229 # mount points that need chromeos_shutdown to umount
230
231 # 1. find mount points on stateful partition
232 mount_point_list = process_utils.Spawn(
233 ['mount', stateful_partition_path], read_stderr=True).stderr_data
234
235 mount_point_list = [line.split()[5] for line in mount_point_list.splitlines()
236 if 'mounted on' in line]
237 # 2. find processes that are using stateful partitions
238
239 def _ListProcOpening(paths):
240 lsof_cmd = ['lsof', '-t'] + paths
241 return [int(line)
242 for line in process_utils.SpawnOutput(lsof_cmd).splitlines()]
243
244 proc_list = _ListProcOpening(mount_point_list)
245
246 if os.getpid in proc_list:
247 logging.error('wipe_init itself is using stateful partition')
248 logging.error(
249 'lsof: %s',
250 process_utils.SpawnOutput('lsof -p %d' % os.getpid(), shell=True))
251 raise RuntimeError('using stateful partition')
252
253 def _KillOpeningBySignal(sig):
254 proc_list = _ListProcOpening(mount_point_list)
255 if not proc_list:
256 return True # we are done
257 for pid in proc_list:
258 os.kill(pid, sig)
259 return False # need to check again
260
261 sync_utils.Retry(10, 0.1, None, _KillOpeningBySignal, signal.SIGTERM)
262 sync_utils.Retry(10, 0.1, None, _KillOpeningBySignal, signal.SIGKILL)
263
264 proc_list = _ListProcOpening(mount_point_list)
265 assert not proc_list, "processes using stateful partition: %s" % proc_list
266
267 os.unlink(os.path.join(root, 'var', 'run'))
268 os.unlink(os.path.join(root, 'var', 'lock'))
269
270 if os.path.exists(os.path.join(root, 'dev', 'mapper', 'encstateful')):
271 def _UmountEncrypted():
272 try:
273 process_utils.Spawn(['mount-encrypted', 'umount'], check_call=True)
274 return True
275 except: # pylint: disable=bare-except
276 return False
277 sync_utils.Retry(10, 0.1, None, _UmountEncrypted)
278
279 for mount_point in mount_point_list:
280 process_utils.Spawn(['umount', '-n', '-R', mount_point], call=True)
281 process_utils.Spawn(['sync'], call=True)
282
283
284def WipeInit(args_file):
285 logfile = '/tmp/wipe_init.log'
286 ResetLog(logfile)
287
288 args = json.load(open(args_file))
289
290 logging.debug('args: %r', args)
291 logging.debug(
292 'lsof: %s',
293 process_utils.SpawnOutput('lsof -p %d' % os.getpid(), shell=True))
294
295 try:
296 _StopAllUpstartJobs(exclude_list=['boot-services', 'console-tty2', 'dbus',
297 'factory-wipe', 'shill',
298 'openssh-server'])
299 _UnmountStatefulPartition(args['old_root'])
300 except: # pylint: disable=bare-except
301 logging.exception('wipe_init failed')
302 OnError(args['state_dev'], logfile)
303 raise
304