forked from openwisp/openwisp-network-topology
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnode.py
More file actions
199 lines (175 loc) · 6.78 KB
/
Copy pathnode.py
File metadata and controls
199 lines (175 loc) · 6.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import json
from collections import OrderedDict
from copy import deepcopy
from datetime import timedelta
import swapper
from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.db.models import JSONField, TextField
from django.db.models.functions import Cast
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from rest_framework.utils.encoders import JSONEncoder
from openwisp_users.mixins import ShareableOrgMixin
from openwisp_utils.base import TimeStampedEditableModel
from .. import settings as app_settings
from ..signals import update_topology
from ..utils import print_info
class AbstractNode(ShareableOrgMixin, TimeStampedEditableModel):
"""
NetJSON NetworkGraph Node Object implementation
"""
topology = models.ForeignKey(
swapper.get_model_name("topology", "Topology"), on_delete=models.CASCADE
)
label = models.CharField(max_length=64, blank=True)
# netjson ID and local_addresses
addresses = JSONField(default=list, blank=True, encoder=DjangoJSONEncoder)
properties = JSONField(
default=dict,
blank=True,
encoder=DjangoJSONEncoder,
)
user_properties = JSONField(
verbose_name=_("user defined properties"),
help_text=_("If you need to add additional data to this node use this field"),
default=dict,
blank=True,
encoder=DjangoJSONEncoder,
)
class Meta:
abstract = True
def __str__(self):
return self.name
def full_clean(self, *args, **kwargs):
self.organization_id = self.get_organization_id()
return super().full_clean(*args, **kwargs)
def clean(self):
if self.properties is None:
self.properties = {}
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
@property
def netjson_id(self):
if len(self.addresses) > 0:
return self.addresses[0]
@cached_property
def local_addresses(self):
if len(self.addresses) > 1:
return self.addresses[1:]
@property
def name(self):
return self.label or self.netjson_id or ""
def get_name(self):
"""
May be overridden/monkey patched to get the node name
from other sources (e.g: device name in openwisp-controller)
"""
return self.name
def get_organization_id(self):
"""
May be overridden/monkey patched to get the node organization
from other sources (e.g: device organization_id in openwisp-controller)
"""
# Node will get organization of topology if it is unspecified.
if self.organization_id is None:
return self.topology.organization_id
else:
# Non-shared node can belong to shared topology. But,
# a shared node cannot belong to non-shared topology.
if (
self.topology.organization_id is not None
and self.topology.organization_id != self.organization_id
):
raise ValidationError(
_("node should have same organization as topology.")
)
return self.organization_id
def json(self, dict=False, original=False, **kwargs):
"""
Returns a NetJSON NetworkGraph Node object.
If ``original`` is passed, the data will be returned
as it has been collected from the network (used when
doing the comparison).
"""
netjson = OrderedDict({"id": self.netjson_id})
label = self.get_name()
if label:
netjson["label"] = label
for attr in ["local_addresses", "properties"]:
value = getattr(self, attr)
if value or attr == "properties":
netjson[attr] = deepcopy(value)
if not original:
netjson["properties"].update(deepcopy(self.user_properties))
netjson["properties"]["created"] = JSONEncoder().default(self.created)
netjson["properties"]["modified"] = JSONEncoder().default(self.modified)
if dict:
return netjson
return json.dumps(netjson, cls=JSONEncoder, **kwargs)
@classmethod
def get_from_address(cls, address, topology):
"""
Find node from one of its addresses and its topology.
:param address: string
:param topology: Topology instance
:returns: Node object or None
"""
needle = '"{}"'.format(address)
return (
cls.objects.filter(topology=topology)
.annotate(_addresses_text=Cast("addresses", output_field=TextField()))
.filter(_addresses_text__contains=needle)
.first()
)
@classmethod
def count_address(cls, address, topology):
"""
Count nodes with the specified address and topology.
:param address: string
:param topology: Topology instance
:returns: int
"""
needle = '"{}"'.format(address)
return (
cls.objects.filter(topology=topology)
.annotate(_addresses_text=Cast("addresses", output_field=TextField()))
.filter(_addresses_text__contains=needle)
.count()
)
@classmethod
def delete_expired_nodes(cls):
"""
deletes nodes that have not been connected to the network
for more than the amount of days specified in
``OPENWISP_NETWORK_TOPOLOGY_NODE_EXPIRATION``
"""
NODE_EXPIRATION = app_settings.NODE_EXPIRATION
LINK_EXPIRATION = app_settings.LINK_EXPIRATION
if NODE_EXPIRATION not in [False, None] and LINK_EXPIRATION not in [
False,
None,
]:
expiration_date = now() - timedelta(days=int(NODE_EXPIRATION))
expired_nodes = cls.objects.filter(
modified__lt=expiration_date,
source_link_set__isnull=True,
target_link_set__isnull=True,
)
expired_nodes_length = len(expired_nodes)
if expired_nodes_length:
print_info("Deleting {0} expired nodes".format(expired_nodes_length))
for node in expired_nodes:
node.delete()
@classmethod
def get_queryset(cls, qs):
"""admin list queryset"""
return qs.select_related("organization", "topology")
@receiver(post_save, sender=swapper.get_model_name("topology", "Node"))
@receiver(post_delete, sender=swapper.get_model_name("topology", "Node"))
def send_topology_signal(sender, instance, **kwargs):
update_topology.send(sender=sender, topology=instance.topology)