[autotest] Add server database to django model.

Server database is added to store information about servers running in an
Autotest instance. This is the first CL to implement it. Design doc:
go/chromeos-lab-serverdb-design

django model uses db_router to rout database calls to different database
connections:
local: All AFE table calls.
global: All TKO table calls.
readonly: Calls from web frontend.

This CL adds another router for all calls to server database to `server`.

DEPLOY=migrate_server_db
CQ-DEPEND=CL:230814
BUG=chromium:424778
TEST=unitest, part of the test is done by atest code that'll be in another cl.
Test server database migration:
    ./database/migrate.py sync 0 -d AUTOTEST_SERVER_DB
    ./database/migrate.py sync 1 -d AUTOTEST_SERVER_DB
    ./database/migrate.py sync 2 -d AUTOTEST_SERVER_DB
    ./database/migrate.py sync 3 -d AUTOTEST_SERVER_DB
python frontend/health/utils_unittest.py

Change-Id: I84be386c8f5b7efd53ae1ecbd6293eae4326f19f
Reviewed-on: https://chromium-review.googlesource.com/231671
Tested-by: Dan Shi <dshi@chromium.org>
Reviewed-by: Fang Deng <fdeng@chromium.org>
Commit-Queue: Dan Shi <dshi@chromium.org>
diff --git a/frontend/server/__init__.py b/frontend/server/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/frontend/server/__init__.py
diff --git a/frontend/server/common.py b/frontend/server/common.py
new file mode 100644
index 0000000..1edf302
--- /dev/null
+++ b/frontend/server/common.py
@@ -0,0 +1,8 @@
+import os, sys
+dirname = os.path.dirname(sys.modules[__name__].__file__)
+autotest_dir = os.path.abspath(os.path.join(dirname, '..', '..'))
+client_dir = os.path.join(autotest_dir, "client")
+sys.path.insert(0, client_dir)
+import setup_modules
+sys.path.pop(0)
+setup_modules.setup(base_path=autotest_dir, root_module_name="autotest_lib")
diff --git a/frontend/server/models.py b/frontend/server/models.py
new file mode 100644
index 0000000..b473b9d
--- /dev/null
+++ b/frontend/server/models.py
@@ -0,0 +1,137 @@
+# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Django model for server database.
+"""
+
+import socket
+from django.db import models as dbmodels
+
+import common
+from autotest_lib.client.common_lib import base_utils as utils
+from autotest_lib.client.common_lib import enum
+from autotest_lib.client.common_lib import error
+from autotest_lib.frontend.afe import model_logic
+
+
+class Server(dbmodels.Model, model_logic.ModelExtensions):
+    """Models a server."""
+    DETAIL_FMT = ('Hostname     : %(hostname)s\n'
+                  'Status       : %(status)s\n'
+                  'Roles        : %(roles)s\n'
+                  'Attributes   : %(attributes)s\n'
+                  'Date Created : %(date_created)s\n'
+                  'Date Modified: %(date_modified)s\n'
+                  'Note         : %(note)s\n')
+
+    STATUS_LIST = ['primary', 'backup', 'repair_required']
+    STATUS = enum.Enum(*STATUS_LIST, string_values=True)
+
+    hostname = dbmodels.CharField(unique=True, max_length=128)
+    cname = dbmodels.CharField(null=True, blank=True, default=None,
+                               max_length=128)
+    status = dbmodels.CharField(unique=False, max_length=128,
+                                choices=STATUS.choices())
+    date_created = dbmodels.DateTimeField(null=True, blank=True)
+    date_modified = dbmodels.DateTimeField(null=True, blank=True)
+    note = dbmodels.TextField(null=True, blank=True)
+
+    objects = model_logic.ExtendedManager()
+
+    class Meta:
+        """Metadata for class Server."""
+        db_table = 'servers'
+
+
+    def __unicode__(self):
+        """A string representation of the Server object.
+        """
+        roles = ','.join([r.role for r in self.roles.all()])
+        attributes = dict([(a.attribute, a.value)
+                           for a in self.attributes.all()])
+        return self.DETAIL_FMT % {'hostname': self.hostname,
+                                  'status': self.status,
+                                  'roles': roles,
+                                  'attributes': attributes,
+                                  'date_created': self.date_created,
+                                  'date_modified': self.date_modified,
+                                  'note': self.note}
+
+
+class ServerRole(dbmodels.Model, model_logic.ModelExtensions):
+    """Role associated with hosts."""
+    # Valid roles for a server.
+    ROLE_LIST = ['scheduler', 'host_scheduler', 'drone', 'devserver',
+                  'database', 'suite_scheduler', 'crash_server']
+    ROLE = enum.Enum(*ROLE_LIST, string_values=True)
+    # When deleting any of following roles from a primary server, a working
+    # backup must be available if user_server_db is enabled in global config.
+    ROLES_REQUIRE_BACKUP = [ROLE.SCHEDULER, ROLE.HOST_SCHEDULER,
+                            ROLE.DATABASE, ROLE.SUITE_SCHEDULER,
+                            ROLE.DRONE]
+    # Roles that must be assigned to a single primary server in an Autotest
+    # instance
+    ROLES_REQUIRE_UNIQUE_INSTANCE = [ROLE.SCHEDULER,
+                                     ROLE.HOST_SCHEDULER,
+                                     ROLE.DATABASE,
+                                     ROLE.SUITE_SCHEDULER]
+
+    server = dbmodels.ForeignKey(Server, related_name='roles')
+    role = dbmodels.CharField(max_length=128, choices=ROLE.choices())
+
+    objects = model_logic.ExtendedManager()
+
+    class Meta:
+        """Metadata for the ServerRole class."""
+        db_table = 'server_roles'
+
+
+class ServerAttribute(dbmodels.Model, model_logic.ModelExtensions):
+    """Attribute associated with hosts."""
+    server = dbmodels.ForeignKey(Server, related_name='attributes')
+    attribute = dbmodels.CharField(max_length=128)
+    value = dbmodels.TextField(null=True, blank=True)
+    date_modified = dbmodels.DateTimeField(null=True, blank=True)
+
+    objects = model_logic.ExtendedManager()
+
+    class Meta:
+        """Metadata for the ServerAttribute class."""
+        db_table = 'server_attributes'
+
+
+# Valid values for each type of input.
+RANGE_LIMITS={'role': ServerRole.ROLE_LIST,
+              'status': Server.STATUS_LIST}
+
+def validate(**kwargs):
+    """Verify command line arguments, raise InvalidDataError if any is invalid.
+
+    The function verify following inputs for the database query.
+    1. Any key in RANGE_LIMITS, i.e., role and status. Value should be a valid
+       role or status.
+    2. hostname. The code will try to resolve given hostname. If the hostname
+       does not exist in the network, InvalidDataError will be raised.
+    Sample usage of this function:
+    validate(role='drone', status='backup', hostname='server1')
+
+    @param kwargs: command line arguments, e.g., `status='primary'`
+    @raise InvalidDataError: If any argument value is invalid.
+    """
+    for key, value in kwargs.items():
+        # Ignore any None value, so callers won't need to filter out None
+        # value as it won't be used in queries.
+        if not value:
+            continue
+        if value not in RANGE_LIMITS.get(key, [value]):
+            raise error.InvalidDataError(
+                    '%s %s is not valid, it must be one of %s.' %
+                    (key, value,
+                     ', '.join(RANGE_LIMITS[key])))
+        elif key == 'hostname':
+            try:
+                utils.normalize_hostname(value)
+            except socket.error:
+                raise error.InvalidDataError('Can not resolve hostname "%s".' %
+                                             value)