Ricky Liang | f78dfcf | 2013-06-03 11:18:13 +0800 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | # |
| 3 | # Copyright (c) 2013 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 | """Common classes for HWID v3 operation.""" |
| 8 | |
| 9 | import collections |
| 10 | import copy |
| 11 | import os |
| 12 | import pprint |
| 13 | import re |
| 14 | |
| 15 | import factory_common # pylint: disable=W0611 |
| 16 | from cros.factory import common, schema, rule |
| 17 | from cros.factory.test import utils |
| 18 | |
| 19 | # The expected location of HWID data within a factory image or the |
| 20 | # chroot. |
| 21 | DEFAULT_HWID_DATA_PATH = ( |
| 22 | os.path.join(os.environ['CROS_WORKON_SRCROOT'], |
| 23 | 'src', 'platform', 'chromeos-hwid', 'v3') |
| 24 | if utils.in_chroot() |
| 25 | else '/usr/local/factory/hwid') |
| 26 | |
| 27 | |
| 28 | def ProbeBoard(): |
| 29 | """Probes the board name by looking up the CHROMEOS_RELEASE_BOARD variable |
| 30 | in /etc/lsb-release. |
| 31 | |
| 32 | Returns: |
| 33 | The probed board name as a string. |
| 34 | |
| 35 | Raises: |
| 36 | HWIDException when probe error. |
| 37 | """ |
| 38 | LSB_RELEASE_FILE = '/etc/lsb-release' |
| 39 | LSB_BOARD_RE = re.compile(r'^CHROMEOS_RELEASE_BOARD=(\w+)$', re.M) |
| 40 | if utils.in_chroot(): |
| 41 | raise HWIDException('Unable to determine board in chroot') |
| 42 | if not os.path.exists(LSB_RELEASE_FILE): |
| 43 | raise HWIDException('%r does not exist, unable to determine board' % |
| 44 | LSB_RELEASE_FILE) |
| 45 | try: |
| 46 | with open(LSB_RELEASE_FILE) as f: |
| 47 | board = LSB_BOARD_RE.findall(f.read())[0].rpartition('_')[-1] |
| 48 | except IndexError: |
| 49 | raise HWIDException('Cannot determine board from %r' % LSB_RELEASE_FILE) |
| 50 | return board |
| 51 | |
| 52 | |
| 53 | # A named tuple to store the probed component name and the error if any. |
| 54 | ProbedComponentResult = collections.namedtuple( |
| 55 | 'ProbedComponentResult', ['component_name', 'probed_values', 'error']) |
| 56 | |
| 57 | UNPROBEABLE_COMPONENT_ERROR = lambda comp_cls: ( |
| 58 | 'Component class %r is unprobeable' % comp_cls) |
| 59 | MISSING_COMPONENT_ERROR = lambda comp_cls: 'Missing %r component' % comp_cls |
| 60 | AMBIGUOUS_COMPONENT_ERROR = lambda comp_cls, probed_value, comp_names: ( |
| 61 | 'Ambiguous probe values %s of %r component. Possible components are: %r' % |
| 62 | (pprint.pformat(probed_value, indent=2), comp_cls, sorted(comp_names))) |
| 63 | UNSUPPORTED_COMPONENT_ERROR = lambda comp_cls, probed_value: ( |
| 64 | 'Unsupported %r component found with probe result %s ' |
| 65 | '(no matching name in the component DB)' % ( |
| 66 | comp_cls, pprint.pformat(probed_value, indent=2))) |
| 67 | |
| 68 | |
| 69 | class HWIDException(Exception): |
| 70 | pass |
| 71 | |
| 72 | |
| 73 | class HWID(object): |
| 74 | """A class that holds all the context of a HWID. |
| 75 | |
| 76 | It verifies the correctness of the HWID when a new HWID object is created. |
| 77 | This class is mainly for internal use. User should not create a HWID object |
| 78 | directly with the constructor. |
| 79 | |
| 80 | With bom (obatined from hardware prober) and board-specific component |
| 81 | database, HWID encoder can derive binary_string and encoded_string. |
| 82 | Reversely, with encoded_string and board-specific component database, HWID |
| 83 | decoder can derive binary_string and bom. |
| 84 | |
| 85 | Attributes: |
| 86 | database: A board-specific Database object. |
| 87 | binary_string: A binary string. Ex: "0000010010..." It is used for fast |
| 88 | component lookups as each component is represented by one or multiple |
| 89 | bits at fixed positions. |
| 90 | encoded_string: An encoded string with board name and checksum. For example: |
| 91 | "CHROMEBOOK ASDF-2345", where CHROMEBOOK is the board name and 45 is the |
| 92 | checksum. Compare to binary_string, it is human-trackable. |
| 93 | bom: A BOM object. |
| 94 | skip_check: Skips HWID self verification. This is needed when we want to |
| 95 | create a HWID object skeleton for further processing, e.g. a skeleton |
| 96 | HWID object to pass to rule evaluation to generate the final HWID. |
| 97 | Defaults to False. |
| 98 | |
| 99 | Raises: |
| 100 | HWIDException if an invalid arg is found. |
| 101 | """ |
| 102 | HEADER_BITS = 5 |
| 103 | COMPONENT_STATUS = utils.Enum(['supported', 'deprecated', 'unsupported']) |
| 104 | |
| 105 | def __init__(self, database, binary_string, encoded_string, bom, |
| 106 | skip_check=False): |
| 107 | self.database = database |
| 108 | self.binary_string = binary_string |
| 109 | self.encoded_string = encoded_string |
| 110 | self.bom = bom |
| 111 | if not skip_check: |
| 112 | self.VerifySelf() |
| 113 | |
| 114 | def VerifySelf(self): |
| 115 | """Verifies the HWID object itself. |
| 116 | |
| 117 | Raises: |
| 118 | HWIDException on verification error. |
| 119 | """ |
| 120 | # pylint: disable=W0404 |
| 121 | from cros.factory.hwid.decoder import BinaryStringToBOM |
| 122 | from cros.factory.hwid.decoder import EncodedStringToBinaryString |
| 123 | self.database.VerifyBOM(self.bom) |
| 124 | self.database.VerifyBinaryString(self.binary_string) |
| 125 | self.database.VerifyEncodedString(self.encoded_string) |
| 126 | if (EncodedStringToBinaryString(self.database, self.encoded_string) != |
| 127 | self.binary_string): |
| 128 | raise HWIDException( |
| 129 | 'Encoded string %s does not decode to binary string %r' % |
| 130 | (self.encoded_string, self.binary_string)) |
| 131 | if BinaryStringToBOM(self.database, self.binary_string) != self.bom: |
| 132 | def GetComponentsDifferences(decoded, target): |
| 133 | results = [] |
| 134 | for comp_cls in set( |
| 135 | decoded.components.keys() + target.components.keys()): |
| 136 | if comp_cls not in decoded.components: |
| 137 | results.append('Decoded: does not exist. BOM: %r' % |
| 138 | target.components[comp_cls]) |
| 139 | elif comp_cls not in target.components: |
| 140 | results.append('Decoded: %r. BOM: does not exist.' % |
| 141 | decoded.components[comp_cls]) |
| 142 | elif decoded.components[comp_cls] != target.components[comp_cls]: |
| 143 | results.append('Decoded: %r != BOM: %r' % |
| 144 | (decoded.components[comp_cls], target.components[comp_cls])) |
| 145 | return results |
| 146 | raise HWIDException( |
| 147 | 'Binary string %r does not decode to BOM. Differences: %r' % |
| 148 | (self.binary_string, GetComponentsDifferences( |
| 149 | self.bom, BinaryStringToBOM(self.database, self.binary_string)))) |
| 150 | # No exception. Everything is good! |
| 151 | |
| 152 | def VerifyComponentStatus(self, rma_mode=False): |
| 153 | """Verifies the status of all components. |
| 154 | |
| 155 | Accepts all 'supported' components, rejects all 'unsupported' components, |
| 156 | and accepts/rejects 'deprecated' components if rma_mode is True/False. |
| 157 | |
| 158 | Args: |
| 159 | rma_mode: Whether to verify components status in RMA mode. |
| 160 | |
| 161 | Raises: |
| 162 | HWIDException is verification fails. |
| 163 | """ |
| 164 | for comp_cls, comps in self.bom.components.iteritems(): |
| 165 | for comp in comps: |
| 166 | comp_name = comp.component_name |
| 167 | if not comp_name: |
| 168 | continue |
| 169 | |
| 170 | status = self.database.components.GetComponentStatus( |
| 171 | comp_cls, comp_name) |
| 172 | if status == HWID.COMPONENT_STATUS.supported: |
| 173 | continue |
| 174 | elif status == HWID.COMPONENT_STATUS.unsupported: |
| 175 | raise HWIDException('Found unsupported component of %r: %r' % |
| 176 | (comp_cls, comp_name)) |
| 177 | elif status == HWID.COMPONENT_STATUS.deprecated: |
| 178 | if not rma_mode: |
| 179 | raise HWIDException( |
| 180 | 'Not in RMA mode. Found deprecated component of %r: %r' % |
| 181 | (comp_cls, comp_name)) |
| 182 | |
| 183 | def VerifyProbeResult(self, probe_result): |
| 184 | """Verifies that the probe result matches the settings encoded in the HWID |
| 185 | object. |
| 186 | |
| 187 | Args: |
| 188 | probe_result: A YAML string of the probe result, which is usually the |
| 189 | output of the probe command. |
| 190 | |
| 191 | Raises: |
| 192 | HWIDException on verification error. |
| 193 | """ |
| 194 | self.database.VerifyComponents(probe_result) |
| 195 | probed_bom = self.database.ProbeResultToBOM(probe_result) |
| 196 | def PackProbedValues(bom, comp_cls): |
| 197 | results = [] |
| 198 | for e in bom.components[comp_cls]: |
| 199 | if e.probed_values is None: |
| 200 | continue |
| 201 | matched_component = self.database.components.MatchComponentsFromValues( |
| 202 | comp_cls, e.probed_values) |
| 203 | if matched_component: |
| 204 | results.extend(matched_component.keys()) |
| 205 | return results |
| 206 | for comp_cls in self.database.components.GetRequiredComponents(): |
| 207 | if comp_cls not in self.database.components.probeable: |
| 208 | continue |
| 209 | probed_components = common.MakeSet(PackProbedValues(probed_bom, comp_cls)) |
| 210 | expected_components = common.MakeSet(PackProbedValues(self.bom, comp_cls)) |
| 211 | extra_components = probed_components - expected_components |
| 212 | missing_components = expected_components - probed_components |
| 213 | if extra_components or missing_components: |
Ricky Liang | b8b1bf2 | 2013-06-07 17:11:38 +0800 | [diff] [blame] | 214 | err_msg = 'Component class %r' % comp_cls |
| 215 | if extra_components: |
| 216 | err_msg += ' has extra components: %r' % sorted(extra_components) |
| 217 | if missing_components: |
| 218 | if extra_components: |
| 219 | err_msg += ' and' |
| 220 | err_msg += ' is missing components: %r' % sorted(missing_components) |
| 221 | err_msg += '. Expected components are: %r' % ( |
| 222 | sorted(expected_components) if expected_components else None) |
| 223 | raise HWIDException(err_msg) |
Ricky Liang | f78dfcf | 2013-06-03 11:18:13 +0800 | [diff] [blame] | 224 | |
| 225 | def GetLabels(self): |
| 226 | """Gets from the database the labels of all the components encoded in this |
| 227 | HWID object. |
| 228 | |
| 229 | Returns: |
| 230 | A dict of the form: |
| 231 | { |
| 232 | 'component_class_1': { |
| 233 | 'component_name_1': { |
| 234 | 'label_key_1': 'LABEL_1_VALUE', |
| 235 | 'label_key_2': 'LABEL_2_VALUE', |
| 236 | ... |
| 237 | }, |
| 238 | ... |
| 239 | }, |
| 240 | 'component_class_2': { |
| 241 | 'component_name_2': None # No labels were defined on this component. |
| 242 | }, |
| 243 | ... |
| 244 | } |
| 245 | """ |
| 246 | results = collections.defaultdict(dict) |
| 247 | for comp_cls, comp_data in self.bom.components.iteritems(): |
| 248 | for comp_value in comp_data: |
| 249 | if comp_value.component_name: |
| 250 | db_comp_attrs = self.database.components.GetComponentAttributes( |
| 251 | comp_cls, comp_value.component_name) |
| 252 | results[comp_cls][comp_value.component_name] = copy.deepcopy( |
| 253 | db_comp_attrs.get('labels', None)) |
| 254 | return results |
| 255 | |
| 256 | |
| 257 | class BOM(object): |
| 258 | """A class that holds all the information regarding a BOM. |
| 259 | |
| 260 | Attributes: |
| 261 | board: A string of board name. |
| 262 | encoding_pattern_index: An int indicating the encoding pattern. Currently, |
| 263 | only 0 is used. |
| 264 | image_id: An int indicating the image id. |
| 265 | components: A dict that maps component classes to a list of |
| 266 | ProbedComponentResult. |
| 267 | encoded_fields: A dict that maps each encoded field to its index. |
| 268 | |
| 269 | Raises: |
| 270 | SchemaException if invalid argument format is found. |
| 271 | """ |
| 272 | _COMPONENTS_SCHEMA = schema.Dict( |
| 273 | 'bom', |
| 274 | key_type=schema.Scalar('component class', str), |
| 275 | value_type=schema.List( |
| 276 | 'list of ProbedComponentResult', |
| 277 | schema.Tuple('ProbedComponentResult', |
| 278 | [schema.Optional(schema.Scalar('component name', str)), |
| 279 | schema.Optional(schema.Dict('probed_values', |
| 280 | key_type=schema.Scalar('key', str), |
| 281 | value_type=schema.AnyOf([ |
| 282 | schema.Scalar('value', str), |
| 283 | schema.Scalar('value', rule.Value)]))), |
| 284 | schema.Optional(schema.Scalar('error', str))]))) |
| 285 | |
| 286 | def __init__(self, board, encoding_pattern_index, image_id, |
| 287 | components, encoded_fields): |
| 288 | self.board = board |
| 289 | self.encoding_pattern_index = encoding_pattern_index |
| 290 | self.image_id = image_id |
| 291 | self.components = components |
| 292 | self.encoded_fields = encoded_fields |
| 293 | BOM._COMPONENTS_SCHEMA.Validate(self.components) |
| 294 | |
| 295 | def Duplicate(self): |
| 296 | """Duplicates this BOM object. |
| 297 | |
| 298 | Returns: |
| 299 | A deepcopy of the original BOM object. |
| 300 | """ |
| 301 | return copy.deepcopy(self) |
| 302 | |
| 303 | def __eq__(self, op2): |
| 304 | return self.__dict__ == op2.__dict__ |
| 305 | |
| 306 | def __ne__(self, op2): |
| 307 | return not self.__eq__(op2) |