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 autotest_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 = autotest_enum.AutotestEnum(*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    # TODO b:169251326 terms below are set outside of this codebase
99    # and should be updated when possible.
100    ROLE_LIST = [
101            'afe',
102            'crash_server',
103            'database',
104            'database_slave',
105            'devserver',
106            'drone',
107            'golo_proxy',
108            'host_scheduler',
109            'scheduler',
110            'sentinel',
111            'shard',
112            'skylab_drone',
113
114            'reserve',
115    ]
116    ROLE = autotest_enum.AutotestEnum(*ROLE_LIST, string_values=True)
117    # Roles that must be assigned to a single primary server in an Autotest
118    # instance
119    ROLES_REQUIRE_UNIQUE_INSTANCE = [ROLE.SCHEDULER,
120                                     ROLE.HOST_SCHEDULER,
121                                     ROLE.DATABASE]
122
123    server = dbmodels.ForeignKey(Server, related_name='roles')
124    role = dbmodels.CharField(max_length=128, choices=ROLE.choices())
125
126    objects = model_logic.ExtendedManager()
127
128    class Meta:
129        """Metadata for the ServerRole class."""
130        db_table = 'server_roles'
131
132
133class ServerAttribute(dbmodels.Model, model_logic.ModelExtensions):
134    """Attribute associated with hosts."""
135    server = dbmodels.ForeignKey(Server, related_name='attributes')
136    attribute = dbmodels.CharField(max_length=128)
137    value = dbmodels.TextField(null=True, blank=True)
138    date_modified = dbmodels.DateTimeField(null=True, blank=True)
139
140    objects = model_logic.ExtendedManager()
141
142    class Meta:
143        """Metadata for the ServerAttribute class."""
144        db_table = 'server_attributes'
145
146
147# Valid values for each type of input.
148RANGE_LIMITS={'role': ServerRole.ROLE_LIST,
149              'status': Server.STATUS_LIST}
150
151def validate(**kwargs):
152    """Verify command line arguments, raise InvalidDataError if any is invalid.
153
154    The function verify following inputs for the database query.
155    1. Any key in RANGE_LIMITS, i.e., role and status. Value should be a valid
156       role or status.
157    2. hostname. The code will try to resolve given hostname. If the hostname
158       does not exist in the network, InvalidDataError will be raised.
159    Sample usage of this function:
160    validate(role='drone', status='repair_required', hostname='server1')
161
162    @param kwargs: command line arguments, e.g., `status='primary'`
163    @raise InvalidDataError: If any argument value is invalid.
164    """
165    for key, value in kwargs.items():
166        # Ignore any None value, so callers won't need to filter out None
167        # value as it won't be used in queries.
168        if not value:
169            continue
170        if value not in RANGE_LIMITS.get(key, [value]):
171            raise error.InvalidDataError(
172                    '%s %s is not valid, it must be one of %s.' %
173                    (key, value,
174                     ', '.join(RANGE_LIMITS[key])))
175        elif key == 'hostname':
176            if not ping_runner.PingRunner().simple_ping(value):
177                raise error.InvalidDataError('Can not reach server with '
178                                             'hostname "%s".' % value)
179