| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238 |
- from collections import OrderedDict
- from itertools import count, groupby
- import svgwrite
- import yaml
- from django.conf import settings
- from django.contrib.auth.models import User
- from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
- from django.contrib.contenttypes.models import ContentType
- from django.contrib.postgres.fields import ArrayField, JSONField
- from django.core.exceptions import ObjectDoesNotExist, ValidationError
- from django.core.validators import MaxValueValidator, MinValueValidator
- from django.db import models
- from django.db.models import Count, F, ProtectedError, Sum
- from django.urls import reverse
- from django.utils.http import urlencode
- from mptt.models import MPTTModel, TreeForeignKey
- from taggit.managers import TaggableManager
- from timezone_field import TimeZoneField
- from dcim.choices import *
- from dcim.constants import *
- from dcim.fields import ASNField
- from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
- from utilities.fields import ColorField, NaturalOrderingField
- from utilities.models import ChangeLoggedModel
- from utilities.utils import foreground_color, to_meters
- from .device_component_templates import (
- ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
- PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
- )
- from .device_components import (
- CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet,
- PowerPort, RearPort,
- )
- __all__ = (
- 'Cable',
- 'CableTermination',
- 'ConsolePort',
- 'ConsolePortTemplate',
- 'ConsoleServerPort',
- 'ConsoleServerPortTemplate',
- 'Device',
- 'DeviceBay',
- 'DeviceBayTemplate',
- 'DeviceRole',
- 'DeviceType',
- 'FrontPort',
- 'FrontPortTemplate',
- 'Interface',
- 'InterfaceTemplate',
- 'InventoryItem',
- 'Manufacturer',
- 'Platform',
- 'PowerFeed',
- 'PowerOutlet',
- 'PowerOutletTemplate',
- 'PowerPanel',
- 'PowerPort',
- 'PowerPortTemplate',
- 'Rack',
- 'RackGroup',
- 'RackReservation',
- 'RackRole',
- 'RearPort',
- 'RearPortTemplate',
- 'Region',
- 'Site',
- 'VirtualChassis',
- )
- #
- # Regions
- #
- class Region(MPTTModel, ChangeLoggedModel):
- """
- Sites can be grouped within geographic Regions.
- """
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='children',
- blank=True,
- null=True,
- db_index=True
- )
- name = models.CharField(
- max_length=50,
- unique=True
- )
- slug = models.SlugField(
- unique=True
- )
- csv_headers = ['name', 'slug', 'parent']
- class MPTTMeta:
- order_insertion_by = ['name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
- def to_csv(self):
- return (
- self.name,
- self.slug,
- self.parent.name if self.parent else None,
- )
- def get_site_count(self):
- return Site.objects.filter(
- Q(region=self) |
- Q(region__in=self.get_descendants())
- ).count()
- #
- # Sites
- #
- class Site(ChangeLoggedModel, CustomFieldModel):
- """
- A Site represents a geographic location within a network; typically a building or campus. The optional facility
- field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
- """
- name = models.CharField(
- max_length=50,
- unique=True
- )
- _name = NaturalOrderingField(
- target_field='name',
- max_length=100,
- blank=True
- )
- slug = models.SlugField(
- unique=True
- )
- status = models.CharField(
- max_length=50,
- choices=SiteStatusChoices,
- default=SiteStatusChoices.STATUS_ACTIVE
- )
- region = models.ForeignKey(
- to='dcim.Region',
- on_delete=models.SET_NULL,
- related_name='sites',
- blank=True,
- null=True
- )
- tenant = models.ForeignKey(
- to='tenancy.Tenant',
- on_delete=models.PROTECT,
- related_name='sites',
- blank=True,
- null=True
- )
- facility = models.CharField(
- max_length=50,
- blank=True
- )
- asn = ASNField(
- blank=True,
- null=True,
- verbose_name='ASN'
- )
- time_zone = TimeZoneField(
- blank=True
- )
- description = models.CharField(
- max_length=100,
- blank=True
- )
- physical_address = models.CharField(
- max_length=200,
- blank=True
- )
- shipping_address = models.CharField(
- max_length=200,
- blank=True
- )
- latitude = models.DecimalField(
- max_digits=8,
- decimal_places=6,
- blank=True,
- null=True
- )
- longitude = models.DecimalField(
- max_digits=9,
- decimal_places=6,
- blank=True,
- null=True
- )
- contact_name = models.CharField(
- max_length=50,
- blank=True
- )
- contact_phone = models.CharField(
- max_length=20,
- blank=True
- )
- contact_email = models.EmailField(
- blank=True,
- verbose_name='Contact E-mail'
- )
- comments = models.TextField(
- blank=True
- )
- custom_field_values = GenericRelation(
- to='extras.CustomFieldValue',
- content_type_field='obj_type',
- object_id_field='obj_id'
- )
- images = GenericRelation(
- to='extras.ImageAttachment'
- )
- tags = TaggableManager(through=TaggedItem)
- csv_headers = [
- 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
- 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
- ]
- clone_fields = [
- 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
- 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email',
- ]
- STATUS_CLASS_MAP = {
- SiteStatusChoices.STATUS_ACTIVE: 'success',
- SiteStatusChoices.STATUS_PLANNED: 'info',
- SiteStatusChoices.STATUS_RETIRED: 'danger',
- }
- class Meta:
- ordering = ('_name',)
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse('dcim:site', args=[self.slug])
- def to_csv(self):
- return (
- self.name,
- self.slug,
- self.get_status_display(),
- self.region.name if self.region else None,
- self.tenant.name if self.tenant else None,
- self.facility,
- self.asn,
- self.time_zone,
- self.description,
- self.physical_address,
- self.shipping_address,
- self.latitude,
- self.longitude,
- self.contact_name,
- self.contact_phone,
- self.contact_email,
- self.comments,
- )
- def get_status_class(self):
- return self.STATUS_CLASS_MAP.get(self.status)
- #
- # Racks
- #
- class RackGroup(ChangeLoggedModel):
- """
- Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
- example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
- campus. If a Site instead represents a single building, a RackGroup might represent a single room or floor.
- """
- name = models.CharField(
- max_length=50
- )
- slug = models.SlugField()
- site = models.ForeignKey(
- to='dcim.Site',
- on_delete=models.CASCADE,
- related_name='rack_groups'
- )
- csv_headers = ['site', 'name', 'slug']
- class Meta:
- ordering = ['site', 'name']
- unique_together = [
- ['site', 'name'],
- ['site', 'slug'],
- ]
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
- def to_csv(self):
- return (
- self.site,
- self.name,
- self.slug,
- )
- class RackRole(ChangeLoggedModel):
- """
- Racks can be organized by functional role, similar to Devices.
- """
- name = models.CharField(
- max_length=50,
- unique=True
- )
- slug = models.SlugField(
- unique=True
- )
- color = ColorField()
- description = models.CharField(
- max_length=100,
- blank=True,
- )
- csv_headers = ['name', 'slug', 'color', 'description']
- class Meta:
- ordering = ['name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return "{}?role={}".format(reverse('dcim:rack_list'), self.slug)
- def to_csv(self):
- return (
- self.name,
- self.slug,
- self.color,
- self.description,
- )
- class RackElevationHelperMixin:
- """
- Utility class that renders rack elevations. Contains helper methods for rendering elevations as a list of
- rack units represented as dictionaries, or an SVG of the elevation.
- """
- @staticmethod
- def _add_gradient(drawing, id_, color):
- gradient = drawing.linearGradient(
- start=('0', '0%'),
- end=('0', '5%'),
- spreadMethod='repeat',
- id_=id_,
- gradientTransform='rotate(45, 0, 0)',
- gradientUnits='userSpaceOnUse'
- )
- gradient.add_stop_color(offset='0%', color='#f7f7f7')
- gradient.add_stop_color(offset='50%', color='#f7f7f7')
- gradient.add_stop_color(offset='50%', color=color)
- gradient.add_stop_color(offset='100%', color=color)
- drawing.defs.add(gradient)
- @staticmethod
- def _setup_drawing(width, height):
- drawing = svgwrite.Drawing(size=(width, height))
- # add the stylesheet
- with open('{}/css/rack_elevation.css'.format(settings.STATICFILES_DIRS[0])) as css_file:
- drawing.defs.add(drawing.style(css_file.read()))
- # add gradients
- RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff')
- RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#f0f0f0')
- RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc7c7')
- return drawing
- @staticmethod
- def _draw_device_front(drawing, device, start, end, text):
- color = device.device_role.color
- link = drawing.add(
- drawing.a(
- href=reverse('dcim:device', kwargs={'pk': device.pk}),
- target='_top',
- fill='black'
- )
- )
- link.set_desc('{} — {} ({}U) {} {}'.format(
- device.device_role, device.device_type.display_name,
- device.device_type.u_height, device.asset_tag or '', device.serial or ''
- ))
- link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
- hex_color = '#{}'.format(foreground_color(color))
- link.add(drawing.text(str(device), insert=text, fill=hex_color))
- @staticmethod
- def _draw_device_rear(drawing, device, start, end, text):
- rect = drawing.rect(start, end, class_="slot blocked")
- rect.set_desc('{} — {} ({}U) {} {}'.format(
- device.device_role, device.device_type.display_name,
- device.device_type.u_height, device.asset_tag or '', device.serial or ''
- ))
- drawing.add(rect)
- drawing.add(drawing.text(str(device), insert=text))
- @staticmethod
- def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
- link = drawing.add(
- drawing.a(
- href='{}?{}'.format(
- reverse('dcim:device_add'),
- urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
- ),
- target='_top'
- )
- )
- if reservation:
- link.set_desc('{} — {} · {}'.format(
- reservation.description, reservation.user, reservation.created
- ))
- link.add(drawing.rect(start, end, class_=class_))
- link.add(drawing.text("add device", insert=text, class_='add-device'))
- def _draw_elevations(self, elevation, reserved_units, face, unit_width, unit_height):
- drawing = self._setup_drawing(unit_width, unit_height * self.u_height)
- unit_cursor = 0
- for unit in elevation:
- # Loop through all units in the elevation
- device = unit['device']
- height = unit.get('height', 1)
- # Setup drawing coordinates
- start_y = unit_cursor * unit_height
- end_y = unit_height * height
- start_cordinates = (0, start_y)
- end_cordinates = (unit_width, end_y)
- text_cordinates = (unit_width / 2, start_y + end_y / 2)
- # Draw the device
- if device and device.face == face:
- self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates)
- elif device and device.device_type.is_full_depth:
- self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates)
- else:
- # Draw shallow devices, reservations, or empty units
- class_ = 'slot'
- reservation = reserved_units.get(unit["id"])
- if device:
- class_ += ' occupied'
- if reservation:
- class_ += ' reserved'
- self._draw_empty(
- drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_, reservation
- )
- unit_cursor += height
- # Wrap the drawing with a border
- drawing.add(drawing.rect((0, 0), (unit_width, self.u_height * unit_height), class_='rack'))
- return drawing
- def merge_elevations(self, face):
- elevation = self.get_rack_units(face=face, expand_devices=False)
- other_face = DeviceFaceChoices.FACE_FRONT if face == DeviceFaceChoices.FACE_REAR else DeviceFaceChoices.FACE_REAR
- other = self.get_rack_units(face=other_face)
- unit_cursor = 0
- for u in elevation:
- o = other[unit_cursor]
- if not u['device'] and o['device']:
- u['device'] = o['device']
- u['height'] = 1
- unit_cursor += u.get('height', 1)
- return elevation
- def get_elevation_svg(
- self,
- face=DeviceFaceChoices.FACE_FRONT,
- unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
- unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
- ):
- """
- Return an SVG of the rack elevation
- :param face: Enum of [front, rear] representing the desired side of the rack elevation to render
- :param width: Width in pixles for the rendered drawing
- :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total
- height of the elevation
- """
- elevation = self.merge_elevations(face)
- reserved_units = self.get_reserved_units()
- return self._draw_elevations(elevation, reserved_units, face, unit_width, unit_height)
- class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
- """
- Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
- Each Rack is assigned to a Site and (optionally) a RackGroup.
- """
- name = models.CharField(
- max_length=50
- )
- _name = NaturalOrderingField(
- target_field='name',
- max_length=100,
- blank=True
- )
- facility_id = models.CharField(
- max_length=50,
- blank=True,
- null=True,
- verbose_name='Facility ID'
- )
- site = models.ForeignKey(
- to='dcim.Site',
- on_delete=models.PROTECT,
- related_name='racks'
- )
- group = models.ForeignKey(
- to='dcim.RackGroup',
- on_delete=models.SET_NULL,
- related_name='racks',
- blank=True,
- null=True
- )
- tenant = models.ForeignKey(
- to='tenancy.Tenant',
- on_delete=models.PROTECT,
- related_name='racks',
- blank=True,
- null=True
- )
- status = models.CharField(
- max_length=50,
- choices=RackStatusChoices,
- default=RackStatusChoices.STATUS_ACTIVE
- )
- role = models.ForeignKey(
- to='dcim.RackRole',
- on_delete=models.PROTECT,
- related_name='racks',
- blank=True,
- null=True
- )
- serial = models.CharField(
- max_length=50,
- blank=True,
- verbose_name='Serial number'
- )
- asset_tag = models.CharField(
- max_length=50,
- blank=True,
- null=True,
- unique=True,
- verbose_name='Asset tag',
- help_text='A unique tag used to identify this rack'
- )
- type = models.CharField(
- choices=RackTypeChoices,
- max_length=50,
- blank=True,
- verbose_name='Type'
- )
- width = models.PositiveSmallIntegerField(
- choices=RackWidthChoices,
- default=RackWidthChoices.WIDTH_19IN,
- verbose_name='Width',
- help_text='Rail-to-rail width'
- )
- u_height = models.PositiveSmallIntegerField(
- default=RACK_U_HEIGHT_DEFAULT,
- verbose_name='Height (U)',
- validators=[MinValueValidator(1), MaxValueValidator(100)]
- )
- desc_units = models.BooleanField(
- default=False,
- verbose_name='Descending units',
- help_text='Units are numbered top-to-bottom'
- )
- outer_width = models.PositiveSmallIntegerField(
- blank=True,
- null=True
- )
- outer_depth = models.PositiveSmallIntegerField(
- blank=True,
- null=True
- )
- outer_unit = models.CharField(
- max_length=50,
- choices=RackDimensionUnitChoices,
- blank=True,
- )
- comments = models.TextField(
- blank=True
- )
- custom_field_values = GenericRelation(
- to='extras.CustomFieldValue',
- content_type_field='obj_type',
- object_id_field='obj_id'
- )
- images = GenericRelation(
- to='extras.ImageAttachment'
- )
- tags = TaggableManager(through=TaggedItem)
- csv_headers = [
- 'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
- 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
- ]
- clone_fields = [
- 'site', 'group', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
- 'outer_depth', 'outer_unit',
- ]
- STATUS_CLASS_MAP = {
- RackStatusChoices.STATUS_RESERVED: 'warning',
- RackStatusChoices.STATUS_AVAILABLE: 'success',
- RackStatusChoices.STATUS_PLANNED: 'info',
- RackStatusChoices.STATUS_ACTIVE: 'primary',
- RackStatusChoices.STATUS_DEPRECATED: 'danger',
- }
- class Meta:
- ordering = ('site', 'group', '_name', 'pk') # (site, group, name) may be non-unique
- unique_together = (
- # Name and facility_id must be unique *only* within a RackGroup
- ('group', 'name'),
- ('group', 'facility_id'),
- )
- def __str__(self):
- return self.display_name or super().__str__()
- def get_absolute_url(self):
- return reverse('dcim:rack', args=[self.pk])
- def clean(self):
- # Validate outer dimensions and unit
- if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
- raise ValidationError("Must specify a unit when setting an outer width/depth")
- elif self.outer_width is None and self.outer_depth is None:
- self.outer_unit = ''
- if self.pk:
- # Validate that Rack is tall enough to house the installed Devices
- top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first()
- if top_device:
- min_height = top_device.position + top_device.device_type.u_height - 1
- if self.u_height < min_height:
- raise ValidationError({
- 'u_height': "Rack must be at least {}U tall to house currently installed devices.".format(
- min_height
- )
- })
- # Validate that Rack was assigned a group of its same site, if applicable
- if self.group:
- if self.group.site != self.site:
- raise ValidationError({
- 'group': "Rack group must be from the same site, {}.".format(self.site)
- })
- def save(self, *args, **kwargs):
- # Record the original site assignment for this rack.
- _site_id = None
- if self.pk:
- _site_id = Rack.objects.get(pk=self.pk).site_id
- super().save(*args, **kwargs)
- # Update racked devices if the assigned Site has been changed.
- if _site_id is not None and self.site_id != _site_id:
- devices = Device.objects.filter(rack=self)
- for device in devices:
- device.site = self.site
- device.save()
- def to_csv(self):
- return (
- self.site.name,
- self.group.name if self.group else None,
- self.name,
- self.facility_id,
- self.tenant.name if self.tenant else None,
- self.get_status_display(),
- self.role.name if self.role else None,
- self.get_type_display() if self.type else None,
- self.serial,
- self.asset_tag,
- self.width,
- self.u_height,
- self.desc_units,
- self.outer_width,
- self.outer_depth,
- self.outer_unit,
- self.comments,
- )
- @property
- def units(self):
- if self.desc_units:
- return range(1, self.u_height + 1)
- else:
- return reversed(range(1, self.u_height + 1))
- @property
- def display_name(self):
- if self.facility_id:
- return "{} ({})".format(self.name, self.facility_id)
- elif self.name:
- return self.name
- return ""
- def get_status_class(self):
- return self.STATUS_CLASS_MAP.get(self.status)
- def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True):
- """
- Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'}
- Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy.
- :param face: Rack face (front or rear)
- :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack
- :param expand_devices: When True, all units that a device occupies will be listed with each containing a
- reference to the device. When False, only the bottom most unit for a device is included and that unit
- contains a height attribute for the device
- """
- elevation = OrderedDict()
- for u in self.units:
- elevation[u] = {'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None}
- # Add devices to rack units list
- if self.pk:
- queryset = Device.objects.prefetch_related(
- 'device_type',
- 'device_type__manufacturer',
- 'device_role'
- ).annotate(
- devicebay_count=Count('device_bays')
- ).exclude(
- pk=exclude
- ).filter(
- rack=self,
- position__gt=0
- ).filter(
- Q(face=face) | Q(device_type__is_full_depth=True)
- )
- for device in queryset:
- if expand_devices:
- for u in range(device.position, device.position + device.device_type.u_height):
- elevation[u]['device'] = device
- else:
- elevation[device.position]['device'] = device
- elevation[device.position]['height'] = device.device_type.u_height
- for u in range(device.position + 1, device.position + device.device_type.u_height):
- elevation.pop(u, None)
- return [u for u in elevation.values()]
- def get_available_units(self, u_height=1, rack_face=None, exclude=list()):
- """
- Return a list of units within the rack available to accommodate a device of a given U height (default 1).
- Optionally exclude one or more devices when calculating empty units (needed when moving a device from one
- position to another within a rack).
- :param u_height: Minimum number of contiguous free units required
- :param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth
- :param exclude: List of devices IDs to exclude (useful when moving a device within a rack)
- """
- # Gather all devices which consume U space within the rack
- devices = self.devices.prefetch_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
- # Initialize the rack unit skeleton
- units = list(range(1, self.u_height + 1))
- # Remove units consumed by installed devices
- for d in devices:
- if rack_face is None or d.face == rack_face or d.device_type.is_full_depth:
- for u in range(d.position, d.position + d.device_type.u_height):
- try:
- units.remove(u)
- except ValueError:
- # Found overlapping devices in the rack!
- pass
- # Remove units without enough space above them to accommodate a device of the specified height
- available_units = []
- for u in units:
- if set(range(u, u + u_height)).issubset(units):
- available_units.append(u)
- return list(reversed(available_units))
- def get_reserved_units(self):
- """
- Return a dictionary mapping all reserved units within the rack to their reservation.
- """
- reserved_units = {}
- for r in self.reservations.all():
- for u in r.units:
- reserved_units[u] = r
- return reserved_units
- def get_0u_devices(self):
- return self.devices.filter(position=0)
- def get_utilization(self):
- """
- Determine the utilization rate of the rack and return it as a percentage. Occupied and reserved units both count
- as utilized.
- """
- # Determine unoccupied units
- available_units = self.get_available_units()
- # Remove reserved units
- for u in self.get_reserved_units():
- if u in available_units:
- available_units.remove(u)
- occupied_unit_count = self.u_height - len(available_units)
- percentage = int(float(occupied_unit_count) / self.u_height * 100)
- return percentage
- def get_power_utilization(self):
- """
- Determine the utilization rate of power in the rack and return it as a percentage.
- """
- power_stats = PowerFeed.objects.filter(
- rack=self
- ).annotate(
- allocated_draw_total=Sum('connected_endpoint__poweroutlets__connected_endpoint__allocated_draw'),
- ).values(
- 'allocated_draw_total',
- 'available_power'
- )
- if power_stats:
- allocated_draw_total = sum(x['allocated_draw_total'] for x in power_stats)
- available_power_total = sum(x['available_power'] for x in power_stats)
- return int(allocated_draw_total / available_power_total * 100) or 0
- return 0
- class RackReservation(ChangeLoggedModel):
- """
- One or more reserved units within a Rack.
- """
- rack = models.ForeignKey(
- to='dcim.Rack',
- on_delete=models.CASCADE,
- related_name='reservations'
- )
- units = ArrayField(
- base_field=models.PositiveSmallIntegerField()
- )
- tenant = models.ForeignKey(
- to='tenancy.Tenant',
- on_delete=models.PROTECT,
- related_name='rackreservations',
- blank=True,
- null=True
- )
- user = models.ForeignKey(
- to=User,
- on_delete=models.PROTECT
- )
- description = models.CharField(
- max_length=100
- )
- class Meta:
- ordering = ['created']
- def __str__(self):
- return "Reservation for rack {}".format(self.rack)
- def clean(self):
- if self.units:
- # Validate that all specified units exist in the Rack.
- invalid_units = [u for u in self.units if u not in self.rack.units]
- if invalid_units:
- raise ValidationError({
- 'units': "Invalid unit(s) for {}U rack: {}".format(
- self.rack.u_height,
- ', '.join([str(u) for u in invalid_units]),
- ),
- })
- # Check that none of the units has already been reserved for this Rack.
- reserved_units = []
- for resv in self.rack.reservations.exclude(pk=self.pk):
- reserved_units += resv.units
- conflicting_units = [u for u in self.units if u in reserved_units]
- if conflicting_units:
- raise ValidationError({
- 'units': 'The following units have already been reserved: {}'.format(
- ', '.join([str(u) for u in conflicting_units]),
- )
- })
- @property
- def unit_list(self):
- """
- Express the assigned units as a string of summarized ranges. For example:
- [0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"
- """
- group = (list(x) for _, x in groupby(sorted(self.units), lambda x, c=count(): next(c) - x))
- return ', '.join('-'.join(map(str, (g[0], g[-1])[:len(g)])) for g in group)
- #
- # Device Types
- #
- class Manufacturer(ChangeLoggedModel):
- """
- A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
- """
- name = models.CharField(
- max_length=50,
- unique=True
- )
- slug = models.SlugField(
- unique=True
- )
- csv_headers = ['name', 'slug']
- class Meta:
- ordering = ['name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
- def to_csv(self):
- return (
- self.name,
- self.slug,
- )
- class DeviceType(ChangeLoggedModel, CustomFieldModel):
- """
- A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
- well as high-level functional role(s).
- Each DeviceType can have an arbitrary number of component templates assigned to it, which define console, power, and
- interface objects. For example, a Juniper EX4300-48T DeviceType would have:
- * 1 ConsolePortTemplate
- * 2 PowerPortTemplates
- * 48 InterfaceTemplates
- When a new Device of this type is created, the appropriate console, power, and interface objects (as defined by the
- DeviceType) are automatically created as well.
- """
- manufacturer = models.ForeignKey(
- to='dcim.Manufacturer',
- on_delete=models.PROTECT,
- related_name='device_types'
- )
- model = models.CharField(
- max_length=50
- )
- slug = models.SlugField()
- part_number = models.CharField(
- max_length=50,
- blank=True,
- help_text='Discrete part number (optional)'
- )
- u_height = models.PositiveSmallIntegerField(
- default=1,
- verbose_name='Height (U)'
- )
- is_full_depth = models.BooleanField(
- default=True,
- verbose_name='Is full depth',
- help_text='Device consumes both front and rear rack faces'
- )
- subdevice_role = models.CharField(
- max_length=50,
- choices=SubdeviceRoleChoices,
- blank=True,
- verbose_name='Parent/child status',
- help_text='Parent devices house child devices in device bays. Leave blank '
- 'if this device type is neither a parent nor a child.'
- )
- comments = models.TextField(
- blank=True
- )
- custom_field_values = GenericRelation(
- to='extras.CustomFieldValue',
- content_type_field='obj_type',
- object_id_field='obj_id'
- )
- tags = TaggableManager(through=TaggedItem)
- clone_fields = [
- 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role',
- ]
- class Meta:
- ordering = ['manufacturer', 'model']
- unique_together = [
- ['manufacturer', 'model'],
- ['manufacturer', 'slug'],
- ]
- def __str__(self):
- return self.model
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Save a copy of u_height for validation in clean()
- self._original_u_height = self.u_height
- def get_absolute_url(self):
- return reverse('dcim:devicetype', args=[self.pk])
- def to_yaml(self):
- data = OrderedDict((
- ('manufacturer', self.manufacturer.name),
- ('model', self.model),
- ('slug', self.slug),
- ('part_number', self.part_number),
- ('u_height', self.u_height),
- ('is_full_depth', self.is_full_depth),
- ('subdevice_role', self.subdevice_role),
- ('comments', self.comments),
- ))
- # Component templates
- if self.consoleport_templates.exists():
- data['console-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- }
- for c in self.consoleport_templates.all()
- ]
- if self.consoleserverport_templates.exists():
- data['console-server-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- }
- for c in self.consoleserverport_templates.all()
- ]
- if self.powerport_templates.exists():
- data['power-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'maximum_draw': c.maximum_draw,
- 'allocated_draw': c.allocated_draw,
- }
- for c in self.powerport_templates.all()
- ]
- if self.poweroutlet_templates.exists():
- data['power-outlets'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'power_port': c.power_port.name if c.power_port else None,
- 'feed_leg': c.feed_leg,
- }
- for c in self.poweroutlet_templates.all()
- ]
- if self.interface_templates.exists():
- data['interfaces'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'mgmt_only': c.mgmt_only,
- }
- for c in self.interface_templates.all()
- ]
- if self.frontport_templates.exists():
- data['front-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'rear_port': c.rear_port.name,
- 'rear_port_position': c.rear_port_position,
- }
- for c in self.frontport_templates.all()
- ]
- if self.rearport_templates.exists():
- data['rear-ports'] = [
- {
- 'name': c.name,
- 'type': c.type,
- 'positions': c.positions,
- }
- for c in self.rearport_templates.all()
- ]
- if self.device_bay_templates.exists():
- data['device-bays'] = [
- {
- 'name': c.name,
- }
- for c in self.device_bay_templates.all()
- ]
- return yaml.dump(dict(data), sort_keys=False)
- def clean(self):
- # If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
- # room to expand within their racks. This validation will impose a very high performance penalty when there are
- # many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence.
- if self.pk is not None and self.u_height > self._original_u_height:
- for d in Device.objects.filter(device_type=self, position__isnull=False):
- face_required = None if self.is_full_depth else d.face
- u_available = d.rack.get_available_units(u_height=self.u_height, rack_face=face_required,
- exclude=[d.pk])
- if d.position not in u_available:
- raise ValidationError({
- 'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of "
- "{}U".format(d, d.rack, self.u_height)
- })
- if (
- self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
- ) and self.device_bay_templates.count():
- raise ValidationError({
- 'subdevice_role': "Must delete all device bay templates associated with this device before "
- "declassifying it as a parent device."
- })
- if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD:
- raise ValidationError({
- 'u_height': "Child device types must be 0U."
- })
- @property
- def display_name(self):
- return '{} {}'.format(self.manufacturer.name, self.model)
- @property
- def is_parent_device(self):
- return self.subdevice_role == SubdeviceRoleChoices.ROLE_PARENT
- @property
- def is_child_device(self):
- return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
- #
- # Devices
- #
- class DeviceRole(ChangeLoggedModel):
- """
- Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
- color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to
- virtual machines as well.
- """
- name = models.CharField(
- max_length=50,
- unique=True
- )
- slug = models.SlugField(
- unique=True
- )
- color = ColorField()
- vm_role = models.BooleanField(
- default=True,
- verbose_name='VM Role',
- help_text='Virtual machines may be assigned to this role'
- )
- description = models.CharField(
- max_length=100,
- blank=True,
- )
- csv_headers = ['name', 'slug', 'color', 'vm_role', 'description']
- class Meta:
- ordering = ['name']
- def __str__(self):
- return self.name
- def to_csv(self):
- return (
- self.name,
- self.slug,
- self.color,
- self.vm_role,
- self.description,
- )
- class Platform(ChangeLoggedModel):
- """
- Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
- NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by
- specifying a NAPALM driver.
- """
- name = models.CharField(
- max_length=100,
- unique=True
- )
- slug = models.SlugField(
- unique=True,
- max_length=100
- )
- manufacturer = models.ForeignKey(
- to='dcim.Manufacturer',
- on_delete=models.PROTECT,
- related_name='platforms',
- blank=True,
- null=True,
- help_text='Optionally limit this platform to devices of a certain manufacturer'
- )
- napalm_driver = models.CharField(
- max_length=50,
- blank=True,
- verbose_name='NAPALM driver',
- help_text='The name of the NAPALM driver to use when interacting with devices'
- )
- napalm_args = JSONField(
- blank=True,
- null=True,
- verbose_name='NAPALM arguments',
- help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)'
- )
- csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
- class Meta:
- ordering = ['name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
- def to_csv(self):
- return (
- self.name,
- self.slug,
- self.manufacturer.name if self.manufacturer else None,
- self.napalm_driver,
- self.napalm_args,
- )
- class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
- """
- A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
- DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
- Each Device must be assigned to a site, and optionally to a rack within that site. Associating a device with a
- particular rack face or unit is optional (for example, vertically mounted PDUs do not consume rack units).
- When a new Device is created, console/power/interface/device bay components are created along with it as dictated
- by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the
- creation of a Device.
- """
- device_type = models.ForeignKey(
- to='dcim.DeviceType',
- on_delete=models.PROTECT,
- related_name='instances'
- )
- device_role = models.ForeignKey(
- to='dcim.DeviceRole',
- on_delete=models.PROTECT,
- related_name='devices'
- )
- tenant = models.ForeignKey(
- to='tenancy.Tenant',
- on_delete=models.PROTECT,
- related_name='devices',
- blank=True,
- null=True
- )
- platform = models.ForeignKey(
- to='dcim.Platform',
- on_delete=models.SET_NULL,
- related_name='devices',
- blank=True,
- null=True
- )
- name = models.CharField(
- max_length=64,
- blank=True,
- null=True
- )
- _name = NaturalOrderingField(
- target_field='name',
- max_length=100,
- blank=True,
- null=True
- )
- serial = models.CharField(
- max_length=50,
- blank=True,
- verbose_name='Serial number'
- )
- asset_tag = models.CharField(
- max_length=50,
- blank=True,
- null=True,
- unique=True,
- verbose_name='Asset tag',
- help_text='A unique tag used to identify this device'
- )
- site = models.ForeignKey(
- to='dcim.Site',
- on_delete=models.PROTECT,
- related_name='devices'
- )
- rack = models.ForeignKey(
- to='dcim.Rack',
- on_delete=models.PROTECT,
- related_name='devices',
- blank=True,
- null=True
- )
- position = models.PositiveSmallIntegerField(
- blank=True,
- null=True,
- validators=[MinValueValidator(1)],
- verbose_name='Position (U)',
- help_text='The lowest-numbered unit occupied by the device'
- )
- face = models.CharField(
- max_length=50,
- blank=True,
- choices=DeviceFaceChoices,
- verbose_name='Rack face'
- )
- status = models.CharField(
- max_length=50,
- choices=DeviceStatusChoices,
- default=DeviceStatusChoices.STATUS_ACTIVE
- )
- primary_ip4 = models.OneToOneField(
- to='ipam.IPAddress',
- on_delete=models.SET_NULL,
- related_name='primary_ip4_for',
- blank=True,
- null=True,
- verbose_name='Primary IPv4'
- )
- primary_ip6 = models.OneToOneField(
- to='ipam.IPAddress',
- on_delete=models.SET_NULL,
- related_name='primary_ip6_for',
- blank=True,
- null=True,
- verbose_name='Primary IPv6'
- )
- cluster = models.ForeignKey(
- to='virtualization.Cluster',
- on_delete=models.SET_NULL,
- related_name='devices',
- blank=True,
- null=True
- )
- virtual_chassis = models.ForeignKey(
- to='VirtualChassis',
- on_delete=models.SET_NULL,
- related_name='members',
- blank=True,
- null=True
- )
- vc_position = models.PositiveSmallIntegerField(
- blank=True,
- null=True,
- validators=[MaxValueValidator(255)]
- )
- vc_priority = models.PositiveSmallIntegerField(
- blank=True,
- null=True,
- validators=[MaxValueValidator(255)]
- )
- comments = models.TextField(
- blank=True
- )
- custom_field_values = GenericRelation(
- to='extras.CustomFieldValue',
- content_type_field='obj_type',
- object_id_field='obj_id'
- )
- images = GenericRelation(
- to='extras.ImageAttachment'
- )
- tags = TaggableManager(through=TaggedItem)
- csv_headers = [
- 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
- 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
- ]
- clone_fields = [
- 'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster',
- ]
- STATUS_CLASS_MAP = {
- DeviceStatusChoices.STATUS_OFFLINE: 'warning',
- DeviceStatusChoices.STATUS_ACTIVE: 'success',
- DeviceStatusChoices.STATUS_PLANNED: 'info',
- DeviceStatusChoices.STATUS_STAGED: 'primary',
- DeviceStatusChoices.STATUS_FAILED: 'danger',
- DeviceStatusChoices.STATUS_INVENTORY: 'default',
- DeviceStatusChoices.STATUS_DECOMMISSIONING: 'warning',
- }
- class Meta:
- ordering = ('_name', 'pk') # Name may be null
- unique_together = (
- ('site', 'tenant', 'name'), # See validate_unique below
- ('rack', 'position', 'face'),
- ('virtual_chassis', 'vc_position'),
- )
- permissions = (
- ('napalm_read', 'Read-only access to devices via NAPALM'),
- ('napalm_write', 'Read/write access to devices via NAPALM'),
- )
- def __str__(self):
- return self.display_name or super().__str__()
- def get_absolute_url(self):
- return reverse('dcim:device', args=[self.pk])
- def validate_unique(self, exclude=None):
- # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
- # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
- # of the uniqueness constraint without manual intervention.
- if self.name and self.tenant is None:
- if Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True):
- raise ValidationError({
- 'name': 'A device with this name already exists.'
- })
- super().validate_unique(exclude)
- def clean(self):
- super().clean()
- # Validate site/rack combination
- if self.rack and self.site != self.rack.site:
- raise ValidationError({
- 'rack': "Rack {} does not belong to site {}.".format(self.rack, self.site),
- })
- if self.rack is None:
- if self.face:
- raise ValidationError({
- 'face': "Cannot select a rack face without assigning a rack.",
- })
- if self.position:
- raise ValidationError({
- 'face': "Cannot select a rack position without assigning a rack.",
- })
- # Validate position/face combination
- if self.position and not self.face:
- raise ValidationError({
- 'face': "Must specify rack face when defining rack position.",
- })
- # Prevent 0U devices from being assigned to a specific position
- if self.position and self.device_type.u_height == 0:
- raise ValidationError({
- 'position': "A U0 device type ({}) cannot be assigned to a rack position.".format(self.device_type)
- })
- if self.rack:
- try:
- # Child devices cannot be assigned to a rack face/unit
- if self.device_type.is_child_device and self.face:
- raise ValidationError({
- 'face': "Child device types cannot be assigned to a rack face. This is an attribute of the "
- "parent device."
- })
- if self.device_type.is_child_device and self.position:
- raise ValidationError({
- 'position': "Child device types cannot be assigned to a rack position. This is an attribute of "
- "the parent device."
- })
- # Validate rack space
- rack_face = self.face if not self.device_type.is_full_depth else None
- exclude_list = [self.pk] if self.pk else []
- try:
- available_units = self.rack.get_available_units(
- u_height=self.device_type.u_height, rack_face=rack_face, exclude=exclude_list
- )
- if self.position and self.position not in available_units:
- raise ValidationError({
- 'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) "
- "{} ({}U).".format(self.position, self.device_type, self.device_type.u_height)
- })
- except Rack.DoesNotExist:
- pass
- except DeviceType.DoesNotExist:
- pass
- # Validate primary IP addresses
- vc_interfaces = self.vc_interfaces.all()
- if self.primary_ip4:
- if self.primary_ip4.interface in vc_interfaces:
- pass
- elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in vc_interfaces:
- pass
- else:
- raise ValidationError({
- 'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format(
- self.primary_ip4),
- })
- if self.primary_ip6:
- if self.primary_ip6.interface in vc_interfaces:
- pass
- elif self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.interface in vc_interfaces:
- pass
- else:
- raise ValidationError({
- 'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(
- self.primary_ip6),
- })
- # Validate manufacturer/platform
- if hasattr(self, 'device_type') and self.platform:
- if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
- raise ValidationError({
- 'platform': "The assigned platform is limited to {} device types, but this device's type belongs "
- "to {}.".format(self.platform.manufacturer, self.device_type.manufacturer)
- })
- # A Device can only be assigned to a Cluster in the same Site (or no Site)
- if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
- raise ValidationError({
- 'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
- })
- # Validate virtual chassis assignment
- if self.virtual_chassis and self.vc_position is None:
- raise ValidationError({
- 'vc_position': "A device assigned to a virtual chassis must have its position defined."
- })
- def save(self, *args, **kwargs):
- is_new = not bool(self.pk)
- super().save(*args, **kwargs)
- # If this is a new Device, instantiate all of the related components per the DeviceType definition
- if is_new:
- ConsolePort.objects.bulk_create(
- [x.instantiate(self) for x in self.device_type.consoleport_templates.all()]
- )
- ConsoleServerPort.objects.bulk_create(
- [x.instantiate(self) for x in self.device_type.consoleserverport_templates.all()]
- )
- PowerPort.objects.bulk_create(
- [x.instantiate(self) for x in self.device_type.powerport_templates.all()]
- )
- PowerOutlet.objects.bulk_create(
- [x.instantiate(self) for x in self.device_type.poweroutlet_templates.all()]
- )
- Interface.objects.bulk_create(
- [x.instantiate(self) for x in self.device_type.interface_templates.all()]
- )
- RearPort.objects.bulk_create(
- [x.instantiate(self) for x in self.device_type.rearport_templates.all()]
- )
- FrontPort.objects.bulk_create(
- [x.instantiate(self) for x in self.device_type.frontport_templates.all()]
- )
- DeviceBay.objects.bulk_create(
- [x.instantiate(self) for x in self.device_type.device_bay_templates.all()]
- )
- # Update Site and Rack assignment for any child Devices
- devices = Device.objects.filter(parent_bay__device=self)
- for device in devices:
- device.site = self.site
- device.rack = self.rack
- device.save()
- def to_csv(self):
- return (
- self.name or '',
- self.device_role.name,
- self.tenant.name if self.tenant else None,
- self.device_type.manufacturer.name,
- self.device_type.model,
- self.platform.name if self.platform else None,
- self.serial,
- self.asset_tag,
- self.get_status_display(),
- self.site.name,
- self.rack.group.name if self.rack and self.rack.group else None,
- self.rack.name if self.rack else None,
- self.position,
- self.get_face_display(),
- self.comments,
- )
- @property
- def display_name(self):
- if self.name:
- return self.name
- elif self.virtual_chassis and self.virtual_chassis.master.name:
- return "{}:{}".format(self.virtual_chassis.master, self.vc_position)
- elif hasattr(self, 'device_type'):
- return "{}".format(self.device_type)
- return ""
- @property
- def identifier(self):
- """
- Return the device name if set; otherwise return the Device's primary key as {pk}
- """
- if self.name is not None:
- return self.name
- return '{{{}}}'.format(self.pk)
- @property
- def primary_ip(self):
- if settings.PREFER_IPV4 and self.primary_ip4:
- return self.primary_ip4
- elif self.primary_ip6:
- return self.primary_ip6
- elif self.primary_ip4:
- return self.primary_ip4
- else:
- return None
- def get_vc_master(self):
- """
- If this Device is a VirtualChassis member, return the VC master. Otherwise, return None.
- """
- return self.virtual_chassis.master if self.virtual_chassis else None
- @property
- def vc_interfaces(self):
- """
- Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another
- Device belonging to the same VirtualChassis.
- """
- filter = Q(device=self)
- if self.virtual_chassis and self.virtual_chassis.master == self:
- filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
- return Interface.objects.filter(filter)
- def get_cables(self, pk_list=False):
- """
- Return a QuerySet or PK list matching all Cables connected to a component of this Device.
- """
- cable_pks = []
- for component_model in [
- ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort
- ]:
- cable_pks += component_model.objects.filter(
- device=self, cable__isnull=False
- ).values_list('cable', flat=True)
- if pk_list:
- return cable_pks
- return Cable.objects.filter(pk__in=cable_pks)
- def get_children(self):
- """
- Return the set of child Devices installed in DeviceBays within this Device.
- """
- return Device.objects.filter(parent_bay__device=self.pk)
- def get_status_class(self):
- return self.STATUS_CLASS_MAP.get(self.status)
- #
- # Virtual chassis
- #
- class VirtualChassis(ChangeLoggedModel):
- """
- A collection of Devices which operate with a shared control plane (e.g. a switch stack).
- """
- master = models.OneToOneField(
- to='Device',
- on_delete=models.PROTECT,
- related_name='vc_master_for'
- )
- domain = models.CharField(
- max_length=30,
- blank=True
- )
- tags = TaggableManager(through=TaggedItem)
- csv_headers = ['master', 'domain']
- class Meta:
- ordering = ['master']
- verbose_name_plural = 'virtual chassis'
- def __str__(self):
- return str(self.master) if hasattr(self, 'master') else 'New Virtual Chassis'
- def get_absolute_url(self):
- return self.master.get_absolute_url()
- def clean(self):
- # Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
- # VirtualChassis.)
- if self.pk and self.master not in self.members.all():
- raise ValidationError({
- 'master': "The selected master is not assigned to this virtual chassis."
- })
- def delete(self, *args, **kwargs):
- # Check for LAG interfaces split across member chassis
- interfaces = Interface.objects.filter(
- device__in=self.members.all(),
- lag__isnull=False
- ).exclude(
- lag__device=F('device')
- )
- if interfaces:
- raise ProtectedError(
- "Unable to delete virtual chassis {}. There are member interfaces which form a cross-chassis "
- "LAG".format(self),
- interfaces
- )
- return super().delete(*args, **kwargs)
- def to_csv(self):
- return (
- self.master,
- self.domain,
- )
- #
- # Power
- #
- class PowerPanel(ChangeLoggedModel):
- """
- A distribution point for electrical power; e.g. a data center RPP.
- """
- site = models.ForeignKey(
- to='Site',
- on_delete=models.PROTECT
- )
- rack_group = models.ForeignKey(
- to='RackGroup',
- on_delete=models.PROTECT,
- blank=True,
- null=True
- )
- name = models.CharField(
- max_length=50
- )
- csv_headers = ['site', 'rack_group_name', 'name']
- class Meta:
- ordering = ['site', 'name']
- unique_together = ['site', 'name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse('dcim:powerpanel', args=[self.pk])
- def to_csv(self):
- return (
- self.site.name,
- self.rack_group.name if self.rack_group else None,
- self.name,
- )
- def clean(self):
- # RackGroup must belong to assigned Site
- if self.rack_group and self.rack_group.site != self.site:
- raise ValidationError("Rack group {} ({}) is in a different site than {}".format(
- self.rack_group, self.rack_group.site, self.site
- ))
- class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
- """
- An electrical circuit delivered from a PowerPanel.
- """
- power_panel = models.ForeignKey(
- to='PowerPanel',
- on_delete=models.PROTECT,
- related_name='powerfeeds'
- )
- rack = models.ForeignKey(
- to='Rack',
- on_delete=models.PROTECT,
- blank=True,
- null=True
- )
- connected_endpoint = models.OneToOneField(
- to='dcim.PowerPort',
- on_delete=models.SET_NULL,
- related_name='+',
- blank=True,
- null=True
- )
- connection_status = models.NullBooleanField(
- choices=CONNECTION_STATUS_CHOICES,
- blank=True
- )
- name = models.CharField(
- max_length=50
- )
- status = models.CharField(
- max_length=50,
- choices=PowerFeedStatusChoices,
- default=PowerFeedStatusChoices.STATUS_ACTIVE
- )
- type = models.CharField(
- max_length=50,
- choices=PowerFeedTypeChoices,
- default=PowerFeedTypeChoices.TYPE_PRIMARY
- )
- supply = models.CharField(
- max_length=50,
- choices=PowerFeedSupplyChoices,
- default=PowerFeedSupplyChoices.SUPPLY_AC
- )
- phase = models.CharField(
- max_length=50,
- choices=PowerFeedPhaseChoices,
- default=PowerFeedPhaseChoices.PHASE_SINGLE
- )
- voltage = models.PositiveSmallIntegerField(
- validators=[MinValueValidator(1)],
- default=POWERFEED_VOLTAGE_DEFAULT
- )
- amperage = models.PositiveSmallIntegerField(
- validators=[MinValueValidator(1)],
- default=POWERFEED_AMPERAGE_DEFAULT
- )
- max_utilization = models.PositiveSmallIntegerField(
- validators=[MinValueValidator(1), MaxValueValidator(100)],
- default=POWERFEED_MAX_UTILIZATION_DEFAULT,
- help_text="Maximum permissible draw (percentage)"
- )
- available_power = models.PositiveIntegerField(
- default=0,
- editable=False
- )
- comments = models.TextField(
- blank=True
- )
- custom_field_values = GenericRelation(
- to='extras.CustomFieldValue',
- content_type_field='obj_type',
- object_id_field='obj_id'
- )
- tags = TaggableManager(through=TaggedItem)
- csv_headers = [
- 'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
- 'amperage', 'max_utilization', 'comments',
- ]
- clone_fields = [
- 'power_panel', 'rack', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
- 'available_power',
- ]
- STATUS_CLASS_MAP = {
- PowerFeedStatusChoices.STATUS_OFFLINE: 'warning',
- PowerFeedStatusChoices.STATUS_ACTIVE: 'success',
- PowerFeedStatusChoices.STATUS_PLANNED: 'info',
- PowerFeedStatusChoices.STATUS_FAILED: 'danger',
- }
- TYPE_CLASS_MAP = {
- PowerFeedTypeChoices.TYPE_PRIMARY: 'success',
- PowerFeedTypeChoices.TYPE_REDUNDANT: 'info',
- }
- class Meta:
- ordering = ['power_panel', 'name']
- unique_together = ['power_panel', 'name']
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse('dcim:powerfeed', args=[self.pk])
- def to_csv(self):
- return (
- self.power_panel.site.name,
- self.power_panel.name,
- self.rack.group.name if self.rack and self.rack.group else None,
- self.rack.name if self.rack else None,
- self.name,
- self.get_status_display(),
- self.get_type_display(),
- self.get_supply_display(),
- self.get_phase_display(),
- self.voltage,
- self.amperage,
- self.max_utilization,
- self.comments,
- )
- def clean(self):
- # Rack must belong to same Site as PowerPanel
- if self.rack and self.rack.site != self.power_panel.site:
- raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format(
- self.rack, self.rack.site, self.power_panel, self.power_panel.site
- ))
- def save(self, *args, **kwargs):
- # Cache the available_power property on the instance
- kva = self.voltage * self.amperage * (self.max_utilization / 100)
- if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
- self.available_power = round(kva * 1.732)
- else:
- self.available_power = round(kva)
- super().save(*args, **kwargs)
- def get_type_class(self):
- return self.TYPE_CLASS_MAP.get(self.type)
- def get_status_class(self):
- return self.STATUS_CLASS_MAP.get(self.status)
- #
- # Cables
- #
- class Cable(ChangeLoggedModel):
- """
- A physical connection between two endpoints.
- """
- termination_a_type = models.ForeignKey(
- to=ContentType,
- limit_choices_to=CABLE_TERMINATION_MODELS,
- on_delete=models.PROTECT,
- related_name='+'
- )
- termination_a_id = models.PositiveIntegerField()
- termination_a = GenericForeignKey(
- ct_field='termination_a_type',
- fk_field='termination_a_id'
- )
- termination_b_type = models.ForeignKey(
- to=ContentType,
- limit_choices_to=CABLE_TERMINATION_MODELS,
- on_delete=models.PROTECT,
- related_name='+'
- )
- termination_b_id = models.PositiveIntegerField()
- termination_b = GenericForeignKey(
- ct_field='termination_b_type',
- fk_field='termination_b_id'
- )
- type = models.CharField(
- max_length=50,
- choices=CableTypeChoices,
- blank=True
- )
- status = models.CharField(
- max_length=50,
- choices=CableStatusChoices,
- default=CableStatusChoices.STATUS_CONNECTED
- )
- label = models.CharField(
- max_length=100,
- blank=True
- )
- color = ColorField(
- blank=True
- )
- length = models.PositiveSmallIntegerField(
- blank=True,
- null=True
- )
- length_unit = models.CharField(
- max_length=50,
- choices=CableLengthUnitChoices,
- blank=True,
- )
- # Stores the normalized length (in meters) for database ordering
- _abs_length = models.DecimalField(
- max_digits=10,
- decimal_places=4,
- blank=True,
- null=True
- )
- # Cache the associated device (where applicable) for the A and B terminations. This enables filtering of Cables by
- # their associated Devices.
- _termination_a_device = models.ForeignKey(
- to=Device,
- on_delete=models.CASCADE,
- related_name='+',
- blank=True,
- null=True
- )
- _termination_b_device = models.ForeignKey(
- to=Device,
- on_delete=models.CASCADE,
- related_name='+',
- blank=True,
- null=True
- )
- csv_headers = [
- 'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id', 'type', 'status', 'label',
- 'color', 'length', 'length_unit',
- ]
- STATUS_CLASS_MAP = {
- CableStatusChoices.STATUS_CONNECTED: 'success',
- CableStatusChoices.STATUS_PLANNED: 'info',
- }
- class Meta:
- ordering = ['pk']
- unique_together = (
- ('termination_a_type', 'termination_a_id'),
- ('termination_b_type', 'termination_b_id'),
- )
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # A copy of the PK to be used by __str__ in case the object is deleted
- self._pk = self.pk
- def __str__(self):
- return self.label or '#{}'.format(self._pk)
- def get_absolute_url(self):
- return reverse('dcim:cable', args=[self.pk])
- def clean(self):
- # Validate that termination A exists
- if not hasattr(self, 'termination_a_type'):
- raise ValidationError('Termination A type has not been specified')
- try:
- self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
- except ObjectDoesNotExist:
- raise ValidationError({
- 'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type)
- })
- # Validate that termination B exists
- if not hasattr(self, 'termination_b_type'):
- raise ValidationError('Termination B type has not been specified')
- try:
- self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
- except ObjectDoesNotExist:
- raise ValidationError({
- 'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
- })
- type_a = self.termination_a_type.model
- type_b = self.termination_b_type.model
- # Validate interface types
- if type_a == 'interface' and self.termination_a.type in NONCONNECTABLE_IFACE_TYPES:
- raise ValidationError({
- 'termination_a_id': 'Cables cannot be terminated to {} interfaces'.format(
- self.termination_a.get_type_display()
- )
- })
- if type_b == 'interface' and self.termination_b.type in NONCONNECTABLE_IFACE_TYPES:
- raise ValidationError({
- 'termination_b_id': 'Cables cannot be terminated to {} interfaces'.format(
- self.termination_b.get_type_display()
- )
- })
- # Check that termination types are compatible
- if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
- raise ValidationError("Incompatible termination types: {} and {}".format(
- self.termination_a_type, self.termination_b_type
- ))
- # A component with multiple positions must be connected to a component with an equal number of positions
- term_a_positions = getattr(self.termination_a, 'positions', 1)
- term_b_positions = getattr(self.termination_b, 'positions', 1)
- if term_a_positions != term_b_positions:
- raise ValidationError(
- "{} has {} positions and {} has {}. Both terminations must have the same number of positions.".format(
- self.termination_a, term_a_positions, self.termination_b, term_b_positions
- )
- )
- # A termination point cannot be connected to itself
- if self.termination_a == self.termination_b:
- raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
- # A front port cannot be connected to its corresponding rear port
- if (
- type_a in ['frontport', 'rearport'] and
- type_b in ['frontport', 'rearport'] and
- (
- getattr(self.termination_a, 'rear_port', None) == self.termination_b or
- getattr(self.termination_b, 'rear_port', None) == self.termination_a
- )
- ):
- raise ValidationError("A front port cannot be connected to it corresponding rear port")
- # Check for an existing Cable connected to either termination object
- if self.termination_a.cable not in (None, self):
- raise ValidationError("{} already has a cable attached (#{})".format(
- self.termination_a, self.termination_a.cable_id
- ))
- if self.termination_b.cable not in (None, self):
- raise ValidationError("{} already has a cable attached (#{})".format(
- self.termination_b, self.termination_b.cable_id
- ))
- # Validate length and length_unit
- if self.length is not None and not self.length_unit:
- raise ValidationError("Must specify a unit when setting a cable length")
- elif self.length is None:
- self.length_unit = ''
- def save(self, *args, **kwargs):
- # Store the given length (if any) in meters for use in database ordering
- if self.length and self.length_unit:
- self._abs_length = to_meters(self.length, self.length_unit)
- else:
- self._abs_length = None
- # Store the parent Device for the A and B terminations (if applicable) to enable filtering
- if hasattr(self.termination_a, 'device'):
- self._termination_a_device = self.termination_a.device
- if hasattr(self.termination_b, 'device'):
- self._termination_b_device = self.termination_b.device
- super().save(*args, **kwargs)
- # Update the private pk used in __str__ in case this is a new object (i.e. just got its pk)
- self._pk = self.pk
- def to_csv(self):
- return (
- '{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model),
- self.termination_a_id,
- '{}.{}'.format(self.termination_b_type.app_label, self.termination_b_type.model),
- self.termination_b_id,
- self.get_type_display(),
- self.get_status_display(),
- self.label,
- self.color,
- self.length,
- self.length_unit,
- )
- def get_status_class(self):
- return self.STATUS_CLASS_MAP.get(self.status)
- def get_compatible_types(self):
- """
- Return all termination types compatible with termination A.
- """
- if self.termination_a is None:
- return
- return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
- def get_path_endpoints(self):
- """
- Traverse both ends of a cable path and return its connected endpoints. Note that one or both endpoints may be
- None.
- """
- a_path = self.termination_b.trace()
- b_path = self.termination_a.trace()
- # Determine overall path status (connected or planned)
- if self.status == CableStatusChoices.STATUS_PLANNED:
- path_status = CONNECTION_STATUS_PLANNED
- else:
- path_status = CONNECTION_STATUS_CONNECTED
- for segment in a_path[1:] + b_path[1:]:
- if segment[1] is None or segment[1].status == CableStatusChoices.STATUS_PLANNED:
- path_status = CONNECTION_STATUS_PLANNED
- break
- a_endpoint = a_path[-1][2]
- b_endpoint = b_path[-1][2]
- return a_endpoint, b_endpoint, path_status
|