address.py 8.3 KB
Newer Older
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
1
2
3
# ---------------------------------------------------------------------
# Address model
# ---------------------------------------------------------------------
4
# Copyright (C) 2007-2020 The NOC Project
Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
5
6
# See LICENSE for details
# ---------------------------------------------------------------------
7

8
9
10
# Python modules
from typing import Optional

Dmitry Volodin's avatar
Dmitry Volodin committed
11
# Third-party modules
12
from django.db import models
13
from django.contrib.postgres.fields import ArrayField
Dmitry Volodin's avatar
Dmitry Volodin committed
14

Dmitry Lukhtionov's avatar
Dmitry Lukhtionov committed
15
# NOC modules
Dmitry Volodin's avatar
Dmitry Volodin committed
16
from noc.config import config
17
from noc.core.model.decorator import on_init
Dmitry Volodin's avatar
Dmitry Volodin committed
18
from noc.core.model.base import NOCModel
19
from noc.project.models.project import Project
Dmitry Volodin's avatar
Dmitry Volodin committed
20
from noc.sa.models.managedobject import ManagedObject
21
from noc.core.model.fields import INETField, MACField
22
from noc.core.validators import ValidationError, check_fqdn, is_ipv4, is_ipv6
23
from noc.main.models.textindex import full_text_search
24
from noc.main.models.label import Label
Dmitry Volodin's avatar
Dmitry Volodin committed
25
from noc.core.model.fields import DocumentReferenceField
Dmitry Volodin's avatar
Dmitry Volodin committed
26
27
from noc.core.wf.decorator import workflow
from noc.wf.models.state import State
Dmitry Volodin's avatar
Dmitry Volodin committed
28
from noc.core.model.decorator import on_delete_check
29
from noc.core.change.decorator import change
30
from noc.core.translation import ugettext as _
Dmitry Volodin's avatar
Dmitry Volodin committed
31
32
from .afi import AFI_CHOICES
from .vrf import VRF
Dmitry Volodin's avatar
Dmitry Volodin committed
33
from .addressprofile import AddressProfile
34
35


36
@Label.model
37
@on_init
38
@change
39
@full_text_search
Dmitry Volodin's avatar
Dmitry Volodin committed
40
@workflow
Dmitry Volodin's avatar
Dmitry Volodin committed
41
@on_delete_check(check=[("ip.Address", "ipv6_transition")])
Dmitry Volodin's avatar
Dmitry Volodin committed
42
class Address(NOCModel):
Dmitry Volodin's avatar
Dmitry Volodin committed
43
    class Meta(object):
44
45
46
47
48
49
        verbose_name = _("Address")
        verbose_name_plural = _("Addresses")
        db_table = "ip_address"
        app_label = "ip"
        unique_together = [("vrf", "afi", "address")]

Dmitry Volodin's avatar
Dmitry Volodin committed
50
    prefix = models.ForeignKey("ip.Prefix", verbose_name=_("Prefix"), on_delete=models.CASCADE)
51
    vrf = models.ForeignKey(
Dmitry Volodin's avatar
Dmitry Volodin committed
52
        VRF, verbose_name=_("VRF"), default=VRF.get_global, on_delete=models.CASCADE
53
    )
Dmitry Volodin's avatar
Dmitry Volodin committed
54
    afi = models.CharField(_("Address Family"), max_length=1, choices=AFI_CHOICES)
55
    address = INETField(_("Address"))
Dmitry Volodin's avatar
Dmitry Volodin committed
56
57
    profile = DocumentReferenceField(AddressProfile, null=False, blank=False)
    name = models.CharField(_("Name"), max_length=255, null=False, blank=False)
58
59
60
61
    fqdn = models.CharField(
        _("FQDN"),
        max_length=255,
        help_text=_("Full-qualified Domain Name"),
Dmitry Volodin's avatar
Dmitry Volodin committed
62
        validators=[check_fqdn],
Dmitry Volodin's avatar
Dmitry Volodin committed
63
64
        null=True,
        blank=True,
Dmitry Volodin's avatar
Dmitry Volodin committed
65
    )
66
    project = models.ForeignKey(
Dmitry Volodin's avatar
Dmitry Volodin committed
67
68
        Project,
        verbose_name="Project",
69
        on_delete=models.SET_NULL,
70
71
        null=True,
        blank=True,
Dmitry Volodin's avatar
Dmitry Volodin committed
72
73
74
        related_name="address_set",
    )
    mac = MACField("MAC", null=True, blank=True, help_text=_("MAC Address"))
75
    auto_update_mac = models.BooleanField(
Dmitry Volodin's avatar
Dmitry Volodin committed
76
77
        "Auto Update MAC", default=False, help_text=_("Set to auto-update MAC field")
    )
78
79
80
    managed_object = models.ForeignKey(
        ManagedObject,
        verbose_name=_("Managed Object"),
Dmitry Volodin's avatar
Dmitry Volodin committed
81
82
        null=True,
        blank=True,
83
84
        related_name="address_set",
        on_delete=models.SET_NULL,
Dmitry Volodin's avatar
Dmitry Volodin committed
85
        help_text=_("Set if address belongs to the Managed Object's interface"),
Dmitry Volodin's avatar
Dmitry Volodin committed
86
    )
Dmitry Volodin's avatar
Dmitry Volodin committed
87
88
    subinterface = models.CharField("SubInterface", max_length=128, null=True, blank=True)
    description = models.TextField(_("Description"), blank=True, null=True)
89
90
91
92
93
    # Labels
    labels = ArrayField(models.CharField(max_length=250), blank=True, null=True, default=list)
    effective_labels = ArrayField(
        models.CharField(max_length=250), blank=True, null=True, default=list
    )
Dmitry Volodin's avatar
Dmitry Volodin committed
94
95
    tt = models.IntegerField(_("TT"), blank=True, null=True, help_text=_("Ticket #"))
    state = DocumentReferenceField(State, null=True, blank=True)
96
97
    allocated_till = models.DateField(
        _("Allocated till"),
Dmitry Volodin's avatar
Dmitry Volodin committed
98
99
100
101
        null=True,
        blank=True,
        help_text=_("Address temporary allocated till the date"),
    )
102
103
104
    ipv6_transition = models.OneToOneField(
        "self",
        related_name="ipv4_transition",
Dmitry Volodin's avatar
Dmitry Volodin committed
105
106
        null=True,
        blank=True,
107
        limit_choices_to={"afi": "6"},
Dmitry Volodin's avatar
Dmitry Volodin committed
108
109
        on_delete=models.SET_NULL,
    )
Dmitry Volodin's avatar
Dmitry Volodin committed
110
111
112
113
114
115
116
117
    source = models.CharField(
        "Source",
        max_length=1,
        choices=[
            ("M", "Manual"),
            ("i", "Interface"),
            ("m", "Management"),
            ("d", "DHCP"),
Dmitry Volodin's avatar
Dmitry Volodin committed
118
            ("n", "Neighbor"),
Dmitry Volodin's avatar
Dmitry Volodin committed
119
        ],
Dmitry Volodin's avatar
Dmitry Volodin committed
120
121
122
        null=False,
        blank=False,
        default="M",
Dmitry Volodin's avatar
Dmitry Volodin committed
123
    )
124
125

    csv_ignored_fields = ["prefix"]
126
    _clean_fields = ["vrf", "prefix", "afi"]
127

Dmitry Volodin's avatar
Dmitry Volodin committed
128
    def __str__(self):
Dmitry Volodin's avatar
Dmitry Volodin committed
129
        return "%s(%s): %s" % (self.vrf.name, self.afi, self.address)
130

131
    @classmethod
132
    def get_by_id(cls, id) -> Optional["Address"]:
133
134
135
136
137
        address = Address.objects.filter(id=id)[:1]
        if address:
            return address[0]
        return None

138
    def iter_changed_datastream(self, changed_fields=None):
Dmitry Volodin's avatar
Dmitry Volodin committed
139
140
141
142
        if config.datastream.enable_address:
            yield "address", self.id
        if config.datastream.enable_dnszone:
            from noc.dns.models.dnszone import DNSZone
Dmitry Volodin's avatar
Dmitry Volodin committed
143

Dmitry Volodin's avatar
Dmitry Volodin committed
144
145
146
147
148
149
150
151
152
153
154
            if self.fqdn:
                # Touch forward zone
                fz = DNSZone.get_zone(self.fqdn)
                if fz:
                    for ds, id in fz.iter_changed_datastream(changed_fields=changed_fields):
                        yield ds, id
                # Touch reverse zone
                rz = DNSZone.get_zone(self.address)
                if rz:
                    for ds, id in rz.iter_changed_datastream(changed_fields=changed_fields):
                        yield ds, id
155
156

    @classmethod
157
    def get_afi(cls, address: str) -> str:
158
159
160
        return "6" if ":" in address else "4"

    @classmethod
161
    def get_collision(cls, vrf: "VRF", address: str) -> Optional["Address"]:
162
163
164
165
166
167
168
        """
        Check VRFGroup restrictions
        :param vrf:
        :param address:
        :return: VRF already containing address or None
        :rtype: VRF or None
        """
169
        if not vrf.vrf_group or vrf.vrf_group.address_constraint != "G":
170
171
            return None
        afi = cls.get_afi(address)
172
173
174
175
        a = Address.objects.get(
            afi=afi, address=address, vrf__in=vrf.vrf_group.vrf_set.exclude(id=vrf.id)
        ).first()
        if a:
176
            return a.vrf
177
        return None
178
179
180
181
182
183

    def clean(self):
        """
        Field validation
        :return:
        """
Dmitry Volodin's avatar
Dmitry Volodin committed
184
185
        # Get proper AFI
        self.afi = "6" if ":" in self.address else "4"
186
        # Check prefix is of AFI type
187
188
189
190
        if self.is_ipv4 and not is_ipv4(self.address):
            raise ValidationError({"address": f"Invalid IPv4 {self.address}"})
        elif self.is_ipv6 and not is_ipv6(self.address):
            raise ValidationError({"address": f"Invalid IPv6 {self.address}"})
Dmitry Volodin's avatar
Dmitry Volodin committed
191
192
193
194
195
        # Check VRF
        if not self.vrf:
            self.vrf = VRF.get_global()
        # Find parent prefix
        self.prefix = Prefix.get_parent(self.vrf, self.afi, self.address)
196
197
198
199
        # Check VRF group restrictions
        cv = self.get_collision(self.vrf, self.address)
        if cv:
            # Collision detected
200
            raise ValidationError({"vrf": f"Address already exists in VRF {cv}"})
201
202

    @property
203
    def short_description(self) -> str:
204
205
206
        """
        First line of description
        """
207
208
209
210
211
        if self.description:
            return self.description.split("\n", 1)[0].strip()
        else:
            return ""

Dmitry Volodin's avatar
Dmitry Volodin committed
212
213
214
215
    def get_index(self):
        """
        Full-text search
        """
Dmitry Volodin's avatar
Dmitry Volodin committed
216
217
218
219
        content = [self.address, self.name]
        card = "Address %s, Name %s" % (self.address, self.name)
        if self.fqdn:
            content += [self.fqdn]
Dmitry Volodin's avatar
Dmitry Volodin committed
220
            card += ", FQDN %s" % self.fqdn
Dmitry Volodin's avatar
Dmitry Volodin committed
221
222
223
224
225
226
227
        if self.mac:
            content += [self.mac]
            card += ", MAC %s" % self.mac
        if self.description:
            content += [self.description]
            card += " (%s)" % self.description
        r = {
228
            "id": "ip.address:%s" % self.id,
Dmitry Volodin's avatar
Dmitry Volodin committed
229
230
            "title": self.address,
            "content": "\n".join(content),
Dmitry Volodin's avatar
Dmitry Volodin committed
231
            "card": card,
Dmitry Volodin's avatar
Dmitry Volodin committed
232
        }
233
234
        if self.labels:
            r["tags"] = self.labels
Dmitry Volodin's avatar
Dmitry Volodin committed
235
236
        return r

237
238
    @classmethod
    def get_search_result_url(cls, obj_id):
239
        return f"/api/card/view/address/{obj_id}/"
Dmitry Volodin's avatar
Dmitry Volodin committed
240
241

    @property
242
    def is_ipv4(self) -> bool:
Dmitry Volodin's avatar
Dmitry Volodin committed
243
244
245
        return self.afi == "4"

    @property
246
    def is_ipv6(self) -> bool:
Dmitry Volodin's avatar
Dmitry Volodin committed
247
        return self.afi == "6"
Dmitry Volodin's avatar
Dmitry Volodin committed
248

249
250
    @classmethod
    def can_set_label(cls, label):
251
        return Label.get_effective_setting(label, setting="enable_ipaddress")
252

Dmitry Volodin's avatar
Dmitry Volodin committed
253
254
255

# Avoid django's validation failure
from .prefix import Prefix