diff --git a/rpm/aims2.spec b/rpm/aims2.spec index 5a1dec5776284b7ff2b40d4b13f0c39f61d1848a..0dda62d3e7455f8505e24467b103f844e7973dd4 100644 --- a/rpm/aims2.spec +++ b/rpm/aims2.spec @@ -84,6 +84,7 @@ mkdir -p $RPM_BUILD_ROOT/ make install DESTDIR=$RPM_BUILD_ROOT/ cd $RPM_BUILD_ROOT/usr/share/man/man1 ln -s aims2client.1.gz aims2.1.gz +ln -s aims2client.future.1.gz aims2.future.1.gz cd - %clean rm -rf $RPM_BUILD_ROOT @@ -117,6 +118,8 @@ rm -rf $RPM_BUILD_ROOT %defattr(-, root, root) /usr/bin/aims2 /usr/bin/aims2client +/usr/bin/aims2.future +/usr/bin/aims2client.future /usr/share/man/man1/* %changelog diff --git a/src/Makefile b/src/Makefile index 59abb7ab5040ceb0197fb79d6d74cb54e47bd81a..395e8d1a47dcb25f09953c6b81126ac4181fceb3 100644 --- a/src/Makefile +++ b/src/Makefile @@ -6,6 +6,7 @@ default: all all: pod2man aims2client > aims2client.1 + pod2man aims2client.future > aims2client.future.1 clean: rm -f aims2client.1 @@ -22,11 +23,15 @@ install: all mkdir -p $(DESTDIR)/etc/logrotate.d/ mkdir -p $(DESTDIR)/var/lock/subsys/aims ln -sf aims2client $(DESTDIR)/usr/bin/aims2 - ln -sf aims2.1 $(DESTDIR)/usr/share/man/man1/aims2client.1 + ln -sf aims2client.future $(DESTDIR)/usr/bin/aims2.future + ln -sf aims2.1 $(DESTDIR)/usr/share/man/man1/aims2client.1 + ln -sf aims2.future.1 $(DESTDIR)/usr/share/man/man1/aims2client.future.1 install -m 755 aims2client $(DESTDIR)/usr/bin/ + install -m 755 aims2client.future $(DESTDIR)/usr/bin/ install -m 755 aims2config $(DESTDIR)/usr/sbin/ install -m 755 aims2sync $(DESTDIR)/usr/sbin/ - install -m 644 aims2client.1 $(DESTDIR)/usr/share/man/man1/ + install -m 644 aims2client.1 $(DESTDIR)/usr/share/man/man1/ + install -m 644 aims2client.future.1 $(DESTDIR)/usr/share/man/man1/ for CGI in $(CGIS); do \ install -m 755 cgi/$$CGI $(DESTDIR)/var/www/cgi-bin/aims2server/; \ done diff --git a/src/aims2client.future b/src/aims2client.future new file mode 100755 index 0000000000000000000000000000000000000000..063603775349192e7af8486146da240010f71617 --- /dev/null +++ b/src/aims2client.future @@ -0,0 +1,1319 @@ +#!/usr/bin/perl -w + +# +# aims2 client +# +# Authors: Jaroslaw Polok <Jaroslaw.Polok@cern.ch> +# Dan Dengate <Dan.Dengate@CERN.CH> +# +# +# Register and enables devices for installation +# using AIMS. Run `aims2 --help` to start. +# + +use strict; +use IO::Socket; +use Getopt::Long; +use FileHandle; +use MIME::Base64; +use Pod::Usage; +use Digest::MD5 qw(md5_hex); + +# Buffer clearing (think PrepareInstall..) +use English; $OUTPUT_AUTOFLUSH = 1; $|=0; + +# Enable the following for debugging. +#use SOAP::Lite+'trace'; +#use Data::Dumper; + +# FIXME: Remove this +my $VERSION = 2; + +# Request handle, to be re-used. +my $request; + +# Command line options/values. +my ($hostname, $kickstart, $kopts, $pxe, $uefi, $lgcy, $arm64); +my ($name, $arch, $description, $vmlinuz, $initrd, $egroups,$testserver); +my $verbose = my $help = my $all = 0; + +$pxe = $uefi = $lgcy = $arm64 = 0; + +# Internal and transport data structures. +my %code = my %message = (); + +# The current working directory +my $pwd = `pwd`; chomp($pwd); + +# Use DNS to pick our server. Sets for Kerberos and SOAP +my $DEFSERVER = "aims.cern.ch"; +my $TESTSERVER = "aimstest.cern.ch"; +my $SERVER = undef; +my $AIMSVERSION = "aims2_4"; +my $MAX_BLOB_SIZE = 500000000; # hard limit defined on server side. + +# Kerberos principial +my $PRINCIPAL = "aims"; + +# SOAP interface +my $INTERFACE = "aims/server"; + +############################################## +my %opts = ( +############################################## + "hostname=s" => \$hostname, + "kickstart=s" => \$kickstart, + "kopts=s" => \$kopts, + "pxe|enable|bios" => \$pxe, + "uefi" => \$uefi, + "lgcy" => \$lgcy, + "arm64" => \$arm64, + "name=s" => \$name, + "arch=s" => \$arch, + "description=s" => \$description, + "vmlinuz=s" => \$vmlinuz, + "initrd=s" => \$initrd, + "egroups=s" => \$egroups, + "help|?" => \&displayhelp, + "full|all" => \$all, + "server=s" => \$SERVER, + "testserver" => \$testserver, +); +my $options = GetOptions(%opts); + +if ($SERVER) { + $SERVER = dns_guess($SERVER); +} else { + if ($testserver) { + $SERVER = dns_guess($TESTSERVER); + } else { + $SERVER = dns_guess($DEFSERVER); + } +} + +############################################## +BEGIN +############################################## +{ + # Check that the client has the nessessary perl libraries. + # SOAP Lite + eval("use SOAP::Lite"); + if($@){ + print STDERR "Client Error: Missing SOAP::Lite modules. Please install SOAP::Lite (perl-SOAP-Lite).\n"; + exit 1; + } + # Believe it or not, Net::DNS isn't that popular... + eval("use Net::DNS"); + if($@){ + print STDERR "Client Error: Missing Perl DNS module. Please install Net::DNS (perl-Net-DNS).\n"; + exit 1; + } + # Kerberos 5 + eval("use Authen::Krb5"); + if($@) { + print STDERR "Client Error: Missing Perl Kerberos module. Please install Authen::Krb5 (perl-Authen-Krb5).\n"; + exit 1; + } +} + +############################################## +sub ParseHost(@){ +############################################## + # ParseHost routine (stolen from PrepareInstall) + # No modification. Returns array of hostnames. + # FIXME: adding spaces does not work because of how I assign ARGV :( + my @host = (); + for my $arg (@_){ + #print "1: $arg\n"; + $arg =~ s/(\d+),(\d+)/$1 $2/g; # "lxdev03,lxplus0[01,03-13]" -> "lxdev03,lxplus0[01 03-13]" + $arg =~ s/(\d+),(\d+)/$1 $2/g; # a 2nd time is necessary for cases like "lxdev[1,2,3]"... + #print "2: $arg\n"; + my @err = (); + for (split(/,/,$arg)){ # ("lxdev03","lxplus0[01 03-13]") + #print "2.1: $_\n"; + if (/\[(.*)\]/){ + my ($pre,$post) = ($`,$'); # ($pre,$post) = ("lxplus0","") + for (split(/ /,$1)){ # ("01","03-13") + if (/-/){ + if ($` < $'){ + map {push(@host,"$pre$_$post")} ($`..$'); + }else{ + push(@err,"wrong range \"$`-$'\" specified"); + } + }else{ + push(@host,"$pre$_$post"); + } + } + }else{ + push(@host,$_); + } + } + if (@err){ + print STDERR "Error parsing \"$arg\":\n"; + map { print STDERR "$_\n"} @err; + return (); + } + } + return @host; +} + +############################################## +sub ReadKickstart +############################################## +{ + my $ks = shift; + + # Read the kickstart from STDIN. + my $ksclob; + if($ks eq "-"){ + while(<STDIN>){ + $ksclob.=$_ + } + $message{'kickstart'} = $ks = undef; + if($ksclob){ + $message{'ksclob'} = encode_base64($ksclob); + }else{ + print STDERR "Client Error: Kickstart STDIN receieved no data.\n"; + exit 1; + } + } + + # The Kickstart is a path? + if($ks){ + if(-e $ks){ + # Open a handle on KICKSTART and read.. + open(*KS,'<'.$ks) or die "Client Error: Unable to open $ks: $!.\n"; + read(*KS, $ksclob, -s *KS); + close(*KS); + $message{'kickstart'} = $ks = undef; + if($ksclob){ + $message{'ksclob'} = encode_base64($ksclob); + }else{ + # Handle received no data (either interupted or file empty). + print STDERR "Client Error: Kickstart STDIN receieved no data.\n"; + exit 1; + } + }else{ + # Cannot make sense of given path. + print STDERR "warning: Invalid kickstart path $ks given. Ignoring.\n"; + $message{'kickstart'} = $ks = undef; + } + } +} + +############################################## +# COMMANDS +############################################## + +############################################## +$code{'addhost'} = sub() +############################################## +{ + + if($pxe + $uefi + $arm64 > 1) { + usage("Specify --pxe OR --uefi OR --arm64."); + } + + if($lgcy + $uefi + $arm64 > 1) { + usage("Both --lgcy AND --uefi / --arm64 specified, but --lgcy can be used only with --bios / --pxe"); + } + # + # Register a device with aims2. + # + # Check for HOSTNAME. + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + usage("No hostname provided.") + } + + # Did the user provide Kernel append options? + $message{'kopts'} = $kopts ? $kopts : undef; + + $message{'kickstart'} = $kickstart ? $kickstart : undef; + if($message{'kickstart'}){ + ReadKickstart($message{'kickstart'}); + }else{ + warn "warning: no kickstart file provided for $message{'hostname'}.\n"; + } + + $message{'imagename'} = $name ? $pxe : undef; + my @hosts = ParseHost( $message{'hostname'} ); + foreach(@hosts){ + + # Quick dns check. + unless( gethostbyname($_) ){ + warn "warning: host $_ has no DNS records. Registration will most likely fail for this host.\n"; + } + $message{'hostname'} = $_; + SendMessage('AddHost', %message); + if($request->result){ + print STDOUT $request->result."\n"; + } + } + + # Did the user provide the PXEON flag? + + my $btype; + + if($pxe) { + $btype="bios"; + } elsif ($uefi) { + $btype="uefi"; + } elsif ($arm64) { + $btype="arm64"; + } + + if (defined($btype)) { + $message{'kopts'} = $kopts = undef; + unless($name){ + usage("Cannot enable pxe without --name TARGET option."); + } + $code{'pxeon'}->($btype); + } + + if ($lgcy) { + $code{'pxelgcyon'}->(); + } + +}; + +############################################## +$code{'showhost'} = sub() +############################################## +{ + # + # Display host information. Possible -all and -showks options + # + # (comment) INTERFACE, HOSTNAME, STATUS, PXEBOOT, KERNEL OPTIONS + # (comment) USER, WHEN REGISTERED, WHEN ENABLED, WHERE DISABLED, WHERE BOOTED + # + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + usage("No hostname provided."); + } + my @hosts = ParseHost( $message{'hostname'} ); + foreach(@hosts){ + $message{'hostname'} = $_; + SendMessage('GetHostByName',%message); + if($request->result){ + + # Get the result. + my $hosts = $request->result; + unless(ref($hosts) and ref($hosts) eq "HASH"){ + print STDERR "Client Error: Server did not return reference.\n"; + exit 1; + } + + # Display column headings. + my $out; + my $type; + my $lgcy; + my $outputformat = "%-17s,%-17s,%-3s,%-20s,%-5s,%-1s,%-8s,\n"; # 76+ + if (!$all) { + printf $outputformat,"Interface HWADDR","Hostname","PXE","PXE Target","Type","L","Boot Opts"; + } + printf "-------------------------------------------------------------------------------\n"; + + # Parse and print each result as a row. + for my $interface (keys %$hosts){ + # kernel options. + unless( $hosts->{$interface}{KOPTS} ){ + $hosts->{$interface}{KOPTS} = "none"; + } + # pxetarget / localboot / pxe menu. + if( $hosts->{$interface}->{STATUS} == 0 ){ + # Device is registered. + $hosts->{$interface}->{STATUS} = "OFF"; + $hosts->{$interface}->{PXEBOOT} = "none"; + } elsif ( $hosts->{$interface}->{STATUS} == 1 ){ + # Device is enabled. + $hosts->{$interface}->{STATUS} = "ON"; + } elsif ( $hosts->{$interface}->{STATUS} == 2 ) { + # Device is disabled (set to localboot). + $hosts->{$interface}->{STATUS} = "OFF"; + $hosts->{$interface}->{PXEBOOT} = "localboot"; + + } + + $type="BIOS"; + $type="UEFI" if(defined($hosts->{$interface}->{TYPE}) && $hosts->{$interface}->{TYPE} == 1); + $type="ARM64" if(defined($hosts->{$interface}->{TYPE}) && $hosts->{$interface}->{TYPE} == 2); + + $lgcy="N"; + $lgcy="Y" if(defined($hosts->{$interface}->{LGCY}) && $hosts->{$interface}->{LGCY} == 1); + + if (!$all) { + printf $outputformat, $interface, $hosts->{$interface}{HOSTNAME}, + $hosts->{$interface}{STATUS}, + $hosts->{$interface}{PXEBOOT}, + $type, + $lgcy, + "[$hosts->{$interface}{KOPTS}]"; + } else { + printf "Hostname: %s\n",$hosts->{$interface}{HOSTNAME}; + printf "Interface HWADDR: %s\n",$interface; + printf "PXE status: %s\n",$hosts->{$interface}{STATUS}; + printf "PXE boot target: %s\n",$hosts->{$interface}{PXEBOOT}, + printf "PXE boot type: %s\n",$type, + printf "PXE boot options: %s\n",$hosts->{$interface}{KOPTS}; + $out=""; + if (defined($hosts->{$interface}{NOEXPIRY}) && $hosts->{$interface}{NOEXPIRY} == 1) { $out.="Y"} else { $out.="N"}; + printf "PXE noexpiry: %s\n",$out; + $out=""; + if ($hosts->{$interface}{SYNCED1} eq "Y") { $out.="Y"} else { $out.="N"}; + if ($hosts->{$interface}{SYNCED2} eq "Y") { $out.="Y"} else { $out.="N"}; + if ($hosts->{$interface}{SYNCED3} eq "Y") { $out.="Y"} else { $out.="N"}; + printf "PXE boot synced: %s\n",$out; + printf "PXE boot legacy: %s\n",$lgcy, + $out=""; + if ($hosts->{$interface}{SYNCEDLGCY1} eq "Y") { $out.="Y"} else { $out.="N"}; + if ($hosts->{$interface}{SYNCEDLGCY2} eq "Y") { $out.="Y"} else { $out.="N"}; + if ($hosts->{$interface}{SYNCEDLGCY3} eq "Y") { $out.="Y"} else { $out.="N"}; + printf "PXE legacy synced: %s\n",$out; + printf "Registered by: %s\n",$hosts->{$interface}{USERNAME}; + printf "Registered at: %s\n",$hosts->{$interface}{REG} || "????/??/?? ??:??:??"; + printf "Enabled at: %s\n",$hosts->{$interface}{ENABLED} || "????/??/?? ??:??:??"; + printf "Booted at: %s\n",$hosts->{$interface}{BOOTED} || "????/??/?? ??:??:??"; + printf "Disabled at: %s\n",$hosts->{$interface}{DISABLED} || "????/??/?? ??:??:??"; + printf "-------------------------------------------------------------------------------\n"; + } + + } + }else{ + print STDERR "No matching hosts found for $_.\n"; + } + } +}; + +############################################## +$code{'remhost'} = sub() +############################################## +{ + # + # Remove (ie, erase) a host from aims2. Removes kickstart and interfaces. + # + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + usage("No hostname provided"); + } + my @hosts = ParseHost( $message{'hostname'} ); + foreach(@hosts){ + $message{'hostname'} = $_; + SendMessage('RemoveHost',%message); + if($request->result){ + print STDOUT $request->result."\n"; + } + } +}; + +$code{'delhost'} = $code{'remhost'}; + +############################################## +$code{'showks'} = sub() +############################################## +{ + # + # Display the kickstart file for HOSTNAME. + # User requires permission to view hostname. + # No wildcards/Perl regex allowed. + # + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + usage("No hostname provided."); + } + my @hosts = ParseHost( $message{'hostname'} ); + foreach(@hosts){ + $message{'hostname'} = $_; + SendMessage('GetKickstartFile', %message); + if($request->result){ + my $ks = $request->result; + if(@$ks){ + my( $host, $KS, $source, $user, $updated) = @$ks; + if ( $all ) { + print STDOUT "#\n# Kickstart file for: $host last updated: $updated\n# by: $user from: $source\n"; + } + print STDOUT decode_base64($KS),"\n"; + } else { + print STDERR "No kickstart file registered for $_.\n"; + } + } + } +}; + +############################################## +$code{'updateks'} = sub() +############################################## +{ + # + # Update the kickstart file for HOSTNAME. + # + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + usage("No hostname provided"); + } + my @hosts = ParseHost( $message{'hostname'} ); + $message{'kickstart'} = $kickstart ? $kickstart : $ARGV[2]; + if($message{'kickstart'}){ + ReadKickstart( $message{'kickstart'} ); + } + + foreach(@hosts){ + $message{'hostname'} = $_; + SendMessage('UpdateKickstartFile', %message); + if($request->result){ + print STDOUT $request->result."\n"; + } + } + +}; + +$code{'updks'}=$code{'updateks'}; + +############################################## +$code{'remks'} = sub() +############################################## +{ + # + # Remove a kickstart file for HOSTNAME. + # + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + usage("No hostname provided"); + } + my @hosts = ParseHost( $message{'hostname'} ); + foreach(@hosts){ + $message{'hostname'} = $_; + SendMessage('RemoveKickstartFile', %message); + if($request->result){ + print STDOUT $request->result."\n"; + } + } +}; + +$code{'delks'} = $code{'remks'}; + +############################################## +$code{'addimage'} = sub() +############################################## +{ + $message{'name'} = $name ? $name : usage("Missing --name option."); + $message{'arch'} = $arch ? $arch : usage('Missing --arch(i386,x86_64,aarch64) value.'); + $message{'uefi'} = $uefi ? $uefi : undef; + $message{'desc'} = $description ? $description : usage("Missing --description=\"A very short description\" option."); + $message{'vmlinuz'} = $vmlinuz ? $vmlinuz : usage('Missing --vmlinuz option.'); + $message{'egroups'} = $egroups ? $egroups : ""; + $message{'kopts'} = $kopts ? $kopts : ""; + $message{'initrd'} = $initrd ? $initrd : undef; + $message{'vmlinuz_blob'} = undef; + $message{'initrd_blob'} = undef; + + unless( defined($message{'initrd'}) ){ + warn "warning: creating pxeboot with no initrd.img\n"; + } + + if( $arch !~ m/i386|i686|x86_64|aarch64/g){ + usage("Error: Expecting i386, i686, x86_64, aarch64. Got $arch."); + } + + # are the egroups comma or space seperated? Quick check. + if($message{'egroups'}){ + my @groups = split(/,|\s/, $message{'egroups'}); + $message{'egroups'} = join(",",@groups); + } + + if( !-e $message{'vmlinuz'} ){ + usage("Error: $message{'vmlinuz'} file not readable.\n"); + } + + if( defined($message{'initrd'}) && !-e $message{'initrd'} ){ + usage("Error: $message{'initrd'} file not readable.\n"); + } + + die "Error: vmlinuz too big (max size: $MAX_BLOB_SIZE bytes)\n" if ( (-s $message{'vmlinuz'}) > $MAX_BLOB_SIZE); + open(*VMLINUZ,'<'.$message{'vmlinuz'}) or die "Error: Unable to open file - ".$message{'vmlinuz'}.": $!.\n"; + { + use bytes; + read(*VMLINUZ, $message{'vmlinuz_blob'}, -s $message{'vmlinuz'}); + } + close(*VMLINUZ); + if(!$message{'vmlinuz_blob'}){ + print STDERR "Error: Unable to read file - ".$message{'vmlinuz'}.".\n"; + exit 1; + } + + if (defined($message{'initrd'})) { + die "Error: initrd too big (max size: $MAX_BLOB_SIZE bytes)\n" if ( (-s $message{'initrd'}) > $MAX_BLOB_SIZE); + open(*INITRD,'<'.$message{'initrd'}) or die "Error: Unable to open file - ".$message{'initrd'}.": $!.\n"; + { + use bytes; + read(*INITRD, $message{'initrd_blob'}, -s $message{'initrd'}); + } + close(*INITRD); + if(!$message{'initrd_blob'}){ + print STDERR "Error: Unable to read file - ".$message{'initrd'}.".\n"; + exit 1; + } + } + + print STDERR "Uploading to server:\n"; + print STDERR " vmlinuz\n"; + print STDERR " file: ".$message{'vmlinuz'}."\n"; + { + use bytes; + print STDERR " md5: ".md5_hex($message{'vmlinuz_blob'})."\n"; + print STDERR " size: ".length($message{'vmlinuz_blob'})."\n"; + } + if(defined($message{'initrd'})) { + print STDERR " initrd\n"; + print STDERR " file: ".$message{'initrd'}."\n"; + { + use bytes; + print STDERR " md5: ".md5_hex($message{'initrd_blob'})."\n"; + print STDERR " size: ".length($message{'initrd_blob'})."\n"; + } + } + print STDERR "(this will take 20-30 seconds for typical linux vmlinuz/initrd sizes)\n"; + print STDERR "Please wait ..."; + + SendMessage('AddImage', %message); + if($request->result){ + print STDOUT $request->result."\n"; + } +}; + +$code{'addimg'} = $code{'addimage'}; + +############################################## +$code{'remimage'} = sub() +############################################## +{ + # + # Remove pxeboot media from aims2. + # + $message{'imagename'} = $name ? $name : $ARGV[1]; + unless( $message{'imagename'} ){ + usage("No pxeboot name provided."); + } + SendMessage('RemoveImage', %message); + if($request->result){ + print STDOUT $request->result."\n"; + } +}; + +$code{'remimg'} = $code{'remimage'}; +$code{'delimg'} = $code{'remimage'}; +$code{'delimage'} = $code{'remimage'}; + +############################################## +$code{'showimage'} = sub() +############################################## +{ + # + # Display pxeboot information. Possible -all option. + # + # NAME, ARCH, KERNEL OPTIONS, DESCRIPTION + # IMAGE, KERNEL, OWNER, [EGROUPS], WHEN UPLOADED + # + $message{'uefi'} = $uefi ? $uefi : undef; + $message{'arch'} = $arch ? $arch : undef; + + $message{'imagename'} = $name ? $name : $ARGV[1]; + unless( $message{'imagename'} ){ + usage("No image name provided."); + } + if($message{'imagename'} eq "all"){ + $message{'imagename'} = "*"; + } + SendMessage('GetImageByName', %message); + if($request->result){ + my $pxeboot = $request->result; + unless(ref($pxeboot) and ref($pxeboot) eq "HASH"){ + print STDERR "Error: Server did not return reference.\n"; + exit 1; + } + # Display column headings. + my $out; + my $outputformat = "%-30s,%-7s,%-4s,%-30s\n"; # 73+ + if (!$all) { + printf $outputformat,"Image NAME","Arch ", "UEFI", "Description"; + } + printf "-------------------------------------------------------------------------------\n"; + for my $name (sort keys %$pxeboot){ + $pxeboot->{$name}{KOPTS} = "none" unless $pxeboot->{$name}{KOPTS}; + + $out=""; + if (defined($pxeboot->{$name}{UEFI}) && $pxeboot->{$name}{UEFI} == 1) { $out.="Y"} else { $out.="N"}; + + if (!$all) { + printf $outputformat,$name,$pxeboot->{$name}{ARCH},$out,$pxeboot->{$name}{DESCRIPTION}; + } else { + printf "Image Name: %s\n",$name; + printf "Architecture: %s\n",$pxeboot->{$name}{ARCH}; + printf "UEFI: %s\n",$out; + printf "Description: %s\n",$pxeboot->{$name}{DESCRIPTION}; + printf "Boot options: [%s]\n",$pxeboot->{$name}{KOPTS} || "none"; + printf "Admin e-groups: %s\n",$pxeboot->{$name}{GROUPS} || "none"; + printf "Kernel source: %s\n",$pxeboot->{$name}{VMLINUZ_SOURCE} || "?"; + printf "Kernel size: %s bytes\n", $pxeboot->{$name}{VMLINUZ_SIZE} || "?"; + printf "Kernel MD5 sum: %s\n",$pxeboot->{$name}{VMLINUZ_SUM} || "?"; + printf "Initial RAM disk source: %s\n",$pxeboot->{$name}{INITRD_SOURCE} || "none"; + printf "Initial RAM disk size: %s bytes\n",$pxeboot->{$name}{INITRD_SIZE} || "n/a"; + printf "Initial RAM disk MD5 sum: %s\n",$pxeboot->{$name}{INITRD_SUM} || "n/a"; + printf "Image Uploader: %s\n",$pxeboot->{$name}{OWNER}; + printf "Image uploaded at: %s\n",$pxeboot->{$name}{UPLOADED}; + $out=""; + if ($pxeboot->{$name}{SYNCED1} eq 'Y') { $out.="Y"} else { $out.="N"}; + if ($pxeboot->{$name}{SYNCED2} eq 'Y') { $out.="Y"} else { $out.="N"}; + if ($pxeboot->{$name}{SYNCED3} eq 'Y') { $out.="Y"} else { $out.="N"}; + printf "Synchronized: %s\n",$out; + printf "-------------------------------------------------------------------------------\n"; + + + } + + } + }else{ + print STDERR "No matching images found.\n"; + } +}; + +$code{'showimg'} = $code{'showimage'}; + + +############################################## +$code{'pxelgcyon'} = sub() +############################################## +{ + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + die "Error: No hostname provided.\n"; + } + my @hosts = ParseHost( $message{'hostname'} ); + + foreach(@hosts){ + $message{'hostname'} = $_; + SendMessage('EnableLGCY', %message); + if($request->result){ + print STDOUT $request->result."\n"; + } + } +}; + +############################################## +$code{'pxelgcyoff'} = sub() +############################################## +{ + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + die "Error: No hostname provided.\n"; + } + my @hosts = ParseHost( $message{'hostname'} ); + + foreach(@hosts){ + $message{'hostname'} = $_; + SendMessage('DisableLGCY', %message); + if($request->result){ + print STDOUT $request->result."\n"; + } + } +}; + + +############################################## +$code{'pxeon'} = sub() +############################################## +{ + my ($type)=@_; + # + # Enable a device for PXE with a pxeboot target. + # + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + die "Error: No hostname provided.\n"; + } + my @hosts = ParseHost( $message{'hostname'} ); + + $message{'imagename'} = $name ? $name : $ARGV[2]; + unless( $message{'imagename'} ){ + die "Error: No pxeboot target defined.\n"; + } + #optional + $message{'noexpiry'} = $name ? $name : $ARGV[3]; + + if (defined($message{'noexpiry'}) && $message{'noexpiry'} !~/^noexpiry$/) { + $message{'type'} = $message{'noexpiry'}; + if (defined($type)) { + $message{'type'} = $type; + } else { + $message{'type'} = $message{'noexpiry'}; + } + undef($message{'noexpiry'}); + } else { + if (defined($type)) { + $message{'type'} = $type; + } else { + $message{'type'} = $name ? $name : $ARGV[4]; + } + } + + + if($kopts){ + warn "warning: kopts are only passed with `addhost`.\n"; + } + foreach(@hosts){ + $message{'hostname'} = $_; + SendMessage('EnablePXE', %message); + if($request->result){ + print STDOUT $request->result."\n"; + } + } +}; + +############################################## +$code{'pxeoff'} = sub() +############################################## +{ + # + # Disable a device for PXE (ie, set the device to localboot) + # + $message{'hostname'} = $hostname ? $hostname : $ARGV[1]; + unless( $message{'hostname'} ){ + die "No hostname provided.\n"; + } + my @hosts = ParseHost( $message{'hostname'} ); + foreach(@hosts){ + $message{'hostname'} = $_; + SendMessage('DisablePXE', %message); + if($request->result){ + print STDOUT $request->result."\n"; + } + } +}; + +############################################## +$code{'batch'} = sub() +############################################## +{ + my(undef, $file) = @ARGV; + + # Slurp up the file, get lines. + my $batch = new FileHandle; + $batch->open($file) or die "Error: Cannot read $file: $!\n"; + my @lines = (); + while(my $line = $batch->getline()){ + chomp($line); + push(@lines, $line); + } + $batch->close(); + + foreach my $l (@lines) { + next if $l =~ m{^#}; + next if $l =~ /^\s+$/; + next if $l =~ /^(\s)*$/; + + # Rebuild @ARGV for GetOpts. + undef(@ARGV); + no warnings; + #@ARGV = grep {defined($_)|$_ ne ''} split(' |"(.*)"', $l); + @ARGV = split(' |"(.*)"', $l); + my $c = $#ARGV; + for (my $i = 0; $i <= $c; $i++) + { + my $value = shift (@ARGV); + if (length ($value)) + { push (@ARGV, $value); } + } + + # The command we want to execute. + my $command = $ARGV[0]; + next if!$command; + + # Now process @ARGV. + $options = GetOptions(%opts); + if($code{$command}) { + $code{$command}->(); + } else { + print STDERR "Ignoring unknown command $command\n"; + } + } +}; + +############################################## +sub SetUser +############################################## +{ + my $kserver = $SERVER; + my $service = $PRINCIPAL; + my $version = $AIMSVERSION; + + # Create a new Kerberos Auth Context. + # A Kerberos 'instance' should have already been created at init(). + my $ac = new Authen::Krb5::AuthContext; + unless($ac){ + print STDERR "Client Error: Cannot create a new auth context.\n"; + exit 1; + } + # Build a Credential Cache object. + my $cc = Authen::Krb5::cc_default(); + unless($cc){ + print STDERR "Client Error: Cannot read credentials cache.\n"; + exit 1; + } + # Extract some data from the Cache. + my $principal = $cc->get_principal(); + unless($principal){ + print STDERR "Client Error: Failed to find principal. Suggest you run `kinit`.\n"; + exit 1; + } + # Get the identity of prinicpal. + my $username = $principal->data; + unless($username){ + print STDERR "Client Error: Failed to extract user.\n"; + exit 1; + } + # Create a Client Principal object, using the prinipal obtained. + my $clientp = Authen::Krb5::parse_name($username); + unless($clientp){ + print STDERR "Client Error: No client principal.\n"; + exit 1; + } + # Create a Server Principal object. + my $serverp = Authen::Krb5::sname_to_principal($kserver,$service,KRB5_NT_SRV_HST); + unless($serverp){ + print STDERR "Client Error: No server principal.\n"; + exit 1; + } + # "Try" to create a ticket using the objects we've created. + my $request = Authen::Krb5::mk_req($ac,AP_OPTS_MUTUAL_REQUIRED,$service,$kserver,$version,$cc); + unless($request){ + print STDERR "Client Error: KRB5 Request Failed: ".Authen::Krb5::error()."\n"; + exit 1; + } + # We should now have all the data we need to try + # and authenticate the user. Prepare our data to be + # transported to the server. + my $password = $version."=".$request; + $password = encode_base64($password); + $message{'username'} = $username; + $message{'password'} = $password; +} + +############################################## +sub SendMessage +############################################## +{ + my($method) = @_; + + # Communicate with the aims2server. + + # Server method-call. + unless($method){ + print STDERR "Client Error: No method call provided.\n"; + exit 1; + } + # Message to give server. + unless(%message){ + print STDERR "Client Error: No data Missing data.\n"; + exit 1; + } + + SetUser; + + # FIXME: https SOAP + my $SOAPServer = "http://$SERVER/$INTERFACE"; + $request=SOAP::Lite + -> uri('urn:/aims') + -> proxy($SOAPServer) + -> $method(%message); + + # Catch errors/faults. + if($request->fault){ + print STDERR $request->faultstring; + } + + # No fault was raised, continue passing result back to local $code{command}. + return; +}; + +############################################## +sub dns_guess +############################################## +{ + my ($server) = @_; + + my $res = Net::DNS::Resolver->new; + my $query = $res->search($server); + my $host; + if ($query) { + foreach my $rr ($query->answer) { + next unless $rr->type eq "A"; + $host = $rr->address; + last; + } + } else { + print STDERR "Error: Cannot find installation server $server in DNS.\n"; + exit 1; + } + my $iaddr = inet_aton($host); + my(@server) = split(/\./, gethostbyaddr($iaddr, AF_INET)); + my $s = join(".",@server); + return $s; +} + +############################################## +sub displayhelp +############################################## +{ +print <<HELPMSG; + +aims2 commands: + + (add|del|show)host HOSTNAME Add/Remove/Show a host. + (show|update|del)ks HOSTNAME Show/Update/Remove a kickstart file. + (add|del|show)img IMGNAME Add/Remove/Show pxeboot media. + pxeon HOSTNAME IMGNAME + [noexpiry] [pxe|bios|uefi|arm64] + Enables PXE for a registered host. + pxeoff HOSTNAME Sets a registered device to localboot. + pxelgcyon HOSTNAME Sets host for legacy PXE/TFTP boot. + pxelgcyoff HOSTNAME Resets host for standard PXE/HTTP boot. + + To see host details use: + + showhost HOSTNAME --full + + To see image details use: + + showimg IMGNAME --full + + For more information, see `man aims2` + +HELPMSG +exit(0); # :) +} + +############################################## +sub usage +############################################## +{ + my($tip) = shift @_; + if(defined($tip)){ + print STDERR "$tip\n"; + }else{ + print STDERR "Usage: aims2 [command] [options] or aims2 --help.\n" + } + exit 1; +} + +############################################## +sub init +############################################## +{ + + # Create a new Kerberos context. + Authen::Krb5::init_context(); + + # Read the command given, and execute it. + my $command = $ARGV[0]; + if( grep { $command eq $_ } qw(addhost showhost remhost delhost showks updateks updks delks remks addimage addimg remimage remimg delimg delimage showimage showimg pxeon pxeoff batch pxelgcyon pxelgcyoff) ){ + $code{$command}->(); + } else { + print STDERR "Ignoring unrecognised command: $command\n"; + } + + # We've finished. + exit 0; # :) +} + +usage() if(!@ARGV); +init(); + +__END__ + +=pod + + +=head1 NAME + +aims2client - aims2 Client Software + + +=head1 DESCRIPTION + +aims2client is the client-side software for communicating with the Linux Automated Installation Management Service (AIMS2). The client is designed to allow you to register and de-register hosts for PXE installation. You can use the client to register your Kickstart file, Anaconda/Kernel append options and the pxeboot target you wish to use for your installation. + +The aims2 client also allows you to interact with the pxeboot media library displaying information about already uploaded images or uploading your own pxeboot media. + +=head1 COMMAND OVERVIEW + + addhost Register a host. + delhost De-register a host. + showhost Display host information. + + showks Display registered host's Kickstart file. + updateks Update the host's Kickstart file. + delks Remove a hosts Kickstart file. + + pxeon Enable a host for installation. + pxeoff Set a device to localboot. + + pxelgcyon Enable legacy PXE/TFTP boot. + pxelgcyoff Reset to standard PXE/HTTP boot. + + addimg Add pxeboot media. + delimg Remove pxeboot media. + showimg Display information about pxeboot media. + +For more information on command I<OPTIONS> please read the relevant section. + +=head1 ONLINE DOCUMENTATION + +You can view this and more documation online via CERN twiki. Please follow the link below. + +http://twiki.cern.ch/twiki/bin/view/LinuxSupport/Aims2 + + +=head1 SOME EXAMPLES + +To register the device LXPLUS204: + +C<aims2 addhost lxplus204> + +To register the device LXPLUS204 with a Kickstart file: + +C<aims2 addhost lxplus204 --kickstart /afs/cern.ch/project/linux/lxplus204.ks> + +To provide Anaconda/Kernel append options for your device: + +C<aims2 addhost lxplus204 --kopts "ksdevice=bootif ramdisk_size=36000"> + +To display information about LXPLUS204: + +C<aims2 showhost lxplus204> + +To display information about ALL registered lxplus nodes: + +C<aims2 showhost lxplus*> + +To remove LXPLUS204: + +C<aims2 delhost lxplus204> + +To show the Kickstart file for LXPLUS204: + +C<aims2 showks lxplus204> + +To sync the Kickstart file for LXPLUS204: + +C<aims2 updateks lxplus204> + +To update the Kickstart file for LXPLUS204: + +C<aims2 updateks lxplus204 /new/kickstart/path> + +To enable lxplus204 for installation with SLC5 x86_64: + +C<aims2 pxeon lxplus204 SLC5_x86_64> + +To display information about SLC5_x86_64: + +C<aims2 showimg slc5_x86_64> + +To see what else is available for installation: + +C<aims2 showimg all> + + +=head1 AUTHENTICATION + +aims2 uses Kerberos 5 as its authentication mechanism. To use the service you must have valid Kerberos 5 credentials for the CERN environment. You may need to run 'kinit' to get these credentials. + +=head1 AUTHORISATION + +For more information on authorisation and aims2, please refer to the CERN TWiki documentation found at the following link. + +https://twiki.cern.ch/twiki/bin/view/LinuxSupport/Aims2client#Authorisation + + +=head1 COMMANDS + +The following sections outline the client commands and their options. + + +=head2 ADDING A HOST + +B<aims2 addhost> I<HOSTNAME> [B<--kickstart> I<PATH>] [B<--kopts> I<OPTONS>] [B<--pxe>|B<--bios>|B<--efi>|B<--arm64>] [B<--lgcy>] [B<--name> I<IMAGENAME>] + +To register your host for installation you must provide the HOSTNAME (or interface alias) of a device as registered in the CERN Network Database (LANDB). Each interface that is defined in the LANDB for the given HOSTNAME is registered for PXE. Only interfaces that are registered should be able to obtain DHCP leases on the CERN network therefore if your device is not registered please visit https://network.cern.ch/ before trying to use aims2. + +B<--pxe>|B<--bios>|B<--efi>|B<--arm64> option specifies platform/architecture of the client system. Default is B<--pxe>|B<--bios>. + +B<--lgcy> option allows to set client system to use legacy PXE/TFTP boot instead of PXE/HTTP boot. This is a workaround to be used on systems where bootloader hangs due to bad IP stack implementation. (gPXE, early iPXE (< 1.0.0) based firmware in network card). This option only makes sense for B<--pxe>|B<--bios> boot. + +To provide a Kickstart file for your installation you should use the B<--kickstart> I<PATH>, where I<PATH> is either a http:// source, a filesystem location (ie /afs/cern.ch/..), - (STDIN) or a local file. The following examples are valid PATHs: + + ../example.ks + ../../example.ks + ./example.ks + example.ks + + /afs/cern.ch/project/foo/sample.ks + http://www.foo.com/sample.ks + +Some device hardware may require you to pass additional options to Anaconda installer or the Linux Kernel at boot. You can specify these options using the B<--kopts> option. + +Using the B<--kopts> +option can be useful for example when your device has multiple network interfaces but you are not sure which one has a cable connected or which one is being raised by the Kernel first. + +If Anaconda is unable to decide which interface to use for the installation you can provide the option "ksdevice=eth0" which instructs Anaconda to use interface eth0 for its installation. Other options for this command include bootif, link or the MAC address of the interface. Not providing this option to a multiple interface device will cause the Anaconda installation to stop until the user selects the correct interface to use, thus your installation will no longer be as automated as you would like. + +The following links provide more information on the available options. + + http://tinyurl.com/rhel-kopts + http://tinyurl.com/fedora-kopts + +The magic variable IPAPPEND 2 for use with BOOTIF is set by default. + +The PXE state of a device is not affected by an B<addhost> command. You can enable PXE for the device at registration by passing the B<--pxe>|B<--bios>|B<--uefi>|B<--arm64> and B<--name> I<IMAGENAME> options. + +You can only register a device with aims2 if you have appropriate permissions on HOSTNAME. +( you are MAIN USER or RESPONSIBLE as defined in LanDB at http://network.cern.ch or you are member of +CERN e-group registered as such ) + + +=head2 REMOVING A HOST + +B<aims2 delhost> I<HOSTNAME> + +Removing a host will remove all information about the host, its registered interfaces and its Kickstart file. + +When a host is removed from aims2 the default PXE behaviour for the device is to first boot the Linux PXE Installation Menu and then time-out to localboot after 10 seconds. + +Some hardware configurations may present issues such as "getting stuck" on the PXE menu after time-out due to errors with the PXE stack and some NIC firmware. If you experience this problem, the workaround is to set your device to localboot (see B<pxeoff> command) which will bypass the menu all together. + +You can only remove a device from aims2 if you have the appropriate permissions on HOSTNAME. + + +=head2 DISPLAYING HOST INFORMATION + +B<aims2 showhost> I<HOSTNAME> [-all] + +The B<showhost> command will display information about the host and its registered network interfaces. From the left to right, the information shown is. + +INTERFACE, HOSTNAME, STATUS, PXETARGET, KOPTS + +To display extra information about a registered host you can use the B<-all> option. From left to right, the extra information shown is + +INTERFACE, HOSTNAME, STATUS, PXETARGET, KOPTS, USER, REGISTERED AT, ENABLED AT, BOOTED AT, DISABLED AT + +The B<showhost> command is accessible by all users and requires no particular host permissions. + + +=head2 DISPLAYING THE KICKSTART FILE + +B<aims2 showks> I<HOSTNAME> + +The B<showks> command will display the Kickstart file registered with the device. The output will also present who uploaded the Kickstart file, when and where from. + +To display a hosts kickstart file requries permissions on HOSTNAME. + + +=head2 UPDATING THE KICKSTART FILE + +B<aims2 updateks> I<HOSTNAME> [--kickstart I<PATH>] + +If you previously provided a Kickstart PATH (either within /afs/cern.ch/ or a http:// location) you can use the updateks command to "(re)sync" your Kickstart file. + +Providing a new --kickstart I<PATH> will update your Kickstart from the new PATH. + +I<PATH> can be either somewhere readable (by linuxsoft.cern.ch) within /afs/cern.ch/, a http:// location, - (STDIN) or a local file location. + +Kickstart files provided via STDIN or local paths are uploaded directly through the client. To prevent serialisation issues, the Kickstart file is base64 encoded before transportation. + +You can only update a Kickstart file if you have the appropriate permissions on HOSTNAME. + + +=head2 ADDING PXEBOOT MEDIA + +B<aims2 addimg --name> I<NAME> B<--arch> I<ARCH> B<--vmlinuz> I<PATH> B<--description> I<DESCRIPTION> [B<--initrd> I<PATH>] [B<--kopts> I<OPTIONS>][B<--egroup> I<ADMINS>] + +Uploading pxeboot media to aims2 requires upload permissions. To plead your case for these permissions please contact Linux.Support@CERN.CH. + +The option B<--name> I<NAME> is for the name of your pxeboot media. When defining a name for your media, try to use the 'name_release_arch' convention. + +The option B<--arch> I<ARCH> is for the architecture of your image. Expected options are i386, i686, x86_64, aarch64. +The ARCH value is used to built the correct PXELINUX/GRUB2 configuration for a host so ensure the value is defined correctly. + +The option B<--vmlinuz> I<PATH> is for your installation kernel. The PATH given must be readable by linuxsoft..cern.ch from within /afs/cern.ch/. + +The option B<--description> I<DESCRIPTION> is for a short description of your pxeboot media. + +The option B<--initrd> I<PATH> is for your installation image, if required. The PATH given must be readable by linuxsoft.cern.ch from within /afs/cern.ch/. + +The option B<--kopts> I<OPTIONS> is for any options that should be appended to the kernel when it is loaded. An example is the providing of "noipv6" for Scientific Linux 5. It's OK to duplicate options on images and hosts, as aims will weed out the duplicate entries for you. aims2 will not however check that your options are syntactically correct. It is presumed you have read appropriate documentation on your kernel and know what you are doing to some degree. A common example is: + +I<--kopts="ramdisk_size=36000 noipv6 ksdevice=eth0"> + +The option I<--egroup> I<ADMINS> is for the (comma-separated) list of w-group of adminstrators of the image + +=head2 REMOVING PXEBOOT MEDIA + +B<aims2 delimg> I<NAME> + +To remove a pxeboot image, use the B<delimg> command with B<--name> I<NAME> where I<NAME> is the name of your pxeboot image. + +Pxeboot media can only be removed by the owner. admin e-group or AIMS Support. + + +=head2 Enable PXE + +B<aims2 pxeon> I<HOSTNAME> I<NAME> [I<noexpiry>] [B<pxe>|B<bios>|B<uefi>|B<arm64>] + +To enable pxeboot for your host, use the B<pxeon> command with the I<HOSTNAME> of the device and I<NAME> of the pxetarget. If I<noexpiry> is specified the pxeboot will not be cleaned up 24 hours after enabling. Default behaviour is to automatically disable pxeboot 24 hours after registration. Boot type option may be specified as I<pxe>|I<bios> (default, PXE/bios boot), I<uefi> (PXE/UEFI boot) or I<arm64> (PXE/UEFI boot for aarch64 , ARM 64 platform). + +Note: AIMS2 does NOT know what is the correct platform of the system registered, make sure to specify the correct one. + +Note: Not all pxe targets can be booted on all platforms, AIMS2 does not check if the choosen image is appropriate for the platform at present. + + +=head2 Disabling PXE + +B<aims2 pxeoff> I<HOSTNAME> + +To disable PXE boot for a device, use the B<pxeoff> command, providing the I<HOSTNAME> of the device. When you B<pxeoff> a device, you are setting the device to localboot. + +If you would like to set your device to localboot after it has installed you can use the aims2 de-registration method in the your Kickstart file. Add the following line to the beginning of your %post section. + +C</usr/bin/wget -O /root/aims2-deregistration http://aims.cern.ch/aims/reboot> + +=head2 Enable Legacy PXE + +B<aims2 pxelgcyon> I<HOSTNAME> + +To enable Legacy PXE boot (using TFTP), use the B<pxelgcyon> command with the I<HOSTNAME>. + +=head2 Disabling Legacy PXE + +B<aims2 pxelgcyoff> I<HOSTNAME> + +To disable Legacy PXE boot (using TFTP), use the B<pxelgcyoff> command with the I<HOSTNAME>. + + + + + +=head1 AUTHOR + +Dan Dengate <Dan.Dengate@CERN.CH> +Jaroslaw Polok <jaroslaw.polok@cern.ch> + +=head1 KNOWN BUGS + +SOAP::Lite Serializer < v0.70_02 + +https://savannah.cern.ch/bugs/?29667 + +SOAP::Lite::as_anyURI() doesn't serialize correctly strings with 'http://' + +=cut +