services.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. from django.contrib.postgres.fields import ArrayField
  2. from django.core.exceptions import ValidationError
  3. from django.core.validators import MaxValueValidator, MinValueValidator
  4. from django.db import models
  5. from django.urls import reverse
  6. from django.utils.translation import gettext_lazy as _
  7. from ipam.choices import *
  8. from ipam.constants import *
  9. from netbox.models import PrimaryModel
  10. from netbox.models.features import ContactsMixin
  11. from utilities.data import array_to_string
  12. __all__ = (
  13. 'Service',
  14. 'ServiceTemplate',
  15. )
  16. class ServiceBase(models.Model):
  17. protocol = models.CharField(
  18. verbose_name=_('protocol'),
  19. max_length=50,
  20. choices=ServiceProtocolChoices
  21. )
  22. ports = ArrayField(
  23. base_field=models.PositiveIntegerField(
  24. validators=[
  25. MinValueValidator(SERVICE_PORT_MIN),
  26. MaxValueValidator(SERVICE_PORT_MAX)
  27. ]
  28. ),
  29. verbose_name=_('port numbers')
  30. )
  31. class Meta:
  32. abstract = True
  33. def __str__(self):
  34. return f'{self.name} ({self.get_protocol_display()}/{self.port_list})'
  35. @property
  36. def port_list(self):
  37. return array_to_string(self.ports)
  38. class ServiceTemplate(ServiceBase, PrimaryModel):
  39. """
  40. A template for a Service to be applied to a device or virtual machine.
  41. """
  42. name = models.CharField(
  43. verbose_name=_('name'),
  44. max_length=100,
  45. unique=True
  46. )
  47. class Meta:
  48. ordering = ('name',)
  49. verbose_name = _('service template')
  50. verbose_name_plural = _('service templates')
  51. def get_absolute_url(self):
  52. return reverse('ipam:servicetemplate', args=[self.pk])
  53. class Service(ContactsMixin, ServiceBase, PrimaryModel):
  54. """
  55. A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
  56. optionally be tied to one or more specific IPAddresses belonging to its parent.
  57. """
  58. device = models.ForeignKey(
  59. to='dcim.Device',
  60. on_delete=models.CASCADE,
  61. related_name='services',
  62. verbose_name=_('device'),
  63. null=True,
  64. blank=True
  65. )
  66. virtual_machine = models.ForeignKey(
  67. to='virtualization.VirtualMachine',
  68. on_delete=models.CASCADE,
  69. related_name='services',
  70. null=True,
  71. blank=True
  72. )
  73. name = models.CharField(
  74. max_length=100,
  75. verbose_name=_('name')
  76. )
  77. ipaddresses = models.ManyToManyField(
  78. to='ipam.IPAddress',
  79. related_name='services',
  80. blank=True,
  81. verbose_name=_('IP addresses'),
  82. help_text=_("The specific IP addresses (if any) to which this service is bound")
  83. )
  84. clone_fields = ['protocol', 'ports', 'description', 'device', 'virtual_machine', 'ipaddresses', ]
  85. class Meta:
  86. ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
  87. verbose_name = _('service')
  88. verbose_name_plural = _('services')
  89. def get_absolute_url(self):
  90. return reverse('ipam:service', args=[self.pk])
  91. @property
  92. def parent(self):
  93. return self.device or self.virtual_machine
  94. def clean(self):
  95. super().clean()
  96. # A Service must belong to a Device *or* to a VirtualMachine
  97. if self.device and self.virtual_machine:
  98. raise ValidationError(_("A service cannot be associated with both a device and a virtual machine."))
  99. if not self.device and not self.virtual_machine:
  100. raise ValidationError(_("A service must be associated with either a device or a virtual machine."))