1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright 2016 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Lifrom __future__ import absolute_import 7*9c5db199SXin Lifrom __future__ import division 8*9c5db199SXin Lifrom __future__ import print_function 9*9c5db199SXin Li 10*9c5db199SXin Liimport abc 11*9c5db199SXin Liimport copy 12*9c5db199SXin Liimport json 13*9c5db199SXin Liimport logging 14*9c5db199SXin Li 15*9c5db199SXin Liimport common 16*9c5db199SXin Lifrom autotest_lib.server.cros import provision 17*9c5db199SXin Liimport six 18*9c5db199SXin Li 19*9c5db199SXin Li 20*9c5db199SXin Liclass HostInfo(object): 21*9c5db199SXin Li """Holds label/attribute information about a host as understood by infra. 22*9c5db199SXin Li 23*9c5db199SXin Li This class is the source of truth of label / attribute information about a 24*9c5db199SXin Li host for the test runner (autoserv) and the tests, *from the point of view 25*9c5db199SXin Li of the infrastructure*. 26*9c5db199SXin Li 27*9c5db199SXin Li Typical usage: 28*9c5db199SXin Li store = AfeHostInfoStore(...) 29*9c5db199SXin Li host_info = store.get() 30*9c5db199SXin Li update_somehow(host_info) 31*9c5db199SXin Li store.commit(host_info) 32*9c5db199SXin Li 33*9c5db199SXin Li Besides the @property listed below, the following rw variables are part of 34*9c5db199SXin Li the public API: 35*9c5db199SXin Li labels: The list of labels for this host. 36*9c5db199SXin Li attributes: The list of attributes for this host. 37*9c5db199SXin Li """ 38*9c5db199SXin Li 39*9c5db199SXin Li __slots__ = ['labels', 'attributes', 'stable_versions'] 40*9c5db199SXin Li 41*9c5db199SXin Li # Constants related to exposing labels as more semantic properties. 42*9c5db199SXin Li _BOARD_PREFIX = 'board' 43*9c5db199SXin Li _MODEL_PREFIX = 'model' 44*9c5db199SXin Li # sku was already used for perf labeling, but it's a human readable 45*9c5db199SXin Li # string (gen'd from HWID) and not the raw sku value, so avoiding collision 46*9c5db199SXin Li # with device-sku instead. 47*9c5db199SXin Li _DEVICE_SKU_PREFIX = 'device-sku' 48*9c5db199SXin Li _BRAND_CODE_PREFIX = 'brand-code' 49*9c5db199SXin Li _OS_PREFIX = 'os' 50*9c5db199SXin Li _POOL_PREFIX = 'pool' 51*9c5db199SXin Li # stable version constants 52*9c5db199SXin Li _SV_CROS_KEY = "cros" 53*9c5db199SXin Li _SV_FAFT_KEY = "faft" 54*9c5db199SXin Li _SV_FIRMWARE_KEY = "firmware" 55*9c5db199SXin Li _SV_SERVO_CROS_KEY = "servo-cros" 56*9c5db199SXin Li 57*9c5db199SXin Li _OS_VERSION_LABELS = ( 58*9c5db199SXin Li provision.CROS_VERSION_PREFIX, 59*9c5db199SXin Li provision.CROS_ANDROID_VERSION_PREFIX, 60*9c5db199SXin Li ) 61*9c5db199SXin Li 62*9c5db199SXin Li _VERSION_LABELS = _OS_VERSION_LABELS + ( 63*9c5db199SXin Li provision.FW_RO_VERSION_PREFIX, 64*9c5db199SXin Li provision.FW_RW_VERSION_PREFIX, 65*9c5db199SXin Li ) 66*9c5db199SXin Li 67*9c5db199SXin Li def __init__(self, labels=None, attributes=None, stable_versions=None): 68*9c5db199SXin Li """ 69*9c5db199SXin Li @param labels: (optional list) labels to set on the HostInfo. 70*9c5db199SXin Li @param attributes: (optional dict) attributes to set on the HostInfo. 71*9c5db199SXin Li @param stable_versions: (optional dict) stable version information to set on the HostInfo. 72*9c5db199SXin Li """ 73*9c5db199SXin Li self.labels = labels if labels is not None else [] 74*9c5db199SXin Li self.attributes = attributes if attributes is not None else {} 75*9c5db199SXin Li self.stable_versions = stable_versions if stable_versions is not None else {} 76*9c5db199SXin Li 77*9c5db199SXin Li 78*9c5db199SXin Li @property 79*9c5db199SXin Li def build(self): 80*9c5db199SXin Li """Retrieve the current build for the host. 81*9c5db199SXin Li 82*9c5db199SXin Li TODO(pprabhu) Make provision.py depend on this instead of the other way 83*9c5db199SXin Li around. 84*9c5db199SXin Li 85*9c5db199SXin Li @returns The first build label for this host (if there are multiple). 86*9c5db199SXin Li None if no build label is found. 87*9c5db199SXin Li """ 88*9c5db199SXin Li for label_prefix in self._OS_VERSION_LABELS: 89*9c5db199SXin Li build_labels = self._get_stripped_labels_with_prefix(label_prefix) 90*9c5db199SXin Li if build_labels: 91*9c5db199SXin Li return build_labels[0] 92*9c5db199SXin Li return None 93*9c5db199SXin Li 94*9c5db199SXin Li 95*9c5db199SXin Li @property 96*9c5db199SXin Li def board(self): 97*9c5db199SXin Li """Retrieve the board label value for the host. 98*9c5db199SXin Li 99*9c5db199SXin Li @returns: The (stripped) board label, or the empty string if no 100*9c5db199SXin Li label is found. 101*9c5db199SXin Li """ 102*9c5db199SXin Li return self.get_label_value(self._BOARD_PREFIX) 103*9c5db199SXin Li 104*9c5db199SXin Li 105*9c5db199SXin Li @property 106*9c5db199SXin Li def model(self): 107*9c5db199SXin Li """Retrieve the model label value for the host. 108*9c5db199SXin Li 109*9c5db199SXin Li @returns: The (stripped) model label, or the empty string if no 110*9c5db199SXin Li label is found. 111*9c5db199SXin Li """ 112*9c5db199SXin Li return self.get_label_value(self._MODEL_PREFIX) 113*9c5db199SXin Li 114*9c5db199SXin Li 115*9c5db199SXin Li @property 116*9c5db199SXin Li def device_sku(self): 117*9c5db199SXin Li """Retrieve the device_sku label value for the host. 118*9c5db199SXin Li 119*9c5db199SXin Li @returns: The (stripped) device_sku label, or the empty string if no 120*9c5db199SXin Li label is found. 121*9c5db199SXin Li """ 122*9c5db199SXin Li return self.get_label_value(self._DEVICE_SKU_PREFIX) 123*9c5db199SXin Li 124*9c5db199SXin Li @property 125*9c5db199SXin Li def brand_code(self): 126*9c5db199SXin Li """Retrieve the brand_code label value for the host. 127*9c5db199SXin Li 128*9c5db199SXin Li @returns: The (stripped) brand_code label, or the empty string if no 129*9c5db199SXin Li label is found. 130*9c5db199SXin Li """ 131*9c5db199SXin Li return self.get_label_value(self._BRAND_CODE_PREFIX) 132*9c5db199SXin Li 133*9c5db199SXin Li @property 134*9c5db199SXin Li def os(self): 135*9c5db199SXin Li """Retrieve the os for the host. 136*9c5db199SXin Li 137*9c5db199SXin Li @returns The os (str) or the empty string if no os label 138*9c5db199SXin Li exists. Returns the first matching os if mutiple labels 139*9c5db199SXin Li are found. 140*9c5db199SXin Li """ 141*9c5db199SXin Li return self.get_label_value(self._OS_PREFIX) 142*9c5db199SXin Li 143*9c5db199SXin Li 144*9c5db199SXin Li @property 145*9c5db199SXin Li def pools(self): 146*9c5db199SXin Li """Retrieve the set of pools for the host. 147*9c5db199SXin Li 148*9c5db199SXin Li @returns: set(str) of pool values. 149*9c5db199SXin Li """ 150*9c5db199SXin Li return set(self._get_stripped_labels_with_prefix(self._POOL_PREFIX)) 151*9c5db199SXin Li 152*9c5db199SXin Li 153*9c5db199SXin Li @property 154*9c5db199SXin Li def cros_stable_version(self): 155*9c5db199SXin Li """Retrieve the cros stable version 156*9c5db199SXin Li """ 157*9c5db199SXin Li return self.stable_versions.get(self._SV_CROS_KEY) 158*9c5db199SXin Li 159*9c5db199SXin Li @property 160*9c5db199SXin Li def faft_stable_version(self): 161*9c5db199SXin Li """Retrieve the faft stable version 162*9c5db199SXin Li """ 163*9c5db199SXin Li return self.stable_versions.get(self._SV_FAFT_KEY) 164*9c5db199SXin Li 165*9c5db199SXin Li @property 166*9c5db199SXin Li def firmware_stable_version(self): 167*9c5db199SXin Li """Retrieve the firmware stable version 168*9c5db199SXin Li """ 169*9c5db199SXin Li return self.stable_versions.get(self._SV_FIRMWARE_KEY) 170*9c5db199SXin Li 171*9c5db199SXin Li @property 172*9c5db199SXin Li def servo_cros_stable_version(self): 173*9c5db199SXin Li """Retrieve the servo cros stable verion 174*9c5db199SXin Li """ 175*9c5db199SXin Li return self.stable_versions.get(self._SV_SERVO_CROS_KEY) 176*9c5db199SXin Li 177*9c5db199SXin Li def get_label_value(self, prefix): 178*9c5db199SXin Li """Retrieve the value stored as a label with a well known prefix. 179*9c5db199SXin Li 180*9c5db199SXin Li @param prefix: The prefix of the desired label. 181*9c5db199SXin Li @return: For the first label matching 'prefix:value', returns value. 182*9c5db199SXin Li Returns '' if no label matches the given prefix. 183*9c5db199SXin Li """ 184*9c5db199SXin Li values = self._get_stripped_labels_with_prefix(prefix) 185*9c5db199SXin Li return values[0] if values else '' 186*9c5db199SXin Li 187*9c5db199SXin Li def has_label(self, name): 188*9c5db199SXin Li """Check if label is present. 189*9c5db199SXin Li 190*9c5db199SXin Li @param name: The name of the desired label. 191*9c5db199SXin Li @return: bool, True if present. 192*9c5db199SXin Li """ 193*9c5db199SXin Li for label in self.labels: 194*9c5db199SXin Li if label == name or label.startswith(name + ':'): 195*9c5db199SXin Li return True 196*9c5db199SXin Li return False 197*9c5db199SXin Li 198*9c5db199SXin Li def clear_version_labels(self, version_prefix=None): 199*9c5db199SXin Li """Clear all or a particular version label(s) for the host. 200*9c5db199SXin Li 201*9c5db199SXin Li @param version_prefix: The prefix label which needs to be cleared. 202*9c5db199SXin Li If this is set to None, all version labels will 203*9c5db199SXin Li be cleared. 204*9c5db199SXin Li """ 205*9c5db199SXin Li version_labels = ([version_prefix] if version_prefix else 206*9c5db199SXin Li self._VERSION_LABELS) 207*9c5db199SXin Li self.labels = [ 208*9c5db199SXin Li label for label in self.labels if 209*9c5db199SXin Li not any(label.startswith(prefix + ':') 210*9c5db199SXin Li for prefix in version_labels)] 211*9c5db199SXin Li 212*9c5db199SXin Li 213*9c5db199SXin Li def set_version_label(self, version_prefix, version): 214*9c5db199SXin Li """Sets the version label for the host. 215*9c5db199SXin Li 216*9c5db199SXin Li If a label with version_prefix exists, this updates the value for that 217*9c5db199SXin Li label, else appends a new label to the end of the label list. 218*9c5db199SXin Li 219*9c5db199SXin Li @param version_prefix: The prefix to use (without the infix ':'). 220*9c5db199SXin Li @param version: The version label value to set. 221*9c5db199SXin Li """ 222*9c5db199SXin Li full_prefix = _to_label_prefix(version_prefix) 223*9c5db199SXin Li new_version_label = full_prefix + version 224*9c5db199SXin Li for index, label in enumerate(self.labels): 225*9c5db199SXin Li if label.startswith(full_prefix): 226*9c5db199SXin Li self.labels[index] = new_version_label 227*9c5db199SXin Li return 228*9c5db199SXin Li else: 229*9c5db199SXin Li self.labels.append(new_version_label) 230*9c5db199SXin Li 231*9c5db199SXin Li 232*9c5db199SXin Li def _get_stripped_labels_with_prefix(self, prefix): 233*9c5db199SXin Li """Search for labels with the prefix and remove the prefix. 234*9c5db199SXin Li 235*9c5db199SXin Li e.g. 236*9c5db199SXin Li prefix = blah 237*9c5db199SXin Li labels = ['blah:a', 'blahb', 'blah:c', 'doo'] 238*9c5db199SXin Li returns: ['a', 'c'] 239*9c5db199SXin Li 240*9c5db199SXin Li @returns: A list of stripped labels. [] in case of no match. 241*9c5db199SXin Li """ 242*9c5db199SXin Li full_prefix = prefix + ':' 243*9c5db199SXin Li prefix_len = len(full_prefix) 244*9c5db199SXin Li return [label[prefix_len:] for label in self.labels 245*9c5db199SXin Li if label.startswith(full_prefix)] 246*9c5db199SXin Li 247*9c5db199SXin Li 248*9c5db199SXin Li def __str__(self): 249*9c5db199SXin Li return ('%s[Labels: %s, Attributes: %s, StableVersions: %s]' 250*9c5db199SXin Li % (type(self).__name__, self.labels, self.attributes, self.stable_versions)) 251*9c5db199SXin Li 252*9c5db199SXin Li 253*9c5db199SXin Li def __eq__(self, other): 254*9c5db199SXin Li if isinstance(other, type(self)): 255*9c5db199SXin Li return all([ 256*9c5db199SXin Li self.labels == other.labels, 257*9c5db199SXin Li self.attributes == other.attributes, 258*9c5db199SXin Li self.stable_versions == other.stable_versions, 259*9c5db199SXin Li ]) 260*9c5db199SXin Li else: 261*9c5db199SXin Li return NotImplemented 262*9c5db199SXin Li 263*9c5db199SXin Li 264*9c5db199SXin Li def __ne__(self, other): 265*9c5db199SXin Li return not (self == other) 266*9c5db199SXin Li 267*9c5db199SXin Li 268*9c5db199SXin Liclass StoreError(Exception): 269*9c5db199SXin Li """Raised when a CachingHostInfoStore operation fails.""" 270*9c5db199SXin Li 271*9c5db199SXin Li 272*9c5db199SXin Liclass CachingHostInfoStore(six.with_metaclass(abc.ABCMeta, object)): 273*9c5db199SXin Li """Abstract class to obtain and update host information from the infra. 274*9c5db199SXin Li 275*9c5db199SXin Li This class describes the API used to retrieve host information from the 276*9c5db199SXin Li infrastructure. The actual, uncached implementation to obtain / update host 277*9c5db199SXin Li information is delegated to the concrete store classes. 278*9c5db199SXin Li 279*9c5db199SXin Li We use two concrete stores: 280*9c5db199SXin Li AfeHostInfoStore: Directly obtains/updates the host information from 281*9c5db199SXin Li the AFE. 282*9c5db199SXin Li LocalHostInfoStore: Obtains/updates the host information from a local 283*9c5db199SXin Li file. 284*9c5db199SXin Li An extra store is provided for unittests: 285*9c5db199SXin Li InMemoryHostInfoStore: Just store labels / attributes in-memory. 286*9c5db199SXin Li """ 287*9c5db199SXin Li 288*9c5db199SXin Li def __init__(self): 289*9c5db199SXin Li self._private_cached_info = None 290*9c5db199SXin Li 291*9c5db199SXin Li 292*9c5db199SXin Li def get(self, force_refresh=False): 293*9c5db199SXin Li """Obtain (possibly cached) host information. 294*9c5db199SXin Li 295*9c5db199SXin Li @param force_refresh: If True, forces the cached HostInfo to be 296*9c5db199SXin Li refreshed from the store. 297*9c5db199SXin Li @returns: A HostInfo object. 298*9c5db199SXin Li """ 299*9c5db199SXin Li if force_refresh: 300*9c5db199SXin Li return self._get_uncached() 301*9c5db199SXin Li 302*9c5db199SXin Li # |_cached_info| access is costly, so do it only once. 303*9c5db199SXin Li info = self._cached_info 304*9c5db199SXin Li if info is None: 305*9c5db199SXin Li return self._get_uncached() 306*9c5db199SXin Li return info 307*9c5db199SXin Li 308*9c5db199SXin Li 309*9c5db199SXin Li def commit(self, info): 310*9c5db199SXin Li """Update host information in the infrastructure. 311*9c5db199SXin Li 312*9c5db199SXin Li @param info: A HostInfo object with the new information to set. You 313*9c5db199SXin Li should obtain a HostInfo object using the |get| or 314*9c5db199SXin Li |get_uncached| methods, update it as needed and then commit. 315*9c5db199SXin Li """ 316*9c5db199SXin Li logging.debug('Committing HostInfo to store %s', self) 317*9c5db199SXin Li try: 318*9c5db199SXin Li self._commit_impl(info) 319*9c5db199SXin Li self._cached_info = info 320*9c5db199SXin Li logging.debug('HostInfo updated to: %s', info) 321*9c5db199SXin Li except Exception: 322*9c5db199SXin Li self._cached_info = None 323*9c5db199SXin Li raise 324*9c5db199SXin Li 325*9c5db199SXin Li 326*9c5db199SXin Li @abc.abstractmethod 327*9c5db199SXin Li def _refresh_impl(self): 328*9c5db199SXin Li """Actual implementation to refresh host_info from the store. 329*9c5db199SXin Li 330*9c5db199SXin Li Concrete stores must implement this function. 331*9c5db199SXin Li @returns: A HostInfo object. 332*9c5db199SXin Li """ 333*9c5db199SXin Li raise NotImplementedError 334*9c5db199SXin Li 335*9c5db199SXin Li 336*9c5db199SXin Li @abc.abstractmethod 337*9c5db199SXin Li def _commit_impl(self, host_info): 338*9c5db199SXin Li """Actual implementation to commit host_info to the store. 339*9c5db199SXin Li 340*9c5db199SXin Li Concrete stores must implement this function. 341*9c5db199SXin Li @param host_info: A HostInfo object. 342*9c5db199SXin Li """ 343*9c5db199SXin Li raise NotImplementedError 344*9c5db199SXin Li 345*9c5db199SXin Li 346*9c5db199SXin Li def _get_uncached(self): 347*9c5db199SXin Li """Obtain freshly synced host information. 348*9c5db199SXin Li 349*9c5db199SXin Li @returns: A HostInfo object. 350*9c5db199SXin Li """ 351*9c5db199SXin Li logging.debug('Refreshing HostInfo using store %s', self) 352*9c5db199SXin Li logging.debug('Old host_info: %s', self._cached_info) 353*9c5db199SXin Li try: 354*9c5db199SXin Li info = self._refresh_impl() 355*9c5db199SXin Li self._cached_info = info 356*9c5db199SXin Li except Exception: 357*9c5db199SXin Li self._cached_info = None 358*9c5db199SXin Li raise 359*9c5db199SXin Li 360*9c5db199SXin Li logging.debug('New host_info: %s', info) 361*9c5db199SXin Li return info 362*9c5db199SXin Li 363*9c5db199SXin Li 364*9c5db199SXin Li @property 365*9c5db199SXin Li def _cached_info(self): 366*9c5db199SXin Li """Access the cached info, enforcing a deepcopy.""" 367*9c5db199SXin Li return copy.deepcopy(self._private_cached_info) 368*9c5db199SXin Li 369*9c5db199SXin Li 370*9c5db199SXin Li @_cached_info.setter 371*9c5db199SXin Li def _cached_info(self, info): 372*9c5db199SXin Li """Update the cached info, enforcing a deepcopy. 373*9c5db199SXin Li 374*9c5db199SXin Li @param info: The new info to update from. 375*9c5db199SXin Li """ 376*9c5db199SXin Li self._private_cached_info = copy.deepcopy(info) 377*9c5db199SXin Li 378*9c5db199SXin Li 379*9c5db199SXin Liclass InMemoryHostInfoStore(CachingHostInfoStore): 380*9c5db199SXin Li """A simple store that gives unittests direct access to backing data. 381*9c5db199SXin Li 382*9c5db199SXin Li Unittests can access the |info| attribute to obtain the backing HostInfo. 383*9c5db199SXin Li """ 384*9c5db199SXin Li 385*9c5db199SXin Li def __init__(self, info=None): 386*9c5db199SXin Li """Seed object with initial data. 387*9c5db199SXin Li 388*9c5db199SXin Li @param info: Initial backing HostInfo object. 389*9c5db199SXin Li """ 390*9c5db199SXin Li super(InMemoryHostInfoStore, self).__init__() 391*9c5db199SXin Li self.info = info if info is not None else HostInfo() 392*9c5db199SXin Li 393*9c5db199SXin Li 394*9c5db199SXin Li def __str__(self): 395*9c5db199SXin Li return '%s[%s]' % (type(self).__name__, self.info) 396*9c5db199SXin Li 397*9c5db199SXin Li def _refresh_impl(self): 398*9c5db199SXin Li """Return a copy of the private HostInfo.""" 399*9c5db199SXin Li return copy.deepcopy(self.info) 400*9c5db199SXin Li 401*9c5db199SXin Li 402*9c5db199SXin Li def _commit_impl(self, info): 403*9c5db199SXin Li """Copy HostInfo data to in-memory store. 404*9c5db199SXin Li 405*9c5db199SXin Li @param info: The HostInfo object to commit. 406*9c5db199SXin Li """ 407*9c5db199SXin Li self.info = copy.deepcopy(info) 408*9c5db199SXin Li 409*9c5db199SXin Li 410*9c5db199SXin Lidef get_store_from_machine(machine): 411*9c5db199SXin Li """Obtain the host_info_store object stuffed in the machine dict. 412*9c5db199SXin Li 413*9c5db199SXin Li The machine argument to jobs can be a string (a hostname) or a dict because 414*9c5db199SXin Li of legacy reasons. If we can't get a real store, return a stub. 415*9c5db199SXin Li """ 416*9c5db199SXin Li if isinstance(machine, dict): 417*9c5db199SXin Li return machine['host_info_store'] 418*9c5db199SXin Li else: 419*9c5db199SXin Li return InMemoryHostInfoStore() 420*9c5db199SXin Li 421*9c5db199SXin Li 422*9c5db199SXin Liclass DeserializationError(Exception): 423*9c5db199SXin Li """Raised when deserialization fails due to malformed input.""" 424*9c5db199SXin Li 425*9c5db199SXin Li 426*9c5db199SXin Li# Default serialzation version. This should be uprevved whenever a change to 427*9c5db199SXin Li# HostInfo is backwards incompatible, i.e. we can no longer correctly 428*9c5db199SXin Li# deserialize a previously serialized HostInfo. An example of such change is if 429*9c5db199SXin Li# a field in the HostInfo object is dropped. 430*9c5db199SXin Li_CURRENT_SERIALIZATION_VERSION = 1 431*9c5db199SXin Li 432*9c5db199SXin Li 433*9c5db199SXin Lidef json_serialize(info, file_obj, version=_CURRENT_SERIALIZATION_VERSION): 434*9c5db199SXin Li """Serialize the given HostInfo. 435*9c5db199SXin Li 436*9c5db199SXin Li @param info: A HostInfo object to serialize. 437*9c5db199SXin Li @param file_obj: A file like object to serialize info into. 438*9c5db199SXin Li @param version: Use a specific serialization version. Should mostly use the 439*9c5db199SXin Li default. 440*9c5db199SXin Li """ 441*9c5db199SXin Li info_json = { 442*9c5db199SXin Li 'serializer_version': version, 443*9c5db199SXin Li 'labels': info.labels, 444*9c5db199SXin Li 'attributes': info.attributes, 445*9c5db199SXin Li 'stable_versions': info.stable_versions, 446*9c5db199SXin Li } 447*9c5db199SXin Li return json.dump(info_json, file_obj, sort_keys=True, indent=4, 448*9c5db199SXin Li separators=(',', ': ')) 449*9c5db199SXin Li 450*9c5db199SXin Li 451*9c5db199SXin Lidef json_deserialize(file_obj): 452*9c5db199SXin Li """Deserialize a HostInfo from the given file. 453*9c5db199SXin Li 454*9c5db199SXin Li @param file_obj: a file like object containing a json_serialized()ed 455*9c5db199SXin Li HostInfo. 456*9c5db199SXin Li @returns: The deserialized HostInfo object. 457*9c5db199SXin Li """ 458*9c5db199SXin Li try: 459*9c5db199SXin Li deserialized_json = json.load(file_obj) 460*9c5db199SXin Li except ValueError as e: 461*9c5db199SXin Li raise DeserializationError(e) 462*9c5db199SXin Li 463*9c5db199SXin Li try: 464*9c5db199SXin Li return HostInfo(deserialized_json['labels'], 465*9c5db199SXin Li deserialized_json.get('attributes', {}), 466*9c5db199SXin Li deserialized_json.get('stable_versions', {})) 467*9c5db199SXin Li except KeyError as e: 468*9c5db199SXin Li raise DeserializationError('Malformed serialized host_info: %r' % e) 469*9c5db199SXin Li 470*9c5db199SXin Li 471*9c5db199SXin Lidef _to_label_prefix(prefix): 472*9c5db199SXin Li """Ensure that prefix has the expected format for label prefixes. 473*9c5db199SXin Li 474*9c5db199SXin Li @param prefix: The (str) prefix to sanitize. 475*9c5db199SXin Li @returns: The sanitized (str) prefix. 476*9c5db199SXin Li """ 477*9c5db199SXin Li return prefix if prefix.endswith(':') else prefix + ':' 478