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 | |
---|
18 | from django.db import models |
---|
19 | from django.core.validators import RegexValidator |
---|
20 | |
---|
21 | import re |
---|
22 | from datetime import date |
---|
23 | |
---|
24 | from psycopg2 import IntegrityError |
---|
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 | |
---|
33 | from tagging.fields import TagField |
---|
34 | from django_extensions.db.fields import CreationDateTimeField, \ |
---|
35 | ModificationDateTimeField |
---|
36 | |
---|
37 | |
---|
38 | |
---|
39 | ###### |
---|
40 | # |
---|
41 | # Classes of sara_cmt.core |
---|
42 | # |
---|
43 | |
---|
44 | |
---|
45 | class 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 | |
---|
58 | class 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 | |
---|
159 | class 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 | |
---|
242 | class 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 | |
---|
374 | class 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 | |
---|
411 | class 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 | |
---|
427 | class 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 | |
---|
449 | class 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 | |
---|
479 | class 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 | |
---|
502 | class 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 | |
---|
527 | class 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 | |
---|
563 | class 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 | |
---|
583 | class 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 | |
---|
602 | class 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 | |
---|
635 | class 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 | |
---|
649 | class 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) |
---|