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 ...' );
}
openSUSE Build Service is sponsored by