[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/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)