Home-assistant: check config for errors

Since it is quite easy to make errors when modifying the different configuration files for home-assistant I needed a smarter way to check for errors without the need to restart home-assistant and look in the log file for errors.

I came across the Tools page which describes a couple of scripts that home-assistant supports, and the check_config was just what I needed.

With the information I decided to create a bash alias for easy access. My alias look like this:

alias check_ha_config='scl enable rh-python35 "source /opt/homeassistant/bin/activate; source /home/homeassistant/.homeassistant/export_environment.sh; hass --script check_config -c /home/homeassistant/.homeassistant/ | grep ERROR; deactivate"'

This is an alias adopted to my environment:

  1. scl enable rh-python35 – Activate python 3.5 under Centos
  2. source /opt/homeassistant/bin/activate – Activates the virtual environment used for home assistant
  3. export_environment.sh – Exports all the environment variables (see previous post)
  4. hass --script check_config -c /home/homeassistant/.homeassistant/ – The actual check
  5. | grep ERROR – Optional grep that only shows errors
  6. deactivate – Deactivates the virtual environment

A simple version of the alias is:

alias check_ha_config='/path/to/hass --script check_config -c /home/homeassistant/.homeassistant/ | grep ERROR'

The following exports all the variables in an environment file:

#!/bin/bash
export $(cat /opt/configuration/environment/ha.env.template | grep -v "^#" | xargs)

Advertisements

Home-Assistant: Load environment and use environment variables

At home I have started using home-assistant to control automations at home.

One thing that I found out early was that I wanted a central place to store all IP-addresses, urls etc. in a way similar to secrets where a !secret keyword is used to access a string stored in a place that is not under source control.

Home-assistant supports the !env_var to access environment variable values.

So my setup looks like this:

  • A systemd unit that loads environment variables from a file before starting home-assistant.
  • A file containing all the environment variables.
  • The environment variables in the configuration instead of hardcoding the IP addresses

This is the home-assistant systemd unit file:

[Unit]
Description=Home Assistant
After=network.target

[Service]
Type=simple
User=homeassistant
EnvironmentFile=/home/homeassistant/.homeassistant/ha.env
ExecStart=/usr/bin/scl enable rh-python35 -- /opt/homeassistant/bin/hass -c "/home/homeassistant/.homeassistant"

[Install]
WantedBy=multi-user.target

Note: Since I’m using home-assistant under centos the software components are used for python 3.5 and activated by the /usr/bin/scl enable rh-python35 part.

The environment variables are defined in the file /home/homeassistant/.homeassistant/ha.env and this looks like this:

HA_TRADFRI_HOST="ikea.local"
HA_VERA_URL="http://vera.local:1234/"
HA_SERVER_IP="192.168.2.2"

Since this file is loaded by the unit file all variables defined in the file is available for home assistant, below is a configuration example using the first variable seen above:

tradfri:
  host: !env_var HA_TRADFRI_HOST
  api_key: !secret tradfri_api_key
  allow_tradfri_groups: false

Using this technique all network specific data can be kept out of the configuration management repository.

Perl multi-rename with regex

I sometimes want to rename multiple files using a regex so, I found this script somewhere developed by Larry Wall, Robin Barker:

#!/usr/bin/perl -w
#
#  This script was developed by Robin Barker (Robin.Barker@npl.co.uk), from Larry Wall's original script eg/rename from the perl source.
#
#  This script is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
#
# Larry(?)'s RCS header:
#  RCSfile: rename,v Revision: 4.1 Date: 92/08/07 17:20:30
#
# $RCSfile: rename,v $$Revision: 1.5 $$Date: 1998/12/18 16:16:31 $
#
# $Log: rename,v $ Revision 1.5 1998/12/18 16:16:31 rmb1 moved to perl/source changed man documentation to POD
#
# Revision 1.4 1997/02/27 17:19:26 rmb1 corrected usage string
#
# Revision 1.3 1997/02/27 16:39:07 rmb1 added -v
#
# Revision 1.2 1997/02/27 16:15:40 rmb1 *** empty log message ***
#
# Revision 1.1 1997/02/27 15:48:51 rmb1 Initial revision
#
use strict; use Getopt::Long; Getopt::Long::Configure('bundling'); my ($verbose, $no_act, $force, $op); die "Usage: rename [-v] [-n] [-f] perlexpr [filenames]\n"
    unless GetOptions(
	'v|verbose' => \$verbose,
	'n|no-act' => \$no_act,
	'f|force' => \$force,
    ) and $op = shift; $verbose++ if $no_act; if (!@ARGV) {
    #print "reading filenames from STDIN\n" if $verbose;
    @ARGV = <STDIN>;
    chop(@ARGV);
}
for (@ARGV) {
    my $was = $_;
    eval $op;
    die $@ if $@;
    next if $was eq $_; # ignore quietly
    if (-e $_ and !$force)
    {
	warn "$was not renamed: $_ already exists\n";
    }
    elsif ($no_act or rename $was, $_)
    {
	print "$was renamed as $_\n" if $verbose;
    }
    else
    {
	warn "Can't rename $was $_: $!\n";
    }
}
__END__ 

=head1 NAME

 rename - renames multiple files 

=head1 SYNOPSIS 

B<rename> S<[ B<-v> ]> S<[ B<-n> ]> S<[ B<-f> ]> I<perlexpr> S<[ I<files> ]> 

=head1 DESCRIPTION 

C<rename> renames the filenames supplied according to the rule specified as the 
first argument. 
The I<perlexpr> argument is a Perl expression which is expected to modify the C<$_> 
string in Perl for at least some of the filenames specified. 
If a given filename is not modified by the expression, it will not be 
renamed. If no filenames are given on the command line, filenames will be read 
via standard input. 

For example, to rename all files matching C<*.bak> to strip the extension, 
you might say
	rename 's/\.bak$//' *.bak 

To translate uppercase names to lower, you'd use

	rename 'y/A-Z/a-z/' * 

=head1 OPTIONS 

=over 8 

=item B<-v>, B<--verbose> 

Verbose: print names of files successfully renamed. 

=item B<-n>, B<--no-act> 

No Action: show what files would have been renamed. 

=item B<-f>, B<--force> 

Force: overwrite existing files. 

=back 

=head1 ENVIRONMENT 

No environment variables are used. 

=head1 AUTHOR 

Larry Wall =head1 SEE ALSO mv(1), perl(1) 

=head1 DIAGNOSTICS 

If you give an invalid Perl expression you'll get a syntax error. 

=head1 BUGS The original C<rename> did not check for the existence of target filenames, 
so had to be used with care. I hope I've fixed that (Robin Barker). 

=cut

It is used in the following way:

$ prename -v "s/\.conf/.conf.bak/g" *.conf
$ prename -v "s/\.conf\.bak/.conf/g" *.conf.bak

The following lines renames all *.conf files to *.conf.bak and then back again

Monitor an OpenVPN connection

On one of my servers I use an OpenVPN connection to securely connect to another network. Though it seems that sometimes the link goes down.

So to solve this I wrote a small script that is triggered by a cron job to check if the connection is active or not and if not, restart the OpenVPN daemon.

In order to use the script the first thing is to find out which interface is the VPN interface. By using ifconfig I could see the tun0 was the active VPN interface.

Here is the script

#!/usr/bin/env bash
#
# Date: 2017-08-15
# Version: 1.0
# Author: Stellan Nordenbro <stellan.nordenbro@gmail.com>
#
# The MIT License (MIT)
#
# Copyright (c) 2017 Stellan Nordenbro
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

IFC="tun0"
PING_TIMEOUT=20
PING_COUNT=3
PING_TARGET="www.google.com"
EMAIL_RECIPIENT="user@example.com"
OPENVPN_SERVICE="openvpn.service"
OK=0

# Ping the target, if down restart and send an email
ping -I $IFC -c $PING_COUNT -W $PING_TIMEOUT -n $PING_TARGET > /dev/null 2>&1
if [ $? -ne $OK ]; then
    echo "OpenVPN service seems to be down" | mail -s "Restarting OpenVPN service" $EMAIL_RECIPIENT
    systemctl restart $OPENVPN_SERVICE
fi

Proxmox resize linux guest partition

I sometimes come across that my partitions are too small in a VM Guest. And I can never remember the exact steps involved when resizing them. This post explains the steps needed to resize the root partition (can be any partition) of a VM.

The machines I am working with are CentOS 7 machines and they are using LVM.

I am going to be using fdisk to resize the disk instead of gparted or similar.

This is what we are starting with (df -h)

Filesystem               Size  Used Avail Use% Mounted on
/dev/mapper/centos-root  7.5G  2.2G  5.3G  30% /

Since I don’t need 100% uptime I can shutdown the VM to be resized (can probably be done live see https://pve.proxmox.com/wiki/Resize_disks)

Resize the disk in proxmox:

  1. Locate the VM to resize in the Proxmox web interface
  2. Select Hardware
  3. Find the Hard Disk to resize in the list of hardware
  4. Select Resize disk and enter the increment of disk size (in this example I selected 1 GB)

Note: Any snapshots on the machine needs to be removed before resize is allowed
Note 2: If the disk image is located on a nfs share, it will probably not work

Now we can boot up the machine again…

If we try the above df -h we can see that nothing has changed, this is because the partitions are the same, lets change that…

Using fdisk -l I get a list of all disks in the machine, here I can see that /dev/vda contains the LVM partition (/dev/vda2) that I want to resize

Disk /dev/vda: 11.8 GB, 11811160064 bytes, 23068672 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000b205b

   Device Boot      Start         End      Blocks   Id  System
/dev/vda1   *        2048     1026047      512000   83  Linux
/dev/vda2         1026048    20930559     9952256   8e  Linux LVM

So lets edit the partition map (here is the part that I find scary since we are going to delete the partition…)

# fdisk /dev/vda
Welcome to fdisk (util-linux 2.23.2).

Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Command (m for help): p

Disk /dev/vda: 11.8 GB, 11811160064 bytes, 23068672 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000b205b

   Device Boot      Start         End      Blocks   Id  System
/dev/vda1   *        2048     1026047      512000   83  Linux
/dev/vda2         1026048    20930559     9952256   8e  Linux LVM

Command (m for help): d
Partition number (1,2, default 2): 2
Partition 2 is deleted

Command (m for help): n
Partition type:
   p   primary (1 primary, 0 extended, 3 free)
   e   extended
Select (default p):
Using default response p
Partition number (2-4, default 2):
First sector (1026048-23068671, default 1026048):
Using default value 1026048
Last sector, +sectors or +size{K,M,G} (1026048-23068671, default 23068671):
Using default value 23068671
Partition 2 of type Linux and of size 10.5 GiB is set

Command (m for help): t
Partition number (1,2, default 2):
Hex code (type L to list all codes): 8e
Changed type of partition 'Linux' to 'Linux LVM'

Command (m for help): p

Disk /dev/vda: 11.8 GB, 11811160064 bytes, 23068672 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000b205b

   Device Boot      Start         End      Blocks   Id  System
/dev/vda1   *        2048     1026047      512000   83  Linux
/dev/vda2         1026048    23068671    11021312   8e  Linux LVM

Command (m for help): w
The partition table has been altered!

So what did we do:

  1. Entering the command fdisk /dev/vda on the command line allows us to edit /dev/vda
  2. Entering the p command lists the current partition table
  3. Entering d and then 2 allows us to delete the partition /dev/vda2
  4. Now we can create the new partition by entering n and using default values by pressing ENTER
  5. Then we change the type of the partition from Linux to Linux LVM by entering command t and the code 8e
  6. After that we print the new partition table using p
  7. And finally we write the partition table to disk using w

Now in order for the system to notice the change there are probably several ways to do this, but the easiest is to reboot. So let’s do that now…

Next it is time to resize the lvm, for reference, this is how it looks before.

Physical volume

# pvdisplay
  --- Physical volume ---
  PV Name               /dev/vda2
  VG Name               centos
  PV Size               9.49 GiB / not usable 3.00 MiB
  ...
  Free PE               1
  Allocated PE          2428

Volume Group

# vgdisplay
  --- Volume group ---
  VG Name               centos
  ...
  VG Size               9.49 GiB
  ...
  Total PE              2429
  Alloc PE / Size       2428 / 9.48 GiB
  Free  PE / Size       1 / 4.00 MiB

Logical volume

# lvdisplay
  --- Logical volume ---
  LV Path                /dev/centos/root
  LV Name                root
  VG Name                centos
  ...
  LV Size                7.49 GiB

  --- Logical volume ---
  LV Path                /dev/centos/swap
  ...

So start by resizing the Physical volume:

# pvresize /dev/vda2
  Physical volume "/dev/vda2" changed
  1 physical volume(s) resized / 0 physical volume(s) not resized

Running a pvdisplay show that the physical volume has changed from

PV Size               9.49 GiB / not usable 3.00 MiB
...
Free PE               1

To

PV Size               10.51 GiB / not usable 2.00 MiB
...
Free PE               262

We can also see that the volume group (vgdisplay) now has increased FreePE.

Let’s extend the Logical volume

# lvresize --extents +100%FREE --resizefs /dev/centos/root
  Size of logical volume centos/root changed from 7.49 GiB (1917 extents) to 8.49 GiB (2173 extents).
  Logical volume centos/root successfully resized.

Here I entered the path to the logical volume /dev/centos/root which can be found in the output of the lvdisplay command on LV Path.

Now the resize is complete and the new output from the previous commands are

Physical volume

# pvdisplay
  --- Physical volume ---
  PV Name               /dev/vda2
  VG Name               centos
  PV Size               10.51 GiB / not usable 2.00 MiB
  ...
  Free PE               0
  Allocated PE          2690

Volume Group

# vgdisplay
  --- Volume group ---
  VG Name               centos
  ...
  VG Size               10.51 GiB
  ...
  Total PE              2690
  Alloc PE / Size       2690 / 10.51 GiB
  Free  PE / Size       0 / 0   

Logical volume

# lvdisplay
  --- Logical volume ---
  LV Path                /dev/centos/root
  LV Name                root
  VG Name                centos
  ...
  LV Size                8.51 GiB

  --- Logical volume ---
  LV Path                /dev/centos/swap
  ...

Disk space usage

# df -h
Filesystem               Size  Used Avail Use% Mounted on
/dev/mapper/centos-root  8.6G  2.2G  6.4G  26% /

Proxmox scheduled snapshot creation

This post is not finished, more text will be provided soon.

The following script creates a snapshot of all running VMs on a Proxmox node, it might be suitable for scheduling by cron

#!/usr/bin/env bash
#
# Date: 2017-05-31
# Version: 1.0
# Author: Stellan Nordenbro <stellan.nordenbro@gmail.com>
#
# The MIT License (MIT)
#
# Copyright (c) 2017 Stellan Nordenbro
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

# Quick-check before we allow bad things to happen
if [ -z "${BASH_VERSINFO}" ]; then
  echo "ERROR: You must execute this script with BASH"
  exit 255
fi

# Go to the correct folder
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd $DIR

# The usage text
usage() { 
  echo "Usage: `basename $0` [-hdqS]" 1>&2
  echo
  echo "Optional parameters:" 1>&2
  echo " -h             Displays this message" 1>&2
  echo " -d             Dry-run: no commands will be executed" 1>&2
  echo " -q             Quiet: output to stdout will be hidden" 1>&2
  echo " -S             Silent: output to both stdout and stderr will be hidden" 1>&2
  echo
  exit 1; 
}

# Parse commandline
ALLARGS="$@"
set -- $(getopt hdqS -- "$@")
while true;
do
    case "$1" in
                (-h) usage;;
		(-d) DRY_RUN=yes;;
                (-q) QUIET=yes;;
                (-S) SILENT=yes;;
                (--) ;;
                (-*) echo "Error: unrecognized option $1" 1>&2; exit 1;;
                (*)  break;;
    esac
    shift
done
 
# send all stdout to /dev/null
if [ "${QUIET}" = "yes" ] || [ "${SILENT}" = "yes" ]; then
        exec 1> /dev/null
fi
 
# send all stdout and stderr to /dev/null
if [ "${SILENT}" = "yes" ]; then
        exec 2> /dev/null
fi

################################################################################
# FUNCTIONS
################################################################################
function GetVMName() {
	if [ $# -ne 1 ]; then
		echo "ERROR: ${FUNCNAME} expected 1 parameters: Usage ${FUNCNAME} VM_ID"
		exit 1
	fi
	
	value=$(qm config $1 | grep "name" | awk '{ print $2}')
	echo "$value"
}

function CreateVMSnapshot() {
	if [ $# -ne 1 ]; then
		echo "ERROR: ${FUNCNAME} expected 1 parameters: Usage ${FUNCNAME} VM_ID"
		exit 1
	fi
	
	VM_ID="$1"
	VM_NAME=$(GetVMName ${VM_ID})
	SNAPSHOT_NAME="AS_$(date +"%Y%m%d_%H%M")"
	SNAPSHOT_DESCRIPTION="Automated snapshot by script"
	
	echo "* Creating snapshot ${SNAPSHOT_NAME} on virtual machine ${VM_NAME} with id ${VM_ID}..."
	if [ "${DRY_RUN}" = "yes" ]; then
        	echo "   DRY-RUN:   qm snapshot ${VM_ID} \"${SNAPSHOT_NAME}\" -description \"${SNAPSHOT_DESCRIPTION}\" -vmstate 0"
	else
        	qm snapshot ${VM_ID} "${SNAPSHOT_NAME}" -description "${SNAPSHOT_DESCRIPTION}" -vmstate 0
        	snapshot_count=$(qm listsnapshot ${VM_ID} | awk '{print $1}' | grep "${SNAPSHOT_NAME}" | wc -l)
        	if [ $snapshot_count != 1 ]; then
        	        echo "ERROR: Snaphot creation failed on virtual machine ${VM_NAME} with id ${VM_ID}"
        	        exit 1
        	fi
	fi
	echo -e "* Done creating snapshot ${SNAPSHOT_NAME} on virtual machine ${VM_NAME} with id ${VM_ID}.\n\n"
}

################################################################################
# CONFIGURATIOM
################################################################################
HOSTNAME=$(hostname)

echo "Creating scheduled snapshots of virtual machines on $HOSTNAME"
echo -e "-------------------------------------------------------\n\n"

################################################################################
# SNAPSHOT
################################################################################

echo "* Retrieving running VMs"
RUNNING_VMS=($(qm list | grep -v VMID | grep "running" | awk '{print $1}'))
echo "* Done."

echo "* Creating snapshot for running VMs"
for VM_ID in "${RUNNING_VMS[@]}"; do
	CreateVMSnapshot "$VM_ID"
done
echo "* Done creating VM snapshots"

echo "Script execution done."

If anyone else have use for this script then go ahead, no guarantees of course, use it on your own risk…

Proxmox automated conversion to template

This post is not finished, more text will be provided soon.

The following script takes a virtual machine with the purpose to act as a base machine for new machines and creates a Proxmox template from it by:

  • Creating a snapshot
  • Deleting prevously created template if it exists
  • Cloning the machine
  • Connecting to the machine using temporary ssh key
  • Using previously published oem-config-prepare script on the cloned machine (with the added removal of the temporary ssh key)
  • Converting the cloned virtual machine to a template

Most configurable options are located in the configurations section

#!/usr/bin/env bash
#
# Date: 2017-05-31
# Version: 1.0
# Author: Stellan Nordenbro <stellan.nordenbro@gmail.com>
#
# The MIT License (MIT)
#
# Copyright (c) 2017 Stellan Nordenbro
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

# Quick-check before we allow bad things to happen
if [ -z "${BASH_VERSINFO}" ]; then
  echo "ERROR: You must execute this script with BASH"
  exit 255
fi

# Go to the correct folder
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd $DIR

# The usage text
usage() {
  echo "Usage: `basename $0` [-hdqS]" 1>&2
  echo
  echo "Optional parameters:" 1>&2
  echo " -h             Displays this message" 1>&2
  echo " -d             Dry-run: no commands will be executed" 1>&2
  echo " -q             Quiet: output to stdout will be hidden" 1>&2
  echo " -S             Silent: output to both stdout and stderr will be hidden" 1>&2
  echo
  exit 1;
}

# Parse commandline
ALLARGS="$@"
set -- $(getopt hdqS -- "$@")
while true;
do
    case "$1" in
                (-h) usage;;
                (-d) DRY_RUN=yes;;
                (-q) QUIET=yes;;
                (-S) SILENT=yes;;
                (--) ;;
                (-*) echo "Error: unrecognized option $1" 1>&2; exit 1;;
                (*)  break;;
    esac
    shift
done

# send all stdout to /dev/null
if [ "${QUIET}" = "yes" ] || [ "${SILENT}" = "yes" ]; then
        exec 1> /dev/null
fi

# send all stdout and stderr to /dev/null
if [ "${SILENT}" = "yes" ]; then
        exec 2> /dev/null
fi

##############################################
# FUNCTIONS
#############################################
function GetVMOption() {
	if [ $# -ne 2 ]; then
                echo "ERROR: ${FUNCNAME} expected 2 parameters: Usage ${FUNCNAME} VM_ID OPTION"
                exit 1
	fi

	value=$(qm config $1 | grep $2 | awk '{ print $2}')
	echo "$value"
}

function SetVMOption() {
	if [ $# -ne 3 ]; then
		echo "ERROR: ${FUNCNAME} expected 2 parameters: Usage ${FUNCNAME} VM_ID OPTION VALUE"
		exit 1
	fi

	if [ "${DRY_RUN}" = "yes" ]; then
		echo "   DRY-RUN:   qm set $1 -${2} $3"
	else
		qm set $1 -${2} $3
	fi
}

#############################################
# CONFIGURATION
#############################################
HOSTNAME=$(hostname)
VM_ID=10000
VM_NAME=$(GetVMOption ${VM_ID} name)
SNAPSHOT_NAME="A$(date +"%Y%m%d_%H%M")"
SNAPSHOT_DESCRIPTION="Automated snapshot by template creation script"
TEMPLATE_ID=10001
TEMPLATE_NAME="template-${VM_NAME}"
TEMPLATE_POOL="Templates"
TEMPLATE_STORAGE="shared-templates"
TEMPLATE_DESCRIPTION="Template created $(date +"%Y-%m-%d %T")"
SSH_KEY="~/.ssh/temp_id_rsa"

echo "Creating template of base virtual machines on $HOSTNAME"
echo -e "-------------------------------------------------------nn"

#############################################
# SNAPSHOT
#############################################
echo "* Creating snapshot ${SNAPSHOT_NAME} on virtual machine ${VM_NAME} with id ${VM_ID}..."
if [ "${DRY_RUN}" = "yes" ]; then
	echo "   DRY-RUN:   qm snapshot ${VM_ID} "${SNAPSHOT_NAME}" -description "${SNAPSHOT_DESCRIPTION}" -vmstate 0"
else
	qm snapshot ${VM_ID} "${SNAPSHOT_NAME}" -description "${SNAPSHOT_DESCRIPTION}" -vmstate 0
	snapshot_count=$(qm listsnapshot ${VM_ID} | awk '{print $1}' | grep "${SNAPSHOT_NAME}" | wc -l)
	if [ $snapshot_count != 1 ]; then
		echo "ERROR: Snaphot creation failed on virtual machine ${VM_NAME} with id ${VM_ID}"
		exit 1
	fi
fi
echo -e "* Done creating snapshot ${SNAPSHOT_NAME}.nn"

#############################################
# EXISTING TEMPLATE CHECK
#############################################
echo "* Checking for existing template vms..."
if [ "${DRY_RUN}" = "yes" ]; then
	echo "   DRY-RUN:   qm list | grep -v "VMID" | grep "${TEMPLATE_NAME}" | awk '{print $1}' | grep ${TEMPLATE_ID} | wc -l"
fi
echo -e "* Done checking for existing tempalte vms.nn"

#############################################
# EXISTING TEMPLATE DELETE
#############################################
vm_count=$(qm list | grep -v "VMID" | grep "${TEMPLATE_NAME}" | awk '{print $1}' | grep ${TEMPLATE_ID} | wc -l)
vm_id_count=$(qm list | grep -v "VMID" | awk '{print $1}' | grep ${TEMPLATE_ID} | wc -l)
if [ $vm_id_count != $vm_count ]; then
	echo "ERROR: There is a virtual machine with id ${TEMPLATE_ID} but probably not then name ${TEMPLATE_NAME}, aborting." | tee /dev/stderr
	exit 1
fi

if [ $vm_count = 1 ]; then
	echo "* Deleting old template ${TEMPLATE_NAME} with id ${TEMPLATE_ID} after clearing protection"
	if [ "${DRY_RUN}" = "yes" ]; then
		SetVMOption ${TEMPLATE_ID} protection 0
		echo "   DRY-RUN:   qm destroy ${TEMPLATE_ID}"
	else
		SetVMOption ${TEMPLATE_ID} protection 0
		qm destroy ${TEMPLATE_ID}
		if [ $(qm list | grep -v "VMID" | awk '{print $1}' | grep ${TEMPLATE_ID} | wc -l) = 1 ]; then
			echo "ERROR: Unable to delete VM ID: ${TEMPLATE_ID}, aborting." | tee /dev/stderr
			exit 1
		fi
	fi
	echo -e "* Done deleting existing template.nn"
fi

#############################################
# CLONE VM TO TEMPLATE
#############################################
echo "* Creating template clone ${TEMPLATE_NAME} from snapshot ${SNAPSHOT_NAME}"
if [ "${DRY_RUN}" = "yes" ]; then
	echo "   DRY-RUN:   qm clone ${VM_ID} ${TEMPLATE_ID} -description "${TEMPLATE_DESCRIPTION}" -format qcow2 -full 1 -name "${TEMPLATE_NAME}" -pool "${TEMPLATE_POOL}" -snapname "${SNAPSHOT_NAME}" -storage "${TEMPLATE_STORAGE}""
else
	qm clone ${VM_ID} ${TEMPLATE_ID} -description "${TEMPLATE_DESCRIPTION}" -format qcow2 -full 1 -name "${TEMPLATE_NAME}" -pool "${TEMPLATE_POOL}" -snapname "${SNAPSHOT_NAME}" -storage "${TEMPLATE_STORAGE}"
	sleep 10
	if [ $(qm list | grep -v "VMID" | grep "${TEMPLATE_NAME}" | awk '{print $1}' | grep ${TEMPLATE_ID} | wc -l) != 1 ]; then
		echo "ERROR: Unable to clone VM ID: ${VM_ID} to TEMPLATE ID: ${TEMPLATE_ID}, aborting." | tee /dev/stderr
		exit 1
	fi
fi
echo -e "* Done creating template clone ${TEMPLATE_NAME}.nn"

#############################################
# TEMPLATE PREPARATION
#############################################
echo "* Preparing template ${TEMPLATE_NAME}"
if [ "${DRY_RUN}" = "yes" ]; then
	echo "   DRY-RUN:   qm start ${TEMPLATE_ID}"
	echo "   DRY_RUN:   sleep 30"
	echo "   DRY-RUN:   ip=$(qm agent ${TEMPLATE_ID} network-get-interfaces | grep ip-address | grep -o "192.168.[0-9]*.[0-9]*" | head -n 1)"
	echo "   DRY_RUN:   ssh -4 -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -i ~/.ssh/base_id_rsa root@${ip} "/sbin/oem-config-prepare -f""
else
	qm start ${TEMPLATE_ID}
	sleep 30
	ip=$(qm agent ${TEMPLATE_ID} network-get-interfaces | grep ip-address | grep -o "192.168.[0-9]*.[0-9]*" | head -n 1)
	count=0
	while [[ -z $ip ]] && [[ $count -lt 600 ]]
	do
		count=$((count+1))
		echo -n "."
		sleep 1
		ip=$(qm agent ${TEMPLATE_ID} network-get-interfaces | grep ip-address | grep -o "192.168.[0-9]*.[0-9]*" | head -n 1)
	done

	if [ -z $ip ]; then
		echo "ERROR: The machine does not seem to have an IP, maybe boot failed, aborting." | tee /dev/stderr
		exit 1
	fi

	ssh -4 -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -i ${SSH_KEY} root@${ip} "/sbin/oem-config-prepare -f"
fi
echo -e "* Done preparing template ${TEMPLATE_NAME}.nn"

#############################################
# CONVERT TEMPLATE
#############################################
echo "* Converting virtual machine ${TEMPLATE_NAME} to template"
if [ "${DRY_RUN}" = "yes" ]; then
	echo "   DRY-RUN:   qm wait ${TEMPLATE_ID}"
	echo "   DRY-RUN:   qm template ${TEMPLATE_ID}"
else
	qm wait ${TEMPLATE_ID}
	qm template ${TEMPLATE_ID}
fi
echo -e "* Done converting virtual machine ${TEMPLATE_NAME} to template.nn"

echo "Script execution done."

If anyone else have use for this script then go ahead, no guarantees of course, use it on your own risk…