blob: 2f905afab6e8187301c50708f89adcf22f88b283 [file] [log] [blame]
Hung-Te Lin0e0f9362015-11-18 18:18:05 +08001# Copyright 2015 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import print_function
6
7import logging
Hung-Te Lin67893542016-04-14 12:34:15 +08008import os
9import pipes
10import re
Peter Shihfdf17682017-05-26 11:38:39 +080011from subprocess import PIPE
12from subprocess import Popen
Hung-Te Lin0e0f9362015-11-18 18:18:05 +080013
Wei-Han Chen94b76632016-04-23 01:27:30 +080014from cros.factory.test.env import paths
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +080015from cros.factory.utils import sys_utils
16from cros.factory.utils.type_utils import Error
Hung-Te Lin0e0f9362015-11-18 18:18:05 +080017from cros.factory.utils.type_utils import Obj
18
19
20# TODO(hungte) Deprecate this by dut.Shell
Yong Hong472830b2019-01-07 13:19:35 +080021def Shell(cmd, stdin=None, log=True, sys_interface=None):
Hung-Te Lin0e0f9362015-11-18 18:18:05 +080022 """Run cmd in a shell, return Obj containing stdout, stderr, and status.
23
24 The cmd stdout and stderr output is debug-logged.
25
26 Args:
Hung-Te Lin67893542016-04-14 12:34:15 +080027 cmd: Full shell command line as a string or list, which can contain
28 redirection (pipes, etc).
Hung-Te Lin0e0f9362015-11-18 18:18:05 +080029 stdin: String that will be passed as stdin to the command.
30 log: log command and result.
31 """
Yilin Yang0724c9d2019-11-15 15:53:45 +080032 if not isinstance(cmd, str):
Hung-Te Lin67893542016-04-14 12:34:15 +080033 cmd = ' '.join(pipes.quote(param) for param in cmd)
Yong Hong472830b2019-01-07 13:19:35 +080034 if sys_interface is None:
Yilin Yang5ae33522020-01-03 12:54:08 +080035 process = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True,
36 encoding='utf-8')
Yong Hong472830b2019-01-07 13:19:35 +080037 else:
38 process = sys_interface.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
Peter Shihfdf17682017-05-26 11:38:39 +080039 stdout, stderr = process.communicate(input=stdin)
Hung-Te Lin0e0f9362015-11-18 18:18:05 +080040 if log:
41 logging.debug('running %s' % repr(cmd) +
42 (', stdout: %s' % repr(stdout.strip()) if stdout else '') +
43 (', stderr: %s' % repr(stderr.strip()) if stderr else ''))
44 status = process.poll()
45 return Obj(stdout=stdout, stderr=stderr, status=status, success=(status == 0))
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +080046
47
Wei-Han Chen94b76632016-04-23 01:27:30 +080048def ExecFactoryPar(*args):
49 """Use os.execl to execute a command (given by args) provided by factory PAR.
50
51 This function will execute "/path/to/factory.par arg0 arg1 ..." using
52 os.exec. Current process will be replaced, therefore this function won't
53 return if there is no exception.
54
55 Example::
56
57 >>> ExecFactoryPar(['gooftool', 'wipe_in_place', ...])
58
59 will execute /path/to/factory.par gooftool wipe_in_place ...
60 current process will be replaced by new process.
61 """
62
63 factory_par = paths.GetFactoryPythonArchivePath()
64 # There are two factory_par in the argument because os.execl's function
65 # signature is: os.execl(exec_path, arg0, arg1, ...)
Wei-Han Chenc8f24562016-04-23 19:42:42 +080066 logging.debug('exec: %s %s', factory_par, args)
Wei-Han Chen94b76632016-04-23 01:27:30 +080067 os.execl(factory_par, factory_par, *args)
68
69
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +080070class Util(object):
71 """A collection of util functions that Gooftool needs."""
72
73 def __init__(self):
74 self.shell = Shell
75
76 def _IsDeviceFixed(self, dev):
77 """Check if a device is a fixed device, i.e. not a removable device.
78
79 Args:
80 dev: A device string under /sys/block.
81
82 Returns:
83 True if the given device is fixed, and false if it is not.
84 """
85
86 sysfs_path = '/sys/block/%s/removable' % dev
87 return (os.path.exists(sysfs_path) and
88 open(sysfs_path).read().strip() == '0')
89
90 def GetPrimaryDevicePath(self, partition=None):
91 """Gets the path for the primary device, which is the only non-removable
92 device in the system.
93
94 Args:
95 partition: The index of the partition on primary device.
96
97 Returns:
98 The path to the primary device. If partition is specified, the path
99 points to that partition of the primary device. e.g. /dev/sda1
100 """
101
Wei-Han Chen5cd57732017-07-25 13:06:30 +0800102 dev_path = self.shell('rootdev -s -d').stdout.strip()
103 if not self._IsDeviceFixed(os.path.basename(dev_path)):
104 raise Error('%s is not a fixed device' % dev_path)
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800105 if partition is None:
106 return dev_path
Wei-Han Chen5cd57732017-07-25 13:06:30 +0800107 fmt_str = '%sp%d' if dev_path[-1].isdigit() else '%s%d'
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800108 return fmt_str % (dev_path, partition)
109
110 def GetPartitionDevice(self, path):
111 """Returns a device path string from partition path.
112
113 /dev/sda1 => /dev/sda.
114 /dev/mmcblk0p2 => /dev/mmcblk0.
115 """
116 return ''.join(re.findall(
117 r'(.*[^0-9][0-9]+)p[0-9]+|(.*[^0-9])[0-9]+', path)[0])
118
Hung-Te Lincdb96522016-04-15 16:51:10 +0800119 def GetDevicePartition(self, device, partition):
120 """Returns a partition path from device path string.
121
122 /dev/sda, 1 => /dev/sda1.
123 /dev/mmcblk0p, 2 => /dev/mmcblk0p2.
124 """
125 return ('%sp%s' if device[-1].isdigit() else '%s%s') % (device, partition)
126
127
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800128 def FindScript(self, script_name):
129 """Finds the script under /usr/local/factory/sh
130
131 Args:
132 script_name: The name of the script to look for.
133
134 Returns:
135 The path of the found script.
136
137 Raises:
138 Error if the script is not found.
139 """
140
141 # __file__ is in /usr/local/factory/py/gooftool/__init__.py
142 factory_base = os.path.realpath(os.path.join(
143 os.path.dirname(os.path.realpath(__file__)), '..', '..'))
144 script_path = os.path.join(factory_base, 'sh', script_name)
145 if not os.path.isfile(script_path):
146 raise Error('Needed script %s does not exist.' % script_path)
147 return script_path
148
149 def FindAndRunScript(self, script_name, post_opts=None, pre_opts=None):
150 """Finds and runs the script with given options.
151
152 Args:
153 script_name: The name of the script to look up and run.
154 post_opts: A list of strings that will be appended in the command after
155 the script's name.
156 pre_opts: A list of strings that will be prepended in the command before
157 the script's name.
158
159 Returns:
You-Cheng Syu461ec032017-03-06 15:56:58 +0800160 The result of execution.
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800161
162 Raises:
You-Cheng Syu461ec032017-03-06 15:56:58 +0800163 Error if execution failed.
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800164 """
165
166 assert not post_opts or isinstance(post_opts, list)
167 assert not pre_opts or isinstance(pre_opts, list)
168
169 script = self.FindScript(script_name)
170 cmd = '%s %s %s' % (' '.join(pre_opts) if pre_opts else '',
171 script,
172 ' '.join(post_opts) if post_opts else '')
173 result = self.shell(cmd.strip())
174 if not result.success:
Hung-Te Lineec4e332017-06-01 23:16:40 +0800175 raise Error('%r failed, stderr: %r' % (cmd, result.stderr))
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800176
177 return result
178
179 def GetReleaseRootPartitionPath(self):
180 """Gets the path for release root partition."""
181
182 return self.GetPrimaryDevicePath(5)
183
184 def GetReleaseKernelPartitionPath(self):
185 """Gets the path for release kernel partition."""
186
187 return self.GetPrimaryDevicePath(4)
188
Hung-Te Lincdb96522016-04-15 16:51:10 +0800189 def GetReleaseKernelPathFromRootPartition(self, rootfs_path):
190 """Gets the path for release kernel from given rootfs path.
191
192 This function assumes kernel partition is always located before rootfs.
193 """
194 device = self.GetPartitionDevice(rootfs_path)
195 kernel_index = int(rootfs_path[-1]) - 1
196 return self.GetDevicePartition(device, kernel_index)
197
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800198 def GetReleaseImageLsbData(self):
199 """Gets the /etc/lsb-release content from release image partition.
200
201 Returns:
202 A dictionary containing the key-value pairs in lsb-release.
203 """
204 lsb_content = sys_utils.MountDeviceAndReadFile(
205 self.GetReleaseRootPartitionPath(), 'etc/lsb-release')
206 return dict(re.findall('^(.+)=(.+)$', lsb_content, re.MULTILINE))
207
208 def GetAllowedReleaseImageChannels(self):
209 """Returns a list of channels allowed for release image."""
210 return ['dev', 'beta', 'stable']
211
212 def GetReleaseImageChannel(self):
213 """Returns the channel of current release image."""
214 return self.GetReleaseImageLsbData().get('CHROMEOS_RELEASE_TRACK')
215
216 def GetReleaseImageVersion(self):
217 """Returns the current release image version."""
218 return self.GetReleaseImageLsbData().get('GOOGLE_RELEASE')
219
220 def GetVBSharedDataFlags(self):
221 """Gets VbSharedData flags.
222
223 Returns:
224 An integer representation of the flags.
225 """
226
227 return int(self.shell('crossystem vdat_flags').stdout.strip(), 0)
228
229 def GetCurrentDevSwitchPosition(self):
230 """Gets the position for the current developer switch.
231
232 Returns:
233 An integer representation of the current developer switch position.
234 """
235 return int(self.shell('crossystem devsw_cur').stdout.strip(), 0)
236
237 def GetCrosSystem(self):
238 """Gets the output of 'crossystem'.
239
240 Returns:
241 A dict for key-value pairs for the output of 'crossystem'.
242 e.g. {'flag_name': 'flag_value'}
243 """
244 crossystem_result = self.shell('crossystem').stdout.strip().splitlines()
245 # The crossytem output contains many lines like:
246 # 'key = value # description'
247 # Use regexps to pull out the key-value pairs and build a dict.
248 # Note that value could also contain equal signs.
249 output = {}
250 for entry in crossystem_result:
251 # Any unrecognized format should fail here.
252 key, value = re.findall(r'\A(\S+)\s+=\s+(.*)#.*\Z', entry)[0]
253 output[key] = value.strip()
254
255 return output
256
257 def GetCgptAttributes(self, device=None):
258 if device is None:
259 device = self.GetPrimaryDevicePath()
260
261 attrs = {}
262 for line in self.shell('cgpt show %s -q' % device).stdout.splitlines():
263 # format: offset size no name
264 part_no = line.split()[2]
265 attrs[part_no] = self.shell('cgpt show %s -i %s -A' %
266 (device, part_no)).stdout.strip()
267 return attrs
268
269 def SetCgptAttributes(self, attrs, device=None):
270 if device is None:
271 device = self.GetPrimaryDevicePath()
272
273 curr_attrs = self.GetCgptAttributes()
Yilin Yang879fbda2020-05-14 13:52:30 +0800274 for k, v in attrs.items():
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800275 if curr_attrs.get(k) == v:
276 continue
277 if not self.shell('cgpt add %s -i %s -A %s' % (device, k, v)).success:
278 raise Error('Failed to set device config: %s#%s=%s' % (device, k, v))
279
280 def InvokeChromeOSPostInstall(self, root_dev=None):
281 """Invokes the ChromeOS post-install script (/postinst)."""
282 if root_dev is None:
Hung-Te Lineec4e332017-06-01 23:16:40 +0800283 root_dev = self.GetReleaseRootPartitionPath()
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800284
285 logging.info('Running ChromeOS post-install on %s...', root_dev)
286
287 # Some compatible and experimental fs (e.g., ext4) may be buggy and still
288 # try to write the file system even if we mount it with "ro" (ex, when
289 # seeing journaling error in ext3, or s_kbytes_written in ext4). It is
290 # safer to always mount the partition with legacy ext2. (ref:
291 # chrome-os-partner:3940)
292 with sys_utils.MountPartition(root_dev, fstype='ext2') as mount_path:
293 # IS_FACTORY_INSTALL is used to prevent postinst trying to update firmare.
294 command = ('IS_FACTORY_INSTALL=1 IS_INSTALL=1 "%s"/postinst %s' %
295 (mount_path, root_dev))
296 result = self.shell(command)
297 if not result.success:
298 raise Error('chromeos-postinst on %s failed with error: code=%s. %s' %
299 (root_dev, result.status, result.stderr))
300
301 def EnableKernel(self, device, part_no):
302 """Enables the kernel partition from GPT."""
303 logging.info('Enabling kernel on %s#%s...', device, part_no)
304 r = self.shell('cgpt add -i %s -P 3 -S 1 -T 0 %s' % (part_no, device))
305 if not r.success:
306 raise Error('Failed to enable kernel on %s#%s' % (device, part_no))
307
308 def DisableKernel(self, device, part_no):
309 """Disables the kernel partition from GPT."""
310 logging.info('Disabling kernel on %s#%s...', device, part_no)
311 r = self.shell('cgpt add -i %s -P 0 -S 0 -T 0 %s' % (part_no, device))
312 if not r.success:
313 raise Error('Failed to disable kernel on %s#%s' % (device, part_no))
314
Earl Ou4f446172016-09-08 11:36:07 +0800315 def IsLegacyChromeOSFirmware(self):
316 """Returns if the system is running legacy ChromeOS firmware."""
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800317 r = self.shell('crossystem mainfw_type')
Earl Ou4f446172016-09-08 11:36:07 +0800318 return not r.success or r.stdout.strip() == 'nonchrome'
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800319
320 def EnableReleasePartition(self, root_dev):
321 """Enables a release image partition on disk."""
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800322 release_no = int(root_dev[-1]) - 1
323 factory_map = {2: 4, 4: 2}
324 if release_no not in factory_map:
325 raise ValueError('EnableReleasePartition: Cannot identify kernel %s' %
326 root_dev)
327
328 factory_no = factory_map[release_no]
329 device = self.GetPartitionDevice(root_dev)
330 curr_attrs = self.GetCgptAttributes(device)
331 try:
332 # When booting with legacy firmware, we need to update the legacy boot
333 # loaders to activate new kernel; on a real ChromeOS firmware, only CGPT
334 # header is used, and postinst is already performed in verify_rootfs.
Earl Ou4f446172016-09-08 11:36:07 +0800335 if self.IsLegacyChromeOSFirmware():
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800336 self.InvokeChromeOSPostInstall(root_dev)
337 self.shell('crossystem disable_dev_request=1')
338 self.DisableKernel(device, factory_no)
339 self.EnableKernel(device, release_no)
340 # Enforce a sync and wait for underlying hardware to flush.
341 logging.info('Syncing disks...')
342 self.shell('sync; sleep 3')
343 logging.info('Enable release partition: Complete.')
Hung-Te Linc8174b52017-06-02 11:11:45 +0800344 except Exception:
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800345 logging.error('FAIL: Failed to enable release partition.')
346 self.shell('crossystem disable_dev_request=0')
347 self.SetCgptAttributes(curr_attrs, device)