File initial_setup.pl of Package suse-infrastructure-vm-bootstrap
#!/usr/bin/perl -W
# Script for the initial configuration of virtual machines in the SUSE infrastructure
# Copyright (C) 2024 SUSE LLC <georg.pfuetzenreuter@suse.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
use v5.26; # Leap 15.6
use Getopt::Std;
use feature 'say';
###
# User config
###
my %SaltMasters = (
'prg2' => [
'2a07:de40:b205:1a:144:39:195:0',
'2a07:de40:b205:1a:144:39:193:0',
],
'prg3' => [
'2a07:de40:b205:1a:144:39:195:0',
'2a07:de40:b205:1a:144:39:193:0',
],
'dmz-prg2' => [
'2a07:de40:b280:9:10:151:9:21',
],
'slc1' => [
'2a07:de40:6101:1a::20',
'2a07:de40:6101:1a::21',
],
'dmz-slc1' => [
'2a07:de40:6180:09::20',
'2a07:de40:6180:09::21',
],
);
my $defaultinterface = 'eth0';
my $hostsfile = '/etc/hosts';
my $nwconfigdir = '/etc/sysconfig/network';
my $saltconfigdir = '/etc/salt';
my $udevfile = '/etc/udev/rules.d/70-persistent-net.rules';
###
# Internal config
###
our $VERSION = '__VERSION__';
my $checkfile = '/var/adm/suse_infra_bootstrapped';
$Getopt::Std::STANDARD_HELP_VERSION = 1;
###
# Pre-flight check
###
if ( -f $checkfile ) {
die "Machine seems to already be bootstrapped, refusing to operate.\n"
}
###
# Functions
###
sub usage {
return <<~'EOT';
Options:
-f FQDN
-g IP address of gateway (WITHOUT prefix)
-i IP address of this machine (WITH prefix)
-v Short form of the VLAN this machine is in
EOT
}
sub HELP_MESSAGE {
say usage();
}
sub replace_string_and_nuke_comments_in_file {
my ($file, $search, $substitute) = @_;
local ($^I, @ARGV) = ('.tmp', $file);
while(<>) {
if ($_ =~ /^[#\s]/) { next };
$_ =~ s/$search/$substitute/;
print;
}
}
sub write_file {
my ($file, $operator, $text) = @_;
open my $fh, $operator, $file or die "Failed to open $file!";
print $fh $text;
close $fh;
}
###
# Argument parsing
###
my %opts;
getopts('f:g:hi:l:v:n', \%opts) or abort();
my $fqdn = $opts{f};
my $gateway = $opts{g};
my $location = $opts{l};
my $network = $opts{i};
my $vlan = $opts{v};
my $reboot = ! $opts{n};
if (
defined( $opts{h} )
||
! defined( $fqdn && $gateway && $network && $vlan )
) {
die usage()
}
say "FQDN: $fqdn, GW IP Addr.: $gateway, MY IP Net.: $network, VLAN: $vlan";
if ( index($fqdn, '.') == -1 ) {
die "Given FQDN is not a FQDN, aborting!\n"
}
if ( index($vlan, '-') == -1 ) {
die "Given VLAN does not conform to our short VLAN naming standard, aborting!\n"
}
if ( index($network, '/') == -1 ) {
die "Given IP address is missing a CIDR mask.\n"
}
say "Will reboot? $reboot";
my ($address, $prefix) = split /\//, $network, 2;
my ($hostname, $domain) = split /\./, $fqdn, 2;
say "MY IP Add.: $address, MY hostname: $hostname, MY domain: $domain";
my $enddomain;
if ( ! defined( $location ) ) {
($location, $enddomain) = split /\./, $domain, 2;
}
say "MY Location: $location";
if ( ! exists($SaltMasters{$location}) ) {
die "No Salt Masters found for the given location, aborting! Verify if the location code is correct.\n"
}
###
# Further validation
# ($defaultinterface must exist, subsequent logic would not work otherwise)
###
if ( qx( ip -br l sh ) !~ /^$defaultinterface/m ) {
die "Failed to locate default interface, aborting!\n"
}
###
# Now the real action
###
# Remove the default interface and routing configuration installed by wicked
unlink "$nwconfigdir/ifcfg-$defaultinterface";
unlink "$nwconfigdir/routes";
# Rename interface via udev
replace_string_and_nuke_comments_in_file($udevfile, $defaultinterface, $vlan);
# Write interface configuration
write_file("$nwconfigdir/ifcfg-$vlan", '>', <<EOF
BOOTPROTO=static
STARTMODE=auto
IPADDR_0=$network
EOF
);
# Write routes configuration
write_file("$nwconfigdir/routes", '>', <<EOF
default $gateway
EOF
);
# Set both transient and static hostname
system( 'hostnamectl', 'set-hostname', $fqdn );
write_file($hostsfile, '>>', <<EOF
$address $fqdn $hostname
EOF
);
# TODO: get rid of the need for grains
write_file("$saltconfigdir/grains", '>', <<EOF
roles: []
EOF
);
# Configure Salt Minion enough to connect to our masters
my $SaltMastersYml = '[' . join(',', map "'$_'", @{$SaltMasters{$location}}) . ']';
write_file("$saltconfigdir/minion.d/bootstrap.conf", '>', <<EOF
hash_type: sha512
ipv6: True
master: $SaltMastersYml
saltenv: production
EOF
);
# Enable services for startup after the first reboot
system( 'systemctl', 'enable', 'salt-minion', 'sshd' );
# Remove myself, I am no longer needed now :(
system( 'zypper', '-n', 'rm', 'suse-infrastructure-vm-bootstrap' );
# Create /etc/machine-id with unique identifier
system( 'systemd-machine-id-setup' );
write_file($checkfile, '>', 'ok');
if ( $reboot == 1 ) {
system( 'shutdown', '-r', 'now', 'Rebooting to apply first boot configuration ...' );
}