blob: 80fd2d35eee9bed6805fcacf2b346d39ff095d9b [file] [log] [blame]
Hung-Te Lin0e0f9362015-11-18 18:18:05 +08001#!/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
7from __future__ import print_function
8
9import logging
Hung-Te Lin67893542016-04-14 12:34:15 +080010import os
11import pipes
12import re
Hung-Te Lin0e0f9362015-11-18 18:18:05 +080013from subprocess import Popen, PIPE
14
15import factory_common # pylint: disable=W0611
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +080016from cros.factory.utils import sys_utils
17from cros.factory.utils.type_utils import Error
Hung-Te Lin0e0f9362015-11-18 18:18:05 +080018from cros.factory.utils.type_utils import Obj
19
20
21# TODO(hungte) Deprecate this by dut.Shell
22def 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 Lin67893542016-04-14 12:34:15 +080028 cmd: Full shell command line as a string or list, which can contain
29 redirection (pipes, etc).
Hung-Te Lin0e0f9362015-11-18 18:18:05 +080030 stdin: String that will be passed as stdin to the command.
31 log: log command and result.
32 """
Hung-Te Lin67893542016-04-14 12:34:15 +080033 if not isinstance(cmd, basestring):
34 cmd = ' '.join(pipes.quote(param) for param in cmd)
Hung-Te Lin0e0f9362015-11-18 18:18:05 +080035 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 Chenfa2bccd2016-04-01 19:28:43 +080043
44
45class 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
Hung-Te Lincdb96522016-04-15 16:51:10 +0800107 def GetDevicePartition(self, device, partition):
108 """Returns a partition path from device path string.
109
110 /dev/sda, 1 => /dev/sda1.
111 /dev/mmcblk0p, 2 => /dev/mmcblk0p2.
112 """
113 return ('%sp%s' if device[-1].isdigit() else '%s%s') % (device, partition)
114
115
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800116 def FindScript(self, script_name):
117 """Finds the script under /usr/local/factory/sh
118
119 Args:
120 script_name: The name of the script to look for.
121
122 Returns:
123 The path of the found script.
124
125 Raises:
126 Error if the script is not found.
127 """
128
129 # __file__ is in /usr/local/factory/py/gooftool/__init__.py
130 factory_base = os.path.realpath(os.path.join(
131 os.path.dirname(os.path.realpath(__file__)), '..', '..'))
132 script_path = os.path.join(factory_base, 'sh', script_name)
133 if not os.path.isfile(script_path):
134 raise Error('Needed script %s does not exist.' % script_path)
135 return script_path
136
137 def FindAndRunScript(self, script_name, post_opts=None, pre_opts=None):
138 """Finds and runs the script with given options.
139
140 Args:
141 script_name: The name of the script to look up and run.
142 post_opts: A list of strings that will be appended in the command after
143 the script's name.
144 pre_opts: A list of strings that will be prepended in the command before
145 the script's name.
146
147 Returns:
148 The result of execusion.
149
150 Raises:
151 Error if execusion failed.
152 """
153
154 assert not post_opts or isinstance(post_opts, list)
155 assert not pre_opts or isinstance(pre_opts, list)
156
157 script = self.FindScript(script_name)
158 cmd = '%s %s %s' % (' '.join(pre_opts) if pre_opts else '',
159 script,
160 ' '.join(post_opts) if post_opts else '')
161 result = self.shell(cmd.strip())
162 if not result.success:
163 raise Error, '%r failed, stderr: %r' % (cmd, result.stderr)
164
165 return result
166
167 def GetReleaseRootPartitionPath(self):
168 """Gets the path for release root partition."""
169
170 return self.GetPrimaryDevicePath(5)
171
172 def GetReleaseKernelPartitionPath(self):
173 """Gets the path for release kernel partition."""
174
175 return self.GetPrimaryDevicePath(4)
176
Hung-Te Lincdb96522016-04-15 16:51:10 +0800177 def GetReleaseKernelPathFromRootPartition(self, rootfs_path):
178 """Gets the path for release kernel from given rootfs path.
179
180 This function assumes kernel partition is always located before rootfs.
181 """
182 device = self.GetPartitionDevice(rootfs_path)
183 kernel_index = int(rootfs_path[-1]) - 1
184 return self.GetDevicePartition(device, kernel_index)
185
Wei-Han Chenfa2bccd2016-04-01 19:28:43 +0800186 def GetReleaseImageLsbData(self):
187 """Gets the /etc/lsb-release content from release image partition.
188
189 Returns:
190 A dictionary containing the key-value pairs in lsb-release.
191 """
192 lsb_content = sys_utils.MountDeviceAndReadFile(
193 self.GetReleaseRootPartitionPath(), 'etc/lsb-release')
194 return dict(re.findall('^(.+)=(.+)$', lsb_content, re.MULTILINE))
195
196 def GetAllowedReleaseImageChannels(self):
197 """Returns a list of channels allowed for release image."""
198 return ['dev', 'beta', 'stable']
199
200 def GetReleaseImageChannel(self):
201 """Returns the channel of current release image."""
202 return self.GetReleaseImageLsbData().get('CHROMEOS_RELEASE_TRACK')
203
204 def GetReleaseImageVersion(self):
205 """Returns the current release image version."""
206 return self.GetReleaseImageLsbData().get('GOOGLE_RELEASE')
207
208 def GetVBSharedDataFlags(self):
209 """Gets VbSharedData flags.
210
211 Returns:
212 An integer representation of the flags.
213 """
214
215 return int(self.shell('crossystem vdat_flags').stdout.strip(), 0)
216
217 def GetCurrentDevSwitchPosition(self):
218 """Gets the position for the current developer switch.
219
220 Returns:
221 An integer representation of the current developer switch position.
222 """
223 return int(self.shell('crossystem devsw_cur').stdout.strip(), 0)
224
225 def GetCrosSystem(self):
226 """Gets the output of 'crossystem'.
227
228 Returns:
229 A dict for key-value pairs for the output of 'crossystem'.
230 e.g. {'flag_name': 'flag_value'}
231 """
232 crossystem_result = self.shell('crossystem').stdout.strip().splitlines()
233 # The crossytem output contains many lines like:
234 # 'key = value # description'
235 # Use regexps to pull out the key-value pairs and build a dict.
236 # Note that value could also contain equal signs.
237 output = {}
238 for entry in crossystem_result:
239 # Any unrecognized format should fail here.
240 key, value = re.findall(r'\A(\S+)\s+=\s+(.*)#.*\Z', entry)[0]
241 output[key] = value.strip()
242
243 return output
244
245 def GetCgptAttributes(self, device=None):
246 if device is None:
247 device = self.GetPrimaryDevicePath()
248
249 attrs = {}
250 for line in self.shell('cgpt show %s -q' % device).stdout.splitlines():
251 # format: offset size no name
252 part_no = line.split()[2]
253 attrs[part_no] = self.shell('cgpt show %s -i %s -A' %
254 (device, part_no)).stdout.strip()
255 return attrs
256
257 def SetCgptAttributes(self, attrs, device=None):
258 if device is None:
259 device = self.GetPrimaryDevicePath()
260
261 curr_attrs = self.GetCgptAttributes()
262 for k, v in attrs.iteritems():
263 if curr_attrs.get(k) == v:
264 continue
265 if not self.shell('cgpt add %s -i %s -A %s' % (device, k, v)).success:
266 raise Error('Failed to set device config: %s#%s=%s' % (device, k, v))
267
268 def InvokeChromeOSPostInstall(self, root_dev=None):
269 """Invokes the ChromeOS post-install script (/postinst)."""
270 if root_dev is None:
271 root_dev = self._util.GetReleaseRootPartitionPath()
272
273 logging.info('Running ChromeOS post-install on %s...', root_dev)
274
275 # Some compatible and experimental fs (e.g., ext4) may be buggy and still
276 # try to write the file system even if we mount it with "ro" (ex, when
277 # seeing journaling error in ext3, or s_kbytes_written in ext4). It is
278 # safer to always mount the partition with legacy ext2. (ref:
279 # chrome-os-partner:3940)
280 with sys_utils.MountPartition(root_dev, fstype='ext2') as mount_path:
281 # IS_FACTORY_INSTALL is used to prevent postinst trying to update firmare.
282 command = ('IS_FACTORY_INSTALL=1 IS_INSTALL=1 "%s"/postinst %s' %
283 (mount_path, root_dev))
284 result = self.shell(command)
285 if not result.success:
286 raise Error('chromeos-postinst on %s failed with error: code=%s. %s' %
287 (root_dev, result.status, result.stderr))
288
289 def EnableKernel(self, device, part_no):
290 """Enables the kernel partition from GPT."""
291 logging.info('Enabling kernel on %s#%s...', device, part_no)
292 r = self.shell('cgpt add -i %s -P 3 -S 1 -T 0 %s' % (part_no, device))
293 if not r.success:
294 raise Error('Failed to enable kernel on %s#%s' % (device, part_no))
295
296 def DisableKernel(self, device, part_no):
297 """Disables the kernel partition from GPT."""
298 logging.info('Disabling kernel on %s#%s...', device, part_no)
299 r = self.shell('cgpt add -i %s -P 0 -S 0 -T 0 %s' % (part_no, device))
300 if not r.success:
301 raise Error('Failed to disable kernel on %s#%s' % (device, part_no))
302
303 def IsChromeOSFirmware(self):
304 """Returns if the system is running ChromeOS firmware."""
305 r = self.shell('crossystem mainfw_type')
306 return r.success and r.stdout.strip() != 'nonchrome'
307
308 def EnableReleasePartition(self, root_dev):
309 """Enables a release image partition on disk."""
310 # TODO(hungte) replce sh/enable_release_partition.sh
311 release_no = int(root_dev[-1]) - 1
312 factory_map = {2: 4, 4: 2}
313 if release_no not in factory_map:
314 raise ValueError('EnableReleasePartition: Cannot identify kernel %s' %
315 root_dev)
316
317 factory_no = factory_map[release_no]
318 device = self.GetPartitionDevice(root_dev)
319 curr_attrs = self.GetCgptAttributes(device)
320 try:
321 # When booting with legacy firmware, we need to update the legacy boot
322 # loaders to activate new kernel; on a real ChromeOS firmware, only CGPT
323 # header is used, and postinst is already performed in verify_rootfs.
324 if self.IsChromeOSFirmware():
325 self.InvokeChromeOSPostInstall(root_dev)
326 self.shell('crossystem disable_dev_request=1')
327 self.DisableKernel(device, factory_no)
328 self.EnableKernel(device, release_no)
329 # Enforce a sync and wait for underlying hardware to flush.
330 logging.info('Syncing disks...')
331 self.shell('sync; sleep 3')
332 logging.info('Enable release partition: Complete.')
333 except: # pylint: disable=bare-except
334 logging.error('FAIL: Failed to enable release partition.')
335 self.shell('crossystem disable_dev_request=0')
336 self.SetCgptAttributes(curr_attrs, device)