Skip to content

Commit

Permalink
Assign automagical hostname and IP
Browse files Browse the repository at this point in the history
  • Loading branch information
hipek8 committed Feb 27, 2025
1 parent b755cdc commit 49a85d3
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 5 deletions.
6 changes: 5 additions & 1 deletion src/ralph/data_center/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
SCMCheckInfo,
SCMStatusCheckInChangeListMixin
)
from ralph.data_center.admin_actions import assign_management_hostname_and_ip
from ralph.data_center.forms import DataCenterAssetForm
from ralph.data_center.models.components import DiskShare, DiskShareMount
from ralph.data_center.models.hosts import DCHost
Expand Down Expand Up @@ -378,7 +379,7 @@ class DataCenterAssetAdmin(
"""Data Center Asset admin class."""

add_form_template = "data_center/datacenterasset/add_form.html"
actions = ["bulk_edit_action", "invoice_report"]
actions = ["bulk_edit_action", "invoice_report", "assign_mgmt_hostname"]

change_views = [
DataCenterAssetComponents,
Expand Down Expand Up @@ -572,6 +573,9 @@ class DataCenterAssetAdmin(
),
)

def assign_mgmt_hostname(self, *args, **kwargs):
return assign_management_hostname_and_ip(self, *args, **kwargs)

def get_export_queryset(self, request):
qs = (
super(RalphAdminImportExportMixin, self)
Expand Down
73 changes: 73 additions & 0 deletions src/ralph/data_center/admin_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import re

from django.utils.translation import ugettext_lazy as _
from typing import Union

from ralph.data_center.models import DataCenterAsset


def assign_management_hostname_and_ip(modeladmin, request, queryset):
for dca in queryset:
try:
if not modeladmin.has_change_permission(request, obj=dca):
raise RuntimeError("insufficient permissions")
if not dca.rack.server_room.data_center.management_hostname_suffix:
raise RuntimeError("dc doesn't have hostname suffix configured")
if not dca.rack.server_room.data_center.management_ip_prefix:
raise RuntimeError("dc doesn't have IP prefix configured")
try:
rack_number_int = int(re.match(r'.*?(\d+).*?', dca.rack.name).groups()[0])
rack_number = '%03d' % rack_number_int # type: str
except:
raise RuntimeError(f"invalid rack name {dca.rack.name}")

hostname = _infer_hostname(dca, rack_number)
if not hostname:
raise RuntimeError("couldn't infer management hostname")

if dca.slot_no: # blade server
dca.management_hostname = hostname
modeladmin.message_user(request, f"Updated management hostname for asset id: {dca.id}", level="INFO")
elif ip := _infer_ip(dca, rack_number): # others (i.e. server rack)
dca.management_ip = ip
dca.management_hostname = hostname
modeladmin.message_user(request, f"Updated management hostname and IP for asset id: {dca.id}", level="INFO")
else:
raise RuntimeError("unknown error")
except Exception as e:
modeladmin.message_user(request, f"Can't update asset id: {dca.id}: {e}", level="ERROR")
return

assign_management_hostname_and_ip.short_description = _("Assign management hostname and IP")

def _infer_hostname(asset: DataCenterAsset, rack_number: str) -> Union[str, None]:
dc = asset.rack.server_room.data_center
hostname_suffix = dc.management_hostname_suffix
asset_position = asset.position
asset_slot = asset.slot_no
if dc and hostname_suffix and asset_position:
if asset_slot is not None:
return f"rack{rack_number}-{asset_position}u-bay{asset_slot}-mgmt.{hostname_suffix}"
else:
return f"rack{rack_number}-{asset_position}u-mgmt.{hostname_suffix}"
else:
return None

def _infer_ip(asset: DataCenterAsset, rack_number: str) -> Union[str, None]:
try:
ip_prefix = asset.rack.server_room.data_center.management_ip_prefix
# invert the numbers to fit into ip 3rd octet e.g. 503 -> X.X.053.X
# this should always be ok because of rack naming conventions
# convert to int to remove zeros at the beginning
rack_ip_part = int(rack_number[1] + rack_number[0] + rack_number[2])
assert int(rack_ip_part) <= 255
except:
raise RuntimeError(f"invalid rack name {asset.rack.name}")

try:
position_ip_part = asset.position + 200 # a magic number
if ip_prefix and rack_ip_part and position_ip_part:
return f"{ip_prefix}.{rack_ip_part}.{position_ip_part}"
except:
raise RuntimeError(f"can't infer management IP address")

23 changes: 23 additions & 0 deletions src/ralph/data_center/migrations/0035_auto_20250225_1404.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.2.28 on 2025-02-25 14:04

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('data_center', '0034_auto_20240628_1207'),
]

operations = [
migrations.AddField(
model_name='datacenter',
name='management_hostname_suffix',
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='management hostname suffix'),
),
migrations.AddField(
model_name='datacenter',
name='management_ip_prefix',
field=models.CharField(blank=True, help_text='First 16 bits e.g. 12.345', max_length=256, null=True, verbose_name='management IP suffix'),
),
]
6 changes: 3 additions & 3 deletions src/ralph/data_center/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class WithManagementIPMixin(object):
* if there is change in address value, existing management ip (IPAddress)
is removed (unless it's reserved IP - then it's detached from current
host) - it's done to not accidentally duplicate existing IPAddress
* then, there is check is IPAddres with new mgmt value already exist
* then, there is check is IPAddress with new mgmt value already exist
* if yes, there is validation if it's not assigned to any other host -
if yes, then ValidationError is raised
* if no, it's attached to current object and marked as management ip
Expand Down Expand Up @@ -80,8 +80,8 @@ def management_ip(self, value):
return

current_mgmt = self.management_ip
# if new management ip value is different than previous, remove previous
# IP entry to not try to change it's value
# if new management ip value is different from previous, remove previous
# IP entry to not try to change its value
if current_mgmt and current_mgmt != value:
del self.management_ip
ip = self._get_or_create_management_ip(value)
Expand Down
9 changes: 9 additions & 0 deletions src/ralph/data_center/models/physical.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ class DataCenter(AdminAbsoluteUrlMixin, NamedMixin, models.Model):
verbose_name=_("shortcut"), max_length=256, blank=True, null=True
)

management_hostname_suffix = models.CharField(
verbose_name=_("management hostname suffix"), max_length=256, blank=True, null=True
)

management_ip_prefix = models.CharField(
verbose_name=_("management IP prefix"), max_length=256, blank=True, null=True,
help_text=_("First 16 bits e.g. 12.345")
)

@property
def rack_set(self):
return Rack.objects.select_related("server_room").filter(
Expand Down
65 changes: 64 additions & 1 deletion src/ralph/data_center/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from unittest import mock

from django.contrib.admin import AdminSite
from django.contrib.auth import get_user_model
from django.contrib.messages.storage.fallback import FallbackStorage
from django.core import mail
from django.db import connection, transaction
from django.test import override_settings, RequestFactory, TransactionTestCase
from django.urls import reverse


from ralph.accounts.tests.factories import UserFactory
from ralph.assets.tests.factories import (
ServiceEnvironmentFactory,
ServiceFactory
)
from ralph.data_center.admin import DataCenterAssetAdmin
from ralph.data_center.models import DataCenterAsset
from ralph.data_center.tests.factories import (
DataCenterAssetFactory,
RackFactory
RackFactory, DataCenterFactory, ServerRoomFactory, DataCenterAssetFullFactory
)
from ralph.lib.custom_fields.models import (
CustomField,
Expand Down Expand Up @@ -193,3 +198,61 @@ def test_if_host_update_is_published_to_hermes_when_dca_is_updated_through_gui(
self.assertEqual(publish_mock.call_count, 1)
# check if on_commit callbacks are removed from current db connections
self.assertEqual(connection.run_on_commit, [])

class DataCenterAssetAdminAssignManagementHostnameTest(TransactionTestCase):
def setUp(self):
self.user = get_user_model().objects.create_superuser(
username="root", password="password", email="[email protected]"
)
result = self.client.login(username="root", password="password")
self.assertEqual(result, True)
self.factory = RequestFactory()

dc = DataCenterFactory(
management_hostname_suffix = "dc1.test",
management_ip_prefix = "12.34"
)
room = ServerRoomFactory(data_center=dc)
rack = RackFactory(name="Rack 123", server_room=room)
self.dca = DataCenterAssetFullFactory(rack=rack, position=18) # type: DataCenterAsset
self.dca.management_hostname = None
self.dca.management_ip = None
self.dca.save()

def build_request(self, dca):
request = self.factory.post(reverse('admin:data_center_datacenterasset_changelist'), {
'action': 'assign_mgmt_hostname',
'_selected_action': [dca.id],
})
request.user = self.user
setattr(request, 'session', 'session')
messages = FallbackStorage(request)
setattr(request, '_messages', messages)
return request

def test_superuser_can_assign_mgmt_hostname_and_ip(self):
admin = DataCenterAssetAdmin(DataCenterAsset, admin_site=AdminSite())
request = self.build_request(self.dca)
admin.assign_mgmt_hostname(request, DataCenterAsset.objects.filter(pk=self.dca.id))
self.assertEqual(self.dca.management_hostname, 'rack123-18u-mgmt.dc1.test')
self.assertEqual(self.dca.management_ip, '12.34.213.218')

def test_superuser_can_assign_mgmt_hostname_for_server_blade(self):
admin = DataCenterAssetAdmin(DataCenterAsset, admin_site=AdminSite())
self.dca.slot_no = 33
# we need to have IP first before setting hostname, this can be whatever
self.dca.management_ip = '10.15.20.25'
self.dca.save()
request = self.build_request(self.dca)
admin.assign_mgmt_hostname(request, DataCenterAsset.objects.filter(pk=self.dca.id))
self.assertEqual(self.dca.management_hostname, 'rack123-18u-bay33-mgmt.dc1.test')
self.assertEqual(self.dca.management_ip, '10.15.20.25')

def test_cant_assign_mgmt_hostname_for_server_blade_if_no_ip(self):
admin = DataCenterAssetAdmin(DataCenterAsset, admin_site=AdminSite())
self.dca.slot_no = 33
self.dca.save()
request = self.build_request(self.dca)
admin.assign_mgmt_hostname(request, DataCenterAsset.objects.filter(pk=self.dca.id))
self.assertIsNone(self.dca.management_hostname)
self.assertIsNone(self.dca.management_ip)

0 comments on commit 49a85d3

Please sign in to comment.