Skip to content

Commit

Permalink
Merge pull request #1 from klauer/testing
Browse files Browse the repository at this point in the history
Add initial implementation to provision TwinCAT/BSD PLCs with ansible
  • Loading branch information
klauer authored Jun 20, 2023
2 parents 751488f + 926c783 commit 977626a
Show file tree
Hide file tree
Showing 16 changed files with 1,154 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.DS_Store
*_rsa
*.pub
# Only include the template:
/host_inventory.yaml
TCBSD*.vdi
TCBSD*.iso
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ repos:
- id: check-xml
- id: check-yaml
- id: debug-statements

- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.2
hooks:
- id: shellcheck
types: [] # Skips non-.sh files without this
files: \.(bashrc|bash_profile|sh)$

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.255
hooks:
- id: ruff
4 changes: 4 additions & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
external-sources=true

disable=SC1091
disable=SC1090
62 changes: 62 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# This is the IP address of the PLC.
PLC_IP ?=
PLC_HOSTNAME ?= my-plcname
PLC_NET_ID ?= $(PLC_IP).1.1
PLC_USERNAME ?= Administrator
SSH_KEY_FILENAME ?= $(shell pwd)/tcbsd_key_rsa

# This auto-detects your local adapter's IP address. It may be completely wrong.
# Change it in your environment (OUR_IP_ADDRESS=... make) or replace the value
# after ?= with your IP.
OUR_IP_ADDRESS ?= $(shell ifconfig | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p' | head -n 1)
OUR_NET_ID ?= $(OUR_IP_ADDRESS).1.1
OUR_ROUTE_NAME ?= $(shell hostname -s)

export PLC_HOSTNAME
export PLC_IP
export PLC_NET_ID
export PLC_USERNAME
export OUR_IP_ADDRESS
export OUR_NET_ID
export OUR_ROUTE_NAME
export SSH_KEY_FILENAME

ifndef PLC_IP
$(error PLC_IP is not set. Set it to the PLC's IP address.)
endif
ifndef PLC_HOSTNAME
$(error PLC_HOSTNAME is not set.)
endif
ifndef OUR_IP_ADDRESS
$(warning OUR_IP_ADDRESS is not set. Will not be able to add route.)
endif

all: ssh-setup run-provision

ssh-setup:
if [ ! -f "$(SSH_KEY_FILENAME)" ]; then \
ssh-keygen -t rsa -f "$(SSH_KEY_FILENAME)"; \
fi
ssh-copy-id -i "$(SSH_KEY_FILENAME)" "$(PLC_USERNAME)@$(PLC_IP)"
$(MAKE) ssh SSH_ARGS='echo "Successfully logged in with the key to $(PLC_IP)"'

ssh:
ssh -i "$(SSH_KEY_FILENAME)" "$(PLC_USERNAME)@$(PLC_IP)" $(SSH_ARGS)

host_inventory.yaml: Makefile host_inventory.yaml.template
# This substitutes our local environment into ``host_inventory.yaml.template``
# and writes ``host_inventory.yaml``
envsubst < "host_inventory.yaml.template" > "$@"

run-bootstrap: host_inventory.yaml tcbsd-bootstrap-playbook.yaml
ansible-playbook tcbsd-bootstrap-playbook.yaml -i host_inventory.yaml

run-provision: run-bootstrap host_inventory.yaml tcbsd-provision-playbook.yaml
ansible-playbook tcbsd-provision-playbook.yaml -i host_inventory.yaml

add-route:
# NOTE: the add_route script lazily uses environment variables instead of
# command-line arguments.
bash add_route.sh

.PHONY: all ssh-setup ssh run-provision run-bootstrap add-route
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,110 @@

### Install requirements

* VirtualBox
* TwinCAT BSD image from Beckhoff
* bash
* ansible
* ``gettext`` to interpolate the host inventory template

### Create a VirtualBox VM

1. Download TwinCAT BSD image from Beckhoff
2. Run ``./create_tc_bsd_vm.sh (plcname) [tcbsd.iso]``

This will generate a VM with:

* 8GB OS primary OS (SATA) disk image
* 1GB of RAM
* One network adapter (NAT -> may require reconfiguration)
* The bootable TwinCAT BSD installation media connected as the second SATA drive
* This will boot until the installation is complete. It can be removed
after installation is done, but it will not interfere with TC/BSD
from booting post-installation even if it remains.

### Deploy useful settings with ansible

* Bootstraps the PLC by installing Python 3.9 (required for ansible)
* Installs system packages that I like and find useful to have everywhere
* Installs system packages specified in the host inventory
* Installs TwinCAT tools and a couple libraries
* Lists all of the available libraries I saw in `pkg search`, with easy uncomment-ability
* Sets an AMS Net ID based on the host inventory
* Sets locked memory size in the TcRegistry based on the host inventory
* (Optionally) Sets heap memory size in the TcRegistry based on the host inventory
* (Optionally) Installs C/C++ development tools
* (Optionally) Changes the default shell for Administrator to `bash`
* (Optionally) Configures initial static routes, by default just adding the
machine from which ansible is being run.
* (Optionally) Configures add or remove a list of static routes with a custom
ansible module ``tcbsd_route`` (see ``library/``).
* Provides a basic bash configuration, which shows the current TC runtime state:
```
[TCBSD: CONFIG] [Administrator@PC-75972A ~]$
[TCBSD: RUN] [Administrator@PC-75972A ~]$
```
* Enable color in ``ls`` and bash tab completion, if using bash.
* Adds a "site" firewall (pf = packet filter) configuration which lets through insecure ADS
* Reloads the packet filter if the configuration was changed
* Restarts the TwinCAT service if any TcRegistry changes were made


### Sample session

1. Install requirements
2. Download TwinCAT BSD image from Beckhoff and put it in the same directory as
these scripts.
3. Pick a name for the VM: let's choose ``"tcbsd-a"``.
4. Run ``./create_tc_bsd_vm.sh tcbsd-a``
5. Open ``"tcbsd-a.vbox"`` in VirtualBox. Start it and run the installation.
a. Select "Install"
b. OK the overwrite of the disk.
c. Set "1" as the password for Administrator and confirm it. (or better
yet, set it according to good password standards and change `1` in the
configuration files)
d. Reboot
6. Update the VM network settings based on where you want to use it and make note
of its IP address.
a. The default setting is NAT, but you should consider switching it to
host-only (or even bridged to allow usage of the VM on your local network;
that's up to you).
b. See also [here](https://infosys.beckhoff.com/english.php?content=../content/1033/twincat_bsd/5620035467.html&id=)
c. Check the VM IP address (log in and run ``ifconfig``)
7. Edit ``Makefile`` to set appropriate ``PLC_IP`` (192.168.2.232 in our case)
a. Alternatively, you can just set it in your environment:
```
$ export PLC_IP=192.168.2.232
$ export PLC_NET_ID=...
```
8. Run ``make`` to pre-configure SSH communication with the VM and then the playbook. (*)
a. Log in to the PLC when asked. The generated SSH key will be used in the
remaining steps.
9. Launch TwinCAT XAE and add a route to your PLC (if on Linux/macOS, you can
also use adstool/ads-async via ``make add-route`` if the auto-generated
``StaticRoutes.xml`` is insufficient)
10. You can use the shortcut ``make ssh`` after this point to log into the PLC
with the generated key.

(*) The ``make`` steps, if too magical, can be broken down a bit further.
Run:

1. ``make ssh-setup`` (SSH key + initial login)
2. ``make host_inventory.yaml`` (create host inventory configuration file)
3. ``make run-bootstrap`` (install Python on the PLC, required for ansible)
4. ``make run-provision`` (provision the PLC)


## Side notes

### ADS

You can download the ADS library source code, which comes with adstool, from
[here](https://github.com/Beckhoff/ADS/).

#### Trouble building adstool on macOS due to clang and C++14 standards being used?

Try:
```bash
$ meson setup build -Dcpp_std=c++14
$ make
```
41 changes: 41 additions & 0 deletions add_route.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash

if [ -z "$OUR_IP_ADDRESS" ]; then
echo "This script is intended to be run from the Makefile as it expects a" 2>/dev/stderr
echo "certain set of variables to be set in advance. Sorry!" 2>/dev/stderr
exit 1
fi

echo "Your local IP is set as: ${OUR_IP_ADDRESS} (**)"
echo "PLC IP address: ${PLC_IP}"
echo "Local AMS net ID: ${OUR_NET_ID}"
echo "PLC username: ${PLC_USERNAME}"
echo "PLC route name: ${OUR_ROUTE_NAME}"
echo
echo "** If using the auto-detection from the Makefile, it may be wrong."
echo " Please set OUR_IP_ADDRESS specifically before running this."
echo
read -p "Enter password for adding PLC route: " -rs PLC_PASSWORD ;
echo

set -e

if command -v adstool &> /dev/null; then
echo "Found adstool, using it..."
adstool "${PLC_IP}" --localams="${OUR_NET_ID}" --log-level=0 addroute \
--addr="${OUR_IP_ADDRESS}" --netid="${OUR_NET_ID}" \
--username="${PLC_USERNAME}" --password="${PLC_PASSWORD}" \
--routename="${OUR_ROUTE_NAME}";
elif command -v ads-async &> /dev/null; then
echo "Found ads-async, using it..."
echo "PLC information:"
ads-async info "${PLC_IP}"
ADS_ASYNC_LOCAL_IP="${OUR_IP_ADDRESS}" ADS_ASYNC_LOCAL_NET_ID="${OUR_NET_ID}" \
ads-async route --route-name "${OUR_ROUTE_NAME}" \
--username "${PLC_USERNAME}" --password "${PLC_PASSWORD}" \
"${PLC_IP}" "${OUR_NET_ID}" "${OUR_IP_ADDRESS}";
else
echo "No ads tools found to get PLC info / add route"
fi
echo "Added a route to the PLC named ${OUR_ROUTE_NAME}:"
echo " ${OUR_IP_ADDRESS} - ${OUR_NET_ID} <-> ${PLC_IP} ${PLC_NET_ID}"
118 changes: 118 additions & 0 deletions create_tc_bsd_vm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
#
# by @klauer, loosely based on https://github.com/PTKu/TwinCAT-BSD-VM-creator

set -e

usage () {
echo "Usage: $0 vm_name [tcbsd_iso_image]" >&2
exit 0
}

die () {
echo "$@" >&2
exit 1
}

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
VM_NAME=$1
TCBSD_ISO_IMAGE=$2
VM_HDD="$PWD/$VM_NAME/$1.vhd"

if [[ $# -lt 1 || $# -gt 2 || -z "$VM_NAME" ]]; then
usage
fi

if [ -z "$TCBSD_ISO_IMAGE" ]; then
# shellcheck disable=SC2012
TCBSD_ISO_IMAGE=$(ls "${SCRIPT_DIR}/TCBSD"*.iso | head -n 1)
fi

echo "* VM name: ${VM_NAME}"
echo "* TcBSD ISO: ${TCBSD_ISO_IMAGE}"
echo "* Disk name: ${VM_NAME}"

if [ ! -f "$TCBSD_ISO_IMAGE" ]; then
die "
* TcBsd ISO image not found. Download it here:
-> https://www.beckhoff.com/en-en/products/ipc/software-and-tools/operating-systems/c9900-s60x-cxxxxx-0185.html
"
fi

if ! command -v VBoxManage &>/dev/null; then
die "VirtualBox installation not found."
fi

TCBSD_VDI_IMAGE="$SCRIPT_DIR/$(basename "${TCBSD_ISO_IMAGE%.*}").vdi"

echo "* TcBSD VirtualBox-specific VDI: ${TCBSD_VDI_IMAGE}"

vbox_manage() {
echo "Running: VBoxManage $*"
VBoxManage "$@"
EXIT_CODE=$?
if [ $EXIT_CODE -gt 0 ]; then
echo "VBoxManage reported exit code $EXIT_CODE; exiting early."
exit "$EXIT_CODE"
fi
echo ""
}

show_vm_info() {
VBoxManage showvminfo "$1"
}

set +e

if [ ! -f "$TCBSD_VDI_IMAGE" ]; then
echo "* Converting ISO to TcBSD VirtualBox-specific VDI"
vbox_manage convertfromraw --format VDI "$TCBSD_ISO_IMAGE" "$TCBSD_VDI_IMAGE" 2>&1
fi

if show_vm_info "$VM_NAME" &> /dev/null; then
echo "VBoxManage reports that the VM ${VM_NAME} already exists."
read -rp "Delete it? [yN]" yn

if [[ "$yn" != "y" ]]; then
echo "VM already exists; cannot continue" >/dev/stderr
exit 1
fi

echo "Unregistering ${VM_NAME} from VirtualBox and moving files to a backup directory."
set -x
vbox_manage unregistervm "$VM_NAME"
mv "$VM_NAME" "${VM_NAME}.old.$(date +%s)"
set +x
fi

echo "* Creating the VM"
vbox_manage createvm --name "$VM_NAME" --basefolder "$PWD" --ostype FreeBSD_64 --register

echo "* Setting VM basic settings"
vbox_manage modifyvm "$VM_NAME" --memory 1024 --vram 128 --acpi on --hpet on --graphicscontroller vmsvga --firmware efi64

echo "* Setting VM storage settings"
vbox_manage storagectl "$VM_NAME" --name SATA --add sata --controller IntelAhci --hostiocache on --bootable on

echo "* Attaching to installation HDD to SATA Port 1"
vbox_manage storageattach "$VM_NAME" --storagectl "SATA" --device 0 --port 1 --type hdd --medium "$TCBSD_VDI_IMAGE"

echo "* Creating an empty 8GB disk image for the TwinCAT BSD installation"
vbox_manage createmedium --filename "$VM_HDD" --size 8192 --format VHD 2>&1

echo "* Attaching the empty disk image to SATA Port 0"
vbox_manage storageattach "$VM_NAME" --storagectl "SATA" --device 0 --port 0 --type hdd --medium "$VM_HDD"

if command -v xdg-open &>/dev/null; then
OPEN="xdg-open"
elif command -v open &>/dev/null; then
OPEN="open"
elif command -v start.exe &>/dev/null; then
OPEN="start.exe"
else
echo "Created VM but unable to figure out how to show it to you. Look in $PWD/$VM_NAME"
ls "$PWD/$VM_NAME"
exit 0
fi

"$OPEN" "$PWD/$VM_NAME"
Loading

0 comments on commit 977626a

Please sign in to comment.