2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-05-02 13:07:46 +00:00

Add Kea DHCP backend

This commit is contained in:
Vinícius Ferrão
2026-04-23 02:01:33 -03:00
parent 40a7e4c43d
commit 6f3d9bb9d1
35 changed files with 3708 additions and 117 deletions

View File

@@ -1871,7 +1871,7 @@ Initialize the dns service. ::
[URI:/services/dhcp] - The dhcp service resource.
-------------------------------------------------
POST - Create the dhcpd.conf for all the networks which are defined in the xCAT Management Node.
POST - Create the DHCP server configuration for all the networks which are defined in the xCAT Management Node.
````````````````````````````````````````````````````````````````````````````````````````````````
Refer to the man page: :doc:`makedhcp </guides/admin-guides/references/man8/makedhcp.8>`
@@ -1882,7 +1882,7 @@ Refer to the man page: :doc:`makedhcp </guides/admin-guides/references/man8/make
**Example:**
Create the dhcpd.conf and restart the dhcpd. ::
Create the DHCP configuration and restart the selected DHCP service. ::
curl -X POST -k 'https://127.0.0.1/xcatws/services/dhcp?userName=root&userPW=cluster&pretty=1'
@@ -2247,4 +2247,3 @@ Get attributes mgtifname and tftpserver from networks table for each row where n
}
]
}

View File

@@ -7,4 +7,5 @@ Code Development
code_standard.rst
builds.rst
debug.rst
kea_dhcp_backend_plan.rst
tips.rst

View File

@@ -0,0 +1,352 @@
Kea DHCP Backend Plan
=====================
Purpose
-------
xCAT currently integrates DHCP through ISC DHCP. That behavior should remain the
default on platforms where ISC DHCP is still available and supported. Kea DHCP
will be added as a second backend for platforms that need it, starting with EL10
and Ubuntu 24.04.
The public xCAT contract remains ``makedhcp``. The implementation underneath
``makedhcp`` will select an ISC or Kea backend based on site configuration and
platform support.
Branch Status
-------------
The ``kea-dhcp-backend`` work implements the Kea backend foundation:
* backend selection through ``site.dhcpbackend``
* ISC as the preserved default on platforms that still support it
* Kea as the automatic default for EL10 and Ubuntu 24.04+
* Kea DHCPv4 and DHCPv6 JSON rendering with Perl's ``JSON`` module
* Kea DHCPv4, DHCPv6, Control Agent, and DHCP-DDNS configuration validation
before install
* backend-aware service mapping for ISC and Kea services
* Kea host reservations through JSON render, validate, backup, and
restart
* optional Control Agent socket, host-commands hook configuration, and live
reservation add/delete through ``reservation-add`` and ``reservation-del``
when ``site.keacontrolagent`` is enabled and the hook library exists
* Kea D2/DHCP-DDNS config generation using the existing ``xcat_key`` material
created by ``makedns``/``ddns``
* shared dynamic range parsing for ISC and Kea output
* centralized Kea boot client classes for BIOS, x86_64 UEFI, ARM64, xNBA, and
IA64
* updates for ``dhcpop``, probes, service monitoring, packaging, man pages, and
site table documentation
* unit tests for backend selection, range parsing, Kea rendering, boot policy,
Kea config validation, and an opt-in live Control Agent smoke test
Remaining work is validation and hardening:
* semantic parity tests against production xCAT tables
* full PXE boot validation on real hardware or nested guests for every
supported architecture
* complete service-node and disjoint-DHCP scenario validation
* CI integration for EL10 and Ubuntu 24.04 containers
Backend Selection
-----------------
Add a site attribute:
``site.dhcpbackend=auto|isc|kea``
Selection rules:
* ``auto`` keeps ISC DHCP on existing supported platforms such as EL8, EL9,
older Ubuntu/Debian releases, and SLES.
* ``auto`` selects Kea DHCP on EL10 and Ubuntu 24.04.
* ``isc`` forces the ISC backend.
* ``kea`` forces the Kea backend.
* A forced backend that is unavailable must fail with a clear error.
This avoids replacing ISC globally while still allowing Kea testing on platforms
where both implementations can be installed.
Architecture
------------
Refactor ``xCAT-server/lib/xcat/plugins/dhcp.pm`` into shared orchestration plus
backend-specific modules.
Suggested modules:
* ``xCAT::DHCP::Backend::ISC``
* ``xCAT::DHCP::Backend::Kea``
* ``xCAT::DHCP::Intent``
* ``xCAT::DHCP::BootPolicy``
``dhcp.pm`` should continue to own:
* ``makedhcp`` option parsing
* service node eligibility checks
* xCAT table reads
* common validation
* lock handling
* callback and error formatting
Shared code should build normalized DHCP intent:
* DHCP interfaces
* subnets
* address pools
* host reservations
* DHCP options
* boot rules
* DDNS intent
Backends render and apply that intent using provider-specific mechanisms.
ISC Backend
-----------
The ISC backend preserves current behavior:
* ``dhcpd.conf`` and ``dhcpd6.conf`` generation
* OMAPI and ``omshell`` host operations
* ``dhcpd`` and optional ``dhcpd6`` service handling
* existing older distribution behavior
The first implementation step should extract the ISC backend with minimal
behavior change and add regression tests before Kea code is introduced.
Kea Static Configuration
------------------------
The Kea backend should generate:
* ``/etc/kea/kea-dhcp4.conf``
* ``/etc/kea/kea-dhcp6.conf`` when IPv6 is configured
* ``/etc/kea/kea-ctrl-agent.conf`` only when REST/control-agent operations are
enabled
* ``/etc/kea/kea-dhcp-ddns.conf`` only when Kea DDNS/D2 support is enabled
Use Kea ``memfile`` leases initially for parity with ISC lease files. Database
lease backends can be considered later if there is a concrete requirement.
Configuration Validation
------------------------
Generated configuration must be validated before any reload or restart.
ISC validation:
``dhcpd -t -cf <config>``
Kea validation:
``kea-dhcp4 -t <config>``
``kea-dhcp6 -t <config>``
Invalid configuration must leave the running service untouched and return a
clear error.
Service Management
------------------
Service control must be backend-aware. Do not add Kea service names blindly to
the generic ``dhcp`` service map.
ISC services:
* ``dhcpd``
* optional ``dhcpd6``
Kea services:
* ``kea-dhcp4``
* optional ``kea-dhcp6``
* optional ``kea-ctrl-agent``
* optional ``kea-dhcp-ddns``
Control Agent must be running before REST operations are attempted. D2 should
only be managed when Kea DDNS support is configured.
Boot Policy
-----------
Boot policy is the riskiest migration area. Existing ISC code uses nested
conditionals and provider-specific statements. Kea uses client classes, test
expressions, and JSON option data.
Do not translate ISC strings directly to Kea strings. Instead, represent boot
behavior once as normalized rules, then render them per backend.
Backends render the same intent as:
* ISC: ``if option ...`` blocks, ``filename``, ``next-server``, and custom
option statements.
* Kea: ``client-classes``, test expressions, ``boot-file-name``,
``next-server``, and ``option-data``.
Boot coverage must include:
* x86 BIOS
* x86_64 UEFI
* ARM64
* OpenPOWER/OPAL
* ONIE
* Cumulus ZTP
* petitboot
* xNBA
* iSCSI boot options
Host Reservations
-----------------
Baseline Kea behavior should be deterministic and not depend on optional hooks:
* render xCAT-owned host reservations into Kea JSON
* validate generated configuration
* reload Kea
Optimized behavior can use Kea Control Agent plus host-commands when available.
This requires verifying that the target distribution packages include the host
commands hook library, such as ``libdhcp_host_cmds.so``. Do not assume this
library is present in EL10 or Ubuntu 24.04 without testing the actual packages.
If host-commands are unavailable, the JSON render and reload path must still
work.
DDNS and D2
-----------
ISC inline DDNS configuration does not map directly to Kea. Kea uses the
separate DHCP-DDNS daemon.
Kea DDNS support uses D2 and should stay separate from the DHCP server config:
* generate ``kea-dhcp-ddns.conf``
* use the existing ``xcat_key`` material from ``/etc/xcat/ddns.key`` or the
``passwd`` table
* render the DHCP server's D2 connection block separately from the global DDNS
behavior flags
* start D2 before the DHCP service when DDNS is enabled
Basic Kea DHCP and PXE support should not depend on DDNS unless a target
deployment explicitly enables ``site.dnshandler=ddns``.
Packaging
---------
Packaging must keep ISC dependencies for platforms using ISC and add Kea
dependencies only for platforms using Kea.
Known areas:
* ``xCAT.spec`` and ``xCATsn.spec`` currently depend on ``/usr/sbin/dhcpd``.
* EL10 and Ubuntu 24.04 packaging should depend on the correct Kea server
packages.
* ``dhclient`` and ``dhcp-client`` are separate client-side genesis/netboot
issues and should not be conflated with the server backend.
Tools, Probes, UI, and Docs
---------------------------
Areas that currently assume ISC DHCP must become backend-aware:
* ``xCAT-server/share/xcat/tools/dhcpop``
* DHCP monitoring in xCAT RMC resources
* ``xCAT-probe`` checks for ``dhcpd``, ``dhcpd.conf``, and ``dhcpd.leases``
* UI paths that run ``service dhcpd restart``
* administrator and developer documentation
Testing Strategy
----------------
Unit tests:
* normalized DHCP intent creation
* ISC renderer regression coverage
* Kea JSON renderer coverage
* host reservation formatting
* subnet and pool mapping
* backend selection and override behavior
Configuration validation tests:
* ``dhcpd -t`` for ISC output
* ``kea-dhcp4 -t`` and ``kea-dhcp6 -t`` for Kea DHCP output
* ``kea-dhcp-ddns -t`` for D2 output
* ``kea-ctrl-agent -t`` for Control Agent output
Backend selection tests:
* ``auto`` uses ISC on EL9
* ``auto`` uses Kea on EL10
* ``auto`` uses Kea on Ubuntu 24.04
* forced ``kea`` works on EL9 when Kea packages are installed
* forced unavailable backend fails clearly
Integration matrix:
* EL9 plus ISC
* EL9 plus forced Kea
* EL10 plus Kea
* Ubuntu 24.04 plus Kea
Semantic parity tests:
* compare normalized DHCP intent, not raw ISC and Kea configuration text
* verify subnets, pools, reservations, routers, DNS, NTP, log servers, lease
times, client classes, and boot rules
Functional smoke tests:
* ``makedhcp -n`` generates valid configuration
* backend services start successfully
* ``makedhcp <node>`` adds a reservation
* ``makedhcp -d <node>`` removes a reservation
* ``makedhcp -q <node>`` returns expected data
* ``XCAT_KEA_LIVE_SMOKE=1`` validates live Control Agent host-commands when
Kea and the host-commands hook are installed
* DHCP offers contain expected boot options
* real PXE boot behavior is validated for each supported architecture
Test Infrastructure
-------------------
Existing container-based EL8, EL9, and EL10 tests should be extended for backend
coverage. The libvirt/KVM infrastructure on ``rome01.local.versatushpc.com.br``
can be used for network and PXE smoke tests that are difficult to validate in
ordinary containers.
Open test infrastructure details to confirm:
* SSH access method and user for ``rome01.local.versatushpc.com.br``
* available base images for EL9, EL10, and Ubuntu 24.04
* libvirt network names and whether isolated DHCP test networks are already
available
* whether nested or privileged test guests can run DHCP client and PXE tests
* cleanup expectations for temporary VMs, networks, and storage volumes
Implementation Order
--------------------
1. Add backend selection model and interface.
2. Extract ISC backend with minimal behavior change.
3. Add ISC regression tests.
4. Add normalized DHCP intent and boot policy structures.
5. Add Kea static JSON renderer.
6. Add config validation.
7. Add backend-aware service handling.
8. Add Kea boot class rendering.
9. Add baseline Kea reservation render and reload path.
10. Verify host-commands packaging and add Control Agent optimization if
available.
11. Add DDNS/D2 support as a separate phase.
12. Update packaging.
13. Update tools, probes, UI, and documentation.
14. Expand CI and KVM smoke tests.
Guiding Rule
------------
``makedhcp`` remains the stable xCAT interface. ISC remains the default backend
where it works. Kea is added as a backend for platforms that need it, with shared
DHCP intent and backend-specific rendering and control.

View File

@@ -41,6 +41,8 @@ DESCRIPTION
The \ **makedhcp**\ command creates and updates the DHCP configuration on the management node and service nodes.
The \ **makedhcp**\ command is supported for both Linux and AIX clusters.
On Linux, the DHCP implementation is selected by the ``site.dhcpbackend`` attribute.
The ``auto`` setting keeps ISC DHCP on platforms where it is still available and uses Kea DHCP on platforms such as EL10 and Ubuntu 24.04.
1.
@@ -65,8 +67,8 @@ The \ **makedhcp**\ command is supported for both Linux and AIX clusters.
4.
Then run \ **makedhcp**\ with a noderange or the \ **-a**\ option. This will inject into dhcpd configuration data pertinent to the specified nodes.
On linux, the configuration information immediately takes effect without a restart of DHCP.
Then run \ **makedhcp**\ with a noderange or the \ **-a**\ option. This will inject DHCP configuration data pertinent to the specified nodes.
With ISC DHCP, Linux node entries are updated through OMAPI. With Kea DHCP, node reservations are rendered into the Kea JSON configuration, validated, and the Kea service is restarted.
@@ -85,7 +87,8 @@ OPTIONS
(Which networks dhcpd should listen on can be controlled by the dhcpinterfaces attribute in the site(5)|site.5 table.)
The \ **makedhcp**\ command will automatically restart the dhcp daemon after this operation.
This option will replace any existing configuration file (making a backup of it first).
For Linux systems the file will include network entries as well as certain general parameters such as a dynamic range and omapi configuration.
For Linux systems using ISC DHCP, the file will include network entries as well as certain general parameters such as a dynamic range and omapi configuration.
For Linux systems using Kea DHCP, the generated files are ``/etc/kea/kea-dhcp4.conf``, ``/etc/kea/kea-dhcp6.conf`` when IPv6 networks are configured, and ``/etc/kea/kea-dhcp-ddns.conf`` when DDNS is enabled.
For AIX systems the file will include network entries.
On AIX systems, if there are any non-xCAT entries in the existing configuration file they will be preserved and added to the end of the new configuration file.
@@ -108,6 +111,7 @@ OPTIONS
\ **-s**\ \ *statements*\
For the input noderange, the argument will be interpreted like dhcp configuration file text.
This option is only supported by the ISC DHCP backend.
@@ -221,6 +225,12 @@ DHCP configuration files:
[RH] /etc/dhcp/dhcpd.conf
[Kea] /etc/kea/kea-dhcp4.conf
[Kea IPv6] /etc/kea/kea-dhcp6.conf
[Kea DDNS] /etc/kea/kea-dhcp-ddns.conf
********
SEE ALSO
@@ -228,4 +238,3 @@ SEE ALSO
noderange(3)|noderange.3

View File

@@ -0,0 +1,158 @@
package xCAT::DHCP::Backend;
use strict;
use warnings;
my %valid_backend = map { $_ => 1 } qw(auto isc kea);
sub normalize {
my ( $class, $backend ) = @_;
$backend = 'auto' unless defined($backend) && $backend ne '';
$backend =~ s/^\s+|\s+$//g;
$backend = lc($backend);
return $backend if $valid_backend{$backend};
return;
}
sub select {
my ( $class, %args ) = @_;
my $requested = exists $args{requested} ? $args{requested} : $class->_site_backend();
my $normalized = $class->normalize($requested);
unless ($normalized) {
return {
error => "Invalid site.dhcpbackend value '$requested'. Valid values are auto, isc, and kea.",
};
}
my $selected = $normalized eq 'auto' ? $class->default_backend(%args) : $normalized;
if ( $args{check_available} && !$class->available( $selected, %args ) ) {
return {
requested => $normalized,
name => $selected,
error => "The selected DHCP backend '$selected' is not available on this system.",
};
}
return {
requested => $normalized,
name => $selected,
};
}
sub default_backend {
my ( $class, %args ) = @_;
my $platform = exists $args{platform} ? $args{platform} : $class->_osver('platform');
return 'kea' if defined($platform) && $platform =~ /^el10\b/i;
my $os = exists $args{os} ? $args{os} : $class->_osver();
return 'kea' if defined($os) && $os =~ /^(?:rhel|rhels|rocky|alma|centos|ol)10(?:\D|$)/i;
my $os_name = exists $args{os_name} ? $args{os_name} : $class->_osver('os');
my $version = exists $args{version} ? $args{version} : $class->_osver('version');
if ( defined($os_name) && $os_name =~ /^ubuntu$/i && _version_at_least( $version, '24.04' ) ) {
return 'kea';
}
return 'isc';
}
sub available {
my ( $class, $backend, %args ) = @_;
if ( exists $args{available} && ref( $args{available} ) eq 'HASH' && exists $args{available}{$backend} ) {
return $args{available}{$backend} ? 1 : 0;
}
if ( $backend eq 'isc' ) {
return _command_exists('dhcpd');
} elsif ( $backend eq 'kea' ) {
return _command_exists('kea-dhcp4');
}
return 0;
}
sub backend_class {
my ( $class, $backend ) = @_;
return 'xCAT::DHCP::Backend::ISC' if $backend eq 'isc';
return 'xCAT::DHCP::Backend::Kea' if $backend eq 'kea';
return;
}
sub new_backend {
my ( $class, %args ) = @_;
my $selection = $class->select(%args);
return $selection if $selection->{error};
my $backend_class = $class->backend_class( $selection->{name} );
eval "require $backend_class";
if ($@) {
return {
%$selection,
error => "Unable to load DHCP backend '$selection->{name}': $@",
};
}
return $backend_class->new( selection => $selection );
}
sub _site_backend {
eval {
require xCAT::TableUtils;
return xCAT::TableUtils->get_site_attribute('dhcpbackend', 'auto');
} || 'auto';
}
sub _osver {
my ( $class, $type ) = @_;
eval {
require xCAT::Utils;
return defined($type) ? xCAT::Utils->osver($type) : xCAT::Utils->osver();
} || 'unknown';
}
sub _command_exists {
my ($command) = @_;
foreach my $dir ( split /:/, $ENV{PATH} || '' ) {
next unless $dir;
my $path = "$dir/$command";
return 1 if -x $path;
}
foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) {
return 1 if -x $path;
}
return 0;
}
sub _version_at_least {
my ( $version, $minimum ) = @_;
return 0 unless defined($version) && $version =~ /^\d+(?:\.\d+)*/;
my @version_parts = split /\./, $version;
my @minimum_parts = split /\./, $minimum;
my $max = @version_parts > @minimum_parts ? @version_parts : @minimum_parts;
for my $idx ( 0 .. $max - 1 ) {
my $left = $version_parts[$idx] || 0;
my $right = $minimum_parts[$idx] || 0;
return 1 if $left > $right;
return 0 if $left < $right;
}
return 1;
}
1;

View File

@@ -0,0 +1,19 @@
package xCAT::DHCP::Backend::ISC;
use strict;
use warnings;
sub new {
my ( $class, %args ) = @_;
return bless \%args, $class;
}
sub name {
return 'isc';
}
sub implemented {
return 1;
}
1;

View File

@@ -0,0 +1,775 @@
package xCAT::DHCP::Backend::Kea;
use strict;
use warnings;
use JSON;
use File::Basename;
use File::Path qw/make_path/;
use Math::BigInt;
use xCAT::DHCP::Range;
use xCAT::NetworkUtils;
sub new {
my ( $class, %args ) = @_;
return bless \%args, $class;
}
sub name {
return 'kea';
}
sub implemented {
return 1;
}
sub dhcp4_config_file {
my ($self) = @_;
return $self->{dhcp4_config_file} || '/etc/kea/kea-dhcp4.conf';
}
sub dhcp6_config_file {
my ($self) = @_;
return $self->{dhcp6_config_file} || '/etc/kea/kea-dhcp6.conf';
}
sub ctrl_agent_config_file {
my ($self) = @_;
return $self->{ctrl_agent_config_file} || '/etc/kea/kea-ctrl-agent.conf';
}
sub ddns_config_file {
my ($self) = @_;
return $self->{ddns_config_file} || '/etc/kea/kea-dhcp-ddns.conf';
}
sub render_dhcp4_config {
my ( $self, $intent ) = @_;
$intent ||= {};
my %dhcp4 = (
'interfaces-config' => {
interfaces => $intent->{interfaces} || [],
},
'lease-database' => _first_defined( $intent->{'lease-database'}, $intent->{lease_database} ) || {
type => 'memfile',
name => '/var/lib/kea/kea-leases4.csv',
},
'valid-lifetime' => _integer( _first_defined( $intent->{'valid-lifetime'}, $intent->{valid_lifetime}, 43200 ) ),
subnet4 => [ map { $self->_render_subnet4($_) } @{ _first_defined( $intent->{subnet4}, $intent->{subnets}, [] ) } ],
);
$dhcp4{'control-socket'} = $intent->{'control-socket'} if $intent->{'control-socket'};
$dhcp4{'hooks-libraries'} = $intent->{'hooks-libraries'} if $intent->{'hooks-libraries'};
$dhcp4{'option-def'} = $intent->{'option-def'} if $intent->{'option-def'};
$dhcp4{'client-classes'} = $intent->{'client-classes'} if $intent->{'client-classes'};
$dhcp4{'option-data'} = $intent->{'option-data'} if $intent->{'option-data'};
$dhcp4{'dhcp-ddns'} = $intent->{'dhcp-ddns'} if $intent->{'dhcp-ddns'};
foreach my $field (qw/ddns-send-updates ddns-override-no-update ddns-override-client-update ddns-qualifying-suffix ddns-update-on-renew/) {
$dhcp4{$field} = $intent->{$field} if exists $intent->{$field};
}
return JSON->new->canonical->pretty->encode( { Dhcp4 => \%dhcp4 } );
}
sub render_dhcp6_config {
my ( $self, $intent ) = @_;
$intent ||= {};
my %dhcp6 = (
'interfaces-config' => {
interfaces => $intent->{interfaces} || [],
},
'lease-database' => _first_defined( $intent->{'lease-database'}, $intent->{lease_database} ) || {
type => 'memfile',
name => '/var/lib/kea/kea-leases6.csv',
},
'preferred-lifetime' => _integer( _first_defined( $intent->{'preferred-lifetime'}, $intent->{preferred_lifetime}, 43200 ) ),
'valid-lifetime' => _integer( _first_defined( $intent->{'valid-lifetime'}, $intent->{valid_lifetime}, 43200 ) ),
'host-reservation-identifiers' => _first_defined( $intent->{'host-reservation-identifiers'}, [ 'duid', 'hw-address' ] ),
subnet6 => [ map { $self->_render_subnet6($_) } @{ _first_defined( $intent->{subnet6}, $intent->{subnets}, [] ) } ],
);
$dhcp6{'control-socket'} = $intent->{'control-socket'} if $intent->{'control-socket'};
$dhcp6{'hooks-libraries'} = $intent->{'hooks-libraries'} if $intent->{'hooks-libraries'};
$dhcp6{'option-data'} = $intent->{'option-data'} if $intent->{'option-data'};
$dhcp6{'dhcp-ddns'} = $intent->{'dhcp-ddns'} if $intent->{'dhcp-ddns'};
foreach my $field (qw/ddns-send-updates ddns-override-no-update ddns-override-client-update ddns-qualifying-suffix ddns-update-on-renew/) {
$dhcp6{$field} = $intent->{$field} if exists $intent->{$field};
}
return JSON->new->canonical->pretty->encode( { Dhcp6 => \%dhcp6 } );
}
sub render_ddns_config {
my ( $self, $intent ) = @_;
$intent ||= {};
my $forward_domains = $intent->{forward_domains};
$forward_domains = $intent->{'forward-ddns'}{'ddns-domains'}
if !defined($forward_domains) && ref( $intent->{'forward-ddns'} ) eq 'HASH';
$forward_domains ||= [];
my $reverse_domains = $intent->{reverse_domains};
$reverse_domains = $intent->{'reverse-ddns'}{'ddns-domains'}
if !defined($reverse_domains) && ref( $intent->{'reverse-ddns'} ) eq 'HASH';
$reverse_domains ||= [];
my %ddns = (
'ip-address' => $intent->{'ip-address'} || '127.0.0.1',
port => _integer( $intent->{port} || 53001 ),
'dns-server-timeout' => _integer( _first_defined( $intent->{'dns-server-timeout'}, 500 ) ),
'ncr-protocol' => $intent->{'ncr-protocol'} || 'UDP',
'ncr-format' => $intent->{'ncr-format'} || 'JSON',
'tsig-keys' => $intent->{'tsig-keys'} || [],
'forward-ddns' => {
'ddns-domains' => $forward_domains,
},
'reverse-ddns' => {
'ddns-domains' => $reverse_domains,
},
);
$ddns{'control-sockets'} = $intent->{'control-sockets'} if $intent->{'control-sockets'};
return JSON->new->canonical->pretty->encode( { DhcpDdns => \%ddns } );
}
sub render_ctrl_agent_config {
my ( $self, $intent ) = @_;
$intent ||= {};
my %sockets = (
dhcp4 => {
'socket-type' => 'unix',
'socket-name' => $intent->{'dhcp4-socket'} || '/var/run/kea/kea4-ctrl-socket',
},
);
if ( $intent->{dhcp6} || $intent->{'dhcp6-socket'} ) {
$sockets{dhcp6} = {
'socket-type' => 'unix',
'socket-name' => $intent->{'dhcp6-socket'} || '/var/run/kea/kea6-ctrl-socket',
};
}
if ( $intent->{ddns} || $intent->{'ddns-socket'} ) {
$sockets{d2} = {
'socket-type' => 'unix',
'socket-name' => $intent->{'ddns-socket'} || '/var/run/kea/kea-ddns-ctrl-socket',
};
}
my $agent = {
'http-host' => $intent->{'http-host'} || '127.0.0.1',
'http-port' => _integer( $intent->{'http-port'} || 8000 ),
'control-sockets' => \%sockets,
};
$agent->{authentication} = $intent->{authentication} if $intent->{authentication};
my $config = {
'Control-agent' => $agent,
};
return JSON->new->canonical->pretty->encode($config);
}
sub host_cmds_hook_path {
my ( $self, @extra_paths ) = @_;
my @default_paths = exists $self->{host_cmds_hook_paths}
? @{ $self->{host_cmds_hook_paths} || [] }
: (
'/usr/lib64/kea/hooks/libdhcp_host_cmds.so',
'/usr/lib/kea/hooks/libdhcp_host_cmds.so',
'/usr/local/lib/kea/hooks/libdhcp_host_cmds.so',
glob('/usr/lib/*/kea/hooks/libdhcp_host_cmds.so'),
);
foreach my $path (
@extra_paths,
@default_paths,
)
{
return $path if defined($path) && -e $path;
}
return;
}
sub load_dhcp4_config {
my ( $self, $path ) = @_;
$path ||= $self->dhcp4_config_file();
return { Dhcp4 => { subnet4 => [] } } unless -e $path;
open( my $fh, '<', $path ) or return { error => "Unable to read $path: $!" };
local $/;
my $content = <$fh>;
close($fh);
my $json = eval { decode_json($content) };
return { error => "Unable to parse $path as JSON: $@" } if $@;
$json->{Dhcp4}{subnet4} ||= [];
return $json;
}
sub load_dhcp6_config {
my ( $self, $path ) = @_;
$path ||= $self->dhcp6_config_file();
return { Dhcp6 => { subnet6 => [] } } unless -e $path;
open( my $fh, '<', $path ) or return { error => "Unable to read $path: $!" };
local $/;
my $content = <$fh>;
close($fh);
my $json = eval { decode_json($content) };
return { error => "Unable to parse $path as JSON: $@" } if $@;
$json->{Dhcp6}{subnet6} ||= [];
return $json;
}
sub write_dhcp4_config {
my ( $self, $intent, %opts ) = @_;
return $self->write_dhcp4_json( $self->render_dhcp4_config($intent), %opts );
}
sub write_dhcp6_config {
my ( $self, $intent, %opts ) = @_;
return $self->write_dhcp6_json( $self->render_dhcp6_config($intent), %opts );
}
sub write_ddns_config {
my ( $self, $intent, %opts ) = @_;
return $self->write_ddns_json( $self->render_ddns_config($intent), %opts );
}
sub write_dhcp4_json {
my ( $self, $json, %opts ) = @_;
$opts{path} ||= $self->dhcp4_config_file();
$opts{validator} ||= sub { $self->validate_dhcp4_config(@_) };
return $self->_write_json_file( $json, %opts );
}
sub write_dhcp6_json {
my ( $self, $json, %opts ) = @_;
$opts{path} ||= $self->dhcp6_config_file();
$opts{validator} ||= sub { $self->validate_dhcp6_config(@_) };
return $self->_write_json_file( $json, %opts );
}
sub write_ddns_json {
my ( $self, $json, %opts ) = @_;
$opts{path} ||= $self->ddns_config_file();
$opts{validator} ||= sub { $self->validate_ddns_config(@_) };
return $self->_write_json_file( $json, %opts );
}
sub _write_json_file {
my ( $self, $json, %opts ) = @_;
my $path = $opts{path};
my $dir = dirname($path);
make_path($dir) unless -d $dir;
my $tmp = "$path.xcat.$$";
open( my $fh, '>', $tmp ) or return { error => "Unable to write $tmp: $!" };
print $fh $json;
close($fh) or return { error => "Unable to close $tmp: $!" };
my $permissions = _set_config_permissions($tmp);
if ( $permissions->{error} ) {
unlink $tmp;
return $permissions;
}
if ( !$opts{skip_validate} ) {
my $validation = $opts{validator}->($tmp);
if ( $validation->{error} ) {
unlink $tmp;
return $validation;
}
}
my $backup;
if ( $opts{backup_existing} && -e $path ) {
$backup = "$path.xcatbak";
rename( $path, $backup ) or do {
unlink $tmp;
return { error => "Unable to back up $path to $backup: $!" };
};
}
unless ( rename( $tmp, $path ) ) {
my $rename_error = $!;
rename( $backup, $path ) if $backup && -e $backup;
unlink $tmp;
return { error => "Unable to replace $path: $rename_error" };
}
return { path => $path, backup => $backup };
}
sub encode_config {
my ( $self, $config ) = @_;
return JSON->new->canonical->pretty->encode($config);
}
sub write_ctrl_agent_config {
my ( $self, $intent, %opts ) = @_;
$opts{path} ||= $self->ctrl_agent_config_file();
$opts{validator} ||= sub { $self->validate_ctrl_agent_config(@_) };
return $self->_write_json_file( $self->render_ctrl_agent_config($intent), %opts );
}
sub validate_dhcp4_config {
my ( $self, $path ) = @_;
return $self->_validate_config_with( 'kea-dhcp4', 'Kea DHCPv4', $path );
}
sub validate_dhcp6_config {
my ( $self, $path ) = @_;
return $self->_validate_config_with( 'kea-dhcp6', 'Kea DHCPv6', $path );
}
sub validate_ddns_config {
my ( $self, $path ) = @_;
return $self->_validate_config_with( 'kea-dhcp-ddns', 'Kea DHCP-DDNS', $path );
}
sub validate_ctrl_agent_config {
my ( $self, $path ) = @_;
return $self->_validate_config_with( 'kea-ctrl-agent', 'Kea Control Agent', $path );
}
sub _validate_config_with {
my ( $self, $command, $label, $path ) = @_;
my $kea = _command_path($command);
return { error => "Unable to validate $label configuration: $command was not found." } unless $kea;
my $cmd = "$kea -t " . _shell_quote($path) . " 2>&1";
my $output = `$cmd`;
my $rc = $? >> 8;
return { error => "$label configuration validation failed: $output" } if $rc != 0;
return { output => $output };
}
sub restart_services {
my ( $self, %opts ) = @_;
require xCAT::Utils;
my @services;
push @services, 'kea-dhcp-ddns' if $opts{ddns};
push @services, 'kea-dhcp4';
push @services, 'kea-dhcp6' if $opts{ipv6};
push @services, 'kea-ctrl-agent' if $opts{ctrl_agent};
foreach my $service (@services) {
if ( $opts{enable} ) {
my $enable_ret = xCAT::Utils->enableservice($service);
return { error => "Failed to enable $service." } if $enable_ret != 0;
}
my $ret = xCAT::Utils->restartservice($service);
return { error => "Failed to restart $service." } if $ret != 0;
}
return { services => \@services };
}
sub check_services {
my ( $self, %opts ) = @_;
require xCAT::Utils;
my @services = ('kea-dhcp4');
push @services, 'kea-dhcp6' if $opts{ipv6};
foreach my $service (@services) {
my $ret = xCAT::Utils->checkservicestatus($service);
return { error => "$service is not running. Please start the Kea DHCP service." } if $ret != 0;
}
return { services => \@services };
}
sub upsert_reservations {
my ( $self, $config, $reservations ) = @_;
foreach my $reservation (@$reservations) {
$self->delete_reservations($config, $reservation);
my $subnet = _find_subnet_by_id( $config, $reservation->{'subnet-id'} );
next unless $subnet;
$subnet->{reservations} ||= [];
$subnet->{reservations} = [
grep { !_reservation_matches( $_, $reservation ) } @{ $subnet->{reservations} }
];
my %stored = %$reservation;
delete $stored{'subnet-id'};
push @{ $subnet->{reservations} }, \%stored;
}
return $config;
}
sub delete_reservations {
my ( $self, $config, $match ) = @_;
my @deleted;
foreach my $subnet ( _subnets_for_config($config) ) {
my @kept;
foreach my $reservation ( @{ $subnet->{reservations} || [] } ) {
if ( _reservation_matches( $reservation, $match ) ) {
push @deleted, { %$reservation, 'subnet-id' => $subnet->{id} };
} else {
push @kept, $reservation;
}
}
$subnet->{reservations} = \@kept;
}
return \@deleted;
}
sub query_reservations {
my ( $self, $config, $match ) = @_;
my @found;
foreach my $subnet ( _subnets_for_config($config) ) {
foreach my $reservation ( @{ $subnet->{reservations} || [] } ) {
if ( _reservation_matches( $reservation, $match ) ) {
push @found, { %$reservation, 'subnet-id' => $subnet->{id}, subnet => $subnet->{subnet} };
}
}
}
return \@found;
}
sub subnet_id_for_ip {
my ( $self, $config, $ip ) = @_;
return unless defined($ip) && $ip ne '';
my $ip_number = xCAT::NetworkUtils::getipaddr( $ip, GetNumber => 1 );
return unless defined($ip_number);
my $bits = $ip =~ /:/ ? 128 : 32;
foreach my $subnet ( _subnets_for_config($config) ) {
next unless $subnet->{subnet} && $subnet->{subnet} =~ m{^([^/]+)/(\d+)$};
my ( $network, $prefix ) = ( $1, $2 );
next if ( $network =~ /:/ ? 128 : 32 ) != $bits;
my $network_number = xCAT::NetworkUtils::getipaddr( $network, GetNumber => 1 );
next unless defined($network_number);
my $mask = Math::BigInt->new( "0b" . ( "1" x $prefix ) . ( "0" x ( $bits - $prefix ) ) );
return $subnet->{id} if ( $ip_number & $mask ) == ( $network_number & $mask );
}
return;
}
sub control_agent_url {
my ( $self, %opts ) = @_;
my $host = $opts{host} || $self->{control_agent_host} || '127.0.0.1';
my $port = $opts{port} || $self->{control_agent_port} || 8000;
return "http://$host:$port/";
}
sub control_agent_command {
my ( $self, $command, $arguments, %opts ) = @_;
my $payload = {
command => $command,
arguments => $arguments || {},
};
$payload->{service} = $opts{service} if $opts{service};
if ( $self->{control_agent_handler} ) {
return $self->{control_agent_handler}->($payload, \%opts);
}
eval { require HTTP::Tiny; };
return { error => "Unable to use Kea Control Agent: HTTP::Tiny is not installed." } if $@;
my $response = HTTP::Tiny->new( timeout => $opts{timeout} || 10 )->post(
$opts{url} || $self->control_agent_url(%opts),
{
headers => {
'Content-Type' => 'application/json',
},
content => $self->encode_config($payload),
}
);
return { error => "Kea Control Agent request failed: $response->{reason}" } unless $response->{success};
my $decoded = eval { decode_json( $response->{content} ) };
return { error => "Unable to parse Kea Control Agent response: $@" } if $@;
return $self->_normalize_control_agent_response($decoded);
}
sub live_upsert_reservations {
my ( $self, $reservations, %opts ) = @_;
my $service = $opts{service} || ['dhcp4'];
foreach my $reservation (@$reservations) {
my $delete = $self->_live_delete_reservation($reservation, service => $service, ignore_not_found => 1);
return $delete if $delete->{error};
my %stored = %$reservation;
my $result = $self->control_agent_command(
'reservation-add',
{
reservation => \%stored,
'operation-target' => 'memory',
},
service => $service,
);
return $result if $result->{error};
}
return { ok => 1 };
}
sub live_delete_reservations {
my ( $self, $reservations, %opts ) = @_;
my $service = $opts{service} || ['dhcp4'];
foreach my $reservation (@$reservations) {
my $result = $self->_live_delete_reservation($reservation, service => $service, ignore_not_found => 1);
return $result if $result->{error};
}
return { ok => 1 };
}
sub _live_delete_reservation {
my ( $self, $reservation, %opts ) = @_;
return { ok => 1 } unless defined $reservation->{'subnet-id'};
my %arguments = (
'subnet-id' => $reservation->{'subnet-id'},
'operation-target' => 'memory',
);
if ( $reservation->{'hw-address'} ) {
$arguments{'identifier-type'} = 'hw-address';
$arguments{identifier} = $reservation->{'hw-address'};
} elsif ( $reservation->{duid} ) {
$arguments{'identifier-type'} = 'duid';
$arguments{identifier} = $reservation->{duid};
} elsif ( $reservation->{'ip-address'} ) {
$arguments{'ip-address'} = $reservation->{'ip-address'};
} elsif ( ref( $reservation->{'ip-addresses'} ) eq 'ARRAY' && @{ $reservation->{'ip-addresses'} } ) {
$arguments{'ip-address'} = $reservation->{'ip-addresses'}[0];
} else {
return { ok => 1 };
}
my $result = $self->control_agent_command(
'reservation-del',
\%arguments,
service => $opts{service} || ['dhcp4'],
);
return { ok => 1 } if $opts{ignore_not_found} && _control_agent_not_found($result);
return $result if $result->{error};
return { ok => 1 };
}
sub _render_subnet4 {
my ( $self, $subnet ) = @_;
my %rendered = (
id => _integer( $subnet->{id} ),
subnet => $subnet->{subnet},
);
$rendered{interface} = $subnet->{interface} if defined $subnet->{interface};
$rendered{'next-server'} = _first_defined( $subnet->{'next-server'}, $subnet->{next_server} );
$rendered{'boot-file-name'} = _first_defined( $subnet->{'boot-file-name'}, $subnet->{boot_file_name} );
$rendered{'option-data'} = _first_defined( $subnet->{'option-data'}, $subnet->{option_data} );
$rendered{reservations} = $subnet->{reservations} if $subnet->{reservations};
delete $rendered{'next-server'} unless defined $rendered{'next-server'};
delete $rendered{'boot-file-name'} unless defined $rendered{'boot-file-name'};
delete $rendered{'option-data'} unless defined $rendered{'option-data'};
if ( $subnet->{pools} ) {
$rendered{pools} = $subnet->{pools};
} elsif ( $subnet->{dynamicrange} ) {
$rendered{pools} = [ xCAT::DHCP::Range->kea_pools( $subnet->{dynamicrange} ) ];
} else {
$rendered{pools} = [];
}
return \%rendered;
}
sub _render_subnet6 {
my ( $self, $subnet ) = @_;
my %rendered = (
id => _integer( $subnet->{id} ),
subnet => $subnet->{subnet},
);
$rendered{interface} = $subnet->{interface} if defined $subnet->{interface};
$rendered{'option-data'} = _first_defined( $subnet->{'option-data'}, $subnet->{option_data} );
$rendered{reservations} = $subnet->{reservations} if $subnet->{reservations};
delete $rendered{'option-data'} unless defined $rendered{'option-data'};
if ( $subnet->{pools} ) {
$rendered{pools} = $subnet->{pools};
} elsif ( $subnet->{dynamicrange} ) {
$rendered{pools} = [ xCAT::DHCP::Range->kea_pools( $subnet->{dynamicrange} ) ];
} else {
$rendered{pools} = [];
}
return \%rendered;
}
sub _find_subnet_by_id {
my ( $config, $subnet_id ) = @_;
return unless defined($subnet_id);
foreach my $subnet ( _subnets_for_config($config) ) {
return $subnet if defined( $subnet->{id} ) && $subnet->{id} == $subnet_id;
}
return;
}
sub _subnets_for_config {
my ($config) = @_;
return @{ $config->{Dhcp4}{subnet4} || [] } if $config->{Dhcp4};
return @{ $config->{Dhcp6}{subnet6} || [] } if $config->{Dhcp6};
return;
}
sub _reservation_matches {
my ( $reservation, $match ) = @_;
foreach my $field ( 'hostname', 'hw-address', 'duid', 'ip-address' ) {
next unless defined $match->{$field} && $match->{$field} ne '';
return 1 if defined $reservation->{$field} && lc( $reservation->{$field} ) eq lc( $match->{$field} );
}
if ( defined $match->{'ip-address'} && ref( $reservation->{'ip-addresses'} ) eq 'ARRAY' ) {
foreach my $ip ( @{ $reservation->{'ip-addresses'} } ) {
return 1 if lc($ip) eq lc( $match->{'ip-address'} );
}
}
return 0;
}
sub _normalize_control_agent_response {
my ( $self, $decoded ) = @_;
my $item = ref($decoded) eq 'ARRAY' ? $decoded->[0] : $decoded;
return { error => 'Kea Control Agent response was empty.' } unless ref($item) eq 'HASH';
return {
ok => defined( $item->{result} ) && $item->{result} == 0 ? 1 : 0,
result => $item->{result},
text => $item->{text},
response => $decoded,
error => defined( $item->{result} ) && $item->{result} == 0 ? undef : ( $item->{text} || 'Kea Control Agent command failed.' ),
};
}
sub _control_agent_not_found {
my ($result) = @_;
return 0 unless $result && $result->{error};
return 1 if defined( $result->{result} ) && $result->{result} == 3;
return 1 if defined( $result->{text} ) && $result->{text} =~ /not\s+(?:deleted|found)/i;
return 0;
}
sub _first_defined {
foreach my $value (@_) {
return $value if defined $value;
}
return;
}
sub _integer {
my ($value) = @_;
return $value unless defined($value) && $value =~ /^\d+$/;
return 0 + $value;
}
sub _set_config_permissions {
my ($path) = @_;
if ($> != 0) {
chmod 0644, $path or return { error => "Unable to set $path permissions to 0644: $!" };
return { ok => 1 };
}
my ( $group, $gid ) = _kea_group();
if ( defined($gid) ) {
chown 0, $gid, $path or return { error => "Unable to set $path ownership to root:$group: $!" };
chmod 0640, $path or return { error => "Unable to set $path permissions to 0640: $!" };
} else {
chmod 0644, $path or return { error => "Unable to set $path permissions to 0644: $!" };
}
return { ok => 1 };
}
sub _kea_group {
foreach my $group ( 'kea', '_kea' ) {
my @entry = getgrnam($group);
return ( $entry[0], $entry[2] ) if @entry;
}
return;
}
sub _command_path {
my ($command) = @_;
foreach my $dir ( split /:/, $ENV{PATH} || '' ) {
next unless $dir;
my $path = "$dir/$command";
return $path if -x $path;
}
foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) {
return $path if -x $path;
}
return;
}
sub _shell_quote {
my ($value) = @_;
$value =~ s/'/'\\''/g;
return "'$value'";
}
1;

View File

@@ -0,0 +1,58 @@
package xCAT::DHCP::BootPolicy;
use strict;
use warnings;
sub kea_client_classes {
my ( $class, %opts ) = @_;
my $bios_boot = $opts{xnba_kpxe} ? 'xcat/xnba.kpxe' : 'pxelinux.0';
my $uefi_boot = $opts{xnba_efi} ? 'xcat/xnba.efi' : '';
my @classes;
if ( $opts{xnba_kpxe} ) {
push @classes, {
name => 'xcat-xnba-bios',
test => "option[77].text == 'xNBA' and option[93].hex == 0x0000",
'boot-file-name' => 'xcat/xnba.kpxe',
};
}
push @classes, (
{
name => 'xcat-bios',
test => 'option[93].hex == 0x0000',
'boot-file-name' => $bios_boot,
},
);
if ($uefi_boot ne '') {
push @classes, {
name => 'xcat-uefi-x64',
test => 'option[93].hex == 0x0007 or option[93].hex == 0x0009',
'boot-file-name' => $uefi_boot,
};
}
push @classes, (
{
name => 'xcat-aarch64',
test => 'option[93].hex == 0x000b',
'boot-file-name' => 'boot/grub2/grub2.aarch64',
},
{
name => 'xcat-ppc64',
test => 'option[93].hex == 0x000c',
'boot-file-name' => '/boot/grub2/grub2.ppc',
},
{
name => 'xcat-ia64',
test => 'option[93].hex == 0x0002',
'boot-file-name' => 'elilo.efi',
},
);
return \@classes;
}
1;

View File

@@ -0,0 +1,135 @@
package xCAT::DHCP::Range;
use strict;
use warnings;
use Math::BigInt;
use Socket;
use xCAT::NetworkUtils qw/getipaddr/;
sub parse_dynamic_ranges {
my ( $class, $ranges ) = @_;
return [] unless defined($ranges) && $ranges ne '';
my @parsed;
foreach my $range ( split /;/, $ranges ) {
my $entry = $class->parse($range);
push @parsed, $entry if $entry;
}
return \@parsed;
}
sub parse {
my ( $class, $range ) = @_;
return unless defined($range);
$range =~ s/^\s+|\s+$//g;
return unless $range ne '';
if ( $range =~ m{/} ) {
return $class->_parse_cidr($range);
}
return $class->_parse_pair($range);
}
sub isc_ranges {
my ( $class, $ranges ) = @_;
return map { $class->isc_range($_) } @{ $class->parse_dynamic_ranges($ranges) };
}
sub kea_pools {
my ( $class, $ranges ) = @_;
return map { { pool => $class->kea_pool($_) } } @{ $class->parse_dynamic_ranges($ranges) };
}
sub isc_range {
my ( $class, $entry ) = @_;
return $entry->{cidr} if $entry->{family} == 6 && $entry->{cidr};
return "$entry->{start} $entry->{end}";
}
sub kea_pool {
my ( $class, $entry ) = @_;
return $entry->{cidr} if $entry->{family} == 6 && $entry->{cidr};
return "$entry->{start} - $entry->{end}";
}
sub bounds {
my ( $class, $entry ) = @_;
return ( $entry->{start_number}, $entry->{end_number} );
}
sub _parse_pair {
my ( $class, $range ) = @_;
my @parts = grep { $_ ne '' } split /[\s,-]+/, $range;
return unless @parts >= 2;
my ( $start, $end ) = @parts[ 0, 1 ];
my $family = ( $start =~ /:/ || $end =~ /:/ ) ? 6 : 4;
my $start_number = getipaddr( $start, GetNumber => 1 );
my $end_number = getipaddr( $end, GetNumber => 1 );
return unless defined($start_number) && defined($end_number);
return {
source => $range,
family => $family,
start => $start,
end => $end,
start_number => $start_number,
end_number => $end_number,
};
}
sub _parse_cidr {
my ( $class, $range ) = @_;
my ( $prefix, $suffix ) = split /\//, $range, 2;
return unless defined($prefix) && defined($suffix) && $suffix =~ /^\d+$/;
my $family = $prefix =~ /:/ ? 6 : 4;
my $numbits = $family == 6 ? 128 : 32;
return if $suffix > $numbits;
my $number = getipaddr( $prefix, GetNumber => 1 );
return unless defined($number);
my $highmask = Math::BigInt->new( "0b" . ( "1" x $suffix ) . ( "0" x ( $numbits - $suffix ) ) );
my $lowmask = Math::BigInt->new( "0b" . ( "1" x ( $numbits - $suffix ) ) );
$number &= $highmask;
my $start_number = $number->copy();
$number |= $lowmask;
my $end_number = $number->copy();
if ( $family == 6 ) {
return {
source => $range,
family => 6,
cidr => $range,
start_number => $start_number,
end_number => $end_number,
};
}
return {
source => $range,
family => 4,
cidr => $range,
start => inet_ntoa( pack( "N*", $start_number->numify() ) ),
end => inet_ntoa( pack( "N*", $end_number->numify() ) ),
start_number => $start_number,
end_number => $end_number,
};
}
1;

View File

@@ -1031,6 +1031,17 @@ passed as argument rather than by table value',
" -------------\n" .
"DHCP ATTRIBUTES\n" .
" -------------\n" .
" dhcpbackend: The DHCP implementation used by makedhcp. Valid values are auto,\n" .
" isc, and kea. The default is auto. In auto mode, xCAT uses ISC\n" .
" DHCP on existing supported platforms and Kea DHCP on platforms\n" .
" where ISC DHCP is no longer available, such as EL10 and Ubuntu\n" .
" 24.04.\n\n" .
" keacontrolagent: Whether makedhcp should generate Kea Control Agent socket\n" .
" configuration. Valid values are 1/yes/true/enabled or\n" .
" 0/no/false/disabled. The default is disabled; host\n" .
" reservations use JSON render and reload unless Control\n" .
" Agent operations are explicitly enabled and the Kea\n" .
" host-commands hook is installed.\n\n" .
" dhcpinterfaces: The network interfaces DHCP should listen on. If it is the same for all\n" .
" nodes, use a comma-separated list of the NICs. To specify different NICs\n" .
" for different nodes, use the format: \"xcatmn|eth1,eth2;service|bond0\", \n" .
@@ -4338,5 +4349,3 @@ foreach (keys %xCAT::Schema::defspec) {
}
}
1;

View File

@@ -4074,14 +4074,18 @@ sub servicemap {
# (general service name) => {list of possible service names}
#
my %svchash = (
"dhcp" => [ "dhcp3-server", "dhcpd", "isc-dhcp-server" ],
"nfs" => [ "nfsserver", "nfs-server", "nfs", "nfs-kernel-server" ],
"named" => [ "named", "bind9" ],
"syslog" => [ "syslog", "syslogd", "rsyslog" ],
"firewall" => [ "iptables", "firewalld", "ufw" ],
"http" => [ "apache2", "httpd" ],
"ntpserver" => [ "ntpd", "ntp" ],
"mysql" => [ "mysqld", "mysql", "mariadb" ],
"dhcp" => [ "dhcp3-server", "dhcpd", "isc-dhcp-server" ],
"kea-dhcp4" => [ "kea-dhcp4", "kea-dhcp4-server", "isc-kea-dhcp4-server" ],
"kea-dhcp6" => [ "kea-dhcp6", "kea-dhcp6-server", "isc-kea-dhcp6-server" ],
"kea-ctrl-agent" => [ "kea-ctrl-agent", "kea-ctrl-agent-server", "isc-kea-ctrl-agent" ],
"kea-dhcp-ddns" => [ "kea-dhcp-ddns", "kea-dhcp-ddns-server", "isc-kea-dhcp-ddns-server" ],
"nfs" => [ "nfsserver", "nfs-server", "nfs", "nfs-kernel-server" ],
"named" => [ "named", "bind9" ],
"syslog" => [ "syslog", "syslogd", "rsyslog" ],
"firewall" => [ "iptables", "firewalld", "ufw" ],
"http" => [ "apache2", "httpd" ],
"ntpserver" => [ "ntpd", "ntp" ],
"mysql" => [ "mysqld", "mysql", "mariadb" ],
);
my $path = undef;

View File

@@ -22,6 +22,8 @@ B<makedhcp> B<[-h|--help]>
The B<makedhcp> command creates and updates the DHCP configuration on the management node and service nodes.
The B<makedhcp> command is supported for both Linux and AIX clusters.
On Linux, the DHCP implementation is selected by the C<site.dhcpbackend> attribute.
The C<auto> setting keeps ISC DHCP on platforms where it is still available and uses Kea DHCP on platforms such as EL10 and Ubuntu 24.04.
=over 3
@@ -41,8 +43,8 @@ Also, get the hostnames and IP addresses pushed to /etc/hosts (using L<makehosts
=item 4.
Then run B<makedhcp> with a noderange or the B<-a> option. This will inject into dhcpd configuration data pertinent to the specified nodes.
On linux, the configuration information immediately takes effect without a restart of DHCP.
Then run B<makedhcp> with a noderange or the B<-a> option. This will inject DHCP configuration data pertinent to the specified nodes.
With ISC DHCP, Linux node entries are updated through OMAPI. With Kea DHCP, node reservations are rendered into the Kea JSON configuration, validated, and the Kea service is restarted.
=back
@@ -58,7 +60,8 @@ Create a new dhcp configuration file with a network statement for each network t
(Which networks dhcpd should listen on can be controlled by the dhcpinterfaces attribute in the L<site(5)|site.5> table.)
The B<makedhcp> command will automatically restart the dhcp daemon after this operation.
This option will replace any existing configuration file (making a backup of it first).
For Linux systems the file will include network entries as well as certain general parameters such as a dynamic range and omapi configuration.
For Linux systems using ISC DHCP, the file will include network entries as well as certain general parameters such as a dynamic range and omapi configuration.
For Linux systems using Kea DHCP, the generated files are /etc/kea/kea-dhcp4.conf, /etc/kea/kea-dhcp6.conf when IPv6 networks are configured, and /etc/kea/kea-dhcp-ddns.conf when DDNS is enabled.
For AIX systems the file will include network entries.
On AIX systems, if there are any non-xCAT entries in the existing configuration file they will be preserved and added to the end of the new configuration file.
@@ -75,6 +78,7 @@ Add the specified nodes to the DHCP server configuration.
=item B<-s> I<statements>
For the input noderange, the argument will be interpreted like dhcp configuration file text.
This option is only supported by the ISC DHCP backend.
=item B<-d> I<noderange>
@@ -150,7 +154,12 @@ DHCP configuration files:
[RH] /etc/dhcp/dhcpd.conf
[Kea] /etc/kea/kea-dhcp4.conf
[Kea IPv6] /etc/kea/kea-dhcp6.conf
[Kea DDNS] /etc/kea/kea-dhcp-ddns.conf
=head1 SEE ALSO
L<noderange(3)|noderange.3>

View File

@@ -493,7 +493,7 @@ sub obtain_log_content {
} else {
$log_content{sender} = $split_line[1];
}
if ($split_line[2] =~ /dhcpd/i) {
if ($split_line[2] =~ /dhcpd|kea-dhcp4/i) {
$log_content{label} = $::LOGLABEL_DHCPD;
} elsif ($split_line[2] =~ /in.tftpd/i) {
$log_content{label} = $::LOGLABEL_TFTP;
@@ -521,7 +521,7 @@ sub obtain_log_content {
} else {
$log_content{sender} = $split_line[3];
}
if ($split_line[4] =~ /dhcpd/i) {
if ($split_line[4] =~ /dhcpd|kea-dhcp4/i) {
$log_content{label} = $::LOGLABEL_DHCPD;
} elsif ($split_line[4] =~ /in.tftpd/i) {
$log_content{label} = $::LOGLABEL_TFTP;
@@ -728,4 +728,3 @@ sub debuglogger {
}
}
1;

View File

@@ -828,6 +828,10 @@ sub dhcp_dynamic_range_check {
my $msg = "DHCP dynamic range is configured";
my $dhcpconfig;
my $kea_config;
if (-e "/etc/kea/kea-dhcp4.conf") {
$kea_config = "/etc/kea/kea-dhcp4.conf";
}
if (-e "/etc/dhcp/dhcpd.conf") {
$dhcpconfig = "/etc/dhcp/dhcpd.conf";
} elsif (-e "/etc/dhcp3/dhcpd.conf") {
@@ -836,9 +840,9 @@ sub dhcp_dynamic_range_check {
$dhcpconfig = "/etc/dhcpd.conf";
}
unless ($dhcpconfig) {
unless ($dhcpconfig || $kea_config) {
probe_utils->send_msg("stdout", "f", $msg);
probe_utils->send_msg("stdout", "d", "Cannot find the dhcpd.conf file.");
probe_utils->send_msg("stdout", "d", "Cannot find the DHCP configuration file.");
return 1;
}
@@ -847,13 +851,40 @@ sub dhcp_dynamic_range_check {
my @dynamic_range;
my %subnet_hash;
unless (open(FILE, $dhcpconfig)) {
if ($kea_config) {
$dhcpconfig = undef;
my $config_json = eval {
require JSON;
open(my $fh, '<', $kea_config) or die $!;
local $/;
my $content = <$fh>;
close($fh);
JSON::decode_json($content);
};
if ($@) {
probe_utils->send_msg("stdout", "f", $msg);
probe_utils->send_msg("stdout", "d", "Cannot parse file $kea_config: $@");
return 1;
}
foreach my $subnet4 (@{ $config_json->{Dhcp4}{subnet4} || [] }) {
next unless $subnet4->{subnet} && $subnet4->{subnet} =~ m{^([^/]+)/(\d+)$};
my $mask = prefix_to_netmask($2);
my @ranges;
foreach my $pool (@{ $subnet4->{pools} || [] }) {
next unless $pool->{pool};
my $range = $pool->{pool};
$range =~ s/\s*-\s*/-/;
push @ranges, $range;
}
$subnet_hash{"$1/$mask"} = @ranges ? \@ranges : "unknown";
}
} elsif (!open(FILE, $dhcpconfig)) {
probe_utils->send_msg("stdout", "f", $msg);
probe_utils->send_msg("stdout", "d", "Cannot open file $dhcpconfig.");
return 1;
}
while ($config_line = <FILE>) {
while ($dhcpconfig && ($config_line = <FILE>)) {
chomp($config_line);
$config_line =~ s/^\s+|\s+$//g;
@@ -1007,6 +1038,17 @@ sub compare_ip_value {
return 0;
}
sub prefix_to_netmask {
my $prefix = shift;
my $mask_num = 0;
for my $idx (0 .. $prefix - 1) {
$mask_num |= (1 << (31 - $idx));
}
return join('.', map { ($mask_num >> (8 * (3 - $_))) & 0xff } 0 .. 3);
}
#------------------------------------------
=head3
@@ -1705,6 +1747,3 @@ sub conclusion_report {
return 0;
}

View File

@@ -1067,10 +1067,10 @@ sub check_dhcp_service {
chomp($checkdhcp);
if ($checkdhcp) {
# on sn, just check dhcpd service whether running
my $dhcpoutput = `ps aux 2>&1| grep dhcpd |grep -v grep`;
# on sn, just check DHCP service whether running
my $dhcpoutput = `ps aux 2>&1| grep -E 'dhcpd|kea-dhcp4' |grep -v grep`;
if (!$dhcpoutput) {
push @$error_ref, "There isn't 'dhcpd' daemon in current server";
push @$error_ref, "There isn't a DHCP daemon in current server";
$rst = 1;
}
} else {
@@ -1180,7 +1180,7 @@ sub check_dhcp_leases {
my $error_ref = shift;
my $rst = 0;
$$checkpoint_ref = "Checking dhcpd.leases file is less than 100M...";
$$checkpoint_ref = "Checking DHCP lease file is less than 100M...";
@$error_ref = ();
my $leasefile = "";
@@ -1190,6 +1190,8 @@ sub check_dhcp_leases {
$leasefile = "/var/lib/dhcp/db/dhcpd.leases";
} elsif (-e "/var/lib/dhcp/dhcpd.leases") {
$leasefile = "/var/lib/dhcp/dhcpd.leases";
} elsif (-e "/var/lib/kea/kea-leases4.csv") {
$leasefile = "/var/lib/kea/kea-leases4.csv";
}
my @fileinfo = stat("$leasefile");
@@ -1492,4 +1494,3 @@ exit $rst;

View File

@@ -4,10 +4,10 @@ $RES::Condition{'CheckDHCPonSN'} = {
Name => q(CheckDHCPonSN),
ResourceClass => q(IBM.Program),
EventExpression => q(Processes.CurPidCount == 0),
EventDescription => q(An event will be generated when the DHCP server is down on the service node. There may be other nodes in this management domain such as HMCs. To exclude them, just change the SelectionString to: "ProgramName=='dhcpd' && NodeNameList >< {'hmc1','hmc2}" where hmc1 and hmc2 are the names for the nodes that you want to exclude.),
EventDescription => q(An event will be generated when the DHCP server is down on the service node. There may be other nodes in this management domain such as HMCs. To exclude them, just change the SelectionString to: "(ProgramName=='dhcpd' || ProgramName=='kea-dhcp4') && NodeNameList >< {'hmc1','hmc2}" where hmc1 and hmc2 are the names for the nodes that you want to exclude.),
RearmExpression => q(Processes.CurPidCount != 0),
RearmDescription => q(A rearm event will be generated when the DHCP server is up on the service node.),
SelectionString => q(ProgramName=='dhcpd'),
SelectionString => q(ProgramName=='dhcpd' || ProgramName=='kea-dhcp4'),
ManagementScope => q(4),
Severity => q(1),
};

View File

@@ -14,6 +14,7 @@ use xCAT::Table;
use xCAT::Utils;
use xCAT::TableUtils;
use xCAT::NetworkUtils;
use xCAT::DHCP::Backend;
use xCAT::MsgUtils;
use xCAT_plugin::dhcp;
@@ -613,15 +614,11 @@ sub setup_DHCP
my $rc = 0;
my $cmd;
my $snonly = 0;
my $dhcpbackend = xCAT::DHCP::Backend->new_backend();
my $dhcpservice = (ref($dhcpbackend) ne 'HASH' && $dhcpbackend->name eq 'kea') ? 'kea-dhcp4' : 'dhcp';
# if on the MN check to see if dhcpd is running, and start it if not.
# if on the MN check to see if DHCP is running, and start it if not.
if (xCAT::Utils->isMN()) { # on the MN
#my @output = xCAT::Utils->runcmd("service dhcpd status", -1);
#if ($::RUNCMD_RC != 0) { # not running
my $dhcpservice = "dhcpd";
if (-e "/etc/init.d/isc-dhcp-server") { #Ubuntu
$dhcpservice = "isc-dhcp-server";
}
my $retcode = xCAT::Utils->checkservicestatus($dhcpservice);
if ($retcode != 0) {
$rc = xCAT::Utils->startservice($dhcpservice);
@@ -669,7 +666,7 @@ sub setup_DHCP
if (xCAT::Utils->isAIX()) {
$rc = xCAT::Utils->startService("dhcpd");
} elsif (xCAT::Utils->isLinux()) {
$rc = xCAT::Utils->startservice("dhcp");
$rc = xCAT::Utils->startservice($dhcpservice);
}
if ($rc != 0)
{

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ BEGIN
use lib "$::XCATROOT/lib/perl";
use strict;
use xCAT::Utils;
use xCAT::DHCP::Backend;
use xCAT::NetworkUtils;
use Getopt::Long;
use xCAT::MsgUtils;
@@ -489,9 +490,12 @@ if ($::INITIALINSTALL || $::FORCE)
&makenetworks;
&setuphttp;
# chkconfig dhcpd on
#system("chkconfig dhcpd on");
xCAT::Utils->enableservice("dhcp");
my $dhcpbackend = xCAT::DHCP::Backend->new_backend();
if (ref($dhcpbackend) ne 'HASH' && $dhcpbackend->name eq 'kea') {
xCAT::Utils->enableservice("kea-dhcp4");
} else {
xCAT::Utils->enableservice("dhcp");
}
# Turn off selinux on RedHat
if (-f "/etc/redhat-release")
@@ -575,7 +579,7 @@ if ($::INITIALINSTALL || $::FORCE)
my $linux_note =
"xCAT is now running, it is recommended to tabedit networks \nand set a dynamic ip address range on any networks where nodes \nare to be discovered. Then, run makedhcp -n to create a new dhcpd \nconfiguration file, and \/etc\/init.d\/dhcpd restart. Either examine sample \nconfiguration templates, or write your own, or specify a value per \nnode with nodeadd or tabedit.";
"xCAT is now running, it is recommended to tabedit networks \nand set a dynamic ip address range on any networks where nodes \nare to be discovered. Then, run makedhcp -n to create a new DHCP \nconfiguration file, and restart the selected DHCP service. Either examine sample \nconfiguration templates, or write your own, or specify a value per \nnode with nodeadd or tabedit.";
xCAT::MsgUtils->message('I', $linux_note);
} else { #AIX

View File

@@ -213,6 +213,9 @@ sub snap_it {
"/etc/dhcp/dhcpd.conf", "/etc/dhcp/dhcpd6.conf",
"/etc/dhcpd6.conf", "/var/lib/dhcp/db/dhcpd.leases",
"/var/lib/dhcp/dhcpd.leases","/etc/goconserver/server.conf",
"/etc/kea/kea-dhcp4.conf", "/etc/kea/kea-dhcp6.conf",
"/etc/kea/kea-ctrl-agent.conf", "/etc/kea/kea-dhcp-ddns.conf",
"/var/lib/kea/kea-leases4.csv", "/var/lib/kea/kea-leases6.csv",
"/var/log/goconserver/*", "/etc/logrotate.d/goconserver",
"/etc/bind/named.conf");
print("@files_array \n");

View File

@@ -133,7 +133,16 @@ if [ `uname` = "AIX" ]
then
runcmd "stopsrc -s dhcpcd"
else # Linux
runcmd "service dhcpd stop"
if command -v systemctl >/dev/null 2>&1
then
runcmd "systemctl stop kea-ctrl-agent kea-dhcp-ddns kea-dhcp6 kea-dhcp4 dhcpd 2>/dev/null || true"
else
runcmd "service kea-ctrl-agent stop 2>/dev/null || true"
runcmd "service kea-dhcp-ddns stop 2>/dev/null || true"
runcmd "service kea-dhcp6 stop 2>/dev/null || true"
runcmd "service kea-dhcp4 stop 2>/dev/null || true"
runcmd "service dhcpd stop 2>/dev/null || true"
fi
fi
##############################################################################

View File

@@ -1,6 +1,14 @@
#!/usr/bin/perl -w
#use Data::Dumper;
BEGIN
{
$::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat';
}
use lib "$::XCATROOT/lib/perl";
use Getopt::Long;
use Fcntl ':flock';
use xCAT::DHCP::Backend;
sub usage{
print "Usage: dhcphelper -h \n";
@@ -26,6 +34,64 @@ if($help){
&usage;
exit 0;
}elsif($rmop){
my $backend = xCAT::DHCP::Backend->new_backend();
if (ref($backend) ne 'HASH' && $backend->name eq 'kea') {
if ($mac && $mac !~ /:/) {
$mac = lc($mac);
$mac =~ s/(\w{2})/$1:/g;
$mac =~ s/:$//;
}
mkdir "/tmp/xcat" unless -d "/tmp/xcat";
open(my $dhcplockfd, ">", "/tmp/xcat/dhcplock") or die "Unable to lock DHCP state: $!";
flock($dhcplockfd, LOCK_EX);
my $config4 = $backend->load_dhcp4_config();
if ($config4->{error}) {
print "Error: $config4->{error}\n";
exit 1;
}
my $config6;
my $have_dhcp6 = -e $backend->dhcp6_config_file();
if ($have_dhcp6) {
$config6 = $backend->load_dhcp6_config();
if ($config6->{error}) {
print "Error: $config6->{error}\n";
exit 1;
}
}
my @matches;
push @matches, { hostname => $hostname } if $hostname;
push @matches, { 'hw-address' => lc($mac) } if $mac;
push @matches, { 'ip-address' => $ip } if $ip;
foreach my $match (@matches) {
$backend->delete_reservations($config4, $match);
$backend->delete_reservations($config6, $match) if $config6;
}
my $result = $backend->write_dhcp4_json($backend->encode_config($config4));
if ($result->{error}) {
print "Error: $result->{error}\n";
exit 1;
}
if ($config6) {
my $result6 = $backend->write_dhcp6_json($backend->encode_config($config6));
if ($result6->{error}) {
print "Error: $result6->{error}\n";
exit 1;
}
}
my $restart = $backend->restart_services(ipv6 => $config6 ? 1 : 0);
if ($restart->{error}) {
print "Error: $restart->{error}\n";
exit 1;
}
exit 0;
}
my $out=qx(tabdump -w key==omapi -w username==xcat_key passwd |tail -n1|awk -F, '{print \$2","\$3}');
$out =~ s/("|\n)//g;
my ($id,$passwd)=split(',',$out);

View File

@@ -621,9 +621,9 @@ my %URIdef = (
desc => "[URI:/services/dhcp] - The dhcp service resource.",
matcher => '^/services/dhcp$',
POST => {
desc => "Create the dhcpd.conf for all the networks which are defined in the xCAT Management Node.",
desc => "Create the DHCP server configuration for all the networks which are defined in the xCAT Management Node.",
usage => "||$usagemsg{non_getreturn}|",
example => "|Create the dhcpd.conf and restart the dhcpd.|POST|/services/dhcp||",
example => "|Create the DHCP configuration and restart the selected DHCP service.|POST|/services/dhcp||",
cmd => "makedhcp",
fhandler => \&nonobjhdl,
outhdler => \&noout,
@@ -2745,4 +2745,3 @@ sub pushFlags {
}
}

View File

@@ -747,9 +747,9 @@ my %URIdef = (
desc => "[URI:/services/dhcp] - The dhcp service resource.",
matcher => '^/services/dhcp$',
POST => {
desc => "Create the dhcpd.conf for all the networks which are defined in the xCAT Management Node.",
desc => "Create the DHCP server configuration for all the networks which are defined in the xCAT Management Node.",
usage => "||$usagemsg{non_getreturn}|",
example => "|Create the dhcpd.conf and restart the dhcpd.|POST|/services/dhcp||",
example => "|Create the DHCP configuration and restart the selected DHCP service.|POST|/services/dhcp||",
cmd => "makedhcp",
fhandler => \&nonobjhdl,
outhdler => \&noout,
@@ -3589,4 +3589,3 @@ sub pushFlags {
}
}

View File

@@ -0,0 +1,108 @@
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use Test::More;
use xCAT::DHCP::Backend;
is( xCAT::DHCP::Backend->normalize(undef), 'auto', 'undefined backend defaults to auto' );
is( xCAT::DHCP::Backend->normalize(' ISC '), 'isc', 'backend values are trimmed and lowercased' );
is( xCAT::DHCP::Backend->normalize('kea'), 'kea', 'kea is valid' );
is( xCAT::DHCP::Backend->normalize('bogus'), undef, 'invalid backend is rejected' );
is(
xCAT::DHCP::Backend->default_backend( platform => 'el9', os => 'rhel9', os_name => 'rhel', version => 9 ),
'isc',
'EL9 defaults to ISC'
);
is(
xCAT::DHCP::Backend->default_backend( platform => 'el10', os => 'rhel10', os_name => 'rhel', version => 10 ),
'kea',
'EL10 defaults to Kea by platform'
);
is(
xCAT::DHCP::Backend->default_backend( platform => '', os => 'rocky10', os_name => 'rocky', version => 10 ),
'kea',
'EL10 derivatives default to Kea by osver'
);
is(
xCAT::DHCP::Backend->default_backend( platform => '', os => 'ubuntu22.04', os_name => 'ubuntu', version => '22.04' ),
'isc',
'Ubuntu 22.04 defaults to ISC'
);
is(
xCAT::DHCP::Backend->default_backend( platform => '', os => 'ubuntu24.04', os_name => 'ubuntu', version => '24.04' ),
'kea',
'Ubuntu 24.04 defaults to Kea'
);
is(
xCAT::DHCP::Backend->default_backend( platform => '', os => 'ubuntu24.10', os_name => 'ubuntu', version => '24.10' ),
'kea',
'Ubuntu releases newer than 24.04 default to Kea'
);
is(
xCAT::DHCP::Backend->select( requested => 'isc', os => 'rhel10', platform => 'el10' )->{name},
'isc',
'explicit ISC override wins on EL10'
);
is(
xCAT::DHCP::Backend->select( requested => 'kea', os => 'rhel9', platform => 'el9' )->{name},
'kea',
'explicit Kea override wins on EL9'
);
is(
xCAT::DHCP::Backend->select( requested => 'auto', os => 'rhel9', platform => 'el9' )->{name},
'isc',
'auto selects ISC on EL9'
);
is(
xCAT::DHCP::Backend->select( requested => 'auto', os => 'rhel10', platform => 'el10' )->{name},
'kea',
'auto selects Kea on EL10'
);
is(
xCAT::DHCP::Backend->select( requested => 'auto', os => 'ubuntu24.04', os_name => 'ubuntu', version => '24.04' )->{name},
'kea',
'auto selects Kea on Ubuntu 24.04'
);
like(
xCAT::DHCP::Backend->select( requested => 'invalid' )->{error},
qr/Invalid site\.dhcpbackend/,
'invalid explicit backend returns a clear error'
);
like(
xCAT::DHCP::Backend->select(
requested => 'kea',
check_available => 1,
available => { kea => 0 },
)->{error},
qr/not available/,
'unavailable forced backend returns a clear error'
);
is(
xCAT::DHCP::Backend->select(
requested => 'kea',
check_available => 1,
available => { kea => 1 },
)->{name},
'kea',
'forced Kea succeeds when available'
);
done_testing();

View File

@@ -0,0 +1,27 @@
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use Test::More;
use xCAT::DHCP::BootPolicy;
my $fallback_classes = xCAT::DHCP::BootPolicy->kea_client_classes();
is( scalar @$fallback_classes, 4, 'Kea boot policy omits xNBA classes when xNBA loaders are unavailable' );
my %fallback_by_name = map { $_->{name} => $_ } @$fallback_classes;
is( $fallback_by_name{'xcat-bios'}{'boot-file-name'}, 'pxelinux.0', 'BIOS clients fall back to pxelinux.0 without xNBA loaders' );
ok( !exists $fallback_by_name{'xcat-xnba-bios'}, 'xNBA user-class is not advertised without xNBA kpxe' );
my $classes = xCAT::DHCP::BootPolicy->kea_client_classes(xnba_kpxe => 1, xnba_efi => 1);
is( scalar @$classes, 6, 'Kea boot policy renders expected xNBA client classes' );
my %by_name = map { $_->{name} => $_ } @$classes;
is( $by_name{'xcat-bios'}{'boot-file-name'}, 'xcat/xnba.kpxe', 'BIOS clients receive xNBA kpxe' );
like( $by_name{'xcat-uefi-x64'}{test}, qr/0x0007/, 'UEFI x64 class matches architecture 7' );
like( $by_name{'xcat-uefi-x64'}{test}, qr/0x0009/, 'UEFI x64 class matches architecture 9' );
is( $by_name{'xcat-aarch64'}{'boot-file-name'}, 'boot/grub2/grub2.aarch64', 'AArch64 clients receive grub2 boot file' );
is( $by_name{'xcat-ppc64'}{'boot-file-name'}, '/boot/grub2/grub2.ppc', 'POWER clients receive grub2 Open Firmware boot file' );
done_testing();

View File

@@ -0,0 +1,229 @@
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use File::Temp qw/tempfile/;
use JSON ();
use Test::More;
use xCAT::DHCP::Backend::Kea;
my $kea_dhcp4 = command_path('kea-dhcp4');
plan skip_all => 'kea-dhcp4 is not installed' unless $kea_dhcp4;
my $validation_dir = validation_temp_dir($kea_dhcp4);
plan skip_all => 'kea-dhcp4 cannot read temporary config files; run as root to validate from /etc/kea'
unless defined $validation_dir;
my $backend = xCAT::DHCP::Backend::Kea->new();
my $json = $backend->render_dhcp4_config(
{
interfaces => ['*'],
'option-def' => [
{ name => 'conf-file', code => 209, type => 'string', space => 'dhcp4' },
{ name => 'iscsi-initiator-iqn', code => 203, type => 'string', space => 'dhcp4' },
{ name => 'cumulus-provision-url', code => 239, type => 'string', space => 'dhcp4' },
],
'client-classes' => [
{
name => 'xcat-xnba-bios',
test => "option[77].text == 'xNBA' and option[93].hex == 0x0000",
'boot-file-name' => 'xcat/xnba.kpxe',
},
{
name => 'xcat-uefi-x64',
test => 'option[93].hex == 0x0007 or option[93].hex == 0x0009',
'boot-file-name' => 'xcat/xnba.efi',
},
],
'dhcp-ddns' => {
'enable-updates' => JSON::true,
'server-ip' => '127.0.0.1',
'server-port' => 53001,
'ncr-protocol' => 'UDP',
'ncr-format' => 'JSON',
},
'ddns-send-updates' => JSON::true,
'ddns-override-no-update' => JSON::true,
'ddns-override-client-update' => JSON::true,
'ddns-qualifying-suffix' => 'cluster.test.',
'ddns-update-on-renew' => JSON::true,
subnets => [
{
id => 1,
subnet => '192.168.122.0/24',
dynamicrange => '192.168.122.100-192.168.122.120',
next_server => '192.168.122.1',
option_data => [
{ name => 'routers', data => '192.168.122.1' },
{ name => 'domain-name', data => 'cluster.test' },
{ name => 'domain-name-servers', data => '192.168.122.1' },
{ name => 'cumulus-provision-url', data => 'http://192.168.122.1:80/install/postscripts/cumulusztp' },
],
reservations => [
{
'hw-address' => '52:54:00:12:34:56',
'ip-address' => '192.168.122.50',
hostname => 'node01',
'next-server' => '192.168.122.1',
'boot-file-name' => 'pxelinux.0',
'option-data' => [ { name => 'host-name', data => 'node01' } ],
},
],
},
],
}
);
my $path = write_validation_file( $json, 'xcat-test-kea-dhcp4' );
my $result = $backend->validate_dhcp4_config($path);
ok( !$result->{error}, 'generated Kea DHCPv4 config validates with kea-dhcp4 -t' )
or diag $result->{error};
unlink $path;
SKIP: {
skip 'kea-dhcp6 is not installed', 1 unless command_path('kea-dhcp6');
my $dhcp6_json = $backend->render_dhcp6_config(
{
interfaces => ['*'],
'dhcp-ddns' => {
'enable-updates' => JSON::true,
'server-ip' => '127.0.0.1',
'server-port' => 53001,
'ncr-protocol' => 'UDP',
'ncr-format' => 'JSON',
},
'ddns-send-updates' => JSON::true,
'ddns-override-no-update' => JSON::true,
'ddns-override-client-update' => JSON::true,
'ddns-qualifying-suffix' => 'cluster.test.',
'ddns-update-on-renew' => JSON::true,
subnets => [
{
id => 6,
subnet => '2001:db8:1::/64',
dynamicrange => '2001:db8:1::100/120',
option_data => [
{ name => 'dns-servers', data => '2001:db8:1::1' },
{ name => 'domain-search', data => 'cluster.test' },
],
reservations => [
{
duid => '00:04:52:54:00:12:34:56',
'ip-addresses' => ['2001:db8:1::50'],
hostname => 'nodev6',
},
],
},
],
}
);
my $dhcp6_path = write_validation_file( $dhcp6_json, 'xcat-test-kea-dhcp6' );
my $dhcp6_result = $backend->validate_dhcp6_config($dhcp6_path);
ok( !$dhcp6_result->{error}, 'generated Kea DHCPv6 config validates with kea-dhcp6 -t' )
or diag $dhcp6_result->{error};
unlink $dhcp6_path;
}
SKIP: {
skip 'kea-dhcp-ddns is not installed', 1 unless command_path('kea-dhcp-ddns');
my $ddns_json = $backend->render_ddns_config(
{
'tsig-keys' => [
{ name => 'xcat_key', algorithm => 'HMAC-SHA256', secret => 'YWJjMTIz' },
],
forward_domains => [
{
name => 'cluster.test.',
'key-name' => 'xcat_key',
'dns-servers' => [ { 'ip-address' => '127.0.0.1', port => 53 } ],
},
],
reverse_domains => [
{
name => '122.168.192.in-addr.arpa.',
'key-name' => 'xcat_key',
'dns-servers' => [ { 'ip-address' => '127.0.0.1', port => 53 } ],
},
],
}
);
my $ddns_path = write_validation_file( $ddns_json, 'xcat-test-kea-ddns' );
my $ddns_result = $backend->validate_ddns_config($ddns_path);
ok( !$ddns_result->{error}, 'generated Kea DHCP-DDNS config validates with kea-dhcp-ddns -t' )
or diag $ddns_result->{error};
unlink $ddns_path;
}
SKIP: {
skip 'kea-ctrl-agent is not installed', 1 unless command_path('kea-ctrl-agent');
my $ctrl_agent_json = $backend->render_ctrl_agent_config(
{
dhcp6 => 1,
ddns => 1,
}
);
my $ctrl_path = write_validation_file( $ctrl_agent_json, 'xcat-test-kea-ctrl-agent' );
my $ctrl_result = $backend->validate_ctrl_agent_config($ctrl_path);
ok( !$ctrl_result->{error}, 'generated Kea Control Agent config validates with kea-ctrl-agent -t' )
or diag $ctrl_result->{error};
unlink $ctrl_path;
}
done_testing();
sub command_path {
my ($command) = @_;
foreach my $dir ( split /:/, $ENV{PATH} || '' ) {
next unless $dir;
return "$dir/$command" if -x "$dir/$command";
}
foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) {
return $path if -x $path;
}
return;
}
sub validation_temp_dir {
my ($kea_dhcp4) = @_;
return '/etc/kea' if -d '/etc/kea' && -w '/etc/kea';
my ( $fh, $path ) = tempfile();
print $fh '{"Dhcp4":{"interfaces-config":{"interfaces":[]},"lease-database":{"type":"memfile","name":"/tmp/xcat-test-kea-leases4.csv"},"valid-lifetime":600,"subnet4":[]}}';
close($fh);
chmod 0644, $path or die "Unable to set $path permissions: $!";
my $command = shell_quote($kea_dhcp4) . ' -t ' . shell_quote($path) . ' 2>&1';
my $output = `$command`;
unlink $path;
return undef if $output =~ /Unable to open file/;
return '';
}
sub write_validation_file {
my ( $content, $template ) = @_;
my %opts = ( TEMPLATE => "$template-XXXXXX", UNLINK => 0 );
$opts{DIR} = $validation_dir if defined $validation_dir;
my ( $fh, $path ) = tempfile(%opts);
print $fh $content;
close($fh);
chmod 0644, $path or die "Unable to set $path permissions: $!";
return $path;
}
sub shell_quote {
my ($value) = @_;
$value =~ s/'/'\\''/g;
return "'$value'";
}

View File

@@ -0,0 +1,166 @@
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use File::Path qw/make_path/;
use File::Temp qw/tempdir/;
use Test::More;
use xCAT::DHCP::Backend::Kea;
plan skip_all => 'set XCAT_KEA_LIVE_SMOKE=1 to run live Kea daemon smoke test'
unless $ENV{XCAT_KEA_LIVE_SMOKE};
plan skip_all => 'live Kea daemon smoke test must run as root' unless $> == 0;
my $kea_dhcp4 = command_path('kea-dhcp4');
my $kea_ctrl = command_path('kea-ctrl-agent');
plan skip_all => 'kea-dhcp4 and kea-ctrl-agent are required'
unless $kea_dhcp4 && $kea_ctrl;
my $backend = xCAT::DHCP::Backend::Kea->new();
my $hook = $backend->host_cmds_hook_path();
plan skip_all => 'Kea host-commands hook is required' unless $hook;
my $tmp = tempdir(CLEANUP => 1);
make_path('/var/run/kea');
make_path('/var/lib/kea');
my $socket = "/var/run/kea/kea4-xcat-smoke-$$.sock";
my $lease_file = "/var/lib/kea/kea-leases4-xcat-smoke-$$.csv";
unlink $socket, $lease_file;
my $dhcp_config = "$tmp/kea-dhcp4.conf";
my $ctrl_config = "$tmp/kea-ctrl-agent.conf";
write_file(
$dhcp_config,
$backend->render_dhcp4_config(
{
interfaces => ['lo'],
'lease-database' => {
type => 'memfile',
name => $lease_file,
},
'control-socket' => {
'socket-type' => 'unix',
'socket-name' => $socket,
},
'hooks-libraries' => [ { library => $hook } ],
subnets => [
{
id => 1,
subnet => '127.0.0.0/8',
pools => [],
},
],
}
)
);
write_file(
$ctrl_config,
$backend->render_ctrl_agent_config(
{
'http-port' => 18000,
'dhcp4-socket' => $socket,
}
)
);
my $dhcp_validation = $backend->validate_dhcp4_config($dhcp_config);
ok( !$dhcp_validation->{error}, 'live smoke DHCPv4 config validates' )
or diag $dhcp_validation->{error};
my $ctrl_validation = $backend->validate_ctrl_agent_config($ctrl_config);
ok( !$ctrl_validation->{error}, 'live smoke Control Agent config validates' )
or diag $ctrl_validation->{error};
my @pids;
END {
kill 'TERM', @pids if @pids;
unlink grep { defined($_) && $_ ne '' } ( $socket, $lease_file );
}
push @pids, start_daemon($kea_dhcp4, '-c', $dhcp_config, '-d', "$tmp/kea-dhcp4.log");
ok( wait_for_process($pids[-1]), 'kea-dhcp4 stays running for smoke test' );
push @pids, start_daemon($kea_ctrl, '-c', $ctrl_config, '-d', "$tmp/kea-ctrl-agent.log");
ok( wait_for_process($pids[-1]), 'kea-ctrl-agent stays running for smoke test' );
my $live_backend = xCAT::DHCP::Backend::Kea->new(control_agent_port => 18000);
my $add = $live_backend->live_upsert_reservations(
[
{
'subnet-id' => 1,
'hw-address' => '52:54:00:12:34:56',
'ip-address' => '127.0.0.50',
hostname => 'node-smoke',
},
],
service => ['dhcp4']
);
ok( !$add->{error}, 'reservation-add succeeds through Kea Control Agent' )
or diag $add->{error};
my $delete = $live_backend->live_delete_reservations(
[
{
'subnet-id' => 1,
'hw-address' => '52:54:00:12:34:56',
'ip-address' => '127.0.0.50',
hostname => 'node-smoke',
},
],
service => ['dhcp4']
);
ok( !$delete->{error}, 'reservation-del succeeds through Kea Control Agent' )
or diag $delete->{error};
done_testing();
sub command_path {
my ($command) = @_;
foreach my $dir ( split /:/, $ENV{PATH} || '' ) {
next unless $dir;
return "$dir/$command" if -x "$dir/$command";
}
foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) {
return $path if -x $path;
}
return;
}
sub write_file {
my ( $path, $content ) = @_;
open(my $fh, '>', $path) or die "Unable to write $path: $!";
print $fh $content;
close($fh) or die "Unable to close $path: $!";
}
sub start_daemon {
my ( $command, @args ) = @_;
my $log = pop @args;
my $pid = fork();
die "Unable to fork $command: $!" unless defined $pid;
if ($pid == 0) {
open(STDOUT, '>', $log) or die "Unable to write $log: $!";
open(STDERR, '>&', \*STDOUT) or die "Unable to redirect stderr: $!";
exec $command, @args;
die "Unable to exec $command: $!";
}
return $pid;
}
sub wait_for_process {
my ($pid) = @_;
for (1 .. 10) {
sleep 1;
return 0 unless kill 0, $pid;
return 1 if $_ >= 2;
}
return kill 0, $pid;
}

View File

@@ -0,0 +1,287 @@
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use File::Temp qw/tempdir/;
use JSON;
use Test::More;
use xCAT::DHCP::Backend::Kea;
my $backend = xCAT::DHCP::Backend::Kea->new();
my $json = $backend->render_dhcp4_config(
{
interfaces => ['eth0'],
valid_lifetime => '600',
subnets => [
{
id => '1',
subnet => '10.0.0.0/24',
interface => 'eth0',
dynamicrange => '10.0.0.100-10.0.0.120;10.0.0.130,10.0.0.140',
next_server => '10.0.0.1',
option_data => [
{ name => 'routers', data => '10.0.0.1' },
{ name => 'domain-name-servers', data => '10.0.0.2, 10.0.0.3' },
{ name => 'domain-name', data => 'cluster.example.com' },
],
reservations => [
{
'hw-address' => 'aa:bb:cc:dd:ee:ff',
'ip-address' => '10.0.0.10',
hostname => 'node01',
},
],
},
],
'client-classes' => [
{
name => 'xcat-uefi-x64',
test => 'option[93].hex == 0x0007',
'boot-file-name' => 'xcat/xnba.efi',
},
],
}
);
my $config = decode_json($json);
ok( $config->{Dhcp4}, 'renderer creates a Dhcp4 document' );
is_deeply( $config->{Dhcp4}{'interfaces-config'}{interfaces}, ['eth0'], 'interfaces are rendered' );
is( $config->{Dhcp4}{'valid-lifetime'}, 600, 'valid lifetime is rendered' );
is( $config->{Dhcp4}{'lease-database'}{type}, 'memfile', 'memfile lease backend is the default' );
my $subnet = $config->{Dhcp4}{subnet4}[0];
is( $subnet->{id}, 1, 'subnet id is rendered' );
is( $subnet->{subnet}, '10.0.0.0/24', 'subnet CIDR is rendered' );
is( $subnet->{interface}, 'eth0', 'subnet interface is rendered' );
is( $subnet->{'next-server'}, '10.0.0.1', 'next-server is rendered' );
is_deeply(
$subnet->{pools},
[
{ pool => '10.0.0.100 - 10.0.0.120' },
{ pool => '10.0.0.130 - 10.0.0.140' },
],
'dynamicrange is rendered as Kea pools'
);
is_deeply(
$subnet->{'option-data'},
[
{ name => 'routers', data => '10.0.0.1' },
{ name => 'domain-name-servers', data => '10.0.0.2, 10.0.0.3' },
{ name => 'domain-name', data => 'cluster.example.com' },
],
'subnet option-data is preserved'
);
is_deeply(
$subnet->{reservations},
[
{
'hw-address' => 'aa:bb:cc:dd:ee:ff',
'ip-address' => '10.0.0.10',
hostname => 'node01',
},
],
'host reservations are preserved'
);
is_deeply(
$config->{Dhcp4}{'client-classes'},
[
{
name => 'xcat-uefi-x64',
test => 'option[93].hex == 0x0007',
'boot-file-name' => 'xcat/xnba.efi',
},
],
'client classes are preserved'
);
my $empty_boot_json = $backend->render_dhcp4_config(
{
subnets => [
{
id => 2,
subnet => '10.0.1.0/24',
pools => [],
next_server => '0.0.0.0',
boot_file_name => '',
},
],
}
);
my $empty_boot_subnet = decode_json($empty_boot_json)->{Dhcp4}{subnet4}[0];
is( $empty_boot_subnet->{'next-server'}, '0.0.0.0', 'false-looking next-server value is preserved' );
is( $empty_boot_subnet->{'boot-file-name'}, '', 'empty boot-file-name is preserved' );
my $reservation_config = decode_json(
$backend->render_dhcp4_config(
{
subnets => [
{
id => 10,
subnet => '10.10.0.0/24',
pools => [],
},
],
}
)
);
$backend->upsert_reservations(
$reservation_config,
[
{
'subnet-id' => 10,
'hw-address' => '00:11:22:33:44:55',
'ip-address' => '10.10.0.12',
hostname => 'node12',
},
]
);
is( scalar @{ $reservation_config->{Dhcp4}{subnet4}[0]{reservations} }, 1, 'reservation is added to matching subnet' );
$backend->upsert_reservations(
$reservation_config,
[
{
'subnet-id' => 10,
'hw-address' => '00:11:22:33:44:55',
'ip-address' => '10.10.0.13',
hostname => 'node12',
},
]
);
is( scalar @{ $reservation_config->{Dhcp4}{subnet4}[0]{reservations} }, 1, 'matching reservation is replaced, not duplicated' );
is( $reservation_config->{Dhcp4}{subnet4}[0]{reservations}[0]{'ip-address'}, '10.10.0.13', 'replacement reservation is stored' );
my $subnet_id = $backend->subnet_id_for_ip( $reservation_config, '10.10.0.13' );
is( $subnet_id, 10, 'subnet lookup by IPv4 address finds the reservation subnet' );
my $found = $backend->query_reservations( $reservation_config, { hostname => 'node12' } );
is( scalar @$found, 1, 'reservation query finds hostname match' );
is( $found->[0]{'subnet-id'}, 10, 'reservation query includes subnet id' );
my $deleted = $backend->delete_reservations( $reservation_config, { 'hw-address' => '00:11:22:33:44:55' } );
is( scalar @$deleted, 1, 'reservation delete returns deleted reservation' );
is( scalar @{ $reservation_config->{Dhcp4}{subnet4}[0]{reservations} }, 0, 'reservation is removed from config' );
my $hookdir = tempdir(CLEANUP => 1);
my $hook = "$hookdir/libdhcp_host_cmds.so";
open(my $hookfh, '>', $hook) or die "Unable to create fake hook: $!";
close($hookfh);
is( $backend->host_cmds_hook_path($hook), $hook, 'host commands hook lookup accepts an explicit existing path' );
my $backend_without_default_hooks = xCAT::DHCP::Backend::Kea->new(host_cmds_hook_paths => []);
is( $backend_without_default_hooks->host_cmds_hook_path("$hookdir/missing.so"), undef, 'host commands hook lookup returns undef when no hook exists' );
my $backupdir = tempdir(CLEANUP => 1);
my $config_path = "$backupdir/kea-dhcp4.conf";
open(my $configfh, '>', $config_path) or die "Unable to create fake config: $!";
print $configfh '{"old":true}';
close($configfh);
my $write_result = $backend->write_dhcp4_json('{"new":true}', path => $config_path, skip_validate => 1, backup_existing => 1);
ok( !$write_result->{error}, 'write_dhcp4_json succeeds with backup_existing' );
is( $write_result->{backup}, "$config_path.xcatbak", 'backup path is reported' );
my $config_mode = ( stat($config_path) )[2] & 07777;
ok( $config_mode == 0640 || $config_mode == 0644, 'written config is readable by the Kea service user' );
open(my $backupfh, '<', "$config_path.xcatbak") or die "Unable to read backup config: $!";
my $backup_content = <$backupfh>;
close($backupfh);
is( $backup_content, '{"old":true}', 'existing Kea config is backed up before replacement' );
my $dhcp6_json = $backend->render_dhcp6_config(
{
interfaces => ['eth0'],
valid_lifetime => '700',
preferred_lifetime => '500',
subnets => [
{
id => '11',
subnet => '2001:db8:1::/64',
dynamicrange => '2001:db8:1::100/120',
option_data => [
{ name => 'dns-servers', data => '2001:db8:1::1' },
{ name => 'domain-search', data => 'cluster.example.com' },
],
reservations => [
{
duid => '00:04:52:54:00:12:34:56',
'ip-addresses' => ['2001:db8:1::50'],
hostname => 'nodev6',
},
],
},
],
}
);
my $dhcp6_config = decode_json($dhcp6_json);
ok( $dhcp6_config->{Dhcp6}, 'renderer creates a Dhcp6 document' );
is( $dhcp6_config->{Dhcp6}{subnet6}[0]{subnet}, '2001:db8:1::/64', 'DHCPv6 subnet is rendered' );
is( $dhcp6_config->{Dhcp6}{'valid-lifetime'}, 700, 'DHCPv6 valid lifetime is numeric' );
is( $dhcp6_config->{Dhcp6}{'preferred-lifetime'}, 500, 'DHCPv6 preferred lifetime is numeric' );
is( $dhcp6_config->{Dhcp6}{subnet6}[0]{id}, 11, 'DHCPv6 subnet id is numeric' );
is( $dhcp6_config->{Dhcp6}{subnet6}[0]{reservations}[0]{duid}, '00:04:52:54:00:12:34:56', 'DHCPv6 DUID reservation is rendered' );
my $ddns_json = $backend->render_ddns_config(
{
port => '53001',
'dns-server-timeout' => '500',
'tsig-keys' => [
{ name => 'xcat_key', algorithm => 'HMAC-SHA256', secret => 'abc123==' },
],
forward_domains => [
{
name => 'cluster.example.com.',
'key-name' => 'xcat_key',
'dns-servers' => [ { 'ip-address' => '10.0.0.1', port => 53 } ],
},
],
reverse_domains => [
{
name => '0.0.10.in-addr.arpa.',
'key-name' => 'xcat_key',
'dns-servers' => [ { 'ip-address' => '10.0.0.1', port => 53 } ],
},
],
}
);
my $ddns_config = decode_json($ddns_json);
ok( $ddns_config->{DhcpDdns}, 'renderer creates a DhcpDdns document' );
is( $ddns_config->{DhcpDdns}{port}, 53001, 'DDNS port is numeric' );
is( $ddns_config->{DhcpDdns}{'dns-server-timeout'}, 500, 'DDNS timeout is numeric' );
is( $ddns_config->{DhcpDdns}{'forward-ddns'}{'ddns-domains'}[0]{name}, 'cluster.example.com.', 'DDNS forward domain is rendered' );
my $ctrl_agent_config = decode_json($backend->render_ctrl_agent_config({ 'http-port' => '8000' }));
is( $ctrl_agent_config->{'Control-agent'}{'http-port'}, 8000, 'Control Agent HTTP port is numeric' );
my @commands;
my $ca_backend = xCAT::DHCP::Backend::Kea->new(
control_agent_handler => sub {
my ($payload) = @_;
push @commands, $payload;
return { ok => 1, result => 0, response => { result => 0 } };
},
);
my $live_result = $ca_backend->live_upsert_reservations(
[
{
'subnet-id' => 10,
'hw-address' => '00:11:22:33:44:55',
'ip-address' => '10.10.0.20',
hostname => 'node20',
'boot-file-name' => 'pxelinux.0',
},
]
);
ok( !$live_result->{error}, 'live reservation upsert succeeds through injected Control Agent handler' );
is( $commands[0]{command}, 'reservation-del', 'live upsert deletes any existing reservation first' );
is( $commands[0]{arguments}{'operation-target'}, 'memory', 'live delete targets runtime memory' );
is( $commands[1]{command}, 'reservation-add', 'live upsert adds reservation through host-commands' );
is( $commands[1]{arguments}{reservation}{'subnet-id'}, 10, 'live add includes subnet id in reservation body' );
is_deeply( $commands[1]{service}, ['dhcp4'], 'live add targets dhcp4 service by default' );
done_testing();

View File

@@ -0,0 +1,55 @@
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../perl-xCAT";
use Test::More;
use xCAT::DHCP::Range;
my $pair = xCAT::DHCP::Range->parse('10.0.0.10-10.0.0.20');
is( $pair->{family}, 4, 'IPv4 range is detected' );
is( $pair->{start}, '10.0.0.10', 'range start is parsed' );
is( $pair->{end}, '10.0.0.20', 'range end is parsed' );
is( xCAT::DHCP::Range->isc_range($pair), '10.0.0.10 10.0.0.20', 'ISC range uses space separator' );
is( xCAT::DHCP::Range->kea_pool($pair), '10.0.0.10 - 10.0.0.20', 'Kea pool uses JSON pool syntax' );
is_deeply(
[ xCAT::DHCP::Range->isc_ranges('10.0.0.10,10.0.0.20;10.0.1.10 10.0.1.20') ],
[ '10.0.0.10 10.0.0.20', '10.0.1.10 10.0.1.20' ],
'multiple ranges are normalized for ISC'
);
is_deeply(
[ xCAT::DHCP::Range->kea_pools('10.0.0.10,10.0.0.20;10.0.1.10 10.0.1.20') ],
[
{ pool => '10.0.0.10 - 10.0.0.20' },
{ pool => '10.0.1.10 - 10.0.1.20' },
],
'multiple ranges are normalized for Kea pools'
);
my $cidr = xCAT::DHCP::Range->parse('192.168.50.0/30');
is( xCAT::DHCP::Range->isc_range($cidr), '192.168.50.0 192.168.50.3', 'IPv4 CIDR is expanded for ISC' );
is( xCAT::DHCP::Range->kea_pool($cidr), '192.168.50.0 - 192.168.50.3', 'IPv4 CIDR is expanded for Kea' );
my ( $start, $end ) = xCAT::DHCP::Range->bounds($cidr);
is( "$start", '3232248320', 'CIDR start numeric bound is tracked' );
is( "$end", '3232248323', 'CIDR end numeric bound is tracked' );
my $zero_start = xCAT::DHCP::Range->parse('0.0.0.0-0.0.0.10');
is( xCAT::DHCP::Range->isc_range($zero_start), '0.0.0.0 0.0.0.10', 'zero-valued range starts are preserved' );
SKIP: {
skip 'Socket6 is not installed', 3 unless eval { require Socket6; 1 };
my $cidr6 = xCAT::DHCP::Range->parse('2001:db8::/120');
is( $cidr6->{family}, 6, 'IPv6 CIDR is detected' );
is( xCAT::DHCP::Range->isc_range($cidr6), '2001:db8::/120', 'IPv6 CIDR is preserved for ISC DHCPv6' );
is( xCAT::DHCP::Range->kea_pool($cidr6), '2001:db8::/120', 'IPv6 CIDR is preserved for Kea DHCPv6' );
}
is_deeply( xCAT::DHCP::Range->parse_dynamic_ranges(undef), [], 'undefined range list returns no ranges' );
is( xCAT::DHCP::Range->parse('not-a-range'), undef, 'invalid range returns undef' );
done_testing();

View File

@@ -9,8 +9,8 @@ Homepage: https://xcat.org/
Package: xcat
Architecture: amd64 ppc64el
Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, xcat-genesis-scripts-ppc64 (>= 2.13-snap000000000000), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000)
Recommends: bind9, net-tools, nmap, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000)
Depends: ${perl:Depends}, goconserver(>= 0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, isc-dhcp-server | kea, apache2, nfs-kernel-server, libxml-parser-perl, rsync, tftpd-hpa, libnet-telnet-perl, xcat-genesis-scripts-ppc64 (>= 2.13-snap000000000000), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000)
Recommends: bind9, net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000)
Suggests: yaboot-xcat
Description: Metapackage for a common, default xCAT setup
xCAT is Extreme Cluster/Cloud Administration Toolkit. xCAT offers complete

View File

@@ -284,7 +284,7 @@ function servicemap {
# specified with structure
# INIT_(general service name) = "list of possible service names"
#
INIT_dhcp="dhcp3-server dhcpd isc-dhcp-server";
INIT_dhcp="dhcp3-server dhcpd isc-dhcp-server kea-dhcp4";
INIT_nfs="nfsserver nfs-server nfs nfs-kernel-server";
@@ -826,4 +826,3 @@ function msgutil_r {
function msgutil {
msgutil_r "" "$@"
}

View File

@@ -70,8 +70,13 @@ Requires: httpd nfs-utils nmap bind perl(CGI)
# on RHEL7, need to specify it explicitly
Requires: net-tools
Requires: /usr/bin/killall
# On RHEL this pulls in dhcp, on SLES it pulls in dhcp-server
# On RHEL this pulls in dhcp, on SLES it pulls in dhcp-server. EL10 uses Kea.
%if 0%{?rhel} >= 10
Requires: kea
Requires: kea-hooks
%else
Requires: /usr/sbin/dhcpd
%endif
# On RHEL this pulls in openssh-server, on SLES it pulls in openssh
Requires: /usr/bin/ssh
%if %nots390x

View File

@@ -8,8 +8,8 @@ Homepage: https://xcat.org/
Package: xcatsn
Architecture: amd64 ppc64el
Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server, apache2, nfs-kernel-server, xcat-genesis-scripts-ppc64 (>= 2.13-snap000000000000), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000)
Recommends: bind9, net-tools, nmap, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000)
Depends: ${perl:Depends}, goconserver (>=0.3.3-snap000000000000), xcat-server (>= 2.13-snap000000000000), xcat-client (>= 2.13-snap000000000000), libdbd-sqlite3-perl, libxml-parser-perl, tftpd-hpa, libnet-telnet-perl, isc-dhcp-server | kea, apache2, nfs-kernel-server, xcat-genesis-scripts-ppc64 (>= 2.13-snap000000000000), xcat-genesis-scripts-amd64 (>= 2.13-snap000000000000)
Recommends: bind9, net-tools, nmap, kea, tftp-hpa, ipmitool-xcat (>= 1.8.17-1), syslinux[any-amd64], libsys-virt-perl, syslinux-xcat, xnba-undi, elilo-xcat, xcat-buildkit (>= 2.13-snap000000000000), xcat-probe (>= 2.13-snap000000000000)
Suggests: yaboot-xcat
Description: Metapackage for a common, default xCAT service node setup
xCATsn is a service node management package intended for at-scale

View File

@@ -48,8 +48,13 @@ Requires: /usr/bin/killall
Requires: /usr/bin/bc
# yaboot-xcat is pulled in so any SN can manage ppc nodes
Requires: httpd nfs-utils nmap bind
# On RHEL this pulls in dhcp, on SLES it pulls in dhcp-server
# On RHEL this pulls in dhcp, on SLES it pulls in dhcp-server. EL10 uses Kea.
%if 0%{?rhel} >= 10
Requires: kea
Requires: kea-hooks
%else
Requires: /usr/sbin/dhcpd
%endif
# On RHEL this pulls in openssh-server, on SLES it pulls in openssh
Requires: /usr/bin/ssh
%ifnarch s390x