[autotest] Forward set_host_attribute RPC

set_host_attribute RPC should be forwarded to shards of affected
hosts to update not only master DB but also the shards' DB.

BUG=chromium:492822
TEST=puppylab. Run ./atest/host mod -a testattr=hello1 test_host11
on the master VM. test_host11 is a host of stumpy shard.
Check if its host attribute has been affected in both master DB
and stumpy shard DB.
DEPLOY=apache

Change-Id: I1fb940ce92df8ca9afe1986cccc517b09c96f43e
Reviewed-on: https://chromium-review.googlesource.com/273947
Reviewed-by: Dan Shi <dshi@chromium.org>
Commit-Queue: Mungyung Ryu <mkryu@google.com>
Tested-by: Mungyung Ryu <mkryu@google.com>
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index 12b3a28..e70a7a6 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -146,7 +146,6 @@
             raise ValueError('Label id (%s) does not exist. Please specify '
                              'the argument, id, as a string (label name).'
                              % id)
-    add_label_to_hosts(id, hosts)
 
     host_objs = models.Host.smart_get_bulk(hosts)
     # Make sure the label exists on the shard with the same id
@@ -165,6 +164,8 @@
             include_hostnames=False, ignore_exception_if_exists=True)
     rpc_utils.fanout_rpc(host_objs, 'add_label_to_hosts', id=id)
 
+    add_label_to_hosts(id, hosts)
+
 
 def remove_label_from_hosts(id, hosts):
     """Removes a label of the given id from the given hosts only in local DB.
@@ -189,10 +190,11 @@
         rpc_utils.route_rpc_to_master('label_remove_hosts', id=id, hosts=hosts)
         return
 
-    remove_label_from_hosts(id, hosts)
     host_objs = models.Host.smart_get_bulk(hosts)
     rpc_utils.fanout_rpc(host_objs, 'remove_label_from_hosts', id=id)
 
+    remove_label_from_hosts(id, hosts)
+
 
 def get_labels(exclude_filters=(), **filter_data):
     """\
@@ -352,15 +354,20 @@
 
 def set_host_attribute(attribute, value, **host_filter_data):
     """
-    @param attribute string name of attribute
-    @param value string, or None to delete an attribute
-    @param host_filter_data filter data to apply to Hosts to choose hosts to act
-    upon
+    @param attribute: string name of attribute
+    @param value: string, or None to delete an attribute
+    @param host_filter_data: filter data to apply to Hosts to choose hosts to
+                             act upon
     """
     assert host_filter_data # disallow accidental actions on all hosts
     hosts = models.Host.query_objects(host_filter_data)
     models.AclGroup.check_for_acl_violation_hosts(hosts)
 
+    # Master forwards this RPC to shards.
+    if not utils.is_shard():
+        rpc_utils.fanout_rpc(hosts, 'set_host_attribute', False,
+                attribute=attribute, value=value, **host_filter_data)
+
     for host in hosts:
         host.set_or_delete_attribute(attribute, value)