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