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