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

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

Merged branch 1.0 (until tag 1.0.0) back to trunk

File size: 21.9 KB
RevLine 
[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]18from django.db import models
[11811]19from django.core.validators import RegexValidator
20
21import re
[12336]22from datetime import date
[11811]23
[11510]24from psycopg2 import IntegrityError
[10765]25
26from IPy import IP
27
28from sara_cmt.logger import Logger
29logger = Logger().getLogger()
30
31from sara_cmt.django_cli import ModelExtension
32
[10899]33from tagging.fields import TagField
[11657]34from 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]45class 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]58class 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]159class 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
242class 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]374class 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]411class 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]427class 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
449class 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]479class 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]502class 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
527class 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]563class 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
583class 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
602class 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]635class 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]649class 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)
Note: See TracBrowser for help on using the repository browser.