Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | # |
| 3 | # Copyright 2015 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 | from __future__ import print_function |
| 8 | |
| 9 | import logging |
Hung-Te Lin | 6789354 | 2016-04-14 12:34:15 +0800 | [diff] [blame] | 10 | import os |
| 11 | import pipes |
| 12 | import re |
Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 13 | from subprocess import Popen, PIPE |
| 14 | |
| 15 | import factory_common # pylint: disable=W0611 |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 16 | from cros.factory.utils import sys_utils |
| 17 | from cros.factory.utils.type_utils import Error |
Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 18 | from cros.factory.utils.type_utils import Obj |
| 19 | |
| 20 | |
| 21 | # TODO(hungte) Deprecate this by dut.Shell |
| 22 | def Shell(cmd, stdin=None, log=True): |
| 23 | """Run cmd in a shell, return Obj containing stdout, stderr, and status. |
| 24 | |
| 25 | The cmd stdout and stderr output is debug-logged. |
| 26 | |
| 27 | Args: |
Hung-Te Lin | 6789354 | 2016-04-14 12:34:15 +0800 | [diff] [blame] | 28 | cmd: Full shell command line as a string or list, which can contain |
| 29 | redirection (pipes, etc). |
Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 30 | stdin: String that will be passed as stdin to the command. |
| 31 | log: log command and result. |
| 32 | """ |
Hung-Te Lin | 6789354 | 2016-04-14 12:34:15 +0800 | [diff] [blame] | 33 | if not isinstance(cmd, basestring): |
| 34 | cmd = ' '.join(pipes.quote(param) for param in cmd) |
Hung-Te Lin | 0e0f936 | 2015-11-18 18:18:05 +0800 | [diff] [blame] | 35 | process = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True) |
| 36 | stdout, stderr = process.communicate(input=stdin) # pylint: disable=E1123 |
| 37 | if log: |
| 38 | logging.debug('running %s' % repr(cmd) + |
| 39 | (', stdout: %s' % repr(stdout.strip()) if stdout else '') + |
| 40 | (', stderr: %s' % repr(stderr.strip()) if stderr else '')) |
| 41 | status = process.poll() |
| 42 | return Obj(stdout=stdout, stderr=stderr, status=status, success=(status == 0)) |
Wei-Han Chen | fa2bccd | 2016-04-01 19:28:43 +0800 | [diff] [blame] | 43 | |
| 44 | |
| 45 | class Util(object): |
| 46 | """A collection of util functions that Gooftool needs.""" |
| 47 | |
| 48 | def __init__(self): |
| 49 | self.shell = Shell |
| 50 | |
| 51 | def _IsDeviceFixed(self, dev): |
| 52 | """Check if a device is a fixed device, i.e. not a removable device. |
| 53 | |
| 54 | Args: |
| 55 | dev: A device string under /sys/block. |
| 56 | |
| 57 | Returns: |
| 58 | True if the given device is fixed, and false if it is not. |
| 59 | """ |
| 60 | |
| 61 | sysfs_path = '/sys/block/%s/removable' % dev |
| 62 | return (os.path.exists(sysfs_path) and |
| 63 | open(sysfs_path).read().strip() == '0') |
| 64 | |
| 65 | def GetPrimaryDevicePath(self, partition=None): |
| 66 | """Gets the path for the primary device, which is the only non-removable |
| 67 | device in the system. |
| 68 | |
| 69 | Args: |
| 70 | partition: The index of the partition on primary device. |
| 71 | |
| 72 | Returns: |
| 73 | The path to the primary device. If partition is specified, the path |
| 74 | points to that partition of the primary device. e.g. /dev/sda1 |
| 75 | """ |
| 76 | |
| 77 | alpha_re = re.compile(r'^/dev/([a-zA-Z]+)[0-9]+$') |
| 78 | alnum_re = re.compile(r'^/dev/([a-zA-Z]+[0-9]+)p[0-9]+$') |
| 79 | matched_alnum = False |
| 80 | dev_set = set() |
| 81 | for path in self.shell('cgpt find -t rootfs').stdout.strip().split(): |
| 82 | for dev in alpha_re.findall(path): |
| 83 | if self._IsDeviceFixed(dev): |
| 84 | dev_set.add(dev) |
| 85 | matched_alnum = False |
| 86 | for dev in alnum_re.findall(path): |
| 87 | if self._IsDeviceFixed(dev): |
| 88 | dev_set.add(dev) |
| 89 | matched_alnum = True |
| 90 | if len(dev_set) != 1: |
| 91 | raise Error('zero or multiple primary devs: %s' % dev_set) |
| 92 | dev_path = os.path.join('/dev', dev_set.pop()) |
| 93 | if partition is None: |
| 94 | return dev_path |
| 95 | fmt_str = '%sp%d' if matched_alnum else '%s%d' |
| 96 | return fmt_str % (dev_path, partition) |
| 97 | |
| 98 | def GetPartitionDevice(self, path): |
| 99 | """Returns a device path string from partition path. |
| 100 | |
| 101 | /dev/sda1 => /dev/sda. |
| 102 | /dev/mmcblk0p2 => /dev/mmcblk0. |
| 103 | """ |
| 104 | return ''.join(re.findall( |
| 105 | r'(.*[^0-9][0-9]+)p[0-9]+|(.*[^0-9])[0-9]+', path)[0]) |
| 106 | |
| 107 | def FindScript(self, script_name): |
| 108 | """Finds the script under /usr/local/factory/sh |
| 109 | |
| 110 | Args: |
| 111 | script_name: The name of the script to look for. |
| 112 | |
| 113 | Returns: |
| 114 | The path of the found script. |
| 115 | |
| 116 | Raises: |
| 117 | Error if the script is not found. |
| 118 | """ |
| 119 | |
| 120 | # __file__ is in /usr/local/factory/py/gooftool/__init__.py |
| 121 | factory_base = os.path.realpath(os.path.join( |
| 122 | os.path.dirname(os.path.realpath(__file__)), '..', '..')) |
| 123 | script_path = os.path.join(factory_base, 'sh', script_name) |
| 124 | if not os.path.isfile(script_path): |
| 125 | raise Error('Needed script %s does not exist.' % script_path) |
| 126 | return script_path |
| 127 | |
| 128 | def FindAndRunScript(self, script_name, post_opts=None, pre_opts=None): |
| 129 | """Finds and runs the script with given options. |
| 130 | |
| 131 | Args: |
| 132 | script_name: The name of the script to look up and run. |
| 133 | post_opts: A list of strings that will be appended in the command after |
| 134 | the script's name. |
| 135 | pre_opts: A list of strings that will be prepended in the command before |
| 136 | the script's name. |
| 137 | |
| 138 | Returns: |
| 139 | The result of execusion. |
| 140 | |
| 141 | Raises: |
| 142 | Error if execusion failed. |
| 143 | """ |
| 144 | |
| 145 | assert not post_opts or isinstance(post_opts, list) |
| 146 | assert not pre_opts or isinstance(pre_opts, list) |
| 147 | |
| 148 | script = self.FindScript(script_name) |
| 149 | cmd = '%s %s %s' % (' '.join(pre_opts) if pre_opts else '', |
| 150 | script, |
| 151 | ' '.join(post_opts) if post_opts else '') |
| 152 | result = self.shell(cmd.strip()) |
| 153 | if not result.success: |
| 154 | raise Error, '%r failed, stderr: %r' % (cmd, result.stderr) |
| 155 | |
| 156 | return result |
| 157 | |
| 158 | def GetReleaseRootPartitionPath(self): |
| 159 | """Gets the path for release root partition.""" |
| 160 | |
| 161 | return self.GetPrimaryDevicePath(5) |
| 162 | |
| 163 | def GetReleaseKernelPartitionPath(self): |
| 164 | """Gets the path for release kernel partition.""" |
| 165 | |
| 166 | return self.GetPrimaryDevicePath(4) |
| 167 | |
| 168 | def GetReleaseImageLsbData(self): |
| 169 | """Gets the /etc/lsb-release content from release image partition. |
| 170 | |
| 171 | Returns: |
| 172 | A dictionary containing the key-value pairs in lsb-release. |
| 173 | """ |
| 174 | lsb_content = sys_utils.MountDeviceAndReadFile( |
| 175 | self.GetReleaseRootPartitionPath(), 'etc/lsb-release') |
| 176 | return dict(re.findall('^(.+)=(.+)$', lsb_content, re.MULTILINE)) |
| 177 | |
| 178 | def GetAllowedReleaseImageChannels(self): |
| 179 | """Returns a list of channels allowed for release image.""" |
| 180 | return ['dev', 'beta', 'stable'] |
| 181 | |
| 182 | def GetReleaseImageChannel(self): |
| 183 | """Returns the channel of current release image.""" |
| 184 | return self.GetReleaseImageLsbData().get('CHROMEOS_RELEASE_TRACK') |
| 185 | |
| 186 | def GetReleaseImageVersion(self): |
| 187 | """Returns the current release image version.""" |
| 188 | return self.GetReleaseImageLsbData().get('GOOGLE_RELEASE') |
| 189 | |
| 190 | def GetVBSharedDataFlags(self): |
| 191 | """Gets VbSharedData flags. |
| 192 | |
| 193 | Returns: |
| 194 | An integer representation of the flags. |
| 195 | """ |
| 196 | |
| 197 | return int(self.shell('crossystem vdat_flags').stdout.strip(), 0) |
| 198 | |
| 199 | def GetCurrentDevSwitchPosition(self): |
| 200 | """Gets the position for the current developer switch. |
| 201 | |
| 202 | Returns: |
| 203 | An integer representation of the current developer switch position. |
| 204 | """ |
| 205 | return int(self.shell('crossystem devsw_cur').stdout.strip(), 0) |
| 206 | |
| 207 | def GetCrosSystem(self): |
| 208 | """Gets the output of 'crossystem'. |
| 209 | |
| 210 | Returns: |
| 211 | A dict for key-value pairs for the output of 'crossystem'. |
| 212 | e.g. {'flag_name': 'flag_value'} |
| 213 | """ |
| 214 | crossystem_result = self.shell('crossystem').stdout.strip().splitlines() |
| 215 | # The crossytem output contains many lines like: |
| 216 | # 'key = value # description' |
| 217 | # Use regexps to pull out the key-value pairs and build a dict. |
| 218 | # Note that value could also contain equal signs. |
| 219 | output = {} |
| 220 | for entry in crossystem_result: |
| 221 | # Any unrecognized format should fail here. |
| 222 | key, value = re.findall(r'\A(\S+)\s+=\s+(.*)#.*\Z', entry)[0] |
| 223 | output[key] = value.strip() |
| 224 | |
| 225 | return output |
| 226 | |
| 227 | def GetCgptAttributes(self, device=None): |
| 228 | if device is None: |
| 229 | device = self.GetPrimaryDevicePath() |
| 230 | |
| 231 | attrs = {} |
| 232 | for line in self.shell('cgpt show %s -q' % device).stdout.splitlines(): |
| 233 | # format: offset size no name |
| 234 | part_no = line.split()[2] |
| 235 | attrs[part_no] = self.shell('cgpt show %s -i %s -A' % |
| 236 | (device, part_no)).stdout.strip() |
| 237 | return attrs |
| 238 | |
| 239 | def SetCgptAttributes(self, attrs, device=None): |
| 240 | if device is None: |
| 241 | device = self.GetPrimaryDevicePath() |
| 242 | |
| 243 | curr_attrs = self.GetCgptAttributes() |
| 244 | for k, v in attrs.iteritems(): |
| 245 | if curr_attrs.get(k) == v: |
| 246 | continue |
| 247 | if not self.shell('cgpt add %s -i %s -A %s' % (device, k, v)).success: |
| 248 | raise Error('Failed to set device config: %s#%s=%s' % (device, k, v)) |
| 249 | |
| 250 | def InvokeChromeOSPostInstall(self, root_dev=None): |
| 251 | """Invokes the ChromeOS post-install script (/postinst).""" |
| 252 | if root_dev is None: |
| 253 | root_dev = self._util.GetReleaseRootPartitionPath() |
| 254 | |
| 255 | logging.info('Running ChromeOS post-install on %s...', root_dev) |
| 256 | |
| 257 | # Some compatible and experimental fs (e.g., ext4) may be buggy and still |
| 258 | # try to write the file system even if we mount it with "ro" (ex, when |
| 259 | # seeing journaling error in ext3, or s_kbytes_written in ext4). It is |
| 260 | # safer to always mount the partition with legacy ext2. (ref: |
| 261 | # chrome-os-partner:3940) |
| 262 | with sys_utils.MountPartition(root_dev, fstype='ext2') as mount_path: |
| 263 | # IS_FACTORY_INSTALL is used to prevent postinst trying to update firmare. |
| 264 | command = ('IS_FACTORY_INSTALL=1 IS_INSTALL=1 "%s"/postinst %s' % |
| 265 | (mount_path, root_dev)) |
| 266 | result = self.shell(command) |
| 267 | if not result.success: |
| 268 | raise Error('chromeos-postinst on %s failed with error: code=%s. %s' % |
| 269 | (root_dev, result.status, result.stderr)) |
| 270 | |
| 271 | def EnableKernel(self, device, part_no): |
| 272 | """Enables the kernel partition from GPT.""" |
| 273 | logging.info('Enabling kernel on %s#%s...', device, part_no) |
| 274 | r = self.shell('cgpt add -i %s -P 3 -S 1 -T 0 %s' % (part_no, device)) |
| 275 | if not r.success: |
| 276 | raise Error('Failed to enable kernel on %s#%s' % (device, part_no)) |
| 277 | |
| 278 | def DisableKernel(self, device, part_no): |
| 279 | """Disables the kernel partition from GPT.""" |
| 280 | logging.info('Disabling kernel on %s#%s...', device, part_no) |
| 281 | r = self.shell('cgpt add -i %s -P 0 -S 0 -T 0 %s' % (part_no, device)) |
| 282 | if not r.success: |
| 283 | raise Error('Failed to disable kernel on %s#%s' % (device, part_no)) |
| 284 | |
| 285 | def IsChromeOSFirmware(self): |
| 286 | """Returns if the system is running ChromeOS firmware.""" |
| 287 | r = self.shell('crossystem mainfw_type') |
| 288 | return r.success and r.stdout.strip() != 'nonchrome' |
| 289 | |
| 290 | def EnableReleasePartition(self, root_dev): |
| 291 | """Enables a release image partition on disk.""" |
| 292 | # TODO(hungte) replce sh/enable_release_partition.sh |
| 293 | release_no = int(root_dev[-1]) - 1 |
| 294 | factory_map = {2: 4, 4: 2} |
| 295 | if release_no not in factory_map: |
| 296 | raise ValueError('EnableReleasePartition: Cannot identify kernel %s' % |
| 297 | root_dev) |
| 298 | |
| 299 | factory_no = factory_map[release_no] |
| 300 | device = self.GetPartitionDevice(root_dev) |
| 301 | curr_attrs = self.GetCgptAttributes(device) |
| 302 | try: |
| 303 | # When booting with legacy firmware, we need to update the legacy boot |
| 304 | # loaders to activate new kernel; on a real ChromeOS firmware, only CGPT |
| 305 | # header is used, and postinst is already performed in verify_rootfs. |
| 306 | if self.IsChromeOSFirmware(): |
| 307 | self.InvokeChromeOSPostInstall(root_dev) |
| 308 | self.shell('crossystem disable_dev_request=1') |
| 309 | self.DisableKernel(device, factory_no) |
| 310 | self.EnableKernel(device, release_no) |
| 311 | # Enforce a sync and wait for underlying hardware to flush. |
| 312 | logging.info('Syncing disks...') |
| 313 | self.shell('sync; sleep 3') |
| 314 | logging.info('Enable release partition: Complete.') |
| 315 | except: # pylint: disable=bare-except |
| 316 | logging.error('FAIL: Failed to enable release partition.') |
| 317 | self.shell('crossystem disable_dev_request=0') |
| 318 | self.SetCgptAttributes(curr_attrs, device) |