blob: b074aa8e6f7000dc50676633c3d1baa36caca318 [file] [log] [blame]
Tom Wai-Hong Tamc0168912012-09-13 13:24:02 +08001#!/usr/bin/python
2# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6'''A module to provide interface to ChromeOS services.'''
7
8import datetime
9import os
10import re
11import shutil
12import struct
13import subprocess
14import tempfile
15import time
16
17class ChromeOSInterfaceError(Exception):
18 '''ChromeOS interface specific exception.'''
19 pass
20
21class Crossystem(object):
22 '''A wrapper for the crossystem utility.'''
23
24 # Code dedicated for user triggering recovery mode through crossystem.
25 USER_RECOVERY_REQUEST_CODE = '193'
26
27 '''
28 The first three legacy boot vector digits are the boot vector base (the
29 entire vector consists of 5 digits). They used to be reported by the BIOS
30 through ACPI, but that scheme has been superseded by the 'crossystem'
31 interface.
32
33 The digits of the boot vector base have the following significance
34
35 - first digit -
36 1 - normal boot
37 2 - developer mode boot
38 3 - recovery initialed by pressing the recovery button
39 4 - recovery from developer mode warning screen
40 5 - recovery caused by both firmware images being invalid
41 6 - recovery caused by both kernel images being invalid
42 8 - recovery initiated by user
43
44 - second digit -
45 0 - recovery firmware
46 1 - rewritable firmware A
47 2 - rewritable firmware B
48
49 - third digit -
50 0 - Read only (recovery) EC firmware
51 1 - rewritable EC firmware
52
53
54 Below is a list of dictionaries to map current system state as reported by
55 'crossystem' into the 'legacy' boot vector digits.
56
57 The three elements of the list represent the three digits of the boot
58 vector. Each list element is a dictionary where the key is the legacy boot
59 vector value in the appropriate position, and the value is in turn a
60 dictionary of name-value pairs.
61
62 If all name-value pairs of a dictionary element match those reported by
63 crossystem, the legacy representation number is considered the appropriate
64 vector digit.
65
66 Note that on some platforms (namely, Mario) same parameters returned by
67 crossystem are set to a wrong value. The class init() routine adjust the
68 list to support those platforms.
69 '''
70
71 VECTOR_MAPS = [
72 { # first vector position
73 '1': {
74 'devsw_boot': '0',
75 'mainfw_type': 'normal',
76 'recoverysw_boot': '0',
77 },
78 '2': {
79 'devsw_boot': '1',
80 'mainfw_type': 'developer',
81 'recoverysw_boot': '0',
82 },
83 '3': {
84 'devsw_boot': '0',
85 'mainfw_type': 'recovery',
86 'recovery_reason' : '2',
87 'recoverysw_boot': '1',
88 },
89 '4': {
90 'devsw_boot': '1',
91 'mainfw_type': 'recovery',
92 'recovery_reason' : '65',
93 'recoverysw_boot': '0',
94 },
95 '5': {
96 'devsw_boot': '0',
97 'mainfw_type': 'recovery',
98 'recovery_reason' : ('3', '23', '27'),
99 'recoverysw_boot': '0',
100 },
101 '6': {
102 'devsw_boot': '0',
103 'mainfw_type': 'recovery',
104 'recovery_reason' : '66',
105 'recoverysw_boot': '0',
106 },
107 '8': {
108 'devsw_boot': '0',
109 'mainfw_type': 'recovery',
110 'recovery_reason' : USER_RECOVERY_REQUEST_CODE,
111 'recoverysw_boot': '0',
112 },
113 },
114 { # second vector position
115 '0': {'mainfw_type': 'recovery',},
116 '1': {'mainfw_act': 'A',},
117 '2': {'mainfw_act': 'B',},
118 },
119 { # third vector position
120 '0': {'ecfw_act': 'RO',},
121 '1': {'ecfw_act': 'RW',},
122 },
123 ]
124
125 def init(self, cros_if):
126 '''Init the instance. If running on Mario - adjust the map.'''
127
128 self.cros_if = cros_if
129
130 # Hack Alert!!! Adjust vector map to work on Mario
131 fwid = self.__getattr__('fwid').lower()
132 if not 'mario' in fwid:
133 return
134 # Mario firmware is broken and always reports recovery switch as set
135 # at boot time when booting up in recovery mode. This is why we
136 # exclude recoverysw_boot from the map when running on mario.
137 for state in self.VECTOR_MAPS[0].itervalues():
138 if state['mainfw_type'] != 'recovery':
139 continue
140 if 'recoverysw_boot' in state:
141 del(state['recoverysw_boot'])
142 if state['recovery_reason'] == self.USER_RECOVERY_REQUEST_CODE:
143 # This is the only recovery reason Mario knows about
144 state['recovery_reason'] = '1'
145
146 def __getattr__(self, name):
147 '''
148 Retrieve a crosssystem attribute.
149
150 Attempt to access crossystemobject.name will invoke `crossystem name'
151 and return the stdout as the value.
152 '''
153 return self.cros_if.run_shell_command_get_output(
154 'crossystem %s' % name)[0]
155
156 def __setattr__(self, name, value):
157 if name in ('cros_if',):
158 self.__dict__[name] = value
159 else:
160 self.cros_if.run_shell_command('crossystem "%s=%s"' % (name, value))
161
162 def request_recovery(self):
163 '''Request recovery mode next time the target reboots.'''
164
165 self.__setattr__('recovery_request', self.USER_RECOVERY_REQUEST_CODE)
166
167 def get_boot_vector_base(self):
168 '''Convert system state into a legacy boot vector base.
169
170 The function looks up the VECTOR_MAPS list above to find the digits
171 matching the current crossystem output, and returns a list of three
172 digits in symbolic representation, which become the base of the 5
173 digit boot state vector.
174
175 Should it be impossible to interpret the state, the function returns
176 a partially built list, which is an indication of a problem for the
177 caller (list shorter than 3 elements).
178 '''
179
180 boot_vector = []
181
182 for vector_map in self.VECTOR_MAPS:
183 for (digit, values) in vector_map.iteritems():
184 for (name, value) in values.iteritems():
185 # Get the actual attribute value from crossystem.
186 attr_value = self.__getattr__(name)
187 if isinstance(value, str):
188 if attr_value != value:
189 break
190 else:
191 # 'value' is a tuple of possible actual values.
192 if attr_value not in value:
193 break
194 else:
195 boot_vector.append(digit)
196 break
197
198 return boot_vector
199
200 def dump(self):
201 '''Dump all crossystem values as multiline text.'''
202
203 return '\n'.join(self.cros_if.run_shell_command_get_output(
204 'crossystem'))
205
206
207class ChromeOSInterface(object):
208 '''An object to encapsulate OS services functions.'''
209
210 def __init__(self, silent):
211 '''Object construction time initialization.
212
213 The only parameter is the Boolean 'silent', when True the instance
214 does not duplicate log messages on the console.
215 '''
216
217 self.silent = silent
218 self.state_dir = None
219 self.log_file = None
220 self.cs = Crossystem()
221
222 def init(self, state_dir=None, log_file=None):
223 '''Initialize the ChromeOS interface object.
224 Args:
225 state_dir - a string, the name of the directory (as defined by the
226 caller). The contents of this directory persist over
227 system restarts and power cycles.
228 log_file - a string, the name of the log file kept in the state
229 directory.
230
231 Default argument values support unit testing.
232 '''
233
234 self.cs.init(self)
235 self.state_dir = state_dir
236
237 if self.state_dir:
238 if not os.path.exists(self.state_dir):
239 try:
240 os.mkdir(self.state_dir)
241 except OSError, err:
242 raise ChromeOSInterfaceError(err)
243 if log_file:
244 if log_file[0] == '/':
245 self.log_file = log_file
246 else:
247 self.log_file = os.path.join(state_dir, log_file)
248
249 def target_hosted(self):
250 '''Return True if running on a ChromeOS target.'''
251 signature = open('/etc/lsb-release', 'r').readlines()[0]
252 return re.search(r'chrom(ium|e)os', signature, re.IGNORECASE) != None
253
254 def state_dir_file(self, file_name):
255 '''Get a full path of a file in the state directory.'''
256 return os.path.join(self.state_dir, file_name)
257
258 def init_environment(self):
259 '''Initialize Chrome OS interface environment.
260
261 If state dir was not set up by the constructor, create a temp
262 directory, otherwise create the directory defined during construction
263 of this object.
264
265 Return the state directory name.
266 '''
267
268 if not self.state_dir:
269 self.state_dir = tempfile.mkdtemp(suffix='_saft')
270 else:
271 # Wipe out state directory, to start the state machine clean.
272 shutil.rmtree(self.state_dir)
273 # And recreate it
274 self.init(self.state_dir, self.log_file)
275
276 return self.state_dir
277
278 def shut_down(self, new_log='/var/saft_log.txt'):
279 '''Destroy temporary environment so that the test can be restarted.'''
280 if os.path.exists(self.log_file):
281 shutil.copyfile(self.log_file, new_log)
282 shutil.rmtree(self.state_dir)
283
284 def log(self, text):
285 '''Write text to the log file and print it on the screen, if enabled.
286
287 The entire log (maintained across reboots) can be found in
288 self.log_file.
289 '''
290
291 # Don't print on the screen unless enabled.
292 if not self.silent:
293 print text
294
295 if not self.log_file or not os.path.exists(self.state_dir):
296 # Called before environment was initialized, ignore.
297 return
298
299 timestamp = datetime.datetime.strftime(
300 datetime.datetime.now(), '%I:%M:%S %p:')
301
302 log_f = open(self.log_file, 'a')
303 log_f.write('%s %s\n' % (timestamp, text))
304 log_f.close()
305
306 def exec_exists(self, program):
307 '''Check if the passed in string is a valid executable found in PATH.'''
308
309 for path in os.environ['PATH'].split(os.pathsep):
310 exe_file = os.path.join(path, program)
311 if (os.path.isfile(exe_file) or os.path.islink(exe_file)
312 ) and os.access(exe_file, os.X_OK):
313 return True
314 return False
315
316 def run_shell_command(self, cmd):
317 '''Run a shell command.
318
319 In case of the command returning an error print its stdout and stderr
320 outputs on the console and dump them into the log. Otherwise suppress all
321 output.
322
323 In case of command error raise an OSInterfaceError exception.
324
325 Return the subprocess.Popen() instance to provide access to console
326 output in case command succeeded.
327 '''
328
329 self.log('Executing %s' % cmd)
330 process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
331 stderr=subprocess.PIPE)
332 process.wait()
333 if process.returncode:
334 err = ['Failed running: %s' % cmd]
335 err.append('stdout:')
336 err.append(process.stdout.read())
337 err.append('stderr:')
338 err.append(process.stderr.read())
339 text = '\n'.join(err)
340 print text
341 self.log(text)
342 raise ChromeOSInterfaceError('command %s failed' % cmd)
343 return process
344
345 def is_removable_device(self, device):
346 '''Check if a certain storage device is removable.
347
348 device - a string, file name of a storage device or a device partition
349 (as in /dev/sda[0-9] or /dev/mmcblk0p[0-9]).
350
351 Returns True if the device is removable, False if not.
352 '''
353
354 if not self.target_hosted():
355 return False
356
357 # Drop trailing digit(s) and letter(s) (if any)
358 base_dev = self.strip_part(device.split('/')[2])
359 removable = int(open('/sys/block/%s/removable' % base_dev, 'r').read())
360
361 return removable == 1
362
363 def get_internal_disk(self, device):
364 '''Get the internal disk by given the current disk.
365
366 If device is removable device, internal disk is decided by which kind
367 of divice (arm or x86). Otherwise, return device itself.
368
369 device - a string, file name of a storage device or a device partition
370 (as in /dev/sda[0-9] or /dev/mmcblk0p[0-9]).
371
372 Return internal kernel disk.
373 '''
374 if self.is_removable_device(device):
375 if os.path.exists('/dev/mmcblk1'):
376 return '/dev/mmcblk1'
377 else:
378 return '/dev/sda'
379 else:
380 return self.strip_part(device)
381
382 def get_root_part(self):
383 '''Return a string, the name of root device with partition number'''
384 return self.run_shell_command_get_output('rootdev -s')[0]
385
386 def get_root_dev(self):
387 '''Return a string, the name of root device without partition number'''
388 return self.strip_part(self.get_root_part())
389
390 def join_part(self, dev, part):
391 '''Return a concatenated string of device and partition number'''
392 if 'mmcblk' in dev:
393 return dev + 'p' + part
394 else:
395 return dev + part
396
397 def strip_part(self, dev_with_part):
398 '''Return a stripped string without partition number'''
399 dev_name_stripper = re.compile('p?[0-9]+$')
400 return dev_name_stripper.sub('', dev_with_part)
401
402 def run_shell_command_get_output(self, cmd):
403 '''Run shell command and return its console output to the caller.
404
405 The output is returned as a list of strings stripped of the newline
406 characters.'''
407
408 process = self.run_shell_command(cmd)
409 return [x.rstrip() for x in process.stdout.readlines()]
410
411 def boot_state_vector(self):
412 '''Read and return to caller a string describing the system state.
413
414 The string has a form of x0:x1:x2:<removable>:<partition_number>,
415 where the field meanings of X# are described in the
416 Crossystem.get_boot_vector_base() docstring above.
417
418 <removable> is set to 1 or 0 depending if the root device is removable
419 or not, and <partition number> is the last element of the root device
420 name, designating the partition where the root fs is mounted.
421
422 This vector fully describes the way the system came up.
423 '''
424
425 state = self.cs.get_boot_vector_base()
426
427 if len(state) != 3:
428 raise ChromeOSInterfaceError(self.cs.dump())
429
430 root_part = self.get_root_part()
431 state.append('%d' % int(self.is_removable_device(root_part)))
432 state.append('%s' % root_part[-1])
433 state_str = ':'.join(state)
434 return state_str
435
436 def cmp_boot_vector(self, vector1, vector2):
437 '''Compare if the two boot vectors are the same
438
439 Note: a wildcard (*) will match any value.
440 '''
441 list1 = vector1.split(':')
442 list2 = vector2.split(':')
443 if len(list1) != len(list2):
444 raise ChromeOSInterfaceError(
445 'Boot vectors (%s %s) should be of the same length'
446 % (vecotr1, vector2))
447 for i in range(len(list1)):
448 if list1[i] != list2[i] and list1[i] != '*' and list2[i] != '*':
449 return False
450 return True
451
452 def get_writeable_mount_point(self, dev, tmp_dir):
453 '''Get mountpoint of the passed in device mounted in read/write mode.
454
455 If the device is already mounted and is writeable - return its mount
456 point. If the device is mounted but read-only - remount it read/write
457 and return its mount point. If the device is not mounted - mount it read
458 write on the passsed in path and return this path.
459 '''
460
461 # The device root file system is mounted on is represented as /dev/root
462 # otherwise.
463 options_filter = re.compile('.*\((.+)\).*')
464 root_part = self.get_root_part()
465 if dev == root_part:
466 dev = '/dev/root'
467
468 for line in self.run_shell_command_get_output('mount'):
469 if not line.startswith('%s ' % dev):
470 continue
471 mount_options = options_filter.match(line).groups(0)[0]
472 # found mounted
473 if 'ro' in mount_options.split(','):
474 # mounted read only
475 self.run_shell_command('mount -o remount,rw %s' % dev)
476 return line.split()[2] # Mountpoint is the third element.
477 # Not found, needs to be mounted
478 self.run_shell_command('mount %s %s' % (dev, tmp_dir))
479 return tmp_dir
480
481 def retrieve_body_version(self, blob):
482 '''Given a blob, retrieve body version.
483
484 Currently works for both, firmware and kernel blobs. Returns '-1' in
485 case the version can not be retrieved reliably.
486 '''
487 header_format = '<8s8sQ'
488 preamble_format = '<40sQ'
489 magic, _, kb_size = struct.unpack_from(header_format, blob)
490
491 if magic != 'CHROMEOS':
492 return -1 # This could be a corrupted version case.
493
494 _, version = struct.unpack_from(preamble_format, blob, kb_size)
495 return version
496
497 def retrieve_datakey_version(self, blob):
498 '''Given a blob, retrieve firmware data key version.
499
500 Currently works for both, firmware and kernel blobs. Returns '-1' in
501 case the version can not be retrieved reliably.
502 '''
503 header_format = '<8s96sQ'
504 magic, _, version = struct.unpack_from(header_format, blob)
505 if magic != 'CHROMEOS':
506 return -1 # This could be a corrupted version case.
507 return version
508
509 def retrieve_kernel_subkey_version(self, blob):
510 '''Given a blob, retrieve kernel subkey version.
511
512 It is in firmware vblock's preamble.
513 '''
514
515 header_format = '<8s8sQ'
516 preamble_format = '<72sQ'
517 magic, _, kb_size = struct.unpack_from(header_format, blob)
518
519 if magic != 'CHROMEOS':
520 return -1
521
522 _, version = struct.unpack_from(preamble_format, blob, kb_size)
523 return version
524
525 def retrieve_preamble_flags(self, blob):
526 '''Given a blob, retrieve preamble flags if available.
527
528 It only works for firmware. If the version of preamble header is less
529 than 2.1, no preamble flags supported, just returns 0.
530 '''
531 header_format = '<8s8sQ'
532 preamble_format = '<32sII64sI'
533 magic, _, kb_size = struct.unpack_from(header_format, blob)
534
535 if magic != 'CHROMEOS':
536 return -1 # This could be a corrupted version case.
537
538 _, ver, subver, _, flags = struct.unpack_from(preamble_format, blob,
539 kb_size)
540
541 if ver > 2 or (ver == 2 and subver >= 1):
542 return flags
543 else:
544 return 0 # Returns 0 if preamble flags not available.
545
546 def read_partition(self, partition, size):
547 '''Read the requested partition, up to size bytes.'''
548 tmp_file = self.state_dir_file('part.tmp')
549 self.run_shell_command('dd if=%s of=%s bs=1 count=%d' % (
550 partition, tmp_file, size))
551 fileh = open(tmp_file, 'r')
552 data = fileh.read()
553 fileh.close()
554 os.remove(tmp_file)
555 return data