# IBM(c) 2010 EPL license http://www.eclipse.org/legal/epl-v10.html package xCAT_plugin::dhcp; BEGIN { $::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat'; } use lib "$::XCATROOT/lib/perl"; use strict; use IPC::Open2; use IPC::Open3; use IO::Select; use File::Temp qw(tempfile); use Symbol qw/gensym/; use POSIX qw/WNOHANG/; use Time::HiRes qw(sleep); use xCAT::Table; #use Data::Dumper; use MIME::Base64; use JSON (); use Getopt::Long; Getopt::Long::Configure("bundling"); Getopt::Long::Configure("pass_through"); use Socket; my $candoipv6 = eval { require Socket6; 1; }; use Sys::Syslog; use IPC::Open2; use xCAT::Utils; use xCAT::SvrUtils; use xCAT::DHCP::BootPolicy; use xCAT::DHCP::Backend; use xCAT::DHCP::OmapiPolicy; use xCAT::DHCP::Range; use xCAT::TableUtils; use xCAT::NetworkUtils qw/getipaddr/; use xCAT::ServiceNodeUtils; use xCAT::NodeRange; use Fcntl ':flock'; my @aixcfg; # hold AIX entries created by NIM my @dhcpconf; #Hold DHCP config file contents to be written back. my @dhcp6conf; #ipv6 equivalent my @nrn; # To hold output of networks table to be consulted throughout process my @nrn6; #holds ip -6 route output on Linux, yeah, name doesn't make much sense now.. my $site_domain; my @alldomains; my $omshell; my $omshell6; #separate session to DHCPv6 instance of dhcp my $omshellpid; my $omshell6pid; my $statements; #Hold custom statements to be slipped into host declarations my $localonly; # flag for running only on local server - needs to be global my $callback; my $restartdhcp; my $restartdhcp6; my $sitenameservers; my $sitentpservers; my $sitelogservers; my $nrhash; my $machash; my $vpdhash; my $iscsients; my $nodetypeents; my $chainents; my $tftpdir = xCAT::TableUtils->getTftpDir(); my $dhcpconffile = $^O eq 'aix' ? '/etc/dhcpsd.cnf' : '/etc/dhcpd.conf'; my %dynamicranges; #track dynamic ranges defined to see if a host that resolves is actually a dynamic address my %netcfgs; my $distro = xCAT::Utils->osver(); my $checkdomain=0; # dhcp 4.x will use /etc/dhcp/dhcpd.conf as the config file my $dhcp6conffile; if ($^O ne 'aix' and not -e $dhcpconffile and -d "/etc/dhcp") { $dhcpconffile = '/etc/dhcp/dhcpd.conf'; $dhcp6conffile = '/etc/dhcp/dhcpd6.conf'; } my $usingipv6; # define usage statement my $usage = "Usage: makedhcp -n\n\tmakedhcp -a\n\tmakedhcp -a -d\n\tmakedhcp -d noderange\n\tmakedhcp [-s statements]\n\tmakedhcp -q\n\tmakedhcp [-h|--help]"; # is this ubuntu ? if ($distro =~ /ubuntu.*/) { if (-e '/etc/dhcp/') { $dhcpconffile = '/etc/dhcp/dhcpd.conf'; } else { $dhcpconffile = '/etc/dhcp3/dhcpd.conf'; } } sub check_uefi_support { my $ntent = shift; my %blacklist = ( "win2k3.*" => 1, "winxp.*" => 1, "SL5.*" => 1, "rhels5.*" => 1, "centos5.*" => 1, "sl5.*" => 1, "sles10.*" => 1, "esxi4.*" => 1); if ($ntent and $ntent->{os}) { foreach (keys %blacklist) { if ($ntent->{os} =~ /$_/) { return 0; } } } if ($ntent->{os} =~ /^win/ or $ntent->{os} =~ /^hyperv/) { #UEFI support is a tad different, need to punt.. return 2; } return 1; } # check whether the proxydhcp has been enabled. sub proxydhcp { my $nrent = shift; if ($nrent && defined $nrent->{'proxydhcp'} && $nrent->{'proxydhcp'} =~ /0|no|n/i) { return 0; } my @output = xCAT::Utils->runcmd("ps -C proxydhcp-xcat", -1); if (@output) { if (grep /proxydhcp-xcat/, @output) { return 1; } } return 0; } sub ipIsDynamic { #meant to be v4/v6 agnostic. DHCPv6 however takes some care to allow a dynamic range to overlap static reservations #xCAT will for now continue to advise people to keep their nodes out of the dynamic range my $ip = shift; my $number = getipaddr($ip, GetNumber => 1); unless ($number) { # shouldn't be possible, but pessimistically presume it dynamically if so return 1; } foreach (values %dynamicranges) { if ($_->[0] <= $number and $_->[1] >= $number) { return 1; } } return 0; #it isn't in any of the dynamic ranges we are aware of } sub handled_commands { return { makedhcp => "dhcp", }; } sub _omapi_settings { my $cb = shift || $callback; my $settings = xCAT::DHCP::OmapiPolicy->settings(); if ($settings->{error}) { $cb->({ error => [ $settings->{error} ], errorcode => [1] }) if $cb; syslog("local4|err", $settings->{error}); return; } return $settings; } sub _omapi_passwd_entry { my $settings = shift; my $passtab = xCAT::Table->new('passwd', -create => 1); return unless $passtab; return $passtab->getAttribs({ key => 'omapi', username => $settings->{key_name} }, qw(username password)); } sub _ubuntu_isc_omapi_limited { # Ubuntu's ISC DHCP 4.4 omshell can hang or crash on failed host opens # and on next-server host statements. Use static local host blocks there. return $distro =~ /^ubuntu(20|20\.04|22|22\.04)/; } sub _omapi_ip_lookup_supported { return !_ubuntu_isc_omapi_limited(); } sub _omapi_pre_create_cleanup_supported { return !_ubuntu_isc_omapi_limited(); } sub _omapi_next_server_statement { my $server = shift; return '' if _ubuntu_isc_omapi_limited(); return 'next-server ' . $server . ';'; } sub _isc_static_host_fallback { return _ubuntu_isc_omapi_limited() && !$::XCATSITEVALS{externaldhcpservers}; } sub _delete_isc_static_host { my $node = shift; my @updated; my $skip = 0; foreach my $line (@dhcpconf) { if ($line =~ /^#xCAT host declaration for \Q$node\E\b.* start$/) { $skip = 1; next; } if ($skip && $line =~ /^#xCAT host declaration for \Q$node\E\b.* end$/) { $skip = 0; next; } push @updated, $line unless $skip; } @dhcpconf = @updated; } sub _static_host_statements { my $statements = shift || ''; $statements =~ s/ddns-hostname \\"([^"]+)\\";/ddns-hostname "$1";/g; $statements =~ s/send host-name \\"([^"]+)\\";/option host-name "$1";/g; $statements =~ s/\\"/"/g; return $statements; } sub _add_isc_static_host { my ($node, $hostname, $mac, $ip, $statements) = @_; return unless $ip && $ip ne 'DENIED'; _delete_isc_static_host($node); my $host_statements = _static_host_statements($statements); push @dhcpconf, "#xCAT host declaration for $node aka host $hostname start\n"; push @dhcpconf, "host $hostname {\n"; push @dhcpconf, " hardware ethernet $mac;\n"; push @dhcpconf, " fixed-address $ip;\n"; push @dhcpconf, " $host_statements\n" if $host_statements; push @dhcpconf, "} #xCAT host declaration for $node aka host $hostname end\n"; $restartdhcp = 1; } sub _open_omshell_writer { my $settings = shift; mkdir "/tmp/xcat" unless -d "/tmp/xcat"; my ($omshell_stdin, $command_file) = tempfile('omshell.XXXXXX', DIR => '/tmp/xcat', UNLINK => 0); return unless $omshell_stdin; return ($omshell_stdin, { command_file => $command_file, omshell_path => $settings->{omshell_path} }); } sub _run_omshell_command_file { my ($command_file, $omshell_path) = @_; my $pid = fork(); return unless defined $pid; if ($pid == 0) { open(STDIN, '<', $command_file) or exit 127; ## no critic (InputOutput::RequireCheckedOpen) open(STDOUT, '>', '/dev/null') or exit 127; ## no critic (InputOutput::RequireCheckedOpen) open(STDERR, '>', '/dev/null') or exit 127; ## no critic (InputOutput::RequireCheckedOpen) exec { $omshell_path } $omshell_path; exit 127; } for (1 .. 100) { if (waitpid($pid, WNOHANG) == $pid) { sleep 1.0; return 1; } sleep 0.1; } kill 'TERM', $pid; for (1 .. 20) { return if waitpid($pid, WNOHANG) == $pid; sleep 0.1; } kill 'KILL', $pid; waitpid($pid, 0); return; } sub _close_omshell_writer { my ($fh, $writer) = @_; close($fh) if $fh; return unless ref($writer) eq 'HASH'; my $ok = _run_omshell_command_file($writer->{command_file}, $writer->{omshell_path}); unlink $writer->{command_file}; syslog("local4|err", "omshell did not complete while updating DHCP reservations") unless $ok; } ###################################################### # Run omshell to query a host and parse the output. # Uses a heredoc pipe instead of open2 to avoid deadlock # when omshell produces output before all input is consumed. ###################################################### sub _omshell_query_host { my $node = shift; my $settings = shift; my $omapikey = shift; my $port = shift; my $omcmds = xCAT::DHCP::OmapiPolicy->omshell_preamble($settings, secret => $omapikey, port => $port); $omcmds .= "connect\nnew host\nset name = \"$node\"\nopen\nclose\n"; my @output = _run_omshell($omcmds, $settings); return _parse_omshell_host_output($node, @output); } sub _run_omshell { my $omcmds = shift; my $settings = shift || _omapi_settings(); return () unless $settings; my ($in, $out); my $err = gensym; my $pid = eval { open3($in, $out, $err, $settings->{omshell_path}) }; return () if $@ || !$pid; print $in $omcmds; close($in); my @output; my $selector = IO::Select->new($out, $err); my $deadline = time + 10; while ($selector->count) { my $remaining = $deadline - time; if ($remaining <= 0) { kill 'TERM', $pid; last; } foreach my $fh ($selector->can_read($remaining)) { my $line = <$fh>; if (defined $line) { push @output, $line if fileno($fh) == fileno($out); } else { $selector->remove($fh); close($fh); } } } for (1 .. 10) { last if waitpid($pid, WNOHANG) == $pid; sleep 0.1; } if (waitpid($pid, WNOHANG) == 0) { kill 'KILL', $pid; waitpid($pid, 0); } return @output; } sub _parse_omshell_host_output { my ($node, @output) = @_; my ($nname, $ipaddr, $hwaddr); foreach my $line (@output) { chomp $line; if ($line =~ /^\s*name\s*=\s*"?(.*?)"?\s*$/) { $nname = $1 if $1 eq $node; } elsif ($line =~ /^\s*hardware-address\s*=/) { $hwaddr = $line; } elsif ($line =~ /^\s*ip-address\s*=\s*(.+?)\s*$/) { my $ip = $1; if ($ip =~ /^(?:[[:xdigit:]]{1,2}:){3}[[:xdigit:]]{1,2}$/) { my @parts = split(/\:/, $ip); $ipaddr = "ip-address = " . join(".", map { hex($_) } @parts); } else { $ipaddr = "ip-address = $ip"; } } } $nname = $node if !$nname && $ipaddr; return ($nname, $ipaddr, $hwaddr); } ###################################################### # List nodes in DHCP for both IPv4 and IPv6 ###################################################### sub listnode { my $node = shift; my $callback = shift; my $rsp; my $settings = _omapi_settings($callback); return unless $settings; my $pent = _omapi_passwd_entry($settings); unless ($pent && $pent->{password}) { $callback->({ error => ["Unable to access omapi key from passwd table, add the key from dhcpd.conf or makedhcp -n to create a new one"], errorcode => [1] }); return; } my $omapikey = $pent->{password}; my $usingipv6; my $nettab = xCAT::Table->new("networks"); my @vnets = $nettab->getAllAttribs('net', 'mgtifname', 'mask', 'dynamicrange', 'nameservers', 'ddnsdomain', 'domain'); foreach (@vnets) { if ($_->{net} =~ /:/) { $usingipv6 = 1; } } my ($nname, $ipaddr, $hwaddr) = _omshell_query_host($node, $settings, $omapikey, undef); if ($ipaddr) { push @{ $rsp->{data} }, "$nname: $ipaddr, $hwaddr"; xCAT::MsgUtils->message("I", $rsp, $callback); } if ($usingipv6) { my ($nname6, $ipaddr6, $hwaddr6) = _omshell_query_host($node, $settings, $omapikey, 7912); if ($ipaddr6) { push @{ $rsp->{data} }, "$nname6: $ipaddr6, $hwaddr6"; xCAT::MsgUtils->message("I", $rsp, $callback); } } } sub delnode { my $node = shift; my $inetn = inet_aton($node); if (_isc_static_host_fallback()) { _delete_isc_static_host($node); $restartdhcp = 1; return; } my $mactab = xCAT::Table->new('mac'); my $ent; if ($machash) { $ent = $machash->{$node}->[0]; } if ($ent and $ent->{mac}) { my @macs = split(/\|/, $ent->{mac}); my $mace; my $count = 0; foreach $mace (@macs) { my $mac; my $hname; ($mac, $hname) = split(/!/, $mace); unless ($hname) { $hname = $node; } #Default to hostname equal to nodename unless ($mac) { next; } #Skip corrupt format if (!grep /:/, $mac) { $mac = lc($mac); $mac =~ s/(\w{2})/$1:/g; $mac =~ s/:$//; } my $hostname = $hname; my %client_nethash = xCAT::DBobjUtils->getNetwkInfo([$node]); if ($client_nethash{$node}{mgtifname} =~ /hf/) { if (scalar(@macs) > 1) { if ($hname !~ /^(.*)-hf(.*)$/) { $hostname = $hname . "-hf" . $count; } else { $hostname = $1 . "-hf" . $count; } } } $count = $count + 2; unless ($hostname) { $hostname = $node; } print $omshell "new host\n"; print $omshell "set name = \"$hostname\"\n"; #Find and destroy conflict name print $omshell "open\n"; print $omshell "remove\n"; print $omshell "close\n"; if ($mac) { print $omshell "new host\n"; print $omshell "set hardware-address = " . $mac . "\n"; #find and destroy mac conflict print $omshell "open\n"; print $omshell "remove\n"; print $omshell "close\n"; } if ($inetn and _omapi_ip_lookup_supported()) { my $ip; if (inet_aton($hostname)) { $ip = inet_ntoa(inet_aton($hostname)); } if ($ip) { print $omshell "new host\n"; print $omshell "set ip-address = $ip\n"; #find and destroy ip conflict print $omshell "open\n"; print $omshell "remove\n"; print $omshell "close\n"; } } } } print $omshell "new host\n"; print $omshell "set name = \"$node\"\n"; #Find and destroy conflict name print $omshell "open\n"; print $omshell "remove\n"; print $omshell "close\n"; if ($inetn and _omapi_ip_lookup_supported()) { my $ip = inet_ntoa(inet_aton($node)); unless ($ip) { return; } print $omshell "new host\n"; print $omshell "set ip-address = $ip\n"; #find and destroy ip conflict print $omshell "open\n"; print $omshell "remove\n"; print $omshell "close\n"; } } sub addnode6 { #omshell to add host dynamically my $node = shift; unless ($vpdhash) { $callback->({ node => [ { name => [$node], warning => ["Skipping DHCPv6 setup due to missing vpd.uuid information."] } ] }); return; } my $ent = $vpdhash->{$node}->[0]; #tab->getNodeAttribs($node, [qw(mac)]); unless ($ent and $ent->{uuid}) { $callback->({ node => [ { name => [$node], warning => ["Skipping DHCPv6 setup due to missing vpd.uuid information."] } ] }); return; } #phase 1, dynamic and static addresses, hopefully ddns-hostname works, may be tricky to do 'send hostname' #since FQDN is the only thing to be sent down, and that RFC clearly suggests that the client #assembles that data, not host #tricky for us since the client wouldn't know it's hostname/fqdn in advance #unless acquired via IPv4 first #don't think dhclient is smart enough to assemble advertised domain with it's own name and then #request FQDN update #goal is simple enough, we want `hostname` to look sane *and* we want DNS to look right my $uuid = $ent->{uuid}; $uuid =~ s/-//g; $uuid =~ s/(..)/$1:/g; $uuid =~ s/:\z//; $uuid =~ s/^/00:04:/; my $ip = getipaddr($node); if ($ip and $ip =~ /:/ and ipIsDynamic($ip)) { $callback->({ error => ["Node $node has IPv6 address $ip which is inside the DHCP dynamic range. Move the node IP outside the dynamic range or adjust the range in the networks table."], errorcode => [1] }); return; } if ($ip and $ip =~ /:/) { $ip = getipaddr($ip, GetNumber => 1); $ip = $ip->as_hex; $ip =~ s/^0x//; $ip =~ s/(..)/$1:/g; $ip =~ s/:\z//; print $omshell6 "set ip-address = $ip\n"; } else { $ip = 0; } print $omshell6 "new host\n"; print $omshell6 "set name = \"$node\"\n"; #Find and destroy conflict name print $omshell6 "open\n"; print $omshell6 "remove\n"; print $omshell6 "close\n"; if ($ip) { print $omshell6 "new host\n"; print $omshell6 "set ip-address = $ip\n"; #find and destroy ip conflict print $omshell6 "open\n"; print $omshell6 "remove\n"; print $omshell6 "close\n"; } print $omshell6 "new host\n"; print $omshell6 "set dhcp-client-identifier = " . $uuid . "\n"; #find and destroy DUID-UUID conflict print $omshell6 "open\n"; print $omshell6 "remove\n"; print $omshell6 "close\n"; print $omshell6 "new host\n"; print $omshell6 "set name = \"$node\"\n"; print $omshell6 "set dhcp-client-identifier = $uuid\n"; print $omshell6 'set statements = "ddns-hostname \"' . $node . '\";";' . "\n"; if ($ip) { print $omshell6 "set ip-address = $ip\n"; } print $omshell6 "create\n"; print $omshell6 "close\n"; } sub addnode { #Use omshell to add the node. #the process used is blind typing commands that should work #it tries to delet any conflicting entries matched by name and #hardware address and ip address before creating a brand now one #unfortunate side effect: dhcpd.leases can look ugly over time, when #doing updates would keep it cleaner, good news, dhcpd restart cleans #up the lease file the way we would want anyway. my $node = shift; my $ent; my $nrent; my $chainent; my $ient; my $ntent; my $tftpserver; my $httpport="80"; my @hports=xCAT::TableUtils->get_site_attribute("httpport"); if ($hports[0]){ $httpport=$hports[0]; } my $portsuffix = ( $httpport eq "80" ) ? "" : ":$httpport"; if ($chainents and $chainents->{$node}) { $chainent = $chainents->{$node}->[0]; } if ($iscsients and $iscsients->{$node}) { $ient = $iscsients->{$node}->[0]; } if ($nodetypeents and $nodetypeents->{$node}) { $ntent = $nodetypeents->{$node}->[0]; } my $lstatements = $statements; my $guess_next_server = 0; my $nxtsrv; if ($nrhash) { $nrent = $nrhash->{$node}->[0]; if ($nrent and $nrent->{tftpserver} and $nrent->{tftpserver} ne '') { #check the value of inet_ntoa(inet_aton("")),if the hostname cannot be resolved, #the value of inet_ntoa() will be "undef", which will cause fatal error my $tmp_name = inet_aton($nrent->{tftpserver}); unless ($tmp_name) { #tell the reason to the user $callback->( { error => ["Unable to resolve the tftpserver for node"], errorcode => [1] } ); return; } $tftpserver = inet_ntoa($tmp_name); $nxtsrv = $tftpserver; $lstatements = _omapi_next_server_statement($tftpserver) . $statements; } else { $guess_next_server = 1; } if ($nrent->{netboot} and ($nrent->{netboot} eq 'petitboot' or $nrent->{netboot} eq 'onie' )) { if ($guess_next_server) { my $node_server = undef; if ($nrent->{xcatmaster}) { $node_server = $nrent->{xcatmaster}; } unless ($node_server) { my @nxtsrvd = xCAT::NetworkUtils->my_ip_facing($node); unless ($nxtsrvd[0]) { $nxtsrv = $nxtsrvd[1]; } elsif ($nxtsrvd[0] == 1) { $callback->({ error => [ $nxtsrvd[1] ] }); } else { $callback->({ error => ["Unable to determine the tftpserver for $node, verify \"xcatmaster\" is set correctly"], errorcode => [1] }); return; } } else { my $tmp_server = inet_aton($node_server); unless ($tmp_server) { $callback->({ error => ["Unable to resolve the tftpserver for $node, verify \"xcatmaster\" is set correctly"], errorcode => [1] }); return; } $nxtsrv = inet_ntoa($tmp_server); } unless ($nxtsrv) { $callback->({ error => ["Unable to determine the tftpserver for $node, verify \"xcatmaster\" is set correctly"], errorcode => [1] }); return; } $guess_next_server = 0; } } #else { # $nrent = $nrtab->getNodeAttribs($node,['servicenode']); # if ($nrent and $nrent->{servicenode}) { # $statements = 'next-server = \"'.inet_ntoa(inet_aton($nrent->{servicenode})).'\";'.$statements; # } #} } else { $guess_next_server = 1; } unless ($machash) { $callback->( { warning => ["Unable to open mac table, it may not exist yet"] } ); return; } $ent = $machash->{$node}->[0]; #tab->getNodeAttribs($node, [qw(mac)]); unless ($ent and $ent->{mac}) { $callback->( { warning => ["Unable to find mac address for $node"] } ); return; } my @macs = split(/\|/, $ent->{mac}); my $mace; my $deflstaments = $lstatements; my $count = 0; foreach $mace (@macs) { $lstatements = $deflstaments; #force recalc on every entry my $mac; my $hname; $hname = ""; ($mac, $hname) = split(/!/, $mace); unless ($hname) { $hname = $node; } #Default to hostname equal to nodename unless ($mac) { next; } #Skip corrupt format if ($mac !~ /^[0-9a-fA-F]{2}(-[0-9a-fA-F]{2}){5,8}$|^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5,8}$/) { $callback->( { error => ["Invalid mac address $mac for $node"], errorcode => [1] } ); next; } my $ip = getipaddr($hname, OnlyV4 => 1); if ($hname eq '*NOIP*') { $hname = $node . "-noip" . $mac; $hname =~ s/://g; $ip = 'DENIED'; # } #if 'guess_next_server', inherit from the network provided value... see how this pans out # if ($guess_next_server and $ip and $ip ne "DENIED") # { # $nxtsrv = xCAT::NetworkUtils->my_ip_facing($hname); # if ($nxtsrv) # { # $tftpserver = $nxtsrv; # $lstatements = "next-server $nxtsrv;$statements"; # } #of course, we set the xNBA variable to let that propogation carry forward into filename uri interpolation } elsif ($guess_next_server) { $nxtsrv = '${next-server}'; #if floating IP support, cause gPXE command-line expansion patch to drive inheritence from network } # The hostname could not be resolved, print a warning message if (!$ip) { $callback->( { warning => [ "The hostname $hname of node $node could not be resolved." ] } ); } if ($ip and $ip ne 'DENIED' and ipIsDynamic($ip)) { $callback->( { error => [ "Node $node has IP $ip which is inside the DHCP dynamic range. Move the node IP outside the dynamic range or adjust the range in the networks table." ], errorcode => [1] } ); next; } my $doiscsi = 0; if ($ient and $ient->{server} and $ient->{target}) { $doiscsi = 1; unless (defined($ient->{lun})) { #Some firmware fails to properly implement the spec, so we must explicitly say zero for such firmware $ient->{lun} = 0; } my $iscsirootpath = 'iscsi:' . $ient->{server} . ':6:3260:' . $ient->{lun} . ':' . $ient->{target}; if (defined($ient->{iname})) { #Attempt to use gPXE or IBM iSCSI formats to specify the initiator #This all goes on one line, but will break it out to at least be readable in here $lstatements = 'if option vendor-class-identifier = \"ISAN\" { ' #This is declared by IBM iSCSI initiators, will call it 'ISAN' mode . 'option isan.iqn \"' . $ient->{iname} . '\"; ' #Use vendor-spcefic option to declare the expected Initiator name . 'option isan.root-path \"' . $iscsirootpath . '\"; ' #We must *not* use standard root-path if using ISAN style options . '} else { ' . 'option root-path \"' . $iscsirootpath . '\"; ' #For everything but ISAN, use standard, RFC defined behavior for root . 'if exists gpxe.bus-id { ' #Since our iscsi-initiator-iqn is in no way a standardized thing, only use it for gPXE . ' option iscsi-initiator-iqn \"' . $ient->{iname} . '\";' #gPXE will consider option 203 for initiator IQN . '}' . '}' . $lstatements; print $lstatements; } else { #We stick to the good old RFC defined behavior, ISAN, gPXE, everyone should be content with this so long as no initiator name need be specified $lstatements = 'option root-path \"' . $iscsirootpath . '\";' . $lstatements; } } my $douefi = check_uefi_support($ntent); if ($nrent and $nrent->{netboot} and $nrent->{netboot} eq 'xnba' and $lstatements !~ /filename/) { if (-f "$tftpdir/xcat/xnba.kpxe") { if ($doiscsi and $chainent and $chainent->{currstate} and ($chainent->{currstate} eq 'iscsiboot' or $chainent->{currstate} eq 'boot')) { $lstatements = 'if option client-architecture = 00:00 and not exists gpxe.bus-id { filename = \"xcat/xnba.kpxe\"; } else { filename = \"\"; } ' . $lstatements; } else { # If proxydhcp daemon is enabled for windows deployment, do vendor-class-identifier of "PXEClient" to bump it over to proxydhcp.c if (($douefi == 2 and $chainent->{currstate} =~ /^install/) or $chainent->{currstate} =~ /^winshell/) { if (proxydhcp($nrent)) { #proxy dhcp required in uefi invocation $lstatements = 'if option client-architecture = 00:00 or option client-architecture = 00:07 or option client-architecture = 00:09 { filename = \"\"; option vendor-class-identifier \"PXEClient\"; } else { filename = \"\"; }' . $lstatements; #If proxydhcp daemon is enable, use it. } else { $lstatements = 'if option user-class-identifier = \"xNBA\" and option client-architecture = 00:00 { always-broadcast on; filename = \"http://' . $nxtsrv . $portsuffix . '/tftpboot/xcat/xnba/nodes/' . $node . '\"; } else if option client-architecture = 00:07 or option client-architecture = 00:09 { filename = \"\"; option vendor-class-identifier \"PXEClient\"; } else if option client-architecture = 00:00 { filename = \"xcat/xnba.kpxe\"; } else { filename = \"\"; }' . $lstatements; #Only PXE compliant clients should ever receive xNBA } } elsif ($douefi and $chainent->{currstate} ne "boot" and $chainent->{currstate} ne "iscsiboot") { $lstatements = 'if option user-class-identifier = \"xNBA\" and option client-architecture = 00:00 { always-broadcast on; filename = \"http://' . $nxtsrv . $portsuffix . '/tftpboot/xcat/xnba/nodes/' . $node . '\"; } else if option user-class-identifier = \"xNBA\" and (option client-architecture = 00:09 or option client-architecture = 00:07) { filename = \"http://' . $nxtsrv . $portsuffix . '/tftpboot/xcat/xnba/nodes/' . $node . '.uefi\"; } else if option client-architecture = 00:07 { filename = \"xcat/xnba.efi\"; } else if option client-architecture = 00:00 { filename = \"xcat/xnba.kpxe\"; } else { filename = \"\"; }' . $lstatements; #Only PXE compliant clients should ever receive xNBA } else { $lstatements = 'if option user-class-identifier = \"xNBA\" and option client-architecture = 00:00 { filename = \"http://' . $nxtsrv . $portsuffix . '/tftpboot/xcat/xnba/nodes/' . $node . '\"; } else if option client-architecture = 00:00 { filename = \"xcat/xnba.kpxe\"; } else { filename = \"\"; }' . $lstatements; #Only PXE compliant clients should ever receive xNBA } } } #TODO: warn when windows } elsif ($nrent and $nrent->{netboot} and $nrent->{netboot} eq 'pxe' and $lstatements !~ /filename/) { if (-f "$tftpdir/xcat/xnba.kpxe") { if ($doiscsi and $chainent and $chainent->{currstate} and ($chainent->{currstate} eq 'iscsiboot' or $chainent->{currstate} eq 'boot')) { $lstatements = 'if exists gpxe.bus-id { filename = \"\"; } else if exists client-architecture { filename = \"xcat/xnba.kpxe\"; } ' . $lstatements; } else { $lstatements = 'if option vendor-class-identifier = \"ScaleMP\" { filename = \"vsmp/pxelinux.0\"; } else { filename = \"pxelinux.0\"; }' . $lstatements; } } } elsif ($nrent and $nrent->{netboot} and $nrent->{netboot} eq 'yaboot') { $lstatements = 'filename = \"/yb/node/yaboot-' . $node . '\";' . $lstatements; } elsif ($nrent and $nrent->{netboot} and $nrent->{netboot} =~ /^grub2[-]?.*$/) { $lstatements = 'filename = \"/boot/grub2/grub2-' . $node . '\";' . $lstatements; } elsif ($nrent and $nrent->{netboot} and $nrent->{netboot} eq 'petitboot') { $lstatements = 'option conf-file \"http://' . $nxtsrv . $portsuffix . '/tftpboot/petitboot/' . $node . '\";' . $lstatements; } elsif ($nrent and $nrent->{netboot} and $nrent->{netboot} eq 'onie') { my $provmethod = $ntent->{provmethod}; if ($provmethod) { my $linuximagetab = xCAT::Table->new('linuximage'); my $imagetab = $linuximagetab->getAttribs({ imagename => $provmethod }, 'pkgdir'); if ($imagetab) { my $image_pkgdir = $imagetab->{'pkgdir'}; my @pkgdirs = split(/,/,$image_pkgdir); my $validpkgdir; foreach my $mypkgdir (@pkgdirs){ if (-f $mypkgdir) { $lstatements = 'if substring (option vendor-class-identifier,0,11) = \"onie_vendor\" { option www-server = \"http://' . $nxtsrv . $portsuffix . $mypkgdir . '\";}' . $lstatements; $validpkgdir = 1; last; } } unless ($validpkgdir) { $callback->({ warning => ["osimage $provmethod pkgdir doesn't exists"]}); } } else { $callback->({ warning => ["osimage $provmethod is not defined in the osimage table"]}); } } else { $callback->({ warning => ["provmethod is not defined for $node"]}); } } elsif ($nrent and $nrent->{netboot} and $nrent->{netboot} eq 'nimol') { $lstatements = 'supersede server.filename=\"/vios/nodes/' . $node . '\";' . $lstatements; } if ($^O eq 'aix') { addnode_aix($ip, $mac, $hname, $tftpserver); } else { if (!grep /:/, $mac) { $mac = lc($mac); $mac =~ s/(\w{2})/$1:/g; $mac =~ s/:$//; } my $hostname = $hname; my $hardwaretype = 1; my %client_nethash = xCAT::DBobjUtils->getNetwkInfo([$node]); if ($client_nethash{$node}{mgtifname} =~ /hf/) { $hardwaretype = 37; if (scalar(@macs) > 1) { if ($hname !~ /^(.*)-hf(.*)$/) { $hostname = $hname . "-hf" . $count; } else { $hostname = $1 . "-hf" . $count; } } } elsif (length($mac) == 23 || length($mac) == 26) { # 8 or 9 bytes of mac address # Currently the only thing that has 8 or 9 bytes is an infiniband # or infiniband like device, which is type 32 (0x20). $hardwaretype = 32; } if (_isc_static_host_fallback()) { if ($ip ne "DENIED") { if ($lstatements) { $lstatements = 'ddns-hostname \"' . $node . '\"; send host-name \"' . $node . '\";' . $lstatements; } else { $lstatements = 'ddns-hostname \"' . $node . '\"; send host-name \"' . $node . '\";'; } } else { $lstatements = "deny booting;"; } _add_isc_static_host($node, $hostname, $mac, $ip, $lstatements); $count = $count + 2; next; } #syslog("local4|err", "Setting $node ($hname|$ip) to " . $mac); if (_omapi_pre_create_cleanup_supported()) { print $omshell "new host\n"; print $omshell "set name = \"$hostname\"\n"; #Find and destroy conflict name print $omshell "open\n"; print $omshell "remove\n"; print $omshell "close\n"; } if ($ip and $ip ne 'DENIED' and _omapi_ip_lookup_supported()) { print $omshell "new host\n"; print $omshell "set ip-address = $ip\n"; #find and destroy ip conflict print $omshell "open\n"; print $omshell "remove\n"; print $omshell "close\n"; } if (_omapi_pre_create_cleanup_supported()) { print $omshell "new host\n"; print $omshell "set hardware-address = " . $mac . "\n"; #find and destroy mac conflict print $omshell "open\n"; print $omshell "remove\n"; print $omshell "close\n"; } print $omshell "new host\n"; print $omshell "set name = \"$hostname\"\n"; print $omshell "set hardware-address = " . $mac . "\n"; print $omshell "set dhcp-client-identifier = " . $mac . "\n"; print $omshell "set hardware-type = $hardwaretype\n"; if ($ip eq "DENIED") { #Blacklist this mac to preclude confusion, give best shot at things working print $omshell "set statements = \"deny booting;\"\n"; } else { if ($ip) { print $omshell "set ip-address = $ip\n"; } if ($lstatements) { $lstatements = 'ddns-hostname \"' . $node . '\"; send host-name \"' . $node . '\";' . $lstatements; } else { $lstatements = 'ddns-hostname \"' . $node . '\"; send host-name \"' . $node . '\";'; } print $omshell "set statements = \"$lstatements\"\n"; } print $omshell "create\n"; print $omshell "close\n"; unless ($::XCATSITEVALS{externaldhcpservers}) { unless (grep /#definition for host $node aka host $hostname/, @dhcpconf) { push @dhcpconf, "#definition for host $node aka host $hostname can be found in the dhcpd.leases file (typically /var/lib/dhcpd/dhcpd.leases)\n"; } } } $count = $count + 2; } } sub addrangedetection { my $net = shift; my $tranges = $net->{dynamicrange}; #temp range, the dollar sign makes it look strange my $myip; my @myipd = xCAT::NetworkUtils->my_ip_facing($net->{net}); unless ($myipd[0]) { $myip = $myipd[1]; } # convert to nameserver IP if ($net->{nameservers} eq '') { $netcfgs{ $net->{net} }->{nameservers} = $myip; } else { $netcfgs{ $net->{net} }->{nameservers} = $net->{nameservers}; } $netcfgs{ $net->{net} }->{ddnsdomain} = $net->{ddnsdomain}; $netcfgs{ $net->{net} }->{domain} = $net->{domain}; unless ($netcfgs{ $net->{net} }->{nameservers}) { # convert to nameserver IP if ($::XCATSITEVALS{nameservers} eq '') { $netcfgs{ $net->{net} }->{nameservers} = $myip; } else { $netcfgs{ $net->{net} }->{nameservers} = $::XCATSITEVALS{nameservers}; } } foreach my $range_entry ( @{ xCAT::DHCP::Range->parse_dynamic_ranges($tranges) } ) { my $isc_range = xCAT::DHCP::Range->isc_range($range_entry); $netcfgs{ $net->{net} }->{range} = $isc_range; my ( $begin_number, $end_number ) = xCAT::DHCP::Range->bounds($range_entry); $dynamicranges{$isc_range} = [ $begin_number, $end_number ] if defined($begin_number) && defined($end_number); } } ###################################################### # Add nodes into dhcpsd.cnf. For AIX only ###################################################### sub addnode_aix { my $ip = shift; my $mac = shift; my $hname = shift; my $tftpserver = shift; $restartdhcp = 1; # Format the mac address to aix $mac =~ s/://g; $mac = lc($mac); delnode_aix($hname); #Find the location to insert node my $isSubnetFound = 0; my $i; my $netmask; for ($i = 0 ; $i < scalar(@dhcpconf) ; $i++) { if ($dhcpconf[$i] =~ /([\d\.]+)\/(\d+) ip configuration end/) { if (xCAT::NetworkUtils::isInSameSubnet($ip, $1, $2, 1)) { $isSubnetFound = 1; $netmask = $2; last; } } } # Format the netmask from AIX format (24) to Linux format (255.255.255.0) my $netmask_linux = xCAT::NetworkUtils::formatNetmask($netmask, 1, 0); # Create node section my @node_section = (); push @node_section, " client 1 $mac $ip #node $hname start\n"; push @node_section, " {\n"; push @node_section, " option 1 $netmask_linux\n"; push @node_section, " option 12 $hname\n"; # push @node_section, " option sa $tftpserver\n"; # push @node_section, " option bf \"/tftpboot/$hname\"\n"; push @node_section, " } # node $hname end\n"; if ($isSubnetFound) { splice @dhcpconf, $i, 0, @node_section; } } ################################################### # Delete nodes in dhcpsd.cnf. For AIX only ################################################### sub delnode_aix { my $hname = shift; my $i; my $node_start = 0; my $node_end = 0; for ($i = 0 ; $i < scalar(@dhcpconf) ; $i++) { if ($dhcpconf[$i] =~ /node $hname start/) { $node_start = $i; } elsif ($dhcpconf[$i] =~ /node $hname end/) { $node_end = $i; last; } } if ($node_start && $node_end) { $restartdhcp = 1; splice @dhcpconf, $node_start, ($node_end - $node_start + 1); return 1; } else { return 0; } } ############################################################ # check_options will process the options for makedhcp and # give a usage error for any invalid options ############################################################ sub check_options { my $req = shift; my $opt = shift; my $callback = shift; my $rc = 0; # Exit if the packet has been preprocessed # Comment this line to make sure check_options can be processed on service node. if ($req->{_xcatpreprocessed}->[0] == 1) { return 0; } # display the usage if -h if ($opt->{h}) { my $rsp = {}; $rsp->{data}->[0] = $usage; xCAT::MsgUtils->message("I", $rsp, $callback, 0); return 0; } # check to see if -q is listed with any other options which is not allowed if ($opt->{q} and ($opt->{a} || $opt->{d} || $opt->{n} || $opt->{r} || $opt->{l} || $statements)) { my $rsp = {}; $rsp->{data}->[0] = "The -q option cannot be used with other options."; xCAT::MsgUtils->message("E", $rsp, $callback, 1); return 1; } # check to see if -n is listed with any other options which is not allowed if ($opt->{n} and ($opt->{a} || $opt->{d} || $opt->{q} || $opt->{r} || $opt->{l} || $statements)) { my $rsp = {}; $rsp->{data}->[0] = "The -n option cannot be used with other options."; xCAT::MsgUtils->message("E", $rsp, $callback, 1); return 1; } unless (($req->{arg} and (@{ $req->{arg} } > 0)) or $req->{node}) { my $rsp = {}; $rsp->{data}->[0] = $usage; xCAT::MsgUtils->message("I", $rsp, $callback, 1); return; } return 0; } ############################################################ # preprocess_request will perform syntax checking and do basic precess checking ############################################################ sub preprocess_request { my $req = shift; my $callback = shift; my $rc = 0; #>>>>>>>used for trace log>>>>>>> my $verbose_on_off = 0; Getopt::Long::Configure("bundling"); $Getopt::Long::ignorecase = 0; Getopt::Long::Configure("no_pass_through"); # Exit if the packet has been preprocessed if ($req->{_xcatpreprocessed}->[0] == 1) { return [$req]; } # Save the arguements in ARGV for GetOptions if ($req && $req->{arg}) { @ARGV = @{ $req->{arg} }; } else { @ARGV = (); } my %opt; # Parse the options for makedhcp if (!GetOptions( 'h|help' => \$opt{h}, 'a' => \$opt{a}, 'd' => \$opt{d}, 'l|localonly' => \$localonly, 'n' => \$opt{n}, 'r' => \$opt{r}, 's=s' => \$statements, # $statements is declared globally 'q' => \$opt{q}, 'V' => \$opt{V} #>>>>>>>used for trace log>>>>>>> )) { # If the arguements do not pass GetOptions then issue error message and return my $rsp = {}; $rsp->{data}->[0] = $usage; xCAT::MsgUtils->message("E", $rsp, $callback, 1); return 1; } #>>>>>>>used for trace log>>>>>>> if ($opt{V}) { $verbose_on_off = 1; } # check the syntax $rc = check_options($req, \%opt, $callback); if ($rc) { xCAT::MsgUtils->trace($verbose_on_off, "e", "dhcp: command syntax error"); return []; } my $snonly = 0; my @entries = xCAT::TableUtils->get_site_attribute("disjointdhcps"); my $t_entry = $entries[0]; if (defined($t_entry)) { $snonly = $t_entry; } xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: disjointdhcps=$t_entry"); my @requests = (); my $hasHierarchy = 0; my @snlist = xCAT::ServiceNodeUtils->getSNList('dhcpserver'); if (@snlist > 0) { $hasHierarchy = 1; } xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: hasHierarchy=$hasHierarchy"); my @dhcpsvrs = (); if ($hasHierarchy) { #hierarchy detected, enforce more rigorous sanity my $ntab = xCAT::Table->new('networks'); if ($ntab) { foreach (@{ $ntab->getAllEntries() }) { next unless ($_->{dynamicrange}); # if dynamicrange specified but dhcpserver was not - issue error message unless ($_->{dhcpserver}) { $callback->({ error => [ "Hierarchy requested, therefore networks.dhcpserver must be set for net=" . $_->{net} . "" ], errorcode => [1] }); return []; } push @dhcpsvrs, $_->{dhcpserver} if (xCAT::NetworkUtils->nodeonmynet($_->{dhcpserver})); xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: dhcp server on $_->{net}: $_->{dhcpserver}"); } } } elsif ($snonly == 1) { $snonly = 0; xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: disjointdhcps mode is disabled as no service nodes are running dhcp service."); } my @nodes = (); # if the new option is not specified if (!$opt{n}) { # save the node names specified if ($req->{node}) { @nodes = @{ $req->{node} }; } # if option all elsif ($opt{a}) { # if option delete - Delete all node entries, that were added by xCAT, from the DHCP server configuration. if ($opt{d}) { my $nodelist = xCAT::Table->new('nodelist'); my @entries = ($nodelist->getAllNodeAttribs([qw(node)])); foreach (@entries) { push @nodes, $_->{node}; } } # Delete not specified so only add - Define all nodes to the DHCP server else { my $mactab = xCAT::Table->new('mac'); my @entries = (); if ($mactab) { @entries = ($mactab->getAllNodeAttribs([qw(mac)])); } foreach (@entries) { push @nodes, $_->{node}; } } } # end - if -a # don't put compute node entries in for AIX nodes # this is handled by NIM - duplicate entires will cause # an error if ($^O eq 'aix') { my @tmplist; my $Imsg; foreach my $n (@nodes) { # get the nodetype for each node my $ntable = xCAT::Table->new('nodetype'); if ($ntable) { my $mytype = $ntable->getNodeAttribs($n, ['nodetype']); if ($mytype->{nodetype} =~ /osi/) { $Imsg++; } # if its aix and not "osi" then add it to the list of nodes unless ($mytype->{nodetype} =~ /osi/) { push @tmplist, $n; } } } # replace nodes with the tmplist of nodes that are not osi nodetype @nodes = @tmplist; # if any nodes were found with a ndoetype of osi - issue message that they are handled by NIM if ($Imsg) { my $rsp; push @{ $rsp->{data} }, "AIX nodes with a nodetype of \'osi\' will not be added to the dhcp configuration file. This is handled by NIM.\n"; xCAT::MsgUtils->message("I", $rsp, $callback); } } } #>>>>>>>used for trace log>>>>>>> my $str_node = join(" ", @nodes); xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: nodes are $str_node"); my $issn = xCAT::Utils->isServiceNode(); #check if this node is the service node for any input node my @hostinfo = xCAT::NetworkUtils->determinehostname(); my %iphash = (); # flag the hostnames in iphash foreach (@hostinfo) { $iphash{$_} = 1; } # If service node and not -n option if (($snonly == 1) && (!$opt{n})) { # if a list of nodes are specified if (@nodes > 0) { # get the hash of service nodes my $sn_hash = xCAT::ServiceNodeUtils->getSNformattedhash(\@nodes, "xcat", "MN"); # if processing only on the local host if ($localonly) { # compare the service node hash with the iphash - a match adds this service node foreach (keys %$sn_hash) { if (exists($iphash{$_})) { my $reqcopy = {%$req}; $reqcopy->{'node'} = $sn_hash->{$_}; $reqcopy->{'_xcatdest'} = $_; $reqcopy->{_xcatpreprocessed}->[0] = 1; push @requests, $reqcopy; last; } } } else { my $handled4me = 0; # indicate myself is already handled. my %prehandledhash = ();# the servers which is already handled. foreach (@dhcpsvrs) { # It is required to handle the whole noderange for the dhcp server which serving dynamic range if (! exists($iphash{$_})) { my $reqcopy = {%$req}; $reqcopy->{'node'} = \@nodes; $reqcopy->{'_xcatdest'} = $_; $reqcopy->{_xcatpreprocessed}->[0] = 1; push @requests, $reqcopy; $prehandledhash{$_} = 1; } elsif ($handled4me == 0) { my $reqcopy = {%$req}; $reqcopy->{'node'} = \@nodes; $reqcopy->{_xcatpreprocessed}->[0] = 1; push @requests, $reqcopy; $handled4me = 1; } } # create a request for each service node foreach (keys %$sn_hash) { # to check if the SN already handled next if (exists($prehandledhash{$_})); if (! exists($iphash{$_})) { my $reqcopy = {%$req}; $reqcopy->{'node'} = $sn_hash->{$_}; $reqcopy->{'_xcatdest'} = $_; $reqcopy->{_xcatpreprocessed}->[0] = 1; push @requests, $reqcopy; } elsif ($handled4me == 0) { my $reqcopy = {%$req}; $reqcopy->{'node'} = $sn_hash->{$_}; $reqcopy->{_xcatpreprocessed}->[0] = 1; push @requests, $reqcopy; $handled4me = 1; } } } } # list of nodes specified # if new specified or there are nodes } # end if service node only and NOT -n option # if -n option or nodes were specified elsif (@nodes > 0 or $opt{n}) { #send the request to every dhservers $req->{'node'} = \@nodes; $req->{_xcatpreprocessed}->[0] = 1; @requests = ({%$req}); #Start with a straight copy to reflect local instance # if not localonly - get list of service nodes and create requests unless ($localonly || $hasHierarchy == 0) { # If running on SN, get site.master from DB and dispatch to it as MN will not be in SN list. if ( $issn ) { my @entries = xCAT::TableUtils->get_site_attribute("master"); my $reqcopy = {%$req}; $reqcopy->{'_xcatdest'} = $entries[0]; push @requests, $reqcopy; } # A request naming nodes only concerns the service nodes that serve # them. Regenerating the networks still reaches every dhcp server, # since a dynamic range is not tied to a node. If none of the named # nodes can be mapped to a service node, keep sending to all of them # rather than risk leaving one out. my %servingsn = (); unless ($opt{n}) { my $sn_hash = xCAT::ServiceNodeUtils->getSNformattedhash(\@nodes, "xcat", "MN"); foreach my $sn (keys %$sn_hash) { $servingsn{$sn} = 1; } } foreach my $s (@snlist) { if (scalar @nodes == 1 and $nodes[0] eq $s) { next; } next if ($issn && exists($iphash{$s})); next if (keys(%servingsn) && !exists($servingsn{$s})); my $reqcopy = {%$req}; $reqcopy->{'_xcatdest'} = $s; push @requests, $reqcopy; } } } #print Dumper(@requests); return \@requests; } ############################################################################# # process_request will perform syntax checking and do basic process checkingi # and call other functions to complete the request to add or delete entries ############################################################################# sub process_request { my $req = shift; $callback = shift; my $oldmask = umask 0077; $restartdhcp = 0; my $rsp; #print Dumper($req); #>>>>>>>used for trace log>>>>>>> my $verbose_on_off = 0; Getopt::Long::Configure("bundling"); $Getopt::Long::ignorecase = 0; Getopt::Long::Configure("no_pass_through"); # Save the arguements in ARGV for GetOptions if ($req && $req->{arg}) { @ARGV = @{ $req->{arg} }; } else { @ARGV = (); } my %opt; # Parse the options for makedhcp if (!GetOptions( 'h|help' => \$opt{h}, 'a' => \$opt{a}, 'd' => \$opt{d}, 'l|localonly' => \$localonly, 'n' => \$opt{n}, 'r' => \$opt{r}, 's=s' => \$statements, # $statements is declared globally 'q' => \$opt{q}, 'V' => \$opt{V} #>>>>>>>used for trace log>>>>>>> )) { # If the arguements do not pass GetOptions then issue error message and return my $rsp = {}; $rsp->{data}->[0] = $usage; xCAT::MsgUtils->message("E", $rsp, $callback, 1); return 1; } #>>>>>>>used for trace log>>>>>>> if ($opt{V}) { $verbose_on_off = 1; } # Check options again in case we are called from plugin and options have not been processed my $rc = 0; $rc = check_options($req, \%opt, $callback); if ($rc) { xCAT::MsgUtils->trace($verbose_on_off, "e", "dhcp: there is invalid option in command"); return []; } my $backend = xCAT::DHCP::Backend->new_backend(check_available => 1); if ( ref($backend) eq 'HASH' && $backend->{error} ) { my $rsp = {}; $rsp->{data}->[0] = $backend->{error}; xCAT::MsgUtils->message("E", $rsp, $callback, 1); return; } if ( $backend->name eq 'kea' && $statements ) { my $rsp = {}; $rsp->{data}->[0] = "The -s option contains ISC DHCP statement text and is not supported with the Kea DHCP backend."; xCAT::MsgUtils->message("E", $rsp, $callback, 1); return; } # if current node is a servicenode, make sure that it is also a dhcpserver my $isok = 1; if (xCAT::Utils->isServiceNode()) { $isok = 0; my @hostinfo = xCAT::NetworkUtils->determinehostname(); my %iphash = (); foreach (@hostinfo) { $iphash{$_} = 1; } my @sn = xCAT::ServiceNodeUtils->getSNList('dhcpserver'); foreach my $s (@sn) { if (exists($iphash{$s})) { $isok = 1; } } } if ($isok == 0) { #do nothing if it is a service node, but not dhcpserver xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: it is a service node, but not dhcpserver. Do nothing"); print "Do nothing\n"; return; } # if not -n, dhcp service needs to be running if (!($opt{n})) { if (xCAT::Utils->isLinux()) { my $ret = 0; my $service_error = "dhcp server is not running. please start the dhcp server."; if ( $backend->name eq 'kea' ) { my $status = $backend->check_services(); $ret = $status->{error} ? 1 : 0; $service_error = $status->{error} if $status->{error}; } else { $ret = xCAT::Utils->checkservicestatus("dhcp"); } if ($ret != 0) { my $rsp = {}; $rsp->{data}->[0] = $service_error; xCAT::MsgUtils->message("E", $rsp, $callback, 1); return; } } else { # AIX my @output = xCAT::Utils->runcmd("lssrc -s dhcpsd ", -1); if ($::RUNCMD_RC != 0) { # not running my $rsp = {}; $rsp->{data}->[0] = "dhcpsd is not running. Run startsrc -s dhcpsd and rerun your command."; xCAT::MsgUtils->message("E", $rsp, $callback, 1); return; } else { # check the status # the return output varies, sometime status is the third sometimes the 4th col if (grep /inoperative/, @output) { my $rsp = {}; $rsp->{data}->[0] = "dhcpsd is not running. Run startsrc -s dhcpsd and rerun your command."; xCAT::MsgUtils->message("E", $rsp, $callback, 1); return; } } } } # if option is query then call listnode for each node and return if ($opt{q}) { if ( $backend->name eq 'kea' ) { kea_process_request($backend, $req, \%opt, {}, $verbose_on_off); return; } # call listnode for each node requested foreach my $node (@{ $req->{node} }) { listnode($node, $callback); } return; } my $servicenodetab = xCAT::Table->new('servicenode'); my @nodeinfo = xCAT::NetworkUtils->determinehostname; my $nodename = pop @nodeinfo; # get hostname my $dhcpinterfaces = $servicenodetab->getNodeAttribs($nodename, ['dhcpinterfaces']); my %activenics; my $querynics = 1; if (xCAT::Utils->isServiceNode() and $dhcpinterfaces and $dhcpinterfaces->{dhcpinterfaces}) { # The keyword 'noboot' was appended to the NICs that doesn't need to reply DHCP configuration file, only used for mknb at present. $dhcpinterfaces->{dhcpinterfaces} =~ s/:noboot//g; my @dhcpifs = split ',', $dhcpinterfaces->{dhcpinterfaces}; foreach my $nic (@dhcpifs) { $activenics{$nic} = 1; $querynics = 0; } } else { my @entries = xCAT::TableUtils->get_site_attribute("dhcpinterfaces"); my $t_entry = $entries[0]; unless (defined($t_entry)) { #LEGACY: singular keyname for old style site value @entries = xCAT::TableUtils->get_site_attribute("dhcpinterface"); $t_entry = $entries[0]; } if (defined($t_entry)) #syntax should be like host|ifname1,ifname2;host2|ifname3,ifname2 etc or simply ifname,ifname2 #depending on complexity of network wished to be described { my $dhcpinterfaces = $t_entry; # The keyword 'noboot' was appended to the NICs that doesn't need to reply DHCP configuration file, only used for mknb at present. $dhcpinterfaces =~ s/:noboot//g; my $dhcpif; INTF: foreach $dhcpif (split /;/, $dhcpinterfaces) { my $host; my $savehost; my $foundself = 1; if ($dhcpif =~ /\|/) { $foundself = 0; (my $ngroup, $dhcpif) = split /\|/, $dhcpif; foreach $host (noderange($ngroup)) { $savehost = $host; unless (xCAT::NetworkUtils->thishostisnot($host)) { $foundself = 1; last; } } if (!defined($savehost)) { # host not defined in db, # probably management node unless (xCAT::NetworkUtils->thishostisnot($ngroup)) { $foundself = 1; } } } unless ($foundself) { next INTF; } foreach (split /[,\s]+/, $dhcpif) { $activenics{$_} = 1; $querynics = 0; } } } } # site nameservers/ntpservers/logservers/domain must be read regardless of how the DHCP # interfaces were determined. They previously lived inside the `else` branch above, so a # service node whose interfaces come from servicenode.dhcpinterfaces (the `if` branch) # never read them -- leaving $site_domain empty and making the Kea backend abort with # "No domain defined for entry in networks table, and no domain defined in site # table." Hoisted out so both branches populate them. my @entries = xCAT::TableUtils->get_site_attribute("nameservers"); my $t_entry = $entries[0]; if (defined($t_entry)) { $sitenameservers = $t_entry; } @entries = xCAT::TableUtils->get_site_attribute("ntpservers"); $t_entry = $entries[0]; if (defined($t_entry)) { $sitentpservers = $t_entry; } @entries = xCAT::TableUtils->get_site_attribute("logservers"); $t_entry = $entries[0]; if (defined($t_entry)) { $sitelogservers = $t_entry; } @entries = xCAT::TableUtils->get_site_attribute("domain"); $t_entry = $entries[0]; unless (defined($t_entry)) { # this may not be an error # $callback->( # {error => ["No domain defined in site tabe"], errorcode => [1]} # ); # return; } else { $site_domain = $t_entry; } xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: sitelogservers=$sitelogservers sitentpservers=$sitentpservers sitenameservers=$sitenameservers site_domain=$site_domain"); if ( $backend->name eq 'kea' ) { kea_process_request($backend, $req, \%opt, \%activenics, $verbose_on_off); return; } @dhcpconf = (); @dhcp6conf = (); my $dhcplockfd; open($dhcplockfd, ">", "/tmp/xcat/dhcplock"); flock($dhcplockfd, LOCK_EX); if ($::XCATSITEVALS{externaldhcpservers}) { # do nothing if remote dhcpservers at this point xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: remote dhcpservers at this point, do nothing"); } elsif ($opt{n}) { if (-e $dhcpconffile) { if ($^O eq 'aix') { # save NIM aix entries - to be restored later my $aixconf; open($aixconf, $dhcpconffile); if ($aixconf) { my $save = 0; while (<$aixconf>) { if ($save) { push @aixcfg, $_; } if ($_ =~ /#Network configuration end\n/) { $save++; } } close($aixconf); } $restartdhcp = 1; @dhcpconf = (); } my $rsp; push @{ $rsp->{data} }, "Renamed existing dhcp configuration file to $dhcpconffile.xcatbak\n"; xCAT::MsgUtils->message("I", $rsp, $callback); my $bakname = "$dhcpconffile.xcatbak"; rename("$dhcpconffile", $bakname); xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: Renamed existing dhcp configuration file to $dhcpconffile.xcatbak"); } } else { xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: load dhcp config file $dhcpconffile"); my $rconf; open($rconf, $dhcpconffile); # Read file into memory if ($rconf) { while (<$rconf>) { push @dhcpconf, $_; } close($rconf); } unless ($dhcpconf[0] =~ /^#xCAT/) { #Discard file if not xCAT originated, like 1.x did $restartdhcp = 1; @dhcpconf = (); } if ($dhcp6conffile and -e $dhcp6conffile) { open($rconf, $dhcp6conffile); while (<$rconf>) { push @dhcp6conf, $_; } close($rconf); } unless ($dhcp6conf[0] =~ /^#xCAT/) { #Discard file if not xCAT originated $restartdhcp6 = 1; @dhcp6conf = (); } } my $nettab = xCAT::Table->new("networks"); my @vnets = $nettab->getAllAttribs('net', 'mgtifname', 'mask', 'dynamicrange', 'nameservers', 'ddnsdomain', 'domain'); # get a list of all domains listed in xCAT network defs # - include the site domain - if any my $nettab = xCAT::Table->new("networks"); my @doms = $nettab->getAllAttribs('domain'); foreach my $netdom (@doms) { if ($netdom->{domain}) { push(@alldomains, $netdom->{domain}) unless grep(/^$netdom->{domain}$/, @alldomains); } } $nettab->close; # add the site domain if ($site_domain) { if (!grep(/^$site_domain$/, @alldomains)) { push(@alldomains, $site_domain); } } foreach (@vnets) { if ($_->{net} =~ /:/) { #IPv6 detected $usingipv6 = 1; } addrangedetection($_); #add to hash for remembering whether a node has a static address or just happens to live dynamically } if ($^O eq 'aix') { @nrn = xCAT::NetworkUtils::get_subnet_aix(); } else { foreach my $route (local_ipv4_routes()) { push @nrn, join(':', @{$route}); } my @ip6routes = `ip -6 route`; foreach (@ip6routes) { #TODO: filter out multicast? Don't know if multicast groups *can* appear in ip -6 route... #ignore link-local, global-local, junk, and routed networks if (/^default/ or /^fe80::\/64/ or /^2002::\/64/ or /^unreachable/ or /^[^ ]+ via/) { next; } my @parts = split /\s+/; push @nrn6, { net => $parts[0], iface => $parts[2] }; } } foreach (@vnets) { #TODO: v6 relayed networks? my $n = $_->{net}; my $if = $_->{mgtifname}; my $nm = $_->{mask}; if ($if =~ /!remote!/ and $n !~ /:/) { #only take in networks with special interface, but only v4 for now push @nrn, "$n:$if:$nm"; } } if ($querynics) { # Use netstat to determine activenics only when no site ent. # TODO: IPv6 auto-detect, or just really really insist people define dhcpinterfaces or suffer doom? foreach (@nrn) { my @ent = split /:/; my $firstoctet = $ent[0]; $firstoctet =~ s/^(\d+)\..*/$1/; if ($ent[0] eq "169.254.0.0" or ($firstoctet >= 224 and $firstoctet <= 239) or $ent[0] eq "127.0.0.0" or $ent[0] eq '127') { next; } my $netif = $ent[1]; if ($netif =~ /!remote!\S+/) { $netif =~ s/!remote!\s*(.*)$/$1/; } # Bridge nics if ((-f "/usr/sbin/brctl") || (-f "/sbin/brctl")) { #system "brctl showmacs $ent[1] 2>&1 1>/dev/null"; system "brctl showmacs $netif 2>&1 1>/dev/null"; if ($? == 0) { #$activenics{$ent[1]} = 1; $activenics{$netif} = 1; next; } } #if ($ent[1] =~ m/(remote|ipoib|ib|vlan|bond|eth|myri|man|wlan|en\S*\d+|em\S*\d+)/) if ($netif =~ m/(remote|ipoib|ib|vlan|bond|eth|myri|man|wlan|en\S*\d+|em\S*\d+)/) { #Mask out many types of interfaces, like xCAT 1.x #$activenics{$ent[1]} = 1; $activenics{$netif} = 1; } } } if ($^O ne 'aix') { my $os = xCAT::Utils->osver(); #add the active nics to /etc/sysconfig/dhcpd or /etc/default/dhcp3-server(ubuntu) my $dhcpver; my %missingfiles = ("dhcpd" => 1, "dhcpd6" => 1, "dhcp3-server" => 1); foreach $dhcpver ("dhcpd", "dhcpd6", "dhcp3-server", "isc-dhcp-server") { # if ipv6 is not present, no need to look at dhcpd6 files if (!$usingipv6 and $dhcpver eq "dhcpd6") { delete($missingfiles{"dhcpd6"}); next; } # check the possible system config paths for the various Linux O/S my $syspath; foreach $syspath ("/etc/sysconfig", "/etc/default") { my $generatedpath = "$syspath/$dhcpver"; my $dhcpd_key = "DHCPDARGS"; # For SLES11+ and RHEL7+ Operating system releases, the # dhcpd/dhcpd6 configuration is stored in the same file if (dhcpd_sysconfig_uses_interface_key($os)) { $dhcpd_key = "DHCPD_INTERFACE"; if ($usingipv6 and $dhcpver eq "dhcpd6") { $dhcpd_key = "DHCPD6_INTERFACE"; $generatedpath = "$syspath/dhcpd"; } } if ($generatedpath and -e "$generatedpath") { # remove the file from the hash because it will be processed if ($dhcpver eq "dhcpd") { # If dhcpd is found, then not necessary to find dhcp3-server delete($missingfiles{"dhcp3-server"}); } # UBUNTU/DEBIAN specific if ($dhcpver eq "isc-dhcp-server") { # UBUNTU/DEBIAN configuration ipv6 & ipv4 uses the isc-dhcp-server # remove all other from the missingfiles hash delete($missingfiles{"dhcpd"}); delete($missingfiles{"dhcpd6"}); delete($missingfiles{"dhcp3-server"}); $dhcpd_key = "INTERFACES"; } delete($missingfiles{$dhcpver}); open DHCPD_FD, "$generatedpath"; my $syscfg_dhcpd = ""; my $found = 0; my $ifarg = "$dhcpd_key=\""; foreach (keys %activenics) { if (/!remote!/) { next; } $ifarg .= " $_"; } $ifarg =~ s/\=\" /\=\"/; $ifarg .= "\"\n"; while () { if ($_ =~ m/^$dhcpd_key/) { $found = 1; $syscfg_dhcpd .= $ifarg; } else { $syscfg_dhcpd .= $_; } } if ($found eq 0) { $syscfg_dhcpd .= $ifarg; } close DHCPD_FD; # write out the new file with the interfaces defined open DBG_FD, '>', "$generatedpath"; print DBG_FD $syscfg_dhcpd; close DBG_FD; } } } if ($usingipv6) { # sles11.3 and rhels7 has dhcpd and dhcpd6 config in the dhcp file if (dhcpd_sysconfig_uses_interface_key($os)) { if ($missingfiles{dhcpd}) { $callback->({ error => ["The file /etc/sysconfig/dhcpd doesn't exist, check the dhcp server"] }); } } else { if ($missingfiles{dhcpd6}) { $callback->({ error => ["The file /etc/sysconfig/dhcpd6 doesn't exist, check the dhcp server"] }); } } } if ($missingfiles{dhcpd}) { $callback->({ error => ["The file /etc/sysconfig/dhcpd doesn't exist, check the dhcp server"] }); } } unless ($dhcpconf[0]) { #populate an empty config with some starter data... $restartdhcp = 1; newconfig(); } if ($usingipv6 and not $dhcp6conf[0]) { $restartdhcp6 = 1; newconfig6(); } if ($^O ne 'aix') { foreach (keys %activenics) { addnic($_, \@dhcpconf); if ($usingipv6) { addnic($_, \@dhcp6conf); } } } #need to transfer CEC/Frame to FSPs/BPAs my @inodes = (); my @validnodes = (); my $pnode; my $cnode; if ($req->{node}) { #@inodes = split /,/,${$req->{noderange}}; my $typehash = xCAT::DBobjUtils->getnodetype(\@{ $req->{node} }); foreach $pnode (@{ $req->{node} }) { my $ntype = $$typehash{$pnode}; if ($ntype =~ /^(cec|frame)$/) { $cnode = xCAT::DBobjUtils->getchildren($pnode); foreach (@$cnode) { push @validnodes, $_; } } else { push @validnodes, $pnode; } } $req->{node} = \@validnodes; } if ((!$req->{node}) && ($opt{a})) { if ($opt{d}) #delete all entries { $req->{node} = []; my $nodelist = xCAT::Table->new('nodelist'); my @entries = ($nodelist->getAllNodeAttribs([qw(node)])); my @nodeentries; foreach (@entries) { push @nodeentries, $_->{node}; } my $typehash = xCAT::DBobjUtils->getnodetype(\@nodeentries); foreach (@entries) { #delete the CEC and Frame node my $ntype = $$typehash{ $_->{node} }; unless ($ntype =~ /^(cec|frame)$/) { push @{ $req->{node} }, $_->{node}; } } } else #add all entries { $req->{node} = []; my $mactab = xCAT::Table->new('mac'); my @entries = (); if ($mactab) { @entries = ($mactab->getAllNodeAttribs([qw(mac)])); } foreach (@entries) { push @{ $req->{node} }, $_->{node}; } # don't put compute node entries in for AIX nodes # this is handled by NIM - duplicate entires will cause # an error if ($^O eq 'aix') { my @tmplist; foreach my $n (@{ $req->{node} }) { # get the nodetype for each node my $ntable = xCAT::Table->new('nodetype'); if ($ntable) { my $ntype = $ntable->getNodeAttribs($n, ['nodetype']); # don't add if it is type "osi" unless ($ntype->{nodetype} =~ /osi/) { push @tmplist, $n; } } } @{ $req->{node} } = @tmplist; } } } foreach (@nrn) { my @line = split /:/; my $firstoctet = $line[0]; $firstoctet =~ s/^(\d+)\..*/$1/; if ($line[0] eq "169.254.0.0" or ($firstoctet >= 224 and $firstoctet <= 239)) { next; } my $netif = $line[1]; if ($netif =~ /!remote!\S+/) { $netif =~ s/!remote!\s*(.*)$/$1/; if (!defined($activenics{$netif})) { addnic($netif, \@dhcpconf); $activenics{$netif} = 1; } } #if ($activenics{$line[1]} and $line[3] !~ /G/) if ($activenics{$netif} and $line[3] !~ /G/) { addnet($line[0], $line[2]); } } if ($checkdomain) { $callback->({ error => [ "above error fail to generate new dhcp configuration file, restore dhcp configuration file $dhcpconffile" ], errorcode => [1] }); my $backupfile = $dhcpconffile.".xcatbak"; rename("$backupfile", $dhcpconffile); xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: Restore dhcp configuration file to $dhcpconffile"); exit 1; } foreach (@nrn6) { #do the ipv6 networks addnet6($_); #already did all the filtering before putting into nrn6 } if ($req->{node}) { my $ip_hash; foreach my $node (@{ $req->{node} }) { #need to change the way of finding IP for nodes my $ifip = xCAT::NetworkUtils->isIpaddr($node); if ($ifip) { $ip_hash->{$node} = $node; } else { my $hoststab = xCAT::Table->new('hosts'); my $ent = $hoststab->getNodeAttribs($node, ['ip']); if ($ent->{ip}) { if ($ip_hash->{ $ent->{ip} }) { $callback->({ error => [ "Duplicated IP addresses in hosts table for following nodes: $node," . $ip_hash->{ $ent->{ip} } ], errorcode => [1] }); return; } $ip_hash->{ $ent->{ip} } = $node; } } } if ($^O ne 'aix' and !_isc_static_host_fallback()) { my $settings = _omapi_settings(); return unless $settings; my $ent = _omapi_passwd_entry($settings); unless ($ent && $ent->{username} && $ent->{password}) { $callback->({ error => ["Unable to access omapi key from passwd table, add the key from dhcpd.conf or makedhcp -n to create a new one"], errorcode => [1] }); syslog("local4|err", "Unable to access omapi key from passwd table, unable to update DHCP configuration"); return; } # TODO sane err #Have nodes to update #open2($omshellout,$omshell,"/usr/bin/omshell"); ($omshell, $omshellpid) = _open_omshell_writer($settings); unless ($omshell) { $callback->({ error => ["Unable to start $settings->{omshell_path}"], errorcode => [1] }); syslog("local4|err", "Unable to start $settings->{omshell_path}"); return; } print $omshell xCAT::DHCP::OmapiPolicy->omshell_preamble($settings, secret => $ent->{password}, server => $::XCATSITEVALS{externaldhcpservers}); print $omshell "connect\n"; if ($usingipv6) { ($omshell6, $omshell6pid) = _open_omshell_writer($settings); unless ($omshell6) { $callback->({ error => ["Unable to start $settings->{omshell_path}"], errorcode => [1] }); syslog("local4|err", "Unable to start $settings->{omshell_path}"); _close_omshell_writer($omshell, $omshellpid); return; } print $omshell6 xCAT::DHCP::OmapiPolicy->omshell_preamble($settings, secret => $ent->{password}, port => 7912, server => $::XCATSITEVALS{externaldhcpservers}); print $omshell6 "connect\n"; } } my $nrtab = xCAT::Table->new('noderes'); my $chaintab = xCAT::Table->new('chain'); if ($chaintab) { $chainents = $chaintab->getNodesAttribs($req->{node}, ['currstate']); } else { $chainents = undef; } $nrhash = $nrtab->getNodesAttribs($req->{node}, [ 'tftpserver', 'netboot', 'proxydhcp', 'xcatmaster', 'servicenode']); my $nodetypetab; $nodetypetab = xCAT::Table->new('nodetype', -create => 0); if ($nodetypetab) { $nodetypeents = $nodetypetab->getNodesAttribs($req->{node}, [qw(os provmethod)]); } my $iscsitab = xCAT::Table->new('iscsi', -create => 0); if ($iscsitab) { $iscsients = $iscsitab->getNodesAttribs($req->{node}, [qw(server target lun iname)]); } my $mactab = xCAT::Table->new('mac'); $machash = $mactab->getNodesAttribs($req->{node}, ['mac']); unless ($opt{n} or $machash) { $callback->( { error => ["Unable to get mac address in mac table for specified nodes"], errorcode => [1] } ); return; } my $vpdtab = xCAT::Table->new('vpd'); $vpdhash = $vpdtab->getNodesAttribs($req->{node}, ['uuid']); foreach (@{ $req->{node} }) { if ($opt{d}) { if ($^O eq 'aix') { delnode_aix $_; } else { delnode $_; } } else { if (xCAT::NetworkUtils->getipaddr($_) and not xCAT::NetworkUtils->nodeonmynet($_)) { next; } addnode $_; if ($usingipv6) { addnode6 $_; } } } _close_omshell_writer($omshell, $omshellpid) if ($^O ne 'aix'); _close_omshell_writer($omshell6, $omshell6pid) if ($omshell6 and $^O ne 'aix'); } writeout(); if (not $::XCATSITEVALS{externaldhcpservers} and $restartdhcp) { xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: restart dhcp service"); if ($^O eq 'aix') { restart_dhcpd_aix(); } else { if ($distro =~ /ubuntu.*/ || $distro =~ /debian.*/i) { if (-e '/etc/dhcp/dhcpd.conf') { system("chmod a+r /etc/dhcp/dhcpd.conf"); #system("/etc/init.d/isc-dhcp-server restart"); } else { #ubuntu config system("chmod a+r /etc/dhcp3/dhcpd.conf"); #system("/etc/init.d/dhcp3-server restart"); } } #else #{ # system("/etc/init.d/dhcpd restart"); # # should not chkconfig dhcpd on every makedhcp invoation # # it is not appropriate and will cause problem for HAMN # # do it in xcatconfig instead # #system("chkconfig dhcpd on"); #} xCAT::Utils->restartservice("dhcp"); print "xx"; } } flock($dhcplockfd, LOCK_UN); umask $oldmask; } sub kea_process_request { my ( $backend, $req, $opt, $activenics, $verbose_on_off ) = @_; if ($::XCATSITEVALS{externaldhcpservers}) { xCAT::MsgUtils->trace($verbose_on_off, "d", "dhcp: remote dhcpservers configured, Kea backend has no local work"); return; } my $dhcplockfd; mkdir "/tmp/xcat" unless -d "/tmp/xcat"; open($dhcplockfd, ">", "/tmp/xcat/dhcplock"); flock($dhcplockfd, LOCK_EX); if ($opt->{q}) { my $loaded4 = $backend->load_dhcp4_config(); if ($loaded4->{error}) { $callback->({ error => [ $loaded4->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } my $loaded6 = $backend->load_dhcp6_config(); $loaded6 = undef if $loaded6->{error}; foreach my $node (@{ $req->{node} || [] }) { kea_query_node($backend, $loaded4, $node); kea_query_node($backend, $loaded6, $node) if $loaded6; } flock($dhcplockfd, LOCK_UN); return; } my $intent4 = kea_build_dhcp4_intent($backend, $activenics); if ($intent4->{error}) { $callback->({ error => [ $intent4->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } my $intent6 = kea_build_dhcp6_intent($backend, $activenics); if ($intent6->{error}) { $callback->({ error => [ $intent6->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } my $using_dhcp6 = @{ $intent6->{subnets} || [] } ? 1 : 0; my $ddns_intent = kea_build_ddns_intent(); my $using_ddns = $ddns_intent && !$ddns_intent->{error} ? 1 : 0; if ($ddns_intent && $ddns_intent->{error}) { $callback->({ error => [ $ddns_intent->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } if ($using_ddns) { my $dhcp_ddns = kea_dhcp_ddns_section(); $intent4->{'dhcp-ddns'} = $dhcp_ddns; kea_apply_ddns_behavior($intent4); $intent6->{'dhcp-ddns'} = $dhcp_ddns if $using_dhcp6; kea_apply_ddns_behavior($intent6) if $using_dhcp6; } if ($opt->{n}) { my $result = $backend->write_dhcp4_config($intent4, backup_existing => 1); if ($result->{error}) { $callback->({ error => [ $result->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } if ($using_dhcp6) { my $dhcp6_result = $backend->write_dhcp6_config($intent6, backup_existing => 1); if ($dhcp6_result->{error}) { $callback->({ error => [ $dhcp6_result->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } } if ($using_ddns) { my $ddns_result = $backend->write_ddns_config($ddns_intent, backup_existing => 1); if ($ddns_result->{error}) { $callback->({ error => [ $ddns_result->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } } if (kea_control_agent_enabled()) { my $ca_result = $backend->write_ctrl_agent_config({ dhcp6 => $using_dhcp6, ddns => $using_ddns }); if ($ca_result->{error}) { $callback->({ error => [ $ca_result->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } } my $restart = $backend->restart_services(ipv6 => $using_dhcp6, ctrl_agent => kea_control_agent_enabled(), ddns => $using_ddns, enable => 1); if ($restart->{error}) { $callback->({ error => [ $restart->{error} ], errorcode => [1] }); } flock($dhcplockfd, LOCK_UN); return; } my $loaded4 = $backend->load_dhcp4_config(); if ($loaded4->{error}) { $callback->({ error => [ $loaded4->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } if (!@{ $loaded4->{Dhcp4}{subnet4} || [] }) { my $result = $backend->write_dhcp4_config($intent4); if ($result->{error}) { $callback->({ error => [ $result->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } $loaded4 = $backend->load_dhcp4_config(); } if ($loaded4->{error}) { $callback->({ error => [ $loaded4->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } my $loaded6; if ($using_dhcp6) { $loaded6 = $backend->load_dhcp6_config(); if ($loaded6->{error}) { $callback->({ error => [ $loaded6->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } if (!@{ $loaded6->{Dhcp6}{subnet6} || [] }) { my $result = $backend->write_dhcp6_config($intent6); if ($result->{error}) { $callback->({ error => [ $result->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } $loaded6 = $backend->load_dhcp6_config(); } } my $nodes = kea_expand_request_nodes($req, $opt); unless ($nodes) { flock($dhcplockfd, LOCK_UN); return; } my @deleted4; my @deleted6; my $reservations4 = []; my $reservations6 = []; my $client_classes_changed = 0; if ($opt->{d}) { foreach my $match (@{ kea_reservation_matches_for_nodes($nodes) }) { push @deleted4, @{ $backend->delete_reservations($loaded4, $match) }; push @deleted6, @{ $backend->delete_reservations($loaded6, $match) } if $loaded6; } $client_classes_changed = kea_remove_xnba_client_classes($loaded4, $nodes); } else { $reservations4 = kea_build_node_reservations($backend, $loaded4, $nodes); $backend->upsert_reservations($loaded4, $reservations4); $client_classes_changed = kea_sync_xnba_client_classes($loaded4, $nodes); if ($loaded6) { $reservations6 = kea_build_node_reservations6($backend, $loaded6, $nodes); $backend->upsert_reservations($loaded6, $reservations6); } } my $result = $backend->write_dhcp4_json( $backend->encode_config($loaded4) ); if ($result->{error}) { $callback->({ error => [ $result->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } if ($loaded6) { my $dhcp6_result = $backend->write_dhcp6_json( $backend->encode_config($loaded6) ); if ($dhcp6_result->{error}) { $callback->({ error => [ $dhcp6_result->{error} ], errorcode => [1] }); flock($dhcplockfd, LOCK_UN); return; } } my $live_ok = 0; if (kea_control_agent_live_enabled($backend)) { my $live4 = $opt->{d} ? $backend->live_delete_reservations(\@deleted4, service => ['dhcp4']) : $backend->live_upsert_reservations($reservations4, service => ['dhcp4']); my $live6 = { ok => 1 }; if ($loaded6) { $live6 = $opt->{d} ? $backend->live_delete_reservations(\@deleted6, service => ['dhcp6']) : $backend->live_upsert_reservations($reservations6, service => ['dhcp6']); } $live_ok = !$live4->{error} && !$live6->{error}; if (!$live_ok) { my $why = $live4->{error} || $live6->{error}; $callback->({ warning => ["Kea Control Agent host update failed, restarting Kea services instead: $why"] }); } } unless ($live_ok && !$client_classes_changed) { my $restart = $backend->restart_services(ipv6 => $using_dhcp6, ctrl_agent => kea_control_agent_enabled(), ddns => $using_ddns); if ($restart->{error}) { $callback->({ error => [ $restart->{error} ], errorcode => [1] }); } } flock($dhcplockfd, LOCK_UN); } sub kea_build_dhcp4_intent { my ($backend, $activenics) = @_; %dynamicranges = (); %netcfgs = (); @alldomains = (); my $httpport = "80"; my @hports = xCAT::TableUtils->get_site_attribute("httpport"); if ($hports[0]) { $httpport = $hports[0]; } my $portsuffix = ( $httpport eq "80" ) ? "" : ":$httpport"; my $nettab = xCAT::Table->new("networks"); return { error => "Unable to open networks table, please run makenetworks" } unless $nettab; my @vnets = $nettab->getAllAttribs('net', 'mgtifname', 'mask', 'dynamicrange', 'nameservers', 'ddnsdomain', 'domain'); my @doms = $nettab->getAllAttribs('domain'); foreach my $netdom (@doms) { if ($netdom->{domain}) { push(@alldomains, $netdom->{domain}) unless grep(/^$netdom->{domain}$/, @alldomains); } } if ($site_domain) { push(@alldomains, $site_domain) unless grep(/^$site_domain$/, @alldomains); } foreach (@vnets) { addrangedetection($_); } my @routes = kea_ipv4_routes(@vnets); my %dhcp_interfaces = %$activenics; if (!keys %dhcp_interfaces) { my %configured_local_interfaces = map { $_->{mgtifname} => 1 } grep { $_->{mgtifname} && $_->{mgtifname} !~ /!remote!/ } @vnets; foreach my $route (@routes) { my ( $net, $netif, undef, $flags ) = @$route; next if kea_skip_ipv4_network($net); next if defined($flags) && $flags =~ /G/; next if $netif =~ /!remote!/; $dhcp_interfaces{$netif} = 1 if $configured_local_interfaces{$netif}; } } my @interfaces = grep { $_ ne '!remote!' && $_ !~ /!remote!/ } sort keys %dhcp_interfaces; if (!@interfaces && !$dhcp_interfaces{'!remote!'}) { return { error => "Unable to infer local Kea DHCP interfaces. Set site.dhcpinterfaces to the intended provisioning interface." }; } my @subnets; my @subnet_classes; my $id = 1; foreach my $route (@routes) { my ( $net, $netif, $mask, $flags ) = @$route; next if kea_skip_ipv4_network($net); next if defined($flags) && $flags =~ /G/; my $interface = $netif; my $remote = 0; if ($interface =~ /!remote!\S*/) { $remote = 1; $interface =~ s/!remote!\s*(.*)$/$1/; next unless $dhcp_interfaces{'!remote!'}; } else { next unless $dhcp_interfaces{$interface}; } my $subnet = kea_subnet4_intent($nettab, $net, $mask, $interface, $remote, $id, $httpport); return $subnet if $subnet->{error}; push @subnet_classes, @{ delete $subnet->{client_classes} || [] }; push @subnets, $subnet; $id++; } $nettab->close; my $intent = { interfaces => \@interfaces, valid_lifetime => kea_dhcp_lease_time(), 'option-def' => kea_option_defs(), 'option-data' => kea_global_option_data(), 'client-classes' => [ @{ kea_boot_client_classes() }, @subnet_classes ], subnets => \@subnets, }; if (kea_control_agent_enabled()) { $intent->{'control-socket'} = { 'socket-type' => 'unix', 'socket-name' => $backend->control_socket_path('kea4-ctrl-socket'), }; my $hook = $backend->host_cmds_hook_path(); if ($hook) { $intent->{'hooks-libraries'} = [ { library => $hook } ]; } else { $callback->({ warning => ["Kea Control Agent was requested, but libdhcp_host_cmds.so was not found. Host reservations will use JSON render and reload."] }); } } return $intent; } sub kea_build_dhcp6_intent { my ($backend, $activenics) = @_; my @interfaces = grep { $_ ne '!remote!' && $_ !~ /!remote!/ } sort keys %$activenics; @interfaces = ('*') unless @interfaces; my $nettab = xCAT::Table->new("networks"); return { error => "Unable to open networks table, please run makenetworks" } unless $nettab; my @vnets = $nettab->getAllAttribs('net', 'mgtifname', 'mask', 'dynamicrange', 'nameservers', 'ddnsdomain', 'domain'); my @subnets; my $id = 10001; foreach my $entry (@vnets) { next unless $entry->{net} && $entry->{net} =~ /:/; my $interface = $entry->{mgtifname} || '*'; my $remote = 0; if ($interface =~ /!remote!\S*/) { $remote = 1; $interface =~ s/!remote!\s*(.*)$/$1/; next if %$activenics && !$activenics->{'!remote!'}; } elsif (%$activenics && !$activenics->{$interface} && $interfaces[0] ne '*') { next; } my $subnet = kea_subnet6_intent($entry, $interface, $remote, $id); return $subnet if $subnet->{error}; push @subnets, $subnet; $id++; } $nettab->close; my $intent = { interfaces => \@interfaces, preferred_lifetime => kea_dhcp_lease_time(), valid_lifetime => kea_dhcp_lease_time(), subnets => \@subnets, }; if (kea_control_agent_enabled()) { $intent->{'control-socket'} = { 'socket-type' => 'unix', 'socket-name' => $backend->control_socket_path('kea6-ctrl-socket'), }; my $hook = $backend->host_cmds_hook_path(); if ($hook) { $intent->{'hooks-libraries'} = [ { library => $hook } ]; } } return $intent; } sub kea_subnet6_intent { my ( $entry, $interface, $remote, $id ) = @_; my $net = $entry->{net}; my $prefix; if ($net =~ m{^(.+)/(\d+)$}) { $prefix = $2; } elsif (defined $entry->{mask} && $entry->{mask} =~ /^(\d+)$/) { $prefix = $1; $net = "$net/$prefix"; } else { return { error => "IPv6 network $net must include a prefix length for Kea DHCPv6." }; } my $domain = $entry->{domain} || $site_domain; my @option_data; if ($domain) { push @option_data, { name => 'domain-search', data => $domain }; } my $nameservers = $entry->{nameservers} || $sitenameservers; if ($nameservers && $nameservers =~ /:/) { push @option_data, { name => 'dns-servers', data => $nameservers }; } my %subnet = ( id => $id, subnet => $net, dynamicrange => $entry->{dynamicrange}, option_data => \@option_data, ); $subnet{interface} = $interface unless $remote || $interface eq '*'; return \%subnet; } sub kea_build_ddns_intent { return unless kea_ddns_enabled(); my $nettab = xCAT::Table->new("networks"); return { error => "Unable to open networks table, please run makenetworks" } unless $nettab; my @vnets = $nettab->getAllAttribs('net', 'mask', 'nameservers', 'ddnsdomain', 'domain'); $nettab->close; my ( $key_algorithm, $key_secret ) = kea_ddns_key(); return { error => "Unable to find DDNS key material for Kea D2. Run makedns with dnshandler=ddns first." } unless $key_secret; my @tsig_keys = ( { name => 'xcat_key', algorithm => $key_algorithm, secret => $key_secret, } ); my ( %forward_seen, %reverse_seen, @forward, @reverse ); foreach my $entry (@vnets) { my $dns = $entry->{nameservers} || $sitenameservers || ''; $dns =~ s/,.*//; next unless $dns; # networks.nameservers / site.nameservers default to the # placeholder. Kea D2 validates dns-servers[].ip-address as a real IP, # so resolve to the management IP facing this network the # same way kea_subnet4_intent does for DHCP options. Skip the network's # DDNS domains if we can't resolve a real IP rather than emit an invalid one. if ($dns =~ //) { my @myipd = xCAT::NetworkUtils->my_ip_facing($entry->{net}); my $myip = $myipd[0] ? undef : $myipd[1]; $dns =~ s//$myip/g if $myip; } next if (!$dns || $dns =~ //); my $domain = $entry->{ddnsdomain} || $entry->{domain} || $site_domain; if ($domain) { $domain .= '.' unless $domain =~ /\.$/; if (!$forward_seen{$domain}++) { push @forward, kea_ddns_domain($domain, $dns); } } my $zone_mask = $entry->{net} =~ /:/ ? undef : $entry->{mask}; foreach my $zone (getzonesfornet($entry->{net}, $zone_mask)) { $zone .= '.' unless $zone =~ /\.$/; if (!$reverse_seen{$zone}++) { push @reverse, kea_ddns_domain($zone, $dns); } } } return { 'tsig-keys' => \@tsig_keys, forward_domains => \@forward, reverse_domains => \@reverse, }; } sub kea_ddns_domain { my ( $name, $server ) = @_; return { name => $name, 'key-name' => 'xcat_key', 'dns-servers' => [ { 'ip-address' => $server, port => 53, } ], }; } sub kea_dhcp_ddns_section { return { 'enable-updates' => JSON::true, 'server-ip' => '127.0.0.1', 'server-port' => 53001, 'ncr-protocol' => 'UDP', 'ncr-format' => 'JSON', }; } sub kea_apply_ddns_behavior { my ($intent) = @_; $intent->{'ddns-send-updates'} = JSON::true; $intent->{'ddns-override-no-update'} = JSON::true; $intent->{'ddns-override-client-update'} = JSON::true; $intent->{'ddns-qualifying-suffix'} = $site_domain ? "$site_domain." : 'xcat.local.'; $intent->{'ddns-update-on-renew'} = JSON::true; } sub dhcpd_sysconfig_uses_interface_key { my $os = shift || ""; my ( $os_family, $os_major, $os_minor ) = xCAT::SvrUtils::parseosver($os); if (!$os_family || !defined $os_major || !length $os_major) { return 0; } my $os_version = $os_major; if (defined $os_minor && length $os_minor) { $os_version .= ".$os_minor"; } if ( $os_family =~ /(sles|opensuse[-_]?leap|leap)/i && xCAT::Utils->version_cmp( $os_version, '11' ) >= 0) { return 1; } if ( $os_family =~ /rhels?/i && xCAT::Utils->version_cmp( $os_version, '7' ) >= 0) { return 1; } return 0; } sub kea_ddns_enabled { return defined($::XCATSITEVALS{dnshandler}) && $::XCATSITEVALS{dnshandler} =~ /ddns/ ? 1 : 0; } sub kea_ddns_key { my $key_path = "/etc/xcat/ddns.key"; if (open(my $fh, '<', $key_path)) { local $/; my $contents = <$fh>; close($fh); my ($algorithm) = $contents =~ /algorithm\s+([A-Za-z0-9-]+)\s*;/; my ($secret) = $contents =~ /secret\s+"([^"]+)"/; $algorithm ||= 'HMAC-SHA256'; $algorithm = uc($algorithm); $algorithm =~ s/^HMAC-/HMAC-/; return ($algorithm, $secret) if $secret; } my $settings = xCAT::DHCP::OmapiPolicy->settings(); my $passtab = xCAT::Table->new('passwd'); my $pent = $passtab ? $passtab->getAttribs({ key => 'omapi', username => $settings->{key_name} }, ['password']) : undef; return (uc($settings->{algorithm}), $pent->{password}) if $pent && $pent->{password}; return; } sub kea_ipv4_routes { my @vnets = @_; my @routes; push @routes, local_ipv4_routes(); foreach (@vnets) { my $n = $_->{net}; my $if = $_->{mgtifname}; my $nm = $_->{mask}; if ($if =~ /!remote!/ and $n !~ /:/) { push @routes, [ $n, $if, $nm, '' ]; } } return @routes; } sub local_ipv4_routes { my @routes; my $ipcmd = kea_command_path('ip'); if ($ipcmd) { my @route_output = split /\n/, `$ipcmd -4 route show 2>/dev/null`; foreach my $line (@route_output) { if ($line =~ /^default\b/ && $line =~ /\bdev\s+(\S+)/) { push @routes, [ '0.0.0.0', $1, '0.0.0.0', 'G' ]; next; } next unless $line =~ /^(\d+(?:\.\d+){3})\/(\d+)\b.*\bdev\s+(\S+)/; push @routes, [ $1, $3, xCAT::NetworkUtils::formatNetmask( $2, 1, 0 ), '' ]; } } else { my $netstat = kea_command_path('netstat'); if ($netstat) { my @nsrnoutput = split /\n/, `$netstat -rn 2>/dev/null`; splice @nsrnoutput, 0, 2; foreach (@nsrnoutput) { my @parts = split /\s+/; next unless $parts[0] && $parts[2] && $parts[7]; push @routes, [ $parts[0], $parts[7], $parts[2], $parts[3] ]; } } } return @routes; } sub kea_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 kea_subnet4_intent { my ( $nettab, $net, $mask, $interface, $remote, $id, $httpport ) = @_; my $portsuffix = ( $httpport eq "80" ) ? "" : ":$httpport"; my @myipd = xCAT::NetworkUtils->my_ip_facing($net); my $myip; unless ($myipd[0]) { $myip = $myipd[1]; } my ($ent) = $nettab->getAttribs( { net => $net, mask => $mask }, qw(tftpserver nameservers ntpservers logservers gateway dynamicrange dhcpserver domain mtu) ); my $ntpservers = $ent && $ent->{ntpservers} ? $ent->{ntpservers} : $sitentpservers; my $logservers = $ent && $ent->{logservers} ? $ent->{logservers} : $sitelogservers; my $domain = $ent && $ent->{domain} ? $ent->{domain} : $site_domain; return { error => "No domain defined for $net entry in networks table, and no domain defined in site table." } unless $domain; my $nameservers; if ($ent and $ent->{nameservers}) { $nameservers = $ent->{nameservers}; } elsif ($sitenameservers) { $nameservers = $sitenameservers; } $nameservers =~ s//$myip/g if $nameservers; if (!$ntpservers || ($ntpservers eq '')) { $ntpservers = $myip; } $nameservers = putmyselffirst($nameservers) if $nameservers; $ntpservers = putmyselffirst($ntpservers) if $ntpservers; $logservers = putmyselffirst($logservers) if $logservers; my $tftp = $ent && $ent->{tftpserver} ? $ent->{tftpserver} : undef; if (!$tftp || ($tftp eq '')) { $tftp = $myip; } my $gateway = $ent && $ent->{gateway} ? $ent->{gateway} : undef; if ($gateway && $gateway eq '') { $gateway = xCAT::NetworkUtils->ip_forwarding_enabled() ? $myip : ''; } if ( $gateway && !xCAT::NetworkUtils::isInSameSubnet( $gateway, $net, $mask, 0 ) ) { return { error => "Specified gateway $gateway is not valid for $net/$mask, must be on same network" }; } my @option_data; push @option_data, { name => 'routers', data => $gateway } if $gateway; push @option_data, { name => 'log-servers', data => $logservers || $myip } if $logservers || $myip; push @option_data, { name => 'ntp-servers', data => $ntpservers } if $ntpservers; if ($nameservers) { push @option_data, { name => 'domain-name', data => $domain }; push @option_data, { name => 'domain-name-servers', data => $nameservers }; } push @option_data, { name => 'interface-mtu', data => $ent->{mtu} } if $ent && $ent->{mtu}; push @option_data, { name => 'cumulus-provision-url', data => "http://$tftp$portsuffix/install/postscripts/cumulusztp" } if $tftp; my $domainstring = join(', ', map { $_ eq $domain ? $_ : $_ } grep { $_ } @alldomains); push @option_data, { name => 'domain-search', data => $domainstring } if $domainstring; my $prefix = xCAT::NetworkUtils::formatNetmask( $mask, 0, 1 ); my $dynamicrange = $ent ? $ent->{dynamicrange} : undef; if ( $dynamicrange && $ent->{dhcpserver} && xCAT::NetworkUtils->thishostisnot( $ent->{dhcpserver} ) ) { $dynamicrange = undef; } my $opal_class = kea_opal_client_class($net, $prefix, $tftp, $httpport); my $xnba_classes = []; if ($dynamicrange) { $xnba_classes = xCAT::DHCP::BootPolicy->kea_xnba_network_classes( net => $net, prefix => $prefix, next_server => $tftp, httpport => $httpport, xnba_kpxe => -f "$tftpdir/xcat/xnba.kpxe" ? 1 : 0, xnba_efi => -f "$tftpdir/xcat/xnba.efi" ? 1 : 0, ); } my %subnet = ( id => $id, subnet => "$net/$prefix", dynamicrange => $dynamicrange, option_data => \@option_data, next_server => $tftp, ); $subnet{interface} = $interface unless $remote; my @client_classes = @$xnba_classes; push @client_classes, $opal_class if $opal_class; if (@client_classes) { $subnet{additional_client_classes} = [ map { $_->{name} } @client_classes ]; $subnet{client_classes} = \@client_classes; } return \%subnet; } sub kea_opal_client_class { my ( $net, $prefix, $tftp, $httpport ) = @_; my $portsuffix = ( $httpport eq "80" ) ? "" : ":$httpport"; return unless $net && defined($prefix) && $tftp; my $class_name = "xcat-opal-v3-$net-$prefix"; $class_name =~ s/[^A-Za-z0-9_.-]/_/g; return { name => $class_name, test => 'option[93].hex == 0x000e', additional_only => JSON::true, 'option-data' => [ { name => 'conf-file', data => "http://$tftp$portsuffix/tftpboot/pxelinux.cfg/p/" . $net . "_" . $prefix, }, ], }; } sub kea_expand_request_nodes { my ( $req, $opt ) = @_; my @nodes; if ($req->{node}) { my $typehash = xCAT::DBobjUtils->getnodetype(\@{ $req->{node} }); foreach my $node (@{ $req->{node} }) { my $ntype = $$typehash{$node}; if ($ntype =~ /^(cec|frame)$/) { my $children = xCAT::DBobjUtils->getchildren($node); push @nodes, @$children; } else { push @nodes, $node; } } return \@nodes; } return unless $opt->{a}; if ($opt->{d}) { my $nodelist = xCAT::Table->new('nodelist'); my @entries = ($nodelist->getAllNodeAttribs([qw(node)])); my @nodeentries = map { $_->{node} } @entries; my $typehash = xCAT::DBobjUtils->getnodetype(\@nodeentries); foreach (@entries) { my $ntype = $$typehash{ $_->{node} }; push @nodes, $_->{node} unless $ntype =~ /^(cec|frame)$/; } } else { my $mactab = xCAT::Table->new('mac'); my @entries = $mactab ? ($mactab->getAllNodeAttribs([qw(mac)])) : (); foreach (@entries) { push @nodes, $_->{node}; } } return \@nodes; } sub kea_build_node_reservations { my ( $backend, $config, $nodes ) = @_; my $nrtab = xCAT::Table->new('noderes'); my $chaintab = xCAT::Table->new('chain'); my $nodetypetab = xCAT::Table->new('nodetype', -create => 0); my $iscsitab = xCAT::Table->new('iscsi', -create => 0); my $mactab = xCAT::Table->new('mac'); $chainents = $chaintab ? $chaintab->getNodesAttribs($nodes, ['currstate']) : undef; $nrhash = $nrtab->getNodesAttribs($nodes, [ 'tftpserver', 'netboot', 'proxydhcp', 'xcatmaster', 'servicenode' ]); $nodetypeents = $nodetypetab ? $nodetypetab->getNodesAttribs($nodes, [qw(os provmethod arch)]) : undef; $iscsients = $iscsitab ? $iscsitab->getNodesAttribs($nodes, [qw(server target lun iname)]) : undef; $machash = $mactab ? $mactab->getNodesAttribs($nodes, ['mac']) : undef; my @reservations; foreach my $node (@$nodes) { my $node_reservations = kea_node_reservations($backend, $config, $node); push @reservations, @$node_reservations; } return \@reservations; } sub kea_build_node_reservations6 { my ( $backend, $config, $nodes ) = @_; my $nrtab = xCAT::Table->new('noderes'); my $mactab = xCAT::Table->new('mac'); my $vpdtab = xCAT::Table->new('vpd', -create => 0); $nrhash = $nrtab ? $nrtab->getNodesAttribs($nodes, [ 'tftpserver', 'xcatmaster', 'servicenode' ]) : undef; $machash = $mactab ? $mactab->getNodesAttribs($nodes, ['mac']) : undef; $vpdhash = $vpdtab ? $vpdtab->getNodesAttribs($nodes, ['uuid']) : undef; my @reservations; foreach my $node (@$nodes) { push @reservations, @{ kea_node_reservations6($backend, $config, $node) }; } return \@reservations; } sub kea_node_reservations { my ( $backend, $config, $node ) = @_; my $nrent = $nrhash && $nrhash->{$node} ? $nrhash->{$node}->[0] : undef; my $chainent = $chainents && $chainents->{$node} ? $chainents->{$node}->[0] : undef; my $ntent = $nodetypeents && $nodetypeents->{$node} ? $nodetypeents->{$node}->[0] : undef; my $ient = $iscsients && $iscsients->{$node} ? $iscsients->{$node}->[0] : undef; my $macent = $machash && $machash->{$node} ? $machash->{$node}->[0] : undef; unless ($macent and $macent->{mac}) { $callback->({ warning => ["Unable to find mac address for $node"] }); return []; } my ( $nxtsrv, $tftpserver ) = kea_next_server_for_node($node, $nrent); my @reservations; my @macs = split(/\|/, $macent->{mac}); foreach my $mace (@macs) { my ( $mac, $hname ) = split(/!/, $mace); $hname ||= $node; next unless $mac; # A NIC whose hostname is the *NOIP* sentinel intentionally has no IP # (e.g. secondary interfaces in the mac table). Skip it: there is no # address to reserve, and trying to resolve "*NOIP*" would otherwise be # treated as an unresolved reservation. This mirrors the ISC path. next if $hname eq '*NOIP*'; my $normalized_mac = kea_normalize_mac($mac); unless ($normalized_mac) { $callback->({ error => ["Invalid mac address $mac for $node"], errorcode => [1] }); next; } $mac = $normalized_mac; my $ip = getipaddr($hname, OnlyV4 => 1); unless ($ip) { $callback->({ warning => ["The hostname $hname of node $node could not be resolved."] }); next; } if (ipIsDynamic($ip)) { $callback->({ error => ["Node $node has IP $ip which is inside the DHCP dynamic range. Move the node IP outside the dynamic range or adjust the range in the networks table."], errorcode => [1] }); next; } my $subnet_id = $backend->subnet_id_for_ip($config, $ip); unless ($subnet_id) { $callback->({ warning => ["Unable to find a Kea subnet for $node ($ip), skipping DHCP reservation"] }); next; } my %reservation = ( 'subnet-id' => $subnet_id, 'hw-address' => $mac, hostname => $hname, 'ip-address' => $ip, ); $reservation{'next-server'} = $nxtsrv if $nxtsrv && $nxtsrv !~ /\$\{/; my $boot = kea_boot_for_node($node, $nrent, $chainent, $ntent, $ient, $nxtsrv); $reservation{'boot-file-name'} = $boot->{'boot-file-name'} if defined $boot->{'boot-file-name'}; $reservation{'option-data'} = $boot->{'option-data'} if @{ $boot->{'option-data'} || [] }; push @reservations, \%reservation; } return \@reservations; } sub kea_sync_xnba_client_classes { my ( $config, $nodes ) = @_; my $changed = kea_remove_xnba_client_classes($config, $nodes); my $classes = kea_xnba_client_classes_for_nodes($nodes); return $changed unless @$classes; $config->{Dhcp4} ||= {}; my @existing = @{ $config->{Dhcp4}{'client-classes'} || [] }; $config->{Dhcp4}{'client-classes'} = [ @$classes, @existing ]; return 1; } sub kea_remove_xnba_client_classes { my ( $config, $nodes ) = @_; return 0 unless $config && $config->{Dhcp4}; my $classes = $config->{Dhcp4}{'client-classes'} || []; my %nodes = map { $_ => 1 } @$nodes; my @kept; my $changed = 0; foreach my $class (@$classes) { my $context = $class->{'user-context'} || {}; if ( ( $context->{'xcat-purpose'} || '' ) eq 'xnba-second-stage' && $nodes{ $context->{'xcat-node'} || '' } ) { $changed = 1; next; } push @kept, $class; } $config->{Dhcp4}{'client-classes'} = \@kept if $changed; return $changed; } sub kea_xnba_client_classes_for_nodes { my ($nodes) = @_; my $nrtab = xCAT::Table->new('noderes'); my $mactab = xCAT::Table->new('mac'); return [] unless $nrtab && $mactab; my $nrents = $nrtab->getNodesAttribs($nodes, [ 'tftpserver', 'netboot', 'proxydhcp', 'xcatmaster', 'servicenode' ]); my $macents = $mactab->getNodesAttribs($nodes, ['mac']); my $httpport = "80"; my @hports = xCAT::TableUtils->get_site_attribute("httpport"); if ($hports[0]) { $httpport = $hports[0]; } my $portsuffix = ( $httpport eq "80" ) ? "" : ":$httpport"; my @records; foreach my $node (@$nodes) { my $nrent = $nrents && $nrents->{$node} ? $nrents->{$node}->[0] : undef; next unless $nrent && $nrent->{netboot} && $nrent->{netboot} eq 'xnba'; my $macent = $macents && $macents->{$node} ? $macents->{$node}->[0] : undef; next unless $macent && $macent->{mac}; my ( $nxtsrv ) = kea_next_server_for_node($node, $nrent); next unless $nxtsrv; foreach my $mace (split(/\|/, $macent->{mac})) { my ($mac) = split(/!/, $mace); $mac = kea_normalize_mac($mac); next unless $mac; push @records, { node => $node, mac => $mac, next_server => $nxtsrv, httpport => $httpport, }; } } return xCAT::DHCP::BootPolicy->kea_xnba_node_classes( nodes => \@records, xnba_efi => -f "$tftpdir/xcat/xnba.efi" ? 1 : 0, ); } sub kea_normalize_mac { my ($mac) = @_; return unless $mac; return unless $mac =~ /\A(?:[0-9a-fA-F]{2}(?:-[0-9a-fA-F]{2}){5,8}|[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5,8})\z/; $mac =~ tr/-/:/; return lc($mac); } sub kea_node_reservations6 { my ( $backend, $config, $node ) = @_; my $macent = $machash && $machash->{$node} ? $machash->{$node}->[0] : undef; my $vpdent = $vpdhash && $vpdhash->{$node} ? $vpdhash->{$node}->[0] : undef; unless ($macent and $macent->{mac}) { $callback->({ warning => ["Unable to find mac address for $node"] }); return []; } my $duid = kea_duid_from_uuid($vpdent ? $vpdent->{uuid} : undef); my @reservations; foreach my $mace (split(/\|/, $macent->{mac})) { my ( $mac, $hname ) = split(/!/, $mace); $hname ||= $node; next unless $mac; # A NIC whose hostname is the *NOIP* sentinel intentionally has no IP # (e.g. secondary interfaces in the mac table). Skip it: there is no # address to reserve, and trying to resolve "*NOIP*" would otherwise be # treated as an unresolved reservation. This mirrors the ISC path. next if $hname eq '*NOIP*'; my $normalized_mac = kea_normalize_mac($mac); unless ($normalized_mac) { $callback->({ error => ["Invalid mac address $mac for $node"], errorcode => [1] }); next; } $mac = $normalized_mac; my $ip = getipaddr($hname, OnlyV6 => 1); next unless $ip; if (ipIsDynamic($ip)) { $callback->({ error => ["Node $node has IPv6 address $ip which is inside the DHCP dynamic range. Move the node IP outside the dynamic range or adjust the range in the networks table."], errorcode => [1] }); next; } my $subnet_id = $backend->subnet_id_for_ip($config, $ip); unless ($subnet_id) { $callback->({ warning => ["Unable to find a Kea DHCPv6 subnet for $node ($ip), skipping DHCPv6 reservation"] }); next; } my %reservation = ( 'subnet-id' => $subnet_id, hostname => $hname, 'ip-addresses' => [$ip], ); if ($duid) { $reservation{duid} = $duid; } else { $reservation{'hw-address'} = $mac; } push @reservations, \%reservation; } return \@reservations; } sub kea_duid_from_uuid { my ($uuid) = @_; return unless $uuid; $uuid =~ s/[^0-9a-fA-F]//g; return unless length($uuid) == 32; $uuid =~ s/(..)/$1:/g; $uuid =~ s/:$//; return "00:04:$uuid"; } sub kea_reservation_matches_for_nodes { my ($nodes) = @_; my $mactab = xCAT::Table->new('mac'); my $machash_local = $mactab ? $mactab->getNodesAttribs($nodes, ['mac']) : undef; my $vpdtab = xCAT::Table->new('vpd', -create => 0); my $vpdhash_local = $vpdtab ? $vpdtab->getNodesAttribs($nodes, ['uuid']) : undef; my @matches; foreach my $node (@$nodes) { push @matches, { hostname => $node }; my $ip = getipaddr($node, OnlyV4 => 1); push @matches, { 'ip-address' => $ip } if $ip; my $ip6 = getipaddr($node, OnlyV6 => 1); push @matches, { 'ip-address' => $ip6 } if $ip6; my $vpdent = $vpdhash_local && $vpdhash_local->{$node} ? $vpdhash_local->{$node}->[0] : undef; my $duid = kea_duid_from_uuid($vpdent ? $vpdent->{uuid} : undef); push @matches, { duid => $duid } if $duid; my $macent = $machash_local && $machash_local->{$node} ? $machash_local->{$node}->[0] : undef; next unless $macent && $macent->{mac}; foreach my $mace (split(/\|/, $macent->{mac})) { my ( $mac, $hname ) = split(/!/, $mace); if ($hname) { push @matches, { hostname => $hname }; my $host_ip = getipaddr($hname, OnlyV4 => 1); push @matches, { 'ip-address' => $host_ip } if $host_ip; my $host_ip6 = getipaddr($hname, OnlyV6 => 1); push @matches, { 'ip-address' => $host_ip6 } if $host_ip6; } next unless $mac; $mac = kea_normalize_mac($mac); next unless $mac; push @matches, { 'hw-address' => $mac }; } } return \@matches; } sub kea_query_node { my ( $backend, $config, $node ) = @_; my @matches = @{ kea_reservation_matches_for_nodes([$node]) }; my @found; foreach my $match (@matches) { push @found, @{ $backend->query_reservations($config, $match) }; } my %seen; foreach my $reservation (@found) { my $key = join('|', map { $reservation->{$_} || '' } qw(subnet-id hw-address ip-address hostname)); next if $seen{$key}++; my $msg = "$node:"; $msg .= " ip-address = $reservation->{'ip-address'}" if $reservation->{'ip-address'}; $msg .= " hardware-address = $reservation->{'hw-address'}" if $reservation->{'hw-address'}; $msg .= " hostname = $reservation->{hostname}" if $reservation->{hostname}; $callback->({ data => [$msg] }); } } sub kea_next_server_for_node { my ( $node, $nrent ) = @_; if ($nrent and $nrent->{tftpserver} and $nrent->{tftpserver} ne '') { my $tmp_name = inet_aton($nrent->{tftpserver}); unless ($tmp_name) { $callback->({ error => ["Unable to resolve the tftpserver for node"], errorcode => [1] }); return; } my $server = inet_ntoa($tmp_name); return ($server, $server); } my $node_server = $nrent && $nrent->{xcatmaster} ? $nrent->{xcatmaster} : undef; if ($node_server) { my $tmp_server = inet_aton($node_server); if ($tmp_server) { my $server = inet_ntoa($tmp_server); return ($server, $server); } } my @nxtsrvd = xCAT::NetworkUtils->my_ip_facing($node); unless ($nxtsrvd[0]) { return ($nxtsrvd[1], $nxtsrvd[1]); } return ('${next-server}', undef); } sub kea_boot_for_node { my ( $node, $nrent, $chainent, $ntent, $ient, $nxtsrv ) = @_; my $httpport = "80"; my @hports = xCAT::TableUtils->get_site_attribute("httpport"); if ($hports[0]) { $httpport = $hports[0]; } my $portsuffix = ( $httpport eq "80" ) ? "" : ":$httpport"; my %boot = ( 'option-data' => [] ); my $netboot = $nrent ? $nrent->{netboot} : undef; if ($ient and $ient->{server} and $ient->{target}) { $ient->{lun} = 0 unless defined($ient->{lun}); my $rootpath = 'iscsi:' . $ient->{server} . ':6:3260:' . $ient->{lun} . ':' . $ient->{target}; push @{ $boot{'option-data'} }, { name => 'root-path', data => $rootpath }; push @{ $boot{'option-data'} }, { name => 'iscsi-initiator-iqn', data => $ient->{iname} } if defined($ient->{iname}); } if ($netboot and $netboot eq 'pxe') { $boot{'boot-file-name'} = 'pxelinux.0'; } elsif ($netboot and $netboot eq 'yaboot') { $boot{'boot-file-name'} = "/yb/node/yaboot-$node"; } elsif ($netboot and $netboot =~ /^grub2[-]?.*$/) { $boot{'boot-file-name'} = "/boot/grub2/grub2-$node"; } elsif ($netboot and $netboot eq 'petitboot') { if ($nxtsrv) { my $petitboot_conf = "http://$nxtsrv$portsuffix/tftpboot/petitboot/$node"; $boot{'boot-file-name'} = $petitboot_conf; push @{ $boot{'option-data'} }, { name => 'conf-file', data => $petitboot_conf }; } } elsif ($netboot and $netboot eq 'onie') { my $onie_url = kea_onie_url_for_node($node, $ntent, $nxtsrv, $httpport); push @{ $boot{'option-data'} }, { name => 'www-server', data => $onie_url } if $onie_url; } push @{ $boot{'option-data'} }, { name => 'host-name', data => $node }; return \%boot; } sub kea_onie_url_for_node { my ( $node, $ntent, $nxtsrv, $httpport ) = @_; my $portsuffix = ( $httpport eq "80" ) ? "" : ":$httpport"; return unless $nxtsrv; my $provmethod = $ntent ? $ntent->{provmethod} : undef; return "http://$nxtsrv$portsuffix/install/onie/onie-installer" unless $provmethod; my $linuximagetab = xCAT::Table->new('linuximage'); my $imagetab = $linuximagetab ? $linuximagetab->getAttribs({ imagename => $provmethod }, 'pkgdir') : undef; return "http://$nxtsrv$portsuffix/install/onie/onie-installer" unless $imagetab; foreach my $pkgdir (split(/,/, $imagetab->{pkgdir} || '')) { return "http://$nxtsrv$portsuffix$pkgdir" if -f $pkgdir; } $callback->({ warning => ["osimage $provmethod pkgdir doesn't exists"]}); return; } sub kea_boot_client_classes { return xCAT::DHCP::BootPolicy->kea_client_classes( xnba_kpxe => -f "$tftpdir/xcat/xnba.kpxe" ? 1 : 0, xnba_efi => -f "$tftpdir/xcat/xnba.efi" ? 1 : 0, ); } sub kea_option_defs { return [ { 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' }, ]; } sub kea_global_option_data { my @options; if ($::XCATSITEVALS{timezone}) { push @options, { name => 'tcode', data => $::XCATSITEVALS{timezone} }; } return \@options; } sub kea_dhcp_lease_time { return $::XCATSITEVALS{'dhcplease'} if defined $::XCATSITEVALS{'dhcplease'} && $::XCATSITEVALS{'dhcplease'} ne ""; return 43200; } sub kea_control_agent_enabled { my @entries = xCAT::TableUtils->get_site_attribute("keacontrolagent"); my $value = $entries[0]; return 0 unless defined($value); return $value =~ /^(1|yes|y|true|enabled)$/i ? 1 : 0; } sub kea_control_agent_live_enabled { my ($backend) = @_; return 0 unless kea_control_agent_enabled(); return $backend->host_cmds_hook_path() ? 1 : 0; } sub kea_skip_ipv4_network { my ($net) = @_; my $firstoctet = $net; $firstoctet =~ s/^(\d+)\..*/$1/; return 1 if $net eq "169.254.0.0"; return 1 if $net eq "127.0.0.0" || $net eq '127'; return 1 if ($firstoctet >= 224 and $firstoctet <= 239); return 0; } # Restart dhcpd on aix sub restart_dhcpd_aix { #Check if dhcpd is running my @res = xCAT::Utils->runcmd('lssrc -s dhcpsd', 0); if ($::RUNCMD_RC != 0) { xCAT::MsgUtils->message("E", "Failed to check dhcpsd status\n"); } if (grep /\sactive/, @res) { xCAT::Utils->runcmd('refresh -s dhcpsd', 0); xCAT::MsgUtils->message("E", "Failed to refresh dhcpsd configuration\n") if ($::RUNCMD_RC); } else { xCAT::Utils->runcmd('startsrc -s dhcpsd', 0); xCAT::MsgUtils->message("E", "Failed to start dhcpsd\n") if ($::RUNCMD_RC); } return 1; } sub getzonesfornet { my $net = shift; my $mask = shift; my @zones = (); if ($net =~ /:/) { #ipv6, for now do the simple stuff under the assumption we won't have a mask indivisible by 4 $net =~ s/\/(.*)//; my $maskbits = $1; if ($mask) { die "Not supporting having a mask like $mask on an ipv6 network like $net"; } my $netnum = getipaddr($net, GetNumber => 1); unless ($netnum) { return (); } $netnum->brsft(128 - $maskbits); my $prefix = $netnum->as_hex(); my $nibbs = $maskbits / 4; $prefix =~ s/^0x//; my $rev; foreach (reverse(split //, $prefix)) { $rev .= $_ . "."; $nibbs--; } if ($nibbs < 0) { # a sub-nibble (not 4-bit-aligned) IPv6 mask leaves $nibbs negative; # bail rather than spin forever in the loop below. $nibbs == 0 is the # normal nibble-aligned case (e.g. a /64) and must still emit a zone. return (); } while ($nibbs > 0) { $rev .= "0."; $nibbs--; } $rev .= "ip6.arpa."; return ($rev); } #return all in-addr reverse zones for a given mask and net #for class a,b,c, the answer is easy #for classless, identify the partial byte, do $netbyte | (0xff&~$maskbyte) to get the highest value #return sequence from $net to value calculated above #since old bind.pm only went as far as class c, we will carry that over for now (more people with smaller than class c complained #and none hit the theoretical conflict. FYI, the 'official' method in RFC 2317 seems cumbersome, but maybe one day it makes sense #since this is dhcpv4 for now, we'll use the inet_aton, ntop functions to generate the answers (dhcpv6 omapi would be nice...) my $netn = inet_aton($net); my $maskn = inet_aton($mask); unless ($netn and $mask) { return (); } my $netnum = unpack('N', $netn); my $masknum = unpack('N', $maskn); if ($masknum >= 0xffffff00) { #treat all netmasks higher than 255.255.255.0 as class C $netnum = $netnum & 0xffffff00; $netn = pack('N', $netnum); $net = inet_ntoa($netn); $net =~ s/\.[^\.]*$//; return (join('.', reverse(split('\.', $net))) . '.IN-ADDR.ARPA.'); } elsif ($masknum > 0xffff0000) { #class b (/16) to /23 my $tempnumber = ($netnum >> 8); $masknum = $masknum >> 8; my $highnet = $tempnumber | (0xffffff & ~$masknum); foreach ($tempnumber .. $highnet) { $netnum = $_ << 8; $net = inet_ntoa(pack('N', $netnum)); $net =~ s/\.[^\.]*$//; push @zones, join('.', reverse(split('\.', $net))) . '.IN-ADDR.ARPA.'; } return @zones; } elsif ($masknum > 0xff000000) { #class a (/8) to /15, could have made it more flexible, for for only two cases, not worth in my $tempnumber = ($netnum >> 16); #the last two bytes are insignificant, shift them off to make math easier $masknum = $masknum >> 16; my $highnet = $tempnumber | (0xffff & ~$masknum); foreach ($tempnumber .. $highnet) { $netnum = $_ << 16; #convert back to the real network value $net = inet_ntoa(pack('N', $netnum)); $net =~ s/\.[^\.]*$//; $net =~ s/\.[^\.]*$//; push @zones, join('.', reverse(split('\.', $net))) . '.IN-ADDR.ARPA.'; } return @zones; } else { #class a (theoretically larger, but those shouldn't exist) my $tempnumber = ($netnum >> 24); #the last two bytes are insignificant, shift them off to make math easier $masknum = $masknum >> 24; my $highnet = $tempnumber | (0xff & ~$masknum); foreach ($tempnumber .. $highnet) { $netnum = $_ << 24; #convert back to the real network value $net = inet_ntoa(pack('N', $netnum)); $net =~ s/\.[^\.]*$//; $net =~ s/\.[^\.]*$//; $net =~ s/\.[^\.]*$//; push @zones, join('.', reverse(split('\.', $net))) . '.IN-ADDR.ARPA.'; } return @zones; } } sub putmyselffirst { my $srvlist = shift; if ($srvlist =~ /,/) { #TODO: only reshuffle when requested, or allow opt out of reshuffle? my @dnsrvs = split /,/, $srvlist; my @reordered; foreach (@dnsrvs) { if (xCAT::NetworkUtils->thishostisnot($_)) { push @reordered, $_; } else { unshift @reordered, $_; } } $srvlist = join(', ', @reordered); } return $srvlist; } sub addnet6 { if ($::XCATSITEVALS{externaldhcpservers}) { return; } if (!$usingipv6) { return; } my $netentry = shift; my $net = $netentry->{net}; my $iface = $netentry->{iface}; my $idx = 0; if (grep /\} # $net subnet_end/, @dhcp6conf) { #need to add to dhcp6conf return; } else { #need to add to dhcp6conf $restartdhcp6 = 1; while ($idx <= $#dhcp6conf) { if ($dhcp6conf[$idx] =~ /\} # $iface nic_end/) { last; } $idx++; } unless ($dhcp6conf[$idx] =~ /\} # $iface nic_end\n/) { $callback->( { error => ["Could not add the subnet $net for interface $iface into $dhcpconffile.\nPlease verify the xCAT database matches networks defined on this system."], errorcode => [1] } ); return 1; } } my $dhcplease = 43200; if (defined $::XCATSITEVALS{'dhcplease'} && $::XCATSITEVALS{'dhcplease'} ne "") { $dhcplease = $::XCATSITEVALS{'dhcplease'}; } my @netent = ( " subnet6 $net {\n", " authoritative;\n", " max-lease-time $dhcplease;\n", " min-lease-time $dhcplease;\n", " default-lease-time $dhcplease;\n", ); #for now, just do address allocatios (phase 1) #phase 2 (by 2.6 presumably) will include the various things like DNS server and other options allowed by dhcpv6 #gateway is *not* currently allowed to be DHCP designated, router advertises its own self indpendent of dhcp. We'll just keep it that way #domain search list is allowed (rfc 3646) #nis domain is also an alloed option (rfc 3898) #sntp server list (rfc 4075) #ntp server rfc 5908 #fqdn rfc 4704 #posix timezone rfc 4833/tzdb timezone #phase 3 will include whatever is required to do Netboot6. That might be in the october timeframe for lack of implementations to test #boot url/param (rfc 59070) my $netdomain = $netcfgs{$net}->{domain}; unless ($netdomain) { $netdomain = $site_domain; } push @netent, " option domain-name \"" . $netdomain . "\";\n"; # add domain-search if not sles10 or rh5 my $osv = xCAT::Utils->osver(); unless (($osv =~ /^sle[sc]10/) || ($osv =~ /^rh.*5$/)) { # We want something like "option domain-search "foo.com", "bar.com";" my $domainstring = qq~"$netcfgs{$net}->{domain}"~; foreach my $dom (@alldomains) { chomp $dom; if ($dom ne $netcfgs{$net}->{domain}) { $domainstring .= qq~, "$dom"~; } } if ($netcfgs{$net}->{domain}) { push @netent, " option domain-search $domainstring;\n"; } } my $nameservers = $netcfgs{$net}->{nameservers}; if ($nameservers and $nameservers =~ /:/) { push @netent, " nameservers " . $netcfgs{$net}->{nameservers} . ";\n"; } my $ddnserver = $nameservers; $ddnserver =~ s/,.*//; my $ddnsdomain; if ($netcfgs{$net}->{ddnsdomain}) { $ddnsdomain = $netcfgs{$net}->{ddnsdomain}; } if ($::XCATSITEVALS{dnshandler} =~ /ddns/) { my $settings = _omapi_settings(); return 1 unless $settings; if ($ddnsdomain) { push @netent, " ddns-domainname \"" . $ddnsdomain . "\";\n"; push @netent, " zone $ddnsdomain. {\n"; } else { push @netent, " zone $netdomain. {\n"; } push @netent, " primary $ddnserver; key $settings->{key_name}; \n"; push @netent, " }\n"; foreach (getzonesfornet($net)) { push @netent, " zone $_ {\n"; push @netent, " primary $ddnserver; key $settings->{key_name}; \n"; push @netent, " }\n"; } } if ($netcfgs{$net}->{range}) { push @netent, " range6 " . $netcfgs{$net}->{range} . ";\n"; } else { $callback->({ warning => ["No dynamic range specified for $net. Hosts with no static address will receive no addresses on this subnet."] }); } push @netent, " } # $net subnet_end\n"; splice(@dhcp6conf, $idx, 0, @netent); } sub addnet { if ($::XCATSITEVALS{externaldhcpservers}) { return; } my $net = shift; my $mask = shift; my $nic; my $domain; my $httpport="80"; my @hports=xCAT::TableUtils->get_site_attribute("httpport"); if ($hports[0]){ $httpport=$hports[0]; } my $portsuffix = ( $httpport eq "80" ) ? "" : ":$httpport"; my $firstoctet = $net; $firstoctet =~ s/^(\d+)\..*/$1/; if ($net eq "169.254.0.0" or ($firstoctet >= 224 and $firstoctet <= 239)) { return; } unless (grep /\} # $net\/$mask subnet_end/, @dhcpconf) { $restartdhcp = 1; foreach (@nrn) { # search for relevant NIC my @ent = split /:/; $firstoctet = $ent[0]; $firstoctet =~ s/^(\d+)\..*/$1/; if ($ent[0] eq "169.254.0.0" or ($firstoctet >= 224 and $firstoctet <= 239)) { next; } if ($ent[0] eq $net and $ent[2] eq $mask) { $nic = $ent[1]; if ($nic =~ /!remote!\S+/) { $nic =~ s/!remote!\s*(.*)$/$1/; } # The first nic that matches the network, # what will happen if there are more than one nics in the same subnet, # and we want to use the second nic as the dhcp interfaces? # this is a TODO last; } } #print " add $net $mask under $nic\n"; my $idx = 0; if ($^O ne 'aix') { while ($idx <= $#dhcpconf) { if ($dhcpconf[$idx] =~ /\} # $nic nic_end\n/) { last; } $idx++; } unless ($dhcpconf[$idx] =~ /\} # $nic nic_end\n/) { $callback->( { error => ["Could not add the subnet $net for interface $nic into $dhcpconffile.\nPlease verify the xCAT database matches networks defined on this system."], errorcode => [1] } ); return 1; } } # if here, means we found the idx before which to insert my $nettab = xCAT::Table->new("networks"); my $nameservers; my $ntpservers; my $logservers; my $gateway; my $tftp; my $range; my $myip; my $mtu; my @myipd = xCAT::NetworkUtils->my_ip_facing($net); unless ($myipd[0]) { $myip = $myipd[1]; } if ($nettab) { my $mask_formated = $mask; if ($^O eq 'aix') { my $mask_shift = 32 - $mask; $mask_formated = inet_ntoa(pack("N", 2**$mask - 1 << $mask_shift)); # $mask_formated = inet_ntoa(pack("N", 2**$mask - 1 << (32 - $mask))); } my ($ent) = $nettab->getAttribs({ net => $net, mask => $mask_formated }, qw(tftpserver nameservers ntpservers logservers gateway dynamicrange dhcpserver domain mtu)); if ($ent and $ent->{ntpservers}) { $ntpservers = $ent->{ntpservers}; } elsif ($sitentpservers) { $ntpservers = $sitentpservers; } if ($ent and $ent->{logservers}) { $logservers = $ent->{logservers}; } elsif ($sitelogservers) { $logservers = $sitelogservers; } if ($ent and $ent->{domain}) { $domain = $ent->{domain}; } elsif ($site_domain) { $domain = $site_domain; } else { $callback->( { error => [ "No domain defined for $net entry in networks table, and no domain defined in site table." ], errorcode => [1] }); $checkdomain=1; } if ($ent and $ent->{nameservers}) { $nameservers = $ent->{nameservers}; } else { if ($sitenameservers) { $nameservers = $sitenameservers; } else { $callback->( { warning => [ "No $net specific entry for nameservers, and no nameservers defined in site table." ] } ); } } # convert to nameserver IP $nameservers =~ s//$myip/g; if (!$ntpservers || ($ntpservers eq '')) { $ntpservers = $myip; } $nameservers = putmyselffirst($nameservers); $ntpservers = putmyselffirst($ntpservers); $logservers = putmyselffirst($logservers); if ($ent and $ent->{tftpserver}) { $tftp = $ent->{tftpserver}; } if (!$tftp || ($tftp eq '')) { $tftp = $myip; } if ($ent and $ent->{gateway}) { $gateway = $ent->{gateway}; if ($gateway eq '') { if (xCAT::NetworkUtils->ip_forwarding_enabled()) { $gateway = $myip; } else { $gateway = ''; } } } if ($ent and $ent->{dynamicrange}) { unless ($ent->{dhcpserver} and xCAT::NetworkUtils->thishostisnot($ent->{dhcpserver})) { #If specific, only one dhcp server gets a dynamic range $range = join ';', xCAT::DHCP::Range->isc_ranges($ent->{dynamicrange}); } } else { $callback->( { warning => [ "No dynamic range specified for $net. If hardware discovery is being used, a dynamic range is required." ] } ); } if ($ent and $ent->{mtu}) { $mtu = $ent->{mtu}; } } else { $callback->( { error => ["Unable to open networks table, please run makenetworks"], errorcode => [1] } ); return 1; } if ($^O eq 'aix') { return gen_aix_net($myip, $net, $mask, $gateway, $tftp, $logservers, $ntpservers, $domain, $nameservers, $range, $mtu); } my @netent; my $maskn = unpack("N", inet_aton($mask)); my $netn = unpack("N", inet_aton($net)); my $dhcplease = 43200; if (defined $::XCATSITEVALS{'dhcplease'} && $::XCATSITEVALS{'dhcplease'} ne "") { $dhcplease = $::XCATSITEVALS{'dhcplease'}; } @netent = ( " subnet $net netmask $mask {\n", " authoritative;\n", " max-lease-time $dhcplease;\n", " min-lease-time $dhcplease;\n", " default-lease-time $dhcplease;\n" ); if ($gateway) { my $gaten = unpack("N", inet_aton($gateway)); if (($gaten & $maskn) == ($maskn & $netn)) { push @netent, " option routers $gateway;\n"; } else { $callback->( { error => [ "Specified gateway $gateway is not valid for $net/$mask, must be on same network" ], errorcode => [1] } ); } } if ($tftp) { push @netent, " next-server $tftp;\n"; } if ($logservers) { push @netent, " option log-servers $logservers;\n"; } elsif ($myip) { push @netent, " option log-servers $myip;\n"; } if ($ntpservers) { push @netent, " option ntp-servers $ntpservers;\n"; } if ($nameservers) { push @netent, " option domain-name \"$domain\";\n"; push @netent, " option domain-name-servers $nameservers;\n"; } if ($mtu) { push @netent, " option interface-mtu $mtu;\n"; } # add domain-search if not sles10 or rh5 my $osv = xCAT::Utils->osver(); unless (($osv =~ /^sle[sc]10/) || ($osv =~ /^rh.*5$/)) { # want something like "option domain-search "foo.com", "bar.com";" my $domainstring = qq~"$domain"~; foreach my $dom (@alldomains) { chomp $dom; if ($dom ne $domain) { $domainstring .= qq~, "$dom"~; } } if ($domain) { push @netent, " option domain-search $domainstring;\n"; } } #for cumulus ZTP process push @netent, " option cumulus-provision-url \"http://$tftp$portsuffix/install/postscripts/cumulusztp\";\n"; my $ddnserver = $nameservers; $ddnserver =~ s/,.*//; my $ddnsdomain; if ($netcfgs{$net}->{ddnsdomain}) { $ddnsdomain = $netcfgs{$net}->{ddnsdomain}; } if ($::XCATSITEVALS{dnshandler} =~ /ddns/) { my $settings = _omapi_settings(); return 1 unless $settings; if ($ddnsdomain) { push @netent, " ddns-domainname \"" . $ddnsdomain . "\";\n"; push @netent, " zone $ddnsdomain. {\n"; } else { push @netent, " zone $domain. {\n"; } if ($ddnserver) { push @netent, " primary $ddnserver; key $settings->{key_name}; \n"; } push @netent, " }\n"; foreach (getzonesfornet($net, $mask)) { push @netent, " zone $_ {\n"; if ($ddnserver) { push @netent, " primary $ddnserver; key $settings->{key_name}; \n"; } push @netent, " }\n"; } } my $tmpmaskn = unpack("N", inet_aton($mask)); my $maskbits = 32; while (not($tmpmaskn & 1)) { $maskbits--; $tmpmaskn = $tmpmaskn >> 1; } # $lstatements = 'if exists gpxe.bus-id { filename = \"\"; } else if exists client-architecture { filename = \"xcat/xnba.kpxe\"; } '.$lstatements; push @netent, " if option user-class-identifier = \"xNBA\" and option client-architecture = 00:00 { #x86, xCAT Network Boot Agent\n"; push @netent, " always-broadcast on;\n"; push @netent, " filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/" . $net . "_" . $maskbits . "\";\n"; push @netent, " } else if option user-class-identifier = \"xNBA\" and option client-architecture = 00:09 { #x86, xCAT Network Boot Agent\n"; push @netent, " filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/" . $net . "_" . $maskbits . ".uefi\";\n"; push @netent, " } else if option user-class-identifier = \"xNBA\" and option client-architecture = 00:07 { #x86-64 UEFI, xCAT Network Boot Agent\n"; push @netent, " filename = \"http://$tftp$portsuffix/tftpboot/xcat/xnba/nets/" . $net . "_" . $maskbits . ".uefi\";\n"; push @netent, " } else if option client-architecture = 00:00 { #x86\n"; push @netent, " filename \"xcat/xnba.kpxe\";\n"; push @netent, " } else if option vendor-class-identifier = \"Etherboot-5.4\" { #x86\n"; push @netent, " filename \"xcat/xnba.kpxe\";\n"; push @netent, " } else if option client-architecture = 00:07 { #x86_64 uefi\n "; push @netent, " filename \"xcat/xnba.efi\";\n"; push @netent, " } else if option client-architecture = 00:09 { #x86_64 uefi alternative id\n "; push @netent, " filename \"xcat/xnba.efi\";\n"; push @netent, " } else if option client-architecture = 00:02 { #ia64\n "; push @netent, " filename \"elilo.efi\";\n"; push @netent, " } else if option client-architecture = 00:0b { #aaarch64\n "; push @netent, " filename \"boot/grub2/grub2.aarch64\";\n"; push @netent, " } else if option client-architecture = 00:0e { #OPAL-v3\n "; push @netent, " option conf-file = \"http://$tftp$portsuffix/tftpboot/pxelinux.cfg/p/" . $net . "_" . $maskbits . "\";\n"; push @netent, " } else if substring (option vendor-class-identifier,0,11) = \"onie_vendor\" { #for onie on cumulus switch\n"; push @netent, " option www-server = \"http://$tftp$portsuffix/install/onie/onie-installer\";\n"; push @netent, " } else if substring(filename,0,1) = null { #otherwise, provide yaboot if the client isn't specific\n "; push @netent, " filename \"/yaboot\";\n"; push @netent, " }\n"; if ($range) { foreach my $singlerange (split /;/, $range) { push @netent, " range dynamic-bootp $singlerange;\n" } } push @netent, " } # $net\/$mask subnet_end\n"; splice(@dhcpconf, $idx, 0, @netent); } } ###################################################### # Generate network configuration for aix ###################################################### sub gen_aix_net { my $myip = shift; my $net = shift; my $mask = shift; my $gateway = shift; my $tftp = shift; my $logservers = shift; my $ntpservers = shift; my $domain = shift; my $nameservers = shift; my $range = shift; my $mtu = shift; my $idx = 0; while ($idx <= $#dhcpconf) { if ($dhcpconf[$idx] =~ /#Network configuration end\n/) { last; } $idx++; } unless ($dhcpconf[$idx] =~ /#Network configuration end\n/) { return 1; #TODO: this is an error condition } $range =~ s/ /-/; my @netent = ("network $net $mask\n{\n"); if ($gateway) { if ($gateway eq '') { if (xCAT::NetworkUtils->ip_forwarding_enabled()) { $gateway = $myip; } else { $gateway = ''; } } if (xCAT::NetworkUtils::isInSameSubnet($gateway, $net, $mask, 1)) { push @netent, " option 3 $gateway\n"; } else { $callback->( { error => [ "Specified gateway $gateway is not valid for $net/$mask, must be on same network" ], errorcode => [1] } ); } } # if ($tftp) # { # push @netent, " option 66 $tftp\n"; # } if ($logservers) { $logservers =~ s/,/ /g; push @netent, " option 7 $logservers\n"; } elsif ($myip) { push @netent, " option 7 $myip\n"; } if ($mtu) { push @netent, " option 26 $mtu\n"; } if ($ntpservers) { $ntpservers =~ s/,/ /g; push @netent, " option 42 $ntpservers\n"; } elsif ($myip) { push @netent, " option 42 $myip\n"; } push @netent, " option 15 \"$domain\"\n"; if ($nameservers) { $nameservers =~ s/,/ /g; push @netent, " option 6 $nameservers\n"; } push @netent, " subnet $net $range\n {\n"; push @netent, " } # $net/$mask ip configuration end\n"; push @netent, "} # $net/$mask subnet_end\n\n"; splice(@dhcpconf, $idx, 0, @netent); } sub addnic { if ($::XCATSITEVALS{externaldhcpservers}) { return; } my $nic = shift; my $conf = shift; my $firstindex = 0; my $lastindex = 0; unless (grep /} # $nic nic_end/, @$conf) { #add a section if not there #$restartdhcp=1; #print "Adding NIC $nic\n"; if ($nic eq '!remote!') { push @$conf, "#shared-network $nic {\n"; push @$conf, "#\} # $nic nic_end\n"; } else { push @$conf, "shared-network $nic {\n"; push @$conf, "\} # $nic nic_end\n"; } } #return; #Don't touch it, it should already be fine.. #my $idx=0; #while ($idx <= $#dhcpconf) { # if ($dhcpconf[$idx] =~ /^shared-network $nic {/) { # $firstindex = $idx; # found the first place to chop... # } elsif ($dhcpconf[$idx] =~ /} # $nic network_end/) { # $lastindex=$idx; # } # $idx++; #} #print Dumper(\@dhcpconf); #if ($firstindex and $lastindex) { # splice @dhcpconf,$firstindex,($lastindex-$firstindex+1); #} #print Dumper(\@dhcpconf); } sub writeout { if ($::XCATSITEVALS{externaldhcpservers}) { return; } # add the new entries to the dhcp config file my $targ; open($targ, '>', $dhcpconffile); my $idx; my $skipone; foreach $idx (0 .. $#dhcpconf) { #avoid writing out empty shared network declarations if ($dhcpconf[$idx] =~ /^shared-network/ and $dhcpconf[ $idx + 1 ] =~ /^} .* nic_end/) { $skipone = 1; next; } elsif ($skipone) { $skipone = 0; next; } print $targ $dhcpconf[$idx]; } if ($^O eq 'aix') { # add back any NIM entries that were saved earlier if (@aixcfg) { foreach $idx (0 .. $#aixcfg) { print $targ $aixcfg[$idx]; } } } close($targ); @dhcpconf = (); #dispose of the file contents in memory, no longer needed @aixcfg = (); if (@dhcp6conf) { open($targ, '>', $dhcp6conffile); foreach $idx (0 .. $#dhcp6conf) { if ($dhcp6conf[$idx] =~ /^shared-network/ and $dhcp6conf[ $idx + 1 ] =~ /^} .* nic_end/) { $skipone = 1; next; } elsif ($skipone) { $skipone = 0; next; } print $targ $dhcp6conf[$idx]; } close($targ); @dhcp6conf = (); } } sub newconfig6 { if ($::XCATSITEVALS{externaldhcpservers}) { return; } my $settings = _omapi_settings(); return 1 unless $settings; #phase 1, basic working #phase 2, ddns too, evaluate other stuff from dhcpv4 as applicable push @dhcp6conf, "#xCAT generated dhcp configuration\n"; push @dhcp6conf, "\n"; push @dhcp6conf, "ddns-update-style interim;\n"; push @dhcp6conf, "ignore client-updates;\n"; # push @dhcp6conf, "update-static-leases on;\n"; push @dhcp6conf, "omapi-port 7912;\n"; #Enable omapi... push @dhcp6conf, "key $settings->{key_name} {\n"; push @dhcp6conf, " algorithm $settings->{algorithm};\n"; my $passtab = xCAT::Table->new('passwd', -create => 1); (my $passent) = $passtab->getAttribs({ key => 'omapi', username => $settings->{key_name} }, 'password'); my $secret = encode_base64(genpassword(32)); #Random from set of 62^32 chomp $secret; if ($passent && $passent->{password}) { $secret = $passent->{password}; } else { $callback->( { data => ["The dhcp server must be restarted for OMAPI function to work"] } ); $passtab->setAttribs({ key => 'omapi', username => $settings->{key_name} }, { username => $settings->{key_name}, password => $secret }); } push @dhcp6conf, " secret \"" . $secret . "\";\n"; push @dhcp6conf, "};\n"; push @dhcp6conf, "omapi-key $settings->{key_name};\n"; #that is all for pristine ipv6 config } sub newconfig { if ($::XCATSITEVALS{externaldhcpservers}) { return; } return newconfig_aix() if ($^O eq 'aix'); my $settings = _omapi_settings(); return 1 unless $settings; # This function puts a standard header in and enough to make omapi work. my $passtab = xCAT::Table->new('passwd', -create => 1); push @dhcpconf, "#xCAT generated dhcp configuration\n"; push @dhcpconf, "\n"; push @dhcpconf, "option conf-file code 209 = text;\n"; push @dhcpconf, "option space isan;\n"; push @dhcpconf, "option isan-encap-opts code 43 = encapsulate isan;\n"; push @dhcpconf, "option isan.iqn code 203 = string;\n"; push @dhcpconf, "option isan.root-path code 201 = string;\n"; push @dhcpconf, "option space gpxe;\n"; push @dhcpconf, "option gpxe-encap-opts code 175 = encapsulate gpxe;\n"; push @dhcpconf, "option gpxe.bus-id code 177 = string;\n"; push @dhcpconf, "option user-class-identifier code 77 = string;\n"; push @dhcpconf, "option gpxe.no-pxedhcp code 176 = unsigned integer 8;\n"; push @dhcpconf, "option tcode code 101 = text;\n"; push @dhcpconf, "option iscsi-initiator-iqn code 203 = string;\n"; #Only via gPXE, not a standard push @dhcpconf, "ddns-update-style interim;\n"; push @dhcpconf, "ignore client-updates;\n"; #Windows clients like to do all caps, very un xCAT-like # push @dhcpconf, "update-static-leases on;\n"; #makedns rendered optional push @dhcpconf, "option client-architecture code 93 = unsigned integer 16;\n"; if ($::XCATSITEVALS{timezone}) { push @dhcpconf, "option tcode \"" . $::XCATSITEVALS{timezone} . "\";\n"; } push @dhcpconf, "option gpxe.no-pxedhcp 1;\n"; push @dhcpconf, "option www-server code 114 = string;\n"; push @dhcpconf, "option cumulus-provision-url code 239 = text;\n"; push @dhcpconf, "\n"; push @dhcpconf, "omapi-port 7911;\n"; #Enable omapi... push @dhcpconf, "key $settings->{key_name} {\n"; push @dhcpconf, " algorithm $settings->{algorithm};\n"; (my $passent) = $passtab->getAttribs({ key => 'omapi', username => $settings->{key_name} }, 'password'); my $secret = encode_base64(genpassword(32)); #Random from set of 62^32 chomp $secret; if ($passent && $passent->{password}) { $secret = $passent->{password}; } else { $callback->( { data => ["The dhcp server must be restarted for OMAPI function to work"] } ); $passtab->setAttribs({ key => 'omapi', username => $settings->{key_name} }, { username => $settings->{key_name}, password => $secret }); } push @dhcpconf, " secret \"" . $secret . "\";\n"; push @dhcpconf, "};\n"; push @dhcpconf, "omapi-key $settings->{key_name};\n"; push @dhcpconf, ('class "pxe" {' . "\n", " match if substring (option vendor-class-identifier, 0, 9) = \"PXEClient\";\n", " ddns-updates off;\n", " max-lease-time 600;\n", "}\n"); } sub newconfig_aix { push @dhcpconf, "#xCAT generated dhcp configuration\n"; push @dhcpconf, "\n"; #push @dhcpconf, "numLogFiles 4\n"; #push @dhcpconf, "logFileSize 100\n"; #push @dhcpconf, "logFileName /var/log/dhcpsd.log\n"; #push @dhcpconf, "logItem SYSERR\n"; #push @dhcpconf, "logItem OBJERR\n"; #push @dhcpconf, "logItem PROTERR\n"; #push @dhcpconf, "logItem WARNING\n"; #push @dhcpconf, "logItem EVENT\n"; #push @dhcpconf, "logItem ACTION\n"; #push @dhcpconf, "logItem INFO\n"; #push @dhcpconf, "logItem ACNTING\n"; #push @dhcpconf, "logItem TRACE\n"; push @dhcpconf, "leaseTimeDefault 43200 seconds\n"; push @dhcpconf, "#Network configuration begin\n"; push @dhcpconf, "#Network configuration end\n"; } sub genpassword { #Generate a pseudo-random password of specified length my $length = shift; my $password = ''; my $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890'; srand; #have to reseed, rand is not rand otherwise while (length($password) < $length) { $password .= substr($characters, int(rand 63), 1); } return $password; } 1;