• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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