Coverage for src/meshadmin/server/networks/forms.py: 87%
203 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-10 16:08 +0200
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-10 16:08 +0200
1import ipaddress
2from datetime import timedelta
4import structlog
5from django import forms
6from django.contrib.auth import get_user_model
7from django.core.exceptions import ValidationError
8from django.shortcuts import get_object_or_404
9from django.utils import timezone
11from meshadmin.server.networks.models import (
12 CA,
13 Group,
14 Host,
15 Network,
16 NetworkMembership,
17 Rule,
18 Template,
19)
20from meshadmin.server.networks.services import (
21 create_network,
22 create_network_ca,
23 network_available_hosts_iterator,
24)
26logger = structlog.get_logger(__name__)
28User = get_user_model()
30RECOMMENDED_RANGES = [
31 ("192.168.0.0/16", "Private network range, ideal for small networks"),
32 ("172.16.0.0/12", "Private network range, good for medium networks"),
33 ("10.0.0.0/8", "Private network range, suitable for large networks"),
34 ("100.64.0.0/10", "Carrier-grade NAT range, recommended by Nebula docs"),
35]
38class NetworkForm(forms.ModelForm):
39 class Meta:
40 model = Network
41 fields = ("name", "cidr", "update_interval")
42 widgets = {
43 "update_interval": forms.NumberInput(attrs={"min": 5, "max": 3600}),
44 }
46 def __init__(self, *args, **kwargs):
47 self.request = kwargs.pop("request", None)
48 super().__init__(*args, **kwargs)
50 def clean_cidr(self):
51 cidr = self.cleaned_data["cidr"]
53 try:
54 network = ipaddress.ip_network(cidr)
55 except ValueError as e:
56 raise ValidationError(f"Invalid CIDR format: {str(e)}")
58 in_recommended_range = False
59 for recommended_cidr, _ in RECOMMENDED_RANGES:
60 if ipaddress.ip_network(cidr).subnet_of(
61 ipaddress.ip_network(recommended_cidr)
62 ):
63 in_recommended_range = True
64 break
66 if not in_recommended_range:
67 suggestions = []
68 network_size = network.num_addresses
69 for recommended_cidr, description in RECOMMENDED_RANGES:
70 rec_network = ipaddress.ip_network(recommended_cidr)
71 if network_size <= rec_network.num_addresses:
72 suggestions.append(f"{recommended_cidr} - {description}")
74 raise ValidationError(
75 f"Warning: The CIDR {cidr} is outside recommended network ranges. "
76 "Consider using one of these ranges instead:\n"
77 + "\n".join(f"• {suggestion}" for suggestion in suggestions)
78 )
80 return cidr
82 def save(self, commit=True):
83 logger.info("save network")
84 instance = super().save(commit=False)
85 if not instance.pk:
86 instance = create_network(
87 network_name=self.cleaned_data["name"],
88 network_cidr=self.cleaned_data["cidr"],
89 update_interval=self.cleaned_data["update_interval"],
90 user=self.request.user,
91 )
92 elif commit:
93 instance.save()
95 self.save_m2m()
96 return instance
99class CAForm(forms.ModelForm):
100 class Meta:
101 model = CA
102 fields = ("name", "network")
104 def __init__(self, *args, **kwargs):
105 network = kwargs.pop("network", None)
106 super().__init__(*args, **kwargs)
108 if network:
109 self.fields.pop("network")
110 self.instance.network = network
112 def save(self, commit=True):
113 instance = super().save(commit=False)
115 if instance.pk:
116 if commit:
117 instance.save()
118 else:
119 instance = create_network_ca(instance.name, instance.network)
121 return instance
124class GroupForm(forms.ModelForm):
125 class Meta:
126 model = Group
127 fields = ("name", "description")
129 def __init__(self, *args, **kwargs):
130 network = kwargs.pop("network", None)
131 super().__init__(*args, **kwargs)
133 if network:
134 self.instance.network = network
137class TemplateForm(forms.ModelForm):
138 expiry_days = forms.IntegerField(
139 required=False,
140 min_value=1,
141 label="Expires in (days)",
142 help_text="Days until the key expires. Leave empty for no expiration.",
143 )
145 class Meta:
146 model = Template
147 fields = (
148 "name",
149 "network",
150 "is_lighthouse",
151 "is_relay",
152 "use_relay",
153 "groups",
154 "reusable",
155 "usage_limit",
156 "ephemeral_peers",
157 )
159 def __init__(self, *args, **kwargs):
160 network = kwargs.pop("network", None)
161 super().__init__(*args, **kwargs)
163 if self.instance and self.instance.pk and self.instance.expires_at:
164 days_remaining = (self.instance.expires_at - timezone.now()).days
165 if days_remaining > 0:
166 self.fields["expiry_days"].initial = days_remaining
168 network_id = None
169 if network:
170 network_id = network.id
171 self.fields.pop("network")
172 self.instance.network = network
173 elif self.instance and self.instance.pk:
174 network_id = self.instance.network_id
175 elif "initial" in kwargs and "network_id" in kwargs["initial"]:
176 network_id = kwargs["initial"]["network_id"]
178 if network_id:
179 self.fields["groups"].queryset = Group.objects.filter(
180 network_id=network_id
181 ).all()
183 def save(self, commit=True):
184 instance = super().save(commit=False)
186 expiry_days = self.cleaned_data.get("expiry_days")
187 if expiry_days:
188 instance.expires_at = timezone.now() + timedelta(days=expiry_days)
189 elif "expiry_days" in self.changed_data:
190 instance.expires_at = None
192 if commit:
193 instance.save()
194 self.save_m2m()
196 return instance
199class HostForm(forms.ModelForm):
200 class Meta:
201 model = Host
202 fields = (
203 "name",
204 "network",
205 "assigned_ip",
206 "is_lighthouse",
207 "is_relay",
208 "use_relay",
209 "groups",
210 "public_ip_or_hostname",
211 "public_auth_key",
212 "interface",
213 )
215 def __init__(self, *args, **kwargs):
216 network = kwargs.pop("network", None)
217 super().__init__(*args, **kwargs)
219 network_id = None
220 if network:
221 network_id = network.id
222 self.fields.pop("network")
223 self.instance.network = network
224 elif self.instance and self.instance.pk:
225 network_id = self.instance.network_id
227 if network_id:
228 self.fields["groups"].queryset = Group.objects.filter(
229 network_id=network_id
230 ).all()
232 if not self.instance.pk:
233 network = Network.objects.get(id=network_id)
234 ipv4_iterator = network_available_hosts_iterator(network)
235 self.initial["assigned_ip"] = next(ipv4_iterator)
238class RuleForm(forms.ModelForm):
239 class Meta:
240 model = Rule
241 fields = (
242 "direction",
243 "proto",
244 "port",
245 "group",
246 "groups",
247 "cidr",
248 "local_cidr",
249 )
251 def __init__(self, *args, **kwargs):
252 super().__init__(*args, **kwargs)
253 security_group_id = kwargs.get("initial", {}).get("security_group_id")
254 if not security_group_id and kwargs.get("instance"):
255 security_group_id = kwargs["instance"].security_group_id
257 if security_group_id:
258 security_group = get_object_or_404(Group, id=security_group_id)
259 group_queryset = Group.objects.filter(network_id=security_group.network_id)
260 self.fields["group"].queryset = group_queryset
261 self.fields["groups"].queryset = group_queryset
263 def clean(self):
264 cleaned_data = super().clean()
266 # Validate port format
267 port = cleaned_data.get("port")
268 if port and port not in ("0", "any", "fragment"):
269 if "-" in port:
270 try:
271 start, end = map(int, port.split("-"))
272 if not (0 <= start <= 65535 and 0 <= end <= 65535):
273 raise ValueError
274 except ValueError:
275 raise ValidationError(
276 {
277 "port": "Port range must be two valid port numbers (0-65535) separated by a hyphen"
278 }
279 )
280 elif not port.isdigit() or not 0 <= int(port) <= 65535:
281 raise ValidationError(
282 {
283 "port": "Port must be 'any', 'fragment', or a number between 0 and 65535"
284 }
285 )
287 # Validate that at least one target specification exists
288 group = cleaned_data.get("group")
289 groups = cleaned_data.get("groups")
290 cidr = cleaned_data.get("cidr")
292 if not any([group, groups.exists() if groups else False, cidr]):
293 raise ValidationError(
294 "At least one of group, groups, or CIDR must be specified to identify "
295 "which hosts the rule applies to."
296 )
298 # Validate CIDR format if provided
299 if cidr:
300 try:
301 ipaddress.ip_network(cidr)
302 except ValueError:
303 raise ValidationError(
304 {"cidr": "Invalid CIDR format. Example: 0.0.0.0/0"}
305 )
307 # Validate local_cidr format if provided
308 local_cidr = cleaned_data.get("local_cidr")
309 if local_cidr:
310 try:
311 ipaddress.ip_network(local_cidr)
312 except ValueError:
313 raise ValidationError(
314 {"local_cidr": "Invalid CIDR format. Example: 0.0.0.0/0"}
315 )
317 return cleaned_data
320class NetworkMembershipForm(forms.ModelForm):
321 email = forms.EmailField(
322 help_text="Enter the email address of the user you want to add to the network"
323 )
325 class Meta:
326 model = NetworkMembership
327 fields = ["role"]
329 def __init__(self, *args, **kwargs):
330 self.network = kwargs.pop("network", None)
331 super().__init__(*args, **kwargs)
332 if self.instance.pk:
333 self.fields["email"].initial = self.instance.user.email
334 self.fields["email"].disabled = True
336 def clean_email(self):
337 email = self.cleaned_data["email"]
338 user = User.objects.filter(email=email).first()
339 if not user:
340 raise ValidationError("No user found with this email address")
341 if (
342 self.network
343 and NetworkMembership.objects.filter(
344 network=self.network, user=user
345 ).exists()
346 ):
347 raise ValidationError("This user is already a member of the network")
348 return email
350 def save(self, commit=True):
351 instance = super().save(commit=False)
352 if not instance.pk:
353 email = self.cleaned_data["email"]
354 user = User.objects.get(email=email)
355 instance.user = user
356 instance.network = self.network
357 if commit:
358 instance.save()
359 return instance