#!/usr/bin/env perl # IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html package xCAT_plugin::nodediscover; use xCAT::Table; use IO::Socket; use strict; use XML::Simple; $XML::Simple::PREFERRED_PARSER = 'XML::Parser'; use POSIX "WNOHANG"; use Storable qw(freeze thaw); use IO::Select; use IO::Handle; use xCAT::Utils; use Sys::Syslog; use Text::Balanced qw(extract_bracketed); use xCAT::data::switchinfo; use xCAT::DiscoveryUtils; sub gethosttag { #This function tries to return a good hostname for a node based on the #network to which it is connected (by $netn or maybe $ifname) #heuristic: #if the client had a valid IP address from a dhcp server, that is used as key #once the matching network is found, and an explicit mapping defined, try that #next, try to see if the ip for the case where hostname==nodename is on this net, if so, return that #next, try to do nodename-ifname, return that if successful #next, repeat process for all networks that have the common mgtifname field #return undef for now if none of the above worked my $node = shift; my $netn = shift; my $ifname = shift; my $usednames = shift; my %netmap = %{ xCAT::NetworkUtils::my_if_netmap() }; my $mgtifname = $netmap{$netn}; my $secondpass = 0; my $name = ""; my $defhost = inet_aton($node); my $nettab = xCAT::Table->new('networks'); my $defn = ""; my @netents = @{ $nettab->getAllEntries() }; my $pass; #TODO: mgtifname field will get trounced in hierarchical setup, use a live check to match accurately foreach (@netents) { if ($_->{net} eq $netn or ($mgtifname and $mgtifname eq $netmap{ $_->{net} })) { #either is the network or shares physical interface if ($_->{nodehostname}) { #Check for a nodehostname rule in the table $name = $node; if ($_->{nodehostname} =~ /^\/[^\/]*\/[^\/]*\/$/) { my $exp = substr($_->{nodehostname}, 1); chop $exp; my @parts = split('/', $exp, 2); $name =~ s/$parts[0]/$parts[1]/; } elsif ($_->{nodehostname} =~ /^\|.*\|.*\|$/) { #Perform arithmetic and only arithmetic operations in bracketed issues on the right. #Tricky part: don't allow potentially dangerous code, only eval if #to-be-evaled expression is only made up of ()\d+-/%$ #Futher paranoia? use Safe module to make sure I'm good my $exp = substr($_->{nodehostname}, 1); chop $exp; my @parts = split('\|', $exp, 2); my $curr; my $next; my $prev; my $retval = $parts[1]; ($curr, $next, $prev) = extract_bracketed($retval, '()', qr/[^()]*/); unless ($curr) { #If there were no paramaters to save, treat this one like a plain regex $name =~ s/$parts[0]/$parts[1]/; } while ($curr) { #my $next = $comps[0]; if ($curr =~ /^[\{\}()\-\+\/\%\*\$\d]+$/ or $curr =~ /^\(sprintf\(["'%\dcsduoxefg]+,\s*[\{\}()\-\+\/\%\*\$\d]+\)\)$/) { use integer; #We only allow integer operations, they are the ones that make sense for the application my $value = $name; $value =~ s/$parts[0]/$curr/ee; $retval = $prev . $value . $next; } else { print "$curr is bad\n"; } ($curr, $next, $prev) = extract_bracketed($retval, '()', qr/[^()]*/); } #At this point, $retval is the expression after being arithmetically contemplated, a generated regex, and therefore #must be applied in total $name =~ s/$parts[0]/$retval/; #print Data::Dumper::Dumper(extract_bracketed($parts[1],'()',qr/[^()]*/)); #use text::balanced extract_bracketed to parse earch atom, make sure nothing but arith operators, parans, and numbers are in it to guard against code execution } print "Name: $name\n"; #$name =~ s/$left/$right/; if ($name and inet_aton($name)) { if ($netn eq $_->{net} and not $usednames->{$name}) { return $name; } #At this point, it could still be valid if block was entered due to mgtifname my $nnetn = inet_ntoa(pack("N", unpack("N", inet_aton($name)) & unpack("N", inet_aton($_->{mask})))); if ($nnetn eq $_->{net} and not $usednames->{$name}) { return $name; } } $name = ""; #Still here, this branch failed } $defn = ""; if ($defhost) { $defn = inet_ntoa(pack("N", unpack("N", $defhost) & unpack("N", inet_aton($_->{mask})))); } if ($defn eq $_->{net} and not $usednames->{$node}) { #the default nodename is on this network return $node; } my $tentativehost = $node . "-" . $ifname; my $tnh = inet_aton($tentativehost); if ($tnh) { my $nnetn = inet_ntoa(pack("N", unpack("N", $tnh) & unpack("N", inet_aton($_->{mask})))); if ($nnetn eq $_->{net} and not $usednames->{$tentativehost}) { return $tentativehost; } } } } } sub handled_commands { return { #discovered => 'chain:ondiscover', discovered => 'nodediscover', }; } sub process_request { my $request = shift; my $callback = shift; my $doreq = shift; my $node = $request->{node}->[0]; my $clientip = $request->{'_xcat_clientip'}; openlog("xcat", '', 'local0'); #First, fill in tables with data fields.. if (defined($request->{mtm}) or defined($request->{serial})) { my $vpdtab = xCAT::Table->new("vpd", -create => 1); if ($request->{uuid}->[0]) { $vpdtab->setNodeAttribs($node, { uuid => $request->{uuid}->[0] }); } if ($request->{mtm}->[0]) { $vpdtab->setNodeAttribs($node, { mtm => $request->{mtm}->[0] }); } if ($request->{serial}) { $vpdtab->setNodeAttribs($node, { serial => $request->{serial}->[0] }); } } # if there is no bmc ip in ipmi table, save bmc ip into ipmi table if (defined($request->{bmc}) && $request->{bmc}->[0]) { my $ipmitab = xCAT::Table->new("ipmi", -create => 1); if ($ipmitab) { my $ipmient = $ipmitab->getNodeAttribs($node, ['bmc']); unless ($ipmient->{'bmc'}) { $ipmitab->setNodeAttribs($node, { bmc => $request->{bmc}->[0] }); } } else { $callback->({ error => ["Open ipmi table failed."], errorcode => ["1"] }); } } # save inventory info into the hwinv table if (defined($request->{cpucount}) or defined($request->{cputype}) or defined($request->{memory}) or defined($request->{disksize})) { my $basicdata; my $hwinv_tab = xCAT::Table->new("hwinv", -create => 1); if ($request->{memory}->[0]) { $basicdata->{memory} = $request->{memory}->[0]; } if ($request->{disksize}->[0]) { $basicdata->{disksize} = $request->{disksize}->[0]; } if ($request->{cpucount}->[0]) { $basicdata->{cpucount} = $request->{cpucount}->[0]; } if ($request->{cputype}->[0]) { $basicdata->{cputype} = $request->{cputype}->[0]; } $hwinv_tab->setNodeAttribs($node, $basicdata); } my $nrtab; my @discoverynics; my @forcenics; #list of 'eth' style interface names to require to come up on post-discovery client dhcp restart if (defined($request->{arch})) { #Set the architecture in nodetype. If 32-bit only x86 or ppc detected, overwrite. If x86_64, only set if either not set or not an x86 family my $typetab = xCAT::Table->new("nodetype", -create => 1); (my $nent) = $typetab->getNodeAttribs($node, [ 'arch', 'supportedarchs' ]); if ($request->{arch}->[0] =~ /x86_64/) { if ($nent and ($nent->{arch} =~ /x86/)) { #If already an x86 variant, do not change unless ($nent and $nent->{supportedarchs} =~ /x86_64/) { $typetab->setNodeAttribs($node, { supportedarchs => "x86,x86_64" }); } } else { $typetab->setNodeAttribs($node, { arch => $request->{arch}->[0], supportedarchs => "x86,x86_64" }); #this check is so that if an admin explicitly declares a node 'x86', the 64 bit capability is ignored } } else { unless ($nent and $nent->{supportedarchs} eq $request->{arch}->[0] and $nent->{arch} eq $request->{arch}->[0]) { $typetab->setNodeAttribs($node, { arch => $request->{arch}->[0], supportedarchs => $request->{arch}->[0] }); } if ($request->{arch}->[0] =~ /ppc/ and $request->{platform}->[0] =~ /PowerNV/) { $typetab->setNodeAttribs($node, { nodetype => 'mp' }); } } my $currboot = ''; $nrtab = xCAT::Table->new('noderes'); #Attempt to check and set if wrong the netboot method on discovery, if admin omitted (my $rent) = $nrtab->getNodeAttribs($node, [ 'netboot', 'discoverynics' ]); if ($rent and defined $rent->{discoverynics}) { @discoverynics = split /,/, $rent->{discoverynics}; } if ($rent and $rent->{'netboot'}) { $currboot = $rent->{'netboot'}; } if ($request->{arch}->[0] =~ /x86/ and $currboot !~ /pxe/ and $currboot !~ /xnba/) { $nrtab->setNodeAttribs($node, { netboot => 'xnba' }); } elsif ($request->{arch}->[0] =~ /ppc/ and $request->{platform}->[0] =~ /PowerNV/) { $nrtab->setNodeAttribs($node, { netboot => 'petitboot' }); } elsif ($request->{arch}->[0] =~ /ppc/ and $currboot !~ /yaboot/) { $nrtab->setNodeAttribs($node, { netboot => 'yaboot' }); } elsif($request->{arch}->[0] =~ /armv7l/ and $currboot !~ /onie/) { #for onie switch, the netboot should be "onie" $nrtab->setNodeAttribs($node, { netboot => 'onie' }); } } if(defined $request->{nodetype} and $request->{nodetype}->[0] = 'switch' and $request->{_xcat_clientmac}->[0]){ #for onie switch, lookup and set the switchtype via mac of mgt interface my $switchestab = xCAT::Table->new('switches'); if ($switchestab) { my $switchtype; my $switchvendor=$xCAT::data::switchinfo::global_mac_identity{substr(lc($request->{_xcat_clientmac}->[0]),0,8)}; if(defined $switchvendor){ my $search_string = join '|', keys(%xCAT::data::switchinfo::global_switch_type); if ($switchvendor =~ /($search_string)/) { $switchtype=$xCAT::data::switchinfo::global_switch_type{$1}; } } if($switchtype){ $switchestab->setNodeAttribs($node,{ switchtype => $switchtype }); } $switchestab->close(); } } my $macstring = ""; if (defined($request->{mac})) { if (!inet_aton($node)) { xCAT::MsgUtils->message("S", "xcat.discovery.nodediscover: Can not resolve IP for the matching node:$node. Make sure \"makehosts\" and \"makedns\" have been run for $node."); $request->{error} = [0]; $request->{error_msg} = ["Can not resolve IP for the matching node:$node."]; return; } my $mactab = xCAT::Table->new("mac", -create => 1); my @ifinfo; my %usednames; my %usednames_for_net; my @hostnames_to_update = (); my %bydriverindex; my $forcenic = 0; #-1 is force skip, 0 is use default behavior, 1 is force to be declared even if hosttag is skipped to do so my $localnic = 0; foreach (@{ $request->{mac} }) { @ifinfo = split /\|/; if ($ifinfo[1] eq 'usb0') { #skip usb nic next; } $bydriverindex{ $ifinfo[0] } += 1; if (scalar @discoverynics) { $forcenic = -1; #$forcenic defaults to explicitly skip nic foreach my $nic (@discoverynics) { if ($nic =~ /:/) { #syntax like 'bnx2:0' to say the first bnx2 managed interface (my $driver, my $index) = split /:/, $nic; if ($driver eq $ifinfo[0] and $index == ($bydriverindex{$driver} - 1)) { $forcenic = 1; #force nic to be put into database push @forcenics, $ifinfo[1]; last; } } else { #simple 'eth2' sort of argument if ($nic eq $ifinfo[1]) { push @forcenics, $ifinfo[1]; $forcenic = 1; last; } } } } if ($forcenic == -1) { #if force to skip, go to next nic next; } my $currmac = lc($ifinfo[2]); if ($ifinfo[3]) { (my $ip, my $netbits) = split /\//, $ifinfo[3]; if ($ip =~ /\d+\.\d+\.\d+\.\d+/) { my $ipn = unpack("N", inet_aton($ip)); my $mask = 2**$netbits - 1 << (32 - $netbits); my $netn = inet_ntoa(pack("N", $ipn & $mask)); my $hosttag = gethosttag($node, $netn, @ifinfo[1], \%usednames); unless ($hosttag) { my $nettagname = $usednames_for_net{$netn}; # For nics not in the install network, don't deal with them if not an available hostname get # In case another nic in install network get a hosttag other than nodename, need to compare the IP address they can convert to if ($nettagname and (inet_aton($nettagname) eq inet_aton($node))) { $hosttag = "$node-$ifinfo[1]"; push @hostnames_to_update, $hosttag; } } #print Dumper($hosttag) . "\n"; if ($hosttag) { $usednames{$hosttag} = 1; unless ($usednames_for_net{$netn}) { $usednames_for_net{$netn} = $hosttag; } if ($hosttag eq $node) { $macstring .= $currmac . "|"; $localnic = 1; } else { $macstring .= $currmac . "!" . $hosttag . "|"; } # following is for manual discovery by nodediscoverdef to define a undef node to predefine node # this this case, the $clientip is null unless ($clientip) { $clientip = $ip; } } else { if ($forcenic == 1) { $macstring .= $currmac . "|"; } else { $macstring .= $currmac . "!*NOIP*|"; } } } } else { if ($forcenic == 1) { $macstring .= $currmac . "|"; } } } unless ($localnic) { $request->{error} = [1]; $request->{error_msg} = ["No nic found in deploy network for $node"]; return; } $macstring =~ s/\|\z//; $mactab->setNodeAttribs($node, { mac => $macstring }); if (scalar @hostnames_to_update) { my $hosttab = xCAT::Table->new('hosts'); if ($hosttab) { my ($ent) = $hosttab->getNodeAttribs($node, ['hostnames']); if ($ent and $ent->{hostnames}) { my @hostnames_array = split /,/, $ent->{hostnames}; push @hostnames_to_update,@hostnames_array; } my %allhostnames = map { $_=>1 } @hostnames_to_update; my $hostnames = join(",", (keys %allhostnames)); $hosttab->setNodeAttribs($node, { hostnames => $hostnames }); $hosttab->commit(); } my %request = ( command => ['makehosts'], node => [$node] ); $doreq->(\%request); } my %request = ( command => ['makedhcp'], node => [$node] ); $doreq->(\%request); } #TODO: mac table? on the one hand, 'the' definitive interface was determined earlier... #Delete the state it was in to make it traverse destiny once agoin my $chaintab = xCAT::Table->new('chain'); if ($chaintab) { $chaintab->setNodeAttribs($node, { currstate => '', currchain => '' }); $chaintab->close(); } # Update the switch port information if the 'updateswitch' flag is added in the request. # 'updateswitch' is default added for sequential discovery if ($request->{'updateswitch'} && $macstring) { my $firstmac; # Get the mac which defined as the management nic my @macents = split(/\|/, $macstring); foreach my $macent (@macents) { my ($mac, $host) = split(/!/, $macent); unless ($firstmac) { $firstmac = $mac; } if (!$host || $host eq $node) { $firstmac = $mac; last; } } # search the management nic and record the switch informaiton foreach my $nic (@{ $request->{nic} }) { if (defined($nic->{'hwaddr'}) && $nic->{'hwaddr'}->[0] =~ /$firstmac/i) { if (defined($nic->{'switchname'}) && defined($nic->{'switchaddr'})) { # update the switch to switches table my $switchestab = xCAT::Table->new('switches'); if ($switchestab) { $switchestab->setAttribs({ switch => $nic->{'switchname'}->[0] }, { comments => $nic->{'switchdesc'}->[0] }); $switchestab->close(); } # update the ip of switch to hosts table my $hosttab = xCAT::Table->new('hosts'); if ($hosttab) { $hosttab->setNodeAttribs($nic->{'switchname'}->[0], { ip => $nic->{'switchaddr'}->[0] }); $hosttab->commit(); } # add the switch as a node to xcat db my $nltab = xCAT::Table->new('nodelist'); if ($nltab) { $nltab->setNodeAttribs($nic->{'switchname'}->[0], { groups => "all,switch" }); $nltab->commit(); } if (defined($nic->{'switchport'})) { # update the switch table my $switchtab = xCAT::Table->new('switch'); if ($switchtab) { $switchtab->setNodeAttribs($node, { switch => $nic->{'switchname'}->[0], port => $nic->{'switchport'}->[0] }); $switchtab->close(); } } } } } } # make sure the node has the correct ip configured unless ($clientip) { $callback->({ error => ["The node [$node] should have a correct IP address which belongs to the management network."], errorcode => ["1"] }); return; } my $bmc_node = undef; if (defined($request->{bmc_node}) and defined($request->{bmc_node}->[0])) { $bmc_node = $request->{bmc_node}->[0]; } if (-x "/usr/bin/goconserver") { xCAT::MsgUtils->message("S", "xcat.discovery.nodediscover: remove gocons session for $bmc_node"); require xCAT::Goconserver; if (xCAT::Goconserver::is_goconserver_running()) { my $api_url = "https://localhost:". xCAT::Goconserver::get_api_port(); xCAT::Goconserver::delete_nodes($api_url, {"$bmc_node" => 1}, 1, $callback); } } else { xCAT::MsgUtils->message("S", "xcat.discovery.nodediscover: After discovery done, please use 'makeconservercf -C' to clean up undefined nodes from conserver"); } if (defined($request->{bmcinband})) { if (defined($bmc_node)) { xCAT::MsgUtils->message("S", "xcat.discovery.nodediscover: Removing discovered BMC nodes definition: $bmc_node..."); my $rmcmd = "rmdef $bmc_node"; xCAT::Utils->runcmd($rmcmd, 0); if ($::RUNCMD_RC != 0) { xCAT::MsgUtils->message("S", "xcat.discovery.nodediscover: Failed to remove $bmc_node from xCAT"); } else { xCAT::MsgUtils->message("S", "xcat.discovery.nodediscover: $bmc_node definition removed from xCAT"); } } } else { if (defined($bmc_node)) { if ($bmc_node =~ /\,/) { xCAT::MsgUtils->message("W", "Multiple BMC nodes matched with no bmcinband specified, please remove manually"); } else { # Only BMC that doesn't support in-band configuration need to run rspconfig out-of-band, such as S822L running in OPAL model xCAT::MsgUtils->message("S", "No bmcinband specified, need to configure BMC out-of-band"); xCAT::Utils->cleanup_for_powerLE_hardware_discovery($request, $doreq); } } } my $restartstring = "restart"; if (scalar @forcenics > 0) { $restartstring .= " (" . join("|", @forcenics) . ")"; } #now, notify the node to continue life my $sock = new IO::Socket::INET( PeerAddr => $clientip, PeerPort => '3001', Timeout => '1', Proto => 'tcp' ); unless ($sock) { xCAT::MsgUtils->message("S", "xcat.discovery.nodediscover: Failed to notify $clientip that it's actually $node. $node has not been discovered."); return; } print $sock $restartstring; close($sock); #Update the discoverydata table to indicate the successful discovery xCAT::DiscoveryUtils->update_discovery_data($request); xCAT::MsgUtils->message("S", "xcat.discovery.nodediscover: $node has been discovered"); } 1;