Skip to content

Commit

Permalink
Add host-in-community support.
Browse files Browse the repository at this point in the history
  • Loading branch information
terjekv committed Jan 22, 2025
1 parent e204a5d commit 1661db9
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 8 deletions.
90 changes: 88 additions & 2 deletions mreg/api/v1/tests/test_network_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db import transaction

from mreg.models.network_policy import NetworkPolicy, NetworkPolicyAttribute, NetworkPolicyAttributeValue, Community
from mreg.models.host import Host, Ipaddress
from mreg.models.network import Network

from .tests import MregAPITestCase
Expand Down Expand Up @@ -224,5 +225,90 @@ def test_create_host_with_community_ok(self):
}
ret = self.assert_post_and_201('/api/v1/hosts/', data=data)
ret = self.assert_get(ret.headers['Location'])
print(ret.json())
net.delete()
net.delete()

def create_policy_setup(self,
community_name: str = "test_community",
host_name: str = "hostwithcommunity.example.com",
ip_address: str = "10.0.0.1",
network: str = "10.0.0.0/24",
policy_name: str = "test_policy",
):
np = self._create_network_policy(policy_name, [])
community = self._create_community(community_name, "community desc", np)
net = Network.objects.create(network=network, description="test_network", policy=np)
host = Host.objects.create(name=host_name)
ip = Ipaddress.objects.create(host=host, ipaddress=ip_address)

self.addCleanup(host.delete)
self.addCleanup(ip.delete)
self.addCleanup(net.delete)
self.addCleanup(community.delete)
self.addCleanup(np.delete)

return np, community, net, host, ip

def test_add_host_to_community_ok(self):
"""Test adding a host to a community."""
np, community, _, host, _ = self.create_policy_setup()

data = {
"id": host.pk
}

ret = self.assert_post_and_201(f'{POLICY_ENDPOINT}{np.pk}/communities/{community.pk}/hosts/', data=data)
self.assertEqual(ret.json()['name'], "hostwithcommunity.example.com")

ret = self.assert_get(f'{POLICY_ENDPOINT}{np.pk}/communities/{community.pk}/hosts/')
self.assertEqual(len(ret.json()['results']), 1)
self.assertEqual(ret.json()['results'][0]['name'], "hostwithcommunity.example.com")


def test_get_individual_host_from_community_ok(self):
"""Test getting a host from a community."""
np, community, _, host, _ = self.create_policy_setup()
host.set_community(community)

ret = self.assert_get(f'{POLICY_ENDPOINT}{np.pk}/communities/{community.pk}/hosts/{host.pk}')
self.assertEqual(ret.json()['name'], "hostwithcommunity.example.com")

def test_get_individual_host_from_community_host_does_not_exist(self):
"""Test getting a host from a community where host does not exist."""
np, community, _, _, _ = self.create_policy_setup()

self.assert_get_and_404(f'{POLICY_ENDPOINT}{np.pk}/communities/{community.pk}/hosts/99999999')

def test_add_host_to_community_ip_not_in_network(self):
"""Test adding a host to a community when the hosts IP is not in the network for the policy."""
np, community, _, host, _ = self.create_policy_setup(ip_address="10.0.1.0")

data = {
"id": host.pk
}

self.assert_post_and_400(f'{POLICY_ENDPOINT}{np.pk}/communities/{community.pk}/hosts/', data=data)

def test_delete_host_from_community_ok(self):
"""Test deleting a host from a community."""
np, community, _, host, _ = self.create_policy_setup()
host.set_community(community)

self.assert_delete_and_204(f'{POLICY_ENDPOINT}{np.pk}/communities/{community.pk}/hosts/{host.pk}')

ret = self.assert_get(f'{POLICY_ENDPOINT}{np.pk}/communities/{community.pk}/hosts/')
self.assertEqual(len(ret.json()['results']), 0)


def test_delete_host_from_community_not_in_community(self):
"""Test deleting a host from a community."""
np, community, _, host, _ = self.create_policy_setup()
self.assert_delete_and_404(f'{POLICY_ENDPOINT}{np.pk}/communities/{community.pk}/hosts/{host.pk}')

# Add to a different community, just to make sure
community_other = self._create_community("community_other", "community desc", np)
host.set_community(community_other)

self.assert_delete_and_404(f'{POLICY_ENDPOINT}{np.pk}/communities/{community.pk}/hosts/{host.pk}')

community_other.delete()

15 changes: 12 additions & 3 deletions mreg/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,19 @@
re_path(r'^zonefiles/(?P<name>(\d+/)?[^/]+)', views_zones.zone_file_detail),
path('permissions/netgroupregex/', views.NetGroupRegexPermissionList.as_view()),
path('permissions/netgroupregex/<pk>', views.NetGroupRegexPermissionDetail.as_view()),

# Network Policy-related endpoints
path("networkpolicies/", views_network_policy.NetworkPolicyList.as_view(), name='networkpolicy-list'),
path("networkpolicies/<int:pk>", views_network_policy.NetworkPolicyDetail.as_view(), name='networkpolicy-detail'),
path("networkpolicies/<int:pk>/communities/", views_network_policy.NetworkCommunityList.as_view(), name='networkpolicy-communities-list'),
path("networkpolicies/<int:pk>/communities/<int:cpk>", views_network_policy.NetworkCommunityDetail.as_view(), name='networkpolicy-community-detail'),
path("networkpolicyattributes/", views_network_policy.NetworkPolicyAttributeList.as_view(), name='networkpolicyattribute-list'),
path("networkpolicies/<int:pk>/communities/",
views_network_policy.NetworkCommunityList.as_view(), name='networkpolicy-communities-list'),
path("networkpolicies/<int:pk>/communities/<int:cpk>",
views_network_policy.NetworkCommunityDetail.as_view(), name='networkpolicy-community-detail'),
path("networkpolicies/<int:pk>/communities/<int:cpk>/hosts/",
views_network_policy.NetworkCommunityHostList.as_view(), name='networkpolicy-community-hosts-list'),
path("networkpolicies/<int:pk>/communities/<int:cpk>/hosts/<int:hostpk>",
views_network_policy.NetworkCommunityHostDetail.as_view(), name='networkpolicy-community-host-detail'),
path("networkpolicyattributes/",
views_network_policy.NetworkPolicyAttributeList.as_view(), name='networkpolicyattribute-list'),

]
73 changes: 71 additions & 2 deletions mreg/api/v1/views_network_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

from rest_framework import generics, exceptions, status, response
from mreg.models.network_policy import NetworkPolicy, NetworkPolicyAttribute, Community
from mreg.models.host import Host
from mreg.api.v1.serializers import (
NetworkPolicySerializer,
NetworkPolicyAttributeSerializer,
CommunitySerializer
CommunitySerializer,
HostSerializer,
)
from mreg.api.v1.views import JSONContentTypeMixin
from mreg.api.permissions import IsGrantedNetGroupRegexPermission, IsSuperOrNetworkAdminMember
Expand Down Expand Up @@ -80,7 +82,7 @@ def perform_create(self, serializer):
raise exceptions.NotFound("NetworkPolicy not found.")
serializer.save(policy=policy)

# Retrieve, update, or delete a specific Community under a specific NetworkPolicy
# Retrieve, update, or delete a specific Community under a specific Network
class NetworkCommunityDetail(JSONContentTypeMixin, generics.RetrieveUpdateDestroyAPIView):
serializer_class = CommunitySerializer
permission_classes = (IsGrantedNetGroupRegexPermission,)
Expand All @@ -94,3 +96,70 @@ def get_object(self):
cpk = self.kwargs.get('cpk')
obj = generics.get_object_or_404(queryset, pk=cpk)
return obj

class HostInCommunityMixin(JSONContentTypeMixin):
def get_policy_and_community(self):
policy_pk = self.kwargs.get('pk') # type: ignore
cpk = self.kwargs.get('cpk') # type: ignore

try:
policy = NetworkPolicy.objects.get(pk=policy_pk)
except NetworkPolicy.DoesNotExist:
raise exceptions.NotFound("NetworkPolicy not found.")

try:
community = Community.objects.get(pk=cpk)
except Community.DoesNotExist:
raise exceptions.NotFound("Community not found.")

if community.policy != policy:
raise exceptions.NotFound("Community does not belong to the requested policy.")

return policy, community

# List all hosts in a specific community, or add a host to a community
class NetworkCommunityHostList(HostInCommunityMixin, generics.ListCreateAPIView):
serializer_class = HostSerializer
permission_classes = (IsSuperOrNetworkAdminMember,)

def get_queryset(self):
# Retrieve community via helper. The policy is not used directly here.
_, community = self.get_policy_and_community()
return Host.objects.filter(network_community=community)

def create(self, request, *args, **kwargs):
_, community = self.get_policy_and_community()
host_id = request.data.get('id')

# Ensure host exists. If not, an appropriate 404 is raised.
host = generics.get_object_or_404(Host, pk=host_id)

# Attempt to set the community (this method will validate if the host has an IP in a network
# that is associated with the policy in which this community is defined).
if not host.set_community(community):
# If set_community returns False, then either the host has no IP address that matches
# any network in the community's policy, or there was another problem.
raise exceptions.ValidationError("Host cannot be associated with the specified community (IP mismatch?)")

return response.Response(HostSerializer(host).data, status=status.HTTP_201_CREATED)

# Retrieve or delete a specific host in a specific community
class NetworkCommunityHostDetail(HostInCommunityMixin, generics.RetrieveDestroyAPIView):
serializer_class = HostSerializer
permission_classes = (IsSuperOrNetworkAdminMember,)

def get_queryset(self):
_, community = self.get_policy_and_community()
return Host.objects.filter(network_community=community)

def get_object(self):
queryset = self.get_queryset()
host_id = self.kwargs.get('hostpk')
obj = generics.get_object_or_404(queryset, pk=host_id)
return obj

def delete(self, request, *args, **kwargs):
host = self.get_object()
host.network_community = None
host.save()
return response.Response(status=status.HTTP_204_NO_CONTENT)
2 changes: 1 addition & 1 deletion mreg/models/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def set_community(self, community: Community) -> bool:
self.network_community = community
self.save()
return True
except Network.DoesNotExist as e:
except Network.DoesNotExist:
return False

return False
Expand Down

1 comment on commit 1661db9

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.