Moblab: Servo Support via Host Attributes.

Moblab devices will look for servo_args inside a host's host
attributes.

Host attributes can be set via the CLI or the Web Frontend.

CLI to see attributes:
atest host stat <host>

CLI to set attribute:
atest host mod --attribute <attribute> --value <value> <host>

Updated the afe's management system so that it properly configures
the admin interface to allow editing of host attributes.

BUG=chromium:394544
TEST=local moblab setup. Tested both CLI and AFE host attribute
manipulation. Launched Servod, and ensure servo host is only
created when the attribute is applied to the host.

Change-Id: Ie3cccab31aa7518435ef0abc6ce206363406c272
Reviewed-on: https://chromium-review.googlesource.com/255550
Reviewed-by: Simran Basi <sbasi@chromium.org>
Commit-Queue: Simran Basi <sbasi@chromium.org>
Tested-by: Simran Basi <sbasi@chromium.org>
diff --git a/cli/host.py b/cli/host.py
index be1f318..53d572d 100644
--- a/cli/host.py
+++ b/cli/host.py
@@ -20,6 +20,7 @@
 See topic_common.py for a High Level Design and Algorithm.
 
 """
+import re
 
 from autotest_lib.cli import action_common, topic_common
 from autotest_lib.client.common_lib import host_protections
@@ -229,12 +230,12 @@
             acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
 
             labels = self.execute_rpc('get_labels', host__hostname=host)
-            results.append ([[stat], acls, labels])
+            results.append([[stat], acls, labels, stat['attributes']])
         return results
 
 
     def output(self, results):
-        for stats, acls, labels in results:
+        for stats, acls, labels, attributes in results:
             print '-'*5
             self.print_fields(stats,
                               keys=['hostname', 'platform',
@@ -243,6 +244,7 @@
             self.print_by_ids(acls, 'ACLs', line_before=True)
             labels = self._cleanup_labels(labels)
             self.print_by_ids(labels, 'Labels', line_before=True)
+            self.print_dict(attributes, 'Host Attributes', line_before=True)
 
 
 class host_jobs(host):
@@ -308,11 +310,14 @@
     """atest host mod --lock|--unlock|--protection
     --mlist <file>|<hosts>"""
     usage_action = 'mod'
+    attribute_regex = r'^(?P<attribute>\w+)=(?P<value>.+)?'
 
     def __init__(self):
         """Add the options specific to the mod action"""
         self.data = {}
         self.messages = []
+        self.attribute = None
+        self.value = None
         super(host_mod, self).__init__()
         self.parser.add_option('-l', '--lock',
                                help='Lock hosts',
@@ -326,6 +331,10 @@
                                      ', '.join('"%s"' % p
                                                for p in self.protections)),
                                choices=self.protections)
+        self.parser.add_option('--attribute', '-a', default='',
+                               help=('Host attribute to add or change. Format '
+                                     'is <attribute>=<value>. Value can be '
+                                     'blank to delete attribute.'))
 
 
     def parse(self):
@@ -338,8 +347,18 @@
             self.data['protection'] = options.protection
             self.messages.append('Protection set to "%s"' % options.protection)
 
-        if len(self.data) == 0:
+        if len(self.data) == 0 and not options.attribute:
             self.invalid_syntax('No modification requested')
+
+        if options.attribute:
+            match = re.match(self.attribute_regex, options.attribute)
+            if not match:
+                self.invalid_syntax('Attributes must be in <attribute>=<value>'
+                                    ' syntax!')
+
+            self.attribute = match.group('attribute')
+            self.value = match.group('value')
+
         return (options, leftover)
 
 
@@ -349,6 +368,10 @@
             try:
                 res = self.execute_rpc('modify_host', item=host,
                                        id=host, **self.data)
+                if self.attribute:
+                    self.execute_rpc('set_host_attribute',
+                                     attribute=self.attribute,
+                                     value=self.value, hostname=host)
                 # TODO: Make the AFE return True or False,
                 # especially for lock
                 successes.append(host)
diff --git a/cli/host_unittest.py b/cli/host_unittest.py
index 33a2103..a9bdd90 100755
--- a/cli/host_unittest.py
+++ b/cli/host_unittest.py
@@ -622,7 +622,8 @@
                               u'synch_id': None,
                               u'shard': None,
                               u'platform': u'plat1',
-                              u'id': 3}]),
+                              u'id': 3,
+                              u'attributes': {}}]),
                            ('get_hosts', {'hostname': 'host0'},
                             True,
                             [{u'status': u'Ready',
@@ -636,7 +637,8 @@
                               u'shard': None,
                               u'synch_id': None,
                               u'platform': u'plat0',
-                              u'id': 2}]),
+                              u'id': 2,
+                              u'attributes': {}}]),
                            ('get_acl_groups', {'hosts__hostname': 'host1'},
                             True,
                             [{u'description': u'',
@@ -697,7 +699,8 @@
                               u'invalid': False,
                               u'synch_id': None,
                               u'platform': u'plat0',
-                              u'id': 2}]),
+                              u'id': 2,
+                              u'attributes': {}}]),
                            ('get_acl_groups', {'hosts__hostname': 'host0'},
                             True,
                             [{u'description': u'',
@@ -747,7 +750,8 @@
                               u'invalid': False,
                               u'synch_id': None,
                               u'platform': u'plat0',
-                              u'id': 2}]),
+                              u'id': 2,
+                              u'attributes': {}}]),
                            ('get_acl_groups', {'hosts__hostname': 'host0'},
                             True,
                             [{u'description': u'',
@@ -795,7 +799,8 @@
                               u'invalid': False,
                               u'synch_id': None,
                               u'platform': u'plat1',
-                              u'id': 3},
+                              u'id': 3,
+                              u'attributes': {}},
                             {u'status': u'Ready',
                               u'hostname': u'host0',
                               u'locked': False,
@@ -806,7 +811,8 @@
                               u'invalid': False,
                               u'synch_id': None,
                               u'platform': u'plat0',
-                              u'id': 2}]),
+                              u'id': 2,
+                              u'attributes': {}}]),
                            ('get_acl_groups', {'hosts__hostname': 'host1'},
                             True,
                             [{u'description': u'',
@@ -865,7 +871,8 @@
                               u'invalid': False,
                               u'synch_id': None,
                               u'platform': u'plat0',
-                              u'id': 5}]),
+                              u'id': 5,
+                              u'attributes': {}}]),
                            ('get_hosts', {'hostname__startswith': 'ho'},
                             True,
                             [{u'status': u'Ready',
@@ -878,7 +885,8 @@
                               u'invalid': False,
                               u'synch_id': None,
                               u'platform': u'plat1',
-                              u'id': 3},
+                              u'id': 3,
+                              u'attributes': {}},
                             {u'status': u'Ready',
                               u'hostname': u'host0',
                               u'locked': False,
@@ -889,7 +897,8 @@
                               u'invalid': False,
                               u'synch_id': None,
                               u'platform': u'plat0',
-                              u'id': 2}]),
+                              u'id': 2,
+                              u'attributes': {}}]),
                            ('get_acl_groups', {'hosts__hostname': 'newhost0'},
                             True,
                             [{u'description': u'',
diff --git a/cli/topic_common.py b/cli/topic_common.py
index 2ee42b8..b191e6c 100644
--- a/cli/topic_common.py
+++ b/cli/topic_common.py
@@ -666,6 +666,23 @@
         return '  '.join(["%%-%ds" % lens[key] for key in keys])
 
 
+    def print_dict(self, items, title=None, line_before=False):
+        """Print a dictionary.
+
+        @param items: Dictionary to print.
+        @param title: Title of the output, default to None.
+        @param line_before: True to print an empty line before the output,
+                            default to False.
+        """
+        if not items:
+            return
+        if line_before:
+            print
+        print title
+        for key, value in items.items():
+            print '%s : %s' % (key, value)
+
+
     def print_table_std(self, items, keys_header, sublist_keys=()):
         """Print a mix of header and lists in a user readable format.
 
diff --git a/frontend/afe/management.py b/frontend/afe/management.py
index 70fd775..3e063ad 100644
--- a/frontend/afe/management.py
+++ b/frontend/afe/management.py
@@ -25,7 +25,7 @@
     PermissionModel = auth.models.Permission
     have_permissions = list(admin_group.permissions.all())
     for model_name in ('host', 'label', 'test', 'aclgroup', 'profiler',
-                       'atomicgroup'):
+                       'atomicgroup', 'hostattribute'):
         for permission_type in ('add', 'change', 'delete'):
             codename = permission_type + '_' + model_name
             permissions = list(PermissionModel.objects.filter(
diff --git a/server/hosts/servo_host.py b/server/hosts/servo_host.py
index c0f2a9b..cbe6099 100644
--- a/server/hosts/servo_host.py
+++ b/server/hosts/servo_host.py
@@ -25,10 +25,17 @@
 from autotest_lib.client.common_lib.cros.network import ping_runner
 from autotest_lib.server import site_utils as server_site_utils
 from autotest_lib.server.cros.servo import servo
+from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
 from autotest_lib.server.hosts import ssh_host
 from autotest_lib.site_utils.rpm_control_system import rpm_client
 
 
+# Names of the host attributes in the database that represent the values for
+# the servo_host and servo_port for a servo connected to the DUT.
+SERVO_HOST_ATTR = 'servo_host'
+SERVO_PORT_ATTR = 'servo_port'
+
+
 class ServoHostException(error.AutoservError):
     """This is the base class for exceptions raised by ServoHost."""
     pass
@@ -325,6 +332,8 @@
         @raises ServoHostVerifyFailure if /var/lib/servod/config does not exist.
 
         """
+        if self._is_localhost:
+            return
         try:
             self.run('test -f /var/lib/servod/config')
         except (error.AutoservRunError, error.AutoservSSHTimeout) as e:
@@ -644,8 +653,19 @@
     @returns: A ServoHost object or None. See comments above.
 
     """
-    lab_servo_hostname = make_servo_hostname(dut)
-    is_in_lab = utils.host_is_in_lab_zone(lab_servo_hostname)
+    if not utils.is_moblab():
+        lab_servo_hostname = make_servo_hostname(dut)
+        is_in_lab = utils.host_is_in_lab_zone(lab_servo_hostname)
+    else:
+        # Servos on Moblab are not in the actual lab.
+        is_in_lab = False
+        afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
+        hosts = afe.get_hosts(hostname=dut)
+        if hosts and SERVO_HOST_ATTR in hosts[0].attributes:
+            servo_args = {}
+            servo_args[SERVO_HOST_ATTR] = hosts[0].attributes[SERVO_HOST_ATTR]
+            servo_args[SERVO_PORT_ATTR] = hosts[0].attributes.get(
+                    SERVO_PORT_ATTR, 9999)
 
     if not is_in_lab:
         if servo_args is None: