#!/usr/bin/env perl # # Copyright (c) 2019-2021 Mischa Peters # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # # vmm(4)/vmd(8) VM deploy script for OpenBSD Amsterdam # 2018/12/17 version 3 - Perl again! :) # 2018/12/19 added: default disk and memory size VM options # 2018/12/20 added: extra interface option for a VM # 2019/04/14 changed: $dir in conf path load # 2019/05/07 added: disk format option, img or qcow2 # 2019/05/31 added: check for OpenBSD 6.6 vmctl command line syntax change # 2019/09/29 added: doas.conf for pkill option # 2020/05/10 changed: includes all install sets, inline with sysupgrade # 2020/05/22 changed: set the hosts password for the users # 2020/05/24 changed: add staggered option to vm.conf, leave disable for now # 2020/09/20 changed: add agentx option to vm.conf for OpenBSD 7.0 # 2020/10/25 changes: doas.conf for pkill option, so it works again # 2021/01/19 changes: add format of disk images in vm.conf (CVE-2010-3851), thanx Johan Finnved # 2021/05/18 changes: restrict the install.conf files in /var/www # use 5.024; use strict; use warnings; use autodie; use Cwd qw(cwd); use User::pwent; # fuction to parse _deploy.conf and vm*.txt files # all variables are stripped and added to either %vms or %conf sub get_variables { my ($hash_name, @files) = @_; my %hash; my $filename; my $vm_name; my $vm_number; for my $file (@files) { # When hash is 'vms' use the vm_name as key # Otherwise use 'conf' as key if ($hash_name eq "vms") { ($filename = $file) =~ s/.*\///; ($vm_name = $filename) =~ s/\.txt//; ($vm_number = $vm_name) =~ s/^vm//; $hash{$vm_name}{'vm_number'} = $vm_number; } open my $fh, "<", "$file"; while (my $row = <$fh>) { next if ($row =~ /^\s*($|#)/); chomp($row); (my $key, my $val) = split(/=/, $row, 2); if ($hash_name eq "vms") { ($hash{$vm_name}{$key} .= $val) =~ s/^"+|"+$//g; } else { ($hash{$hash_name}{$key} .= $val) =~ s/^"+|"+$//g; } } close $fh; } return %hash; } # function to render the vm.conf(5) file # if the disk image file doesn't exist "boot bsd.rd" is used # if the disk image file exists "boot bsd.rd" won't be used sub render_vm_conf { my %conf = %{$_[0]}; my %vms = %{$_[1]}; my $_etc = $conf{'conf'}{'ETC'}; my $_VERSION = qx(uname -r); open my $fh_vm, ">", "$_etc/vm.conf"; printf $fh_vm "#\n# File generated by deploy.pl\n#\n"; printf $fh_vm "socket owner :%s\n", $conf{'conf'}{'VMDUSERS'}; printf $fh_vm "staggered start parallel 2 delay 90\n"; if ($_VERSION > 6.9) { printf $fh_vm "agentx\n"; } printf $fh_vm "\n"; printf $fh_vm "switch \"%s\" {\n", $conf{'conf'}{'SWITCH'}; printf $fh_vm "\tinterface %s\n", $conf{'conf'}{'INTERFACE'}; printf $fh_vm "}\n\n"; if ($conf{'conf'}{'SWITCH2'}) { printf $fh_vm "switch \"%s\" {\n", $conf{'conf'}{'SWITCH2'}; printf $fh_vm "\tinterface %s\n", $conf{'conf'}{'INTERFACE2'}; printf $fh_vm "}\n\n"; } for my $vm_name (sort keys %vms) { my $_instance = $vms{$vm_name}{'instance'} || $vm_name; my $_disk_format = $vms{$vm_name}{'format'} || $conf{'conf'}{'FORMAT'}; my $_disk = $conf{'conf'}{'IMAGES'} . "/" . $_instance . "." . $_disk_format; my $_disk2 = $conf{'conf'}{'IMAGES'} . "/" . $_instance . "_extra." . $_disk_format if $vms{$vm_name}{'disk2'}; $_disk_format = $_disk_format eq "img" ? "raw" : $_disk_format; my $_owner = $vms{$vm_name}{'owner'} || $vms{$vm_name}{'username'}; my $_memory = $vms{$vm_name}{'memory'} || $conf{'conf'}{'MEMORY'}; my $_boot = $conf{'conf'}{'IMAGES'} . "/bsd.rd"; my $_switch = $vms{$vm_name}{'switch'} || $conf{'conf'}{'SWITCH'}; my $_switch2 = $vms{$vm_name}{'switch2'}; my $_mac = $vms{$vm_name}{'mac'} || $conf{'conf'}{'MAC_PREFIX'} . ":" . $vms{$vm_name}{'vm_number'}; printf $fh_vm "vm \"%s\" {\n", $_instance; printf $fh_vm "\tdisable\n"; printf $fh_vm "\towner %s\n", $_owner; printf $fh_vm "\tmemory %s\n", $_memory if $_memory; printf $fh_vm "\tboot \"%s\"\n", $_boot if (! -e $_disk); printf $fh_vm "\tdisk \"%s\" format %s\n", $_disk, $_disk_format; printf $fh_vm "\tdisk \"%s\" format %s\n", $_disk2, $_disk_format if $_disk2; printf $fh_vm "\tinterface tap {\n"; printf $fh_vm "\t\tswitch \"%s\"\n", $_switch; printf $fh_vm "\t\tlladdr %s\n", $_mac; printf $fh_vm "\t}\n"; printf $fh_vm "\tinterface tap { switch \"%s\" }\n", $_switch2 if $_switch2; printf $fh_vm "}\n"; } close $fh_vm; } # function to render the dhcpd.conf(5) file # if the disk image file doesn't exist "auto_install" is used as filename # if the disk image file exists "auto_upgrade" is used as filename sub render_dhcpd_conf { my %conf = %{$_[0]}; my %vms = %{$_[1]}; my $_etc = $conf{'conf'}{'ETC'}; open my $fh_dhcpd, ">", "$_etc/dhcpd.conf"; printf $fh_dhcpd "#\n# File generated by deploy.pl\n#\n"; printf $fh_dhcpd "option domain-name \"%s\";\n", $conf{'conf'}{'DOMAIN'}; printf $fh_dhcpd "option domain-name-servers %s;\n\n", $conf{'conf'}{'DNS'}; printf $fh_dhcpd "subnet %s netmask %s {\n", $conf{'conf'}{'SUBNET'}, $conf{'conf'}{'NETMASK'}; printf $fh_dhcpd "\toption routers %s;\n", $conf{'conf'}{'ROUTER'}; printf $fh_dhcpd "\tserver-name \"%s.%s\";\n", $conf{'conf'}{'SERVER'}, $conf{'conf'}{'DOMAIN'}; for my $vm_name (sort keys %vms) { my $_instance = $vms{$vm_name}{'instance'} || $vm_name; my $_disk_format = $vms{$vm_name}{'format'} || $conf{'conf'}{'FORMAT'}; my $_disk = $conf{'conf'}{'IMAGES'} . "/" . $_instance . "." . $_disk_format; my $_mac = $vms{$vm_name}{'mac'} || $conf{'conf'}{'MAC_PREFIX'} . ":" . $vms{$vm_name}{'vm_number'}; my $_ip = $vms{$vm_name}{'ip'} || $conf{'conf'}{'IP_PREFIX'} . "." . ($conf{'conf'}{'IP_START'} + $vms{$vm_name}{'vm_number'}); my $_ipv6 = $conf{'conf'}{'IPV6_PREFIX'} . ":" . ($conf{'conf'}{'IPV6_START'} + $vms{$vm_name}{'vm_number'}) . "::" . ($conf{'conf'}{'IP_START'} + $vms{$vm_name}{'vm_number'}); my $_hostname = $vms{$vm_name}{'hostname'}; printf $fh_dhcpd "\thost %s {\n", $_instance; printf $fh_dhcpd "\t\thardware ethernet %s;\n", $_mac; printf $fh_dhcpd "\t\tfixed-address %s;\n", $_ip; printf $fh_dhcpd "\t\t#fixed-address-ipv6 %s;\n", $_ipv6; if (! -e $_disk) { printf $fh_dhcpd "\t\tfilename \"auto_install\";\n"; } else { printf $fh_dhcpd "\t\tfilename \"auto_upgrade\";\n"; } printf $fh_dhcpd "\t\toption host-name \"%s\";\n", $_hostname; printf $fh_dhcpd "\t}\n"; } printf $fh_dhcpd "}\n"; close $fh_dhcpd; } # function to render the -install.conf file for initial # provisioning using autoinstall(8) sub render_install_conf { my %conf = %{$_[0]}; my %vms = %{$_[1]}; printf "autoinstall(8) files:\n"; for my $vm_name (sort keys %vms) { my $_instance = $vms{$vm_name}{'instance'} || $vm_name; my $_disk_format = $vms{$vm_name}{'format'} || $conf{'conf'}{'FORMAT'}; my $_disk = $conf{'conf'}{'IMAGES'} . "/" . $_instance . "." . $_disk_format; my $_mac = $vms{$vm_name}{'mac'} || $conf{'conf'}{'MAC_PREFIX'} . ":" . $vms{$vm_name}{'vm_number'}; my $_htdocs = $conf{'conf'}{'HTDOCS'}; if (! -e $_disk) { my $_hostname = $vms{$vm_name}{'hostname'}; my $jot_pass = qx(jot -rcs '' 20 33 126); chomp($jot_pass); my $_ipv6 = $conf{'conf'}{'IPV6_PREFIX'} . ":" . ($conf{'conf'}{'IPV6_START'} + $vms{$vm_name}{'vm_number'}) . "::" . ($conf{'conf'}{'IP_START'} + $vms{$vm_name}{'vm_number'}); my $_ipv6_gateway = $conf{'conf'}{'IPV6_PREFIX'} . ":" . ($conf{'conf'}{'IPV6_START'} + $vms{$vm_name}{'vm_number'}) . "::1"; my $_username = $vms{$vm_name}{'username'}; my $_sshkey = $vms{$vm_name}{'sshkey'}; open my $fh_install, ">", "$_htdocs/$_mac-install.conf"; printf $fh_install "#\n# File generated by deploy.pl\n#\n"; printf $fh_install "System hostname = %s\n", $_hostname; printf $fh_install "Password for root = %s\n", $jot_pass; printf $fh_install "Which speed should com0 = 115200\n"; printf $fh_install "Network interfaces = vio0\n"; printf $fh_install "IPv4 address for vio0 = dhcp\n"; printf $fh_install "IPv6 address for vio0 = %s\n", $_ipv6; printf $fh_install "IPv6 default router = %s\n", $_ipv6_gateway; printf $fh_install "Setup a user = %s\n", $_username; printf $fh_install "Password for user = %s\n", $jot_pass; printf $fh_install "Public ssh key for user = %s %s\n", $_sshkey, $jot_pass; printf $fh_install "Which disk is the root disk = sd0\n"; printf $fh_install "What timezone are you in = Europe/Amsterdam\n"; printf $fh_install "Location of sets = http\n"; printf $fh_install "Server = mirror.openbsd.amsterdam\n"; printf $fh_install "Set name(s) = +site*\n"; printf $fh_install "Continue anyway = yes\n"; printf $fh_install "Continue without verification = yes\n"; close $fh_install; chmod 0440, "$_htdocs/$_mac-install.conf"; chown 67, 67, "$_htdocs/$_mac-install.conf"; printf "%16s %s created\n", $_instance, $_htdocs . "/" . $_mac . "-install.conf"; } elsif (-e $_disk && -e "$_htdocs/$_mac-install.conf") { unlink "$_htdocs/$_mac-install.conf" or warn "Unable to unlink file: $!\n"; printf "%16s %s deleted\n", $_instance, $_htdocs . "/" . $_mac . "-install.conf"; } } } # function to render the doas.conf(5) file sub render_doas_conf { my %conf = %{$_[0]}; my %vms = %{$_[1]}; my $_etc = $conf{'conf'}{'ETC'}; open my $fh_doas, ">", "$_etc/doas.conf"; printf $fh_doas "permit nopass setenv { ENV PS1 SSH_AUTH_SOCK PATH=\$PATH HOME=\$HOME USER=\$USER } mischa\n"; printf $fh_doas "permit nopass keepenv root as root\n"; for my $vm_name (sort keys %vms) { my $_instance = $vms{$vm_name}{'instance'} || $vm_name; my $_owner = $vms{$vm_name}{'owner'} || $vms{$vm_name}{'username'}; printf $fh_doas "permit nopass %s as root cmd pkill args -9 -xf \"vmd: %s\"\n", $_owner, $_instance; } close $fh_doas; } # function to create accounts on the host for vmctl(8) access sub create_accounts { my %conf = %{$_[0]}; my %vms = %{$_[1]}; printf "useradd(8) users:\n"; for my $vm_name (sort keys %vms) { my $_instance = $vms{$vm_name}{'instance'} || $vm_name; my $_owner = $vms{$vm_name}{'owner'} || $vms{$vm_name}{'username'}; my $_group = $conf{'conf'}{'VMDUSERS'}; my $_sshkey = $vms{$vm_name}{'sshkey'}; my $id = getpwnam("$_owner"); if (! $id) { my $jot_pass = qx(jot -rcs '' 20 43 125); chomp($jot_pass); my $encrypt_pass = qx(encrypt '${jot_pass}'); chomp($encrypt_pass); my $output = qx(/usr/sbin/useradd -m -G $_group -p '${encrypt_pass}' $_owner); open my $fh_authorized, ">>", "/home/$_owner/.ssh/authorized_keys"; printf $fh_authorized "%s\n", $_sshkey; close $fh_authorized; printf "%16s %s account created\n", $_instance, $_owner; } } } # function to create the disk image files for vmm(4)/vmd(8) sub create_img_files { my %conf = %{$_[0]}; my %vms = %{$_[1]}; my $_VERSION = qx(uname -r); my $vmctl_create; printf "vmm(4)/vmd(8) files:\n"; for my $vm_name (sort keys %vms) { my $_instance = $vms{$vm_name}{'instance'} || $vm_name; my $_disk_format = $vms{$vm_name}{'format'} || $conf{'conf'}{'FORMAT'}; my $_disk = $conf{'conf'}{'IMAGES'} . "/" . $_instance . "." . $_disk_format; my $_disk_size = $vms{$vm_name}{'disk'} || $conf{'conf'}{'DISKSIZE'}; my $_disk2 = $conf{'conf'}{'IMAGES'} . "/" . $_instance . "_extra." . $_disk_format if $vms{$vm_name}{'disk2'}; my $_disk2_size = $vms{$vm_name}{'disk2'} if $vms{$vm_name}{'disk2'}; if (! -e $_disk) { if ($_VERSION < 6.6) { $vmctl_create= "vmctl create $_disk -s $_disk_size 2>&1"; } else { $vmctl_create= "vmctl create -s $_disk_size $_disk 2>&1"; } my $output = qx($vmctl_create); if ($? == 0) { printf "%16s %s created (size %s)\n", $_instance, $_disk, $_disk_size; } else { printf "%16s %s NOT created!!!\n", $_instance, $_disk; } } if ($_disk2) { if (! -e $_disk2) { if ($_VERSION < 6.6) { $vmctl_create = "vmctl create $_disk2 -s $_disk2_size 2>&1"; } else { $vmctl_create = "vmctl create -s $_disk2_size $_disk2 2>&1"; } my $output = qx($vmctl_create); if ($? == 0) { printf "%16s %s created (size %s)\n", $_instance, $_disk2, $_disk2_size; } else { printf "%16s %s NOT created (size %s)!!!\n", $_instance, $_disk2, $_disk2_size; } } } } } # function to print all keys & values for debug purposes sub debug_parse { my %conf = %{$_[0]}; my %vms = %{$_[1]}; for my $vm_name (sort keys %vms) { for my $key (keys %{$vms{$vm_name}}) { printf "VMS: %s %s = %s\n", $vm_name, $key, $vms{$vm_name}{$key}; } } } # check if _deploy.conf exists in current working directory my %conf; my $dir = cwd; if (-e "$dir/_deploy.conf") { %conf = get_variables('conf', "$dir/_deploy.conf"); } else { printf "Unable to find config file in current directory (%s).\n", $dir; printf "Create the config file _deploy.conf in %s.\n", $dir; exit 1; } # parse all vm*.txt files in the VMS directory my %vms; my @files = glob "$conf{'conf'}{'VMS'}/*.txt"; %vms = get_variables('vms', @files); # run all functions #debug_parse(\%conf, \%vms); render_vm_conf(\%conf, \%vms); render_dhcpd_conf(\%conf, \%vms); render_install_conf(\%conf, \%vms); create_accounts(\%conf, \%vms); create_img_files(\%conf, \%vms); render_doas_conf(\%conf, \%vms);