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