blob: 7c32ac220cbfa503d5b830f40d72209e6e940e92 [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
Wei-Han Chene97d3532016-03-31 19:22:01 +0800110 Daemonize()
111
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800112 logfile = '/tmp/wipe_in_tmpfs.log'
Wei-Han Chene97d3532016-03-31 19:22:01 +0800113 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
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800176 args = []
177 if wipe_args is not None:
178 args += ['--wipe_args', wipe_args,]
179 if cutoff_args is not None:
180 args += ['--cutoff_args', cutoff_args,]
181 if shopfloor_url is not None:
182 args += ['--shopfloor_url', shopfloor_url,]
183
Wei-Han Chene97d3532016-03-31 19:22:01 +0800184 # Restart gooftool under new root. Since current gooftool might be using
185 # some resource under stateful partition, restarting gooftool ensures that
186 # everything new gooftool is using comes from tmpfs and we can safely
187 # unmount stateful partition.
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800188 ExecFactoryPar('gooftool', 'wipe_init',
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800189 '--state_dev', state_dev,
190 '--release_rootfs', release_rootfs,
191 '--root_disk', root_disk,
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800192 '--old_root', old_root,
193 *args)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800194 raise RuntimeError('Should not reach here')
195 except: # pylint: disable=bare-except
196 logging.exception('wipe_in_place failed')
197 OnError(state_dev, logfile)
198 raise
199
200
201def _StopAllUpstartJobs(exclude_list=None):
202 logging.debug('stopping upstart jobs')
203
204 # Try three times to stop running services because some service will respawn
205 # one time after being stopped, e.g. shill_respawn. Two times should be enough
206 # to stop shill. Adding one more try for safety.
207
208 if exclude_list is None:
209 exclude_list = []
210
211 for unused_tries in xrange(3):
212 service_list = process_utils.SpawnOutput(['initctl', 'list']).splitlines()
213 service_list = [
214 line.split()[0] for line in service_list if 'start/running' in line]
215 for service in service_list:
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800216 if service in exclude_list or service.startswith('console-'):
Wei-Han Chene97d3532016-03-31 19:22:01 +0800217 continue
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800218 process_utils.Spawn(['stop', service], call=True, log=True)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800219
220
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800221def _UnmountStatefulPartition(root, state_dev):
Wei-Han Chene97d3532016-03-31 19:22:01 +0800222 logging.debug('unmount stateful partition')
Wei-Han Chene97d3532016-03-31 19:22:01 +0800223 # mount points that need chromeos_shutdown to umount
224
225 # 1. find mount points on stateful partition
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800226 mount_output = process_utils.SpawnOutput(['mount'], log=True)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800227
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800228 mount_point_list = []
229 for line in mount_output.splitlines():
230 fields = line.split()
231 if fields[0] == state_dev:
232 mount_point_list.append(fields[2])
233
234 logging.debug('stateful partitions mounted on: %s', mount_point_list)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800235 # 2. find processes that are using stateful partitions
236
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800237 def _ListProcOpening(path_list):
238 lsof_cmd = ['lsof', '-t'] + path_list
Wei-Han Chene97d3532016-03-31 19:22:01 +0800239 return [int(line)
240 for line in process_utils.SpawnOutput(lsof_cmd).splitlines()]
241
242 proc_list = _ListProcOpening(mount_point_list)
243
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800244 if os.getpid() in proc_list:
Wei-Han Chene97d3532016-03-31 19:22:01 +0800245 logging.error('wipe_init itself is using stateful partition')
246 logging.error(
247 'lsof: %s',
248 process_utils.SpawnOutput('lsof -p %d' % os.getpid(), shell=True))
249 raise RuntimeError('using stateful partition')
250
251 def _KillOpeningBySignal(sig):
252 proc_list = _ListProcOpening(mount_point_list)
253 if not proc_list:
254 return True # we are done
255 for pid in proc_list:
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800256 try:
257 os.kill(pid, sig)
258 except: # pylint: disable=bare-except
259 logging.exception('killing process %d failed', pid)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800260 return False # need to check again
261
262 sync_utils.Retry(10, 0.1, None, _KillOpeningBySignal, signal.SIGTERM)
263 sync_utils.Retry(10, 0.1, None, _KillOpeningBySignal, signal.SIGKILL)
264
265 proc_list = _ListProcOpening(mount_point_list)
266 assert not proc_list, "processes using stateful partition: %s" % proc_list
267
268 os.unlink(os.path.join(root, 'var', 'run'))
269 os.unlink(os.path.join(root, 'var', 'lock'))
270
271 if os.path.exists(os.path.join(root, 'dev', 'mapper', 'encstateful')):
272 def _UmountEncrypted():
273 try:
274 process_utils.Spawn(['mount-encrypted', 'umount'], check_call=True)
275 return True
276 except: # pylint: disable=bare-except
277 return False
278 sync_utils.Retry(10, 0.1, None, _UmountEncrypted)
279
280 for mount_point in mount_point_list:
281 process_utils.Spawn(['umount', '-n', '-R', mount_point], call=True)
282 process_utils.Spawn(['sync'], call=True)
283
284
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800285def _StartWipe(release_rootfs, root_disk, wipe_args, cutoff_args,
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800286 shopfloor_url):
287 stateful_partition_path = '/mnt/stateful_partition'
288
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800289 clobber_state_env = os.environ.copy()
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800290 clobber_state_env.update(ROOT_DEV=release_rootfs,
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800291 ROOT_DISK=root_disk,
292 FACTORY_RETURN_AFTER_WIPING='YES')
293 logging.debug('clobber-state: root_dev=%s, root_disk=%s',
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800294 release_rootfs, root_disk)
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800295 process_utils.Spawn(['clobber-state', wipe_args], env=clobber_state_env,
296 call=True)
297
298 shutil.move('/tmp/clobber-state.log', os.path.join(stateful_partition_path,
299 'unencrypted',
300 'clobber-state.log'))
301 # remove developer flag, which is created by clobber-state after wiping.
302 try:
303 os.unlink(os.path.join(stateful_partition_path, '.developer'))
304 except OSError:
305 pass
306
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800307 logging.debug('enable release partition: %s', release_rootfs)
308 Util().EnableReleasePartition(release_rootfs)
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800309
310 if shopfloor_url:
311 logging.debug('inform shopfloor %s', shopfloor_url)
312 proc = process_utils.Spawn([os.path.join(SCRIPT_DIR,
313 'inform_shopfloor.sh'),
314 shopfloor_url, 'factory_wipe'], check_call=True)
315 logging.debug('stdout: %s', proc.stdout_data)
316 logging.debug('stderr: %s', proc.stderr_data)
317
318 if cutoff_args is None:
319 cutoff_args = ''
320 logging.debug('battery_cutoff: args=%s', cutoff_args)
321 battery_cutoff_script = os.path.join(SCRIPT_DIR, 'battery_cutoff.sh')
322 process_utils.Spawn('%s %s' % (battery_cutoff_script, cutoff_args),
323 shell=True, check_call=True)
324 # should not reach here
325 time.sleep(1e8)
326
327
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800328def WipeInit(wipe_args, cutoff_args, shopfloor_url, state_dev, release_rootfs,
329 root_disk, old_root):
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800330 Daemonize()
Wei-Han Chene97d3532016-03-31 19:22:01 +0800331 logfile = '/tmp/wipe_init.log'
332 ResetLog(logfile)
333
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800334 logging.debug('wipe_args: %s', wipe_args)
335 logging.debug('cutoff_args: %s', cutoff_args)
336 logging.debug('shopfloor_url: %s', shopfloor_url)
337 logging.debug('state_dev: %s', state_dev)
338 logging.debug('release_rootfs: %s', release_rootfs)
339 logging.debug('root_disk: %s', root_disk)
340 logging.debug('old_root: %s', old_root)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800341
Wei-Han Chene97d3532016-03-31 19:22:01 +0800342 try:
Wei-Han Chenc8f24562016-04-23 19:42:42 +0800343 _StopAllUpstartJobs(exclude_list=['boot-services', 'dbus', 'factory-wipe',
344 'shill', 'openssh-server'])
345 _UnmountStatefulPartition(old_root, state_dev)
Wei-Han Chen9adf9de2016-04-01 19:35:41 +0800346
347 process_utils.Spawn([os.path.join(SCRIPT_DIR, 'display_wipe_message.sh'),
348 'wipe'], call=True)
349
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800350 _StartWipe(release_rootfs, root_disk, wipe_args, cutoff_args, shopfloor_url)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800351 except: # pylint: disable=bare-except
352 logging.exception('wipe_init failed')
Wei-Han Chen0a3320e2016-04-23 01:32:07 +0800353 OnError(state_dev, logfile)
Wei-Han Chene97d3532016-03-31 19:22:01 +0800354 raise