Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 1 | # 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 | |
| 5 | from __future__ import print_function |
| 6 | |
| 7 | import logging |
Hung-Te Lin | 6789354 | 2016-04-14 12:34:15 +0800 | [diff] [blame] | 8 | import os |
| 9 | import pipes |
| 10 | import re |
Peter Shih | fdf1768 | 2017-05-26 11:38:39 +0800 | [diff] [blame] | 11 | from subprocess import PIPE |
| 12 | from subprocess import Popen |
Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 13 | |
Wei-Han Chen | 94b7663 | 2016-04-23 01:27:30 +0800 | [diff] [blame] | 14 | from cros.factory.test.env import paths |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 15 | from cros.factory.utils import sys_utils |
| 16 | from cros.factory.utils.type_utils import Error |
Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 17 | from cros.factory.utils.type_utils import Obj |
| 18 | |
| 19 | |
| 20 | # TODO(hungte) Deprecate this by dut.Shell |
Yong Hong | 472830b | 2019-01-07 13:19:35 +0800 | [diff] [blame] | 21 | def Shell(cmd, stdin=None, log=True, sys_interface=None): |
Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 22 | """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 Lin | 6789354 | 2016-04-14 12:34:15 +0800 | [diff] [blame] | 27 | cmd: Full shell command line as a string or list, which can contain |
| 28 | redirection (pipes, etc). |
Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 29 | stdin: String that will be passed as stdin to the command. |
| 30 | log: log command and result. |
| 31 | """ |
Yilin Yang | 0724c9d | 2019-11-15 15:53:45 +0800 | [diff] [blame] | 32 | if not isinstance(cmd, str): |
Hung-Te Lin | 6789354 | 2016-04-14 12:34:15 +0800 | [diff] [blame] | 33 | cmd = ' '.join(pipes.quote(param) for param in cmd) |
Yong Hong | 472830b | 2019-01-07 13:19:35 +0800 | [diff] [blame] | 34 | if sys_interface is None: |
Yilin Yang | 5ae3352 | 2020-01-03 12:54:08 +0800 | [diff] [blame] | 35 | process = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True, |
| 36 | encoding='utf-8') |
Yong Hong | 472830b | 2019-01-07 13:19:35 +0800 | [diff] [blame] | 37 | else: |
| 38 | process = sys_interface.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) |
Peter Shih | fdf1768 | 2017-05-26 11:38:39 +0800 | [diff] [blame] | 39 | stdout, stderr = process.communicate(input=stdin) |
Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 40 | 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 Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 46 | |
| 47 | |
Wei-Han Chen | 94b7663 | 2016-04-23 01:27:30 +0800 | [diff] [blame] | 48 | def 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 Chen | c8f2456 | 2016-04-23 19:42:42 +0800 | [diff] [blame] | 66 | logging.debug('exec: %s %s', factory_par, args) |
Wei-Han Chen | 94b7663 | 2016-04-23 01:27:30 +0800 | [diff] [blame] | 67 | os.execl(factory_par, factory_par, *args) |
| 68 | |
| 69 | |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 70 | class 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 Chen | 5cd5773 | 2017-07-25 13:06:30 +0800 | [diff] [blame] | 102 | 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 Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 105 | if partition is None: |
| 106 | return dev_path |
Wei-Han Chen | 5cd5773 | 2017-07-25 13:06:30 +0800 | [diff] [blame] | 107 | fmt_str = '%sp%d' if dev_path[-1].isdigit() else '%s%d' |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 108 | 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 Lin | cdb9652 | 2016-04-15 16:51:10 +0800 | [diff] [blame] | 119 | 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 Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 128 | 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 Syu | 461ec03 | 2017-03-06 15:56:58 +0800 | [diff] [blame] | 160 | The result of execution. |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 161 | |
| 162 | Raises: |
You-Cheng Syu | 461ec03 | 2017-03-06 15:56:58 +0800 | [diff] [blame] | 163 | Error if execution failed. |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 164 | """ |
| 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 Lin | eec4e33 | 2017-06-01 23:16:40 +0800 | [diff] [blame] | 175 | raise Error('%r failed, stderr: %r' % (cmd, result.stderr)) |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 176 | |
| 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 Lin | cdb9652 | 2016-04-15 16:51:10 +0800 | [diff] [blame] | 189 | 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 Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 198 | 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 Yang | 879fbda | 2020-05-14 13:52:30 +0800 | [diff] [blame^] | 274 | for k, v in attrs.items(): |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 275 | 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 Lin | eec4e33 | 2017-06-01 23:16:40 +0800 | [diff] [blame] | 283 | root_dev = self.GetReleaseRootPartitionPath() |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 284 | |
| 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 Ou | 4f44617 | 2016-09-08 11:36:07 +0800 | [diff] [blame] | 315 | def IsLegacyChromeOSFirmware(self): |
| 316 | """Returns if the system is running legacy ChromeOS firmware.""" |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 317 | r = self.shell('crossystem mainfw_type') |
Earl Ou | 4f44617 | 2016-09-08 11:36:07 +0800 | [diff] [blame] | 318 | return not r.success or r.stdout.strip() == 'nonchrome' |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 319 | |
| 320 | def EnableReleasePartition(self, root_dev): |
| 321 | """Enables a release image partition on disk.""" |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 322 | 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 Ou | 4f44617 | 2016-09-08 11:36:07 +0800 | [diff] [blame] | 335 | if self.IsLegacyChromeOSFirmware(): |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 336 | 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 Lin | c8174b5 | 2017-06-02 11:11:45 +0800 | [diff] [blame] | 344 | except Exception: |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 345 | logging.error('FAIL: Failed to enable release partition.') |
| 346 | self.shell('crossystem disable_dev_request=0') |
| 347 | self.SetCgptAttributes(curr_attrs, device) |