diff --git a/.github/workflows/git-auto-issue-branch-creation.yml b/.github/workflows/git-auto-issue-branch-creation.yml index a6d9ae4..413e006 100644 --- a/.github/workflows/git-auto-issue-branch-creation.yml +++ b/.github/workflows/git-auto-issue-branch-creation.yml @@ -6,6 +6,10 @@ on: jobs: create-issue-branch: runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + issues: write steps: - name: Checkout code uses: actions/checkout@v4 @@ -22,7 +26,7 @@ jobs: # Given an issue title: "Fix the 2'nd bug in UI where there's a # in the form" # Becomes: # - # Note there is a space here, to keep both '-' and spaces ' ' + # Note there is a space here, to keep both '-' and spaces ' ' echo ISSUE_BRANCH_NAME=`echo "${{ github.event.issue.title }}" | \ # Remove non alpha/numeric chars tr -cd '[:alnum:]- ' | \ @@ -41,3 +45,12 @@ jobs: git checkout -b "${{ steps.issue.outputs.number }}-${{ env.ISSUE_BRANCH_NAME }}" git push -u origin HEAD + - name: Creating PR based on branch name ${{ github.ref_name }} + run: | + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/Subscribie/subscribie/pulls \ + -d '{"title": "#${{ steps.issue.outputs.number }} ${{ env.ISSUE_BRANCH_NAME }}","body":"Pull request related issue: #${{ steps.issue.outputs.number }}. Please pull these awesome changes in!","head":"${{ steps.issue.outputs.number }}-${{ env.ISSUE_BRANCH_NAME }}","base":"master"}' diff --git a/.gitignore b/.gitignore index 0ba8554..f517a57 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ node_modules test-results/ playwright-report/ playwright/.cache/ +key* +deploy/serve/www/* +!deploy/serve/www/bootfile diff --git a/recieve-repo-server-bootstrap-ncl-repo/.gitkeep b/recieve-repo-server-bootstrap-ncl-repo/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/repo-server-bootstrap-ncl-issue-20/README.md b/repo-server-bootstrap-ncl-issue-20/README.md new file mode 100644 index 0000000..2a62114 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/README.md @@ -0,0 +1,50 @@ +# Overview + +> [!NOTE] +> This repo & docs is very much in flux. +> For project purpose see [Server Bootstrap Project Concise project summary & deliverables](https://docs.google.com/document/d/15YR0hAkHfq8g_2rFzpKjpKf1mytyygREyR7a1ZWfzhE/edit?usp=sharing) +> For background reading also see [https://github.com/KarmaComputing/server-bootstrap](https://github.com/KarmaComputing/server-bootstrap) + +![diagram](docs/diagram.drawio.png) + + +## Table of contents + +- [Alpine Mirroring](./deploy/alpine-mirror/README.md) + + + +## Run for debugging + +- In `internal/runner` + + ```sh + URL=https://192.168.0.230 USERNAME=Administrator PASSWORD=A0F7HKUU VALIDCERT=false WIPEINTERVAL=300 go run . + ``` + +## Full deploy + +1. Build + - Building ipxe.iso image + - Use new/existing iPXE config file in `deploy/build/ipxe/scripts` + - Input its name as FILE variable in `deploy/scripts/build-ipxe-iso.sh` + - Run `deploy/scripts/build-ipxe-iso.sh` + - `ipxe.iso` is placed in `deploy/serve/www` + - Building alpine-netboot image + - Run `deploy/scripts/build-alpine.sh` + - Files are placed in `deploy/serve/www/iso` + - SSH keys + - An SSH keypair is automatically generated upon building an `ipxe.iso` image with the above command + - The **private** key is placed at `deploy/ssh/key` + - The **public** key is placed at `deploy/serve/www/ssh/key.pub` + - Up **re**building the `ipxe.iso`, the script will prompt to replace these keys or not + +2. Run stack + - Ensure the files are correctly placed from step 1 + - `podman compose up -d` in repository root + +3. !! VM FOR TESTING !! + - Ensure `qemu` is installed and runnable + - Ensure web server is accessible at whatever address is defined in the iPXE boot script + - `qemu-system-x86_64 -cdrom -net nic -net user,hostfwd=tcp::2223-:22 -m 3072 -smp $(nproc)` + - VM can be accessed over SSH at `localhost:2223` \ No newline at end of file diff --git a/repo-server-bootstrap-ncl-issue-20/compose.yaml b/repo-server-bootstrap-ncl-issue-20/compose.yaml new file mode 100644 index 0000000..af424a2 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/compose.yaml @@ -0,0 +1,21 @@ +services: + controller: + build: ./internal/runner + # ports: + # - 8081:8080 + volumes: + - ./deploy/ssh/key:/app/key:z + - ./deploy/ansible:/app/ansible:z + environment: + - LC_ALL=en_US.UTF-8 + - USERNAME=Administrator + - PASSWORD=A0F7HKUU + - URL=https://192.168.0.230 + - VALIDCERT=false + - WIPEINTERVAL=300 + apache: + build: ./deploy/serve + volumes: + - ./deploy/serve/www:/usr/local/apache2/htdocs:z + ports: + - 8080:80 diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/Containerfile b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/Containerfile new file mode 100644 index 0000000..30ebd9d --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/Containerfile @@ -0,0 +1,10 @@ +FROM docker.io/library/alpine:3.20 + +RUN apk add rsync + +COPY ./synchroniser /app +RUN cat /app/crontab >> /etc/crontabs/root + +RUN chmod +x /app/mirror.sh + +CMD ["crond", "-f"] \ No newline at end of file diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/README.md b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/README.md new file mode 100644 index 0000000..fd818c6 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/README.md @@ -0,0 +1,23 @@ +# How to Use + +- Adjust `synchroniser/crontab` for desired schedule of synchronising +- Adjust `synchroniser/mirror.sh` for which repos to exclude, etc. +- The container image must be re-built when `crontab` or `mirror.sh` are changed +- Run by ` compose up` + +# Notes + +- [Alpine Wiki - How to setup a Mirror](https://wiki.alpinelinux.org/wiki/How_to_setup_a_Alpine_Linux_mirror) +- You can trigger the syncs manually via + - ` exec -it alpine-mirror-sync /app/mirror.sh` + +# How I've Tested It + +1. `git clone git@github.com:KarmaComputing/server-bootstrap-ncl.git` +2. `cd server-bootstrap-ncl/deploy/alpine-mirror` +3. `sudo docker compose up` +4. `sudo docker run --rm -it --network host alpine:3.21 sh` + - `echo -e "http://localhost/v3.21/main\nhttp://localhost/v3.21/community" > /etc/apk/repositories` + - `apk update` + - Feel free to install anything to test, with `apk add` + - e.g. `apk add fastfetch` and `fastfetch` \ No newline at end of file diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/compose.yaml b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/compose.yaml new file mode 100644 index 0000000..7848bc6 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/compose.yaml @@ -0,0 +1,13 @@ +services: + sync: + container_name: alpine-mirror-sync + build: . + volumes: + - ./repo:/site:z + server: + container_name: alpine-mirror-server + image: docker.io/library/httpd:alpine + volumes: + - ./repo:/usr/local/apache2/htdocs:z + ports: + - 8080:80 \ No newline at end of file diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/repo/.gitignore b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/repo/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/repo/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/synchroniser/crontab b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/synchroniser/crontab new file mode 100644 index 0000000..f1dcd1e --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/synchroniser/crontab @@ -0,0 +1,2 @@ +# every 5 days +* * */5 * * sh /app/mirror.sh diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/synchroniser/mirror.sh b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/synchroniser/mirror.sh new file mode 100755 index 0000000..be3ac37 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/alpine-mirror/synchroniser/mirror.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +# make sure we never run 2 rsync at the same time +lockfile="/tmp/alpine-mirror.lock" +if [ -z "$flock" ] ; then + exec env flock=1 flock -n $lockfile "$0" "$@" +fi + +src=rsync://rsync.alpinelinux.org/alpine/ +dest=/site + +exclude="--exclude v2.* --exclude v3.0 --exclude v3.1 --exclude v3.2 --exclude v3.3 --exclude v3.4 --exclude v3.5 --exclude v3.6 --exclude v3.7 --exclude v3.8 --exclude v3.9 --exclude v3.10 --exclude v3.11 --exclude v3.12 --exclude v3.13 --exclude v3.14 --exclude v3.15 --exclude v3.16 --exclude v3.17" + +echo "--- Starting Sync ---" + +mkdir -p "$dest" +/usr/bin/rsync \ + --archive \ + --update \ + --hard-links \ + --delete \ + --delete-after \ + --delay-updates \ + --timeout=600 \ + $exclude \ + "$src" "$dest" + +echo "--- Finished Sync ---" \ No newline at end of file diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/ansible/playbook.yml b/repo-server-bootstrap-ncl-issue-20/deploy/ansible/playbook.yml new file mode 100644 index 0000000..bb2b500 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/ansible/playbook.yml @@ -0,0 +1,13 @@ +--- +- name: Test + hosts: all + gather_facts: false + + tasks: + - name: Install python + raw: test -e /usr/bin/python3 || apk add --no-cache python3 + + - name: Gather facts + setup: + vars: + ansible_python_interpreter: /usr/bin/python3 diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/build/alpine/Containerfile b/repo-server-bootstrap-ncl-issue-20/deploy/build/alpine/Containerfile new file mode 100644 index 0000000..133abbc --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/build/alpine/Containerfile @@ -0,0 +1,21 @@ +FROM alpine:latest + +RUN apk update && apk add --no-cache alpine-sdk \ + build-base \ + apk-tools \ + busybox \ + fakeroot \ + syslinux \ + xorriso \ + squashfs-tools \ + sudo \ + git \ + grub \ + grub-efi + +WORKDIR /build +COPY build.sh . + +RUN chmod +x build.sh + +CMD ["sh", "/build/build.sh"] diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/build/alpine/build.sh b/repo-server-bootstrap-ncl-issue-20/deploy/build/alpine/build.sh new file mode 100644 index 0000000..e331ccc --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/build/alpine/build.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +# +# !!!!! +# THIS SCRIPT IS NOT MEANT TO BE MANUALLY RUN, USE THE ADJACENT CONTAINERFILE +# !!!!! +# + +# https://github.com/KarmaComputing/server-bootstrap/blob/main/build-alpine-netboot-zfs.sh +# Purpose: +# Build netboot image with zfs kernel module included + +set -x + +# Note we now build alpine-conf from source (rather than doing apk add alpine-conf) +# due to issue https://github.com/KarmaComputing/server-bootstrap/issues/20 + +# Clone and build latest alpine-conf +git clone https://gitlab.alpinelinux.org/alpine/alpine-conf.git +cd alpine-conf +make +make install +cd - + +# Start build +adduser build --disabled-password -G abuild +# Set password non interactively +echo -e "password\npassword" | passwd build +echo "build ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/abuild + +su - build << 'EOF' +set -x +SUDO=sudo abuild-keygen -n -i -a +# aports contains build utilities such as mkimage.sh +git clone --depth 1 https://gitlab.alpinelinux.org/alpine/aports.git +cd aports + +# Create & build alpine netboot profile with zfs kernel module enabled +cat > ./scripts/mkimg.zfsnetboot.sh << 'EOFINNER' + +profile_zfsnetboot() { + profile_standard + kernel_cmdline="overlay_size=0 console=tty0 console=ttyS0,115200" + syslinux_serial="0 115200" + kernel_addons="zfs" + apks="$apks zfs-scripts zfs zfs-utils-py python3 mkinitfs syslinux util-linux linux-firmware" + initfs_features="base network squashfs usb virtio" + output_format="netboot" + image_ext="tar.gz" +} +EOFINNER + +cat ./scripts/mkimg.zfsnetboot.sh +echo Running mkimage.sh +./scripts/mkimage.sh --arch x86_64 --repository https://dl-cdn.alpinelinux.org/alpine/v3.20/main --profile zfsnetboot +EOF + +ls -la /home/build/aports +cp /home/build/aports/alpine-zfsnetboot-*.tar.gz /output --force +tar -xvf /home/build/aports/alpine-zfsnetboot-*.tar.gz --directory /output +chmod 0644 /output/boot/* # fix permissions, specifically for initramfs for some reason +exit diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/build/ipxe/Containerfile b/repo-server-bootstrap-ncl-issue-20/deploy/build/ipxe/Containerfile new file mode 100644 index 0000000..06cfe05 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/build/ipxe/Containerfile @@ -0,0 +1,34 @@ +FROM alpine:latest + +RUN apk update && apk add --no-cache build-base \ + git \ + gcc \ + binutils \ + make \ + perl \ + xz-dev \ + mtools \ + syslinux \ + xorriso \ + curl \ + openssl \ + coreutils + +WORKDIR /build + +RUN git clone https://github.com/ipxe/ipxe.git +WORKDIR /build/ipxe/src + +RUN curl -s http://ca.ipxe.org/ca.crt > ca.pem &&\ + curl -s https://letsencrypt.org/certs/isrgrootx1.pem > isrgrootx1.pem &&\ + curl -s https://letsencrypt.org/certs/lets-encrypt-r3.pem > lets-encrypt-r3.pem + +RUN sed -i 's$//#define PING_CMD$#define PING_CMD$g' config/general.h &&\ + sed -i 's$//#define NET_PROTO_IPV6$#define NET_PROTO_IPV6$g' config/general.h &&\ + sed -i 's/undef.*DOWNLOAD_PROTO_HTTPS/define DOWNLOAD_PROTO_HTTPS/g' config/general.h + +CMD make -j${ISO_MAKE_THREADS} bin/ipxe.iso EMBED=/input/${FILE} \ + DEBUG=tls,httpcore,x509,certstore \ + CERT=ca.pem,isrgrootx1.pem,lets-encrypt-r3.pem \ + TRUST=ca.pem,isrgrootx1.pem,lets-encrypt-r3.pem \ + && mv bin/ipxe.iso /output/ipxe.iso diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/build/ipxe/scripts/alpinebooter.ipxe b/repo-server-bootstrap-ncl-issue-20/deploy/build/ipxe/scripts/alpinebooter.ipxe new file mode 100644 index 0000000..94ec34b --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/build/ipxe/scripts/alpinebooter.ipxe @@ -0,0 +1,8 @@ +#!ipxe + +echo Setting temporary DHCP address to chain into bootfile +dhcp + +set chain-url http://192.168.0.170:8080/bootfile +echo Chaining to ${chain_url} +chain ${chain-url}?uuid=${uuid} diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/build/ipxe/scripts/vm-test.ipxe b/repo-server-bootstrap-ncl-issue-20/deploy/build/ipxe/scripts/vm-test.ipxe new file mode 100644 index 0000000..8e84458 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/build/ipxe/scripts/vm-test.ipxe @@ -0,0 +1,23 @@ +#!ipxe + +# The iPXE script below is based on: +# https://boot.alpinelinux.org/boot.ipxe + +dhcp + +set console console=tty0 +set cmdline modules=loop,squashfs nomodeset +set branch v3.20 +set flavor lts +set arch x86_64 + +set serverip 192.168.1.29:8080 +set server-url http://${serverip} +set base-url ${server-url}/iso/alpine-netboot/boot +set repo-url http://dl-cdn.alpinelinux.org/alpine/${branch}/main +set sshkey-url ${server-url}/ssh/key.pub + +imgfree +kernel ${base-url}/vmlinuz-${flavor} ${cmdline} ${console} ip=dhcp alpine_repo=${repo-url} modloop=${base-url}/modloop-${flavor} alpine_dev=tmpfs ssh_key=${sshkey-url} +initrd ${base-url}/initramfs-${flavor} +boot diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/scripts/build-alpine.sh b/repo-server-bootstrap-ncl-issue-20/deploy/scripts/build-alpine.sh new file mode 100755 index 0000000..6a4bb83 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/scripts/build-alpine.sh @@ -0,0 +1,18 @@ +#!/bin/bash +cd "$(dirname "$0")" + +set -exu + +PODMAN_IMAGE_NAME="alpine_builder" +WWW_DIR="../serve/www/iso/alpine-netboot" +BUILD_DIR="../build/alpine" + +echo "--- Creating directory at ${WWW_DIR} ---" +mkdir -p ${WWW_DIR} + +echo "--- Building ${PODMAN_IMAGE_NAME} ---" +podman build --tag ${PODMAN_IMAGE_NAME} ${BUILD_DIR} + +echo "--- Running ${PODMAN_IMAGE_NAME} ---" +podman run --rm -v ${WWW_DIR}:/output:z \ + localhost/${PODMAN_IMAGE_NAME}:latest diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/scripts/build-ipxe-iso.sh b/repo-server-bootstrap-ncl-issue-20/deploy/scripts/build-ipxe-iso.sh new file mode 100755 index 0000000..20f9fa0 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/scripts/build-ipxe-iso.sh @@ -0,0 +1,46 @@ +#!/bin/bash +cd "$(dirname "$0")" + +set -exu + +FILE="alpinebooter.ipxe" + +WWW_DIR="../serve/www" +BUILD_DIR="../build/ipxe" +PODMAN_IMAGE_NAME="ipxe_builder" +SSH_KEY_DIR="../ssh" +ISO_MAKE_THREADS=16 + +echo "--- Creating serve directory at ${WWW_DIR} ---" +mkdir -p ${WWW_DIR} + +echo "--- Creating ssh key directory at ${SSH_KEY_DIR} ---" +mkdir -p ${SSH_KEY_DIR} + +echo "--- Generating new SSH key pair in ${SSH_KEY_DIR} ---" +ssh-keygen -t rsa -f ${SSH_KEY_DIR}/key -N "" + +echo "--- Copying public SSH key to ${WWW_DIR} ---" +mkdir -p ${WWW_DIR}/ssh +mv -f ${SSH_KEY_DIR}/key.pub ${WWW_DIR}/ssh/key.pub + +echo "--- Setting correct permissions for private key ---" +chmod 600 ${SSH_KEY_DIR}/key + +# echo "--- Building ${PODMAN_IMAGE_NAME} ---" +# podman build \ +# --tag ${PODMAN_IMAGE_NAME} \ +# ${BUILD_DIR} + +if [ $? -neq 0 ]; then + echo "!!! BUILD FAILED, EXITING !!!" + exit +fi + +echo "--- Building ipxe.iso with ${FILE} embedded, writing to ${WWW_DIR} (using ${ISO_MAKE_THREADS} threads) ---" +podman run \ + --rm \ + -e FILE="$FILE" \ + --volume ${BUILD_DIR}/scripts:/input:z \ + --volume ${WWW_DIR}:/output:z \ + localhost/${PODMAN_IMAGE_NAME}:latest diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/serve/Containerfile b/repo-server-bootstrap-ncl-issue-20/deploy/serve/Containerfile new file mode 100644 index 0000000..560d914 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/serve/Containerfile @@ -0,0 +1,5 @@ +FROM docker.io/library/httpd:latest + +EXPOSE 80 + +CMD ["httpd", "-D", "FOREGROUND"] diff --git a/repo-server-bootstrap-ncl-issue-20/deploy/serve/www/bootfile b/repo-server-bootstrap-ncl-issue-20/deploy/serve/www/bootfile new file mode 100644 index 0000000..fc5db08 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/deploy/serve/www/bootfile @@ -0,0 +1,49 @@ +#!ipxe + +# The iPXE script below is based on: +# https://boot.alpinelinux.org/boot.ipxe +# https://wiki.alpinelinux.org/wiki/PXE_boot + +# --- Network configuration (static IP) --- +set static-iface +set static-ip 192.168.0.248 +set static-gw 192.168.0.1 +set static-mask 255.255.255.0 +set static-dns 194.168.4.123 + +# --- Alpine version info --- +set alpine-branch v3.20 +set alpine-flavour lts +set arch x86_64 + +# --- iPXE boot sources --- +set web-server http://192.168.0.170:8080 +set boot-base-url ${web-server}/iso/alpine-netboot/boot +set kernel-url ${boot-base-url}/vmlinuz-${alpine-flavour} +set modloop-url ${boot-base-url}/modloop-${alpine-flavour} +set initramfs-image initramfs-${alpine-flavour} +set initramfs-url ${boot-base-url}/${initramfs-image} +set sshkey-url ${web-server}/ssh/key.pub +set alpine-repo http://dl-cdn.alpinelinux.org/alpine/${alpine-branch}/main +#set alpine-repo http://alpine.mirror.karmacomputing.co.uk/${alpine-branch}/main + +# --------------------------- +# | Boot script start | +# --------------------------- + +# --- Configure network for PXE --- +set pxe-iface net0 +echo Configuring iface for PXE (${pxe-iface}: ip=${static-ip},mask=${static-mask},gw=${static-gw},dns=${static-dns}) +ifopen ${pxe-iface} +set ${pxe-iface}/ip:ipv4 ${static-ip} +set ${pxe-iface}/netmask:ipv4 ${static-mask} +set ${pxe-iface}/gateway:ipv4 ${static-gw} +set ${pxe-iface}/dns:ipv4 ${static-dns} +ifstat + +imgfree + +initrd ${initramfs-url} +kernel ${kernel-url} console=tty0 modules=loop,squashfs nomodeset ip=${static-ip}::${static-gw}:${static-mask}::${static-iface}:none:${static-dns} alpine_repo=${alpine-repo} modloop=${modloop-url} ssh_key=${sshkey-url} + +boot diff --git a/repo-server-bootstrap-ncl-issue-20/docs/diagram.drawio b/repo-server-bootstrap-ncl-issue-20/docs/diagram.drawio new file mode 100644 index 0000000..c4f0651 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/docs/diagram.drawio @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/repo-server-bootstrap-ncl-issue-20/docs/diagram.drawio.png b/repo-server-bootstrap-ncl-issue-20/docs/diagram.drawio.png new file mode 100644 index 0000000..323723b Binary files /dev/null and b/repo-server-bootstrap-ncl-issue-20/docs/diagram.drawio.png differ diff --git a/repo-server-bootstrap-ncl-issue-20/internal/runner/Containerfile b/repo-server-bootstrap-ncl-issue-20/internal/runner/Containerfile new file mode 100644 index 0000000..63ea0b5 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/internal/runner/Containerfile @@ -0,0 +1,12 @@ +# Builder stage +FROM docker.io/library/golang:latest as builder +WORKDIR /src +COPY . . +RUN go build -o /app/main + +# Final stage +FROM docker.io/library/alpine:latest +WORKDIR /app +COPY --from=builder /app . +RUN apk update && apk add --no-cache ansible gcompat openssh +ENTRYPOINT ["/app/main"] # Entry point diff --git a/repo-server-bootstrap-ncl-issue-20/internal/runner/go.mod b/repo-server-bootstrap-ncl-issue-20/internal/runner/go.mod new file mode 100644 index 0000000..4dbbbc5 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/internal/runner/go.mod @@ -0,0 +1,10 @@ +module server_bootstrap + +go 1.24.3 + +require ( + github.com/stmcginnis/gofish v0.20.0 + golang.org/x/crypto v0.38.0 +) + +require golang.org/x/sys v0.33.0 // indirect diff --git a/repo-server-bootstrap-ncl-issue-20/internal/runner/go.sum b/repo-server-bootstrap-ncl-issue-20/internal/runner/go.sum new file mode 100644 index 0000000..2d6e02e --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/internal/runner/go.sum @@ -0,0 +1,8 @@ +github.com/stmcginnis/gofish v0.20.0 h1:hH2V2Qe898F2wWT1loApnkDUrXXiLKqbSlMaH3Y1n08= +github.com/stmcginnis/gofish v0.20.0/go.mod h1:PzF5i8ecRG9A2ol8XT64npKUunyraJ+7t0kYMpQAtqU= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= diff --git a/repo-server-bootstrap-ncl-issue-20/internal/runner/lom/lom.go b/repo-server-bootstrap-ncl-issue-20/internal/runner/lom/lom.go new file mode 100644 index 0000000..a5360ce --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/internal/runner/lom/lom.go @@ -0,0 +1,141 @@ +package lom + +import ( + "fmt" + + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" +) + +type SystemPowerState string + +const ( + PowerStateUnknown SystemPowerState = "" + PowerStateOn SystemPowerState = "On" + PowerStateOff SystemPowerState = "Off" +) + +type LOM struct { + gofish.APIClient + systems []*redfish.ComputerSystem +} + +func NewFromConfig(clientConfig *gofish.ClientConfig) (*LOM, error) { + apiClient, err := gofish.Connect(*clientConfig) + if err != nil { + return nil, err + } + + systems, err := apiClient.Service.Systems() + if err != nil { + return nil, err + } + + return &LOM{ + APIClient: *apiClient, + systems: systems, + }, nil +} + +func (lom *LOM) AllSystems() *[]*redfish.ComputerSystem { + return &lom.systems +} + +func (lom *LOM) SystemsCount() int { + return len(lom.systems) +} + +func (lom *LOM) SystemPowerOn(systemNumber int) error { + system, err := lom.GetSystem(systemNumber) + if err != nil { + return err + } + + err = system.Reset(redfish.OnResetType) + if err != nil { + return err + } + + return nil +} + +func (lom *LOM) SystemPowerOff(systemNumber int) error { + system, err := lom.GetSystem(systemNumber) + if system != nil { + return err + } + + err = system.Reset(redfish.ForceOffResetType) + if err != nil { + return err + } + + return nil +} + +func (lom *LOM) SystemPowerToggle(systemNumber int) error { + isPoweredOn, err := lom.IsSystemPoweredOn(systemNumber) + if err != nil { + return err + } + + if isPoweredOn { + return lom.SystemPowerOff(systemNumber) + } else { + return lom.SystemPowerOn(systemNumber) + } +} + +func (lom *LOM) SystemRestart(systemNumber int) error { + system, err := lom.GetSystem(systemNumber) + if system == nil { + return err + } + + err = system.Reset(redfish.ForceRestartResetType) + if err != nil { + return err + } + + return nil +} + +func (lom *LOM) GetSystem(systemNumber int) (*redfish.ComputerSystem, error) { + if systemNumber < 0 || systemNumber >= lom.SystemsCount() { + return nil, fmt.Errorf("This LOM does not have %d systems", systemNumber) + } + + system := lom.systems[systemNumber] + return system, nil +} + +func (lom *LOM) SystemPowerState(systemNumber int) (SystemPowerState, error) { + system, err := lom.GetSystem(systemNumber) + if err != nil { + return PowerStateUnknown, err + } + + return SystemPowerState(system.PowerState), nil +} + +func (lom *LOM) IsSystemPoweredOn(systemNumber int) (bool, error) { + system, err := lom.GetSystem(systemNumber) + if err != nil { + return false, err + } + + return SystemPowerState(system.PowerState) == PowerStateOn, nil +} + +func (lom *LOM) IsSystemPoweredOff(systemNumber int) (bool, error) { + system, err := lom.GetSystem(systemNumber) + if err != nil { + return false, err + } + + return SystemPowerState(system.PowerState) == PowerStateOff, nil +} + +func (lom *LOM) Logout() { + lom.APIClient.Logout() +} diff --git a/repo-server-bootstrap-ncl-issue-20/internal/runner/main.go b/repo-server-bootstrap-ncl-issue-20/internal/runner/main.go new file mode 100644 index 0000000..64c3623 --- /dev/null +++ b/repo-server-bootstrap-ncl-issue-20/internal/runner/main.go @@ -0,0 +1,184 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "server_bootstrap/lom" + "strconv" + "strings" + "time" + + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/redfish" + "golang.org/x/crypto/ssh" +) + +type Config struct { + gofish.ClientConfig + wipeInterval int +} + +func main() { + config := tryGetConfigFromEnvironment() + + lom, err := lom.NewFromConfig(&config.ClientConfig) + if err != nil { + log.Fatal(err) + } + defer lom.Logout() + + err = bootstrap(lom) + if err != nil { + log.Fatal(err) + } +} + +func awaitAndConnectSSH(signer ssh.Signer, user, addr string) (*ssh.Client, error) { + config := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: Verify host? Not sure if that is possible (ssh.FixedHostKey()) + Timeout: 5 * time.Second, + } + + var conn *ssh.Client + for { + c, err := ssh.Dial("tcp", addr, config) + if err == nil { + conn = c + break + } + time.Sleep(5 * time.Second) + fmt.Println("Failed connection. Retrying...") + } + + return conn, nil +} + +func tryGetConfigFromEnvironment() *Config { + urlAddress := os.Getenv("URL") + if urlAddress == "" { + log.Fatal("URL is not set, expects format e.g. 'https://192.168.0.230'") + } + + username := os.Getenv("USERNAME") + if username == "" { + log.Fatal("USERNAME is not set, expects string") + } + + password := os.Getenv("PASSWORD") + if password == "" { + log.Fatal("PASSWORD is not set, expects string") + } + + var validCertOnly bool + { + env := os.Getenv("VALIDCERT") + if env == "" { + log.Fatal("VALIDCERT is not set, expects boolean") + } + switch strings.ToLower(env) { + case "true": + validCertOnly = true + case "false": + validCertOnly = false + default: + log.Fatal("validCert is not ") + } + } + + var wipeInterval int + { + env := os.Getenv("WIPEINTERVAL") + if env == "" { + log.Fatal("WIPEINTERVAL is not set, expects seconds in an integer") + } + value, err := strconv.Atoi(env) + if err != nil { + log.Fatal("WIPEINTERVAL is not an integer") + } + wipeInterval = value + } + + return &Config{ + ClientConfig: gofish.ClientConfig{Endpoint: urlAddress, + Username: username, + Password: password, + Insecure: !validCertOnly}, + wipeInterval: wipeInterval, + } +} + +func bootstrap(lom *lom.LOM) error { + systems := *lom.AllSystems() + + bootOverride := redfish.Boot{ + BootSourceOverrideTarget: redfish.CdBootSourceOverrideTarget, + BootSourceOverrideEnabled: redfish.OnceBootSourceOverrideEnabled, + } + emptyVirtualMediaPayload := map[string]any{"Image": nil} + occupiedVirtualMediaPayload := map[string]string{"Image": "http://192.168.0.170:8080/ipxe.iso"} + + for _, system := range systems { + // Ensure boot option is actually set + fmt.Printf("Setting boot option of system: %s\n", system.HostName) + err := system.SetBoot(bootOverride) + if err != nil { + return err + } + + fmt.Printf("Removing virtual media for system: %s\n", system.HostName) + _, err = lom.APIClient.Patch("/redfish/v1/managers/1/virtualmedia/2", emptyVirtualMediaPayload) + if err != nil { + fmt.Println(err) + } + + fmt.Printf("Setting virtual media for system: %s\n", system.HostName) + _, err = lom.APIClient.Patch("/redfish/v1/managers/1/virtualmedia/2", occupiedVirtualMediaPayload) + if err != nil { + fmt.Println(err) + } + + // Reboot + fmt.Printf("Restarting system: %s\n", system.HostName) + err = system.Reset(redfish.ForceRestartResetType) + if err != nil { + return err + } + } + + // Await Alpine SSH access + addr := "192.168.0.248:22" + privateKeyBytes, _ := os.ReadFile("./key") + user := "root" + + signer, err := ssh.ParsePrivateKey(privateKeyBytes) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Waiting for SSH access to %s@%s\n", user, addr) + conn, err := awaitAndConnectSSH(signer, user, addr) + if err != nil { + log.Fatal(err) + } + conn.Close() + + // Run ansible playbook + fmt.Println("Running playbook") + cmd := exec.Command("ansible-playbook", "-i", addr+",", "--private-key", "/app/key", "./ansible/playbook.yml") + cmd.Env = append(cmd.Env, "ANSIBLE_SSH_COMMON_ARGS='-o StrictHostKeyChecking=no'") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err = cmd.Run() + if err != nil { + log.Fatal(err) + } + // Wait for real OS SSH access + // Ansible playbook 2: electric boogaloo? + + return nil +}