package xCAT_plugin::esx; use strict; use warnings; use xCAT::Table; use xCAT::Utils; use Time::HiRes qw (sleep); use xCAT::MsgUtils; use xCAT::SvrUtils; use xCAT::NodeRange; use xCAT::Common; use xCAT::VMCommon; use POSIX "WNOHANG"; use Getopt::Long; use Thread qw(yield); use POSIX qw(WNOHANG nice); use File::Path qw/mkpath rmtree/; use File::Temp qw/tempdir/; use File::Copy; use IO::Socket; #Need name resolution #use Data::Dumper; Getopt::Long::Configure("bundling"); Getopt::Long::Configure("pass_through"); my @cpiopid; our @ISA = 'xCAT::Common'; #in xCAT, the lifetime of a process ends on every request #therefore, the lifetime of assignments to these glabals as architected #is to be cleared on every request #my %esx_comm_pids; my %hyphash; #A data structure to hold hypervisor-wide variables (i.e. the current resource pool, virtual machine folder, connection object my %vcenterhash; #A data structure to reflect the state of vcenter connectivity to hypervisors my %hypready; #A structure for hypervisor readiness to be tracked before proceeding to normal operations my %running_tasks; #A struct to track this processes my $output_handler; #Pointer to the function to drive results to client my $executerequest; my $usehostnamesforvcenter; my %tablecfg; #to hold the tables my $currkey; my $viavcenter; my $viavcenterbyhyp; my $vmwaresdkdetect = eval { require VMware::VIRuntime; VMware::VIRuntime->import(); 1; }; my %guestidmap = ( "rhel.5.*" => "rhel5_", "rhel4.*" => "rhel4_", "centos5.*" => "rhel5_", "centos4.*" => "rhel4_", "sles11.*" => "sles11_", "sles10.*" => "sles10_", "win2k8" => "winLonghorn", "win2k8r2" => "windows7Server", "win2k3" => "winNetStandard", "imagex" => "winNetStandard", "boottarget" => "otherLinux" #otherGuest, otherGuest64, otherLinuxGuest, otherLinux64Guest ); sub handled_commands{ return { copycd => 'esx', mknetboot => "nodetype:os=(esxi.*)", rpower => 'nodehm:power,mgt', rsetboot => 'nodehm:power,mgt', rmigrate => 'nodehm:power,mgt', mkvm => 'nodehm:mgt', rmvm => 'nodehm:mgt', rmhypervisor => 'hypervisor:type', #lsvm => 'nodehm:mgt', not really supported yet }; } sub preprocess_request { my $request = shift; my $callback = shift; my $username = 'root'; my $password = ''; my $vusername = "Administrator"; my $vpassword = ""; unless ($request and $request->{command} and $request->{command}->[0]) { return; } if ($request->{command}->[0] eq 'copycd') { #don't farm out copycd return [$request]; }elsif($request->{command}->[0] eq 'mknetboot'){ return [$request]; } xCAT::Common::usage_noderange($request,$callback); if ($request->{_xcatpreprocessed} and $request->{_xcatpreprocessed}->[0] == 1) { return [$request]; } # exit if preprocesses my @requests; my $noderange = $request->{node}; # array ref my $command = $request->{command}->[0]; my $extraargs = $request->{arg}; my @exargs=($request->{arg}); my %hyp_hash = (); # Get nodes from mp table and assign nodes to mp hash. my $passtab = xCAT::Table->new('passwd'); my $tmp; if ($passtab) { ($tmp) = $passtab->getAttribs({'key'=>'vmware'},'username','password'); if (defined($tmp)) { $username = $tmp->{username}; $password = $tmp->{password}; } ($tmp) = $passtab->getAttribs({'key'=>'vcenter'},'username','password'); if (defined($tmp)) { $vusername = $tmp->{username}; $vpassword = $tmp->{password}; } } my $vmtab = xCAT::Table->new("vm"); unless($vmtab){ $callback->({data=>["Cannot open vm table"]}); $request = {}; return; } my $vmtabhash = $vmtab->getNodesAttribs($noderange,['host']); foreach my $node (@$noderange){ if ($command eq "rmhypervisor") { $hyp_hash{$node}{nodes} = [$node]; } else { my $ent = $vmtabhash->{$node}->[0]; if(defined($ent->{host})) { push @{$hyp_hash{$ent->{host}}{nodes}}, $node; } else { $callback->({data=>["no host defined for guest $node"]}); $request = {}; return; } if(defined($ent->{id})) { push @{$hyp_hash{$ent->{host}}{ids}},$ent->{id}; }else{ push @{$hyp_hash{$ent->{host}}{ids}}, ""; } } } # find service nodes for the MMs # build an individual request for each service node my $service = "xcat"; my @hyps=keys(%hyp_hash); if ($command eq 'rmigrate') { my $dsthyp = $extraargs->[0]; push @hyps,$dsthyp; } #TODO: per hypervisor table password lookup my $sn = xCAT::Utils->get_ServiceNode(\@hyps, $service, "MN"); #vmtabhash was from when we had vm.host do double duty for hypervisor data #$vmtabhash = $vmtab->getNodesAttribs(\@hyps,['host']); #We now use hypervisor fields to be unambiguous my $hyptab = xCAT::Table->new('hypervisor'); my $hyptabhash={}; if ($hyptab) { $hyptabhash = $hyptab->getNodesAttribs(\@hyps,['mgr']); } # build each request for each service node foreach my $snkey (keys %$sn){ my $reqcopy = {%$request}; $reqcopy->{'_xcatdest'} = $snkey; $reqcopy->{_xcatpreprocessed}->[0] = 1; my $hyps1=$sn->{$snkey}; my @moreinfo=(); my @nodes=(); foreach (@$hyps1) { #This preserves the constructed data to avoid redundant table lookup my $cfgdata; if ($hyp_hash{$_}{nodes}) { push @nodes, @{$hyp_hash{$_}{nodes}}; $cfgdata = "[$_][".join(',',@{$hyp_hash{$_}{nodes}})."][$username][$password][$vusername][$vpassword]"; #TODO: not use vm.host? } else { $cfgdata = "[$_][][$username][$password][$vusername][$vpassword]"; #TODO: not use vm.host? } if (defined $hyptabhash->{$_}->[0]->{mgr}) { $cfgdata .= "[". $hyptabhash->{$_}->[0]->{mgr}."]"; } else { $cfgdata .= "[]"; } push @moreinfo, $cfgdata; #"[$_][".join(',',@{$hyp_hash{$_}{nodes}})."][$username][$password]"; } $reqcopy->{node} = \@nodes; #print "nodes=@nodes\n"; $reqcopy->{moreinfo}=\@moreinfo; push @requests, $reqcopy; } return \@requests; } sub process_request { #$SIG{INT} = $SIG{TERM} = sub{ # foreach (keys %esx_comm_pids){ # kill 2,$_; # } # exit 0; #}; my $request = shift; $output_handler = shift; $executerequest = shift; my $level = shift; my $distname = undef; my $arch = undef; my $path = undef; my $command = $request->{command}->[0]; #The first segment is fulfilling the role of this plugin as #a hypervisor provisioning plugin (akin to anaconda, windows, sles plugins) if($command eq 'copycd'){ return copycd($request,$executerequest); }elsif($command eq 'mknetboot'){ return mknetboot($request,$executerequest); } #From here on out, code for managing guests under VMware #Detect whether or not the VMware SDK is available on this specific system unless ($vmwaresdkdetect) { $vmwaresdkdetect = eval { require VMware::VIRuntime; VMware::VIRuntime->import(); 1; }; } unless ($vmwaresdkdetect) { sendmsg([1,"VMWare SDK required for operation, but not installed"]); return; } my $moreinfo; my $noderange = $request->{node}; xCAT::VMCommon::grab_table_data($noderange,\%tablecfg,$output_handler); my @exargs; unless($command){ return; # Empty request } if (ref($request->{arg})) { @exargs = @{$request->{arg}}; } else { @exargs = ($request->{arg}); } my $sitetab = xCAT::Table->new('site'); if($sitetab){ (my $ref) = $sitetab->getAttribs({key => 'usehostnamesforvcenter'}, 'value'); if ($ref and $ref->{value}) { $usehostnamesforvcenter = $ref->{value}; } } if ($request->{moreinfo}) { $moreinfo=$request->{moreinfo}; } else { $moreinfo=build_more_info($noderange,$output_handler);} foreach my $info (@$moreinfo) { $info=~/^\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/; my $hyp=$1; my @nodes=split(',', $2); my $username = $3; my $password = $4; $hyphash{$hyp}->{vcenter}->{name} = $7; $hyphash{$hyp}->{vcenter}->{username} = $5; $hyphash{$hyp}->{vcenter}->{password} = $6; $hyphash{$hyp}->{username}=$username;# $nodeid; $hyphash{$hyp}->{password}=$password;# $nodeid; unless ($hyphash{$hyp}->{vcenter}->{password}) { $hyphash{$hyp}->{vcenter}->{password} = ""; } my $ent; for (my $i=0; $i<@nodes; $i++){ my $node = $nodes[$i]; #my $nodeid = $ids[$i]; $hyphash{$hyp}->{nodes}->{$node}=1;# $nodeid; } } my $hyptab = xCAT::Table->new('hypervisor',create=>0); if ($hyptab) { my @hyps = keys %hyphash; $tablecfg{hypervisor} = $hyptab->getNodesAttribs(\@hyps,['mgr','netmap','defaultnet','cluster','preferdirect']); } my $hoststab = xCAT::Table->new('hosts',create=>0); if ($hoststab) { my @hyps = keys %hyphash; $tablecfg{hosts} = $hoststab->getNodesAttribs(\@hyps,['hostnames']); } #my $children = 0; #my $vmmaxp = 84; #$SIG{CHLD} = sub { my $cpid; while ($cpid = waitpid(-1, WNOHANG) > 0) { delete $esx_comm_pids{$cpid}; $children--; } }; $viavcenter = 0; if ($command eq 'rmigrate' or $command eq 'rmhypervisor') { #Only use vcenter when required, fewer prereqs $viavcenter = 1; } my $keytab = xCAT::Table->new('prodkey'); if ($keytab) { my @hypes = keys %hyphash; $tablecfg{prodkey} = $keytab->getNodesAttribs(\@hypes,[qw/product key/]); } foreach my $hyp (sort(keys %hyphash)){ #if($pid == 0){ if ($viavcenter or (defined $tablecfg{hypervisor}->{$hyp}->[0]->{mgr} and not $tablecfg{hypervisor}->{$hyp}->[0]->{preferdirect})) { $viavcenterbyhyp->{$hyp}=1; $hypready{$hyp} = 0; #This hypervisor requires a flag be set to signify vCenter sanenes before proceeding my $vcenter = $hyphash{$hyp}->{vcenter}->{name}; unless ($vcenterhash{$vcenter}->{conn}) { eval { $vcenterhash{$vcenter}->{conn} = Vim->new(service_url=>"https://$vcenter/sdk"); $vcenterhash{$vcenter}->{conn}->login( user_name => $hyphash{$hyp}->{vcenter}->{username}, password => $hyphash{$hyp}->{vcenter}->{password} ); }; if ($@) { $vcenterhash{$vcenter}->{conn} = undef; sendmsg([1,"Unable to reach $vcenter vCenter server to manage $hyp: $@"]); next; } } $hyphash{$hyp}->{conn} = $vcenterhash{$hyphash{$hyp}->{vcenter}->{name}}->{conn}; $hyphash{$hyp}->{vcenter}->{conn} = $vcenterhash{$hyphash{$hyp}->{vcenter}->{name}}->{conn}; if (validate_vcenter_prereqs($hyp, \&declare_ready, { hyp=>$hyp, vcenter=>$vcenter }) eq "failed") { $hypready{$hyp} = -1; } } else { eval { $hyphash{$hyp}->{conn} = Vim->new(service_url=>"https://$hyp/sdk"); $hyphash{$hyp}->{conn}->login(user_name=>$hyphash{$hyp}->{username},password=>$hyphash{$hyp}->{password}); }; if ($@) { $hyphash{$hyp}->{conn} = undef; sendmsg([1,"Unable to reach $hyp to perform operation"]); $hypready{$hyp} = -1; next; } validate_licenses($hyp); } #}else{ # $esx_comm_pids{$pid} = 1; #} } while (grep { $_ == 0 } values %hypready) { wait_for_tasks(); sleep (1); #We'll check back in every second. Unfortunately, we have to poll since we are in web service land } if (grep { $_ == -1 } values %hypready) { my @badhypes; foreach (keys %hypready) { if ($hypready{$_} == -1) { push @badhypes,$_; my @relevant_nodes = sort (keys %{$hyphash{$_}->{nodes}}); foreach (@relevant_nodes) { sendmsg([1,": hypervisor unreachable"],$_); } delete $hyphash{$_}; } } sendmsg([1,": The following hypervisors failed to become ready for the operation: ".join(',',@badhypes)]); } do_cmd($command,@exargs); foreach my $hyp (sort(keys %hyphash)){ $hyphash{$hyp}->{conn}->logout(); } } sub validate_licenses { my $hyp = shift; my $conn = $hyphash{$hyp}->{conn}; unless ($tablecfg{prodkey}->{$hyp}) { #if no license specified, no-op return; } my $hv = get_hostview(hypname=>$hyp,conn=>$conn,properties=>['configManager','name']); my $lm = $conn->get_view(mo_ref=>$hv->configManager->licenseManager); my @licenses; foreach (@{$lm->licenses}) { push @licenses,uc($_->licenseKey); } my @newlicenses; foreach (@{$tablecfg{prodkey}->{$hyp}}) { if ($_->{product} eq 'esx') { my $key = uc($_->{key}); unless (grep /$key/,@licenses) { push @newlicenses,$key; } } } foreach (@newlicenses) { $lm->UpdateLicense(licenseKey=>$_); } } sub do_cmd { my $command = shift; my @exargs = @_; if ($command eq 'rpower') { generic_vm_operation(['config.name','config','runtime.powerState','runtime.host'],\&power,@exargs); } elsif ($command eq 'rmvm') { generic_vm_operation(['config.name','runtime.powerState','runtime.host'],\&rmvm,@exargs); } elsif ($command eq 'rsetboot') { generic_vm_operation(['config.name','runtime.host'],\&setboot,@exargs); } elsif ($command eq 'rmhypervisor') { generic_hyp_operation(\&rmhypervisor,@exargs); } elsif ($command eq 'mkvm') { generic_hyp_operation(\&mkvms,@exargs); } elsif ($command eq 'rmigrate') { #Technically, on a host view, but vcenter path is 'weirder' generic_hyp_operation(\&migrate,@exargs); } wait_for_tasks(); } #this function will check pending task status sub process_tasks { foreach (keys %running_tasks) { my $curcon; if (defined $running_tasks{$_}->{conn}) { $curcon = $running_tasks{$_}->{conn}; } else { $curcon = $hyphash{$running_tasks{$_}->{hyp}}->{conn}; } my $curt = $curcon->get_view(mo_ref=>$running_tasks{$_}->{task}); my $state = $curt->info->state->val; unless ($state eq 'running' or $state eq 'queued') { $running_tasks{$_}->{callback}->($curt,$running_tasks{$_}->{data}); delete $running_tasks{$_}; } if ($state eq 'running' and not $running_tasks{$_}->{questionasked}) { # and $curt->info->progress == 95) { #This is unfortunate, there should be a 'state' to indicate a question is blocking #however there isn't, so if we see something running at 95%, we just manually see if a question blocked the rest my $vm = $curcon->get_view(mo_ref=>$curt->info->entity); if ($vm->{summary} and $vm->summary->{runtime} and $vm->summary->runtime->{question} and $vm->summary->runtime->question) { $running_tasks{$_}->{questionasked}=1; $running_tasks{$_}->{callback}->($curt,$running_tasks{$_}->{data},$vm->summary->runtime->question,$vm); } } } } #this function is a barrier to ensure prerequisites are met sub wait_for_tasks { while (scalar keys %running_tasks) { process_tasks; sleep (1); #We'll check back in every second. Unfortunately, we have to poll since we are in web service land } } sub connecthost_callback { my $task = shift; my $args = shift; my $hv = $args->{hostview}; my $state = $task->info->state->val; if ($state eq "success") { $hypready{$args->{hypname}}=1; #declare readiness enable_vmotion(hypname=>$args->{hypname},hostview=>$args->{hostview},conn=>$args->{conn}); $vcenterhash{$args->{vcenter}}->{$args->{hypname}} = 'good'; if (defined $args->{depfun}) { #If a function is waiting for the host connect to go valid, call it $args->{depfun}->($args->{depargs}); } return; } my $thumbprint; eval { $thumbprint = $task->{info}->error->fault->thumbprint; }; if ($thumbprint) { #was an unknown certificate error, retry and accept the unknown certificate $args->{connspec}->{sslThumbprint}=$task->info->error->fault->thumbprint; my $task; if (defined $args->{hostview}) {#It was a reconnect request $task = $hv->ReconnectHost_Task(cnxSpec=>$args->{connspec}); } elsif (defined $args->{foldview}) {#was an add host request $task = $args->{foldview}->AddStandaloneHost_Task(spec=>$args->{connspec},addConnected=>1); } elsif (defined $args->{cluster}) {#was an add host to cluster request $task = $args->{cluster}->AddHost_Task(spec=>$args->{connspec},asConnected=>1); } $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&connecthost_callback; $running_tasks{$task}->{conn} = $args->{conn}; $running_tasks{$task}->{data} = $args; #{ conn_spec=>$connspec,hostview=>$hv,hypname=>$args->{hypname},vcenter=>$args->{vcenter} }; } elsif ($state eq 'error') { my $error = $task->info->error->localizedMessage; if (defined ($task->info->error->fault->faultMessage)) { #Only in 4.0, support of 3.5 must be careful? foreach(@{$task->info->error->fault->faultMessage}) { $error.=$_->message; } } sendmsg([1,$error]); #,$node); $hypready{$args->{hypname}} = -1; #Impossible for this hypervisor to ever be ready $vcenterhash{$args->{vcenter}}->{$args->{hypname}} = 'bad'; } } sub get_clusterview { my %args = @_; my $clustname = $args{clustname}; my %subargs = ( view_type=>'ClusterComputeResource', ); if ($args{properties}) { $subargs{properties}=$args{properties}; } foreach (@{$args{conn}->find_entity_views(%subargs)}) { if ($_->name eq "$clustname") { return $_; last; } } } sub get_hostview { my %args = @_; my $host = $args{hypname}; my %subargs = ( view_type=>'HostSystem', ); if ($args{properties}) { $subargs{properties}=$args{properties}; } my @addrs = gethostbyname($host); my $ip; my $name; my $aliases; if ($addrs[4]) { $ip=inet_ntoa($addrs[4]); ($name, $aliases) = gethostbyaddr($addrs[4],AF_INET); #TODO: IPv6 } else { ($ip,$name,$aliases) = ($host,$host,""); } my @matchvalues = ($host,$ip,$name); foreach (split /\s+/,$aliases) { push @matchvalues,$_; } foreach (@{$args{conn}->find_entity_views(%subargs)}) { my $view = $_; if ($_->name =~ /$host(?:\.|\z)/ or $_->name =~ /localhost(?:\.|\z)/ or grep { $view->name =~ /$_(?:\.|\z)/ } @matchvalues) { return $view; last; } } } sub enable_vmotion { #TODO: vmware 3.x semantics too? this is 4.0... my %args = @_; unless ($args{hostview}) { $args{hostview} = get_hostview(conn=>$args{conn},hypname=>$args{hypname},properties=>['configManager','name']); } my $nicmgr=$args{conn}->get_view(mo_ref=>$args{hostview}->configManager->virtualNicManager); my $qnc = $nicmgr->QueryNetConfig(nicType=>"vmotion"); if ($qnc->{selectedVnic}) { return 1; } else { if (scalar @{$qnc->candidateVnic} eq 1) { #There is only one possible path, use it $nicmgr->SelectVnicForNicType(nicType=>"vmotion",device=>$qnc->candidateVnic->[0]->device); return 1; } else { sendmsg([1,"TODO: use configuration to pick the nic ".$args{hypname}]); } return 0; } } sub mkvm_callback { my $task = shift; my $args = shift; my $node = $args->{node}; if ($task->info->state->val eq 'error') { my $error = $task->info->error->localizedMessage; sendmsg([1,$error],$node); } } sub relay_vmware_err { my $task = shift; my $extratext = shift; my @nodes = @_; my $error = $task->info->error->localizedMessage; if (defined ($task->info->error->fault->faultMessage)) { #Only in 4.0, support of 3.5 must be careful? foreach(@{$task->info->error->fault->faultMessage}) { $error.=$_->message; } } if (@nodes) { foreach (@nodes) { sendmsg([1,$extratext.$error],$_); } }else { sendmsg([1,$extratext.$error]); } } sub migrate_callback { my $task = shift; my $parms = shift; my $state = $task->info->state->val; if ($state eq 'success') { my $vmtab = xCAT::Table->new('vm'); $vmtab->setNodeAttribs($parms->{node},{host=>$parms->{target}}); sendmsg("migrated to ".$parms->{target},$parms->{node}); } else { relay_vmware_err($task,"Migrating to ".$parms->{target}." ",$parms->{node}); } } sub poweron_task_callback { my $task = shift; my $parms = shift; my $q = shift; #question if blocked my $vm = shift; #path to answer questions if asked my $state = $task->info->state->val; my $node = $parms->{node}; my $intent = $parms->{successtext}; if ($state eq 'success') { sendmsg($intent,$node); } elsif ($state eq 'error') { relay_vmware_err($task,"",$node); } elsif ($q and $q->text =~ /^msg.uuid.altered:/ and ($q->choice->choiceInfo->[0]->summary eq 'Cancel' and ($q->choice->choiceInfo->[0]->key eq '0'))) { #make sure it is what is what we have seen it to be if ($parms->{forceon} and $q->choice->choiceInfo->[1]->summary eq 'I _moved it' and $q->choice->choiceInfo->[1]->key eq '1') { #answer the question as 'moved' $vm->AnswerVM(questionId=>$q->id,answerChoice=>'1'); } else { $vm->AnswerVM(questionId=>$q->id,answerChoice=>'0'); sendmsg([1,"Failure powering on VM, it mismatched against the hypervisor. If positive VM is not running on another hypervisor, use -f to force VM on"],$node); } } elsif ($q) { if ($q->choice->choiceInfo->[0]->summary eq 'Cancel') { sendmsg([1,":Cancelling due to unexpected question executing task: ".$q->text],$node); } else { sendmsg([1,":Task hang due to unexpected question executing task, need to use VMware tools to clean up the mess for now: ".$q->text],$node); } } } sub generic_task_callback { my $task = shift; my $parms = shift; my $state = $task->info->state->val; my $node = $parms->{node}; my $intent = $parms->{successtext}; if ($state eq 'success') { sendmsg($intent,$node); } elsif ($state eq 'error') { relay_vmware_err($task,"",$node); } } sub sendmsg { my $callback = $output_handler; my $text = shift; my $node = shift; my $descr; my $rc; if (ref $text eq 'HASH') { return $callback->($text); } elsif (ref $text eq 'ARRAY') { $rc = $text->[0]; $text = $text->[1]; } if ($text =~ /:/) { ($descr,$text) = split /:/,$text,2; } $text =~ s/^ *//; $text =~ s/ *$//; my $msg; my $curptr; if ($node) { $msg->{node}=[{name => [$node]}]; $curptr=$msg->{node}->[0]; } else { $msg = {}; $curptr = $msg; } if ($rc) { $curptr->{errorcode}=[$rc]; $curptr->{error}=[$text]; $curptr=$curptr->{error}->[0]; } else { $curptr->{data}=[{contents=>[$text]}]; $curptr=$curptr->{data}->[0]; if ($descr) { $curptr->{desc}=[$descr]; } } $callback->($msg); } sub actually_migrate { my %args = %{shift()}; my @nodes = @{$args{nodes}}; my $target = $args{target}; my $hyp = $args{hyp}; my $vcenter = $args{vcenter}; if ($vcenterhash{$vcenter}->{$hyp} eq 'bad' or $vcenterhash{$vcenter}->{$target} eq 'bad') { sendmsg([1,"Unable to migrate ".join(',',@nodes)." to $target due to inability to validate vCenter connectivity"]); return; } if ($vcenterhash{$vcenter}->{$hyp} eq 'good' and $vcenterhash{$vcenter}->{$target} eq 'good') { unless (validate_datastore_prereqs(\@nodes,$target)) { sendmsg([1,"Unable to verify storage state on target system"]); return; } unless (validate_network_prereqs(\@nodes,$target)) { sendmsg([1,"Unable to verify target network state"]); return; } my $dstview = get_hostview(conn=>$hyphash{$target}->{conn},hypname=>$target,properties=>['name','parent']); unless ($hyphash{$target}->{pool}) { $hyphash{$target}->{pool} = $hyphash{$target}->{conn}->get_view(mo_ref=>$dstview->parent,properties=>['resourcePool'])->resourcePool; } foreach (@nodes) { my $srcview = $hyphash{$hyp}->{conn}->find_entity_view(view_type=>'VirtualMachine',properties=>['config.name'],filter=>{name=>$_}); my $task = $srcview->MigrateVM_Task( host=>$dstview, pool=>$hyphash{$target}->{pool}, priority=>VirtualMachineMovePriority->new('highPriority')); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&migrate_callback; $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $_, target=>$target }; } } else { #sendmsg("Waiting for BOTH to be 'good'"); return; #One of them is still 'pending' } } sub migrate { my %args = @_; my $nodes = $args{nodes}; my $hyp = $args{hyp}; my $exargs = $args{exargs}; my $tgthyp = $exargs->[0]; my $destination = ${$args{exargs}}[0]; my $vcenter = $hyphash{$hyp}->{vcenter}->{name}; actually_migrate({ nodes=>$nodes, hyp=>$hyp, target=>$tgthyp, vcenter=>$vcenter }); #The following code is now redundant. validate_vcenter_prereqs is now called well before this point. #We do target first to prevent multiple sources to single destination from getting confused #one source to multiple destinations (i.e. revacuate) may require other provisions #by getting confused, I mean that actually migrate not thinking both are good before it's correct # validate_vcenter_prereqs($tgthyp, \&actually_migrate, { # nodes=>$nodes, # hyp=>$hyp, # target=>$tgthyp, # vcenter=>$vcenter # }); # validate_vcenter_prereqs($hyp, \&actually_migrate, { # nodes=>$nodes, # hyp=>$hyp, # target=>$tgthyp, # vcenter=>$vcenter # }); } sub reconfig_callback { my $task = shift; my $args = shift; #$args->{reconfig_args}->{vmview}->update_view_data(); delete $args->{reconfig_args}->{vmview}; #Force a reload of the view, update_view_data seems to not work as advertised.. $args->{reconfig_fun}->(%{$args->{reconfig_args}}); } sub repower { #Called to try power again after power down for reconfig my $task = shift; my $args = shift; my $powargs=$args->{power_args}; $powargs->{pretendop}=1; #$powargs->{vmview}->update_view_data(); delete $powargs->{vmview}; #Force a reload of the view, update_view_data seems to not work as advertised.. power(%$powargs); } sub retry_rmvm { my $task = shift; my $args = shift; my $node = $args->{node}; my $state = $task->info->state->val; if ($state eq "success") { #$Data::Dumper::Maxdepth=2; delete $args->{args}->{vmview}; rmvm(%{$args->{args}}); } elsif ($state eq 'error') { relay_vmware_err($task,"",$node); } } sub rmvm { my %args = @_; my $node = $args{node}; my $hyp = $args{hyp}; if (not defined $args{vmview}) { #attempt one refresh $args{vmview} = $hyphash{$hyp}->{conn}->find_entity_view(view_type => 'VirtualMachine',properties=>['config.name','runtime.powerState'],filter=>{name=>$node}); } if (not defined $args{vmview}) { sendmsg([1,"VM does not appear to exist"],$node); return; } @ARGV= @{$args{exargs}}; require Getopt::Long; my $forceremove; my $purge; GetOptions( 'f' => \$forceremove, 'p' => \$purge, ); my $task; unless ($args{vmview}->{'runtime.powerState'}->val eq 'poweredOff') { if ($forceremove) { $task = $args{vmview}->PowerOffVM_Task(); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&retry_rmvm, $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $node, args=>\%args }; return; } else { sendmsg([1,"Cannot rmvm active guest (use -f argument to force)"],$node); return; } } if ($purge) { $task = $args{vmview}->Destroy_Task(); $running_tasks{$task}->{data} = { node => $node, successtext => 'purged' }; $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&generic_task_callback; $running_tasks{$task}->{hyp} = $args{hyp}; #$hyp_conns->{$hyp}; } else { $task = $args{vmview}->UnregisterVM(); } } sub getreconfigspec { my %args = @_; my $node = $args{node}; my $vmview = $args{view}; my $currid=$args{view}->config->guestId(); my $rightid=getguestid($node); my %conargs; my $reconfigneeded=0; if ($currid ne $rightid) { $reconfigneeded=1; $conargs{guestId}=$rightid; } my $newmem; if ($newmem = getUnits($tablecfg{vm}->{$node}->[0]->{memory},"M",1048576)) { my $currmem = $vmview->config->hardware->memoryMB; if ($newmem ne $currmem) { $conargs{memoryMB} = $newmem; $reconfigneeded=1; } } my $newcpus; if ($newcpus = $tablecfg{vm}->{$node}->[0]->{cpus}) { my $currncpu = $vmview->config->hardware->numCPU; if ($newcpus ne $currncpu) { $conargs{numCPUs} = $newcpus; $reconfigneeded=1; } } if ($reconfigneeded) { return VirtualMachineConfigSpec->new(%conargs); } else { return 0; } } #This routine takes a single node, managing vmv instance, and task tracking hash to submit a power on request sub power { my %args = @_; my $node = $args{node}; my $hyp = $args{hyp}; my $pretendop = $args{pretendop}; #to pretend a system was on for reset or boot when we have to turn it off internally for reconfig if (not defined $args{vmview}) { #attempt one refresh $args{vmview} = $hyphash{$hyp}->{conn}->find_entity_view(view_type => 'VirtualMachine',properties=>['config.name','config','runtime.powerState'],filter=>{name=>$node}); } @ARGV = @{$args{exargs}}; #for getoptions; my $forceon; require Getopt::Long; GetOptions( 'force|f' => \$forceon, ); my $subcmd = $ARGV[0]; my $intent=""; my $task; my $currstat; if ($args{vmview}) { $currstat = $args{vmview}->{'runtime.powerState'}->val; if (grep /$subcmd/,qw/on reset boot/) { my $reconfigspec; if ($reconfigspec = getreconfigspec(node=>$node,view=>$args{vmview})) { if ($currstat eq 'poweredOff') { #sendmsg("Correcting guestId because $currid and $rightid are not the same...");#DEBUG my $task = $args{vmview}->ReconfigVM_Task(spec=>$reconfigspec); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&reconfig_callback; $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $node, reconfig_fun=>\&power, reconfig_args=>\%args }; return; } elsif (grep /$subcmd/,qw/reset boot/) { #going to have to do a 'cycle' and present it up normally.. #sendmsg("DEBUG: forcing a cycle"); $task = $args{vmview}->PowerOffVM_Task(); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&repower; $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $node, power_args=>\%args}; return; #we have to wait } #TODO: fixit #sendmsg("I see vm has $currid and I want it to be $rightid"); } } } else { $currstat = 'off'; } if ($currstat eq 'poweredOff') { $currstat = 'off'; } elsif ($currstat eq 'poweredOn') { $currstat = 'on'; } elsif ($currstat eq 'suspended') { $currstat = 'suspend'; } if ($subcmd =~ /^stat/) { sendmsg($currstat,$node); return; } if ($subcmd =~ /boot/) { $intent = "$currstat "; if ($currstat eq 'on' or $args{pretendop}) { $intent = "on "; $subcmd = 'reset'; } else { $subcmd = 'on'; } } if ($subcmd =~ /on/) { if ($currstat eq 'off' or $currstat eq 'suspend') { if (not $args{vmview}) { #We are asking to turn on a system the hypervisor #doesn't know, attempt to register it first register_vm($hyp,$node,undef,\&power,\%args); return; #We'll pick it up on the retry if it gets registered } eval { $task = $args{vmview}->PowerOnVM_Task(host=>$hyphash{$hyp}->{hostview}); }; if ($@) { sendmsg([1,":".$@],$node); return; } $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&poweron_task_callback; $running_tasks{$task}->{hyp} = $args{hyp}; #$hyp_conns->{$hyp}; $running_tasks{$task}->{data} = { node => $node, successtext => $intent.'on', forceon=>$forceon }; } else { sendmsg($currstat,$node); } } elsif ($subcmd =~ /off/) { if ($currstat eq 'on') { $task = $args{vmview}->PowerOffVM_Task(); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&generic_task_callback; $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $node, successtext => 'off' }; } else { sendmsg($currstat,$node); } } elsif ($subcmd =~ /suspend/) { if ($currstat eq 'on') { $task = $args{vmview}->SuspendVM_Task(); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&generic_task_callback; $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $node, successtext => 'suspend' }; } else { sendmsg("off",$node); } } elsif ($subcmd =~ /reset/) { if ($currstat eq 'on') { $task = $args{vmview}->ResetVM_Task(); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&generic_task_callback; $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $node, successtext => $intent.'reset' }; } elsif ($args{pretendop}) { #It is off, but pretend it was on eval { $task = $args{vmview}->PowerOnVM_Task(host=>$hyphash{$hyp}->{hostview}); }; if ($@) { sendmsg([1,":".$@],$node); return; } $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&generic_task_callback; $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $node, successtext => $intent.'reset' }; } else { sendmsg($currstat,$node); } } } sub generic_vm_operation { #The general form of firing per-vm requests to ESX hypervisor my $properties = shift; #The relevant properties to the general task, MUST INCLUDE config.name my $function = shift; #The function to actually run against the right VM view my @exargs = @_; #Store the rest to pass on my $hyp; my $vmviews; my %vcviews; #views populated once per vcenter server for improved performance foreach $hyp (keys %hyphash) { if ($viavcenterbyhyp->{$hyp}) { if ($vcviews{$hyphash{$hyp}->{vcenter}->{name}}) { next; } $vcviews{$hyphash{$hyp}->{vcenter}->{name}} = $hyphash{$hyp}->{conn}->find_entity_views(view_type => 'VirtualMachine',properties=>$properties); foreach (@{$vcviews{$hyphash{$hyp}->{vcenter}->{name}}}) { my $node = $_->{'config.name'}; unless (defined $tablecfg{vm}->{$node}) { $node =~ s/\..*//; #try the short name; } if (defined $tablecfg{vm}->{$node}) { #see if the host pointer requires a refresh my $host = $hyphash{$hyp}->{conn}->get_view(mo_ref=>$_->{'runtime.host'}); $host = $host->summary->config->name; if ( $tablecfg{vm}->{$node}->[0]->{host} eq "$host" ) { next; } my $newnhost = inet_aton($host); my $oldnhost = inet_aton($tablecfg{vm}->{$node}->[0]->{host}); if ($newnhost eq $oldnhost) { next; } #it resolved fine my $shost = $host; $shost =~ s/\..*//; if ( $tablecfg{vm}->{$node}->[0]->{host} eq "$shost" ) { next; } #time to figure out which of these is a node my @nodes = noderange("$host,$shost"); my $vmtab = xCAT::Table->new("vm",-create=>1); unless($vmtab){ die "Error opening vm table"; } if ($nodes[0]) { print $node. " and ".$nodes[0]; $vmtab->setNodeAttribs($node,{host=>$nodes[0]}); } #else { # $vmtab->setNodeAttribs($node,{host=>$host}); #} } } } } foreach $hyp (keys %hyphash) { if ($viavcenterbyhyp->{$hyp}) { $vmviews= $vcviews{$hyphash{$hyp}->{vcenter}->{name}} } else { $vmviews = $hyphash{$hyp}->{conn}->find_entity_views(view_type => 'VirtualMachine',properties=>$properties); } my %mgdvms; #sort into a hash for convenience foreach (@$vmviews) { $mgdvms{$_->{'config.name'}} = $_; } my $node; foreach $node (sort (keys %{$hyphash{$hyp}->{nodes}})){ $function->( node=>$node, hyp=>$hyp, vmview=>$mgdvms{$node}, exargs=>\@exargs ); #REMINDER FOR RINV TO COME # foreach (@nothing) { #@{$mgdvms{$node}->config->hardware->device}) { # if (defined $_->{macAddress}) { # print "\nFound a mac: ".$_->macAddress."\n"; # } # } } } } sub generic_hyp_operation { #The general form of firing per-hypervisor requests to ESX hypervisor my $function = shift; #The function to actually run against the right VM view my @exargs = @_; #Store the rest to pass on my $hyp; foreach $hyp (keys %hyphash) { my @relevant_nodes = sort (keys %{$hyphash{$hyp}->{nodes}}); unless (scalar @relevant_nodes) { next; } $function->( nodes => \@relevant_nodes, hyp => $hyp, exargs => \@exargs ); #my $vmviews = $hyp_conns->{$hyp}->find_entity_views(view_type => 'VirtualMachine',properties=>['runtime.powerState','config.name']); #my %mgdvms; #sort into a hash for convenience #foreach (@$vmviews) { # $mgdvms{$_->{'config.name'}} = $_; #} #my $node; #foreach $node (sort (keys %{$hyp_hash->{$hyp}->{nodes}})){ # $function->($node,$mgdvms{$node},$taskstotrack,$callback,@exargs); #REMINDER FOR RINV TO COME # foreach (@nothing) { #@{$mgdvms{$node}->config->hardware->device}) { # if (defined $_->{macAddress}) { # print "\nFound a mac: ".$_->macAddress."\n"; # } # } # } } } sub rmhypervisor_disconnected { my $task = shift; my $parms = shift; my $node = $parms->{node}; my $hyp = $node; my $state = $task->info->state->val; if ($state eq 'success') { my $task = $hyphash{$hyp}->{hostview}->Destroy_Task(); $running_tasks{$task}->{data} = { node => $node, successtext => 'removed' }; $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&generic_task_callback; $running_tasks{$task}->{hyp} =$hyp; } elsif ($state eq 'error') { relay_vmware_err($task,"",$node); } } sub rmhypervisor_inmaintenance { my $task = shift; my $parms = shift; my $state = $task->info->state->val; my $node = $parms->{node}; my $intent = $parms->{successtext}; if ($state eq 'success') { my $hyp = $parms->{node}; my $task = $hyphash{$hyp}->{hostview}->DisconnectHost_Task(); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&rmhypervisor_disconnected; $running_tasks{$task}->{hyp} = $hyp; $running_tasks{$task}->{data} = { node => $hyp }; } elsif ($state eq 'error') { relay_vmware_err($task,"",$node); } } sub rmhypervisor { my %args = @_; my $hyp = $args{hyp}; $hyphash{$hyp}->{hostview} = get_hostview(hypname=>$hyp,conn=>$hyphash{$hyp}->{conn}); #,properties=>['config','configManager']); if (defined $hyphash{$hyp}->{hostview}) { my $task = $hyphash{$hyp}->{hostview}->EnterMaintenanceMode_Task(timeout=>0); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&rmhypervisor_inmaintenance; $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $hyp }; } return; } sub mkvms { my %args = @_; my $nodes = $args{nodes}; my $hyp = $args{hyp}; @ARGV = @{$args{exargs}}; #for getoptions; my $disksize; require Getopt::Long; GetOptions( 'size|s=s' => \$disksize ); my $node; $hyphash{$hyp}->{hostview} = get_hostview(hypname=>$hyp,conn=>$hyphash{$hyp}->{conn}); #,properties=>['config','configManager']); unless (validate_datastore_prereqs($nodes,$hyp)) { return; } $hyphash{$hyp}->{vmfolder} = $hyphash{$hyp}->{conn}->get_view(mo_ref=>$hyphash{$hyp}->{conn}->find_entity_view(view_type=>'Datacenter',properties=>['vmFolder'])->vmFolder); $hyphash{$hyp}->{pool} = $hyphash{$hyp}->{conn}->get_view(mo_ref=>$hyphash{$hyp}->{hostview}->parent,properties=>['resourcePool'])->resourcePool; my $cfg; foreach $node (@$nodes) { if ($hyphash{$hyp}->{conn}->find_entity_view(view_type=>"VirtualMachine",filter=>{name=>$node})) { sendmsg([1,"Virtual Machine already exists"],$node); next; } else { register_vm($hyp,$node,$disksize); } } my @dhcpnodes; foreach (keys %{$tablecfg{dhcpneeded}}) { push @dhcpnodes,$_; delete $tablecfg{dhcpneeded}->{$_}; } $executerequest->({command=>['makedhcp'],node=>\@dhcpnodes}); } sub setboot { my %args = @_; my $node = $args{node}; my $hyp = $args{hyp}; if (not defined $args{vmview}) { #attempt one refresh $args{vmview} = $hyphash{$hyp}->{conn}->find_entity_view(view_type => 'VirtualMachine',properties=>['config.name'],filter=>{name=>$node}); } my $bootorder = ${$args{exargs}}[0]; #NOTE: VMware simply does not currently seem to allow programatically changing the boot #order like other virtualization solutions supported by xCAT. #This doesn't behave quite like any existing mechanism: #vm.bootorder was meant to take the place of system nvram, vmware imitates that unfortunate aspect of bare metal too well.. #rsetboot was created to describe the ipmi scenario of a transient boot device, this is persistant *except* for setup, which is not #rbootseq was meant to be entirely persistant and ordered. #rsetboot is picked, the usage scenario matches about as good as I could think of my $reconfigspec; if ($bootorder =~ /setup/) { unless ($bootorder eq 'setup') { sendmsg([1,"rsetboot parameter may not contain 'setup' with other items, assuming vm.bootorder is just 'setup'"],$node); } $reconfigspec = VirtualMachineConfigSpec->new( bootOptions=>VirtualMachineBootOptions->new(enterBIOSSetup=>1), ); } else { $bootorder = "allow:".$bootorder; $reconfigspec = VirtualMachineConfigSpec->new( bootOptions=>VirtualMachineBootOptions->new(enterBIOSSetup=>0), extraConfig => [OptionValue->new(key => 'bios.bootDeviceClasses',value=>$bootorder)] ); } my $task = $args{vmview}->ReconfigVM_Task(spec=>$reconfigspec); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&generic_task_callback; $running_tasks{$task}->{hyp} = $args{hyp}; $running_tasks{$task}->{data} = { node => $node, successtext => ${$args{exargs}}[0] }; } sub register_vm {#Attempt to register existing instance of a VM my $hyp = shift; my $node = shift; my $disksize = shift; my $blockedfun = shift; #a pointer to a blocked function to call on success my $blockedargs = shift; #hash reference to call blocked function with my $task; validate_network_prereqs([keys %{$hyphash{$hyp}->{nodes}}],$hyp); unless (defined $hyphash{$hyp}->{datastoremap} or validate_datastore_prereqs([keys %{$hyphash{$hyp}->{nodes}}],$hyp)) { die "unexpected condition"; } unless (defined $hyphash{$hyp}->{vmfolder}) { $hyphash{$hyp}->{vmfolder} = $hyphash{$hyp}->{conn}->get_view(mo_ref=>$hyphash{$hyp}->{conn}->find_entity_view(view_type=>'Datacenter',properties=>['vmFolder'])->vmFolder); } unless (defined $hyphash{$hyp}->{pool}) { $hyphash{$hyp}->{pool} = $hyphash{$hyp}->{conn}->get_view(mo_ref=>$hyphash{$hyp}->{hostview}->parent,properties=>['resourcePool'])->resourcePool; } my $success = eval { $task = $hyphash{$hyp}->{vmfolder}->RegisterVM_Task(path=>getcfgdatastore($node,$hyphash{$hyp}->{datastoremap})." /$node/$node.vmx",name=>$node,pool=>$hyphash{$hyp}->{pool},asTemplate=>0); }; if ($@ or not $success) { print $@; register_vm_callback(undef, { node => $node, disksize => $disksize, blockedfun => $blockedfun, blockedargs => $blockedargs, hyp => $hyp }); } if ($task) { $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \®ister_vm_callback; $running_tasks{$task}->{hyp} = $hyp; $running_tasks{$task}->{data} = { node => $node, disksize => $disksize, blockedfun => $blockedfun, blockedargs => $blockedargs, hyp => $hyp }; } } sub register_vm_callback { my $task = shift; my $args = shift; if (not $task or $task->info->state->val eq 'error') { #TODO: fail for 'rpower' flow, mkvm is too invasive in VMWare to be induced by 'rpower on' if (not defined $args->{blockedfun}) { mknewvm($args->{node},$args->{disksize},$args->{hyp}); } else { sendmsg([1,"mkvm must be called before use of this function"],$args->{node}); } } elsif (defined $args->{blockedfun}) { #If there is a blocked function, call it here) $args->{blockedfun}->(%{$args->{blockedargs}}); } } sub getcfgdatastore { my $node = shift; my $dses = shift; my $cfgdatastore = $tablecfg{vm}->{$node}->[0]->{cfgstore}; unless ($cfgdatastore) { $cfgdatastore = $tablecfg{vm}->{$node}->[0]->{storage}; #TODO: if multiple drives are specified, make sure to split this out #DONE: I believe the regex after this conditional takes care of that case already.. } $cfgdatastore =~ s/=.*//; (my $method,my $location) = split /:\/\//,$cfgdatastore,2; (my $server,my $path) = split/\//,$location,2; $server =~ s/:$//; #tolerate habitual colons my $servern = inet_aton($server); unless ($servern) { sendmsg([1,"could not resolve '$server' to an address from vm.storage/vm.cfgstore"]); } $server = inet_ntoa($servern); my $uri = "nfs://$server/$path"; $cfgdatastore = "[".$dses->{$uri}."]"; #$cfgdatastore =~ s/,.*$//; #these two lines of code were kinda pointless #$cfgdatastore =~ s/\/$//; return $cfgdatastore; } sub mknewvm { my $node=shift; my $disksize=shift; my $hyp=shift; #TODO: above my $cfg = build_cfgspec($node,$hyphash{$hyp}->{datastoremap},$hyphash{$hyp}->{nets},$disksize,$hyp); my $task = $hyphash{$hyp}->{vmfolder}->CreateVM_Task(config=>$cfg,pool=>$hyphash{$hyp}->{pool},host=>$hyphash{$hyp}->{hostview}); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&mkvm_callback; $running_tasks{$task}->{hyp} = $hyp; $running_tasks{$task}->{data} = { node => $node }; } sub getUnits { my $amount = shift; my $defunit = shift; my $divisor=shift; unless ($amount) { return; } unless ($divisor) { $divisor = 1; } if ($amount =~ /(\D)$/) { #If unitless, add unit $defunit=$1; chop $amount; } if ($defunit =~ /k/i) { return $amount*1024/$divisor; } elsif ($defunit =~ /m/i) { return $amount*1048576/$divisor; } elsif ($defunit =~ /g/i) { return $amount*1073741824/$divisor; } } sub getguestid { my $osfound=0; my $node = shift; my $nodeos = $tablecfg{nodetype}->{$node}->[0]->{os}; my $nodearch = $tablecfg{nodetype}->{$node}->[0]->{arch}; foreach (keys %guestidmap) { if ($nodeos =~ /$_/) { if ($nodearch eq 'x86_64') { $nodeos=$guestidmap{$_}."64Guest"; } else { $nodeos=$guestidmap{$_}; $nodeos =~ s/_$//; $nodeos .= "Guest"; } $osfound=1; last; } } unless ($osfound) { if ($nodearch eq 'x86_64') { $nodeos="otherGuest64"; } else { $nodeos="otherGuest"; } } return $nodeos; } sub build_cfgspec { my $node = shift; my $dses = shift; #map to match vm table to datastore names my $netmap = shift; my $disksize = shift; my $hyp = shift; my $memory; my $ncpus; unless ($memory = getUnits($tablecfg{vm}->{$node}->[0]->{memory},"M",1048576)) { $memory = 512; } unless ($ncpus = $tablecfg{vm}->{$node}->[0]->{cpus}) { $ncpus = 1; } my @devices; $currkey=0; push @devices,create_storage_devs($node,$dses,$disksize); push @devices,create_nic_devs($node,$netmap,$hyp); #my $cfgdatastore = $tablecfg{vm}->{$node}->[0]->{storage}; #TODO: need a new cfglocation field in case of stateless guest? #$cfgdatastore =~ s/,.*$//; #$cfgdatastore =~ s/\/$//; #$cfgdatastore = "[".$dses->{$cfgdatastore}."]"; my $cfgdatastore = getcfgdatastore($node,$dses); my $vfiles = VirtualMachineFileInfo->new(vmPathName=>$cfgdatastore); #my $nodeos = $tablecfg{nodetype}->{$node}->[0]->{os}; #my $nodearch = $tablecfg{nodetype}->{$node}->[0]->{arch}; my $nodeos = getguestid($node); #nodeos=>$nodeos,nodearch=>$nodearch); return VirtualMachineConfigSpec->new( name => $node, files => $vfiles, guestId=>$nodeos, memoryMB => $memory, numCPUs => $ncpus, deviceChange => \@devices, ); } sub create_nic_devs { my $node = shift; my $netmap = shift; my $hyp = shift; my @networks = split /,/,$tablecfg{vm}->{$node}->[0]->{nics}; my @devs; my $idx = 0; my @macs = xCAT::VMCommon::getMacAddresses(\%tablecfg,$node,scalar @networks); my $connprefs=VirtualDeviceConnectInfo->new( allowGuestControl=>1, connected=>0, startConnected => 1 ); foreach (@networks) { my $pgname = $hyphash{$hyp}->{pgnames}->{$_}; s/.*://; s/=.*//; my $netname = $_; #print Dumper($netmap); my $backing = VirtualEthernetCardNetworkBackingInfo->new( network => $netmap->{$pgname}, deviceName=>$pgname, ); my $newcard=VirtualE1000->new( key=>0,#3, #$currkey++, backing=>$backing, addressType=>"manual", macAddress=>shift @macs, connectable=>$connprefs, wakeOnLanEnabled=>1, #TODO: configurable in tables? ); push @devs,VirtualDeviceConfigSpec->new(device => $newcard, operation => VirtualDeviceConfigSpecOperation->new('add')); $idx++; } return @devs; die "Stop running for test"; } sub create_storage_devs { my $node = shift; my $sdmap = shift; my $sizes = shift; my @sizes = split /[,:]/, $sizes; my $scsicontrollerkey=0; my $idecontrollerkey=200; #IDE 'controllers' exist at 200 and 201 invariably, with no flexibility? #Cannot find documentation that declares this absolute, but attempts to do otherwise #lead in failure, also of note, these are single-channel controllers, so two devs per controller my $backingif; my @devs; my $havescsidevs =0; my $disktype = 'ide'; my $unitnum=0; #Going to make IDE controllers for now, aiming for #lowest common denominator guest driver for #changing hypervisor technology my %disktocont; my $dev; my @storelocs = split /,/,$tablecfg{vm}->{$node}->[0]->{storage}; #number of devices is the larger of the specified sizes (TODO: masters) or storage pools to span my $numdevs = (scalar @storelocs > scalar @sizes ? scalar @storelocs : scalar @sizes); while ($numdevs-- > 0) { my $storeloc = shift @storelocs; unless (scalar @storelocs) { @storelocs = ($storeloc); } #allow reuse of one cfg specified pool for multiple devs my $disksize = shift @sizes; unless (scalar @sizes) { @sizes = ($disksize); } #if we emptied the array, stick the last entry back on to allow it to specify all remaining disks $disksize = getUnits($disksize,'G',1024); $disktype = 'ide'; if ($storeloc =~ /=/) { ($storeloc,$disktype) = split /=/,$storeloc; } $storeloc =~ s/\/$//; (my $method,my $location) = split /:\/\//,$storeloc,2; (my $server,my $path) = split/\//,$location,2; $server =~ s/:$//; #tolerate habitual colons my $servern = inet_aton($server); unless ($servern) { sendmsg([1,"could not resolve '$server' to an address from vm.storage"]); return; } $server = inet_ntoa($servern); my $uri = "nfs://$server/$path"; $backingif = VirtualDiskFlatVer2BackingInfo->new(diskMode => 'persistent', fileName => "[".$sdmap->{$uri}."]"); if ($disktype eq 'ide' and $idecontrollerkey eq 1 and $unitnum eq 0) { #reserve a spot for CD $unitnum = 1; } elsif ($disktype eq 'ide' and $unitnum eq 2) { #go from current to next ide 'controller' $idecontrollerkey++; $unitnum=0; } unless ($disktype eq 'ide') { push @{$disktocont{$scsicontrollerkey}},$currkey; } my $controllerkey; if ($disktype eq 'ide') { $controllerkey = $idecontrollerkey; } else { $controllerkey = $scsicontrollerkey; $havescsidevs=1; } $dev =VirtualDisk->new(backing=>$backingif, controllerKey => $controllerkey, key => $currkey++, unitNumber => $unitnum++, capacityInKB => $disksize); push @devs,VirtualDeviceConfigSpec->new(device => $dev, fileOperation => VirtualDeviceConfigSpecFileOperation->new('create'), operation => VirtualDeviceConfigSpecOperation->new('add')); } #It *seems* that IDE controllers are not subject to require creation, so we skip it if ($havescsidevs) { #need controllers to attach the disks to foreach(0..$scsicontrollerkey) { $dev=VirtualLsiLogicController->new(key => $_, device => \@{$disktocont{$_}}, sharedBus => VirtualSCSISharing->new('noSharing'), busNumber => $_); push @devs,VirtualDeviceConfigSpec->new(device => $dev, operation => VirtualDeviceConfigSpecOperation->new('add')); } } return @devs; # my $ctlr = VirtualIDEController->new( } sub declare_ready { my %args = %{shift()}; $hypready{$args{hyp}}=1; } sub validate_vcenter_prereqs { #Communicate with vCenter and ensure this host is added correctly to a vCenter instance when an operation requires it my $hyp = shift; my $depfun = shift; my $depargs = shift; my $vcenter = $hyphash{$hyp}->{vcenter}->{name}; unless ($hyphash{$hyp}->{vcenter}->{conn}) { eval { $hyphash{$hyp}->{vcenter}->{conn} = Vim->new(service_url=>"https://$vcenter/sdk"); $hyphash{$hyp}->{vcenter}->{conn}->login(user_name=>$hyphash{$hyp}->{vcenter}->{username},password=>$hyphash{$hyp}->{vcenter}->{password}); }; if ($@) { $hyphash{$hyp}->{vcenter}->{conn} = undef; } } unless ($hyphash{$hyp}->{vcenter}->{conn}) { sendmsg([1,": Unable to reach vCenter server managing $hyp"]); return undef; } my $foundhyp; my $name=$hyp; if ($usehostnamesforvcenter and $usehostnamesforvcenter !~ /no/i) { if ($tablecfg{hosts}->{$hyp}->[0]->{hostnames}) { $name = $tablecfg{hosts}->{$hyp}->[0]->{hostnames}; } } my $connspec = HostConnectSpec->new( hostName=>$name, password=>$hyphash{$hyp}->{password}, userName=>$hyphash{$hyp}->{username}, force=>1, ); foreach (@{$hyphash{$hyp}->{vcenter}->{conn}->find_entity_views(view_type=>'HostSystem',properties=>['summary.config.name','summary.runtime.connectionState','runtime.inMaintenanceMode','parent','configManager'])}) { if ($_->{'summary.config.name'} =~ /^$hyp(?:\.|\z)/ or $_->{'summary.config.name'} =~ /^$name(?:\.|\z)/) { #Looks good, call the dependent function after declaring the state of vcenter to hypervisor as good if ($_->{'summary.runtime.connectionState'}->val eq 'connected') { enable_vmotion(hypname=>$hyp,hostview=>$_,conn=>$hyphash{$hyp}->{vcenter}->{conn}); $vcenterhash{$vcenter}->{$hyp} = 'good'; $depfun->($depargs); return 1; } else { my $ref_to_delete; if ($_->parent->type eq 'ClusterComputeResource') { #We are allowed to specifically kill a host in a cluster $ref_to_delete = $_->{mo_ref}; } elsif ($_->parent->type eq 'ComputeResource') { #For some reason, we must delete the container instead $ref_to_delete = $_->{parent}; } my $task = $hyphash{$hyp}->{vcenter}->{conn}->get_view(mo_ref=>$ref_to_delete)->Destroy_Task(); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&addhosttovcenter; $running_tasks{$task}->{conn} = $hyphash{$hyp}->{vcenter}->{conn}; $running_tasks{$task}->{data} = { depfun => $depfun, depargs => $depargs, conn=> $hyphash{$hyp}->{vcenter}->{conn}, connspec=>$connspec,hostview=>$_,hypname=>$hyp,vcenter=>$vcenter }; return undef; #The rest would be shorter/ideal, but seems to be confused a lot by stateless #Maybe in a future VMWare technology level the following would work better #than it does today # my $task = $_->ReconnectHost_Task(cnxSpec=>$connspec); # my $task = $_->DisconnectHost_Task(); # $running_tasks{$task}->{task} = $task; # $running_tasks{$task}->{callback} = \&disconnecthost_callback; # $running_tasks{$task}->{conn} = $hyphash{$hyp}->{vcenter}->{conn}; # $running_tasks{$task}->{data} = { depfun => $depfun, depargs => $depargs, conn=> $hyphash{$hyp}->{vcenter}->{conn}, connspec=>$connspec,hostview=>$_,hypname=>$hyp,vcenter=>$vcenter }; #ADDHOST } last; } } #If still in function, haven't found any likely host entries, make a new one eval { $hyphash{$hyp}->{conn} = Vim->new(service_url=>"https://$hyp/sdk"); #Direct connect to install/check licenses $hyphash{$hyp}->{conn}->login(user_name=>$hyphash{$hyp}->{username},password=>$hyphash{$hyp}->{password}); }; if ($@) { sendmsg([1,": Failed to communicate with $hyp"]); $hyphash{$hyp}->{conn} = undef; return "failed"; } validate_licenses($hyp); addhosttovcenter(undef,{ depfun => $depfun, depargs => $depargs, conn=>$hyphash{$hyp}->{vcenter}->{conn}, connspec=>$connspec, hypname=>$hyp, vcenter=>$vcenter, }); } sub addhosttovcenter { my $task = shift; my $args = shift; my $hyp = $args->{hypname}; my $depfun = $args->{depfun}; my $depargs = $args->{depargs}; my $connspec = $args->{connspec}; my $vcenter = $args->{vcenter}; if ($task) { my $state = $task->info->state->val; if ($state eq 'error') { die; } } if ($tablecfg{hypervisor}->{$hyp}->[0]->{cluster}) { my $cluster = get_clusterview(clustname=>$tablecfg{hypervisor}->{$hyp}->[0]->{cluster},conn=>$hyphash{$hyp}->{vcenter}->{conn}); unless ($cluster) { sendmsg([1,$tablecfg{hypervisor}->{$hyp}->[0]->{cluster}. " is not a known cluster to the vCenter server."]); $hypready{$hyp}=-1; #Declare impossiblility to be ready return; } $task = $cluster->AddHost_Task(spec=>$connspec,asConnected=>1); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&connecthost_callback; $running_tasks{$task}->{conn} = $hyphash{$hyp}->{vcenter}->{conn}; $running_tasks{$task}->{data} = { depfun => $depfun, depargs=> $depargs, conn=> $hyphash{$hyp}->{vcenter}->{conn}, connspec=>$connspec, cluster=>$cluster, hypname=>$hyp, vcenter=>$vcenter }; } else { my $datacenter = validate_datacenter_prereqs($hyp); my $hfolder = $datacenter->hostFolder; #$hyphash{$hyp}->{vcenter}->{conn}->find_entity_view(view_type=>'Datacenter',properties=>['hostFolder'])->hostFolder; $hfolder = $hyphash{$hyp}->{vcenter}->{conn}->get_view(mo_ref=>$hfolder); $task = $hfolder->AddStandaloneHost_Task(spec=>$connspec,addConnected=>1); $running_tasks{$task}->{task} = $task; $running_tasks{$task}->{callback} = \&connecthost_callback; $running_tasks{$task}->{conn} = $hyphash{$hyp}->{vcenter}->{conn}; $running_tasks{$task}->{data} = { depfun => $depfun, depargs=> $depargs, conn=> $hyphash{$hyp}->{vcenter}->{conn}, connspec=>$connspec, foldview=>$hfolder, hypname=>$hyp, vcenter=>$vcenter }; } #print Dumper @{$hyphash{$hyp}->{vcenter}->{conn}->find_entity_views(view_type=>'HostSystem',properties=>['runtime.connectionState'])}; } sub validate_datacenter_prereqs { my ($hyp) = @_; my $datacenter = $hyphash{$hyp}->{vcenter}->{conn}->find_entity_view(view_type => 'Datacenter', properties=>['hostFolder']); if (!defined $datacenter) { my $vconn = $hyphash{$hyp}->{vcenter}->{conn}; my $root_folder = $vconn->get_view(mo_ref=>$vconn->get_service_content()->rootFolder); $root_folder->CreateDatacenter(name=>'xcat-datacenter'); $datacenter = $hyphash{$hyp}->{vcenter}->{conn}->find_entity_view(view_type => 'Datacenter', properties=>['hostFolder']); } return $datacenter; } sub get_default_switch_for_hypervisor { #This will make sure the default, implicit switch is in order in accordance #with the configuration. If nothing specified, it just spits out vSwitch0 #if something specified, make sure it exists #if it doesn't exist, and the syntax explains how to build it, build it #return undef if something is specified, doesn't exist, and lacks instruction my $hyp = shift; my $defswitch = 'vSwitch0'; my $switchmembers; if ($tablecfg{hypervisor}->{$hyp}->[0]->{defaultnet}) { $defswitch = $tablecfg{hypervisor}->{$hyp}->[0]->{defaultnet}; ($defswitch,$switchmembers) = split /=/,$defswitch,2; my $vswitch; my $hostview = $hyphash{$hyp}->{hostview}; foreach $vswitch (@{$hostview->config->network->vswitch}) { if ($vswitch->name eq $defswitch) { return $defswitch; } } #If still here, means we need to build the switch unless ($switchmembers) { return undef; } #No hope, no idea how to make it return create_vswitch($hyp,$defswitch,split(/&/,$switchmembers)); } else { return 'vSwitch0'; } } sub get_switchname_for_portdesc { #Thisk function will examine all current switches to find or create a switch to match the described requirement my $hyp = shift; my $portdesc = shift; my $description; #actual name to use for the virtual switch if ($tablecfg{hypervisor}->{$hyp}->[0]->{netmap}) { foreach (split /,/,$tablecfg{hypervisor}->{$hyp}->[0]->{netmap}) { if (/^$portdesc=/) { ($description,$portdesc) = split /=/,$_,2; last; } } } else { $description = 'vsw'.$portdesc; } unless ($description) { sendmsg([1,": Invalid format for hypervisor.netmap detected for $hyp"]); return undef; } my %requiredports; my %portkeys; foreach (split /&/,$portdesc) { $requiredports{$_}=1; } my $hostview = $hyphash{$hyp}->{hostview}; unless ($hostview) { $hyphash{$hyp}->{hostview} = get_hostview(hypname=>$hyp,conn=>$hyphash{$hyp}->{conn}); #,properties=>['config','configManager']); $hostview = $hyphash{$hyp}->{hostview}; } foreach (@{$hostview->config->network->pnic}) { if ($requiredports{$_->device}) { #We establish lookups both ways $portkeys{$_->key}=$_->device; delete $requiredports{$_->device}; } } if (keys %requiredports) { sendmsg([1,":Unable to locate the following nics on $hyp: ".join(',',keys %requiredports)]); return undef; } my $foundmatchswitch; my $cfgmismatch=0; my $vswitch; foreach $vswitch (@{$hostview->config->network->vswitch}) { $cfgmismatch=0; #new switch, no sign of mismatch foreach (@{$vswitch->pnic}) { if ($portkeys{$_}) { $foundmatchswitch=$vswitch->name; delete $requiredports{$portkeys{$_}}; delete $portkeys{$_}; } else { $cfgmismatch=1; #If this turns out to have anything, it is bad } } if ($foundmatchswitch) { last; } } if ($foundmatchswitch) { if ($cfgmismatch) { sendmsg([1,": Aggregation mismatch detected, request nic is aggregated with a nic not requested"]); return undef; } unless (keys %portkeys) { return $foundmatchswitch; } die "TODO: add physical nics to aggregation if requested"; } else { return create_vswitch($hyp,$description,values %portkeys); } die "impossible occurance"; return undef; } sub create_vswitch { my $hyp = shift; my $description = shift; my @ports = @_; my $vswitch = HostVirtualSwitchBondBridge->new( nicDevice=>\@ports ); my $vswspec = HostVirtualSwitchSpec->new( bridge=>$vswitch, mtu=>1500, numPorts=>64 ); my $hostview = $hyphash{$hyp}->{hostview}; my $netman=$hyphash{$hyp}->{conn}->get_view(mo_ref=>$hostview->configManager->networkSystem); $netman->AddVirtualSwitch( vswitchName=>$description, spec=>$vswspec ); return $description; } sub validate_network_prereqs { my $nodes = shift; my $hyp = shift; my $hypconn = $hyphash{$hyp}->{conn}; my $hostview = $hyphash{$hyp}->{hostview}; if ($hostview) { $hostview->update_view_data(); #pull in changes induced by previous activity } else { $hyphash{$hyp}->{hostview} = get_hostview(hypname=>$hyp,conn=>$hyphash{$hyp}->{conn}); #,properties=>['config','configManager','network']); $hostview = $hyphash{$hyp}->{hostview}; } my $node; my $method; my $location; if (defined $hostview->{network}) { foreach (@{$hostview->network}) { my $nvw = $hypconn->get_view(mo_ref=>$_); if (defined $nvw->name) { $hyphash{$hyp}->{nets}->{$nvw->name}=$_; } } } foreach $node (@$nodes) { my @networks = split /,/,$tablecfg{vm}->{$node}->[0]->{nics}; foreach (@networks) { my $switchname = get_default_switch_for_hypervisor($hyp); my $tabval=$_; my $pgname; s/=.*//; #TODO specify nic model with =model if (/:/) { #The config specifies a particular path in some way s/(.*)://; $switchname = get_switchname_for_portdesc($hyp,$1); $pgname=$switchname."-".$_; } else { #Use the default vswitch per table config to connect this through, use the same name we did before to maintain compatibility $pgname=$_; } my $netname = $_; my $netsys; $hyphash{$hyp}->{pgnames}->{$tabval}=$pgname; my $policy = HostNetworkPolicy->new(); unless ($hyphash{$hyp}->{nets}->{$pgname}) { my $vlanid; if ($netname =~ /trunk/) { $vlanid=4095; } elsif ($netname =~ /vl(an)?(\d+)$/) { $vlanid=$2; } else { $vlanid = 0; } my $hostgroupdef = HostPortGroupSpec->new( name =>$pgname, vlanId=>$vlanid, policy=>$policy, vswitchName=>$switchname ); unless ($netsys) { $netsys = $hyphash{$hyp}->{conn}->get_view(mo_ref=>$hostview->configManager->networkSystem); } $netsys->AddPortGroup(portgrp=>$hostgroupdef); #$hyphash{$hyp}->{nets}->{$netname}=1; $hostview->update_view_data(); #pull in changes induced by previous activity if (defined $hostview->{network}) { #We load the new object references foreach (@{$hostview->network}) { my $nvw = $hypconn->get_view(mo_ref=>$_); if (defined $nvw->name) { $hyphash{$hyp}->{nets}->{$nvw->name}=$_; } } } } } } return 1; } sub validate_datastore_prereqs { my $nodes = shift; my $hyp = shift; my $hypconn = $hyphash{$hyp}->{conn}; my $hostview = $hyphash{$hyp}->{hostview}; unless ($hostview) { $hyphash{$hyp}->{hostview} = get_hostview(hypname=>$hyp,conn=>$hypconn); #,properties=>['config','configManager']); $hostview = $hyphash{$hyp}->{hostview}; } my $node; my $method; my $location; if (defined $hostview->{datastore}) { # only iterate if it exists foreach (@{$hostview->datastore}) { my $dsv = $hypconn->get_view(mo_ref=>$_); if (defined $dsv->info->{nas}) { if ($dsv->info->nas->type eq 'NFS') { my $mnthost = inet_aton($dsv->info->nas->remoteHost); if ($mnthost) { $mnthost = inet_ntoa($mnthost); } else { $mnthost = $dsv->info->nas->remoteHost; sendmsg([1,"Unable to resolve VMware specified host '".$dsv->info->nas->remoteHost."' to an address, problems may occur"]); } $hyphash{$hyp}->{datastoremap}->{"nfs://".$mnthost.$dsv->info->nas->remotePath}=$dsv->info->name; } #TODO: care about SMB } #TODO: care about VMFS } } my $refresh_names=0; foreach $node (@$nodes) { my @storage = split /,/,$tablecfg{vm}->{$node}->[0]->{storage}; if ($tablecfg{vm}->{$node}->[0]->{cfgstore}) { push @storage,$tablecfg{vm}->{$node}->[0]->{cfgstore}; } foreach (@storage) { s/=.*//; #remove device type information from configuration s/\/$//; #Strip trailing slash if specified, to align to VMware semantics if (/:\/\//) { ($method,$location) = split /:\/\//,$_,2; (my $server, my $path) = split /\//,$location,2; $server =~ s/:$//; #remove a : if someone put it in out of nfs mount habit my $servern = inet_aton($server); unless ($servern) { sendmsg([1,": Unable to resolve '$server' to an address, check vm.cfgstore/vm.storage"]); return 0; } $server = inet_ntoa($servern); my $uri = "nfs://$server/$path"; unless ($method =~ /nfs/) { sendmsg([1,": $method is unsupported at this time (nfs would be)"],$node); return 0; } unless ($hyphash{$hyp}->{datastoremap}->{$uri}) { #If not already there, must mount it $refresh_names=1; $hyphash{$hyp}->{datastoremap}->{$uri}=mount_nfs_datastore($hostview,$location); } } else { sendmsg([1,": $_ not supported storage specification for ESX plugin, 'nfs:///' only currently supported vm.storage supported for ESX at the moment"],$node); return 0; } #TODO: raw device mapping, VMFS via iSCSI, VMFS via FC? } } if ($refresh_names) { #if we are in a vcenter context, vmware can rename a datastore behind our backs immediately after adding $hostview->update_view_data(); if (defined $hostview->{datastore}) { # only iterate if it exists foreach (@{$hostview->datastore}) { my $dsv = $hypconn->get_view(mo_ref=>$_); if (defined $dsv->info->{nas}) { if ($dsv->info->nas->type eq 'NFS') { my $mnthost = inet_aton($dsv->info->nas->remoteHost); if ($mnthost) { $mnthost = inet_ntoa($mnthost); } else { $mnthost = $dsv->info->nas->remoteHost; sendmsg([1,"Unable to resolve VMware specified host '".$dsv->info->nas->remoteHost."' to an address, problems may occur"]); } $hyphash{$hyp}->{datastoremap}->{"nfs://".$mnthost.$dsv->info->nas->remotePath}=$dsv->info->name; } #TODO: care about SMB } #TODO: care about VMFS } } } return 1; } sub getlabel_for_datastore { my $method = shift; my $location = shift; $location =~ s/\//_/g; $location= $method.'_'.$location; #VMware has a 42 character limit, we will start mangling to get under 42. #Will try to preserve as much informative detail as possible, hence several conditionals instead of taking the easy way out if (length($location) > 42) { $location =~ s/nfs_//; #Ditch unique names for different protocols to the same path, seems unbelievably unlikely } if (length($location) > 42) { $location =~ s/\.//g; #Next, ditch host delimiter, it is unlikely that hosts will have unique names if their dots are removed } if (length($location) > 42) { $location =~ s/_//g; #Next, ditch path delimiter, it is unlikely that two paths will happen to look the same without delimiters } if (length($location) > 42) { #finally, replace the middle with ellipsis substr($location,20,-20,'..'); } return $location; } sub mount_nfs_datastore { my $hostview = shift; my $location = shift; my $server; my $path; ($server,$path) = split /\//,$location,2; $location = getlabel_for_datastore('nfs',$location); my $nds = HostNasVolumeSpec->new(accessMode=>'readWrite', remoteHost=>$server, localPath=>$location, remotePath=>"/".$path); my $dsmv = $hostview->{vim}->get_view(mo_ref=>$hostview->configManager->datastoreSystem); eval { $dsmv->CreateNasDatastore(spec=>$nds); }; if ($@) { die "$@" unless $@ =~ m/Fault detail: DuplicateNameFault/; die "esx plugin: a datastore was discovered with the same name referring to a different nominatum- cannot continue\n$@" unless &match_nfs_datastore($server,"/$path",$hostview->{vim}); } return $location; } sub lsvm { my $hyp = shift; my $hyphash = shift; my $callback = shift; my ($node,$img, $imgname, $out); my $f1 = `ssh $hyp "ls /vmfs/volumes/images/"`; $callback->({data=>[$f1]}); } sub build_more_info{ die("TODO: fix this function if called"); print "Does this acually get called????**********************************\n"; my $noderange=shift; my $callback=shift; my $vmtab = xCAT::Table->new("vm"); my @moreinfo=(); unless ($vmtab) { $callback->({data=>["Cannot open mp table"]}); return @moreinfo; } my %mpa_hash=(); foreach my $node (@$noderange) { my $ent=$vmtab->getNodeAttribs($node,['mpa', 'id']); if (defined($ent->{mpa})) { push @{$mpa_hash{$ent->{mpa}}{nodes}}, $node;} else { $callback->({data=>["no mpa defined for node $node"]}); return @moreinfo;; } if (defined($ent->{id})) { push @{$mpa_hash{$ent->{mpa}}{ids}}, $ent->{id};} else { push @{$mpa_hash{$ent->{mpa}}{ids}}, "";} } foreach (keys %mpa_hash) { push @moreinfo, "\[$_\]\[" . join(',',@{$mpa_hash{$_}{nodes}}) ."\]\[" . join(',',@{$mpa_hash{$_}{ids}}) . "\]"; } return \@moreinfo; } sub copycd { my $request = shift; my $doreq = shift; my $distname = ""; my $path; my $arch; my $darch; my $installroot; $installroot = "/install"; my $sitetab = xCAT::Table->new('site'); if($sitetab){ (my $ref) = $sitetab->getAttribs({key => 'installdir'}, 'value'); if ($ref and $ref->{value}) { $installroot = $ref->{value}; } } @ARGV = @{$request->{arg}}; GetOptions( 'n=s' => \$distname, 'a=s' => \$arch, 'p=s' => \$path ); # run a few tests to see if the copycds should use this plugin unless ($path){ # can't use us cause we need a path and you didn't provide one! return; } if( $distname and $distname !~ /^esx/ ){ # we're for esx, so if you didn't specify that its not us! return; } my $found = 0; if (-r $path . "/README" and -r $path . "/build_number" and -d $path . "/VMware" and -r $path . "/packages.xml") { #We have a probable new style ESX media open(LINE,$path."/packages.xml"); my $product; my $version; while () { if (/roductLineId>([^<]*)<\/Prod/) { $product = $1; } if (/ersion>([^<]*)<\/version/) { $version = $1; $version =~ s/\.0$//; } if (/arch>([^>]*)<\/arch/) { unless ($darch and $darch =~ /x86_64/) { #prefer to be characterized as x86_64 $darch = $1; $arch = $1; } } } close(LINE); if ($product and $version) { $distname = $product.$version; $found = 1; } } elsif (-r $path . "/README" and -r $path . "/open_source_licenses.txt" and -d $path . "/VMware") { #Candidate to be ESX 3.5 open(LINE,$path."/README"); while() { if (/VMware ESX Server 3.5\s*$/) { $darch ='x86'; $arch = 'x86'; $distname = 'esx3.5'; $found = 1; last; } } close(LINE); } elsif (-r $path . "/README.txt" and -r $path . "/vmkernel.gz"){ # its an esxi dvd! # if we got here its probably ESX they want to copy my $line; my $darch; open(LINE, $path . "/README.txt") or die "couldn't open!"; while($line = ){ chomp($line); if($line =~ /VMware ESXi version 4\.(\d+)/){ $darch = "x86_64"; $distname = "esxi4"; if ($1) { $distname .= '.'.$1; } $found = 1; if( $arch and $arch ne $darch){ sendmsg([1, "Requested distribution architecture $arch, but media is $darch"]); return; } $arch = $darch; last; # we found our distro! end this loop madness. } } close(LINE); unless($found){ sendmsg([1,"I don't recognize this VMware ESX DVD"]); return; # doesn't seem to be a valid DVD or CD } } elsif (-r $path . "/vmkernel.gz" and -r $path . "/isolinux.cfg"){ open(LINE,$path . "/isolinux.cfg"); while () { if (/ThinESX Installer/) { $darch = 'x86'; $arch='x86'; $distname='esxi3.5'; $found=1; last; } } close(LINE); } unless ($found) { return; } #not our media sendmsg("Copying media to $installroot/$distname/$arch/"); my $omask = umask 0022; mkpath("$installroot/$distname/$arch"); umask $omask; my $rc; my $reaped = 0; $SIG{INT} = $SIG{TERM} = sub { foreach(@cpiopid){ kill 2, $_; } if ($::CDMOUNTPATH) { chdir("/"); system("umount $::CDMOUNTPATH"); } }; my $KID; chdir $path; my $numFiles = `find . -print | wc -l`; my $child = open($KID, "|-"); unless (defined $child) { sendmsg([1,"Media copy operation fork failure"]); return; } if ($child) { push @cpiopid, $child; my @finddata = `find .`; for (@finddata) { print $KID $_; } close($KID); $rc = $?; } else { nice 10; my $c = "nice -n 20 cpio -vdump $installroot/$distname/$arch"; my $k2 = open(PIPE, "$c 2>&1 |") || sendmsg([1,"Media copy operation fork failure"]); push @cpiopid, $k2; my $copied = 0; my ($percent, $fout); while(){ next if /^cpio:/; $percent = $copied / $numFiles; $fout = sprintf "%0.2f%%", $percent * 100; $output_handler->({sinfo => "$fout"}); ++$copied; } exit; } # let everyone read it #chdir "/tmp"; chmod 0755, "$installroot/$distname/$arch"; if ($rc != 0){ sendmsg([1,"Media copy operation failed, status $rc"]); }else{ sendmsg("Media copy operation successful"); my @ret=xCAT::SvrUtils->update_tables_with_templates($distname, $arch); if ($ret[0] != 0) { sendmsg("Error when updating the osimage tables: " . $ret[1]); } } } sub makecustomizedmod { my $osver = shift; my $dest = shift; my $passtab = xCAT::Table->new('passwd'); my $tmp; my $password; if ($passtab) { ($tmp) = $passtab->getAttribs({'key'=>'vmware'},'username','password'); if (defined($tmp)) { $password = $tmp->{password}; } } unless ($password) { return 0; } mkpath("/tmp/xcat"); my $tempdir = tempdir("/tmp/xcat/esxmodcustXXXXXXXX"); my $shadow; mkpath($tempdir."/etc/"); open($shadow,">",$tempdir."/etc/shadow"); $password = crypt($password,'$1$'.xCAT::Utils::genpassword(8)); my $dayssince1970 = int(time()/86400); #Be truthful about /etc/shadow my @otherusers = qw/nobody nfsnobody dcui daemon vimuser/; print $shadow "root:$password:$dayssince1970:0:99999:7:::\n"; foreach (@otherusers) { print $shadow "$_:*:$dayssince1970:0:99999:7:::\n"; } close($shadow); if (-e "$::XCATROOT/share/xcat/netboot/esxi/47.xcat-networking") { mkpath($tempdir."/etc/vmware/init/init.d"); copy( "$::XCATROOT/share/xcat/netboot/esxi/47.xcat-networking",$tempdir."/etc/vmware/init/init.d/47.xcat-networking"); } #TODO: auto-enable ssh and request boot-time customization rather than on-demand? require Cwd; my $dir=Cwd::cwd(); chdir($tempdir); if (-e "$dest/mod.tgz") { unlink("$dest/mod.tgz"); } system("tar czf $dest/mod.tgz *"); chdir($dir); rmtree($tempdir); return 1; } sub mknetboot { my $req = shift; my $doreq = shift; my $tftpdir = "/tftpboot"; my @nodes = @{$req->{node}}; my $ostab = xCAT::Table->new('nodetype'); my $sitetab = xCAT::Table->new('site'); my $bptab = xCAT::Table->new('bootparams',-create=>1); my $installroot = "/install"; if ($sitetab){ (my $ref) = $sitetab->getAttribs({key => 'installdir'}, 'value'); if ($ref and $ref->{value}) { $installroot = $ref->{value}; } ($ref) = $sitetab->getAttribs({key => 'tftpdir'}, 'value'); if ($ref and $ref->{value}) { $tftpdir = $ref->{value}; } } my %donetftp=(); my $bpadds = $bptab->getNodesAttribs(\@nodes,['addkcmdline']); my %tablecolumnsneededforaddkcmdline; my %nodesubdata; foreach my $key (keys %$bpadds){ #First, we identify all needed table.columns needed to aggregate database call my $add = $bpadds->{$key}->[0]->{addkcmdline}; next if ! defined $add; while ($add =~ /#NODEATTRIB:([^:#]+):([^:#]+)#/) { push @{$tablecolumnsneededforaddkcmdline{$1}},$2; $add =~ s/#NODEATTRIB:([^:#]+):([^:#]+)#//; } } foreach my $table (keys %tablecolumnsneededforaddkcmdline) { my $tab = xCAT::Table->new($table,-create=>0); if ($tab) { $nodesubdata{$table}=$tab->getNodesAttribs(\@nodes,$tablecolumnsneededforaddkcmdline{$table}); } } foreach my $node (@nodes){ my $ent = $ostab->getNodeAttribs($node, ['os', 'arch', 'profile']); my $arch = $ent->{'arch'}; my $profile = $ent->{'profile'}; my $osver = $ent->{'os'}; #if($arch ne 'x86'){ # sendmsg([1,"VMware ESX hypervisors are x86, please change the nodetype.arch value to x86 instead of $arch for $node before proceeding: #e.g: nodech $node nodetype.arch=x86\n"]); # return; #} # first make sure copycds was done: my $custprofpath = $profile; unless ($custprofpath =~ /^\//) {#If profile begins with a /, assume it already is a path $custprofpath = $installroot."/custom/install/esxi/$arch/$profile"; } unless( -r "$custprofpath/vmkboot.gz" or -r "$installroot/$osver/$arch/mboot.c32" or -r "$installroot/$osver/$arch/install.tgz" ){ sendmsg([1,"Please run copycds first for $osver or create custom image in $custprofpath/"]); } mkpath("$tftpdir/xcat/netboot/$osver/$arch/"); my @reqmods = qw/vmkboot.gz vmk.gz sys.vgz cim.vgz oem.tgz license.tgz/; #Required modules for an image to be considered complete my %mods; foreach (@reqmods) { $mods{$_} = 1; } my $shortprofname = $profile; $shortprofname =~ s/\/\z//; $shortprofname =~ s/.*\///; unless($donetftp{$osver,$arch}) { my $srcdir = "$installroot/$osver/$arch"; my $dest = "$tftpdir/xcat/netboot/$osver/$arch/$shortprofname"; cpNetbootImages($osver,$srcdir,$dest,$custprofpath,\%mods); if (makecustomizedmod($osver,$dest)) { push @reqmods,"mod.tgz"; $mods{"mod.tgz"}=1; } if (-r "$::XCATROOT/share/xcat/netboot/syslinux/mboot.c32") { #prefer xCAT patched mboot.c32 with BOOTIF for mboot copy("$::XCATROOT/share/xcat/netboot/syslinux/mboot.c32", $dest); } else { copy("$srcdir/mboot.c32", $dest); } $donetftp{$osver,$arch,$profile} = 1; } my $tp = "xcat/netboot/$osver/$arch/$shortprofname"; my $bail=0; foreach (@reqmods) { unless (-r "$tftpdir/$tp/$_") { sendmsg([1,"$_ is missing from the target destination, ensure that either copycds has been run or that $custprofpath contains this file"]); $bail=1; #only flag to bail, present as many messages as possible to user } } if ($bail) { #if the above loop detected one or more failures, bail out return; } # now make file entry stuff my $kernel = "$tp/mboot.c32"; my $prepend = "$tp/vmkboot.gz"; delete $mods{"vmkboot.gz"}; my $append = " --- $tp/vmk.gz"; delete $mods{"vmk.gz"}; $append .= " --- $tp/sys.vgz"; delete $mods{"sys.vgz"}; $append .= " --- $tp/cim.vgz"; delete $mods{"cim.vgz"}; $append .= " --- $tp/oem.tgz"; delete $mods{"oem.tgz"}; $append .= " --- $tp/license.tgz"; delete $mods{"license.tgz"}; if ($mods{"mod.tgz"}) { $append .= " --- $tp/mod.tgz"; delete $mods{"mod.tgz"}; } foreach (keys %mods) { $append .= " --- $tp/$_"; } if (defined $bpadds->{$node}->[0]->{addkcmdline}) { my $modules; my $kcmdline; ($kcmdline,$modules) = split /---/,$bpadds->{$node}->[0]->{addkcmdline},2; $kcmdline =~ s/#NODEATTRIB:([^:#]+):([^:#]+)#/$nodesubdata{$1}->{$node}->[0]->{$2}/eg; if ($modules) { $append .= " --- ".$modules; } $prepend .= " ".$kcmdline; } $append = $prepend.$append; $output_handler->({node=>[{name=>[$node],'_addkcmdlinehandled'=>[1]}]}); $bptab->setNodeAttribs( $node, { kernel => $kernel, initrd => "", kcmdline => $append } ); } # end of node loop } # this is where we extract the netboot images out of the copied ISO image sub cpNetbootImages { my $osver = shift; my $srcDir = shift; my $destDir = shift; my $overridedir = shift; my $modulestoadd = shift; my $tmpDir = "/tmp/xcat.$$"; if($osver =~ /esxi4/){ # we don't want to go through this all the time, so if its already # there we're not going to extract: unless( -r "$destDir/vmk.gz" and -r "$destDir/vmkboot.gz" and -r "$destDir/sys.vgz" and -r "$destDir/license.tgz" and -r "$destDir/oem.tgz" and -r "$destDir/pkgdb.tgz" and -r "$destDir/cim.vgz" and -r "$destDir/cimstg.tgz" and -r "$destDir/boot.cfg" ){ if (-r "$srcDir/image.tgz") { #it still may work without image.tgz if profile customization has everything replaced mkdir($tmpDir); chdir($tmpDir); sendmsg("extracting netboot files from OS image. This may take about a minute or two...hopefully you have ~1GB free in your /tmp dir\n"); my $cmd = "tar zxvf $srcDir/image.tgz"; print "\n$cmd\n"; if(system("tar zxf $srcDir/image.tgz")){ sendmsg([1,"Unable to extract $srcDir/image.tgz\n"]); } # this has the big image and may take a while. # this should now create: # /tmp/xcat.1234/usr/lib/vmware/installer/VMware-VMvisor-big-164009-x86_64.dd.bz2 or some other version. We need to extract partition 5 from it. system("bunzip2 $tmpDir/usr/lib/vmware/installer/*bz2"); sendmsg("finished extracting, now copying files...\n"); # now we need to get partition 5 which has the installation goods in it. my $scmd = "fdisk -lu $tmpDir/usr/lib/vmware/installer/*dd 2>&1 | grep dd5 | awk '{print \$2}'"; print "running: $scmd\n"; my $sector = `$scmd`; chomp($sector); my $offset = $sector * 512; mkdir "/mnt/xcat"; my $mntcmd = "mount $tmpDir/usr/lib/vmware/installer/*dd /mnt/xcat -o loop,offset=$offset"; print "$mntcmd\n"; if(system($mntcmd)){ sendmsg([1,"unable to mount partition 5 of the ESX netboot image to /mnt/xcat"]); return; } if (! -d $destDir) { print "making $destDir\n"; mkpath($destDir); } if(system("cp /mnt/xcat/* $destDir/")){ sendmsg([1,"Could not copy netboot contents to $destDir"]); system("umount /mnt/xcat"); return; } chdir("/tmp"); system("umount /mnt/xcat"); print "tempDir: $tmpDir\n"; system("rm -rf $tmpDir"); } } if (-d $overridedir) { #Copy over all modules use File::Basename; foreach (glob "$overridedir/*") { my $mod = scalar fileparse($_); if ($mod =~ /gz\z/ and $mod !~ /pkgdb.tgz/) { $modulestoadd->{$mod}=1; } copy($_,"$destDir/$mod") or sendmsg([1,"Could not copy netboot contents from $overridedir to $destDir"]); } } }else{ sendmsg([1,"VMware $osver is not supported for netboot"]); } } # compares nfs target described by parameters to every share mounted by target hypervisor # returns 1 if matching datastore is present and 0 otherwise sub match_nfs_datastore { my ($host, $path, $hypconn) = @_; die "esx plugin bug: no host provided for match_datastore" unless defined $host; die "esx plugin bug: no path provided for match_datastore" unless defined $path; my @ip; eval { if ($host =~ m/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\//) { use Socket; @ip = ( $host ); $host = gethostbyaddr(inet_aton($host, AF_INET), AF_INET); } else { use Socket; (undef, undef, undef, undef, @ip) = gethostbyname($host); my @ip_ntoa = (); foreach (@ip) { push (@ip_ntoa, inet_ntoa($_)); } @ip = @ip_ntoa; } }; if ($@) { die "error while resolving datastore host: $@\n"; } my %viewcrit = ( view_type => 'HostSystem', properties => [ 'config.fileSystemVolume' ], ); my $dsviews = $hypconn->find_entity_views(%viewcrit); foreach (@$dsviews) { foreach my $mount (@{$_->get_property('config.fileSystemVolume.mountInfo')}) { next unless $mount->{'volume'}{'type'} eq 'NFS'; my $hostMatch = 0; HOSTMATCH: foreach (@ip, $host) { next HOSTMATCH unless $mount->{'volume'}{'remoteHost'} eq $_; $hostMatch = 1; last HOSTMATCH; } next unless $hostMatch; next unless $mount->{'volume'}{'remotePath'} eq $path; return 1; } } return 0; } 1; # vi: set ts=4 sw=4 filetype=perl: