diff --git a/.changeset/stop-failed-install-recipes.md b/.changeset/stop-failed-install-recipes.md new file mode 100644 index 000000000..294fc0058 --- /dev/null +++ b/.changeset/stop-failed-install-recipes.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Keep private FTW data out of the release Docker build context. Install commands stop on download errors and refuse to overwrite an earlier Docker test. The English and Swedish guides put the physical SD-card swap before installation and treat Docker as a separate path. diff --git a/.github/scripts/classify-test-changes.sh b/.github/scripts/classify-test-changes.sh index 99f1a6524..8516f7201 100644 --- a/.github/scripts/classify-test-changes.sh +++ b/.github/scripts/classify-test-changes.sh @@ -31,7 +31,7 @@ while IFS= read -r file; do web/*|package.json|package-lock.json) web=true ;; - Dockerfile|Dockerfile.optimizer|docker-compose*.yml|scripts/enable-modular-stack.sh|scripts/migrate-legacy-compose.sh|scripts/upgrade-paired-release.sh|scripts/test-upgrade-paired-release.sh|scripts/test-modular-compose.sh|scripts/test-container-boundaries.sh|scripts/install-macos.sh|.github/scripts/classify-test-changes.sh|.github/scripts/test-change-classifier.sh) + deploy/docker/*|docs/native-beta.md|docs/setup-guide/update-sv.md|scripts/test_install_docs.py|scripts/test-release-docker-context.sh|Dockerfile|Dockerfile.optimizer|docker-compose*.yml|scripts/enable-modular-stack.sh|scripts/migrate-legacy-compose.sh|scripts/upgrade-paired-release.sh|scripts/test-upgrade-paired-release.sh|scripts/test-modular-compose.sh|scripts/test-container-boundaries.sh|scripts/install-macos.sh|.github/scripts/classify-test-changes.sh|.github/scripts/test-change-classifier.sh) compose=true ;; .github/workflows/beta.yml|.github/workflows/release.yml|.github/workflows/release-assets.yml|scripts/check-legacy-release-line.sh|scripts/check-stable-release.py|scripts/package-linux.py|scripts/test_package_linux.py|scripts/upload-release-assets.sh|scripts/test-upload-release-assets.sh|scripts/check-ghcr-write-access.sh|scripts/test-ghcr-write-access.sh|scripts/test-exact-image-promotion.sh|scripts/github-release-by-id.sh|scripts/test-github-release-by-id.sh|scripts/promote-paired-latest.sh|scripts/test-promote-paired-latest.sh) diff --git a/.github/scripts/test-change-classifier.sh b/.github/scripts/test-change-classifier.sh index 789ff665e..9c920e0d9 100644 --- a/.github/scripts/test-change-classifier.sh +++ b/.github/scripts/test-change-classifier.sh @@ -58,4 +58,9 @@ assert_paths 'scripts/upgrade-paired-release.sh' \ assert_paths 'scripts/test-upgrade-paired-release.sh' \ 'core=false' 'optimizer=false' 'web=false' 'drivers=false' 'compose=true' +for path in deploy/docker/.dockerignore docs/native-beta.md docs/setup-guide/update-sv.md scripts/test_install_docs.py scripts/test-release-docker-context.sh; do + assert_paths "$path" \ + 'core=false' 'optimizer=false' 'web=false' 'drivers=false' 'compose=true' +done + echo "test workflow path classifier contract passed" diff --git a/Makefile b/Makefile index f9774e426..22f798539 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,8 @@ compose-migration-test: container-boundary-test: release-workflow-test bash scripts/test-container-boundaries.sh + PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s scripts -p 'test_install_docs.py' + bash scripts/test-release-docker-context.sh release-workflow-test: bash scripts/test-install-native.sh diff --git a/README.md b/README.md index 0e912fb22..638b85c7f 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,14 @@ The new line starts at `v0.131.0-beta.1`; old 0.x releases up to 0.130.x are also retired. A lower version number or a saved `beta` channel does not tell you which install path you have. Identify it before running commands. -Use a fresh 64-bit Raspberry Pi OS, Debian or Ubuntu host for native systemd, -or run the same release package in Docker on Linux. Existing sites can switch -using a second SD card/host or a separate Docker project with new data. The +**Old FTW on a Raspberry Pi? Use a second SD card.** Keep the old card, +prepare the new one and swap cards before running install commands. Follow +[the card-by-card steps](docs/native-beta.md#raspberry-pi-use-a-second-sd-card). +Do not try Docker next if the native installer refuses the old card. + +Use a fresh 64-bit Raspberry Pi OS, Debian or Ubuntu host for native systemd. +Docker on the same Linux host is a separate, advanced option: identify and +stop old Core and its updater, and preserve any MQTT service first. The guided migration of settings and history is not ready; preserve the old data and get help if those data must move before you switch. Only one Core may control the equipment, including after reboot. diff --git a/deploy/docker/.dockerignore b/deploy/docker/.dockerignore new file mode 100644 index 000000000..d864cae7c --- /dev/null +++ b/deploy/docker/.dockerignore @@ -0,0 +1,3 @@ +* +!Dockerfile +!.dockerignore diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index c9a911f3a..85283dff7 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* RUN set -eu; \ - test -n "${FTW_VERSION}" || { echo "Set FTW_VERSION, for example v0.135.1-beta.1" >&2; exit 1; }; \ + test -n "${FTW_VERSION}" || { echo "Set FTW_VERSION to an exact published new 0.x tag" >&2; exit 1; }; \ pkg="ftw-linux-$(dpkg --print-architecture).tar.gz"; \ url="https://github.com/srcfl/ftw/releases/download/${FTW_VERSION}"; \ cd /tmp; \ @@ -26,9 +26,9 @@ RUN set -eu; \ # `docker compose exec ftw ftw status` runs the same status, backup and # support commands as a native install. Updates change FTW_VERSION instead. -# Numeric uid and gid 100:101, as in the 2.x image, so existing data -# directories keep working. HOME points into the data volume because no -# account exists. +# The new, separate data directory belongs to uid:gid 100:101. This does not +# make data from an older FTW compatible. HOME points into the data volume +# because no account exists. ENV HOME=/app/data USER 100:101 WORKDIR /app/data diff --git a/deploy/docker/compose.yaml b/deploy/docker/compose.yaml index af38a2d07..33b150898 100644 --- a/deploy/docker/compose.yaml +++ b/deploy/docker/compose.yaml @@ -1,15 +1,9 @@ -# FTW 0.x in Docker. Put this file, the Dockerfile beside it and a .env -# with FTW_VERSION in one directory, then see docs/native-beta.md: -# -# mkdir -p data && sudo chown 100:101 data -# echo FTW_VERSION=v0.X.Y-beta.N > .env # replace with an exact published tag -# docker compose up -d --build -# -# Update: change FTW_VERSION in .env and run `docker compose up -d --build`. -# Going back is the same with the previous version. -# -# The project has its own name so it never mixes with an older FTW stack, -# whose project is called ftw. +# FTW 0.x in Docker. Follow docs/native-beta.md#docker before starting. +# Keep compose.yaml, Dockerfile, .dockerignore and .env together. The ignore +# file keeps private data out of the build context, including on later builds. +# Use separate empty data; stop old Core and its updater before starting here. +# A separate project name does not prevent two Cores controlling one site. +# Update: change FTW_VERSION in .env and rebuild as the guide describes. name: ftw-local services: diff --git a/docs/native-beta.md b/docs/native-beta.md index e35297fb6..e9101d5c3 100644 --- a/docs/native-beta.md +++ b/docs/native-beta.md @@ -19,17 +19,50 @@ on 64-bit Linux use the same release package. ## Choose your path +**Still using the SD card that runs old FTW? Do not run the native install +commands on that card.** For a Raspberry Pi, use the second-card path below. +A failed native install is a stop point, not a reason to try Docker next. + +Choose **one** path. These are alternatives, not steps to run in order: + +- **Old FTW on a Pi:** [use a second SD card](#raspberry-pi-use-a-second-sd-card). + Keep the old card. This is the recommended path if you need step-by-step help. +- **Already on new 0.x:** [update native](#update-a-native-box) or + [update new Docker](#update-new-docker). Do not run a new install. +- **An empty Linux host with no FTW:** [install native](#install). + [Docker](#docker) is an alternative for people who manage Docker themselves. + +
+Other install types and the full path table + | What runs now | How to switch or update | |---|---| | Old Docker: 0.x up to 0.130.x, 1.x or 2.x, including `ghcr.io/frahlg/forty-two-watts` images | [Switch from an older FTW](#coming-from-an-older-ftw). Use a new SD card/host, or separate Docker project and data. | | Docker 3.x, including beta | The same switch. No intermediate release and no further 3.x updates. | | The old ready-made FTW Raspberry Pi image | It runs old Docker. Use a second card with Raspberry Pi OS Lite 64-bit; keep the old card. | | New 0.x native with launcher/release slots | [Update the native box](#update-a-native-box). Do not reinstall. | -| New 0.x Docker built from the release package | [Change `FTW_VERSION` and rebuild](#docker). `ftw update` does not apply. | +| New 0.x Docker built from the release package | [Change `FTW_VERSION` and rebuild](#update-new-docker). `ftw update` does not apply. | | Older direct native service without release slots | Identify its unit and data paths first. Use a separate host/card, or a checked migration for that site. The [native migration pilot](self-update.md#pilot-an-older-native-systemd-site) is not a general installer. | | Home Assistant app on the old line | Run new FTW on another Linux host. Stop the old app and its automatic start/watchdog before new Core controls the equipment. The app does not provide new 0.x yet. | | Custom build, manual binary, macOS or Windows | Identify the process and data first. The release install paths here require 64-bit Linux. | +
+ +### If you already got an error + +**Stop at the first error. Do not paste the next command block or change +install methods.** Save the command and its output, and ask in +[Discord](https://discord.gg/UK2ygPBu8N) if the next step is unclear. + +| Message or symptom | What to do | +|---|---| +| `Existing FTW installation found` or `--fresh-host` refused | You are on a host/card with FTW data. Keep those files. For a Pi, shut down and use the prepared new card. Some published installers say “try 0.x beside it”; this does not mean Docker is the next step. | +| `curl: (22)` / `404` | The download failed. Copy the exact tag from Releases and check the URL. Do not run an old `install.sh`, reuse old Compose files or continue to build. | +| `ftw-local` already exists | An earlier attempt has left files. Stop and inspect that project before retrying; do not delete its data or overwrite its `.env`. | +| `requires buildx plugin` | Docker is missing a build tool. Follow the Docker prerequisite below before building. | +| `checking context: no permission to read .../data/applink.json` | Private FTW data is entering the build. Keep its permissions. The `.dockerignore` step under [Update new Docker](#update-new-docker) fixes the supplied recipe; do not try `chmod` or a build as root to read those files. | +| New `ftw-local` container keeps `Restarting` while old Core is `Up` | Stop the **new test container** using its confirmed name (`sudo docker stop --time 60 `). Keep both data sets. Diagnose before starting it again; do not stop a shared MQTT broker. | + ### Find out what is running Run these read-only checks on the FTW host, through SSH if needed: @@ -61,28 +94,6 @@ running Core; inspect containers and their restart rules. A service called - FTW controls the battery and charger you configure. Keep the equipment's own app at hand to take control back. -## Install - -Choose an exact published new 0.x beta from [Releases](https://github.com/srcfl/ftw/releases). -Check that it includes the Linux package for your architecture and its SHA-256 -file. Do not use `releases/latest`: it still points to the retired 2.x line. -Replace `v0.X.Y-beta.N` below with that tag and use its own installer. -`--fresh-host` confirms that this host has no existing FTW site, including -stopped containers or data in a custom directory: - -```bash -tag=v0.X.Y-beta.N -curl -fsSLO "https://raw.githubusercontent.com/srcfl/ftw/${tag}/scripts/install.sh" -bash install.sh --fresh-host --tag "${tag}" -``` - -It checks the package's SHA-256, creates the `ftw` user, puts releases in -`/opt/ftw`, data in `/var/lib/ftw`, the `ftw` command in `/usr/local/bin` -and starts the `ftw` service. Open `http://:8080/setup` to set up the -site. - -To run it in Docker instead, see [Docker](#docker). - ## Coming from an older FTW Move directly to the new line using one of the paths below. Keep the old @@ -99,20 +110,34 @@ new Core, and stop new Core before returning to the old install. ### Raspberry Pi: use a second SD card -1. Keep the old card intact. Write Raspberry Pi OS Lite **64-bit** to a **new** - card, as in the [setup guide](setup-guide/README.md). Choose a login name - other than `ftw`; the installer creates that service account. -2. Shut the Pi down, swap cards, start it and follow [Install](#install). - Set up the site again at `http://:8080/setup`. -3. If the old card supplied MQTT, provide a broker for the new setup before - connecting the devices. The old card's broker does not move with FTW. -4. Follow [Verify the switch](#verify-the-switch). +1. **On the old card:** save the settings and off-host backup described above. + Do not run an installer here. Keep this card intact. +2. **On your normal computer:** write Raspberry Pi OS Lite **64-bit** to a + **second** card. The [Pi setup guide](setup-guide/en.md) shows the Imager + steps. Enable SSH and choose a login name other than `ftw`, such as `pi`. +3. **At the Pi:** shut it down, unplug power, remove the old card and insert + the new one. Reconnect power. The physical card swap must happen before + you run any install command. +4. **Back on your computer:** open a new SSH connection using the login you + chose for the new card. The IP address may have changed; check the router. + If you have not swapped cards, stop here. +5. **Before setting up devices:** stop old FTW on any other host that could + control the same equipment. If the old card supplied MQTT, arrange a broker + for the new setup. The old card's broker does not move with FTW. +6. **In SSH on the new card:** follow [Install](#install), then open + `http://:8080/setup` and set up the site again. +7. Follow [Verify the switch](#verify-the-switch). To return, shut down and put the old card back. Also stop any new FTW you ran on another machine. Data collected by the new setup stays on the new card. ### Same Linux host: use a separate Docker project +**Advanced alternative to changing cards.** Choose it only if you can identify +and stop the old Core and updater, handle their start rules, and preserve MQTT. +Otherwise use a second card or ask for help. Do not start this path just +because the native installer refused the old card. + 1. Identify the old project's directory, Compose files, overrides, name and data mounts. Common paths are `/opt/ftw` on the old Pi image, `~/ftw` and `~/forty-two-watts`; use the path found on this host. Save its Compose files @@ -141,6 +166,45 @@ Core, Start on boot and Watchdog before the new site takes control. Keep a Home Assistant backup and the old app's data for recovery. The MQTT integration with a separate FTW host still works; see [Home Assistant](ha-integration.md). +## Install + +**Only on the new card or an empty host.** If old FTW ran on this card, +return to [the second-card steps](#raspberry-pi-use-a-second-sd-card) first. +This section does not update or migrate an old installation. + +Choose an exact published new 0.x beta from [Releases](https://github.com/srcfl/ftw/releases). +Check that it includes the Linux package for your architecture and its SHA-256 +file. Do not use `releases/latest`: it still points to the retired 2.x line. +Copy the tag from the release title; do not retype it. Replace only +`v0.X.Y-beta.N` in the block below. Paste the **whole block, including `(` +and `)`**, into SSH on the new host. It stops at the first error and downloads +that release's installer to a temporary file. +`--fresh-host` confirms that this host has no existing FTW site, including +stopped containers or data in a custom directory: + +```bash +( + set -eu + tag=v0.X.Y-beta.N + if [[ ! "$tag" =~ ^v0\.([0-9]+)\.[0-9]+(-beta\.[0-9]+)?$ ]] || (( 10#${BASH_REMATCH[1]} < 131 )); then + echo "STOP: copy an exact published new 0.x tag from Releases." >&2 + exit 1 + fi + installer=$(mktemp) + trap 'rm -f "$installer"' EXIT + curl -fSL "https://raw.githubusercontent.com/srcfl/ftw/${tag}/scripts/install.sh" -o "$installer" + bash "$installer" --fresh-host --tag "$tag" +) +``` + +It checks the package's SHA-256, creates the `ftw` user, puts releases in +`/opt/ftw`, data in `/var/lib/ftw`, the `ftw` command in `/usr/local/bin` +and starts the `ftw` service. Open `http://:8080/setup` to set up the +site. + +If it reports an error, stop and use [the error table](#if-you-already-got-an-error). +Do not continue to the Docker instructions. + ## Verify the switch Check the expected running version, healthy devices, advancing measurements, @@ -265,34 +329,80 @@ service definition. When a release notes changes to them, refresh them with the installer from that release. Replace the placeholder with its exact tag: ```bash -tag=v0.X.Y-beta.N -curl -fsSLO "https://raw.githubusercontent.com/srcfl/ftw/${tag}/scripts/install.sh" -bash install.sh --refresh --tag "${tag}" +( + set -eu + tag=v0.X.Y-beta.N + if [[ ! "$tag" =~ ^v0\.([0-9]+)\.[0-9]+(-beta\.[0-9]+)?$ ]] || (( 10#${BASH_REMATCH[1]} < 131 )); then + echo "STOP: copy an exact published new 0.x tag from Releases." >&2 + exit 1 + fi + installer=$(mktemp) + trap 'rm -f "$installer"' EXIT + curl -fSL "https://raw.githubusercontent.com/srcfl/ftw/${tag}/scripts/install.sh" -o "$installer" + bash "$installer" --refresh --tag "$tag" +) ``` ## Docker -Docker runs FTW as well as the native install does. Compose builds a small -local image from the same checksummed release package; nothing is compiled. -You need Docker Engine with Compose on Linux. Docker Desktop on macOS or -Windows has a different network setup; this Linux host-network recipe does -not establish LAN device access there. Choose an exact published new 0.x tag -as described under [Install](#install). Replace the placeholder below. These -steps create a new site; use an empty directory, not an existing install. +This is an alternative to native installation, not its next step. It creates +a new site. If old FTW is on this host, complete [the same-host switch steps](#same-linux-host-use-a-separate-docker-project) +first, including stopping old Core and its updater. Do not connect both to the +same equipment. + +You need Docker Engine, Compose **and Buildx** on 64-bit Linux. The +[Docker Debian instructions](https://docs.docker.com/engine/install/debian/) +include both plugins. Do not reinstall Docker on a running site without +checking its existing services. Docker Desktop has a different network setup; +this Linux host-network recipe does not establish LAN device access there. + +Copy an exact published new 0.x tag from [Releases](https://github.com/srcfl/ftw/releases). +Replace only `v0.X.Y-beta.N` and paste the **whole block, including `(` and `)`**. +It downloads both files to a temporary directory before creating the project, +so a failed download leaves no project behind. Correct the tag or connection +and paste the whole block again. It stops at the first error and refuses an +existing `~/ftw-local` directory, including an earlier test. If that happens, inspect it before doing anything +else; do not delete it to make the command pass. ```bash -tag=v0.X.Y-beta.N -mkdir -p ~/ftw-local && cd ~/ftw-local -base="https://raw.githubusercontent.com/srcfl/ftw/${tag}/deploy/docker" -curl -fsSLO "${base}/compose.yaml" -O "${base}/Dockerfile" -mkdir -p data && sudo chown 100:101 data -printf 'FTW_VERSION=%s\n' "$tag" > .env -docker compose up -d --build +( + set -eu + tag=v0.X.Y-beta.N + if [[ ! "$tag" =~ ^v0\.([0-9]+)\.[0-9]+(-beta\.[0-9]+)?$ ]] || (( 10#${BASH_REMATCH[1]} < 131 )); then + echo "STOP: copy an exact published new 0.x tag from Releases." >&2 + exit 1 + fi + sudo docker compose version + sudo docker buildx version + sudo docker info >/dev/null + install_dir="$HOME/ftw-local" + if [ -e "$install_dir" ] || [ -L "$install_dir" ]; then + echo "STOP: $install_dir already exists. Inspect it before retrying." >&2 + exit 1 + fi + downloads=$(mktemp -d) + trap 'rm -rf "$downloads"' EXIT + base="https://raw.githubusercontent.com/srcfl/ftw/${tag}/deploy/docker" + curl -fSL "${base}/compose.yaml" -o "$downloads/compose.yaml" + curl -fSL "${base}/Dockerfile" -o "$downloads/Dockerfile" + mkdir "$install_dir" + cp "$downloads/compose.yaml" "$downloads/Dockerfile" "$install_dir/" + cd "$install_dir" + printf '*\n!Dockerfile\n!.dockerignore\n' > .dockerignore + printf 'FTW_VERSION=%s\n' "$tag" > .env + mkdir data + sudo chown 100:101 data + sudo docker compose up -d --build +) ``` -The two files stay the same between releases; `FTW_VERSION` chooses one. Open -`http://:8080/setup`. Data lives in `~/ftw-local/data`. If Docker -answers "permission denied", put `sudo` in front of `docker`. +The block writes `.dockerignore` locally because earlier release tags do not +include it. This keeps private data and `.env` out of every build. Compose +builds a local image from the checksummed release package; it compiles nothing. +Open `http://:8080/setup`. Data lives in `~/ftw-local/data`. + +After success, use `cd ~/ftw-local` before the commands below. Use `sudo docker` +if your login cannot access Docker without it. ```bash docker compose ps # running and healthy @@ -302,6 +412,22 @@ docker compose logs --tail 100 # logs docker compose restart # restart ``` +### Update new Docker + +Use this only for an existing **new 0.x** project from the recipe above, not +for old 2.x/3.x Docker. Confirm the project and data path, then `cd ~/ftw-local`. +Before rebuilding an install made with the old instructions, create the missing +`.dockerignore` beside its `Dockerfile`: + +```bash +if [ ! -e .dockerignore ]; then + printf '*\n!Dockerfile\n!.dockerignore\n' > .dockerignore +fi +``` + +If a file already exists, check that it excludes `data` and `.env`. Keep private +files private; do not change their permissions to make a build pass. + To update, set the new version and rebuild. To go back, set the previous version. If its image is still on the host, Compose can reuse it. Going back works while both releases read the same data, as on a native install. @@ -342,6 +468,10 @@ Give this section to an agent that operates FTW for you. FTW controls real equipment, so the agent uses the same commands a person would and leaves decisions about the site to you. +- Choose one install path. Do not treat the native and Docker sections as a + sequence. Stop on a download or install error; do not use stale files or + switch methods to get past a refusal. Never make private data readable to + fix a Docker build; exclude it from the build context. - Identify the host, running version, process/service or Compose project, data paths, restart rules and target release before changing anything. Use [Choose your path](#choose-your-path), including for older 0.x versions. diff --git a/docs/operations.md b/docs/operations.md index 53bc96187..338969ceb 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -11,13 +11,10 @@ safely when it is unavailable. [Install and update FTW](native-beta.md) is the full guide and explains how to choose a published new 0.x beta. Use the installer from the same tag you -install, on a fresh 64-bit Raspberry Pi OS, Debian or Ubuntu host: +install, on a fresh 64-bit Raspberry Pi OS, Debian or Ubuntu host. -```bash -tag=v0.X.Y-beta.N # the exact release chosen for this site -curl -fsSLO "https://raw.githubusercontent.com/srcfl/ftw/${tag}/scripts/install.sh" -bash install.sh --fresh-host --tag "${tag}" -``` +Follow the complete [native install block](native-beta.md#install). It stops +on errors and uses a temporary download. Do not run it on the old SD card. Do not use GitHub `releases/latest`; it stays on 2.x for old boxes. `--fresh-host` confirms that no FTW site exists, including a stopped Docker diff --git a/docs/setup-guide/sv.md b/docs/setup-guide/sv.md index d69979d11..887a4e1b5 100644 --- a/docs/setup-guide/sv.md +++ b/docs/setup-guide/sv.md @@ -118,7 +118,7 @@ Grattis — du är nu "inne" i Raspberry Pin. ## Steg 11 — Installera FTW -Följ [installationsguiden för nya 0.x](../native-beta.md#install) och välj en exakt publicerad beta. På en ny 64-bitars Pi kan du installera nu. Kör FTW redan på din Pi, följ [bytesguiden](update-sv.md) först: använd ett nytt kort och ställ in anläggningen på nytt. Den guidade flytten av gamla inställningar och historik är ännu inte klar. +Följ [installationsguiden för nya 0.x på svenska](update-sv.md#installera-på-det-nya-kortet-eller-en-tom-linux-maskin) och välj en exakt publicerad beta. På en ny 64-bitars Pi kan du installera nu. Kör FTW redan på din Pi, följ [bytesguiden](update-sv.md) först: använd ett nytt kort och ställ in anläggningen på nytt. Den guidade flytten av gamla inställningar och historik är ännu inte klar. ## Efter installationen diff --git a/docs/setup-guide/update-sv.md b/docs/setup-guide/update-sv.md index da47fe3e8..3da3743a9 100644 --- a/docs/setup-guide/update-sv.md +++ b/docs/setup-guide/update-sv.md @@ -1,6 +1,6 @@ # Byt till nya FTW och håll det uppdaterat -[English and release commands](../native-beta.md). +[English](../native-beta.md). **FTW 2.x och 3.x får inga fler uppdateringar. All fortsatt utveckling går till den nya 0.x-serien. Vill du ha de senaste funktionerna och rättningarna @@ -18,7 +18,23 @@ klar. Behöver du föra över dessa data innan du byter, ta hjälp med just din installation. Kopiera inte gamla databaser till den nya installationen på egen hand. -## Välj väg efter hur FTW körs +## Välj en väg + +**Sitter det gamla FTW-kortet fortfarande i Pi:n? Kör ingen nyinstallation där.** +Vägen för dig som vill ha hjälp steg för steg är ett **andra SD-kort**. +Du ska byta det fysiska kortet innan du kör installationskommandot. + +Välj **en** av följande vägar. De är alternativ, inte steg efter varandra: + +- **Gammal FTW på Raspberry Pi:** [byt med ett nytt SD-kort](#byt-på-raspberry-pi-med-ett-nytt-sd-kort). + Det gamla kortet och dess data blir kvar. +- **Redan nya 0.x:** [uppdatera din befintliga installation](#uppdatera-när-du-redan-kör-nya-ftw). + Kör ingen nyinstallation. +- **Tom Linux-maskin utan FTW:** [installera native](#installera-på-det-nya-kortet-eller-en-tom-linux-maskin). + Docker är ett eget alternativ för dig som kan hantera Docker. + +
+Andra installationer och hela tabellen med vägval | Det du har nu | Vägen till nya FTW | Nästa uppdatering | |---|---|---| @@ -35,6 +51,23 @@ hand. körsätten åt. Samma tjänstenamn har använts tidigare, och status visar den Core som svarar på adressen. +
+ +## Om du redan har fått ett fel + +**Stanna vid första felet. Kör inte nästa block och byt inte metod för att +komma förbi felet.** Spara kommandot och svaret. Be om hjälp i +[Discord](https://discord.gg/UK2ygPBu8N) om nästa steg är oklart. + +| Det du ser | Gör så här | +|---|---| +| `Existing FTW installation found` eller nekad `--fresh-host` | Du kör på en maskin eller ett kort med FTW-data. Behåll filerna. På Pi: stäng av och byt till det förberedda nya kortet. Äldre publicerade skript kan säga “try 0.x beside it”; det betyder inte att Docker är nästa steg. | +| `curl: (22)` eller `404` | Nedladdningen misslyckades. Kopiera den exakta taggen från Releases och kontrollera länken. Kör inte en gammal `install.sh` eller kvarlämnade Docker-filer. | +| `ftw-local` finns redan | Ett tidigare försök har lämnat filer. Kontrollera projektet innan du gör mer. Radera inte data och skriv inte över `.env`. | +| `requires buildx plugin` | Docker saknar ett byggverktyg. Ordna förkraven i Docker-guiden innan du bygger. | +| `checking context: no permission to read .../data/applink.json` | Docker försöker läsa privata FTW-data när det bygger. Behåll filernas rättigheter. Se steget med `.dockerignore` under [Docker-uppdatering](#docker-från-det-nya-releasepaketet). Lös inte felet med `chmod` eller genom att bygga som root. | +| Nya `ftw-local` visar `Restarting` medan gamla Core visar `Up` | Stoppa **den nya testcontainern** med dess kontrollerade namn: `sudo docker stop --time 60 `. Behåll båda installationernas data och felsök innan du startar den igen. Stoppa inte en delad MQTT-broker. | + ## Börja här om du inte vet vad som körs Öppna en terminal på FTW-maskinen, eller anslut med SSH. Följande kommandon @@ -58,26 +91,27 @@ den igen vid omboot. ## Byt på Raspberry Pi med ett nytt SD-kort -1. Spara uppgifter om enheter, adresser, mål och scheman. Ta en full backup - enligt instruktionerna för den installerade versionen och spara den utanför - Pi:n. Behåll även det gamla kortet. -2. Skriv Raspberry Pi OS Lite **64-bit** till ett **nytt** kort. Välj ett - användarnamn som inte är `ftw`, och slå på SSH. Skriv inte över det gamla - kortet. -3. Stäng av Pi:n, byt kort och starta. Om gamla FTW körs på en annan maskin, - stoppa den innan nya FTW får styra samma utrustning. -4. Välj en publicerad 0.x-beta från - [Releases](https://github.com/srcfl/ftw/releases). Den ska ha paketet för - din arkitektur och dess SHA-256-fil. Använd inte `releases/latest`: den - länken pekar fortfarande på den gamla serien. -5. Följ [installationsstegen](https://github.com/srcfl/ftw/blob/master/docs/native-beta.md#install) - med den valda taggen. Installationsskriptet och paketet ska komma från samma - tagg. `--fresh-host` gäller bara en värd utan en befintlig FTW-installation. -6. Öppna `http://:8080/setup` och ställ in anläggningen på nytt. - Historik, inlärning och gamla inställningar ligger kvar på det gamla kortet; - de följer inte automatiskt med. Ersätt gamla kalenderhändelser med mål och - ready-by-scheman. Kontrollera även MQTT: en broker på det gamla kortet - följer inte med till det nya. +1. **På gamla kortet:** spara enheternas adresser, mål och scheman. Ta en full + backup enligt den installerade versionens instruktioner och spara den + utanför Pi:n. Kör inget installationskommando på detta kort. +2. **På din vanliga dator:** skriv Raspberry Pi OS Lite **64-bit** till ett + **andra** kort. Följ [Pi-guidens steg 1–7](sv.md#steg-1--hämta-programmet-som-förbereder-minneskortet). + Välj ett användarnamn som inte är `ftw`, till exempel `pi`, och slå på SSH. + Skriv inte över det gamla kortet. +3. **Vid Pi:n:** stäng av, dra ur strömmen, ta ut gamla kortet och sätt i det + nya. Anslut strömmen igen. **Det fysiska kortbytet ska vara klart innan du + kör någon installation.** +4. **På din vanliga dator:** öppna en ny SSH-anslutning till Pi:n med det + användarnamn du valde för nya kortet. IP-adressen kan ha ändrats; kontrollera + routern. Har du inte bytt kort, stanna här. +5. **Innan du ställer in enheterna:** stoppa gammal FTW på andra maskiner + som kan styra samma utrustning. Ordna MQTT om utrustningen behöver det. + En broker på gamla kortet följer inte med. +6. **I SSH på nya kortet:** följ [Installera på det nya kortet](#installera-på-det-nya-kortet-eller-en-tom-linux-maskin) + nedan. Öppna sedan `http://:8080/setup` och ställ in anläggningen. + Historik, inlärning och gamla inställningar ligger kvar på gamla kortet; + de följer inte automatiskt med. Använd mål och ready-by-scheman i stället + för gamla kalenderhändelser. 7. Kontrollera rätt version, friska enheter, färska mätvärden och aktuell plan. Starta sedan om Pi:n när anläggningen kan tåla avbrottet och kontrollera samma saker igen. @@ -88,8 +122,13 @@ som den nya installationen har samlat ligger kvar på det nya kortet. ## Byt på samma Linux-maskin med Docker -Den nya Docker-installationen använder en egen katalog och egen data. Det är -en ny installation, inte en flytt av gamla data. +**Detta är ett avancerat alternativ till kortbytet ovan.** Välj det bara om +du kan identifiera och stoppa gamla Core och updater, hantera deras startregler +och bevara MQTT. Välj annars nytt kort eller be om hjälp. En nekad +native-installation på gamla kortet är inte ett skäl att fortsätta med Docker. + +Den nya Docker-installationen använder en egen katalog och egen data. +Den flyttar inte gamla data. 1. Identifiera det gamla Compose-projektet, dess Core, updater, datakataloger och startregler. Vanliga kataloger är `/opt/ftw`, `~/ftw` och @@ -120,6 +159,39 @@ sin data. styra även om den inte kan starta sin webbsida. En enda synlig webbsida är därför inget bevis på att bara en FTW körs. +## Installera på det nya kortet eller en tom Linux-maskin + +**Bara på nya kortet eller en tom värd.** Har gamla FTW körts på det här +kortet, gå tillbaka till kortbytet ovan. `--fresh-host` betyder att ingen +FTW-installation eller dess data finns här, även om den är stoppad. + +1. Öppna [Releases](https://github.com/srcfl/ftw/releases) på din vanliga dator. + Välj en publicerad **ny 0.x-beta** med Linux-paket och SHA-256-fil för din + maskin. Använd inte `releases/latest`; den pekar på gamla 2.x. +2. Kopiera taggen från releasens rubrik. Skriv inte av den för hand. Byt bara + ut `v0.X.Y-beta.N` i blocket nedan mot taggen du kopierade. +3. Klistra in **hela blocket, inklusive `(` och `)`**, i SSH på nya kortet. + Det stannar vid första felet och hämtar skriptet till en tillfällig fil. + +```bash +( + set -eu + tag=v0.X.Y-beta.N + if [[ ! "$tag" =~ ^v0\.([0-9]+)\.[0-9]+(-beta\.[0-9]+)?$ ]] || (( 10#${BASH_REMATCH[1]} < 131 )); then + echo "STOP: copy an exact published new 0.x tag from Releases." >&2 + exit 1 + fi + installer=$(mktemp) + trap 'rm -f "$installer"' EXIT + curl -fSL "https://raw.githubusercontent.com/srcfl/ftw/${tag}/scripts/install.sh" -o "$installer" + bash "$installer" --fresh-host --tag "$tag" +) +``` + +Om installationen lyckas, öppna `http://:8080/setup` och ställ in +anläggningen. Om den visar ett fel, stanna och läs [feltabellen](#om-du-redan-har-fått-ett-fel). +Fortsätt inte med Docker-kommandon. + ## Uppdatera när du redan kör nya FTW ### Native med launcher @@ -153,6 +225,19 @@ pilotinstallation med annan katalogstruktur behöver sina egna kontroller. ### Docker från det nya releasepaketet +Detta gäller bara **nya 0.x**, inte gamla 2.x/3.x. Gå till det kontrollerade +nya projektets katalog, normalt `cd ~/ftw-local`. Om du installerade med den +tidigare guiden, skapa den saknade `.dockerignore` bredvid `Dockerfile` först: + +```bash +if [ ! -e .dockerignore ]; then + printf '*\n!Dockerfile\n!.dockerignore\n' > .dockerignore +fi +``` + +Om filen redan finns, kontrollera att den utesluter `data` och `.env` från +bygget. Behåll datafilernas rättigheter. + Öppna `.env` i det verifierade nya Compose-projektet och ändra `FTW_VERSION` till den exakta publicerade release du vill installera. Behåll övriga rader. Kör sedan i samma projekt: @@ -170,6 +255,9 @@ kompatibla `FTW_VERSION` och köra Compose igen. Docker har ingen automatisk ## För agenter som hjälper till +- Välj en väg. Native och Docker är alternativ, inte steg efter varandra. + Stanna vid nedladdningsfel eller nekad installation. Använd inte gamla + nedladdade filer och byt inte metod för att komma förbi spärren. - Följ samma vägval som människan. Fastställ värd, version, körsätt, tjänst/projekt, datavägar, startregler och målversion innan du ändrar något. - Skilj på uppdatering inom nya serien, nyinstallation med nya data och diff --git a/scripts/test-release-docker-context.sh b/scripts/test-release-docker-context.sh new file mode 100644 index 000000000..722215576 --- /dev/null +++ b/scripts/test-release-docker-context.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# A rebuild must not read or send the site's private data to Docker. +set -euo pipefail +root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +if ! docker info >/dev/null 2>&1; then + echo 'SKIP release Docker context: Docker daemon unavailable' + exit 0 +fi +probe=$(mktemp -d) +cleanup() { + chmod 600 "$probe/context/data/applink.json" 2>/dev/null || true + rm -rf "$probe" +} +trap cleanup EXIT +mkdir -p "$probe/context/data" +printf 'private test fixture\n' > "$probe/context/data/applink.json" +printf 'private env fixture\n' > "$probe/context/.env" +printf 'old installer fixture\n' > "$probe/context/install.sh" +chmod 000 "$probe/context/data/applink.json" +cp "$root/deploy/docker/.dockerignore" "$probe/context/.dockerignore" +# COPY forces Docker to evaluate the context; no base image or network needed. +printf 'FROM scratch\nCOPY . /context/\n' > "$probe/context/Dockerfile" +docker buildx build --network=none --progress=plain --output "type=local,dest=$probe/result" "$probe/context" +test -d "$probe/result/context" +if find "$probe/result/context" -type f | grep -Eq '/(applink.json|\.env|install.sh)$'; then + echo 'Private or unrelated files entered the release Docker context' >&2 + exit 1 +fi +echo 'release Docker context excludes private data and unrelated files' diff --git a/scripts/test_install_docs.py b/scripts/test_install_docs.py new file mode 100644 index 000000000..7481266a8 --- /dev/null +++ b/scripts/test_install_docs.py @@ -0,0 +1,138 @@ +"""Run the copyable install blocks with fake downloads and Docker calls.""" +import os +from pathlib import Path +import re +import shlex +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +EN = ROOT / 'docs/native-beta.md' +SV = ROOT / 'docs/setup-guide/update-sv.md' + + +def recipe(path, marker): + blocks = re.findall(r'```bash\n(.*?)\n```', path.read_text(), re.S) + matches = [block for block in blocks if marker in block and 'tag=v0.X.Y-beta.N' in block] + assert len(matches) == 1, (path, marker, len(matches)) + return matches[0] + + +class InstallDocsTest(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory(prefix='ftw-install-docs-') + self.addCleanup(self.tmp.cleanup) + self.work = Path(self.tmp.name) + self.bin = self.work / 'bin' + self.bin.mkdir() + self.log = self.work / 'calls' + self.project = self.work / 'ftw-local' + self.env = dict(os.environ, PATH=f'{self.bin}:{os.environ["PATH"]}', + FTW_TEST_LOG=str(self.log), FTW_TEST_FAIL='') + self.stub('sudo', 'exec "$@"') + self.stub('chown', 'echo "chown $*" >> "$FTW_TEST_LOG"') + self.stub('docker', '''echo "docker $*" >> "$FTW_TEST_LOG" +if [ "$FTW_TEST_FAIL" = buildx ] && [ "$1" = buildx ]; then exit 1; fi''') + self.stub('curl', '''echo "curl $*" >> "$FTW_TEST_LOG" +case "$*" in + *Dockerfile*) [ "$FTW_TEST_FAIL" != dockerfile ] || exit 22 ;; +esac +[ "$FTW_TEST_FAIL" != download ] || exit 22 +while [ "$1" != -o ]; do shift; done +shift +printf '%s\\n' 'echo INSTALL >> "$FTW_TEST_LOG"' > "$1"''') + + def stub(self, name, body): + path = self.bin / name + path.write_text('#!/usr/bin/env bash\nset -eu\n' + body + '\n') + path.chmod(0o755) + + def run_recipe(self, marker, failure='', tag='v0.138.2-beta.1'): + block = recipe(EN, marker).replace('v0.X.Y-beta.N', tag) + # Change only the destination, never the user's HOME or real Docker state. + if marker == 'compose up': + self.assertEqual(block.count('"$HOME/ftw-local"'), 1) + block = block.replace('"$HOME/ftw-local"', shlex.quote(str(self.project))) + self.env['FTW_TEST_FAIL'] = failure + result = subprocess.run(['bash', '-c', block], cwd=self.work, env=self.env, + capture_output=True, text=True) + calls = self.log.read_text() if self.log.exists() else '' + return result, calls + + def test_translations_use_the_same_native_commands(self): + self.assertEqual(recipe(EN, '--fresh-host'), recipe(SV, '--fresh-host')) + + def test_rejects_placeholder_old_line_and_mistyped_tag_before_work(self): + for marker in ('--fresh-host', 'compose up'): + for tag in ('v0.X.Y-beta.N', 'v3.8.0-beta.1', 'v0.130.0', 'v0.0.138-beta.1'): + with self.subTest(marker=marker, tag=tag): + result, calls = self.run_recipe(marker, tag=tag) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(calls, '') + self.assertFalse(self.project.exists()) + + def test_native_download_failure_does_not_run_stale_installer(self): + (self.work / 'install.sh').write_text('echo STALE >> "$FTW_TEST_LOG"\n') + result, calls = self.run_recipe('--fresh-host', 'download') + self.assertNotEqual(result.returncode, 0) + self.assertNotIn('INSTALL', calls) + self.assertNotIn('STALE', calls) + download = shlex.split(calls.strip())[-1] + self.assertFalse(Path(download).exists()) + + def test_native_success_runs_download_and_removes_temp_file(self): + result, calls = self.run_recipe('--fresh-host') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('INSTALL\n', calls) + download = shlex.split(calls.splitlines()[0])[-1] + self.assertFalse(Path(download).exists()) + + def test_failed_docker_download_leaves_no_project_and_allows_retry(self): + for failure in ('download', 'dockerfile'): + with self.subTest(failure=failure): + result, calls = self.run_recipe('compose up', failure) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(self.project.exists()) + self.assertNotIn('chown', calls) + self.assertNotIn('docker compose up', calls) + for call in calls.splitlines(): + if call.startswith('curl '): + self.assertFalse(Path(shlex.split(call)[-1]).parent.exists()) + result, calls = self.run_recipe('compose up') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('docker compose up -d --build', calls) + + def test_existing_project_stays_untouched(self): + self.project.mkdir() + (self.project / '.env').write_text('keep previous version\n') + result, calls = self.run_recipe('compose up') + self.assertNotEqual(result.returncode, 0) + self.assertEqual((self.project / '.env').read_text(), 'keep previous version\n') + self.assertNotIn('curl', calls) + self.assertNotIn('docker compose up', calls) + + def test_missing_buildx_stops_before_creating_project(self): + result, calls = self.run_recipe('compose up', 'buildx') + self.assertNotEqual(result.returncode, 0) + self.assertFalse(self.project.exists()) + self.assertNotIn('curl', calls) + + def test_docker_success_writes_ignore_before_build(self): + # Make the fake builder inspect the inputs at the instant of the build. + self.stub('docker', '''echo "docker $*" >> "$FTW_TEST_LOG" +if [ "$1 ${2:-}" = 'compose up' ]; then + cmp .dockerignore "$FTW_TEST_IGNORE" + test -d data + test -f compose.yaml + test -f Dockerfile + grep -qx 'FTW_VERSION=v0.138.2-beta.1' .env +fi''') + self.env['FTW_TEST_IGNORE'] = str(ROOT / 'deploy/docker/.dockerignore') + result, calls = self.run_recipe('compose up') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('docker compose up -d --build', calls) + + +if __name__ == '__main__': + unittest.main()