mirror of
https://github.com/opnsense/core.git
synced 2026-05-28 04:34:51 -04:00
Services: Kea DHCPv4/6: Build reservation status from control socket output, so it matches the scope of individual subnet (#10276)
* Services: Kea DHCPv4/6: Build reservation status from control socket output, so it matches the scope of individual subnets as well. Add client-id since it's relevant for IPv4 leases as well in default configuration. We return an array now, change frontend detection if it's dynamic or static lease Missed a closing bracket Typo in client_id Remove unused imports in LeasesController Add comment to build_reserved_matches() to explain why the subnet-id logic exists now * Add state as well, helpful for troubleshooting * Add a state formatter to convert number status into their documented meaning * Some data-width micro management
This commit is contained in:
parent
48da1ce7b9
commit
9b93f84c24
4 changed files with 94 additions and 47 deletions
|
|
@ -31,8 +31,6 @@ namespace OPNsense\Kea\Api;
|
|||
use OPNsense\Base\ApiControllerBase;
|
||||
use OPNsense\Core\Backend;
|
||||
use OPNsense\Core\Config;
|
||||
use OPNsense\Kea\KeaDhcpv4;
|
||||
use OPNsense\Kea\KeaDhcpv6;
|
||||
use OPNsense\Base\UserException;
|
||||
|
||||
abstract class LeasesController extends ApiControllerBase
|
||||
|
|
@ -50,7 +48,6 @@ abstract class LeasesController extends ApiControllerBase
|
|||
$interfaces = [];
|
||||
|
||||
$leases = json_decode($backend->configdpRun($this->configd_fetch_leases), true) ?? [];
|
||||
$ifconfig = json_decode($backend->configdRun('interface list ifconfig'), true);
|
||||
$mac_db = json_decode($backend->configdRun('interface list macdb'), true) ?? [];
|
||||
|
||||
$ifmap = [];
|
||||
|
|
@ -61,23 +58,6 @@ abstract class LeasesController extends ApiControllerBase
|
|||
];
|
||||
}
|
||||
|
||||
// Mark records as reserved based on hwaddr (IPv4) or duid/hwaddr (IPv6) match
|
||||
$resv4 = [];
|
||||
$resv6 = [];
|
||||
|
||||
foreach ((new KeaDhcpv4())->reservations->reservation->iterateItems() as $reservation) {
|
||||
$resv4[strtolower($reservation->hw_address->getValue())] = 'hwaddr';
|
||||
}
|
||||
|
||||
foreach ((new KeaDhcpv6())->reservations->reservation->iterateItems() as $reservation) {
|
||||
// At least one of these is required in the model
|
||||
if (!$reservation->duid->isEmpty()) {
|
||||
$resv6[strtolower($reservation->duid->getValue())] = 'duid';
|
||||
} elseif (!$reservation->hw_address->isEmpty()) {
|
||||
$resv6[strtolower($reservation->hw_address->getValue())] = 'hwaddr';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($leases) && isset($leases['records'])) {
|
||||
$records = $leases['records'];
|
||||
foreach ($records as &$record) {
|
||||
|
|
@ -92,23 +72,6 @@ abstract class LeasesController extends ApiControllerBase
|
|||
// Vendor
|
||||
$mac = strtoupper(substr(str_replace(':', '', $record['hwaddr']), 0, 6));
|
||||
$record['mac_info'] = isset($mac_db[$mac]) ? $mac_db[$mac] : '';
|
||||
// Reservation
|
||||
$record['is_reserved'] = '';
|
||||
$addr = $record['address'] ?? '';
|
||||
if (strpos($addr, ':') !== false) {
|
||||
$duid = strtolower($record['duid'] ?? '');
|
||||
$mac = strtolower($record['hwaddr'] ?? '');
|
||||
if (isset($resv6[$duid])) {
|
||||
$record['is_reserved'] = $resv6[$duid];
|
||||
} elseif (isset($resv6[$mac])) {
|
||||
$record['is_reserved'] = $resv6[$mac];
|
||||
}
|
||||
} else {
|
||||
$mac = strtolower($record['hwaddr'] ?? '');
|
||||
if (isset($resv4[$mac])) {
|
||||
$record['is_reserved'] = $resv4[$mac];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$records = [];
|
||||
|
|
|
|||
|
|
@ -87,10 +87,20 @@
|
|||
return moment.unix(row[column.id]).local().format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
"reservation": function (column, row) {
|
||||
return row.is_reserved !== ''
|
||||
return row.is_reserved.length > 0
|
||||
? "{{ lang._('static') }}"
|
||||
: "{{ lang._('dynamic') }}";
|
||||
},
|
||||
"state": function (column, row) {
|
||||
const states = {
|
||||
0: "{{ lang._('assigned') }}",
|
||||
1: "{{ lang._('declined') }}",
|
||||
2: "{{ lang._('expired reclaimed') }}",
|
||||
3: "{{ lang._('released') }}",
|
||||
4: "{{ lang._('registered') }}"
|
||||
};
|
||||
return states[row.state] || row.state;
|
||||
},
|
||||
"commands": function (column, row) {
|
||||
const baseUrl = `/ui/kea/dhcp/v4#reservations`;
|
||||
const searchUrl = `${baseUrl}&search=${encodeURIComponent(row.hwaddr || '')}`;
|
||||
|
|
@ -103,7 +113,7 @@
|
|||
|
||||
let reservationBtn;
|
||||
|
||||
if (row.is_reserved !== '') {
|
||||
if (row.is_reserved.length > 0) {
|
||||
reservationBtn = $(`
|
||||
<button type="button" class="btn btn-xs" data-toggle="tooltip"
|
||||
title="{{ lang._('Find Reservation') }}">
|
||||
|
|
@ -163,9 +173,11 @@
|
|||
<th data-column-id="if_descr" data-type="string">{{ lang._('Interface') }}</th>
|
||||
<th data-column-id="address" data-identifier="true" data-type="string" data-formatter="overflowformatter">{{ lang._('IP Address') }}</th>
|
||||
<th data-column-id="hwaddr" data-type="string" data-formatter="macformatter" data-width="9em">{{ lang._('MAC Address') }}</th>
|
||||
<th data-column-id="valid_lifetime" data-type="integer">{{ lang._('Lifetime') }}</th>
|
||||
<th data-column-id="client_id" data-type="string" data-width="9em">{{ lang._('Client ID') }}</th>
|
||||
<th data-column-id="valid_lifetime" data-width="6em" data-type="integer">{{ lang._('Lifetime') }}</th>
|
||||
<th data-column-id="expire" data-type="string" data-formatter="timestamp">{{ lang._('Expire') }}</th>
|
||||
<th data-column-id="hostname" data-type="string" data-formatter="overflowformatter">{{ lang._('Hostname') }}</th>
|
||||
<th data-column-id="state" data-type="string" data-formatter="state" data-width="8em">{{ lang._('State') }}</th>
|
||||
<th data-column-id="is_reserved" data-type="string" data-formatter="reservation" data-width="6em">{{ lang._('Lease Type') }}</th>
|
||||
<th data-column-id="commands" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -87,17 +87,27 @@
|
|||
return moment.unix(row[column.id]).local().format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
"reservation": function (column, row) {
|
||||
return row.is_reserved !== ''
|
||||
return row.is_reserved.length > 0
|
||||
? "{{ lang._('static') }}"
|
||||
: "{{ lang._('dynamic') }}";
|
||||
},
|
||||
"state": function (column, row) {
|
||||
const states = {
|
||||
0: "{{ lang._('assigned') }}",
|
||||
1: "{{ lang._('declined') }}",
|
||||
2: "{{ lang._('expired reclaimed') }}",
|
||||
3: "{{ lang._('released') }}",
|
||||
4: "{{ lang._('registered') }}"
|
||||
};
|
||||
return states[row.state] || row.state;
|
||||
},
|
||||
"commands": function (column, row) {
|
||||
const baseUrl = `/ui/kea/dhcp/v6#reservations`;
|
||||
let searchValue = '';
|
||||
|
||||
if (row.is_reserved === 'duid') {
|
||||
if (row.is_reserved.includes('duid')) {
|
||||
searchValue = row.duid || '';
|
||||
} else if (row.is_reserved === 'hwaddr') {
|
||||
} else if (row.is_reserved.includes('hwaddr')) {
|
||||
searchValue = row.hwaddr || '';
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +127,7 @@
|
|||
|
||||
let reservationBtn;
|
||||
|
||||
if (row.is_reserved !== '') {
|
||||
if (row.is_reserved.length > 0) {
|
||||
reservationBtn = $(`
|
||||
<button type="button" class="btn btn-xs" data-toggle="tooltip"
|
||||
title="{{ lang._('Find Reservation') }}">
|
||||
|
|
@ -181,9 +191,10 @@
|
|||
<th data-column-id="duid" data-type="string" data-width="18em">{{ lang._('DUID') }}</th>
|
||||
<th data-column-id="iaid" data-type="string" data-width="4em">{{ lang._('IAID') }}</th>
|
||||
<th data-column-id="hwaddr" data-type="string" data-formatter="macformatter" data-width="9em">{{ lang._('MAC Address') }}</th>
|
||||
<th data-column-id="valid_lifetime" data-type="integer">{{ lang._('Lifetime') }}</th>
|
||||
<th data-column-id="valid_lifetime" data-width="6em" data-type="integer">{{ lang._('Lifetime') }}</th>
|
||||
<th data-column-id="expire" data-type="string" data-formatter="timestamp">{{ lang._('Expire') }}</th>
|
||||
<th data-column-id="hostname" data-type="string" data-formatter="overflowformatter">{{ lang._('Hostname') }}</th>
|
||||
<th data-column-id="state" data-type="string" data-formatter="state" data-width="8em">{{ lang._('State') }}</th>
|
||||
<th data-column-id="is_reserved" data-type="string" data-formatter="reservation" data-width="6em">{{ lang._('Lease Type') }}</th>
|
||||
<th data-column-id="commands" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,58 @@ def build_ranges(proto):
|
|||
|
||||
return ranges
|
||||
|
||||
def get_reservation_keys(record, proto):
|
||||
keys = []
|
||||
subnet_id = record.get('subnet-id')
|
||||
|
||||
if subnet_id is not None:
|
||||
hwaddr = record.get('hw-address', '').lower()
|
||||
duid = record.get('duid', '').lower()
|
||||
client_id = record.get('client-id', '').lower()
|
||||
|
||||
if hwaddr:
|
||||
keys.append((subnet_id, 'hwaddr', hwaddr))
|
||||
if proto == 'inet6' and duid:
|
||||
keys.append((subnet_id, 'duid', duid))
|
||||
if proto == 'inet' and client_id:
|
||||
keys.append((subnet_id, 'client_id', client_id))
|
||||
|
||||
return keys
|
||||
|
||||
def build_reserved_matches(config, leases, proto, dhcp_key, config_key):
|
||||
"""
|
||||
Kea does not expose whether a lease came from a reservation, so we infer it
|
||||
by comparing client identity within the same subnet-id. The subnet-id check
|
||||
matters because the same MAC, DUID or client-id may have reservations on
|
||||
one subnet, but due to client roaming currently exist on a different subnet as lease.
|
||||
They should not be marked as reserved when they are not in the expected subnet-id scope.
|
||||
"""
|
||||
wanted = set()
|
||||
matches = {}
|
||||
|
||||
for lease in leases:
|
||||
wanted.update(get_reservation_keys(lease, proto))
|
||||
|
||||
if wanted:
|
||||
for subnet in config.get('arguments', {}).get(dhcp_key, {}).get(config_key, []):
|
||||
subnet_id = subnet.get('id')
|
||||
if subnet_id is None:
|
||||
continue
|
||||
|
||||
for reservation in subnet.get('reservations', []):
|
||||
hwaddr = reservation.get('hw-address', '').lower()
|
||||
duid = reservation.get('duid', '').lower()
|
||||
client_id = reservation.get('client-id', '').lower()
|
||||
|
||||
if hwaddr and (subnet_id, 'hwaddr', hwaddr) in wanted:
|
||||
matches[(subnet_id, 'hwaddr', hwaddr)] = 'hwaddr'
|
||||
if proto == 'inet6' and duid and (subnet_id, 'duid', duid) in wanted:
|
||||
matches[(subnet_id, 'duid', duid)] = 'duid'
|
||||
if proto == 'inet' and client_id and (subnet_id, 'client_id', client_id) in wanted:
|
||||
matches[(subnet_id, 'client_id', client_id)] = 'client_id'
|
||||
|
||||
return matches
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--proto', help='protocol to fetch (inet, inet6)', default='inet', choices=['inet', 'inet6'])
|
||||
|
|
@ -69,11 +121,14 @@ if __name__ == '__main__':
|
|||
]
|
||||
|
||||
leases = KeaCtrl.send_command(lease_cmd, {"subnets": subnets}, service)
|
||||
leases = leases.get("arguments", {}).get("leases", [])
|
||||
reserved_matches = build_reserved_matches(config, leases, inputargs.proto, dhcp_key, config_key)
|
||||
|
||||
records = []
|
||||
for lease in leases.get("arguments", {}).get("leases", []):
|
||||
for lease in leases:
|
||||
address = lease.get("ip-address")
|
||||
lease_if = None
|
||||
is_reserved = []
|
||||
|
||||
if address:
|
||||
for net, ifname in ranges.items():
|
||||
|
|
@ -81,19 +136,25 @@ if __name__ == '__main__':
|
|||
lease_if = ifname
|
||||
break
|
||||
|
||||
for key in get_reservation_keys(lease, inputargs.proto):
|
||||
if key in reserved_matches:
|
||||
is_reserved.append(reserved_matches[key])
|
||||
|
||||
records.append({
|
||||
"address": address,
|
||||
"prefix_len": lease.get("prefix-len", 128),
|
||||
"type": lease.get("type", ""),
|
||||
"hwaddr": lease.get("hw-address", ""),
|
||||
"duid": lease.get("duid", ""),
|
||||
"client_id": lease.get("client-id", ""),
|
||||
"iaid": lease.get("iaid", ""),
|
||||
"valid_lifetime": lease.get("valid-lft", 0),
|
||||
"expire": lease.get("cltt", 0) + lease.get("valid-lft", 0),
|
||||
"hostname": lease.get("hostname", ""),
|
||||
"state": lease.get("state", 0),
|
||||
"if": lease_if,
|
||||
"if_descr": "",
|
||||
"is_reserved": ""
|
||||
"is_reserved": is_reserved
|
||||
})
|
||||
|
||||
if records:
|
||||
|
|
|
|||
Loading…
Reference in a new issue