xref: /aosp_15_r20/external/autotest/server/hosts/afe_store.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Copyright 2017 The Chromium OS Authors. All rights reserved.
2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
3*9c5db199SXin Li# found in the LICENSE file.
4*9c5db199SXin Li
5*9c5db199SXin Liimport logging
6*9c5db199SXin Li
7*9c5db199SXin Liimport common
8*9c5db199SXin Lifrom autotest_lib.frontend.afe.json_rpc import proxy as rpc_proxy
9*9c5db199SXin Lifrom autotest_lib.server.hosts import host_info
10*9c5db199SXin Lifrom autotest_lib.server.cros.dynamic_suite import frontend_wrappers
11*9c5db199SXin Li
12*9c5db199SXin Liclass AfeStore(host_info.CachingHostInfoStore):
13*9c5db199SXin Li    """Directly interact with the (given) AFE for host information."""
14*9c5db199SXin Li
15*9c5db199SXin Li    _RETRYING_AFE_TIMEOUT_MIN = 5
16*9c5db199SXin Li    _RETRYING_AFE_RETRY_DELAY_SEC = 10
17*9c5db199SXin Li
18*9c5db199SXin Li    def __init__(self, hostname, afe=None):
19*9c5db199SXin Li        """
20*9c5db199SXin Li        @param hostname: The name of the host for which we want to track host
21*9c5db199SXin Li                information.
22*9c5db199SXin Li        @param afe: A frontend.AFE object to make RPC calls. Will create one
23*9c5db199SXin Li                internally if None.
24*9c5db199SXin Li        """
25*9c5db199SXin Li        super(AfeStore, self).__init__()
26*9c5db199SXin Li        self._hostname = hostname
27*9c5db199SXin Li        self._afe = afe
28*9c5db199SXin Li        if self._afe is None:
29*9c5db199SXin Li            self._afe = frontend_wrappers.RetryingAFE(
30*9c5db199SXin Li                    timeout_min=self._RETRYING_AFE_TIMEOUT_MIN,
31*9c5db199SXin Li                    delay_sec=self._RETRYING_AFE_RETRY_DELAY_SEC)
32*9c5db199SXin Li
33*9c5db199SXin Li
34*9c5db199SXin Li    def __str__(self):
35*9c5db199SXin Li        return '%s[%s]' % (type(self).__name__, self._hostname)
36*9c5db199SXin Li
37*9c5db199SXin Li
38*9c5db199SXin Li    def _refresh_impl(self):
39*9c5db199SXin Li        """Obtains HostInfo directly from the AFE."""
40*9c5db199SXin Li        try:
41*9c5db199SXin Li            hosts = self._afe.get_hosts(hostname=self._hostname)
42*9c5db199SXin Li        except rpc_proxy.JSONRPCException as e:
43*9c5db199SXin Li            raise host_info.StoreError(e)
44*9c5db199SXin Li
45*9c5db199SXin Li        if not hosts:
46*9c5db199SXin Li            raise host_info.StoreError('No hosts founds with hostname: %s' %
47*9c5db199SXin Li                                       self._hostname)
48*9c5db199SXin Li
49*9c5db199SXin Li        if len(hosts) > 1:
50*9c5db199SXin Li            logging.warning(
51*9c5db199SXin Li                    'Found %d hosts with the name %s. Picking the first one.',
52*9c5db199SXin Li                    len(hosts), self._hostname)
53*9c5db199SXin Li        host = hosts[0]
54*9c5db199SXin Li        return host_info.HostInfo(host.labels, host.attributes)
55*9c5db199SXin Li
56*9c5db199SXin Li
57*9c5db199SXin Li    def _commit_impl(self, new_info):
58*9c5db199SXin Li        """Commits HostInfo back to the AFE.
59*9c5db199SXin Li
60*9c5db199SXin Li        @param new_info: The new HostInfo to commit.
61*9c5db199SXin Li        """
62*9c5db199SXin Li        # TODO(pprabhu) crbug.com/680322
63*9c5db199SXin Li        # This method has a potentially malignent race condition. We obtain a
64*9c5db199SXin Li        # copy of HostInfo from the AFE and then add/remove labels / attribtes
65*9c5db199SXin Li        # based on that. If another user tries to commit it's changes in
66*9c5db199SXin Li        # parallel, we'll end up with corrupted labels / attributes.
67*9c5db199SXin Li        old_info = self._refresh_impl()
68*9c5db199SXin Li        self._remove_labels_on_afe(
69*9c5db199SXin Li                list(set(old_info.labels) - set(new_info.labels)))
70*9c5db199SXin Li        self._add_labels_on_afe(
71*9c5db199SXin Li                list(set(new_info.labels) - set(old_info.labels)))
72*9c5db199SXin Li        self._update_attributes_on_afe(old_info.attributes, new_info.attributes)
73*9c5db199SXin Li
74*9c5db199SXin Li
75*9c5db199SXin Li    def _remove_labels_on_afe(self, labels):
76*9c5db199SXin Li        """Requests the AFE to remove the given labels.
77*9c5db199SXin Li
78*9c5db199SXin Li        @param labels: Remove these.
79*9c5db199SXin Li        """
80*9c5db199SXin Li        if not labels:
81*9c5db199SXin Li            return
82*9c5db199SXin Li
83*9c5db199SXin Li        logging.debug('removing labels: %s', labels)
84*9c5db199SXin Li        try:
85*9c5db199SXin Li            self._afe.run('host_remove_labels', id=self._hostname,
86*9c5db199SXin Li                          labels=labels)
87*9c5db199SXin Li        except rpc_proxy.JSONRPCException as e:
88*9c5db199SXin Li            raise host_info.StoreError(e)
89*9c5db199SXin Li
90*9c5db199SXin Li
91*9c5db199SXin Li    def _add_labels_on_afe(self, labels):
92*9c5db199SXin Li        """Requests the AFE to add the given labels.
93*9c5db199SXin Li
94*9c5db199SXin Li        @param labels: Add these.
95*9c5db199SXin Li        """
96*9c5db199SXin Li        if not labels:
97*9c5db199SXin Li            return
98*9c5db199SXin Li
99*9c5db199SXin Li        logging.info('adding labels: %s', labels)
100*9c5db199SXin Li        try:
101*9c5db199SXin Li            self._afe.run('host_add_labels', id=self._hostname, labels=labels)
102*9c5db199SXin Li        except rpc_proxy.JSONRPCException as e:
103*9c5db199SXin Li            raise host_info.StoreError(e)
104*9c5db199SXin Li
105*9c5db199SXin Li
106*9c5db199SXin Li    def _update_attributes_on_afe(self, old_attributes, new_attributes):
107*9c5db199SXin Li        """Updates host attributes on the afe to give dict.
108*9c5db199SXin Li
109*9c5db199SXin Li        @param old_attributes: The current attributes on AFE.
110*9c5db199SXin Li        @param new_attributes: The new host attributes dict to set to.
111*9c5db199SXin Li        """
112*9c5db199SXin Li        left_only, right_only, differing = _dict_diff(old_attributes,
113*9c5db199SXin Li                                                      new_attributes)
114*9c5db199SXin Li        for key in left_only:
115*9c5db199SXin Li            self._afe.set_host_attribute(key, None, hostname=self._hostname)
116*9c5db199SXin Li        for key in right_only | differing:
117*9c5db199SXin Li            self._afe.set_host_attribute(key, new_attributes[key],
118*9c5db199SXin Li                                         hostname=self._hostname)
119*9c5db199SXin Li
120*9c5db199SXin Li
121*9c5db199SXin Liclass AfeStoreKeepPool(AfeStore):
122*9c5db199SXin Li    """Interact with AFE for host information without deleting pool label."""
123*9c5db199SXin Li
124*9c5db199SXin Li    def _adjust_pool(self, old_info, new_info):
125*9c5db199SXin Li        """Adjust pool labels when calculating the labels to remove/add.
126*9c5db199SXin Li
127*9c5db199SXin Li        @param old_info: The HostInfo the host has previously, fetched from AFE.
128*9c5db199SXin Li        @param new_info: The HostInfo the host has after repair/provision.
129*9c5db199SXin Li
130*9c5db199SXin Li        @returns: A tuple of list (labels_to_remove, labels_to_add).
131*9c5db199SXin Li        """
132*9c5db199SXin Li        labels_to_remove = list(set(old_info.labels) - set(new_info.labels))
133*9c5db199SXin Li        labels_to_add = list(set(new_info.labels) - set(old_info.labels))
134*9c5db199SXin Li        pool_to_remove = [l for l in labels_to_remove if 'pool:' in l]
135*9c5db199SXin Li        pool_to_add = [l for l in labels_to_add if 'pool:' in l]
136*9c5db199SXin Li        if pool_to_remove and not pool_to_add:
137*9c5db199SXin Li            labels_to_remove = list(set(labels_to_remove) - set(pool_to_remove))
138*9c5db199SXin Li
139*9c5db199SXin Li        return labels_to_remove, labels_to_add
140*9c5db199SXin Li
141*9c5db199SXin Li    def _commit_impl(self, new_info):
142*9c5db199SXin Li        """Commits HostInfo back to the AFE.
143*9c5db199SXin Li
144*9c5db199SXin Li        @param new_info: The new HostInfo to commit.
145*9c5db199SXin Li
146*9c5db199SXin Li        It won't delete pool label if no pool label will be added later.
147*9c5db199SXin Li        """
148*9c5db199SXin Li        # TODO(pprabhu) crbug.com/680322
149*9c5db199SXin Li        # This method has a potentially malignent race condition. We obtain a
150*9c5db199SXin Li        # copy of HostInfo from the AFE and then add/remove labels / attribtes
151*9c5db199SXin Li        # based on that. If another user tries to commit it's changes in
152*9c5db199SXin Li        # parallel, we'll end up with corrupted labels / attributes.
153*9c5db199SXin Li        old_info = self._refresh_impl()
154*9c5db199SXin Li        labels_to_remove, labels_to_add = self._adjust_pool(old_info, new_info)
155*9c5db199SXin Li        self._remove_labels_on_afe(labels_to_remove)
156*9c5db199SXin Li        self._add_labels_on_afe(labels_to_add)
157*9c5db199SXin Li        self._update_attributes_on_afe(old_info.attributes, new_info.attributes)
158*9c5db199SXin Li
159*9c5db199SXin Li
160*9c5db199SXin Lidef _dict_diff(left_dict, right_dict):
161*9c5db199SXin Li    """Return the keys where the given dictionaries differ.
162*9c5db199SXin Li
163*9c5db199SXin Li    This function assumes that the values in the dictionary support checking for
164*9c5db199SXin Li    equality.
165*9c5db199SXin Li
166*9c5db199SXin Li    @param left_dict: The "left" dictionary in the diff.
167*9c5db199SXin Li    @param right_dict: The "right" dictionary in the diff.
168*9c5db199SXin Li    @returns: A 3-tuple (left_only, right_only, differing) of keys where
169*9c5db199SXin Li            left_only contains the keys that exist in left_dict only, right_only
170*9c5db199SXin Li            contains keys that exist in right_dict only and differing contains
171*9c5db199SXin Li            keys that exist in both, but where values differ.
172*9c5db199SXin Li    """
173*9c5db199SXin Li    left_keys = set(left_dict)
174*9c5db199SXin Li    right_keys = set(right_dict)
175*9c5db199SXin Li    differing_keys = {key for key in left_keys & right_keys
176*9c5db199SXin Li                      if left_dict[key] != right_dict[key]}
177*9c5db199SXin Li    return left_keys - right_keys, right_keys - left_keys, differing_keys
178