# IBM(c) 2012 EPL license http://www.eclipse.org/legal/epl-v10.html #------------------------------------------------------- =head1 xCAT plugin to support PCM node management These commands are designed to be called by PCM GUI. =cut #------------------------------------------------------- package xCAT_plugin::pcmnodes; use strict; use warnings; require xCAT::Table; require xCAT::DBobjUtils; require xCAT::Utils; require xCAT::TableUtils; require xCAT::NetworkUtils; require xCAT::MsgUtils; require xCAT::PCMNodeMgmtUtils; # Globals. # These 2 global variables are for storing the parse result of hostinfo file. # These 2 global varialbes are set in lib xCAT::DBobjUtils->readFileInput. #%::FILEATTRS; #@::fileobjnames; # All database records. my %allhostnames; my %allbmcips; my %allmacs; my %allips; my %allinstallips; my %allnicips; my %allracks; my %allchassis; # Define parameters for xcat requests. my $request; my $callback; my $request_command; my $command; my $args; # Put arguments in a hash. my %args_dict; #------------------------------------------------------- =head3 handled_commands Return list of commands handled by this plugin =cut #------------------------------------------------------- sub handled_commands { return { addhost_hostfile => 'pcmnodes', addhost_discover => 'pcmnodes', removehost => 'pcmnodes', updatehost => 'pcmnodes', }; } #------------------------------------------------------- =head3 process_request Process the command. This is the main call. =cut #------------------------------------------------------- sub process_request { my $lock = xCAT::PCMNodeMgmtUtils->acquire_lock(); unless ($lock){ setrsp_errormsg("Can not acquire lock, some process is operating node related actions."); return; } $request = shift; $callback = shift; $request_command = shift; $command = $request->{command}->[0]; $args = $request->{arg}; if ($command eq "addhost_hostfile"){ addhost_hostfile() } elsif ($command eq "removehost"){ removehost(); } elsif ($command eq "updatehost"){ updatehost(); } xCAT::PCMNodeMgmtUtils->release_lock($lock); } #------------------------------------------------------- =head3 parse_args Description : Parse arguments. We placed arguments into a directory %args_dict Arguments : args - args of xCAT requests. Returns : undef - parse succeed. A string - parse arguments failed, the return value is error message. =cut #----------------------------------------------------- sub parse_args{ foreach my $arg (@$args){ my @argarray = split(/=/,$arg); my $arglen = @argarray; if ($arglen > 2){ return "Illegal arg $arg specified."; } # translate the profile names into real group names in db. if($argarray[0] eq "networkprofile"){ $args_dict{$argarray[0]} = "__NetworkProfile_".$argarray[1]; } elsif ($argarray[0] eq "imageprofile"){ $args_dict{$argarray[0]} = "__ImageProfile_".$argarray[1]; } elsif ($argarray[0] eq "hardwareprofile"){ $args_dict{$argarray[0]} = "__HardwareProfile_".$argarray[1]; } else{ $args_dict{$argarray[0]} = $argarray[1]; } } return undef; } #------------------------------------------------------- =head3 addhost_hostfile Description : Create PCM nodes by importing hostinfo file. This sub maps to request "addhost_hostfile", we need to call this command from CLI like following steps: # ln -s /opt/xcat/bin/xcatclientnnr /opt/xcat/bin/addhost_hostfile # addhost_hostfile file=/root/hostinfo.file networkprofile=network_cn imageprofile=rhel63_cn hardwareprofile=ipmi groups=group1,group2 The hostinfo file should be written like: (MAC address is mandatory attribute) # Auto generate hostname for this node entry. __hostname__: mac=11:11:11:11:11:11 # Specified hostname node. node01: mac=22:22:22:22:22:22 After this call finished, the compute node's info will be updated automatically in /etc/hosts, dns config, dhcp config, TFTP config... =cut #------------------------------------------------------- sub addhost_hostfile { # Parse arges. xCAT::MsgUtils->message('S', "[PCM nodes mgmt]Import PCM nodes through hostinfo file.\n"); my $retstr = parse_args(); if ($retstr){ setrsp_errormsg($retstr); return; } # Make sure the specified parameters are valid ones. # TODO: support privisioning template. my @enabledparams = ('file', 'groups', 'networkprofile', 'hardwareprofile', 'imageprofile'); foreach my $argname (keys %args_dict){ if (! grep{ $_ eq $argname} @enabledparams){ setrsp_errormsg("Illegal attribute $argname specified."); return; } } # validate hostinfo file. if (! exists $args_dict{'file'}){ setrsp_errormsg("No hostinfo file specified."); return; } elsif(! (-e $args_dict{'file'})){ setrsp_errormsg("The hostinfo file not exists."); return; } # Get database records: all hostnames, all ips, all racks... xCAT::MsgUtils->message('S', "[PCM nodes mgmt]Getting database records.\n"); my $recordsref = xCAT::PCMNodeMgmtUtils->get_allnode_singleattrib_hash('nodelist', 'node'); %allhostnames = %$recordsref; $recordsref = xCAT::PCMNodeMgmtUtils->get_allnode_singleattrib_hash('ipmi', 'bmc'); %allbmcips = %$recordsref; $recordsref = xCAT::PCMNodeMgmtUtils->get_allnode_singleattrib_hash('mac', 'mac'); %allmacs = %$recordsref; $recordsref = xCAT::PCMNodeMgmtUtils->get_allnode_singleattrib_hash('hosts', 'ip'); %allinstallips = %$recordsref; $recordsref = xCAT::NetworkUtils->get_all_nicips(1); %allips = %$recordsref; # Merge all BMC IPs and install IPs into allips. %allips = (%allips, %allbmcips, %allinstallips); #my $recordsref = xCAT::PCMNodeMgmtUtils->get_allnode_singleattrib_hash('rack', 'rackname'); #%allracks = %$recordsref; #my $recordsref = xCAT::PCMNodeMgmtUtils->get_allchassis(1); #%allchassis = %$recordsref; # Generate temporary hostnames for hosts entries in hostfile. xCAT::MsgUtils->message('S', "[PCM nodes mgmt]Generate temporary hostnames.\n"); my ($retcode_read, $retstr_read) = read_and_generate_hostnames($args_dict{'file'}); if ($retcode_read != 0){ setrsp_errormsg($retstr_read); return; } # Parse and validate the hostinfo string. The real hostnames will be generated here. xCAT::MsgUtils->message('S', "[PCM nodes mgmt]Parsing hostinfo string and validate it.\n"); my ($hostinfo_dict_ref, $invalid_records_ref) = parse_hosts_string($retstr_read); my %hostinfo_dict = %$hostinfo_dict_ref; my @invalid_records = @$invalid_records_ref; if (@invalid_records){ setrsp_invalidrecords(\@invalid_records); return; } unless (%hostinfo_dict){ setrsp_errormsg("No valid host records found in hostinfo file."); return; } # Create the real hostinfo string in stanza file format. xCAT::MsgUtils->message('S', "[PCM nodes mgmt]Generating new hostinfo string.\n"); my ($retcode_gen, $retstr_gen) = gen_new_hostinfo_string(\%hostinfo_dict); unless ($retcode_gen){ setrsp_errormsg($retstr_gen); return; } # call mkdef to create hosts and then call nodemgmt for node management plugins. xCAT::MsgUtils->message('S', "[PCM nodes mgmt]call mkdef to create pcm nodes.\n"); my $retref = xCAT::Utils->runxcmd({command=>["mkdef"], stdin=>[$retstr_gen], arg=>['-z']}, $request_command, 0, 1); my @nodelist = keys %hostinfo_dict; xCAT::MsgUtils->message('S', "[PCM nodes mgmt]call nodemgmt plugins.\n"); $retref = xCAT::Utils->runxcmd({command=>["kitcmd_nodemgmt_add"], node=>\@nodelist}, $request_command, 0, 1); $retref = xCAT::Utils->runxcmd({command=>["kitcmd_nodemgmt_finished"], node=>\@nodelist}, $request_command, 0, 1); setrsp_success(\@nodelist); } #------------------------------------------------------- =head3 removehost Description : Remove PCM nodes. After nodes removed, their info in /etc/hosts, dhcp, dns... will be removed automatically. Arguments : N/A =cut #------------------------------------------------------- sub removehost{ my $nodes = $request->{node}; xCAT::MsgUtils->message('S', "[PCM nodes mgmt]Remove PCM nodes.\n"); # For remove nodes, we should call 'nodemgmt' in front of 'noderm' xCAT::MsgUtils->message('S', "[PCM nodes mgmt]call nodemgmt plugins.\n"); my $retref = xCAT::Utils->runxcmd({command=>["kitcmd_nodemgmt_remove"], node=>$nodes}, $request_command, 0, 1); $retref = xCAT::Utils->runxcmd({command=>["kitcmd_nodemgmt_finished"], node=>$nodes}, $request_command, 0, 1); xCAT::MsgUtils->message('S', "[PCM nodes mgmt]call noderm to remove nodes.\n"); $retref = xCAT::Utils->runxcmd({command=>["noderm"], node=>$nodes}, $request_command, 0, 1); setrsp_success($nodes); } #------------------------------------------------------- =head3 updatehost Description : Update PCM node profiles: imageprofile, networkprofile and hardwareprofile. Arguments : N/A =cut #------------------------------------------------------- sub updatehost{ my $nodes = $request->{node}; my %updated_groups; xCAT::MsgUtils->message('S', "[PCM nodes mgmt]Update PCM nodes settings.\n"); # Parse arges. my $retstr = parse_args(); if ($retstr){ setrsp_errormsg($retstr); return; } # Make sure the specified parameters are valid ones. # TODO: support privisioning template. my @enabledparams = ('networkprofile', 'hardwareprofile', 'imageprofile'); foreach my $argname (keys %args_dict){ if (! grep{ $_ eq $argname} @enabledparams){ setrsp_errormsg("Illegal attribute $argname specified."); return; } } # Get current templates for all nodes. xCAT::MsgUtils->message('S', "[PCM nodes mgmt]Read database to get groups for all nodes.\n"); my %groupdict; my $nodelstab = xCAT::Table->new('nodelist'); my $nodeshashref = $nodelstab->getNodesAttribs($nodes, ['groups']); my %nodeshash = %$nodeshashref; my %updatenodeshash; foreach (keys %nodeshash){ my @groups; my $attrshashref = $nodeshash{$_}[0]; my %attrshash = %$attrshashref; if ($attrshash{'groups'}){ @groups = split(/,/, $attrshash{'groups'}); my $groupsref; # Replace the old template name with new specified ones in args_dict if(exists $args_dict{'networkprofile'}){ $groupsref = replace_item_in_array(\@groups, "NetworkProfile", $args_dict{'networkprofile'}); } if(exists $args_dict{'hardwareprofile'}){ $groupsref = replace_item_in_array(\@groups, "HardwareProfile", $args_dict{'hardwareprofile'}); } if(exists $args_dict{'imageprofile'}){ $groupsref = replace_item_in_array(\@groups, "ImageProfile", $args_dict{'imageprofile'}); } $updatenodeshash{$_}{'groups'} = join (',', @$groupsref); } } #update DataBase. xCAT::MsgUtils->message('S', "[PCM nodes mgmt]Update database records.\n"); my $nodetab = xCAT::Table->new('nodelist',-create=>1); $nodetab->setNodesAttribs(\%updatenodeshash); $nodetab->close(); # call plugins xCAT::MsgUtils->message('S', "[PCM nodes mgmt]call nodemgmt plugins.\n"); my $retref = xCAT::Utils->runxcmd({command=>["kitcmd_nodemgmt_update"], node=>$nodes}, $request_command, 0, 1); $retref = xCAT::Utils->runxcmd({command=>["kitcmd_nodemgmt_finished"], node=>$nodes}, $request_command, 0, 1); setrsp_success($nodes); } #------------------------------------------------------- =head3 replace_item_in_array Description : Replace an item in a list with new value. This item should match specified pattern. Arguments : arrayref - the list. pattern - the pattern which the old item must match. newitem - the updated value. =cut #------------------------------------------------------- sub replace_item_in_array{ my $arrayref = shift; my $pattern = shift; my $newitem = shift; my @newarray; foreach (@$arrayref){ if ($_ =~ /__$pattern/){ next; } push (@newarray, $_); } push(@newarray, $newitem); return \@newarray; } #------------------------------------------------------- =head3 gen_new_hostinfo_string Description : Generate a stanza file format string used for 'mkdef' to create nodes. Arguments : hostinfo_dict_ref - The reference of hostinfo dict. Returns : (returnvalue, returnmsg) returnvalue - 0, stands for generate new hostinfo string failed. 1, stands for generate new hostinfo string OK. returnnmsg - error messages if generate failed. - the new hostinfo string if generate OK. =cut #------------------------------------------------------- sub gen_new_hostinfo_string{ my $hostinfo_dict_ref = shift; my %hostinfo_dict = %$hostinfo_dict_ref; # Get free ips list for all networks in network profile. my @allknownips = keys %allips; my $netprofileattrsref = xCAT::PCMNodeMgmtUtils->get_netprofile_nic_attrs($args_dict{'networkprofile'}); my %netprofileattr = %$netprofileattrsref; my %freeipshash; foreach (keys %netprofileattr){ my $netname = $netprofileattr{$_}{'network'}; if($netname and (! exists $freeipshash{$netname})) { $freeipshash{$netname} = xCAT::PCMNodeMgmtUtils->get_allocable_staticips_innet($netname, \@allknownips); } } # Get networkprofile's installip my $noderestab = xCAT::Table->new('noderes'); my $networkprofile = $args_dict{'networkprofile'}; my $nodereshashref = $noderestab->getNodeAttribs($networkprofile, ['installnic']); my %nodereshash = %$nodereshashref; my $installnic = $nodereshash{'installnic'}; # Get node's provisioning method my $provmethod = xCAT::PCMNodeMgmtUtils->get_imageprofile_prov_method($args_dict{'imageprofile'}); # compose the stanza string for hostinfo file. my $hostsinfostr = ""; foreach my $item (keys %hostinfo_dict){ # Generate IPs for all interfaces. my %ipshash; foreach (keys %netprofileattr){ my $netname = $netprofileattr{$_}{'network'}; my $freeipsref; if ($netname){ $freeipsref = $freeipshash{$netname}; } my $nextip = shift @$freeipsref; if (!$nextip){ return 0, "No sufficient IP address in network $netname for interface $_"; }else{ $ipshash{$_} = $nextip; $allips{$nextip} = 0; } } my $nicips = ""; foreach(keys %ipshash){ $nicips = "$_:$ipshash{$_},$nicips"; } $hostinfo_dict{$item}{"nicips"} = $nicips; # Generate IP address if no IP specified. if (! exists $hostinfo_dict{$item}{"ip"}) { if (exists $ipshash{$installnic}){ $hostinfo_dict{$item}{"ip"} = $ipshash{$installnic}; }else{ return 0, "No sufficient IP address for interface $installnic"; } } $hostinfo_dict{$item}{"objtype"} = "node"; $hostinfo_dict{$item}{"groups"} = "__Managed"; if (exists $args_dict{'networkprofile'}){$hostinfo_dict{$item}{"groups"} .= ",".$args_dict{'networkprofile'}} if (exists $args_dict{'imageprofile'}){$hostinfo_dict{$item}{"groups"} .= ",".$args_dict{'imageprofile'}} if (exists $args_dict{'hardwareprofile'}){$hostinfo_dict{$item}{"groups"} .= ",".$args_dict{'hardwareprofile'}} # Update BMC records. if (exists $netprofileattr{"bmc"}){ $hostinfo_dict{$item}{"mgt"} = "ipmi"; $hostinfo_dict{$item}{"chain"} = 'runcmd=bmcsetup,'.$provmethod; if (exists $ipshash{"bmc"}){ $hostinfo_dict{$item}{"bmc"} = $ipshash{"bmc"}; } else{ return 0, "No sufficient IP addresses for BMC"; } } else{ $hostinfo_dict{$item}{"chain"} = $provmethod; } # Generate the hostinfo string. $hostsinfostr = "$hostsinfostr$item:\n"; my $itemdictref = $hostinfo_dict{$item}; my %itemdict = %$itemdictref; foreach (keys %itemdict){ $hostsinfostr = "$hostsinfostr $_=\"$itemdict{$_}\"\n"; } } return 1, $hostsinfostr; } #------------------------------------------------------- =head3 read_and_generate_hostnames Description : Read hostinfo file and generate temporary hostnames for no-hostname specified ones. Arguments : hostfile - the location of hostinfo file. Returns : (returnvalue, returnmsg) returnvalue - 0, stands for a failed return 1, stands for a success return returnnmsg - error messages for failed return. - the contents of the hostinfo string. =cut #------------------------------------------------------- sub read_and_generate_hostnames{ my $hostfile = shift; # Get 10000 temprary hostnames. my $freehostnamesref = xCAT::PCMNodeMgmtUtils->gen_numric_hostnames("TMPHOSTS","", 4); # Auto generate hostnames for "__hostname__" entries. open(HOSTFILE, $hostfile); my $filecontent = join("", ); while ((index $filecontent, "__hostname__:") >= 0){ my $nexthost = shift @$freehostnamesref; # no more valid hostnames to assign. if (! $nexthost){ return 1, "Can not generate hostname automatically: No more valid hostnames available ."; } # This hostname already specified in hostinfo file. if ((index $filecontent, "$nexthost:") >= 0){ next; } # This hostname should not in database. if (exists $allhostnames{$nexthost}){ next; } $filecontent =~ s/__hostname__/$nexthost/; } close(HOSTFILE); return 0, $filecontent; } #------------------------------------------------------- =head3 parse_hosts_string Description : Parse the hostinfo string and validate it. Arguments : filecontent - The content of hostinfo file. Returns : (hostinfo_dict, invalid_records) hostinfo_dict - Reference of hostinfo dict. Key are hostnames and values is an attributes dict. invalid_records - Reference of invalid records list. =cut #------------------------------------------------------- sub parse_hosts_string{ my $filecontent = shift; my %hostinfo_dict; my @invalid_records; my $nicstab = xCAT::Table->new('nics'); my $nodehashref = $nicstab->getNodeAttribs($args_dict{'networkprofile'}, ['hostnameformat']); my $nameformat = $nodehashref->{'hostnameformat'}; my $nameformattype = xCAT::PCMNodeMgmtUtils->get_hostname_format_type($nameformat); my %freehostnames; # Parse hostinfo file string. xCAT::DBobjUtils->readFileInput($filecontent); # Record duplicated items. # We should go through list @::fileobjnames first as %::FILEATTRS is just a hash, # it not tells whether there are some duplicated hostnames in the hostinfo string. my %hostnamedict; foreach my $hostname (@::fileobjnames){ if (exists $hostnamedict{$hostname}){ push @invalid_records, [$hostname, "Duplicated hostname defined"]; } else{ $hostnamedict{$hostname} = 0; } } # Verify each node entry. foreach (keys %::FILEATTRS){ my $errmsg = validate_node_entry($_, $::FILEATTRS{$_}); if ($errmsg) { if ($_=~ /^TMPHOSTS/){ push @invalid_records, ["__hostname__", $errmsg]; } else{ push @invalid_records, [$_, $errmsg]; } next; } # We need generate hostnames for this entry. if ($_=~ /^TMPHOSTS/) { # rack + numric hostname format, we must specify rack in node's definition. my $numricformat; # Need convert hostname format into numric format first. if ($nameformattype eq "rack"){ if (! exists $::FILEATTRS{$_}{"rack"}){ push @invalid_records, ["__hostname__", "No rack info specified. Do specify it because the nameformat contains rack info."]; next; } $numricformat = xCAT::PCMNodeMgmtUtils->rackformat_to_numricformat($nameformat, $::FILEATTRS{$_}{"rack"}); } else{ # pure numric hostname format $numricformat = $nameformat; } # Generate hostnames based on numric hostname format. if (! exists $freehostnames{$numricformat}){ $freehostnames{$numricformat} = xCAT::PCMNodeMgmtUtils->genhosts_with_numric_tmpl($numricformat); } my $hostnamelistref = $freehostnames{$numricformat}; my $nexthostname = shift @$hostnamelistref; while (exists $allhostnames{$nexthostname}){ $nexthostname = shift @$hostnamelistref; } $hostinfo_dict{$nexthostname} = $::FILEATTRS{$_}; } else{ $hostinfo_dict{$_} = $::FILEATTRS{$_}; } } return (\%hostinfo_dict, \@invalid_records); } #------------------------------------------------------- =head3 validate_node_entry Description : Validate a node info hash. Arguments : node_name - node hostname. node_entry_ref - Reference of the node info hash. Returns : errormsg - undef: stands for no errror. - valid string: stands for the error message of validation. =cut #------------------------------------------------------- sub validate_node_entry{ my $node_name = shift; my $node_entry_ref = shift; my %node_entry = %$node_entry_ref; # duplicate hostname found in hostinfo file. if (exists $allhostnames{$node_name}) { return "Specified hostname $node_name conflicts with database records."; } # Must specify either MAC or switch + port. if (exists $node_entry{"mac"} || exists $node_entry{"switch"} && exists $node_entry{"port"}){ } else{ return "Neither MAC nor switch + port specified"; } if (! xCAT::NetworkUtils->isValidHostname($node_name)){ return "Specified hostname: $node_name is invalid"; } # validate each single value. foreach (keys %node_entry){ if ($_ eq "mac"){ if (exists $allmacs{$node_entry{$_}}){ return "Specified MAC address $node_entry{$_} conflicts with MACs in database or hostinfo file"; }elsif(! xCAT::NetworkUtils->isValidMAC($node_entry{$_})){ return "Specified MAC address $node_entry{$_} is invalid"; }else{ $allmacs{$node_entry{$_}} = 0; } }elsif ($_ eq "ip"){ if (exists $allips{$node_entry{$_}}){ return "Specified IP address $node_entry{$_} conflicts with IPs in database or hostinfo file"; }elsif((xCAT::NetworkUtils->validate_ip($node_entry{$_}))[0][0] ){ return "Specified IP address $node_entry{$_} is invalid"; }elsif(xCAT::NetworkUtils->isReservedIP($node_entry{$_})){ return "Specified IP address $node_entry{$_} is invalid"; }else { #push the IP into allips list. $allips{$node_entry{$_}} = 0; } }elsif ($_ eq "switch"){ #TODO: xCAT switch discovery enhance: verify whether switch exists. }elsif ($_ eq "port"){ #TODO: xCAT switch discovery enhance: verify whether port exists. }elsif ($_ eq "rack"){ if (not exists $allracks{$node_entry{$_}}){ return "Specified rack $node_entry{$_} not defined"; } }elsif ($_ eq "chassis"){ if (not exists $allchassis{$node_entry{$_}}){ return "Specified chassis $node_entry{$_} not defined"; } }elsif ($_ eq "unit"){ # Not a valid number. if (!($node_entry{$_} =~ /^\d+$/)){ return "Specified unit $node_entry{$_} is a invalid number"; } }elsif ($_ eq "height"){ # Not a valid number. if (!($node_entry{$_} =~ /^\d+$/)){ return "Specified height $node_entry{$_} is a invalid number"; } }else{ return "Invalid attribute $_ specified"; } } # For blades, don't support specify unit and height. if(exists $node_entry{"chassis"} ){ if(exists $node_entry{"unit"}){ return "Can not specify 'unit' together with 'chassis'"; } if(exists $node_entry{"height"}){ return "can not specify 'height' together with 'chassis'"; } } # push hostinfo into global dicts. $allhostnames{$node_name} = 0; return undef; } #------------------------------------------------------- =head3 setrsp_invalidrecords Description : Set response for processing invalid host records. Arguments : recordsref - Refrence of invalid nodes list. =cut #------------------------------------------------------- sub setrsp_invalidrecords { my $recordsref = shift; my $rsp; my $master=xCAT::TableUtils->get_site_Master(); # The total number of invalid records. $rsp->{invalid_records_num} = scalar @$recordsref; # We write details of invalid records into a file. my ($fh, $filename) = xCAT::PCMNodeMgmtUtils->get_output_filename(); foreach (@$recordsref){ my @erroritem = @$_; print $fh "nodename $erroritem[0], error: $erroritem[1]\n"; } close $fh; # Tells the URL of the details file. xCAT::MsgUtils->message('S', "Detailed response info placed in file: http://$master/$filename\n"); $rsp->{details} = "http://$master/$filename"; $callback->($rsp); } #------------------------------------------------------- =head3 setrsp_errormsg Description : Set response for error messages. Arguments : errormsg - Error messages. =cut #------------------------------------------------------- sub setrsp_errormsg { my $errormsg = shift; my $rsp; xCAT::MsgUtils->message('S', "$errormsg\n"); $rsp->{error}->[0] = $errormsg; $callback->($rsp); } #------------------------------------------------------- =head3 setrsp_success Description : Set response for successfully processed nodes. Arguments : recordsref - Refrence of nodes list. =cut #------------------------------------------------------- sub setrsp_success { my $recordsref = shift; my $rsp; my $master=xCAT::TableUtils->get_site_Master(); # The total number of success nodes. $rsp->{success_nodes_num} = scalar @$recordsref; my ($fh, $filename) = xCAT::PCMNodeMgmtUtils->get_output_filename(); foreach (@$recordsref){ print $fh "success: $_\n"; } close $fh; # Tells the URL of the details file. xCAT::MsgUtils->message('S', "Detailed response info placed in file: http://$master/$filename\n"); $rsp->{details} = "http://$master/$filename"; $callback->($rsp); } 1;