返回文章列表

整合 IP 位址應用程式與 DHCP 伺服器

本文探討如何將 IP 位址應用程式與 DHCP 伺服器整合,實作動態 DHCP 租賃管理和 IP 位址狀態監控。文章涵蓋了 DHCP 設定檔生成、子網路遮罩計算、反向名稱解析、IP 位址狀態檢查以及使用 OMAPI 動態管理 DHCP 租賃等關鍵技術,並提供程式碼範例和組態說明。

網路管理 Web 開發

透過 Python 與 Django 框架,我們可以建立一個網頁應用程式,用於管理 IP 位址並與 DHCP 伺服器整合。文章首先介紹如何從資料函式庫讀取 IP 位址資訊,並生成 DHCP 伺服器可讀取的設定檔。接著,示範如何計運算元網路遮罩、進行反向名稱解析以取得主機名稱,以及使用 ICMP ECHO 請求檢查 IP 位址是否在使用中。最後,文章說明如何利用 JavaScript 和 AJAX 技術非同步更新 IP 位址狀態,並透過 OMAPI 實作動態 DHCP 租賃管理,提升 IP 位址管理效率。

將 IP 位址應用程式與 DHCP 整合

產生 DHCP 設定檔

我們已經擁有所需的所有資訊,但目前的形式並不實用。所有資料都儲存在資料函式庫表格中,雖然它闡述了 DHCP 伺服器的組態方式,但無法直接使用。我們需要編寫一個檢視(view)來產生 DHCP 伺服器能夠理解的設定檔。

讓我們回到 DHCP 設定檔應該是什麼樣子的問題。由於我們使用的是 ISC DHCP 伺服器,設定檔(僅包含我們感興趣的元素)的結構如下:

  • DHCP 設定專案或通用 DHCP 選項
  • 類別定義
  • 網路定義
  • 子網定義
  • 子網選項
  • 地址池定義

讓我們將這個設定檔作為一個網頁資源提供。因此,我們需要以類別似生成使用者介面頁面的方式來處理它:定義一個提供資料的檢視和一個將資料佈局在頁面上的範本——在這個例子中,是一個純文字檔案。

首先是檢視,如清單 4-17 所示。

清單 4-17:收集 DHCP 設定檔資料的檢視

def dhcpd_conf_generate(request):
    class_rules = ClassRule.objects.all()
    networks = []
    for net in DHCPNetwork.objects.all():
        networks.append({'dhcp_net': net,
                         'pools': DHCPAddressPool.objects.filter(dhcp_network=net),
                        })
    return render_to_response('dhcpd.conf.txt',
                              {'class_rules': class_rules,
                               'networks': networks,
                              },
                              mimetype='text/plain')

我們沒有將 DHCP 伺服器設定專案儲存在資料函式庫中,因此將直接把它們放到範本中。類別規則只是簡單地列在其他結構之外,因此我們生成了一個包含系統上所有類別規則的列表,並將其作為列表傳遞。

每個 DHCP 子網可能在其範圍內定義了幾個不同的 DHCP 地址池,因此這些地址池需要出現在特定的 DHCP 網路定義中。因此,我們遍歷所有可用的 DHCP 網路,並生成一個列表,其中包含:

  • DHCP 位址物件
  • 與給定 DHCP 網路相關的所有 DHCP 地址池列表

清單 4-18:DHCP 設定檔的範本

{% autoescape off %}
ignore client-updates;
ddns-update-style interim;

{% if class_rules %}
{% for cr in class_rules %}
# {{ cr.description }}
class "class_rule_{{ cr.id }}" {
    {{ cr.rule }};
}
{% endfor %}
{% endif %}

{% if networks %}
{% for net in networks %}
shared-network network_{{ net.dhcp_net.id }} {
    subnet {{ net.dhcp_net.physical_net.address }} netmask {{ net.dhcp_net.physical_net.get_netmask }} {
        option routers {{ net.dhcp_net.router }};
        option domain-name-servers {{ net.dhcp_net.dns_server.address }};
        option domain-name {{ net.dhcp_net.domain_name.name }};

        {% if net.pools %}
        {% for pool in net.pools %}
        pool {
            allow members of "class_rule_{{ pool.class_rule.id }}";
            range {{ pool.range_start }} {{ pool.range_finish }};
        }
        {% endfor %}
        {% endif %}
    }
}
{% endfor %}
{% endif %}

{% endautoescape %}

程式碼解說:

  1. {% autoescape off %}:關閉 Django 範本引擎的自動轉義功能,以確保特殊字元不會被轉換為 HTML 編碼。
  2. ignore client-updates;ddns-update-style interim;:標準的 DHCP 伺服器組態專案。
  3. {% if class_rules %}...{% endif %}:檢查 class_rules 列表是否為空,如果不為空,則遍歷列表並顯示類別規則。
  4. {% if networks %}...{% endif %}:檢查 networks 列表是否為空,如果不為空,則遍歷列表並顯示網路定義、子網定義和地址池定義。
  5. shared-networksubnetpool:DHCP 設定檔中的關鍵字,用於定義網路、子網和地址池。
  6. {{ net.dhcp_net.physical_net.get_netmask }}:呼叫 PhysicalNetwork 物件的 get_netmask 方法,計運算元網掩碼。

計運算元網掩碼

PhysicalNetwork 物件中,我們定義了一個 get_netmask 方法,用於計運算元網掩碼,如清單 4-19 所示。

清單 4-19:計運算元網掩碼

def get_netmask(self):
    bit_netmask = 0
    bit_netmask = pow(2, self.network_size) - 1
    bit_netmask = bit_netmask << (32 - self.network_size)
    nmask_array = []
    for c in range(4):
        dec = bit_netmask & 255
        bit_netmask = bit_netmask >> 8
        nmask_array.insert(0, str(dec))
    return ".".join(nmask_array)

程式碼解說:

  1. bit_netmask = pow(2, self.network_size) - 1:計運算元網掩碼的二進製表示。
  2. bit_netmask = bit_netmask << (32 - self.network_size):將子網路遮罩左移至正確的位置。
  3. nmask_array = []:初始化一個列表,用於儲存子網路遮罩的四個部分。
  4. for c in range(4):遍歷四個部分,計算每個部分的十進位制值,並將其插入到列表中。
  5. return ".".join(nmask_array):將四個部分連線起來,形成完整的子網路遮罩。

將 IP 位址應用程式與 DHCP 整合

產生子網路遮罩的函式

在整合 IP 位址應用程式與 DHCP 的過程中,我們需要產生子網路遮罩。以下是一個簡單的函式,用於計運算元網路遮罩:

  1. 設定一個整數變數 bits 為所需位元數。
  2. 計算 2^bits - 1,然後左移至正確的位置,填補剩餘位元以 0。
  3. 將結果分割成四個部分,每部分 8 位元,並轉換成十進位數字字串。
  4. 使用點符號將這些數字連線起來,形成子網路遮罩。

以下是一個範例程式碼:

def generate_netmask(bits):
    # 計運算元網路遮罩
    netmask = (0xffffffff >> (32 - bits)) << (32 - bits)
    # 分割成四個部分並轉換成十進位數字字串
    octets = [(netmask >> i) & 0xff for i in (24, 16, 8, 0)]
    return '.'.join(map(str, octets))

內容解密:

  • generate_netmask 函式接收一個引數 bits,代表子網路遮罩的位元數。
  • (0xffffffff >> (32 - bits)) << (32 - bits) 用於計運算元網路遮罩的二進位表示。
  • 使用列表推導式將子網路遮罩分割成四個部分,每部分 8 位元,並轉換成十進位數字。
  • '.'.join(map(str, octets)) 將四個十進位數字連線起來,形成子網路遮罩字串。

新增 URL 模式

為了呼叫產生 DHCP 設定檔的檢視,我們需要新增一個 URL 模式:

url(r'^dhcpd.conf/$', views.dhcpd_conf_generate, name='dhcp-conf-generate')

內容解密:

  • url 函式用於定義 URL 模式。
  • r'^dhcpd.conf/$' 是 URL 模式的正規表示式,匹配 /dhcpd.conf/ 路徑。
  • views.dhcpd_conf_generate 是處理該 URL 的檢視函式。

解析 IP 位址為主機名稱

為了取得更多關於 IP 位址的資訊,我們可以進行反向名稱解析,並在每個位址條目旁列印完整網域名稱。

在 Model 類別中新增方法

我們可以在 NetworkAddress Model 類別中新增一個方法,用於執行反向查詢:

import socket

class NetworkAddress(models.Model):
    # ...

    def get_hostname(self):
        try:
            fqdn = socket.gethostbyaddr(str(self.address))[0]
        except:
            fqdn = ''
        return fqdn

內容解密:

  • get_hostname 方法使用 socket.gethostbyaddr 函式執行反向查詢。
  • 如果查詢成功,傳回完整網域名稱;否則,傳回空字串。

在範本中顯示主機名稱

我們可以在範本中顯示主機名稱(如果可用):

{% for address in addresses_list %}
    <li>
        <a href="{% url networkaddress-display address %}">{{ address.address }}/{{ address.network_size }}</a>
        {% ifequal address.network_size 32 %}(host){% else %}(network){% endifequal %}
        {{ address.description }}
        {% if address.get_hostname %} ({{ address.get_hostname }}) {% endif %}
        (<a href="{% url networkaddress-delete address %}">delete</a> |
        <a href="{% url networkaddress-modify address %}">modify</a>)
    </li>
{% endfor %}

內容解密:

  • 使用 if 陳述式檢查 get_hostname 方法是否傳回非空值。
  • 如果傳回非空值,顯示主機名稱。

檢查 IP 位址是否在使用中

我們可以實作一個簡單的函式,用於檢查 IP 位址是否在使用中。

使用 ICMP ECHO 請求檢查 IP 位址

我們可以使用 ping 命令傳送 ICMP ECHO 請求到 IP 位址,並等待回應:

def responding_to_ping(address, timeout=1):
    import subprocess
    rc = subprocess.call("ping -c 1 -W %d %s" % (timeout, address),
                         shell=True, stdout=open('/dev/null', 'w'),
                         stderr=subprocess.STDOUT)
    if rc == 0:
        return True
    else:
        return False

內容解密:

  • responding_to_ping 函式使用 subprocess.call 函式執行 ping 命令。
  • -c 1 選項指定傳送一個 ECHO 請求封包。
  • -W %d 選項指定逾時時間(秒)。

非同步呼叫 ping URL

為了避免影回應用程式的回應時間,我們可以使用 JavaScript 非同步呼叫 ping URL。

新增 URL 模式

我們需要新增兩個 URL 模式:

url(r'^networkaddress/(?P<address>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/ping/$',
    views.networkaddress_ping, name='networkaddress-ping'),
url(r'^networkaddress/$', views.networkaddress_ping,
    name='networkaddress-ping-url'),

內容解密:

  • 第一個 URL 模式匹配 /networkaddress/<IP 位址>/ping/ 路徑。
  • 第二個 URL 模式用於 housekeeping。

將 IP 位址應用程式與 DHCP 整合

非同步更新 IP 位址狀態

本章節將介紹如何使用 JavaScript 和 AJAX 技術實作 IP 位址狀態的非同步更新。首先,我們需要在 HTML 範本中新增一個 <span> 標籤,該標籤將用於顯示 IP 位址的狀態。

{% for address in addresses_list %}
<li>
    <a href="{% url networkaddress-display address %}">{{ address.address }} / {{ address.network_size }}</a>
    {% ifequal address.network_size 32 %}(host){% else %}(network){% endifequal %}
    {{ address.description }}
    {% if address.get_hostname %} ({{ address.get_hostname }}) {% endif %}
    (<a href="{% url networkaddress-delete address %}">delete</a> | <a href="{% url networkaddress-modify address %}">modify</a>)
    {% ifequal address.network_size 32 %}
    [Status: <span class="address" id="ip_{{ address.get_formated_address }}">Unknown</span> ]
    {% endifequal %}
</li>
{% endfor %}

程式碼解析:

  1. 迴圈遍歷地址列表:使用 Django 範本語言的 for 迴圈遍歷 addresses_list
  2. 顯示地址資訊:顯示每個地址的詳細資訊,包括地址、網路大小、描述和主機名。
  3. 狀態顯示:對於網路大小為 32 的地址,顯示一個 <span> 標籤,該標籤的類別為 address,ID 為 ip_ 後面跟著格式化後的地址。

接下來,我們需要新增 JavaScript 程式碼來實作非同步更新 IP 位址狀態。

$(document).ready(function(){
    $(".address").each(function () {
        var curId = $(this).attr('id');
        updateStatus(curId);
    });
});

function updateStatus(attrId) {
    address = attrId.replace('ip_', '');
    address = address.replace(/_/g, '.');
    $.ajax({
        url: '{% url networkaddress-ping-url %}' + address + '/ping/',
        success: function(response) {
            $('#' + attrId).html(response);
        }
    });
}

程式碼解析:

  1. 檔案準備就緒:當檔案準備就緒時,執行函式遍歷所有類別為 address<span> 標籤。
  2. 更新狀態:對每個 <span> 標籤,呼叫 updateStatus 函式,傳入其 ID。
  3. AJAX 請求:在 updateStatus 函式中,先處理 ID 以獲得正確的 IP 位址,然後傳送 AJAX 請求到伺服器查詢該 IP 位址的狀態。
  4. 更新網頁內容:當收到伺服器的回應後,更新對應 <span> 標籤的內容。

動態 DHCP 租賃管理

為了實作動態 DHCP 租賃管理,我們將使用 OMAPI(Object Management Application Programming Interface)。OMAPI 是 ISC DHCP 伺服器的一個 API 介面,允許我們操縱執行中的服務內部資料結構。

使用 Python 介面操作 OMAPI

我們將使用 pypureomapi 函式庫來存取 ISC DHCP OMAPI 介面。首先,需要安裝 pypureomapi

# pip install pypureomapi

設定 ISC DHCP 伺服器

預設情況下,ISC DHCP 不允許透過 OMAPI 進行管理。要啟用此功能,需要建立額外的組態檔案來定義身份驗證和連線詳細資訊。

首先,生成一個 HMAC-MD5 金鑰,用於連線到 ISC DHCP 伺服器:

# dnssec-keygen -r /dev/urandom -a HMAC-MD5 -b 256 -n USER omapikey

這將產生兩個檔案,分別包含金鑰和元資料資訊。金鑰將用於組態 ISC DHCP 伺服器。

程式碼與組態解析:

  1. 安裝 pypureomapi:使用 pip 安裝 pypureomapi 函式庫。
  2. 生成 HMAC-MD5 金鑰:使用 dnssec-keygen 命令生成金鑰。
  3. 組態 ISC DHCP 伺服器:在 ISC DHCP 伺服器的組態檔案中新增 OMAPI 組態,包括金鑰和連線詳細資訊。

此圖示呈現了動態 DHCP 租賃管理的流程:

@startuml
skinparam backgroundColor #FEFEFE
skinparam sequenceArrowThickness 2

title 整合 IP 位址應用程式與 DHCP 伺服器

actor "客戶端" as client
participant "API Gateway" as gateway
participant "認證服務" as auth
participant "業務服務" as service
database "資料庫" as db
queue "訊息佇列" as mq

client -> gateway : HTTP 請求
gateway -> auth : 驗證 Token
auth --> gateway : 認證結果

alt 認證成功
    gateway -> service : 轉發請求
    service -> db : 查詢/更新資料
    db --> service : 回傳結果
    service -> mq : 發送事件
    service --> gateway : 回應資料
    gateway --> client : HTTP 200 OK
else 認證失敗
    gateway --> client : HTTP 401 Unauthorized
end

@enduml

此圖示解析:

  1. 啟用 OMAPI:在 ISC DHCP 伺服器上啟用 OMAPI 功能。
  2. 生成金鑰:生成用於身份驗證的 HMAC-MD5 金鑰。
  3. 組態伺服器:根據生成的金鑰組態 ISC DHCP 伺服器。
  4. 操作 OMAPI:使用 pypureomapi 函式庫操作 OMAPI 介面。
  5. 動態管理:實作對 DHCP 租賃的動態管理。