Skip to content

Commit

Permalink
Allow control over IP allocation method during host create (unioslo#551)
Browse files Browse the repository at this point in the history
* Allow users/clients to request how IPs are allocated from networks during Host creation.
  • Loading branch information
terjekv authored Nov 20, 2024
1 parent 8a7d34f commit a82de33
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 6 deletions.
31 changes: 31 additions & 0 deletions mreg/api/v1/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from mreg.models.host import Host, Ipaddress, PtrOverride
from mreg.models.zone import ForwardZone, ReverseZone
from mreg.models.resource_records import Txt
from mreg.types import IPAllocationMethod

from mreg.utils import nonify

Expand Down Expand Up @@ -557,6 +558,36 @@ def test_hosts_post_case_insenstive_201_created(self):
response = self.assert_post_and_201('/hosts/', data)
self.assertEqual(response['Location'], '/api/v1/hosts/%s' % self.post_data['name'])

def prepare_allocator_data(self, allocation_method):
network = '10.0.0.0/24'
Network.objects.create(network=network)
data = self.post_data.copy()
data['allocation_method'] = allocation_method
data['network'] = network
del data['ipaddress']
return data

def assert_successful_allocation(self, allocation_method):
data = self.prepare_allocator_data(allocation_method)
response = self.assert_post_and_201('/hosts/', data)
self.assertEqual(response['Location'], f'/api/v1/hosts/{self.post_data["name"]}')
# TODO: Implement validation for IP address allocation
Network.objects.get(network=data['network']).delete()

def test_hosts_post_with_allocation_first_201_created(self):
"""Posting a new host with an allocator of 'first' should return 201"""
self.assert_successful_allocation(IPAllocationMethod.FIRST.value)

def test_hosts_post_with_allocation_random_201_created(self):
"""Posting a new host with an allocator of 'random' should return 201"""
self.assert_successful_allocation(IPAllocationMethod.RANDOM.value)

def test_hosts_post_with_allocation_broken_400_bad_request(self):
"""Posting a new host with an invalid allocator should return 400"""
data = self.prepare_allocator_data("broken")
self.assert_post_and_400('/hosts/', data)
Network.objects.get(network=data['network']).delete()

def test_hosts_post_400_invalid_ip(self):
""""Posting a new host with an invalid IP should return 400"""
post_data = {'name': 'failing.example.org', 'ipaddress': '300.400.500.600',
Expand Down
40 changes: 34 additions & 6 deletions mreg/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from mreg.models.host import Host, Ipaddress, PtrOverride
from mreg.models.network import Network, NetGroupRegexPermission
from mreg.models.resource_records import Cname, Loc, Naptr, Srv, Sshfp, Txt, Hinfo, Mx
from mreg.types import IPAllocationMethod

from mreg.api.permissions import (
IsAuthenticatedAndReadOnly,
Expand Down Expand Up @@ -304,32 +305,59 @@ def post(self, request, *args, **kwargs):
content = {"ERROR": "'ipaddress' and 'network' is mutually exclusive"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)

if "allocation_method" in request.data and "network" not in request.data:
return Response(
{"ERROR": "allocation_method is only allowed with 'network'"},
status=status.HTTP_400_BAD_REQUEST)

# request.data is immutable
hostdata = request.data.copy()

# Hostdata *may* be MultiValueDict, which means that pop will return a list, even if get
# would return a single value...

if "network" in hostdata:
network_key = hostdata.pop("network")
if isinstance(network_key, list):
network_key = network_key[0]

try:
ipaddress.ip_network(hostdata["network"])
ipaddress.ip_network(network_key)
except ValueError as error:
content = {"ERROR": str(error)}
return Response(content, status=status.HTTP_400_BAD_REQUEST)

network = Network.objects.filter(network=hostdata["network"]).first()
network = Network.objects.filter(network=network_key).first()
if not network:
content = {"ERROR": "no such network"}
return Response(content, status=status.HTTP_404_NOT_FOUND)

ip = network.get_random_unused()
try:
allocation_key = hostdata.pop("allocation_method", IPAllocationMethod.FIRST.value)
if isinstance(allocation_key, list):
allocation_key = allocation_key[0]
request_ip_allocator = IPAllocationMethod(allocation_key.lower())
except ValueError:
options = [method.value for method in IPAllocationMethod]
content = {"ERROR": f"allocation_method must be one of {', '.join(options)}"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)

if request_ip_allocator == IPAllocationMethod.RANDOM:
ip = network.get_random_unused()
else:
ip = network.get_first_unused()

if not ip:
content = {"ERROR": "no available IP in network"}
return Response(content, status=status.HTTP_404_NOT_FOUND)

hostdata["ipaddress"] = ip
del hostdata["network"]

if "ipaddress" in hostdata:
ipkey = hostdata["ipaddress"]
del hostdata["ipaddress"]
ipkey = hostdata.pop("ipaddress")
if isinstance(ipkey, list):
ipkey = ipkey[0]

host = Host()
hostserializer = HostSerializer(host, data=hostdata)

Expand Down
5 changes: 5 additions & 0 deletions mreg/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from enum import Enum

class IPAllocationMethod(Enum):
RANDOM = "random"
FIRST = "first"

0 comments on commit a82de33

Please sign in to comment.