[autotest] Route modify_host RPC to master when called on a shard.

Users mistakenly lock/unlock on a shard AFE page.
It incurs database inconsistency between master DB and the shard DB.
This CL makes modify_host / modify_hosts RPCs routed to the master
if they are called on a shard.

BUG=chromium:444600
TEST=Puppylab and unittest.

Change-Id: I97d0cfcd85fe8a399224028a4c5fdbe32910fb88
Reviewed-on: https://chromium-review.googlesource.com/297595
Commit-Ready: Mungyung Ryu <mkryu@google.com>
Tested-by: Mungyung Ryu <mkryu@google.com>
Reviewed-by: Fang Deng <fdeng@chromium.org>
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index 3959c71..a322149 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -268,31 +268,47 @@
                                   protection=protection).id
 
 
-@rpc_utils.forward_single_host_rpc_to_shard
-def modify_host(id, **data):
+@rpc_utils.route_rpc_to_master
+def modify_host(id, **kwargs):
     """Modify local attributes of a host.
 
     If this is called on the master, but the host is assigned to a shard, this
-    will also forward the call to the responsible shard. This means i.e. if a
-    host is being locked using this function, this change will also propagate to
-    shards.
+    will call `modify_host_local` RPC to the responsible shard. This means if
+    a host is being locked using this function, this change will also propagate
+    to shards.
+    When this is called on a shard, the shard just routes the RPC to the master
+    and does nothing.
 
     @param id: id of the host to modify.
-    @param **data: key=value pairs of values to set on the host.
+    @param kwargs: key=value pairs of values to set on the host.
     """
-    rpc_utils.check_modify_host(data)
+    rpc_utils.check_modify_host(kwargs)
     host = models.Host.smart_get(id)
+    rpc_utils.check_modify_host_locking(host, kwargs)
 
-    rpc_utils.check_modify_host_locking(host, data)
-    host.update_object(data)
+    rpc_utils.fanout_rpc([host], 'modify_host_local',
+                         include_hostnames=False, id=id, **kwargs)
+    host.update_object(kwargs)
 
 
+def modify_host_local(id, **kwargs):
+    """Modify host attributes in local DB.
+
+    @param id: Host id.
+    @param kwargs: key=value pairs of values to set on the host.
+    """
+    models.Host.smart_get(id).update_object(kwargs)
+
+
+@rpc_utils.route_rpc_to_master
 def modify_hosts(host_filter_data, update_data):
     """Modify local attributes of multiple hosts.
 
     If this is called on the master, but one of the hosts in that match the
-    filters is assigned to a shard, this will also forward the call to the
-    responsible shard.
+    filters is assigned to a shard, this will call `modify_hosts_local` RPC
+    to the responsible shard.
+    When this is called on a shard, the shard just routes the RPC to the master
+    and does nothing.
 
     The filters are always applied on the master, not on the shards. This means
     if the states of a host differ on the master and a shard, the state on the
@@ -321,10 +337,9 @@
             affected_shard_hostnames.add(host.shard.rpc_hostname())
             affected_host_ids.append(host.id)
 
-    if not utils.is_shard():
-        # Caution: Changing the filter from the original here. See docstring.
-        rpc_utils.run_rpc_on_multiple_hostnames(
-            'modify_hosts', affected_shard_hostnames,
+    # Caution: Changing the filter from the original here. See docstring.
+    rpc_utils.run_rpc_on_multiple_hostnames(
+            'modify_hosts_local', affected_shard_hostnames,
             host_filter_data={'id__in': affected_host_ids},
             update_data=update_data)
 
@@ -332,6 +347,16 @@
         host.update_object(update_data)
 
 
+def modify_hosts_local(host_filter_data, update_data):
+    """Modify attributes of hosts in local DB.
+
+    @param host_filter_data: Filters out which hosts to modify.
+    @param update_data: A dictionary with the changes to make to the hosts.
+    """
+    for host in models.Host.query_objects(host_filter_data):
+        host.update_object(update_data)
+
+
 def add_labels_to_host(id, labels):
     """Adds labels to a given host only in local DB.