1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Django model for server database.
6"""
7
8from django.db import models as dbmodels
9
10import common
11from autotest_lib.client.common_lib import enum
12from autotest_lib.client.common_lib import error
13from autotest_lib.client.common_lib.cros.network import ping_runner
14from autotest_lib.frontend.afe import model_logic
15
16
17class Server(dbmodels.Model, model_logic.ModelExtensions):
18    """Models a server."""
19    DETAIL_FMT = ('Hostname     : %(hostname)s\n'
20                  'Status       : %(status)s\n'
21                  'Roles        : %(roles)s\n'
22                  'Attributes   : %(attributes)s\n'
23                  'Date Created : %(date_created)s\n'
24                  'Date Modified: %(date_modified)s\n'
25                  'Note         : %(note)s\n')
26
27    STATUS_LIST = ['primary', 'repair_required']
28    STATUS = enum.Enum(*STATUS_LIST, string_values=True)
29
30    hostname = dbmodels.CharField(unique=True, max_length=128)
31    cname = dbmodels.CharField(null=True, blank=True, default=None,
32                               max_length=128)
33    status = dbmodels.CharField(unique=False, max_length=128,
34                                choices=STATUS.choices())
35    date_created = dbmodels.DateTimeField(null=True, blank=True)
36    date_modified = dbmodels.DateTimeField(null=True, blank=True)
37    note = dbmodels.TextField(null=True, blank=True)
38
39    objects = model_logic.ExtendedManager()
40
41    class Meta:
42        """Metadata for class Server."""
43        db_table = 'servers'
44
45
46    def __unicode__(self):
47        """A string representation of the Server object.
48        """
49        roles = ','.join([r.role for r in self.roles.all()])
50        attributes = dict([(a.attribute, a.value)
51                           for a in self.attributes.all()])
52        return self.DETAIL_FMT % {'hostname': self.hostname,
53                                  'status': self.status,
54                                  'roles': roles,
55                                  'attributes': attributes,
56                                  'date_created': self.date_created,
57                                  'date_modified': self.date_modified,
58                                  'note': self.note}
59
60
61    def get_role_names(self):
62        """Get a list of role names of the server.
63
64        @return: A list of role names of the server.
65        """
66        return [r.role for r in self.roles.all()]
67
68
69    def get_details(self):
70        """Get a dictionary with all server details.
71
72        For example:
73        {
74            'hostname': 'server1',
75            'status': 'primary',
76            'roles': ['drone', 'scheduler'],
77            'attributes': {'max_processes': 300}
78        }
79
80        @return: A dictionary with all server details.
81        """
82        details = {}
83        details['hostname'] = self.hostname
84        details['status'] = self.status
85        details['roles'] = self.get_role_names()
86        attributes = dict([(a.attribute, a.value)
87                           for a in self.attributes.all()])
88        details['attributes'] = attributes
89        details['date_created'] = self.date_created
90        details['date_modified'] = self.date_modified
91        details['note'] = self.note
92        return details
93
94
95class ServerRole(dbmodels.Model, model_logic.ModelExtensions):
96    """Role associated with hosts."""
97    # Valid roles for a server.
98    ROLE_LIST = ['afe', 'scheduler', 'host_scheduler', 'drone', 'devserver',
99                 'database', 'database_slave', 'crash_server', 'shard',
100                 'golo_proxy', 'sentinel', 'reserve']
101    ROLE = enum.Enum(*ROLE_LIST, string_values=True)
102    # Roles that must be assigned to a single primary server in an Autotest
103    # instance
104    ROLES_REQUIRE_UNIQUE_INSTANCE = [ROLE.SCHEDULER,
105                                     ROLE.HOST_SCHEDULER,
106                                     ROLE.DATABASE]
107
108    server = dbmodels.ForeignKey(Server, related_name='roles')
109    role = dbmodels.CharField(max_length=128, choices=ROLE.choices())
110
111    objects = model_logic.ExtendedManager()
112
113    class Meta:
114        """Metadata for the ServerRole class."""
115        db_table = 'server_roles'
116
117
118class ServerAttribute(dbmodels.Model, model_logic.ModelExtensions):
119    """Attribute associated with hosts."""
120    server = dbmodels.ForeignKey(Server, related_name='attributes')
121    attribute = dbmodels.CharField(max_length=128)
122    value = dbmodels.TextField(null=True, blank=True)
123    date_modified = dbmodels.DateTimeField(null=True, blank=True)
124
125    objects = model_logic.ExtendedManager()
126
127    class Meta:
128        """Metadata for the ServerAttribute class."""
129        db_table = 'server_attributes'
130
131
132# Valid values for each type of input.
133RANGE_LIMITS={'role': ServerRole.ROLE_LIST,
134              'status': Server.STATUS_LIST}
135
136def validate(**kwargs):
137    """Verify command line arguments, raise InvalidDataError if any is invalid.
138
139    The function verify following inputs for the database query.
140    1. Any key in RANGE_LIMITS, i.e., role and status. Value should be a valid
141       role or status.
142    2. hostname. The code will try to resolve given hostname. If the hostname
143       does not exist in the network, InvalidDataError will be raised.
144    Sample usage of this function:
145    validate(role='drone', status='repair_required', hostname='server1')
146
147    @param kwargs: command line arguments, e.g., `status='primary'`
148    @raise InvalidDataError: If any argument value is invalid.
149    """
150    for key, value in kwargs.items():
151        # Ignore any None value, so callers won't need to filter out None
152        # value as it won't be used in queries.
153        if not value:
154            continue
155        if value not in RANGE_LIMITS.get(key, [value]):
156            raise error.InvalidDataError(
157                    '%s %s is not valid, it must be one of %s.' %
158                    (key, value,
159                     ', '.join(RANGE_LIMITS[key])))
160        elif key == 'hostname':
161            if not ping_runner.PingRunner().simple_ping(value):
162                raise error.InvalidDataError('Can not reach server with '
163                                             'hostname "%s".' % value)
164