xref: /aosp_15_r20/external/autotest/server/hosts/host_info.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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