source: trunk/sara_cmt/sara_cmt/cluster/models.py

Last change on this file was 14194, checked in by sil, 8 years ago

Merged branch 1.0 (until tag 1.0.0) back to trunk

File size: 21.9 KB
Line 
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
18from django.db import models
19from django.core.validators import RegexValidator
20
21import re
22from datetime import date
23
24from psycopg2 import IntegrityError
25
26from IPy import IP
27
28from sara_cmt.logger import Logger
29logger = Logger().getLogger()
30
31from sara_cmt.django_cli import ModelExtension
32
33from tagging.fields import TagField
34from django_extensions.db.fields import CreationDateTimeField, \
35                                        ModificationDateTimeField
36
37
38
39######
40#
41# Classes of sara_cmt.core
42#
43
44
45class Cluster(ModelExtension):
46    """
47        A labeled group of hardware pieces.
48    """
49    name = models.CharField(max_length=255, unique=True)
50
51    class Meta:
52        ordering = ('name',)
53
54    def __unicode__(self):
55        return self.name or None
56
57
58class HardwareUnit(ModelExtension):
59    """
60        A specific piece of hardware.
61    """
62    STATE_CHOICES = (
63        ('new', 'new'),
64        ('clean', 'clean'),
65        ('configured', 'configured'),
66        ('unknown', 'unknown'),
67        ('off', 'off'))
68       
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')
80    seller = models.ForeignKey('Connection', related_name='sold', null=True, blank=True)
81    owner = models.ForeignKey('Connection', related_name='owns', null=True, blank=True)
82    state = models.CharField(max_length=10, null=True, blank=True, choices=STATE_CHOICES, default='unknown')
83    warranty_tag = models.CharField(max_length=255, blank=True, null=True,
84                                    help_text='Service tag',
85                                    unique=True)
86    serial_number = models.CharField(max_length=255, blank=True, null=True, unique=True)
87    first_slot   = models.PositiveIntegerField(blank=True, null=True)
88    label        = models.CharField(max_length=255)
89
90    class Meta:
91        #verbose_name = "piece of hardware"
92        verbose_name_plural = "hardware"
93        #ordering = ('cluster__name', 'rack__label', 'first_slot')
94        ordering = ('rack__label', 'first_slot')
95        unique_together = ('rack', 'first_slot')
96
97    @property
98    def address(self):
99        return self.rack.address
100
101    @property
102    def room(self):
103        return self.rack.room
104
105    @property
106    def roles(self):
107        return [str(role.label) for role in self.role.all()]
108
109    @property
110    def in_support(self):
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
118            logger.warning("Hardware with label '%s' hasn't got a warranty \
119                contract" % self.label)
120        return retval
121
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
128
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()
137
138        # Solution for empty unique fields described on:
139        # http://stackoverflow.com/questions/454436/unique-fields-that-allow-nulls-in-django
140        if not self.warranty_tag:
141            self.warranty_tag = None
142        if not self.serial_number:
143            self.serial_number = None
144        if not self.first_slot:
145            self.first_slot = None
146
147        super(HardwareUnit, self).save(force_insert, force_update)
148
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
157
158
159class Interface(ModelExtension):
160    """
161        An interface of a piece of hardware.
162    """
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')
166
167    network   = models.ForeignKey('Network', related_name='interfaces')
168    host      = models.ForeignKey('HardwareUnit', related_name='interfaces',
169                                  verbose_name='machine')
170    iftype    = models.ForeignKey('InterfaceType', related_name='interfaces',
171                                  verbose_name='type')
172    label     = models.CharField(max_length=255, help_text='Automagically \
173                                 generated if kept empty')
174    aliases   = models.CharField(max_length=255, help_text='Cnames comma-seperated', blank=True, null=True)
175
176    hwaddress = models.CharField(max_length=17, blank=True, null=True,
177                                 verbose_name='hardware address',
178                                 help_text="6 Octets, optionally delimited by \
179                                 a space ' ', a hyphen '-', or a colon ':'.",
180                                 validators=[hwaddress_validator])
181    ip        = models.IPAddressField(blank=True)
182
183    class Meta:
184        unique_together = ('network', 'hwaddress')
185        ordering = ('host__cluster__name', 'host__rack__label', 'host__first_slot')
186
187    @property
188    def fqdn(self):
189        return '%s.%s' % (self.label, self.network.domain)
190
191    @property
192    def cnames(self):
193        if self.aliases:
194            return self.aliases.split(',')
195
196    def __unicode__(self):
197        #return self.fqdn
198        return self.label or 'anonymous'
199
200
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:
208            if not self.hwaddress:
209                self.hwaddress = None
210
211            if self.hwaddress and len(self.hwaddress) >= 12:
212                self.hwaddress = ':'.join(self.re_mac_octets.findall(self.hwaddress.lower()))
213            # To be sure that the interface has a valid network
214            #assert isinstance(self.network, Network), "network doesn't exist"
215
216            try:
217                if self.network:
218                    network = IP('%s/%s' % (self.network.netaddress,
219                                            self.network.netmask))
220            except ValueError, e:
221                print ValueError, e
222            except Exception, e:
223                print 'An error occured:', e
224
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()
230
231            self.label = self.label or \
232                         self.network.construct_interface_label(self.host)
233
234            try:
235                super(Interface, self).save(force_insert, force_update)
236            except IntegrityError, e:
237                logger.error(e)
238        except AssertionError, e: # !!! TODO: exception on other errors !!!
239            print AssertionError, e
240
241
242class Network(ModelExtension):
243    """
244        Class with information about a network. Networks are connected with
245        Interfaces (and HardwareUnits as equipment through Interface).
246    """
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')
251    gateway    = models.IPAddressField(blank=True, help_text='Automagically generated if kept empty')
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}''')
259
260    class Meta:
261        ordering = ('name', 'domain')
262        verbose_name = 'network'
263        verbose_name_plural = 'networks'
264
265    def __unicode__(self):
266        return self.name
267
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
280    def _max_hosts(self):
281        """
282            Give the total amount of IP-addresses which could be assigned to hosts in
283            this network.
284
285            Returns an integer.
286        """
287        network = IP("%s/%s" % (self.netaddress, self.netmask))
288        return int(network.len()-2)
289
290    def _ips_assigned(self):
291        """
292            Make a set with already assigned IP-addresses in the network.
293
294            Returns a set.
295        """
296        return set([interface.ip for interface in
297            Interface.objects.filter(network=self).filter(ip__isnull=False)])
298
299    def count_ips_assigned(self):
300        """
301            Count the amount of assigned IP-addresses in the network.
302
303            Returns an integer.
304        """
305        return len(self._ips_assigned())
306
307    def count_ips_free(self):
308        """
309            Calculate the size of the pool of unassigned IP-addresses in the network.
310
311            Returns an integer.
312        """
313        return self._max_hosts() - self.count_ips_assigned()
314
315    def pick_ip(self):
316        """
317            Pick an IP-address in the network which hasn't been assigned yet.
318
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
326
327        found = False
328        while not found and poll_ip < broadcast:
329            poll_ip_str = IP(poll_ip).strNormal()
330            if poll_ip_str in assigned or poll_ip_str == self.gateway:
331                poll_ip += 1
332                continue
333            found = True
334
335        if found:
336            #ip = IP(poll_ip).strNormal()
337            ip = poll_ip_str
338        else:
339            logger.warning("No more IP's available in network '%s'"%self)
340            ip = None
341
342        return ip
343
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
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
358
359    @property
360    def cidr(self):
361        network = IP("%s/%s" % (self.netaddress, self.netmask))
362        return network.strNormal()
363
364    def save(self, force_insert=False, force_update=False):
365        if not self.gateway:
366            self.gateway = self.default_gateway() 
367        try:
368            super(Network, self).save(force_insert, force_update)
369        except IntegrityError, e:
370            logger.error(e)
371
372
373
374class Rack(ModelExtension):
375    """
376        A Rack is a standardized system for mounting various HardwareUnits in a
377        stack of slots.
378    """
379
380    room = models.ForeignKey('Room', related_name='racks')
381
382    label    = models.SlugField(max_length=255)
383    capacity = models.PositiveIntegerField(verbose_name='number of slots')
384
385    class Meta:
386        unique_together = ('room', 'label')
387        ordering = ('label',)
388        verbose_name = 'rack'
389        verbose_name_plural = 'racks'
390
391    @property
392    def address(self):
393        return self.room.address
394
395    def __unicode__(self):
396        return 'rack %s' % (self.label)
397
398#
399#
400#
401######
402
403
404
405######
406#
407# Classes for sara_cmt.locations
408#
409
410
411class Country(ModelExtension):
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''')
418
419    class Meta:
420        verbose_name_plural = 'countries'
421        ordering = ('name',)
422
423    def __unicode__(self):
424        return self.name
425
426
427class Address(ModelExtension):
428    """
429        A class to hold information about the physical location of a model.
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)
435
436    @property
437    def companies(self):
438        return ' | '.join([comp.name for comp in self._companies.all()]) or '-'
439
440    class Meta:
441        unique_together = ('address', 'city')
442        verbose_name_plural = 'addresses'
443        ordering = ('postalcode',)
444
445    def __unicode__(self):
446        return '%s - %s' % (self.city, self.address)
447
448
449class Room(ModelExtension):
450    """
451        A room is located at an address. This is where racks of hardware can be
452        found.
453    """
454    address = models.ForeignKey(Address, related_name='rooms')
455
456    floor   = models.IntegerField(max_length=2)
457    label   = models.CharField(max_length=255, blank=False)
458
459    class Meta:
460        unique_together = ('address', 'floor', 'label')
461        ordering = ('address__postalcode', 'floor')
462
463    def __unicode__(self):
464        #return unicode('%s - %s'%(self.address,self.label))
465        return '%s (%s, %s)' % (self.label, self.address.address, self.address.city)
466
467#
468#
469#
470######
471
472
473######
474#
475# Classes for sara_cmt.contacts
476#
477
478
479class Company(ModelExtension):
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    """
484
485    addresses = models.ManyToManyField(Address, related_name='_companies')
486
487    #type    = models.ChoiceField() # !!! TODO: add choices like vendor / support / partner / customer / etc... !!!
488    name    = models.CharField(max_length=255)
489    website = models.URLField()
490
491    def get_addresses(self):
492        return ' | '.join([address.address for address in self.addresses.all()]) or '-'
493
494    def __unicode__(self):
495        return self.name
496
497    class Meta:
498        verbose_name_plural = 'companies'
499        ordering = ('name',) 
500
501
502class Connection(ModelExtension):
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    """
508    address = models.ForeignKey(Address, blank=True, null=True, related_name='connections')
509    company = models.ForeignKey(Company, related_name='companies')
510
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)
514
515    def __unicode__(self):
516        return self.name
517
518    def _address(self):
519        return address.address
520
521    class Meta:
522        verbose_name = 'contact'
523        unique_together = ('company', 'name')
524        ordering = ('company', 'address')
525
526
527class Telephonenumber(ModelExtension):
528    """
529        Telephonenumber to link to a contact. Split in country-, area- and
530        subscriber-part for easy filtering.
531    """
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)
540    number_type = models.CharField(max_length=1, choices=NUMBER_CHOICES)
541
542    # !!! TODO: link to company / contact / etc... !!!
543
544    def __unicode__(self):
545        return '+%i(%s)%s-%i' % (self.country.country_code, self.areacode[:1], self.areacode[1:], self.subscriber_number)
546
547    class Meta:
548        ordering = ('connection',)
549
550
551#
552#
553#
554#####
555
556
557######
558#
559# Classes for sara_cmt.specifications
560#
561
562
563class HardwareModel(ModelExtension):
564    """
565        This model is being used to specify some extra information about a
566        specific type (model) of hardware.
567    """
568    vendor = models.ForeignKey(Company, related_name='model specifications')
569
570    name       = models.CharField(max_length=255, unique=True)
571    vendorcode = models.CharField(max_length=255, blank=True, null=True, unique=True, help_text='example: CISCO7606-S')
572    rackspace  = models.PositiveIntegerField(help_text='size in U for example')
573    expansions = models.PositiveIntegerField(default=0, help_text='number of expansion slots')
574
575    class Meta:
576        verbose_name = 'model'
577        ordering = ('vendor', 'name')
578
579    def __unicode__(self):
580        return '%s (%s)' % (self.name, self.vendor)
581
582
583class Role(ModelExtension):
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)
592
593    class Meta:
594        ordering = ('label',)
595        verbose_name = 'role'
596        verbose_name_plural = 'roles'
597
598    def __unicode__(self):
599        return str(self.label)
600
601
602class InterfaceType(ModelExtension):
603    """
604        Contains information about different types of interfaces.
605    """
606    vendor = models.ForeignKey('Company', null=True, blank=True, related_name='interfaces')
607
608    label = models.CharField(max_length=255, help_text="'DRAC 4' for example")
609
610    class Meta:
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')
616        ordering = ('label',)
617        verbose_name = 'type of interface'
618        verbose_name_plural = 'types of interfaces'
619
620    def __unicode__(self):
621        return self.label
622
623#
624#
625#
626######
627
628
629######
630#
631# Classes for sara_cmt.support
632#
633
634
635class WarrantyType(ModelExtension):
636    """
637        A type of warranty offered by a company.
638    """
639    contact = models.ForeignKey(Connection, related_name='warranty types')
640
641    label = models.CharField(max_length=255, unique=True)
642
643    class Meta:
644        ordering = ('contact__company__name', 'label')
645    def __unicode__(self):
646        return self.label
647
648
649class WarrantyContract(ModelExtension):
650    """
651        A class which contains warranty information of (a collection of) hardware. (SLA)
652    """
653    warranty_type = models.ForeignKey(WarrantyType, blank=True, null=True, related_name='contracts')
654
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')
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')
660    date_to.in_support_filter = True
661
662    class Meta:
663        ordering = ('label',)
664
665    @property
666    def expired(self):
667        return self.date_to < date.today()
668
669    def __unicode__(self):
670        return self.label
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:
679            self.contract_number = None
680
681        super(WarrantyContract, self).save(force_insert, force_update)
Note: See TracBrowser for help on using the repository browser.