services.py 3.3 KB

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