diff --git a/aii-core/src/main/perl/Playbook.pm b/aii-core/src/main/perl/Playbook.pm new file mode 100644 index 00000000..4f775303 --- /dev/null +++ b/aii-core/src/main/perl/Playbook.pm @@ -0,0 +1,83 @@ +#${PMpre} AII::Playbook${PMpost} + +use LC::Exception qw (SUCCESS); +use parent qw(CAF::Object); +use CAF::TextRender; +use CAF::Path; + +use AII::Role; + +# hosts: playbook hosts +sub _initialize +{ + my ($self, $hosts, %opts) = @_; + + %opts = () if !%opts; + + $self->{log} = $opts{log} if $opts{log}; + + $self->{data} = { + hosts => $hosts + }; + $self->{roles} = []; + + return SUCCESS; +} + +sub add_role +{ + my ($self, $name) = @_; + my $role = AII::Role->new($name, log => $self); + push @{$self->{roles}}, $role; + return $role; +} + + +# Generate hashref to render into yaml +sub make_data { + my $self = shift; + + # make copy of basic data + my $data = {%{$self->{data}}}; + + # add roles + $data->{roles} = [map {$_->{name}} @{$self->{roles}}]; + + return $data; +} + +# Generate playbook and roles +# root: base working dir +sub write +{ + my ($self, $root) = @_; + + # Make roles subdir in root + my $cafpath = CAF::Path::mkcafpath(log => $self); + $cafpath->directory("$root/roles"); + + # Generate all roles and playbook data + my $files = { + main => $self->make_data() + }; + + foreach my $role (@{$self->{roles}}) { + $files->{"roles/$role->{name}"} = $role->make_data(); + } + + # Write + foreach my $filename (sort keys %$files) { + my $trd = CAF::TextRender->new( + 'yamlmulti', + {'host' => [$files->{$filename}]}, # use yamlmulti to bypass arrayref issue + log => $self, + ); + my $fh = $trd->filewriter( + "$root/$filename.yml", + log => $self, + ); + $fh->close(); + }; +} + +1; diff --git a/aii-core/src/main/perl/Role.pm b/aii-core/src/main/perl/Role.pm new file mode 100644 index 00000000..46d96650 --- /dev/null +++ b/aii-core/src/main/perl/Role.pm @@ -0,0 +1,46 @@ +#${PMpre} AII::Role${PMpost} + +use LC::Exception qw (SUCCESS); +use parent qw(CAF::Object); + +use AII::Task; + +# name: name of role +sub _initialize +{ + my ($self, $name, %opts) = @_; + + %opts = () if !%opts; + + $self->{log} = $opts{log} if $opts{log}; + + $self->{name} = $name; + $self->{data} = { + }; + $self->{tasks} = []; + + return SUCCESS; +} + +sub add_task +{ + my ($self, $name, $data) = @_; + my $task = AII::Task->new($name, $data, log => $self); + push @{$self->{tasks}}, $task; + return $task; +} + +# Generate hashref to render into yaml +sub make_data { + my $self = shift; + + # make copy of basic data + my $data = {%{$self->{data}}}; + + # add tasks + $data->{tasks} = [map {$_->make_data()} @{$self->{tasks}}]; + + return $data; +} + +1; diff --git a/aii-core/src/main/perl/Shellfe.pm b/aii-core/src/main/perl/Shellfe.pm index f535b7f9..6362f01e 100755 --- a/aii-core/src/main/perl/Shellfe.pm +++ b/aii-core/src/main/perl/Shellfe.pm @@ -14,9 +14,14 @@ specified in the profile. Check aii-shellfe for option documentation +=head1 FUNCTIONS + +=over + =cut use CAF::FileWriter; +use CAF::FileReader; use CAF::Lock qw (FORCE_IF_STALE); use EDG::WP4::CCM::CacheManager; use EDG::WP4::CCM::Fetch::ProfileCache qw($ERROR); @@ -31,6 +36,7 @@ use File::Basename qw(basename dirname); use DB_File; use Readonly; use Parallel::ForkManager 0.7.6; +use AII::Playbook; use NCM::Component::metaconfig 18.6.0; @@ -42,9 +48,8 @@ use 5.10.1; use constant MODULEBASE => 'NCM::Component::'; use constant USEMODULE => "use " . MODULEBASE; use constant PROFILEINFO => 'profiles-info.xml'; + use constant NODHCP => 'nodhcp'; -use constant NONBP => 'nonbp'; -use constant NOOSINSTALL=> 'noosinstall'; use constant OSINSTALL => '/system/aii/osinstall'; use constant NBP => '/system/aii/nbp'; use constant CDBURL => 'cdburl'; @@ -56,7 +61,7 @@ use constant LOCKFILE => '/var/lock/quattor/aii'; use constant RETRIES => 6; use constant TIMEOUT => 60; use constant PARTERR_ST => 16; -use constant COMMANDS => qw (remove configure install boot rescue firmware livecd status metaconfig); +use constant COMMANDS => qw (remove configure install boot rescue firmware livecd status metaconfig ansible); use constant INCLUDE => 'include'; use constant CAFILE => 'ca_file'; use constant CADIR => 'ca_dir'; @@ -82,11 +87,23 @@ use constant LIVECDMETHOD => 'Livecd'; Readonly our $PROTECTED_COMMANDS => 'remove|configure|(re)?install'; Readonly our $PROTECTED_OPTION => 'confirm'; +# Keep this list in sync with the options list (to support no) +# this is also the order in which the plugins run (in iter_plugins) +# TODO: no dhcp? (but there's a nodhcp option) +Readonly::Array my @PLUGIN_NAMES => qw(osinstall nbp discovery); + +Readonly my $STATE_FILENAME => 'aii-configured'; + use parent qw (CAF::Application CAF::Reporter); our $ec = LC::Exception::Context->new->will_store_errors; -# List of options for this application. +=item app_options + +List of options for this application (extends L default list). + +=cut + sub app_options { @@ -140,13 +157,12 @@ sub app_options HELP => 'File with the nodes to be booted in rescue mode', DEFAULT => undef }, - { NAME => 'include=s', - HELP => 'Directories to add to include path', + { NAME => INCLUDE.'=s', + HELP => 'Directories to add to include path (: delimited list)', DEFAULT => '' }, { NAME => 'status=s', - HELP => 'Report current boot/install status for the node ' . - '(can be a regexp)', + HELP => 'Report current boot/install status for the node (can be a regexp)', DEFAULT => undef }, { NAME => 'statuslist=s', @@ -177,6 +193,12 @@ sub app_options '(can be a regexp)', DEFAULT => undef }, + { NAME => 'ansible=s', + HELP => 'Node(s) to generate all ansible playbooks for, ' . + 'relative to the cachemanager cachepath for that host' . + '(can be a regexp)', + DEFAULT => undef }, + { NAME => CDBURL.'=s', HELP => 'URL for CDB location', DEFAULT => undef }, @@ -185,18 +207,19 @@ sub app_options HELP => 'File with the nodes and the actions to perform', DEFAULT => undef }, - # aii-* parameters + # aii-* parameters - { NAME => 'nodiscovery|nodhcp', - HELP => 'Do not update discovery (e.g. dhcp) configuration', + # disable plugins + { NAME => 'nodiscovery', + HELP => 'Do not update discovery configuration via discovery plugin', DEFAULT => undef }, { NAME => 'nonbp', - HELP => 'Do not update Network Boot Protocol (e.g. pxe) configuration', + HELP => 'Do not update Network Boot Protocol (e.g. pxe) configuration via nbp plugin', DEFAULT => undef }, { NAME => 'noosinstall', - HELP => 'Do not update OS installer (e.g. kickstart) configuration', + HELP => 'Do not update OS installer (e.g. kickstart) configuration via osinstall plugin', DEFAULT => undef }, { NAME => "$PROTECTED_OPTION=s", @@ -207,7 +230,12 @@ sub app_options 'command invocation.', DEFAULT => undef }, - # other common options + # DHCP option, not a plugin + { NAME => NODHCP, + HELP => 'Do not update DHCP configuration', + DEFAULT => undef }, + + # other common options { NAME => 'cfgfile=s', HELP => 'configuration file for aii-shellfe defaults', @@ -250,17 +278,17 @@ sub app_options HELP => "Add process ID to the log messages (disabled by default, unless parallel is > 1)", DEFAULT => undef }, - # Options for osinstall plug-ins + # Options for osinstall plug-ins { NAME => 'osinstalldir=s', HELP => 'Directory where Kickstart files will be installed', DEFAULT => '/osinstall/ks' }, - # Options for DISCOVERY plug-ins - { NAME => 'dhcpcfg=s', - HELP => 'name of aii configuration file for dhcp', + # Options for DISCOVERY plug-ins + { NAME => 'dhcpcfg=s', + HELP => 'name of aii configuration file for dhcp', DEFAULT => '/etc/aii/aii-dhcp.conf' }, - # Options for NBP plug-ins + # Options for NBP plug-ins { NAME => NBPDIR_PXELINUX.'=s', HELP => 'Directory where files for PXELINUX NBP should be stored', DEFAULT => OSINSTALL_DEF_ROOT_PATH . OSINSTALL_DEF_PXELINUX_DIR }, @@ -287,39 +315,36 @@ sub app_options HELP => 'Generic "boot from rescue image" file', DEFAULT => 'rescue.cfg' }, - # Options for HTTPS - { NAME => CAFILE.'=s', - HELP => 'Certificate file for the CA' }, + # Options for HTTPS + { NAME => CAFILE.'=s', + HELP => 'Certificate file for the CA' }, - { NAME => CADIR.'=s', - HELP => 'Directory where allCA certificates can be found' }, + { NAME => CADIR.'=s', + HELP => 'Directory where allCA certificates can be found' }, - { NAME => KEY.'=s', - HELP => 'Private key for the certificate' }, + { NAME => KEY.'=s', + HELP => 'Private key for the certificate' }, - { NAME => CERT.'=s', - HELP => 'Certificate file to be used' }, + { NAME => CERT.'=s', + HELP => 'Certificate file to be used' }, { NAME => "template-path=s", HELP => 'store for Template Toolkit files', - DEFAULT => '/usr/share/templates/quattor' - }, + DEFAULT => '/usr/share/templates/quattor' }, - # options inherited from CAF - # --help - # --version - # --verbose - # --debug - # --quiet - - ); + ); return(\@array); } -# Initializes the application object. Creates the lock and locks the -# application. +=item _initialize + +Initializes the application object. Creates the lock and locks the +application. + +=cut + sub _initialize { my $self = shift; @@ -371,6 +396,9 @@ sub _initialize $kernel_root = '' if ( $kernel_root eq '/' ); $self->{CONFIG}->set(GRUB2_EFI_KERNEL_ROOT, $kernel_root); } + } else { + $self->{CONFIG}->set(GRUB2_EFI_KERNEL_ROOT, undef) + if $self->option(GRUB2_EFI_KERNEL_ROOT) eq NBPDIR_VARIANT_DISABLED; } # GRUB2_EFI_INITRD_CMD is always derived from GRUB2_EFI_LINUX_CMD as # Grub2 has a set of linux/initrd command pairs that must match together. @@ -388,10 +416,15 @@ sub _initialize } -# Extract CAF::Download::LWP or CCM HTTPS (or other) -# download related options from aii-shellfe config/options -# Returns a hashref with options. -# type can be lwp or ccm +=item _download_options + +Extract C or C HTTPS (or other) +download related options from aii-shellfe config/options +Returns a hashref with options. +C can be C or C. + +=cut + sub _download_options { my ($self, $type) = @_; @@ -420,9 +453,13 @@ sub _download_options return $opts; } +=item lock_node + +Lock a node being configured, needs to be called in every method that contains +node operations (ie configure etc) + +=cut -# Lock a node being configured, needs to be called in every method that contains -# node operations (ie configure etc) sub lock_node { my ($self, $node) = @_; @@ -430,27 +467,39 @@ sub lock_node mkdir($self->option("lockdir")); my $lockfile = $self->option("lockdir") . "/$node"; my $lock = CAF::Lock->new ($lockfile, log => $self); - if ($lock) { - $lock->set_lock (RETRIES, TIMEOUT, FORCE_IF_STALE) or return undef; + if ($lock && $lock->set_lock(RETRIES, TIMEOUT, FORCE_IF_STALE)) { + $self->debug(3, "aii-shellfe: locked node $node (lockfile $lockfile)"); + return $lock; } else { - return undef; + $self->debug(3, "aii-shellfe: failed to lock node $node (lockfile $lockfile)"); + return; } - $self->debug(3, "aii-shellfe: locked node $node"); - return $lock; } -# Overwrite the report method to allow the KS plug-in to print -# debugging output. See CAF::Reporter (8) for more information. +=item report + +Overwrite the report method to allow the KS plug-in to print +debugging output. See L for more information. + +=cut + sub report { my $self = shift; - my $st = join ('', @_); - print STDOUT "$st\n" unless $SUPER::_REP_SETUP->{QUIET}; + my $msg = join ('', @_); + print STDOUT "$msg\n" unless $SUPER::_REP_SETUP->{QUIET}; $self->log (@_); return SUCCESS; } -sub plugin_handler { +=item plugin_handler + +Handler for exceptions during plugin run + +=cut + +sub plugin_handler +{ my ($self, $plugin, $ec, $e) = @_; $self->error("$plugin: $e"); $self->{status} = PARTERR_ST; @@ -458,175 +507,245 @@ sub plugin_handler { return; } -# Runs $method on the plug-in given at $path for $node. Arguments: -# 1: the node state (value of hash returned by fetch_profiles) -# 2: the PAN path of the plug-in to be run. If the path does not -# exist, nothing will be done. -# 3: the method to be run. -# 4: optional modulename: when provided, use module with that name -# (PAN path is ignored when determining module(s) to use). -# when none is provided, all keys of the PAN path will be used as modules. +=item run_plugin + +Runs C on the plug-in given at C for node state C +(value of hash returned by fetch_profiles). + +C is the PAN path of the plug-in to be run. If the path does not +exist, nothing will be done. + +Optional C to select the name of the module. +When provided, use module with that name (PAN path is ignored when determining module(s) to use). +When none is provided, all keys of the PAN path will be used as modules. + +=cut + +# all plugins should return success on success + sub run_plugin { my ($self, $st, $path, $method, $only_modulename) = @_; - return unless $st->{configuration}->elementExists ($path); + my $name = $st->{name}; + my $tree = $st->{configuration}->getTree($path); + if (!$tree) { + $self->verbose("No configuration for plugin path $path for $name. Skipping"); + return; + } # This is here because CacheManager and Fetch objects may have # problems when they get out of scope. - my %rm = $st->{configuration}->getElement ($path)->getHash; - - my @modules = $only_modulename ? ($only_modulename) : sort keys %rm; + my %pmodules; + if ($only_modulename) { + $pmodules{$only_modulename} = $only_modulename; + } else { + %pmodules = map {$_ => $tree->{$_}->{plugin_modulename} || $tree->{$_}->{'ncm-module'} || $_} keys %$tree; + } # Iterate over module names, handling each - foreach my $modulename (@modules) { + # TODO: when dealing with ansible, this order should be resolved via the dependencies + foreach my $pname (sort keys %pmodules) { + my $modulename = $pmodules{$pname}; if ($modulename !~ m/^[a-zA-Z_]\w+(::[a-zA-Z_]\w+)*$/) { - $self->error ("Invalid Perl identifier $modulename specified as a plug-in. Skipping."); + $self->error ("Invalid Perl identifier $modulename specified as a plug-in for $pname. Skipping."); $self->{status} = PARTERR_ST; next; } local $@; - if (!exists $self->{plugins}->{$modulename}) { + + my $plug = $self->{plugins}->{$modulename}; + + if (!defined $plug) { $self->debug (4, "Loading plugin module $modulename"); eval (USEMODULE . $modulename); if ($@) { - $self->error ("Couldn't load plugin module $modulename for path $path: $@"); + $self->error ("Couldn't load plugin module $modulename for $pname for path $path: $@"); $self->{status} = PARTERR_ST; next; } + $self->debug (4, "Instantiating $modulename"); my $class = MODULEBASE.$modulename; # Plugins as derived from NCM::Component, so they need a name argument - my $module = eval { $class->new($modulename) }; + $plug = eval { $class->new($modulename) }; if ($@) { $self->error ("Couldn't call 'new' on plugin module $modulename: $@"); $self->{status} = PARTERR_ST; - next; + return; } - $self->{plugins}->{$modulename} = $module; + + $self->{plugins}->{$modulename} = $plug; } - my $plug = $self->{plugins}->{$modulename}; if ($plug->can($method)) { - $self->debug (4, "Running plugin module $modulename -> $method"); + $self->debug (4, "Running plugin module $modulename -> $method for $name"); $aii_shellfev2::__EC__ = LC::Exception::Context->new; $aii_shellfev2::__EC__->error_handler(sub { $self->plugin_handler($modulename, @_); }); - if (!eval { $plug->$method ($st->{configuration}) }) { - $self->error ("Failed to execute plugin module's $modulename $method method"); - $self->{status} = PARTERR_ST; + if ($method eq 'ansible_command') { + my $ansible = $st->{configuration}->{ansible}; + # make role for component name, and also pass it via configuration hack + my $role = $ansible->{playbook}->add_role($pname); + # placeholder for current/last active role + $ansible->{role} = $role; } + + # Set active config + if ($plug->can('set_active_config')) { + $plug->set_active_config($st->{configuration}); + } + + # The plugin method has to return success + my $res = eval { $plug->$method ($st->{configuration}) }; if ($@) { - $self->error ("Errors running plugin module $modulename $method method: $@"); + $self->error ("Errors running plugin module $modulename $method method for $name: $@"); + $self->{status} = PARTERR_ST; + } elsif (! $res) { + $self->error ("Failed to execute plugin module $modulename $method method for $name"); $self->{status} = PARTERR_ST; } } else { - $self->debug(4, "no method $method available for plugin module $modulename"); + # TODO: should be warn or error? it's configured, but the code doesn't allow it do anything + $self->info("no method $method available for plugin module $modulename for $name"); } } + + return; } -# Call AII::DHCP with the configuration object received as argument. It -# uses the MAC of the first card marked with "boot"=true. -# dhcpmgr is the instance of AII:DHCP that collects the nodes to configure or remove +=item dhcp + +Runs aii-dhcp on the configuration object received as argument. It +uses the MAC of the first card marked with C<<"boot"=true>>. + +=cut + sub dhcp { - my ($self, $node, $st, $cmd, $dhcpmgr) = @_; + my ($self, $st, $cmd, $dhcpmgr) = @_; - return unless $st->{configuration}->elementExists (DHCPPATH); + my $name = $st->{name}; + my $tree = $st->{configuration}->getTree(DHCPPATH); + if (! $tree) { + $self->verbose("No configuration for DHCP path ".DHCPPATH." for $name. Skipping"); + return; + } my $mac; - my $cards = $st->{configuration}->getElement (HWCARDS)->getTree; - foreach my $cfg (sort(values (%$cards))) { - if ($cfg->{boot}) { - $cfg->{hwaddr} =~ m{^((?:[0-9a-f]{2}[-:])+(?:[0-9a-f]{2}))$}i; - $mac = $1; - last; - } + my $cards = $st->{configuration}->getTree(HWCARDS); + foreach my $cfg (sort values (%$cards)) { + if ($cfg->{boot}) { + if ($cfg->{hwaddr} =~ m{^((?:[0-9a-f]{2}[-:])+(?:[0-9a-f]{2}))$}i) { + $mac = $1; + $self->verbose("Using macaddress from (first) boot nic"); + last; + }; + } } my $ec; if ($cmd eq CONFIGURE) { - my $opts = $st->{configuration}->getElement (DHCPOPTION)->getTree; - $self->debug (4, "Going to add dhcp entry of $node to configure"); - $ec = $dhcpmgr->new_configure_entry($node, $mac, $opts->{tftpserver} // '', $opts->{addoptions} // ()); + my $opts = $st->{configuration}->getTree(DHCPOPTION); + $self->debug (4, "Going to add dhcp entry of $name to configure"); + $ec = $dhcpmgr->new_configure_entry($name, $mac, $opts->{tftpserver} // '', $opts->{addoptions} // ()); } elsif ($cmd eq REMOVEMETHOD) { if ($st->{reinstall}) { - $self->debug(3, "No dhcp removal with reinstall set for $node"); + $self->debug(3, "No dhcp removal with reinstall set for $name"); } else { - $self->debug (4, "Going to add dhcp entry of $node to remove"); - $ec = $dhcpmgr->new_remove_entry($node); + $self->debug (4, "Going to add dhcp entry of $name to remove"); + $ec = $dhcpmgr->new_remove_entry($name); } } else { - $self->error("on node $node: dhcp should only run for configure and remove methods, not $cmd"); + $self->error("$name: dhcp should only run for configure and remove methods, not $cmd"); $ec = 1; } if ($ec) { - $self->error("Error running method $cmd on node $node"); + $self->error("Error running method $cmd on $name"); } } +=item iter_plugins + +Given node state and optional hook, iterate over all plugins in PLUGIN_NAMES. + +=cut sub iter_plugins { my ($self, $st, $hook) = @_; - foreach my $plug (qw(osinstall nbp discovery)) { - my $path = "/system/aii/$plug"; - if (!$self->option("no$plug")) { + foreach my $pluginname (@PLUGIN_NAMES) { + my $path = "/system/aii/$pluginname"; + if ($self->option("no$pluginname")) { + $self->verbose("no$pluginname option set, skipping $pluginname plugin"); + } else { $self->run_plugin($st, $path, $hook); } } } -# Returns an array with the list of nodes specified in the file given -# as an argument. Arguments: -# -# $_[1]: file name containing the list of nodes. Each element of the -# list can be a regular expression! -# $_[2]: whether or not the fully qualified domain name should be used -# in the profile name. +=item filenodelist + +Returns an array with the list of nodes specified in the filename given +as an argument. Each element of the list can be a regular expression. + +The second argument is a boolean whether or not the +fully qualified domain name should be used in the profile name. + +=cut + sub filenodelist { - my ($self, $rx, $fqdn) = @_; + my ($self, $filename, $fqdn) = @_; my @nl; - open (FH, "<$rx") or throw_error ("Couldn't open file: $rx"); + my $fh = CAF::FileReader->new($filename, log => $self); - while (my $l = ) { + while (my $l = <$fh>) { next if $l =~ m/^#/; chomp ($l); - $self->debug (3, "Evaluating regexp $l"); + $self->debug(3, "Evaluating regexp $l"); push (@nl, $self->nodelist ($l, $fqdn)); } - close (FH); - $self->debug (1, "Node list: " . join ("\t", @nl)); + $self->debug (1, "Node list from $filename: " . join (", ", @nl)); return @nl; } -# Returns the list of profiles on the CDB server that match a given -# regular expression. -# -# Arguments: -# $_[1]: the regular expression. -# $_[2]: whether or not to use fully qualified domain names in the -# profiles names. +=item nodelist + +Returns the list of profiles that match a given regular expression. + +The second argument is a boolean whether or not the +fully qualified domain name should be used in the profile name. + +The original list of all known profiles is determined once +based on the C option. + +=cut + sub nodelist { - my ($self, $rx, $fqdn) = @_; + my ($self, $pattern, $fqdn) = @_; + + my $orig_pattern = $pattern; # allow the nodename to be specified as either simple nodename, or - # as filename (i.e. .xml). However, to make sure our regexes make + # as filename (i.e. .xml or .json.gz). + # However, to make sure our regexes make # sense, we normalize to forget about the .xml for now. + # TODO: we are actually normalizing a regex pattern, which is insane + my $extension = '\.(?:xml|json)(?:\.gz)?$'; - $rx =~ s{$extension}{}; - my $prefix = $self->option (PREFIX) || ''; + $pattern =~ s{$extension}{}; + my $prefix = $self->option(PREFIX) || ''; if (!$profiles_info) { + # Populate the module variable profiles_info with all known profile names if ($self->option (CDBURL) =~ m{^dir://(.*)$} ) { my $dir = $1; $self->debug (4, "Creating profiles-info from local directory $dir"); @@ -653,24 +772,36 @@ sub nodelist }; } - $rx =~ m{^([^.]*)(.*)}; - $rx = $1; - $rx .= "($2)" if $fqdn; + if ($pattern =~ m{^([^.]*)(.*)}) { + $pattern = $1; + $pattern .= "($2)" if $fqdn; + }; + my @nl; - foreach (@{$profiles_info->{profile}}) { - if ($_->{content} =~ m/$prefix($rx)\.(?:xml|json)\b/) { + foreach my $profile (@{$profiles_info->{profile}}) { + if ($profile->{content} =~ m/$prefix($pattern)$extension\b/) { my $host = $1; $self->debug (4, "Added $host to the list"); push (@nl, $host); } } - $self->error ("No node matches $rx") unless (@nl); + $self->error ("No node matches $pattern (original $orig_pattern)") unless (@nl); return @nl; } -sub cachedir { +=item cachedir + +Generate the name of the node cachedir from the node name and the C option. + +If the C opion is used, and additional subdirectory is used (with domainname as value). + +=cut + +sub cachedir +{ my ($self, $node) = @_; + my $basedir = $self->option("cachedir"); my $cachedir = $basedir; if ($self->option('use_fqdn') and $node =~ m{\.(.*)}) { @@ -684,13 +815,19 @@ sub cachedir { return $cachedir; } -# Returns a hash with the node names given as arguments as keys and -# the pair { fetch, cachemanager } objects associated to their -# profiles as values. +=item fetch_profiles + +Returns a hash with the node names given as arguments as keys and +a hashref with the C and C, C and C instances +associated to their profiles as values. + +(This hashref is sometimes referred to as the node state in this code). + +=cut + sub fetch_profiles { my ($self, @nl) = @_; - my %h; my $cdb = $self->option (CDBURL); my $prefix = $self->option (PREFIX) || ''; @@ -699,9 +836,9 @@ sub fetch_profiles if ($suffix =~ m{^([-\w\.]*)$}) { $suffix = $1; } else { - $self->error ("Invalid suffix for profiles. Leaving"); + $self->error ("Invalid suffix $suffix for profiles"); $self->{status} = PARTERR_ST; - return (); + return; } if ($cdb =~ m{([\w\-\.+]+://[+\w\.\-%?=/:]+)}) { @@ -709,9 +846,9 @@ sub fetch_profiles # All profiles from dir:// can be accessed as file:// $cdb =~ s{^dir://}{file://}; } else { - $self->error ("Invalid base URL. Leaving"); + $self->error ("Invalid base URL $cdb"); $self->{status} = PARTERR_ST; - return (); + return; } # Read the config of the current host @@ -723,8 +860,11 @@ sub fetch_profiles # for each foreign profile EDG::WP4::CCM::CCfg::resetCfg(); + my %node_states; foreach my $node (@nl) { - next if exists $h{$node}; + # ignore duplicate entries + next if exists $node_states{$node}; + my $ccmdir = $self->cachedir($node); my $url = "$cdb/$prefix$node.$suffix"; $self->debug (1, "Fetching profile: $url"); @@ -737,7 +877,7 @@ sub fetch_profiles my $cfg_fh = CAF::FileWriter->new($config, log => $self); my $err = $ec->error(); - if(defined($err)) { + if (defined($err)) { $self->error("failed to create config file $config: ".$err->reason()); next; } else { @@ -757,7 +897,12 @@ sub fetch_profiles }; # we use CDB_File, since it's the fastest - my $fh = EDG::WP4::CCM::Fetch->new ({PROFILE_URL => $url, FOREIGN => 1, CONFIG => $config, DBFORMAT => 'CDB_File'}); + my $fh = EDG::WP4::CCM::Fetch->new ({ + PROFILE_URL => $url, + FOREIGN => 1, + CONFIG => $config, + DBFORMAT => 'CDB_File', # CDB_File is the fastest + }); unless ($fh) { $self->error ("Error creating Fetch object for $url"); $self->{status} = PARTERR_ST; @@ -777,22 +922,22 @@ sub fetch_profiles next; } - my $cm = EDG::WP4::CCM::CacheManager->new ($fh->{CACHE_ROOT}, $config); + my $cm = EDG::WP4::CCM::CacheManager->new($fh->{CACHE_ROOT}, $config); if ($cm) { - my $cfg = $cm->getLockedConfiguration (0); - $h{$node} = { + my $cfg = $cm->getLockedConfiguration(0); + $node_states{$node} = { + name => $node, fetch => $fh, cachemanager => $cm, configuration=> $cfg, }; } else { - $self->error ("Failed to create CacheManager ", - "object for node $node"); + $self->error ("Failed to create CacheManager object for node $node. Skipping node."); $self->{status} = PARTERR_ST; } $self->debug (1, "Inserted structure for $node on fetching structure"); } - return %h; + return %node_states; } # Initiate the Parallel:ForkManager with requested threads if option is given @@ -821,7 +966,8 @@ sub init_pm } # Run the cmd on the list of nodes -sub run_cmd { +sub run_cmd +{ my ($self, $cmd, %node_states) = @_; my $method = "_$cmd"; my %responses; @@ -870,16 +1016,26 @@ foreach my $cmd (COMMANDS) { use strict 'refs'; -# Runs the Install method of the NBP plugins of the nodes given as -# arguments. +# All the _ methods below should return 0 or undef as success. + +=item _install + +Runs the Install method of the NBP plugins of the node. + +=cut + sub _install { my ($self, $node, $st) = @_; $self->run_plugin ($st, NBP, INSTALLMETHOD); } -# Runs the Status method of the NBP plugins of the nodes given as -# arguments. +=item _status + +Runs the Status method of the NBP plugins of the node. + +=cut + sub _status { my ($self, $node, $st) = @_; @@ -888,32 +1044,51 @@ sub _status $self->run_plugin ($st, NBP, STATUSMETHOD); } -# Runs the Boot method of the NBP plugins of the nodes given as -# arguments. +=item _boot + +Runs the Boot method of the NBP plugins of the node. + +=cut + sub _boot { my ($self, $node, $st) = @_; $self->run_plugin ($st, NBP, BOOTMETHOD); } -# Runs the Firmware method of the NBP plugins of the nodes given as -# arguments. +=item _firmware + +Runs the Firmware method of the NBP plugins of the node. + +=cut + sub _firmware { my ($self, $node, $st) = @_; $self->run_plugin ($st, NBP, FIRMWAREMETHOD); } -# Runs the Livecd method of the NBP plugins of the nodes given as -# arguments +=item _livecd + +Runs the Livecd method of the NBP plugins of the node. + +=cut + sub _livecd { my ($self, $node, $st) = @_; $self->run_plugin($st, NBP, LIVECDMETHOD); } -# Runs the Remove method of the NBP plugins of the nodes given as -# arguments. +=item _remove + +Runs the Unconfigure method of all plugins of the node. + +If the node is not being reinstalled, also runs dhcp +and removes the cache dir (unless noaction is set). + +=cut + sub _remove { my ($self, $node, $st) = @_; @@ -927,8 +1102,12 @@ sub _remove }; } -# Runs the Rescue method of the NBP plugins of the nodes given as -# arguments. +=item _rescue + +Runs the Rescue method of the NBP plugins of the node. + +=cut + sub _rescue { my ($self, $node, $st) = @_; @@ -936,47 +1115,108 @@ sub _rescue $self->run_plugin ($st, NBP, RESCUEMETHOD); } -# Configures DISCOVERY, OSINSTALL and NBP on the nodes received as -# arguments. +=item _configure + +Runs the Configure method of all plugins and dhcp of the node. + +=cut + sub _configure { my ($self, $node, $st) = @_; my $when = time(); - $self->iter_plugins($st, CONFIGURE); + my $res = $self->iter_plugins($st, CONFIGURE); $self->set_cache_time($node, $when) unless $self->option('noaction'); + return $res; } +=item _metaconfig + +Runs the aii_command method of the metaconfig component of the node. + +=cut + sub _metaconfig { my ($self, $node, $st) = @_; $self->run_plugin($st, '/software/components/metaconfig', 'aii_command', 'metaconfig'); } -sub get_cache_time { +=item _metaconfig + +Runs the ansible_command method of all components of the node. + +=cut + +sub _ansible +{ + my ($self, $node, $st) = @_; + my $playbook = AII::Playbook->new($node, log => $self); + + # ugly hack: pass it to the component via the configuration instance + $st->{configuration}->{ansible}->{playbook} = $playbook; + + $self->run_plugin($st, '/software/components', 'ansible_command'); + + # for now, write it in the cache dir + $playbook->write($st->{configuration}->{cache_path}. "/ansible"); +} + +=item get_cache_time + +Return the mtime of the C AII statefile. + +=cut + +sub get_cache_time +{ my ($self, $node) = @_; my $cachedir = $self->cachedir($node); - return (stat("$cachedir/aii-configured"))[9] || 0; + return (stat("$cachedir/$STATE_FILENAME"))[9] || 0; } +=item set_cache_time + +Set the mtime of the C AII statefile to C. + +=cut + sub set_cache_time { my ($self, $node, $when) = @_; + my $cachedir = $self->cachedir($node); - if (!open(TOUCH, ">$cachedir/aii-configured")) { - $self->error("aii-shellfe: failed to update state for $node: $!"); - } - close(TOUCH); + my $filename = "$cachedir/$STATE_FILENAME"; + + my $fh = CAF::FileWriter->new($filename, mtime => $when, log => $self); + print $fh ''; + $fh->close(); } -sub remove_cache_node { +=item remove_cache_node + +Remove the C cachedir. +(Does not check the noaction option) + +=cut + +sub remove_cache_node +{ my ($self, $node) = @_; my $cachedir = $self->cachedir($node); rmtree($cachedir); } -# If a host is protected, check the protectid is correct, else don't include to process further +=item check_protected + +Given a hash with node states (as returned by C), +remove all protected hosts whose C value does +not match the confirm option. + +=cut + sub check_protected { my ($self, %hash) = @_; my @to_delete; @@ -1004,19 +1244,28 @@ sub check_protected { return %hash; } +=item change_dhcp + +Make dhcp changes for nodes + +=cut + sub change_dhcp { my ($self, $method, %nodes) = @_; + $self->debug(5, "logfile:", $self->option('logfile'), " dhcpcfg:", $self->option('dhcpcfg')); - my $dhcpmgr = AII::DHCP->new('script', + my $dhcpmgr = AII::DHCP->new( + 'script', "--logfile=".$self->option('logfile'), "--cfgfile=".$self->option('dhcpcfg'), - log => $self); + log => $self, + ); foreach my $node (sort keys %nodes) { my $st = $nodes{$node}; $self->debug(3, "Checking dhcp config on node $node for method $method."); if ($st->{configuration}->elementExists(DHCPPATH)) { - $self->dhcp($node, $st, $method, $dhcpmgr); + $self->dhcp($st, $method, $dhcpmgr); } } $dhcpmgr->configure_dhcp(); @@ -1024,8 +1273,12 @@ sub change_dhcp return 1; } +=item cmds + +Runs all the commands + +=cut -# Runs all the commands sub cmds { my $self = shift; @@ -1121,15 +1374,29 @@ sub cmds } -sub finish { +=item finish + +Run the finish method of all plugins + +=cut + +sub finish +{ my ($self) = @_; $self->debug(5, "closing down"); - foreach my $plugin (keys %{$self->{plugins}}) { - if ($self->{plugins}->{$plugin}->can("finish")) { - $self->debug(5, "invoking finish for $plugin"); - $self->{plugins}->{$plugin}->finish(); + foreach my $pluginname (@PLUGIN_NAMES) { + my $plugin = $self->{plugins}->{$pluginname}; + if ($plugin && $plugin->can('finish')) { + $self->debug(5, "invoking finish for $pluginname"); + $plugin->finish(); } } } +=pod + +=back + +=cut + 1; diff --git a/aii-core/src/main/perl/Task.pm b/aii-core/src/main/perl/Task.pm new file mode 100644 index 00000000..c4700954 --- /dev/null +++ b/aii-core/src/main/perl/Task.pm @@ -0,0 +1,34 @@ +#${PMpre} AII::Task${PMpost} + +use LC::Exception qw (SUCCESS); +use parent qw(CAF::Object); + +# name: name of task +sub _initialize +{ + my ($self, $name, $data, %opts) = @_; + + %opts = () if !%opts; + + $self->{log} = $opts{log} if $opts{log}; + + $self->{name} = $name; + $self->{data} = $data || {}; + + return SUCCESS; +} + +# Generate hashref to render into yaml +sub make_data { + my $self = shift; + + # make copy of basic data + my $data = {%{$self->{data}}}; + + # add tasks + $data->{name} = $self->{name}; + + return $data; +} + +1; diff --git a/aii-core/src/test/perl/NCM/Component/ansible.pm b/aii-core/src/test/perl/NCM/Component/ansible.pm new file mode 100644 index 00000000..bffc94a8 --- /dev/null +++ b/aii-core/src/test/perl/NCM/Component/ansible.pm @@ -0,0 +1,15 @@ +package NCM::Component::ansible; + +sub new { + my $class = shift; + + return bless {}, $class; +} + +sub ansible_command { + my ($self, $configuration) = @_; + $configuration->{ansible}->{role}->add_task("mytask"); + return 1; # must return success +} + +1; diff --git a/aii-core/src/test/perl/NCM/Component/doesexist.pm b/aii-core/src/test/perl/NCM/Component/doesexist.pm new file mode 100644 index 00000000..e5600e16 --- /dev/null +++ b/aii-core/src/test/perl/NCM/Component/doesexist.pm @@ -0,0 +1,11 @@ +package NCM::Component::doesexist; + +sub new { + my $class = shift; + + return bless {}, $class; +} + +sub Test {1}; + +1; diff --git a/aii-core/src/test/perl/parallel.t b/aii-core/src/test/perl/parallel.t index e0364a2e..8868ffff 100644 --- a/aii-core/src/test/perl/parallel.t +++ b/aii-core/src/test/perl/parallel.t @@ -3,7 +3,6 @@ use strict; use warnings; use CAF::Object; -use Test::Deep; use Test::More; use Test::Quattor qw(basic); use Test::MockModule; @@ -18,12 +17,8 @@ $CAF::Object::NoAction = 1; my $caflock = Test::MockModule->new('CAF::Lock'); my $cfg_basic = get_config_for_profile('basic'); my $config_basic = { configuration => $cfg_basic }; -my %h = ( - 'test01.cluster' => $config_basic, - 'test02.cluster' => $config_basic, - 'test03.cluster' => $config_basic, - 'test04.cluster' => $config_basic, -); +my %h = (map {("test$_.cluster", {configuration => $cfg_basic, name => "test$_.cluster"})} qw(01 02 03 04)); + my $defres = {}; foreach my $host (keys %h) { $defres->{$host} = { @@ -34,9 +29,11 @@ foreach my $host (keys %h) { }; $caflock->mock('set_lock', 1 ); -my @opts = qw(script --logfile=target/test/parallel.log --cfgfile=src/test/resources/parallel.cfg); - - +my @opts = qw(script + --logfile target/test/parallel.log + --cfgfile src/test/resources/parallel.cfg + --debug 5 +); my $mod = AII::Shellfe->new(@opts); @@ -49,42 +46,43 @@ foreach my $host (keys %h) { }; my $ok = $mod->remove(%h); -cmp_deeply( $ok, $defres, 'correct result' ) ; +is_deeply( $ok, $defres, 'correct result' ) ; $mod = AII::Shellfe->new(@opts, "--parallel", 2 ); ($pm, %responses) = $mod->init_pm('test'); -ok($pm, 'parallel fork manager initiated'); +ok($pm, 'parallel fork manager initiated p=2'); foreach my $host (keys %h) { $defres->{$host}->{mode} = 1; }; $ok = $mod->remove(%h); -cmp_deeply( $ok, $defres, 'correct result' ) ; +is_deeply($ok, $defres, 'correct remove resul p=2t' ) ; foreach my $host (keys %h) { $defres->{$host}->{method} = '_status'; }; $ok = $mod->status(%h); -cmp_deeply( $ok, $defres, 'correct result' ) ; +is_deeply($ok, $defres, 'correct status result p=2' ) ; $mod = AII::Shellfe->new(@opts, "--parallel", 4 ); ($pm, %responses) = $mod->init_pm('test'); -ok($pm, 'parallel fork manager initiated'); +ok($pm, 'parallel fork manager initiated p=4'); $ok = $mod->status(%h); -cmp_deeply( $ok, $defres, 'correct result' ) ; +is_deeply( $ok, $defres, 'correct result p=4' ) ; foreach my $host (keys %h) { $defres->{$host}->{method} = '_configure'; }; $ok = $mod->configure(%h); -cmp_deeply( $ok, $defres, 'correct result' ) ; +diag " configure p=4 ok ", explain $ok, explain $defres; +is_deeply( $ok, $defres, 'correct configure result p=4' ) ; foreach my $host (keys %h) { $defres->{$host}->{method} = '_install'; }; $ok = $mod->install(%h); -cmp_deeply( $ok, $defres, 'correct result' ) ; +is_deeply( $ok, $defres, 'correct install result p=4' ) ; done_testing(); diff --git a/aii-core/src/test/perl/playbook.t b/aii-core/src/test/perl/playbook.t new file mode 100644 index 00000000..fca74360 --- /dev/null +++ b/aii-core/src/test/perl/playbook.t @@ -0,0 +1,46 @@ +use strict; +use warnings; +use Test::More; +use Test::Quattor; +use Test::Quattor::Object; +use Cwd; + +use AII::Playbook; + +$CAF::Object::NoAction = 1; + +my $obj = Test::Quattor::Object->new(); + +my $pb = AII::Playbook->new("myhost", log => $obj); + +my $root = getcwd . "/target/test/playbook/myhost"; + +$pb->write($root); + +# No roles +my $fh = get_file("$root/main.yml"); +is("$fh", "---\n- hosts: myhost\n roles: []\n"); + +# Add role +my $role = $pb->add_role("first"); +isa_ok ($role, "AII::Role", "Correct class after add_role"); +my $task = $role->add_task("task1"); +isa_ok ($task, "AII::Task", "Correct class after add_task"); + +$task = $role->add_task("task2"); + +$role = $pb->add_role("second"); +$task = $role->add_task("task3", {some => 'thing'}); + +$pb->write($root); + +# No roles +$fh = get_file("$root/main.yml"); +is("$fh", "---\n- hosts: myhost\n roles:\n - first\n - second\n"); +$fh = get_file("$root/roles/first.yml"); +is("$fh", "---\n- tasks:\n - name: task1\n - name: task2\n"); +$fh = get_file("$root/roles/second.yml"); +is("$fh", "---\n- tasks:\n - name: task3\n some: thing\n"); + + +done_testing; diff --git a/aii-core/src/test/perl/shellfe-dhcp.t b/aii-core/src/test/perl/shellfe-dhcp.t index 2c1c23aa..2d27027a 100644 --- a/aii-core/src/test/perl/shellfe-dhcp.t +++ b/aii-core/src/test/perl/shellfe-dhcp.t @@ -41,24 +41,24 @@ my $cfg_3 = { configuration => get_config_for_profile('shellfe-dhcp-3') }; my $cfg_4 = { configuration => get_config_for_profile('shellfe-dhcp-4') }; my $cfg_b = { configuration => get_config_for_profile('shellfe-dhcp-b') }; -my %configure = ( - 'host1.example.com' => $cfg_1, - 'host2.example.com' => $cfg_2, - 'host3.example1.com' => $cfg_3, - 'host4.example.com' => $cfg_4, -); - -my %remove = ( - 'host1.example.com' => $cfg_1, - 'host5.example.com' => $cfg_b, - 'host6.example1.com' => $cfg_b, -); - - - +sub mk_node_state +{ + return map {("host$_.example".(($_ == 3 || $_ == 6) ? 1 : '').".com" => { + name => "host$_.example".(($_ == 3 || $_ == 6) ? 1 : '').".com", + configuration => get_config_for_profile("shellfe-dhcp-".($_ > 4 ? 'b' : $_ )) + } + )} @_; +} +my %configure = mk_node_state(1, 2, 3, 4); +my %remove = mk_node_state(1, 5, 6); -my @opts = qw(script --logfile=target/test/dhcp.log --cfgfile=src/test/resources/shellfe.cfg --dhcpcfg=src/test/resources/dhcp.cfg); +my @opts = qw(script + --logfile target/test/shellfe-dhcp.log + --cfgfile src/test/resources/shellfe.cfg + --dhcpcfg src/test/resources/dhcp.cfg + --debug 5 +); my $dhcpd = <_download_options('ccm'), {}, "empty config returns hashref for # Test metaconfig my $cfg = get_config_for_profile('metaconfig'); -$cli->_metaconfig("somenode", {configuration => $cfg}); +$cli->_metaconfig("somenode", {configuration => $cfg, name => 'somename'}); my $fh = get_file(getcwd . "/target/test/cache/metaconfig/metaconfig/etc/something"); is("$fh", "a=1\n\n", "metaconfig option rendered file in cache dir"); +# Test ansible +$cfg = get_config_for_profile('ansible'); +$cli->_ansible("ansinode", {configuration => $cfg, name => 'ansiname'}); + +$fh = get_file(getcwd . "/target/test/cache/ansible/ansible/main.yml"); +is("$fh", "---\n- hosts: ansinode\n roles:\n - ansible\n - myalias\n", "ansible playbook rendered in cache dir"); +$fh = get_file(getcwd . "/target/test/cache/ansible/ansible/roles/ansible.yml"); +is("$fh", "---\n- tasks:\n - name: mytask\n", "ansible role1 rendered in cache dir"); +$fh = get_file(getcwd . "/target/test/cache/ansible/ansible/roles/myalias.yml"); +is("$fh", "---\n- tasks:\n - name: mytask\n", "ansible role2 rendered in cache dir"); + + +# test modulename +$cfg = get_config_for_profile('modulename_not_exists'); + +$cli->{status} = 0; +$cli->run_plugin({configuration => $cfg}, "/system/aii/osinstall", 'Test'); +is($cli->{status}, 16, "Failure"); +my $text; +{local $/; open(my $fh, '<', $AII_LOG_FILE); $text = <$fh>;} +like($text, qr{ERROR.*?Couldn't load plugin module doesnotexist}, + "Failure due to osinstall module missing"); + +$cli->{status} = 0; +$cfg = get_config_for_profile('modulename_exists'); +$cli->run_plugin({configuration => $cfg}, "/system/aii/osinstall", 'Test'); +is($cli->{status}, 0, "No failure"); done_testing; diff --git a/aii-core/src/test/resources/ansible.pan b/aii-core/src/test/resources/ansible.pan new file mode 100644 index 00000000..f5dd5490 --- /dev/null +++ b/aii-core/src/test/resources/ansible.pan @@ -0,0 +1,8 @@ +object template ansible; + +prefix "/software/components/myalias"; +"ncm-module" = "ansible"; +"data" = 1; + +prefix "/software/components/ansible"; +"data" = 2; diff --git a/aii-core/src/test/resources/modulename_exists.pan b/aii-core/src/test/resources/modulename_exists.pan new file mode 100644 index 00000000..a682aefc --- /dev/null +++ b/aii-core/src/test/resources/modulename_exists.pan @@ -0,0 +1,3 @@ +object template modulename_exists; + +"/system/aii/osinstall/doesnotexist/plugin_modulename" = "doesexist"; diff --git a/aii-core/src/test/resources/modulename_not_exists.pan b/aii-core/src/test/resources/modulename_not_exists.pan new file mode 100644 index 00000000..3047b05f --- /dev/null +++ b/aii-core/src/test/resources/modulename_not_exists.pan @@ -0,0 +1,3 @@ +object template modulename_not_exists; + +"/system/aii/osinstall/doesnotexist" = dict(); diff --git a/aii-ks/src/main/pan/quattor/aii/ks/schema.pan b/aii-ks/src/main/pan/quattor/aii/ks/schema.pan index 5d1599d9..e51f6efc 100644 --- a/aii-ks/src/main/pan/quattor/aii/ks/schema.pan +++ b/aii-ks/src/main/pan/quattor/aii/ks/schema.pan @@ -75,6 +75,7 @@ type structure_ks_mail = { for user customization are under /system/ks/hooks/. } type structure_ks_ks_info = { + "plugin_modulename" ? string "ackurl" ? type_absoluteURI with {deprecated(0, "ackurl is deprecated, use acklist instead"); true; } "acklist" ? type_absoluteURI[] "auth" : string[] = list ("enableshadow", "passalgo=sha512") @@ -90,6 +91,10 @@ type structure_ks_ks_info = { @{deprecated boolean. when defined, precedes value of mail/success.} "email_success" ? boolean with {deprecated(0, "email_success is deprecated; use mail/success instead"); true; } "firewall" ? structure_ks_ksfirewall + @{Kickstart installtype (string in exact kickstart repo command syntax). + If this contains a '@pattern@' substring, the installtype + (including the url and optional proxy option) is generated based on + the (first) enabled SPMA repository with name matching this glob pattern (without the '@').} "installtype" : string "installnumber" ? string "lang" : string = "en_US.UTF-8" @@ -108,6 +113,12 @@ type structure_ks_ks_info = { "pre_install_script" ? type_absoluteURI "post_install_script" ? type_absoluteURI "post_reboot_script" ? type_absoluteURI + @{List of repositories (string in exact kickstart repo command syntax). + If a string contains a '@pattern@' substring, the repository + (including the baseurl and optional proxy, includepkgs and exclude pkgs options) + is generated based on the enabled SPMA repositories + with name(s) matching this glob pattern (without the '@'). + } "repo" ? string[] "timezone" : string @{NTP servers used by Anaconda} @@ -118,8 +129,12 @@ type structure_ks_ks_info = { "ignoredisk" ? string[] @{Base packages needed for a Quattor client to run (CAF, CCM...)} "base_packages" : string[] - @{Repositories to disable while SPMA is not available} + @{Repositories to disable while SPMA is not available (evaluated as glob matching the repository name)} "disabled_repos" : string[] = list() + @{Repositories to enable while SPMA is not available (evaluated as glob matching the repository name)} + "enabled_repos" : string[] = list() + @{Repositories to ignore while SPMA is not available (evaluated as glob matching the repository name)} + "ignored_repos" : string[] = list() "packages_args" : string[] = list("--ignoremissing", "--resolvedeps") "end_script" ? string with { deprecated(0, "end_script is deprecated and will be removed in a future release"); @@ -135,6 +150,8 @@ type structure_ks_ks_info = { @{agree with EULA (EL7+)} 'eula' ? boolean 'packagesinpost' ? boolean + @{install the correct kernel rpms as defined in /software/packages (if any)} + 'kernelinpost' : boolean = true @{configure bonding (when not defined, it will be tried best-effort depending on OS version and configuration)} 'bonding' ? boolean 'lvmforce' ? boolean diff --git a/aii-ks/src/main/perl/ks.pm b/aii-ks/src/main/perl/ks.pm index cf60ee0a..3394e773 100755 --- a/aii-ks/src/main/perl/ks.pm +++ b/aii-ks/src/main/perl/ks.pm @@ -17,7 +17,7 @@ our $EC = LC::Exception::Context->new->will_store_all; our $this_app = $main::this_app; # Modules that may be interesting for hooks. -our @EXPORT_OK = qw (ksuserhooks ksinstall_rpm); +our @EXPORT_OK = qw (ksuserhooks ksinstall_rpm get_repos replace_repo_glob get_fqdn); # PAN paths for some of the information needed to generate the # Kickstart. @@ -50,8 +50,6 @@ use constant { KS => "/system/aii/osinstall/ks", CCM_CONFIG_PATH => "/software/components/ccm", NAMESERVER => "/system/network/nameserver/0", FORWARDPROXY => "forward", - BASE_PKGS => "/system/aii/osinstall/ks/base_packages", - DISABLED_REPOS => "/system/aii/osinstall/ks/disabled_repos", LOCALHOST => hostname(), INIT_SPMA_IGN_DEPS => "/system/aii/osinstall/ks/init_spma_ignore_deps", }; @@ -112,21 +110,36 @@ sub get_anaconda_version return $version; } +sub _ks_filename +{ + my ($self, $ksdir, $fqdn) = @_; + return "$ksdir/$fqdn.ks"; +} -# Opens the kickstart file and sets its handle as the default. -sub ksopen +sub ks_filename { my ($self, $cfg) = @_; my $fqdn = get_fqdn($cfg); my $ksdir = $this_app->option (KSDIROPT); - $self->debug(3,"Kickstart file directory = $ksdir"); + $self->debug(3, "Kickstart file directory = $ksdir"); + + return $self->_ks_filename($ksdir, $fqdn); +} + + +# Opens the kickstart file and sets its handle as the default. +sub ksopen +{ + my ($self, $cfg) = @_; + + my $ks = CAF::FileWriter->open( + $self->ks_filename($cfg), + mode => 0664, + log => $this_app + ); - my $ks = CAF::FileWriter->open ("$ksdir/$fqdn.ks", - mode => 0664, - log => $this_app - ); select ($ks); } @@ -135,9 +148,11 @@ sub ksopen # only the post_reboot script. sub kspostreboot_hereopen { + my ($self, $dest) = @_; + print < /etc/rc.d/init.d/ks-post-reboot +cat < $dest EOF } @@ -390,6 +405,10 @@ sub ksuserhooks throw_error ("Couldn't instantiate object of hook class $hook->{module} ($modulename): $@"); } else { if ($hook_inst->can($method)) { + if ($hook_inst->can('set_active_config')) { + $hook_inst->set_active_config($config); + } + $this_app->debug (5, "Running hook $hook->{module} method $method ($modulename->$method)"); $hook_inst->$method ($config, "$path/$idx"); } else { @@ -411,15 +430,24 @@ sub kscommands my @packages = @{$tree->{packages}}; push(@packages, 'bind-utils'); # required for nslookup usage in ks-post-install + my $repos = get_repos($config); + # error reported in get_repos + return if ! $repos; + + my $proxy = proxy($config); + my $installtype = $tree->{installtype}; - if ($installtype =~ /http/) { - my ($proxyhost, $proxyport, $proxytype) = proxy($config); - if ($proxyhost && $proxytype eq "reverse") { - if ($proxyport) { - $proxyhost .= ":$proxyport"; - } - $installtype =~ s{(https?)://([^/]*)/}{$1://$proxyhost/}; - } + my $proxy_noglob = sub { + my $txt = shift; + return [proxy_url($proxy, $txt)] + }; + my $inst_msg = "installtype $installtype"; + my $inst_globbed = replace_repo_glob($installtype, $repos, $proxy_noglob, 'url', {proxy => 'proxy'}, $inst_msg); + if (defined($inst_globbed)) { + $installtype = $inst_globbed->[0]; + } else { + $this_app->error("$inst_msg glob had no matches"); + return; } my $ntp_servers = ''; @@ -438,18 +466,17 @@ timezone --utc $tree->{timezone}$ntp_servers rootpw --iscrypted $tree->{rootpw} EOF - if ($tree->{repo}) { - foreach my $url (@{$tree->{repo}}) { - if ($url =~ /http/) { - my ($proxyhost, $proxyport, $proxytype) = proxy($config); - if ($proxyhost && $proxytype eq "reverse") { - if ($proxyport) { - $proxyhost .= ":$proxyport"; - } - $url =~ s{(https?)://([^/]*)/}{$1://$proxyhost/}; - } + my %repo_opt_map = map {$_ => $_} (qw(name proxy includepkgs excludepkgs)); + + foreach my $url (@{$tree->{repo} || []}) { + my $globbed = replace_repo_glob($url, $repos, $proxy_noglob, 'baseurl', \%repo_opt_map); + if (defined($globbed)) { + foreach my $newurl (@$globbed) { + print "repo $newurl\n"; } - print "repo $url\n"; + } else { + $this_app->error("repo url $url glob had no matches"); + return; } } @@ -547,9 +574,15 @@ EOF my @packages_in_packages = @packages; if ($tree->{packagesinpost}) { # to be installed later in %post using all repos - # disabled/ignored packages cannot be handled in packagesinpost + # disabled/ignored packages can be handled in packagesinpost (at least in EL7+), + # but better make sure they are not pulled in via some other method, so adding them here as well my $pattern = '^-'; @packages_in_packages = grep {m/$pattern/} @packages; + + # for EL7+, use never matching pattern, so all packages are also carried over + # to the %post section (incl the ones starting with a -) + $pattern = '$^' if $version >= ANACONDA_VERSION_EL_7_0; + push(@$unprocessed_packages, grep {$_ !~ m/$pattern/} @packages); } @@ -560,8 +593,7 @@ EOF print "\n"; print $version >= ANACONDA_VERSION_EL_6_0 ? '%end' : '', "\n"; - return $unprocessed_packages; - + return $unprocessed_packages, $repos; } # Writes the mountpoint definitions and LVM and MD settings @@ -650,9 +682,9 @@ EOF # ends with the %postconfig section ksuserhooks ($config, ANACONDAHOOK); - my $packages = kscommands ($config); + my ($packages, $repos) = kscommands ($config); - return $packages; + return $packages, $repos; } # Create the action to be taken on the log files @@ -979,24 +1011,19 @@ sub ksinstall_rpm my $tree = $config->getElement(KS)->getTree; my $version = get_anaconda_version($tree); - # DISABLED_REPOS doesn't exist in 13.1 - my $disabled = []; - if ( $config->elementExists(DISABLED_REPOS) ) { - $disabled = $config->getElement(DISABLED_REPOS)->getTree(); - } my $packager = $version >= ANACONDA_VERSION_EL_8_0 ? "dnf" : "yum"; - my $cmd = "$packager -c /tmp/aii/yum/yum.conf -y install "; - - $cmd .= " --disablerepo=" . join(",", @$disabled) . " " if @$disabled; - - print $cmd, join("\\\n ", @pkgs), - "|| fail 'Unable to install packages'\n"; + print join("\\\n ", + "$packager -c /tmp/aii/yum/yum.conf -y install", + (map {s/^-//; "-x '$_'"} grep {$_ =~ /^-/} @pkgs), + (grep {$_ !~ /^-/} @pkgs) + ), " || fail 'Unable to install packages'\n"; } sub proxy { my ($config) = @_; - my ($proxyhost, $proxyport, $proxytype); + + my $proxy = {}; my $spma = $config->getTree(SPMA); my $use_proxy = $spma->{proxy} || 0; @@ -1008,25 +1035,44 @@ sub proxy my @proxies = split /,/, $tmp_proxyhost; if (scalar(@proxies) == 1) { # there's only one proxy specified - $proxyhost = $spma->{proxyhost}; + $proxy->{host} = $spma->{proxyhost}; } elsif (scalar(@proxies) > 1) { # optimize by picking the responding server as the proxy my $localhost = LOCALHOST; # need a variable, not a constant my ($me) = grep { /\b$localhost\b/ } @proxies; $me ||= $proxies[0]; - $proxyhost = $me; + $proxy->{host} = $me; } + if ($spma->{proxyport}) { - $proxyport = $spma->{proxyport}; + $proxy->{port} = $spma->{proxyport}; } if ($spma->{proxytype}) { - $proxytype = $spma->{proxytype}; + $proxy->{type} = $spma->{proxytype}; } } - return ($proxyhost, $proxyport, $proxytype); + return $proxy; } +# adapt url with reverse proxy settings +# returns possibly modified url +sub proxy_url +{ + my ($proxy, $url) = @_; + + if ($url =~ /http/) { + if ($proxy->{host} && ($proxy->{type} || '') eq "reverse") { + my $proxyhost = $proxy->{host}; + if ($proxy->{port}) { + $proxyhost .= ":$proxy->{port}"; + } + $url =~ s{(https?)://([^/]*)/}{$1://$proxyhost/}; + } + } + + return $url; +} # Prints the header functions and definitions of the post_reboot # script. @@ -1210,7 +1256,7 @@ EOF sub kspostreboot_tail { - my $config = shift; + my ($config, $as_service) = @_; ksuserscript ($config, POSTREBOOTSCRIPT); @@ -1218,6 +1264,11 @@ sub kspostreboot_tail success +EOF + + if ($as_service) { + print <getTree(KS); + + my %value_map = ( + ignore => -1, + disable => 0, + enable => 1, + ); + + my %filter = map {$_ => ($ks->{$_."d_repos"} || [])} qw(enable disable ignore); + + my @res = (); + + foreach my $action (sort keys %filter) { + push(@res, map {[$_, $value_map{$action}]} @{$filter{$action}}); + } + + # reverse ordered length of glob sort + return [sort {length($b->[0]) <=> length($a->[0])} @res] +}; + +# first match of filter wins +# return values +# undef: no match -> continue +# -1: ignore +# 0: disable +# 1: enable +sub enable_disable_ignore_repo { + my ($name, $filter) = @_; + + foreach my $op (@$filter) { + return $op->[1] if match_glob($op->[0], $name); + } + + return undef; +} + + + +# create repo information with baseurl and proxy settings +# return hashref with key the repo name +sub get_repos +{ + my ($config) = @_; + + my %res; + + unless ( $config->elementExists(REPO) ) { + $this_app->error(REPO." not defined in configuration"); + return + } + + my $repos = $config->getTree(REPO); + + my $filter = make_enable_disable_ignore_repo_filter($config); + + my $proxy = proxy($config); + + foreach my $repo (@$repos) { + my $name = $repo->{name}; + my $edi = enable_disable_ignore_repo($name, $filter); + if (defined($edi)) { + if ($edi == -1) { + $this_app->debug(5, "Ignore YUM repository $name"); + next; + } else { + $this_app->debug(5, 'Force ', ($edi ? 'enable' : 'disable'), " YUM repository $name"); + $repo->{enabled} = $edi; + } + } + + $repo->{protocols}->[0]->{url} = proxy_url($proxy, $repo->{protocols}->[0]->{url}); + + $repo->{baseurl} = $repo->{protocols}->[0]->{url}; + + # mandatory in 16.4 schema + # these values are the default values in the schema + $repo->{enabled} = 1 if(! defined($repo->{enabled})); + $repo->{gpgcheck} = 0 if(! defined($repo->{gpgcheck})); + + if (! $repo->{proxy} && + ($proxy->{type} || '') eq 'forward') { + $repo->{proxy} = "http://$proxy->{host}:$proxy->{port}/"; + } + + $res{$name} = $repo; + + } + + return \%res; +} + + +# Given a string $txt and hashref $repos, replace any occurence of glob @pattern@ with matching +# repository converted in options. There can only be one glob. +# $baseurl_key is the optionname that is prefixed to the glob (--=) +# unless it is not defined +# $opt_map is the optional mapping to the generated text appended at the end: --=$repo{} +# it returns a arrayref with all replaced text (empty list when there is a glob, but no repo was matched) +# noglob is an anonymous sub that is called on the original $txt when there is no glob present +# this sub must return an arrayref +# If only_one_txt is defined, the result is checked if there is exactly one, and the text is used +# to generate a warning +# returns undef when there is no match +sub replace_repo_glob +{ + my ($txt, $repos, $noglob, $baseurl_key, $opt_map, $only_one_txt) = @_; + + my $res; + + if ($txt =~ m/^([^@]*)@([^@]+)@([^@]*)$/) { + $res = []; + + my $begin = $1; + my $glob_pattern = $2; + my $end = $3; + + # find at least one repo with matching name + my @matches = match_glob($glob_pattern, sort keys %$repos); + if (@matches) { + foreach my $reponame (@matches) { + my $repo = $repos->{$reponame}; + next if ! $repo->{enabled}; + + my $txt = (defined($baseurl_key) ? "--$baseurl_key=" : '').$repo->{baseurl}; + + my @opts; + foreach my $key (sort keys %{$opt_map || {}}) { + my $val = $repo->{$opt_map->{$key}}; + push(@opts, "--$key=". (ref($val) eq 'ARRAY' ? join(',', @$val) : $val)) if defined($val); + } + push(@$res, join(' ', "$begin$txt$end", @opts)); + } + + if ($only_one_txt && (scalar @$res > 1)) { + $this_app->warn("$only_one_txt glob had more than one match.", + "Only using first match. All matches: ", join('|', @$res)); + }; + $this_app->debug(5, "replace_repo_glob: pattern $glob_pattern matches ", join(',', @matches), + " (from text $txt) with ", join('|', @$res)); + } else { + $this_app->error("replace_repo_glob: no spma repositories that match $glob_pattern (from text $txt)"); + } + } else { + $res = $noglob->($txt); + } + + return $res; +}; + sub yum_setup { - my ($self, $config) = @_; + my ($self, $config, $repos) = @_; - $self->debug(5,"Configuring YUM repositories..."); + $self->debug(5, "Configuring YUM repositories..."); # SPMA_OBSOLETES doesn't exist in 13.1 , assume false by default my $obsoletes = 0; if ( $config->elementExists(SPMA_OBSOLETES) ) { $obsoletes = $config->getElement (SPMA_OBSOLETES)->getTree(); } - my $repos; - unless ( $config->elementExists(REPO) ) { - $this_app->error(REPO." not defined in configuration"); - return - } - $repos = $config->getElement (REPO)->getTree(); my $extra_yum_opts = {}; if ( $config->elementExists(SPMA_YUMCONF) ) { $extra_yum_opts = $config->getElement (SPMA_YUMCONF)->getTree(); } - print < /tmp/aii/yum/yum.conf [main] EOF @@ -1384,29 +1600,19 @@ end_of_yum_conf cat < /tmp/aii/yum/repos/aii.repo EOF - my ($phost, $pport, $ptype) = proxy($config); + $self->debug(5, "Adding YUM repositories..."); - $self->debug(5," Adding YUM repositories..."); - - foreach my $repo (@$repos) { - if ($ptype && $ptype eq 'reverse') { - $repo->{protocols}->[0]->{url} =~ s{://[^/]*}{://$phost:$pport}; - } - # mandatory in 16.4 schema - # these values are the default values in the schema - $repo->{enabled} = 1 if(! defined($repo->{enabled})); - $repo->{gpgcheck} = 0 if(! defined($repo->{gpgcheck})); + foreach my $name (sort keys %$repos) { + my $repo = $repos->{$name}; print <{name}] -name=$repo->{name} -baseurl=$repo->{protocols}->[0]->{url} +[$name] +name=$name +baseurl=$repo->{baseurl} skip_if_unavailable=1 EOF if ($repo->{proxy}) { print "proxy=$repo->{proxy}\n"; - } elsif ($ptype && $ptype eq 'forward') { - print "proxy=http://$phost:$pport/\n"; } # Handle inconsistent name mapping @@ -1507,12 +1713,18 @@ sub yum_install_packages { my ($self, $config, $packages) = @_; - $self->debug(5,"Adding packages to install with YUM..."); + $self->debug(5, "Adding packages to install with YUM..."); my @pkgs; - my $t = $config->getElement (PKG)->getTree(); + my $pkgtree = $config->getTree(PKG); + my $ks = $config->getTree(KS); + + my %base = map(($_ => 1), @{$ks->{base_packages}}); - my %base = map(($_ => 1), @{$config->getElement (BASE_PKGS)->getTree()}); + + my @install = ("ncm-spma", "ncm-grub"); + push(@install, "kernel") if $ks->{kernelinpost}; + my $pattern = '^('.join('|', @install).')'; print <{$pkg}]); + if ($pkgst =~ m{$pattern} || exists($base{$pkgst})) { + push (@pkgs, [$pkgst, $pkgtree->{$pkg}]); } } @@ -1546,42 +1758,59 @@ EOF # this method. sub post_install_script { - my ($self, $config, $packages) = @_; + my ($self, $config, $packages, $repos, $is_kickstart) = @_; + + $is_kickstart = 1 if ! defined($is_kickstart); + + # Location where to put the ks-post-install script + my $kspi_filename = $is_kickstart ? "/etc/rc.d/init.d/ks-post-reboot" : "/root/ks-post-reboot-script"; + # Run ks-post-install script as service/unit or not + my $kspi_as_service = $is_kickstart; my $tree = $config->getElement (KS)->getTree; my $version = get_anaconda_version($tree); - $self->debug(5,"Adding postinstall script..."); + $self->debug(5, "Adding postinstall script..."); - my $logfile='/tmp/post-log.log'; + my $logfile = '/tmp/post-log.log'; my $logaction = log_action($config, $logfile); - print <kspostreboot_hereopen; - $self->post_reboot_script ($config); + $self->kspostreboot_hereopen($kspi_filename); + $self->post_reboot_script ($config, $kspi_filename); $self->kspostreboot_hereclose; ksuserhooks ($config, POSTHOOK); @@ -1589,7 +1818,7 @@ EOF print "\n# Disable selinux via kernel parameter\ngrubby --update-kernel=DEFAULT --args=selinux=0\n"; }; - $self->yum_setup ($config); + $self->yum_setup ($config, $repos); $self->yum_install_packages ($config, $packages); ksuserscript ($config, POSTSCRIPT); @@ -1600,7 +1829,7 @@ EOF } # restore UEFI pxeboot first - if ($tree->{pxeboot}) { + if ($is_kickstart && $tree->{pxeboot}) { print < /usr/lib/systemd/system/ks-post-reboot.service @@ -1690,7 +1924,7 @@ Wants=network.service [Service] Type=oneshot -ExecStart=/etc/rc.d/init.d/ks-post-reboot start +ExecStart=$kspi_filename start ExecStop=/bin/true ExecStopPost=/usr/bin/rm -fv /system-update FailureAction=reboot @@ -1704,7 +1938,7 @@ TTYVHangup=yes EOF_reboot_unit # /system-update is expected to be a symlink, identifying which update script to run - ln -sf /etc/rc.d/init.d/ks-post-reboot /system-update + ln -sf $kspi_filename /system-update # The documentation recommends creating the .wants symlink directly instead of using [Install] and 'systemctl enable' mkdir -p /etc/systemd/system/system-update.target.wants @@ -1718,10 +1952,19 @@ LogLevel=debug EOF_systemd_logging else - ln -s /etc/rc.d/init.d/ks-post-reboot /etc/rc.d/rc3.d/S86ks-post-reboot + ln -s $kspi_filename /etc/rc.d/rc3.d/S86ks-post-reboot fi EOF + } else { + # Run the script + print <elementExists (ACKLIST) ) { @@ -1743,10 +1986,15 @@ echo 'End of post section' # Drain remote logger (0 if not relevant) sleep \$drainsleep +EOF + + if ($is_kickstart) { + print <ksopen ($config); - my $packages = $self->install ($config); + my ($packages, $repos) = $self->install ($config); $self->pre_install_script ($config); - $self->post_install_script ($config, $packages); + $self->post_install_script ($config, $packages, $repos); $self->ksclose; return 1; } @@ -1788,7 +2036,8 @@ sub Unconfigure return 1; } - my $ksdir = $main::this_app->option (KSDIROPT); - unlink ("$ksdir/$fqdn.ks"); + unlink ($self->ks_filename($config)); return 1; } + +1; diff --git a/aii-ks/src/main/perl/ks_post_script.pm b/aii-ks/src/main/perl/ks_post_script.pm new file mode 100644 index 00000000..1aaae4fd --- /dev/null +++ b/aii-ks/src/main/perl/ks_post_script.pm @@ -0,0 +1,73 @@ +#${PMpre} NCM::Component::ks_post_script${PMpost} + +# Generate a the ks post section of a node as a standalone script. + +use parent qw (NCM::Component::ks); + +use CAF::FileWriter; +use NCM::Component::ks qw(get_fqdn); + +sub _ks_filename +{ + my ($self, $ksdir, $fqdn) = @_; + return "$ksdir/kickstart_post_$fqdn.sh"; +} + +# Cancels the open filewriter instance on the script +# and returns everything (the select magic/hack) to its normal state. +# Returns the content of the cancelled filewriter instance +sub ksclose +{ + my $fh = select; + + my $text = "$fh"; + + select (STDOUT); + + $fh->cancel(); + $fh->close(); + + return "$text"; +} + +sub make_script +{ + my ($self, $cfg, $post_script) = @_; + + my $fh = CAF::FileWriter->open($self->ks_filename($cfg), mode => 0755, log => $self); + print $fh $post_script; + $fh->close(); +} + +# Prints the kickstart file. +sub Configure +{ + my ($self, $config) = @_; + + my $fqdn = get_fqdn($config); + if ($CAF::Object::NoAction) { + $self->info ("Would run " . ref ($self) . " on $fqdn"); + return 1; + } + + $self->ksopen($config); + my ($packages, $repos) = $self->install ($config); + $self->ksclose(); + + $self->ksopen($config); + # ignore the packages that are treated in install for now + # for now assume, all is there already + # this is not kickstart + # won't generate the POSTNOCHROOTHOOK + # no repos passed + $self->post_install_script ($config, [], $repos, 0); + + my $post_script = $self->ksclose(); + + $self->make_script($config, $post_script); + + return 1; +} + + +1; diff --git a/aii-ks/src/test/perl/00-load.t b/aii-ks/src/test/perl/00-load.t index cfabbec9..d4dcfa29 100644 --- a/aii-ks/src/test/perl/00-load.t +++ b/aii-ks/src/test/perl/00-load.t @@ -3,6 +3,7 @@ use strict; use warnings; use Test::Quattor; -use Test::More tests => 1; +use Test::More tests => 2; use_ok("NCM::Component::ks"); +use_ok("NCM::Component::ks_post_script"); diff --git a/aii-ks/src/test/perl/kickstart_commands.t b/aii-ks/src/test/perl/kickstart_commands.t index 0f41a230..a8b04a74 100644 --- a/aii-ks/src/test/perl/kickstart_commands.t +++ b/aii-ks/src/test/perl/kickstart_commands.t @@ -1,7 +1,7 @@ use strict; use warnings; use Test::More; -use Test::Quattor qw(kickstart_commands); +use Test::Quattor qw(kickstart_commands kickstart_commands_glob); use NCM::Component::ks; use CAF::FileWriter; use CAF::Object; @@ -45,4 +45,20 @@ like($fh, qr{^%packages\s--ignoremissing\s--resolvedeps\n^package\n^package2\nbi # close the selected FH and reset STDOUT NCM::Component::ks::ksclose; + +$fh = CAF::FileWriter->new("target/test/ks"); +select($fh); +$cfg = get_config_for_profile('kickstart_commands_glob'); +NCM::Component::ks::kscommands($cfg); + +like($fh, qr{^url\s--url=http://www.example.com/some/extra/whatever\s--noverifyssl}m, 'installtype present (glob)'); + +like($fh, qr{^repo someurl}m, "repo as string (no glob)"); +like($fh, qr{^repo --abc=def --baseurl=http://www.example1.com/weird --other=option --excludepkgs=woo,hoo\* --includepkgs=everything,else --name=repo1}m, + "repo from pattern (glob)"); +unlike($fh, qr{^repo --name=repo0}m, "repo from pattern did not match other repo (glob)"); + +# close the selected FH and reset STDOUT +NCM::Component::ks::ksclose; + done_testing(); diff --git a/aii-ks/src/test/perl/kickstart_packagesinpost.t b/aii-ks/src/test/perl/kickstart_packagesinpost.t index 2af83b79..839fa4b1 100644 --- a/aii-ks/src/test/perl/kickstart_packagesinpost.t +++ b/aii-ks/src/test/perl/kickstart_packagesinpost.t @@ -23,16 +23,20 @@ select($fh); my $ks = NCM::Component::ks->new('ks'); my $cfg = get_config_for_profile('kickstart_packagesinpost'); -my $packref = NCM::Component::ks::kscommands($cfg); +my ($packref, $repos) = NCM::Component::ks::kscommands($cfg); + +diag "kscommands packref ", explain $packref, " repos ", explain $repos, "\n$fh"; like($fh, qr{^%packages --ignoremissing --resolvedeps\n-notthispackage\n%end}m, "Only disabled packages in packages section"); unlike($fh, qr{package2}, "No package2 in commands"); # one of the packages unlike($fh, qr{bind-utils}, "No bind-utils in commands"); # one of the auto-added packages + $ks->yum_install_packages($cfg, $packref); +diag "yum_install_packages\n$fh"; like($fh, qr{\spackage2}, "package2 added"); # one of the packages like($fh, qr{\sbind-utils}, "bind-utils added"); # one of the auto-added packages -unlike($fh, qr{\snotthispackage}, - "disabled/ignored packages are not added to the package install in post"); +like($fh, qr{-x\s'notthispackage'}, + "disabled/ignored packages are added to the package install (again) in post"); # close the selected FH and reset STDOUT NCM::Component::ks::ksclose; diff --git a/aii-ks/src/test/perl/kickstart_post_script.t b/aii-ks/src/test/perl/kickstart_post_script.t new file mode 100644 index 00000000..eec3e6c9 --- /dev/null +++ b/aii-ks/src/test/perl/kickstart_post_script.t @@ -0,0 +1,24 @@ +use strict; +use warnings; +use Test::More; +use Test::Quattor qw(kickstart_post_script); +use NCM::Component::ks_post_script; + +our $this_app = $main::this_app; + +$this_app->{CONFIG}->define('osinstalldir'); +$this_app->{CONFIG}->set('osinstalldir', '/some/path'); + +my $obj = Test::Quattor::Object->new(); + +my $ks = NCM::Component::ks_post_script->new('ks_post_script', $obj); + +my $cfg = get_config_for_profile('kickstart_post_script'); + +$ks->Configure($cfg); + +my $fh = get_file('/some/path/kickstart_post_x.y.sh'); +like("$fh", qr{# %post phase}, "contains the post code"); +unlike("$fh", qr{^%post}m, "does not contain the %post tag"); + +done_testing; diff --git a/aii-ks/src/test/perl/kickstart_proxy.t b/aii-ks/src/test/perl/kickstart_proxy.t index 62d4bfef..7fdf4f99 100644 --- a/aii-ks/src/test/perl/kickstart_proxy.t +++ b/aii-ks/src/test/perl/kickstart_proxy.t @@ -25,14 +25,12 @@ use NCM::Component::ks; my $cfg = get_config_for_profile('kickstart_proxy'); -is_deeply([NCM::Component::ks::proxy($cfg)], - [qw(proxy.server2 1234 forward)], +is_deeply(NCM::Component::ks::proxy($cfg), + {host => 'proxy.server2', port => 1234, type => 'forward'}, "Return expected proxy config"); $cfg = get_config_for_profile('kickstart_noproxy'); -is_deeply([NCM::Component::ks::proxy($cfg)], - [undef,undef,undef], - "undef/disabled proxy config"); +is_deeply(NCM::Component::ks::proxy($cfg), {}, "empty/disabled proxy config"); done_testing(); diff --git a/aii-ks/src/test/perl/kickstart_yum_setup.t b/aii-ks/src/test/perl/kickstart_yum_setup.t index 2dab4254..eb5f2d23 100644 --- a/aii-ks/src/test/perl/kickstart_yum_setup.t +++ b/aii-ks/src/test/perl/kickstart_yum_setup.t @@ -1,7 +1,7 @@ use strict; use warnings; use Test::More; -use Test::Quattor qw(kickstart_yum_setup); +use Test::Quattor qw(kickstart_yum_setup kickstart_yum_edi); use Test::Quattor::RegexpTest; use NCM::Component::ks; use CAF::FileWriter; @@ -18,14 +18,45 @@ Tests for the C method. $CAF::Object::NoAction = 1; +my $ks = NCM::Component::ks->new('ks'); +my $cfg = get_config_for_profile('kickstart_yum_edi'); +my $repos = NCM::Component::ks::get_repos($cfg); + +my $filter = NCM::Component::ks::make_enable_disable_ignore_repo_filter($cfg); +diag "filter", explain $filter; +is_deeply($filter, + [['disable*not', 1], ['disable*', 0], ['repo1', -1]], + "enable/disable/ignore filter"); + +is(NCM::Component::ks::enable_disable_ignore_repo("disable_me_not", $filter), 1, "enable"); +is(NCM::Component::ks::enable_disable_ignore_repo("disable_me_now", $filter), 0, "disable"); +is(NCM::Component::ks::enable_disable_ignore_repo("repo1", $filter), -1, "ignore"); +ok(!defined(NCM::Component::ks::enable_disable_ignore_repo("repo2", $filter)), "continue"); + +$repos = NCM::Component::ks::get_repos($cfg); +diag "filtered repos", explain $repos; +is_deeply({map {$_ => $repos->{$_}->{enabled}} keys %$repos}, + {repo0 => 1, disable_me => 0, disable_me_not => 1}, + "filtered enabled/disabled repos"); + +# no filtering + +$cfg = get_config_for_profile('kickstart_yum_setup'); +$filter = NCM::Component::ks::make_enable_disable_ignore_repo_filter($cfg); +diag "no filter", explain $filter; +is_deeply($filter, [], "No enable/disable/ignore filter"); + +$repos = NCM::Component::ks::get_repos($cfg); +diag "unfiltered repos", explain $repos; +is_deeply({map {$_ => $repos->{$_}->{enabled}} keys %$repos}, + {repo0 => 1, repo1 => 1}, + "unfiltered enabled/disabled repos"); + my $fh = CAF::FileWriter->new("target/test/ks_yum_setup"); # This module simply prints to the default filehandle. select($fh); -my $ks = NCM::Component::ks->new('ks'); -my $cfg = get_config_for_profile('kickstart_yum_setup'); - -NCM::Component::ks::yum_setup($ks, $cfg); +NCM::Component::ks::yum_setup($ks, $cfg, $repos); diag "$fh"; diff --git a/aii-ks/src/test/resources/kickstart.pan b/aii-ks/src/test/resources/kickstart.pan index 3fb6a2dd..07ad1000 100644 --- a/aii-ks/src/test/resources/kickstart.pan +++ b/aii-ks/src/test/resources/kickstart.pan @@ -21,6 +21,27 @@ prefix "/software/packages"; "{ncm-spma}/{14.2.1-1}/arch/noarch" = ""; "{kernel-module-foo}" = nlist(); +prefix "/software/repositories/0"; +"name" = "repo0"; +"owner" = "me@example.com"; +"protocols/0/name" = "http"; +"protocols/0/url" = "http://www.example.com"; +"gpgcheck" = false; +"repo_gpgcheck" = false; +"gpgkey" = list( + "file:///path/to/key", + "https://somewhere/very/very/far", + "ftp://because/ftp/and/security/go/well/together", +); +"gpgcakey" = "file:///super/ca/key"; + +prefix "/software/repositories/1"; +"name" = "repo1"; +"owner" = "me@example.com"; +"protocols/0/name" = "http"; +"protocols/0/url" = "http://www.example1.com"; +"excludepkgs" = list('woo', 'hoo*'); +"includepkgs" = list('everything', 'else'); # pxelinux and kickstart couple if bootproto is not dhcp prefix "/system/aii/nbp/pxelinux"; diff --git a/aii-ks/src/test/resources/kickstart_bonding.pan b/aii-ks/src/test/resources/kickstart_bonding.pan index 7d9d0450..c8d8d576 100644 --- a/aii-ks/src/test/resources/kickstart_bonding.pan +++ b/aii-ks/src/test/resources/kickstart_bonding.pan @@ -1,11 +1,11 @@ -@{ -Profile to test kickstart bonding configuration +@{ +Profile to test kickstart bonding configuration @} object template kickstart_bonding; include 'kickstart'; -prefix "/system/network"; +prefix "/system/network"; "interfaces/bond0/ip" = "1.2.3.0"; "interfaces/bond0/netmask" = "255.255.255.0"; "interfaces/bond0/bonding_opts" = nlist( @@ -23,6 +23,6 @@ prefix "/system/network"; ); prefix "/system/aii/osinstall/ks"; -"bootproto" = "static"; +"bootproto" = "static"; "version" = "13.21"; -"bonding" = true; \ No newline at end of file +"bonding" = true; \ No newline at end of file diff --git a/aii-ks/src/test/resources/kickstart_commands.pan b/aii-ks/src/test/resources/kickstart_commands.pan index cedb69e5..561a6b94 100644 --- a/aii-ks/src/test/resources/kickstart_commands.pan +++ b/aii-ks/src/test/resources/kickstart_commands.pan @@ -1,7 +1,6 @@ -@{ -Profile to ensure that the kickstart commands and packages section are generated +@{ +Profile to ensure that the kickstart commands and packages section are generated @} object template kickstart_commands; include 'kickstart'; - diff --git a/aii-ks/src/test/resources/kickstart_commands_glob.pan b/aii-ks/src/test/resources/kickstart_commands_glob.pan new file mode 100644 index 00000000..f6613c2c --- /dev/null +++ b/aii-ks/src/test/resources/kickstart_commands_glob.pan @@ -0,0 +1,11 @@ +@{ +Profile to ensure that the kickstart commands and packages section are generated +@} +object template kickstart_commands_glob; + +include 'kickstart'; + +prefix "/system/aii/osinstall/ks"; +"installtype" = "url @*epo0@/some/extra/whatever --noverifyssl"; +"repo/0" = "someurl"; +"repo/1" = "--abc=def @*po1*@/weird --other=option"; # should match repo1, not repo0 diff --git a/aii-ks/src/test/resources/kickstart_post_script.pan b/aii-ks/src/test/resources/kickstart_post_script.pan new file mode 100644 index 00000000..2b347a7b --- /dev/null +++ b/aii-ks/src/test/resources/kickstart_post_script.pan @@ -0,0 +1,3 @@ +object template kickstart_post_script; + +include 'kickstart'; diff --git a/aii-ks/src/test/resources/kickstart_yum_edi.pan b/aii-ks/src/test/resources/kickstart_yum_edi.pan new file mode 100644 index 00000000..50fbbc8f --- /dev/null +++ b/aii-ks/src/test/resources/kickstart_yum_edi.pan @@ -0,0 +1,25 @@ +@{ +Profile to test enable/disable/ignore repos +@} +object template kickstart_yum_edi; + +include 'kickstart'; + +prefix "/system/aii/osinstall/ks"; +"enabled_repos" = list("disable*not"); +"disabled_repos" = list("disable*"); +"ignored_repos" = list("repo1"); + +prefix "/software/repositories/2"; +"name" = "disable_me"; +"enabled" = true; +"owner" = "me@example.com"; +"protocols/0/name" = "http"; +"protocols/0/url" = "http://www.example.com"; + +prefix "/software/repositories/3"; +"name" = "disable_me_not"; +"enabled" = false; +"owner" = "me@example.com"; +"protocols/0/name" = "http"; +"protocols/0/url" = "http://www.example.com"; diff --git a/aii-ks/src/test/resources/kickstart_yum_setup.pan b/aii-ks/src/test/resources/kickstart_yum_setup.pan index 51d324f6..4e0dc3db 100644 --- a/aii-ks/src/test/resources/kickstart_yum_setup.pan +++ b/aii-ks/src/test/resources/kickstart_yum_setup.pan @@ -5,28 +5,6 @@ object template kickstart_yum_setup; include 'kickstart'; -prefix "/software/repositories/0"; -"name" = "repo0"; -"owner" = "me@example.com"; -"protocols/0/name" = "http"; -"protocols/0/url" = "http://www.example.com"; -"gpgcheck" = false; -"repo_gpgcheck" = false; -"gpgkey" = list( - "file:///path/to/key", - "https://somewhere/very/very/far", - "ftp://because/ftp/and/security/go/well/together", -); -"gpgcakey" = "file:///super/ca/key"; - -prefix "/software/repositories/1"; -"name" = "repo1"; -"owner" = "me@example.com"; -"protocols/0/name" = "http"; -"protocols/0/url" = "http://www.example.com"; -"excludepkgs" = list('woo', 'hoo*'); -"includepkgs" = list('everything', 'else'); - prefix "/software/components/spma/main_options"; "exclude" = list("a", "b"); "retries" = 40; diff --git a/aii-ks/src/test/resources/regexps/kickstart_yum_setup b/aii-ks/src/test/resources/regexps/kickstart_yum_setup index 3e1c221b..2af39af6 100644 --- a/aii-ks/src/test/resources/regexps/kickstart_yum_setup +++ b/aii-ks/src/test/resources/regexps/kickstart_yum_setup @@ -32,7 +32,7 @@ Test the yum_setup ^\s{4}ftp://because/ftp/and/security/go/well/together$ ^\[repo1\]$ ^name=repo1$ -^baseurl=http://www.example.com$ +^baseurl=http://www.example1.com$ ^skip_if_unavailable=1$ ^exclude=woo hoo\*$ ^enabled=1$ diff --git a/aii-ks/src/test/resources/regexps/pre_noblock_functions b/aii-ks/src/test/resources/regexps/pre_noblock_functions index 4104776d..eb075646 100644 --- a/aii-ks/src/test/resources/regexps/pre_noblock_functions +++ b/aii-ks/src/test/resources/regexps/pre_noblock_functions @@ -4,6 +4,39 @@ Test the functions in the pre section ^echo 'Begin of pre section'$ ^set -x$ ^$ +^wipe_metadata \(\) \{$ +^\s{4}local path clear SIZE ENDSEEK ENDSEEK_OFFSET$ +^\s{4}path="\$1"$ +^$ +^\s{4}# default to 1$ +^\s{4}clearmb="\$\{2:-1\}"$ +^$ +^\s{4}# wipe at least 4 MiB at begin and end$ +^\s{4}ENDSEEK_OFFSET=4$ +^\s{4}if \[ "\$clearmb" -gt \$ENDSEEK_OFFSET \]; then$ +^\s{8}ENDSEEK_OFFSET=\$clearmb$ +^\s{4}fi$ +^\s{4}\# try to get the size with fdisk$ +^\s{4}SIZE=`disksize_MiB "\$path"`$ +^$ +^\s{4}\# if empty, assume we failed and try with parted$ +^\s{4}if \[ \$SIZE -eq 0 \]; then$ +^\s{8}\# the SIZE has not been determined,$ +^\s{8}\# set it equal to ENDSEEK_OFFSET, the entire disk gets wiped.$ +^\s{8}SIZE=\$ENDSEEK_OFFSET$ +^\s{8}echo "\[WARN\] Could not determine the size of device \$path with both fdisk and parted. Wiping whole drive instead"$ +^\s{4}fi$ +^$ +^\s{4}let ENDSEEK=\$SIZE-\$ENDSEEK_OFFSET$ +^\s{4}if \[ \$ENDSEEK -lt 0 \]; then$ +^\s{8}ENDSEEK=0$ +^\s{4}fi$ +^\s{4}echo "\[INFO\] wipe path \$path with SIZE \$SIZE and ENDSEEK \$ENDSEEK"$ +^\s{4}\# dd with 1 MiB blocksize \(unit used by disksize_MiB and faster then e.g. bs=512\)$ +^\s{4}dd if=/dev/zero of="\$path" bs=1048576 count=\$ENDSEEK_OFFSET$ +^\s{4}dd if=/dev/zero of="\$path" bs=1048576 seek=\$ENDSEEK$ +^\s{4}sync$ +^\}$ ^$ ^disksize_MiB \(\) \{$ ^\s{4}local path BYTES MB RET$ @@ -46,4 +79,3 @@ Test the functions in the pre section ^\s{4}echo "\[\$msg\] Found path \$path size \$SIZE min \$min max \$max"$ ^\s{4}return \$RET$ ^\}$ -^$ diff --git a/aii-pxelinux/src/main/pan/quattor/aii/pxelinux/schema.pan b/aii-pxelinux/src/main/pan/quattor/aii/pxelinux/schema.pan index d7f2be10..0f43c56e 100644 --- a/aii-pxelinux/src/main/pan/quattor/aii/pxelinux/schema.pan +++ b/aii-pxelinux/src/main/pan/quattor/aii/pxelinux/schema.pan @@ -11,9 +11,18 @@ include 'pan/types'; PXE configuration } type structure_pxelinux_pxe_info = { - "initrd" : string + @{Kernel path (string in exact syntax). + If this contains a '@pattern@' substring, the kernel path is generated based on + the (first) enabled SPMA repository with name matching this glob pattern (without the '@').} "kernel" : string - "ksdevice" : string with match(SELF, '^(bootif|link)$') || is_hwaddr(SELF) || exists("/system/network/interfaces/" + escape(SELF)) + @{Initrd path (string in exact syntax). + If this contains a '@pattern@' substring, the initrd path is generated based on + the (first) enabled SPMA repository with name matching this glob pattern (without the '@').} + "initrd" : string + @{try to resolve the hostname (when relevant) for EFI kernel and/or initrd; to use the ip instead of the hostname} + "efi_name_lookup" ? boolean + "ksdevice" : string with match(SELF, '^(bootif|link)$') || is_hwaddr(SELF) || + exists("/system/network/interfaces/" + escape(SELF)) "kslocation" : type_absoluteURI "label" : string "append" ? string diff --git a/aii-pxelinux/src/main/perl/pxelinux.pm b/aii-pxelinux/src/main/perl/pxelinux.pm index 7d31b61b..e825dca9 100755 --- a/aii-pxelinux/src/main/perl/pxelinux.pm +++ b/aii-pxelinux/src/main/perl/pxelinux.pm @@ -3,11 +3,12 @@ use Sys::Hostname; use CAF::FileWriter; use CAF::Object qw(SUCCESS CHANGED); -use NCM::Component::ks qw (ksuserhooks); +use NCM::Component::ks qw (ksuserhooks get_repos replace_repo_glob); use File::stat; use File::Basename qw(dirname); use Time::localtime; use Readonly; +use Socket; use parent qw (NCM::Component CAF::Path); @@ -310,7 +311,7 @@ sub _pxe_network_bonding { # create a list with all kernel parameters for the kickstart installation sub _kernel_params_ks { - my ($self, $cfg, $variant) = @_; + my ($self, $cfg, $variant, $initrd_path) = @_; my $pxe_config = $cfg->getElement (PXEROOT)->getTree; @@ -340,9 +341,7 @@ sub _kernel_params_ks # with previous AII versions for easier comparisons. my @kernel_params = ("ramdisk=32768"); if ($variant == PXE_VARIANT_PXELINUX) { - my $initrd = $pxe_config->{initrd}; - $initrd =~ s{\bLOCALHOST\b}{LOCALHOST}e; - push @kernel_params, "initrd=$initrd"; + push @kernel_params, "initrd=$initrd_path"; } push (@kernel_params, "${keyprefix}ks=$ksloc"); @@ -421,10 +420,10 @@ sub _kernel_params_ks # create a list with all required kernel parameters, based on the configuration sub _kernel_params { - my ($self, $cfg, $variant) = @_; + my ($self, $cfg, $variant, $initrd_path) = @_; if ($cfg->elementExists(KS)) { - return $self->_kernel_params_ks($cfg, $variant); + return $self->_kernel_params_ks($cfg, $variant, $initrd_path); } else { # Non-linux hosts may use pxelinux to chain-load their own bootloader $self->debug (1, "No Kickstart-related parameters in configuration: no kernel parameters added."); @@ -432,6 +431,57 @@ sub _kernel_params } } +sub _kernel_initrd_path +{ + my ($self, $cfg, $prefix) = @_; + + my $repos = get_repos($cfg); + + my $localhost_noglob = sub { + my $txt = shift; + $txt =~ s{\bLOCALHOST\b}{LOCALHOST}e; + return [$txt]; + }; + + my $pxe_config = $cfg->getTree(PXEROOT); + + my @res; + foreach my $key (qw(kernel initrd)) { + my $val = $pxe_config->{$key}; + # assuming the prefix contains no part of the globs + my $globbed = replace_repo_glob($val, $repos, $localhost_noglob, undef, undef, "$key $val"); + if (!defined($globbed)) { + $this_app->error("$key $val glob had no matches"); + return; + } + + my $res = $globbed->[0]; + + if (defined($prefix)) { + # aka EFI + if ($res =~ m{^(http(?:s)?)://([^/]+)/(.*)$}) { + # typically from the glob, as this is not a valid kernel/initrd path + $res = "($1,$2)/$3"; + } else { + # Avoid having an uncoditional "/" at the beginning, because that would + # break if the kernel/initrd location uses the "(http,XXXX)/..." or + # "(tftp,XXXX)/..." syntax + $res = ($res =~ m/^\((http|tftp),/ ? "" : $prefix) . $res; + }; + }; + + if ($pxe_config->{efi_name_lookup} && + $res =~ m{^\((tftp|http(?:s)?),([^/:]+)(:\d+)?\)/(.*)$}) { + my $address = inet_ntoa(inet_aton($2)); + $res = "($1,$address$3)/$4"; + }; + + push(@res, $res); + } + + return @res; +}; + # Write the PXELINUX configuration file. sub _write_pxelinux_config { @@ -440,13 +490,13 @@ sub _write_pxelinux_config my $fh = CAF::FileWriter->open ($self->_file_path ($cfg, PXE_VARIANT_PXELINUX), log => $self, mode => 0644); + # pass undef, this is not EFI + my ($kernel_path, $initrd_path) = $self->_kernel_initrd_path($cfg, undef); + my $appendtxt = ''; - my @appendoptions = $self->_kernel_params($cfg, PXE_VARIANT_PXELINUX); + my @appendoptions = $self->_kernel_params($cfg, PXE_VARIANT_PXELINUX, $initrd_path); $appendtxt = join(" ", "append", @appendoptions) if @appendoptions; - my $kernel = $pxe_config->{kernel}; - $kernel =~ s{\bLOCALHOST\b}{LOCALHOST}e; - my $entry_label = "Install $pxe_config->{label}"; print $fh <option(GRUB2_EFI_KERNEL_ROOT); - $kernel_root = '' unless defined($kernel_root); - my $kernel_path = "$kernel_root/$pxe_config->{kernel}"; - my $initrd_path = "$kernel_root/$pxe_config->{initrd}"; - $kernel_path =~ s{\bLOCALHOST\b}{LOCALHOST}e; - $initrd_path =~ s{\bLOCALHOST\b}{LOCALHOST}e; + my ($kernel_path, $initrd_path) = $self->_kernel_initrd_path($cfg, defined($kernel_root) ? "$kernel_root/" : ""); - my @kernel_params = $self->_kernel_params($cfg, PXE_VARIANT_PXELINUX); + my @kernel_params = $self->_kernel_params($cfg, PXE_VARIANT_GRUB2); @kernel_params = () unless @kernel_params; my $kernel_params_text = join(' ', @kernel_params); $kernel_params_text = ' ' . $kernel_params_text if $kernel_params_text; diff --git a/aii-pxelinux/src/test/perl/NCM/Component/ks.pm b/aii-pxelinux/src/test/perl/NCM/Component/ks.pm index d94decdc..959ca608 100644 --- a/aii-pxelinux/src/test/perl/NCM/Component/ks.pm +++ b/aii-pxelinux/src/test/perl/NCM/Component/ks.pm @@ -14,7 +14,7 @@ use strict; use warnings; use parent qw(Exporter); -our @EXPORT_OK = qw(ksuserhooks get_ks_userhook_args); +our @EXPORT_OK = qw(ksuserhooks get_ks_userhook_args get_repos replace_repo_glob); my $_args; @@ -25,4 +25,19 @@ sub get_ks_userhook_args { sub ksuserhooks { $_args = \@_; } +sub get_repos +{ + my ($config) = @_; + + return {}; +} + + +sub replace_repo_glob +{ + my ($txt, $repos, $noglob, $baseurl_key, $opt_map, $only_one_txt) = @_; + + return $noglob->($txt); +} + 1; diff --git a/aii-pxelinux/src/test/perl/pxelinux_ks_logging.t b/aii-pxelinux/src/test/perl/pxelinux_ks_logging.t index e2dcbf9e..c8cfea32 100644 --- a/aii-pxelinux/src/test/perl/pxelinux_ks_logging.t +++ b/aii-pxelinux/src/test/perl/pxelinux_ks_logging.t @@ -51,7 +51,7 @@ for my $variant_constant (@PXE_VARIANTS) { $comp->$config_method($cfg); my $fh = get_file($fp); - + like($fh, qr{^\s{4}$kernel_params_cmd\s.*?\ssyslog=logserver:514\sloglevel=debug(\s|$)}m, "append line (variant=$variant_name)"); }; @@ -67,7 +67,7 @@ for my $variant_constant (@PXE_VARIANTS) { $comp->$config_method($cfg); my $fh = get_file($fp); - + unlike($fh, qr{\ssyslog}, "no syslog config (variant=$variant_name)"); unlike($fh, qr{\sloglevel}, "no loglevel config (variant=$variant_name)"); }; diff --git a/aii-pxelinux/src/test/perl/write_grub2_config.t b/aii-pxelinux/src/test/perl/write_grub2_config.t index a3b16ff0..931d8d79 100644 --- a/aii-pxelinux/src/test/perl/write_grub2_config.t +++ b/aii-pxelinux/src/test/perl/write_grub2_config.t @@ -1,7 +1,7 @@ use strict; use warnings; use Test::More; -use Test::Quattor qw(pxelinux_base_config); +use Test::Quattor qw(pxelinux_base_config pxelinux_grub2 pxelinux_grub_glob); use NCM::Component::PXELINUX::constants qw(:pxe_variants :pxe_constants); use NCM::Component::pxelinux; use CAF::FileWriter; @@ -36,16 +36,17 @@ sub check_config { # Retrieve config file name matching the configuration my $fp = $comp->_file_path($cfg, PXE_VARIANT_GRUB2); - + # Check config file contents my $fh = get_file($fp); my $hostname = hostname(); + my $prefix = $kernel_root ? "$kernel_root/" : ""; like($fh, qr{^set default=0$}m, "default kernel ($test_msg)"); like($fh, qr{^set timeout=\d+$}m, "Grub2 menu timeout ($test_msg)"); like($fh, qr(^menuentry\s"Install\s[\w\-\s()\[\]]+"\s\{$)m, "Grub2 menu entry ($test_msg)"); like($fh, qr{^\s{4}set root=\(pxe\)$}m, "Grub2 root ($test_msg)"); - like($fh, qr{^\s{4}$TEST_EFI_LINUX_CMD $kernel_root/mykernel}m, "Kernel loading ($test_msg)"); - like($fh, qr{^\s{4}$test_efi_initrd_cmd $kernel_root/path/to/initrd$}m, "initrd loading ($test_msg)"); + like($fh, qr{^\s{4}$TEST_EFI_LINUX_CMD ${prefix}mykernel}m, "Kernel loading ($test_msg)"); + like($fh, qr{^\s{4}$test_efi_initrd_cmd ${prefix}path/to/initrd$}m, "initrd loading ($test_msg)"); like($fh, qr(^})m, "end of menu entry ($test_msg)"); } @@ -68,4 +69,15 @@ $this_app->{CONFIG}->define(GRUB2_EFI_KERNEL_ROOT); $this_app->{CONFIG}->set(GRUB2_EFI_KERNEL_ROOT, $GRUB2_EFI_KERNEL_ROOT_VALUE); check_config($comp, $cfg, $GRUB2_EFI_KERNEL_ROOT_VALUE, 'No GRUB2_EFI_KERNEL_ROOT'); +$cfg = get_config_for_profile('pxelinux_grub2'); + +$this_app->{CONFIG}->set(GRUB2_EFI_KERNEL_ROOT, "/foo/bar"); +check_config($comp, $cfg, qr{\(http,myhost\.example\)}, 'Profile ignores GRUB2_EFI_KERNEL_ROOT'); + +$cfg = get_config_for_profile('pxelinux_grub_glob'); + +$this_app->{CONFIG}->set(GRUB2_EFI_KERNEL_ROOT, "/foooo/baarr"); +check_config($comp, $cfg, qr{\(http,abc.def\)}, + 'Profile ignores GRUB2_EFI_KERNEL_ROOT, and replaces protocol'); + done_testing(); diff --git a/aii-pxelinux/src/test/resources/pxelinux_config_common.pan b/aii-pxelinux/src/test/resources/pxelinux_config_common.pan index eb54b2ce..0282a8d6 100644 --- a/aii-pxelinux/src/test/resources/pxelinux_config_common.pan +++ b/aii-pxelinux/src/test/resources/pxelinux_config_common.pan @@ -14,18 +14,18 @@ prefix "/system/network"; "nameserver/0" = 'nm1'; "nameserver/1" = 'nm2'; "default_gateway" = "133.2.85.1"; -"interfaces/eth0" = nlist("ip", "133.2.85.234", - "netmask", "255.255.255.0", - ); -"interfaces/eth1" = nlist("onboot", "no", - ); - +"interfaces/eth0" = dict( + "ip", "133.2.85.234", + "netmask", "255.255.255.0", + ); +"interfaces/eth1" = dict( + "onboot", "no", + ); prefix "/hardware/cards/nic"; "eth0/hwaddr" = "00:11:22:33:44:55"; "eth1/hwaddr" = "00:11:22:33:44:66"; - prefix "/system/aii/nbp/pxelinux"; "initrd" = "path/to/initrd"; "kernel" = 'mykernel'; @@ -35,4 +35,3 @@ prefix "/system/aii/nbp/pxelinux"; "firmware" = "firmware.cfg"; "livecd" = "livecd.cfg"; "rescue" = "rescue.cfg"; - diff --git a/aii-pxelinux/src/test/resources/pxelinux_grub2.pan b/aii-pxelinux/src/test/resources/pxelinux_grub2.pan new file mode 100644 index 00000000..f8f72616 --- /dev/null +++ b/aii-pxelinux/src/test/resources/pxelinux_grub2.pan @@ -0,0 +1,13 @@ +@{ +Grub2 object template for aii-pxelinux unit tests. +Only include pxelinux_config.common.pan +} + +object template pxelinux_grub2; + +include 'pxelinux_config_common'; + +prefix "/system/aii/nbp/pxelinux"; + +"kernel" = "(http,myhost.example)/mykernel"; +"initrd" = "(http,myhost.example)/path/to/initrd"; diff --git a/aii-pxelinux/src/test/resources/pxelinux_grub_glob.pan b/aii-pxelinux/src/test/resources/pxelinux_grub_glob.pan new file mode 100644 index 00000000..1da2030b --- /dev/null +++ b/aii-pxelinux/src/test/resources/pxelinux_grub_glob.pan @@ -0,0 +1,14 @@ +@{ +Grub2 object template for aii-pxelinux unit tests. +Only include pxelinux_config.common.pan +} + +object template pxelinux_grub_glob; + +include 'pxelinux_config_common'; + +prefix "/system/aii/nbp/pxelinux"; +# yeah, just pass non-glob for now +# get_repos is mocked with simple {} for now +"kernel" = "http://abc.def/mykernel"; +"initrd" = "http://abc.def/path/to/initrd";