[14194] | 1 | # This file is part of CMT, a Cluster Management Tool made at SARA. |
---|
| 2 | # Copyright (C) 2012 Sil Westerveld |
---|
| 3 | # |
---|
| 4 | # This program is free software; you can redistribute it and/or modify |
---|
| 5 | # it under the terms of the GNU General Public License as published by |
---|
| 6 | # the Free Software Foundation; either version 2 of the License, or |
---|
| 7 | # (at your option) any later version. |
---|
| 8 | # |
---|
| 9 | # This program is distributed in the hope that it will be useful, |
---|
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
| 12 | # GNU General Public License for more details. |
---|
| 13 | # |
---|
| 14 | # You should have received a copy of the GNU General Public License |
---|
| 15 | # along with this program; if not, write to the Free Software |
---|
| 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
---|
| 17 | |
---|
[10765] | 18 | from django.db import models |
---|
[11811] | 19 | from django.core.validators import RegexValidator |
---|
| 20 | |
---|
| 21 | import re |
---|
[12336] | 22 | from datetime import date |
---|
[11811] | 23 | |
---|
[11510] | 24 | from psycopg2 import IntegrityError |
---|
[10765] | 25 | |
---|
| 26 | from IPy import IP |
---|
| 27 | |
---|
| 28 | from sara_cmt.logger import Logger |
---|
| 29 | logger = Logger().getLogger() |
---|
| 30 | |
---|
| 31 | from sara_cmt.django_cli import ModelExtension |
---|
| 32 | |
---|
[10899] | 33 | from tagging.fields import TagField |
---|
[11657] | 34 | from django_extensions.db.fields import CreationDateTimeField, \ |
---|
| 35 | ModificationDateTimeField |
---|
[10765] | 36 | |
---|
| 37 | |
---|
[11103] | 38 | |
---|
[10999] | 39 | ###### |
---|
| 40 | # |
---|
| 41 | # Classes of sara_cmt.core |
---|
| 42 | # |
---|
[11657] | 43 | |
---|
| 44 | |
---|
[10999] | 45 | class Cluster(ModelExtension): |
---|
[12967] | 46 | """ |
---|
| 47 | A labeled group of hardware pieces. |
---|
| 48 | """ |
---|
[11657] | 49 | name = models.CharField(max_length=255, unique=True) |
---|
[10765] | 50 | |
---|
[11657] | 51 | class Meta: |
---|
[12100] | 52 | ordering = ('name',) |
---|
[10765] | 53 | |
---|
[12972] | 54 | def __unicode__(self): |
---|
[11657] | 55 | return self.name or None |
---|
[10765] | 56 | |
---|
[10857] | 57 | |
---|
[10999] | 58 | class HardwareUnit(ModelExtension): |
---|
[12967] | 59 | """ |
---|
| 60 | A specific piece of hardware. |
---|
| 61 | """ |
---|
[13004] | 62 | STATE_CHOICES = ( |
---|
| 63 | ('new', 'new'), |
---|
| 64 | ('clean', 'clean'), |
---|
| 65 | ('configured', 'configured'), |
---|
| 66 | ('unknown', 'unknown'), |
---|
| 67 | ('off', 'off')) |
---|
| 68 | |
---|
[11657] | 69 | cluster = models.ForeignKey('Cluster', related_name='hardware') |
---|
| 70 | role = models.ManyToManyField('Role', related_name='hardware') |
---|
| 71 | network = models.ManyToManyField('Network', related_name='hardware', |
---|
| 72 | through='Interface') |
---|
| 73 | specifications = models.ForeignKey('HardwareModel', |
---|
| 74 | related_name='hardware', null=True, |
---|
| 75 | blank=True) |
---|
| 76 | warranty = models.ForeignKey('WarrantyContract', |
---|
| 77 | related_name='hardware', null=True, |
---|
| 78 | blank=True) |
---|
| 79 | rack = models.ForeignKey('Rack', related_name='contents') |
---|
[13004] | 80 | seller = models.ForeignKey('Connection', related_name='sold', null=True, blank=True) |
---|
| 81 | owner = models.ForeignKey('Connection', related_name='owns', null=True, blank=True) |
---|
[13073] | 82 | state = models.CharField(max_length=10, null=True, blank=True, choices=STATE_CHOICES, default='unknown') |
---|
[11844] | 83 | warranty_tag = models.CharField(max_length=255, blank=True, null=True, |
---|
[13004] | 84 | help_text='Service tag', |
---|
[11844] | 85 | unique=True) |
---|
[13004] | 86 | serial_number = models.CharField(max_length=255, blank=True, null=True, unique=True) |
---|
[11741] | 87 | first_slot = models.PositiveIntegerField(blank=True, null=True) |
---|
[11657] | 88 | label = models.CharField(max_length=255) |
---|
[10765] | 89 | |
---|
[11657] | 90 | class Meta: |
---|
| 91 | #verbose_name = "piece of hardware" |
---|
| 92 | verbose_name_plural = "hardware" |
---|
[12102] | 93 | #ordering = ('cluster__name', 'rack__label', 'first_slot') |
---|
| 94 | ordering = ('rack__label', 'first_slot') |
---|
[11798] | 95 | unique_together = ('rack', 'first_slot') |
---|
[10765] | 96 | |
---|
[12098] | 97 | @property |
---|
| 98 | def address(self): |
---|
[11657] | 99 | return self.rack.address |
---|
[10765] | 100 | |
---|
[12098] | 101 | @property |
---|
| 102 | def room(self): |
---|
[11657] | 103 | return self.rack.room |
---|
[11098] | 104 | |
---|
[12098] | 105 | @property |
---|
| 106 | def roles(self): |
---|
[12008] | 107 | return [str(role.label) for role in self.role.all()] |
---|
[11103] | 108 | |
---|
[12098] | 109 | @property |
---|
| 110 | def in_support(self): |
---|
[11657] | 111 | retval = False |
---|
| 112 | try: |
---|
| 113 | assert bool(self.warranty), 'No warranty contract for %s %s' % \ |
---|
| 114 | (self.__class__.__name__, self.label) |
---|
| 115 | retval = not self.warranty.expired |
---|
| 116 | except: |
---|
| 117 | retval = False |
---|
[12015] | 118 | logger.warning("Hardware with label '%s' hasn't got a warranty \ |
---|
[11657] | 119 | contract" % self.label) |
---|
| 120 | return retval |
---|
[11106] | 121 | |
---|
[11657] | 122 | def __unicode__(self): |
---|
| 123 | try: |
---|
| 124 | assert self.label, "piece of hardware hasn't got a label yet" |
---|
| 125 | return self.label |
---|
| 126 | except AssertionError, e: |
---|
| 127 | return e |
---|
[11103] | 128 | |
---|
[11657] | 129 | def save(self, force_insert=False, force_update=False): |
---|
| 130 | """ |
---|
| 131 | First check if the label has already been filled in. If it's still |
---|
| 132 | empty, then set it to the default basename (based on rack# and |
---|
| 133 | node#). |
---|
| 134 | """ |
---|
| 135 | if not self.label: |
---|
| 136 | self.label = self.default_label() |
---|
[10765] | 137 | |
---|
[11793] | 138 | # Solution for empty unique fields described on: |
---|
| 139 | # http://stackoverflow.com/questions/454436/unique-fields-that-allow-nulls-in-django |
---|
[11850] | 140 | if not self.warranty_tag: |
---|
| 141 | self.warranty_tag = None |
---|
[13004] | 142 | if not self.serial_number: |
---|
| 143 | self.serial_number = None |
---|
[11830] | 144 | if not self.first_slot: |
---|
| 145 | self.first_slot = None |
---|
[11793] | 146 | |
---|
[11657] | 147 | super(HardwareUnit, self).save(force_insert, force_update) |
---|
[10765] | 148 | |
---|
[11657] | 149 | def default_label(self): |
---|
| 150 | # TODO: make dynamic for different types of clusters |
---|
| 151 | try: |
---|
| 152 | assert self.rack.label is not None and self.first_slot is not \ |
---|
| 153 | None, 'not able to generate a label' |
---|
| 154 | return 'r%sn%s' % (self.rack.label, self.first_slot) |
---|
| 155 | except: |
---|
| 156 | pass |
---|
[11103] | 157 | |
---|
| 158 | |
---|
[11811] | 159 | class Interface(ModelExtension): |
---|
[12967] | 160 | """ |
---|
| 161 | An interface of a piece of hardware. |
---|
| 162 | """ |
---|
[12308] | 163 | re_valid_mac = re.compile(r'([A-Fa-f\d]{2}[:-]?){5}[A-Fa-f\d]{2}') |
---|
| 164 | re_mac_octets = re.compile(r'[A-Fa-f\d]{2}') |
---|
| 165 | hwaddress_validator = RegexValidator(re_valid_mac,'Enter a valid hardware address.', 'invalid') |
---|
[11808] | 166 | |
---|
[11657] | 167 | network = models.ForeignKey('Network', related_name='interfaces') |
---|
[11803] | 168 | host = models.ForeignKey('HardwareUnit', related_name='interfaces', |
---|
| 169 | verbose_name='machine') |
---|
[12457] | 170 | iftype = models.ForeignKey('InterfaceType', related_name='interfaces', |
---|
[11657] | 171 | verbose_name='type') |
---|
| 172 | label = models.CharField(max_length=255, help_text='Automagically \ |
---|
| 173 | generated if kept empty') |
---|
[11932] | 174 | aliases = models.CharField(max_length=255, help_text='Cnames comma-seperated', blank=True, null=True) |
---|
| 175 | |
---|
[12999] | 176 | hwaddress = models.CharField(max_length=17, blank=True, null=True, |
---|
[11657] | 177 | verbose_name='hardware address', |
---|
| 178 | help_text="6 Octets, optionally delimited by \ |
---|
| 179 | a space ' ', a hyphen '-', or a colon ':'.", |
---|
[13019] | 180 | validators=[hwaddress_validator]) |
---|
[11799] | 181 | ip = models.IPAddressField(blank=True) |
---|
[10999] | 182 | |
---|
[12100] | 183 | class Meta: |
---|
[13028] | 184 | unique_together = ('network', 'hwaddress') |
---|
[12100] | 185 | ordering = ('host__cluster__name', 'host__rack__label', 'host__first_slot') |
---|
| 186 | |
---|
[12098] | 187 | @property |
---|
| 188 | def fqdn(self): |
---|
[11657] | 189 | return '%s.%s' % (self.label, self.network.domain) |
---|
[10999] | 190 | |
---|
[12098] | 191 | @property |
---|
| 192 | def cnames(self): |
---|
[11964] | 193 | if self.aliases: |
---|
| 194 | return self.aliases.split(',') |
---|
| 195 | |
---|
[11657] | 196 | def __unicode__(self): |
---|
| 197 | #return self.fqdn |
---|
| 198 | return self.label or 'anonymous' |
---|
[11106] | 199 | |
---|
[11808] | 200 | |
---|
[11657] | 201 | def save(self, force_insert=False, force_update=False): |
---|
| 202 | """ |
---|
| 203 | First check for a correct IP address before saving the object. |
---|
| 204 | Pick a new one in the related network when the IP hasn't been set |
---|
| 205 | yet, or when the network has been changed. |
---|
| 206 | """ |
---|
| 207 | try: |
---|
[12999] | 208 | if not self.hwaddress: |
---|
| 209 | self.hwaddress = None |
---|
| 210 | |
---|
[11811] | 211 | if self.hwaddress and len(self.hwaddress) >= 12: |
---|
[12308] | 212 | self.hwaddress = ':'.join(self.re_mac_octets.findall(self.hwaddress.lower())) |
---|
[11657] | 213 | # To be sure that the interface has a valid network |
---|
[11739] | 214 | #assert isinstance(self.network, Network), "network doesn't exist" |
---|
[10999] | 215 | |
---|
[11657] | 216 | try: |
---|
[11739] | 217 | if self.network: |
---|
| 218 | network = IP('%s/%s' % (self.network.netaddress, |
---|
| 219 | self.network.netmask)) |
---|
[11657] | 220 | except ValueError, e: |
---|
| 221 | print ValueError, e |
---|
[11739] | 222 | except Exception, e: |
---|
| 223 | print 'An error occured:', e |
---|
[10999] | 224 | |
---|
[11657] | 225 | # Pick a new IP when it's not defined yet or when the network has |
---|
| 226 | # been changed |
---|
| 227 | ip = IP(self.ip or 0) |
---|
| 228 | if ip not in network: |
---|
| 229 | self.ip = self.network.pick_ip() |
---|
[10999] | 230 | |
---|
[11657] | 231 | self.label = self.label or \ |
---|
[11803] | 232 | self.network.construct_interface_label(self.host) |
---|
[10999] | 233 | |
---|
[11657] | 234 | try: |
---|
| 235 | super(Interface, self).save(force_insert, force_update) |
---|
| 236 | except IntegrityError, e: |
---|
[11939] | 237 | logger.error(e) |
---|
[11811] | 238 | except AssertionError, e: # !!! TODO: exception on other errors !!! |
---|
[11657] | 239 | print AssertionError, e |
---|
[10999] | 240 | |
---|
| 241 | |
---|
| 242 | class Network(ModelExtension): |
---|
[10765] | 243 | """ |
---|
[11657] | 244 | Class with information about a network. Networks are connected with |
---|
| 245 | Interfaces (and HardwareUnits as equipment through Interface). |
---|
[10765] | 246 | """ |
---|
[11657] | 247 | name = models.CharField(max_length=255, help_text='example: \ |
---|
| 248 | infiniband') |
---|
| 249 | netaddress = models.IPAddressField(help_text='example: 192.168.1.0') |
---|
| 250 | netmask = models.IPAddressField(help_text='example: 255.255.255.0') |
---|
[11952] | 251 | gateway = models.IPAddressField(blank=True, help_text='Automagically generated if kept empty') |
---|
[11657] | 252 | domain = models.CharField(max_length=255, help_text='example: \ |
---|
| 253 | irc.sara.nl') |
---|
| 254 | vlan = models.PositiveIntegerField(max_length=3, null=True, |
---|
| 255 | blank=True) |
---|
| 256 | hostnames = models.CharField(max_length=255, help_text='''stringformat \ |
---|
| 257 | of hostnames in the network, example: \ |
---|
| 258 | 'ib-{machine}''') |
---|
[10765] | 259 | |
---|
[11657] | 260 | class Meta: |
---|
[12913] | 261 | ordering = ('name', 'domain') |
---|
[11657] | 262 | verbose_name = 'network' |
---|
| 263 | verbose_name_plural = 'networks' |
---|
[10765] | 264 | |
---|
[11657] | 265 | def __unicode__(self): |
---|
| 266 | return self.name |
---|
[10765] | 267 | |
---|
[12458] | 268 | # |
---|
| 269 | def _rev_name(self): |
---|
| 270 | network = IP("%s/%s" % (self.netaddress, self.netmask)) |
---|
| 271 | reverse_name = network.reverseName() |
---|
| 272 | return reverse_name |
---|
| 273 | |
---|
| 274 | # |
---|
| 275 | def _rev_names(self): |
---|
| 276 | network = IP("%s/%s" % (self.netaddress, self.netmask)) |
---|
| 277 | reverse_names = network.reverseNames() |
---|
| 278 | return reverse_names |
---|
| 279 | |
---|
[11657] | 280 | def _max_hosts(self): |
---|
| 281 | """ |
---|
| 282 | Give the total amount of IP-addresses which could be assigned to hosts in |
---|
| 283 | this network. |
---|
[10765] | 284 | |
---|
[11657] | 285 | Returns an integer. |
---|
| 286 | """ |
---|
| 287 | network = IP("%s/%s" % (self.netaddress, self.netmask)) |
---|
| 288 | return int(network.len()-2) |
---|
[10765] | 289 | |
---|
[11657] | 290 | def _ips_assigned(self): |
---|
| 291 | """ |
---|
| 292 | Make a set with already assigned IP-addresses in the network. |
---|
[10765] | 293 | |
---|
[11657] | 294 | Returns a set. |
---|
| 295 | """ |
---|
| 296 | return set([interface.ip for interface in |
---|
| 297 | Interface.objects.filter(network=self).filter(ip__isnull=False)]) |
---|
[10765] | 298 | |
---|
[11657] | 299 | def count_ips_assigned(self): |
---|
| 300 | """ |
---|
| 301 | Count the amount of assigned IP-addresses in the network. |
---|
[10765] | 302 | |
---|
[11657] | 303 | Returns an integer. |
---|
| 304 | """ |
---|
| 305 | return len(self._ips_assigned()) |
---|
[10765] | 306 | |
---|
[11657] | 307 | def count_ips_free(self): |
---|
| 308 | """ |
---|
| 309 | Calculate the size of the pool of unassigned IP-addresses in the network. |
---|
[10765] | 310 | |
---|
[11657] | 311 | Returns an integer. |
---|
| 312 | """ |
---|
| 313 | return self._max_hosts() - self.count_ips_assigned() |
---|
[10765] | 314 | |
---|
[11657] | 315 | def pick_ip(self): |
---|
| 316 | """ |
---|
| 317 | Pick an IP-address in the network which hasn't been assigned yet. |
---|
[11303] | 318 | |
---|
[11657] | 319 | Returns a string. |
---|
| 320 | """ |
---|
| 321 | assigned = self._ips_assigned() |
---|
| 322 | network = IP("%s/%s" % (self.netaddress, self.netmask)) |
---|
| 323 | netaddress = network.net().ip |
---|
| 324 | broadcast = network.broadcast().ip |
---|
| 325 | poll_ip = netaddress + 1 # netaddress is in use already |
---|
[11303] | 326 | |
---|
[11657] | 327 | found = False |
---|
| 328 | while not found and poll_ip < broadcast: |
---|
[11939] | 329 | poll_ip_str = IP(poll_ip).strNormal() |
---|
| 330 | if poll_ip_str in assigned or poll_ip_str == self.gateway: |
---|
[11657] | 331 | poll_ip += 1 |
---|
| 332 | continue |
---|
| 333 | found = True |
---|
[10765] | 334 | |
---|
[11657] | 335 | if found: |
---|
[11939] | 336 | #ip = IP(poll_ip).strNormal() |
---|
| 337 | ip = poll_ip_str |
---|
[11657] | 338 | else: |
---|
[11939] | 339 | logger.warning("No more IP's available in network '%s'"%self) |
---|
[11657] | 340 | ip = None |
---|
[10765] | 341 | |
---|
[11657] | 342 | return ip |
---|
[11303] | 343 | |
---|
[11939] | 344 | def default_gateway(self): |
---|
| 345 | """ |
---|
| 346 | Return the first available ip address as the default gateway. |
---|
| 347 | """ |
---|
| 348 | network = IP("%s/%s" % (self.netaddress, self.netmask)) |
---|
| 349 | return IP(network.ip+1).strNormal() |
---|
| 350 | |
---|
[11657] | 351 | def construct_interface_label(self, machine): |
---|
| 352 | """ |
---|
| 353 | Construct a label for an interface that's asking for it. The default |
---|
| 354 | label of an interface is based on info of its machine and network. |
---|
| 355 | """ |
---|
| 356 | interface_label = self.hostnames.format(machine=machine) |
---|
| 357 | return interface_label |
---|
[10899] | 358 | |
---|
[12100] | 359 | @property |
---|
[11657] | 360 | def cidr(self): |
---|
| 361 | network = IP("%s/%s" % (self.netaddress, self.netmask)) |
---|
| 362 | return network.strNormal() |
---|
[10857] | 363 | |
---|
[11939] | 364 | def save(self, force_insert=False, force_update=False): |
---|
| 365 | if not self.gateway: |
---|
[12016] | 366 | self.gateway = self.default_gateway() |
---|
[11939] | 367 | try: |
---|
| 368 | super(Network, self).save(force_insert, force_update) |
---|
| 369 | except IntegrityError, e: |
---|
| 370 | logger.error(e) |
---|
[10765] | 371 | |
---|
[11939] | 372 | |
---|
| 373 | |
---|
[11657] | 374 | class Rack(ModelExtension): |
---|
| 375 | """ |
---|
| 376 | A Rack is a standardized system for mounting various HardwareUnits in a |
---|
[12967] | 377 | stack of slots. |
---|
[11657] | 378 | """ |
---|
[10765] | 379 | |
---|
[11657] | 380 | room = models.ForeignKey('Room', related_name='racks') |
---|
[10765] | 381 | |
---|
[11657] | 382 | label = models.SlugField(max_length=255) |
---|
| 383 | capacity = models.PositiveIntegerField(verbose_name='number of slots') |
---|
[10765] | 384 | |
---|
[11657] | 385 | class Meta: |
---|
[12903] | 386 | unique_together = ('room', 'label') |
---|
[13068] | 387 | ordering = ('label',) |
---|
[11657] | 388 | verbose_name = 'rack' |
---|
| 389 | verbose_name_plural = 'racks' |
---|
[11098] | 390 | |
---|
[12098] | 391 | @property |
---|
| 392 | def address(self): |
---|
[11657] | 393 | return self.room.address |
---|
[11098] | 394 | |
---|
[11657] | 395 | def __unicode__(self): |
---|
| 396 | return 'rack %s' % (self.label) |
---|
[10765] | 397 | |
---|
[10999] | 398 | # |
---|
| 399 | # |
---|
| 400 | # |
---|
| 401 | ###### |
---|
[10765] | 402 | |
---|
[10999] | 403 | |
---|
| 404 | |
---|
| 405 | ###### |
---|
| 406 | # |
---|
| 407 | # Classes for sara_cmt.locations |
---|
| 408 | # |
---|
| 409 | |
---|
[11657] | 410 | |
---|
[11041] | 411 | class Country(ModelExtension): |
---|
[11657] | 412 | """ |
---|
| 413 | Model for country - country-code pairs. Country-codes can be found on: |
---|
| 414 | http://www.itu.int/dms_pub/itu-t/opb/sp/T-SP-E.164D-2009-PDF-E.pdf |
---|
| 415 | """ |
---|
| 416 | name = models.CharField(max_length=255, unique=True) |
---|
| 417 | country_code = models.PositiveIntegerField(unique=True, help_text='''Example: In case of The Netherlands it's 31''') |
---|
[11041] | 418 | |
---|
[11657] | 419 | class Meta: |
---|
| 420 | verbose_name_plural = 'countries' |
---|
[12100] | 421 | ordering = ('name',) |
---|
[11041] | 422 | |
---|
[11657] | 423 | def __unicode__(self): |
---|
| 424 | return self.name |
---|
[11041] | 425 | |
---|
| 426 | |
---|
[10999] | 427 | class Address(ModelExtension): |
---|
[11657] | 428 | """ |
---|
[11800] | 429 | A class to hold information about the physical location of a model. |
---|
[11657] | 430 | """ |
---|
| 431 | country = models.ForeignKey(Country, null=True, blank=True, related_name='addresses') |
---|
| 432 | address = models.CharField(max_length=255) |
---|
| 433 | postalcode = models.CharField(max_length=9, blank=True) |
---|
| 434 | city = models.CharField(max_length=255) |
---|
[10999] | 435 | |
---|
[12098] | 436 | @property |
---|
| 437 | def companies(self): |
---|
[11657] | 438 | return ' | '.join([comp.name for comp in self._companies.all()]) or '-' |
---|
[11098] | 439 | |
---|
[11657] | 440 | class Meta: |
---|
| 441 | unique_together = ('address', 'city') |
---|
| 442 | verbose_name_plural = 'addresses' |
---|
[12100] | 443 | ordering = ('postalcode',) |
---|
[10999] | 444 | |
---|
[11657] | 445 | def __unicode__(self): |
---|
| 446 | return '%s - %s' % (self.city, self.address) |
---|
[10999] | 447 | |
---|
| 448 | |
---|
| 449 | class Room(ModelExtension): |
---|
[12967] | 450 | """ |
---|
| 451 | A room is located at an address. This is where racks of hardware can be |
---|
| 452 | found. |
---|
| 453 | """ |
---|
[11657] | 454 | address = models.ForeignKey(Address, related_name='rooms') |
---|
[10999] | 455 | |
---|
[11657] | 456 | floor = models.IntegerField(max_length=2) |
---|
| 457 | label = models.CharField(max_length=255, blank=False) |
---|
[10999] | 458 | |
---|
[11657] | 459 | class Meta: |
---|
| 460 | unique_together = ('address', 'floor', 'label') |
---|
[12100] | 461 | ordering = ('address__postalcode', 'floor') |
---|
[10999] | 462 | |
---|
[11657] | 463 | def __unicode__(self): |
---|
| 464 | #return unicode('%s - %s'%(self.address,self.label)) |
---|
[12912] | 465 | return '%s (%s, %s)' % (self.label, self.address.address, self.address.city) |
---|
[10999] | 466 | |
---|
| 467 | # |
---|
| 468 | # |
---|
| 469 | # |
---|
| 470 | ###### |
---|
| 471 | |
---|
| 472 | |
---|
| 473 | ###### |
---|
| 474 | # |
---|
| 475 | # Classes for sara_cmt.contacts |
---|
| 476 | # |
---|
| 477 | |
---|
[11657] | 478 | |
---|
[10999] | 479 | class Company(ModelExtension): |
---|
[11657] | 480 | """ |
---|
| 481 | The Company-model can be linked to hardware. This way you are able to define |
---|
| 482 | contactpersons for a specific piece of hardware. |
---|
| 483 | """ |
---|
[11053] | 484 | |
---|
[11657] | 485 | addresses = models.ManyToManyField(Address, related_name='_companies') |
---|
[10999] | 486 | |
---|
[12967] | 487 | #type = models.ChoiceField() # !!! TODO: add choices like vendor / support / partner / customer / etc... !!! |
---|
[11657] | 488 | name = models.CharField(max_length=255) |
---|
| 489 | website = models.URLField() |
---|
[11098] | 490 | |
---|
[11657] | 491 | def get_addresses(self): |
---|
| 492 | return ' | '.join([address.address for address in self.addresses.all()]) or '-' |
---|
[10999] | 493 | |
---|
[11657] | 494 | def __unicode__(self): |
---|
| 495 | return self.name |
---|
[10999] | 496 | |
---|
[11657] | 497 | class Meta: |
---|
| 498 | verbose_name_plural = 'companies' |
---|
[12100] | 499 | ordering = ('name',) |
---|
[10999] | 500 | |
---|
[11657] | 501 | |
---|
[10999] | 502 | class Connection(ModelExtension): |
---|
[11657] | 503 | """ |
---|
| 504 | Contacts can be linked to different sites, hardware, or its vendors. |
---|
| 505 | This makes it possible to lookup contactpersons in case of problems on a |
---|
| 506 | site or with specific hardware. |
---|
| 507 | """ |
---|
[12015] | 508 | address = models.ForeignKey(Address, blank=True, null=True, related_name='connections') |
---|
[11657] | 509 | company = models.ForeignKey(Company, related_name='companies') |
---|
[10999] | 510 | |
---|
[11657] | 511 | active = models.BooleanField(editable=True, default=True) |
---|
| 512 | name = models.CharField(verbose_name='full name', max_length=255) |
---|
| 513 | email = models.EmailField(blank=True, null=True) |
---|
[10999] | 514 | |
---|
[11657] | 515 | def __unicode__(self): |
---|
| 516 | return self.name |
---|
[10999] | 517 | |
---|
[11657] | 518 | def _address(self): |
---|
| 519 | return address.address |
---|
[10999] | 520 | |
---|
[11657] | 521 | class Meta: |
---|
| 522 | verbose_name = 'contact' |
---|
| 523 | unique_together = ('company', 'name') |
---|
[12100] | 524 | ordering = ('company', 'address') |
---|
[10999] | 525 | |
---|
| 526 | |
---|
| 527 | class Telephonenumber(ModelExtension): |
---|
[12967] | 528 | """ |
---|
| 529 | Telephonenumber to link to a contact. Split in country-, area- and |
---|
| 530 | subscriber-part for easy filtering. |
---|
| 531 | """ |
---|
[11657] | 532 | NUMBER_CHOICES = ( |
---|
| 533 | ('T', 'Telephone'), |
---|
| 534 | ('C', 'Cellphone'), |
---|
| 535 | ('F', 'Fax')) |
---|
| 536 | country = models.ForeignKey(Country, related_name='telephone_numbers') |
---|
| 537 | connection = models.ForeignKey(Connection, blank=False, null=False, related_name='telephone_numbers') |
---|
| 538 | areacode = models.CharField(max_length=4) # because it can start with a zero |
---|
| 539 | subscriber_number = models.IntegerField(verbose_name='number', max_length=15) |
---|
[12997] | 540 | number_type = models.CharField(max_length=1, choices=NUMBER_CHOICES) |
---|
[10999] | 541 | |
---|
[11657] | 542 | # !!! TODO: link to company / contact / etc... !!! |
---|
[10999] | 543 | |
---|
[11657] | 544 | def __unicode__(self): |
---|
| 545 | return '+%i(%s)%s-%i' % (self.country.country_code, self.areacode[:1], self.areacode[1:], self.subscriber_number) |
---|
[10999] | 546 | |
---|
[11657] | 547 | class Meta: |
---|
| 548 | ordering = ('connection',) |
---|
[10999] | 549 | |
---|
[11058] | 550 | |
---|
[10999] | 551 | # |
---|
| 552 | # |
---|
| 553 | # |
---|
| 554 | ##### |
---|
| 555 | |
---|
| 556 | |
---|
| 557 | ###### |
---|
| 558 | # |
---|
| 559 | # Classes for sara_cmt.specifications |
---|
| 560 | # |
---|
| 561 | |
---|
| 562 | |
---|
[11053] | 563 | class HardwareModel(ModelExtension): |
---|
[11657] | 564 | """ |
---|
[12967] | 565 | This model is being used to specify some extra information about a |
---|
[11657] | 566 | specific type (model) of hardware. |
---|
| 567 | """ |
---|
| 568 | vendor = models.ForeignKey(Company, related_name='model specifications') |
---|
[10999] | 569 | |
---|
[11657] | 570 | name = models.CharField(max_length=255, unique=True) |
---|
[13004] | 571 | vendorcode = models.CharField(max_length=255, blank=True, null=True, unique=True, help_text='example: CISCO7606-S') |
---|
[11657] | 572 | rackspace = models.PositiveIntegerField(help_text='size in U for example') |
---|
| 573 | expansions = models.PositiveIntegerField(default=0, help_text='number of expansion slots') |
---|
[10999] | 574 | |
---|
[11657] | 575 | class Meta: |
---|
| 576 | verbose_name = 'model' |
---|
| 577 | ordering = ('vendor', 'name') |
---|
[10999] | 578 | |
---|
[11657] | 579 | def __unicode__(self): |
---|
| 580 | return '%s (%s)' % (self.name, self.vendor) |
---|
[10999] | 581 | |
---|
| 582 | |
---|
| 583 | class Role(ModelExtension): |
---|
[11657] | 584 | """ |
---|
| 585 | This describes a possible role of a HardwareUnit in the cluster. A piece of |
---|
| 586 | hardware can have a role like 'switch', 'compute node', 'patchpanel', 'pdu', |
---|
| 587 | 'admin node', 'login node', etc... |
---|
| 588 | Those roles can be used for all kinds of rules on HardwareUnits which exist |
---|
| 589 | in the cluster. |
---|
| 590 | """ |
---|
| 591 | label = models.CharField(max_length=255, unique=True) |
---|
[10999] | 592 | |
---|
[11657] | 593 | class Meta: |
---|
[12100] | 594 | ordering = ('label',) |
---|
| 595 | verbose_name = 'role' |
---|
[11657] | 596 | verbose_name_plural = 'roles' |
---|
[10999] | 597 | |
---|
[11657] | 598 | def __unicode__(self): |
---|
[12008] | 599 | return str(self.label) |
---|
[10999] | 600 | |
---|
| 601 | |
---|
| 602 | class InterfaceType(ModelExtension): |
---|
[12967] | 603 | """ |
---|
| 604 | Contains information about different types of interfaces. |
---|
| 605 | """ |
---|
[11657] | 606 | vendor = models.ForeignKey('Company', null=True, blank=True, related_name='interfaces') |
---|
[10765] | 607 | |
---|
[12967] | 608 | label = models.CharField(max_length=255, help_text="'DRAC 4' for example") |
---|
| 609 | |
---|
[11657] | 610 | class Meta: |
---|
[12102] | 611 | # Note in docs of Model Meta options, |
---|
| 612 | # see http://docs.djangoproject.com/en/dev/ref/models/options/#ordering |
---|
| 613 | # "Regardless of how many fields are in ordering, the admin site uses |
---|
| 614 | # only the first field." |
---|
| 615 | #ordering = ('vendor', 'label') |
---|
[12103] | 616 | ordering = ('label',) |
---|
[11657] | 617 | verbose_name = 'type of interface' |
---|
| 618 | verbose_name_plural = 'types of interfaces' |
---|
[10899] | 619 | |
---|
[11657] | 620 | def __unicode__(self): |
---|
| 621 | return self.label |
---|
[10857] | 622 | |
---|
[10999] | 623 | # |
---|
| 624 | # |
---|
| 625 | # |
---|
| 626 | ###### |
---|
| 627 | |
---|
| 628 | |
---|
| 629 | ###### |
---|
| 630 | # |
---|
| 631 | # Classes for sara_cmt.support |
---|
| 632 | # |
---|
| 633 | |
---|
[11657] | 634 | |
---|
[11053] | 635 | class WarrantyType(ModelExtension): |
---|
[11657] | 636 | """ |
---|
[12967] | 637 | A type of warranty offered by a company. |
---|
[11657] | 638 | """ |
---|
| 639 | contact = models.ForeignKey(Connection, related_name='warranty types') |
---|
[10999] | 640 | |
---|
[11657] | 641 | label = models.CharField(max_length=255, unique=True) |
---|
[10999] | 642 | |
---|
[12100] | 643 | class Meta: |
---|
| 644 | ordering = ('contact__company__name', 'label') |
---|
[11657] | 645 | def __unicode__(self): |
---|
| 646 | return self.label |
---|
[10999] | 647 | |
---|
[11053] | 648 | |
---|
[10999] | 649 | class WarrantyContract(ModelExtension): |
---|
[11657] | 650 | """ |
---|
[12997] | 651 | A class which contains warranty information of (a collection of) hardware. (SLA) |
---|
[11657] | 652 | """ |
---|
[12997] | 653 | warranty_type = models.ForeignKey(WarrantyType, blank=True, null=True, related_name='contracts') |
---|
[10999] | 654 | |
---|
[13004] | 655 | contract_number = models.CharField(max_length=255, blank=True, null=True, unique=True, help_text='NSEN420201') |
---|
| 656 | annual_cost = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True, help_text='433.61') |
---|
[11657] | 657 | label = models.CharField(max_length=255, unique=True) |
---|
| 658 | date_from = models.DateField(verbose_name='valid from') |
---|
| 659 | date_to = models.DateField(verbose_name='expires at') |
---|
[12336] | 660 | date_to.in_support_filter = True |
---|
[10999] | 661 | |
---|
[12100] | 662 | class Meta: |
---|
[12101] | 663 | ordering = ('label',) |
---|
[12100] | 664 | |
---|
[12098] | 665 | @property |
---|
| 666 | def expired(self): |
---|
[12336] | 667 | return self.date_to < date.today() |
---|
[10999] | 668 | |
---|
[11657] | 669 | def __unicode__(self): |
---|
| 670 | return self.label |
---|
[13300] | 671 | |
---|
| 672 | def save(self, force_insert=False, force_update=False): |
---|
| 673 | """ |
---|
| 674 | The contract number is an optional field, but when filled in it |
---|
| 675 | should have a unique value. When kept blank, it should be stored as |
---|
| 676 | None. |
---|
| 677 | """ |
---|
| 678 | if not self.contract_number: |
---|
[13698] | 679 | self.contract_number = None |
---|
[13303] | 680 | |
---|
| 681 | super(WarrantyContract, self).save(force_insert, force_update) |
---|