From 4b15a7bcabd42369e0088826f2b6c36e80222b14 Mon Sep 17 00:00:00 2001 From: Julien Oculi Date: Wed, 13 Mar 2024 14:46:29 +0100 Subject: [PATCH] initial commit --- .env.example | 5 ++++ .gitignore | 2 ++ README.md | 49 ++++++++++++++++++++++++++++++++++ config.json | 29 ++++++++++++++++++++ deno.json | 20 ++++++++++++++ scripts/flash.ts | 39 +++++++++++++++++++++++++++ scripts/image.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++++ scripts/prepare.ts | 54 +++++++++++++++++++++++++++++++++++++ 8 files changed, 264 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.json create mode 100644 deno.json create mode 100644 scripts/flash.ts create mode 100644 scripts/image.ts create mode 100644 scripts/prepare.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..315eadd --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +USER_PASSWORD="gowest user password on the raspberry pi" + +WIFI_PASSWORD="wifi password" + +SSH_KEY="gowest user ssh public key" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..808147f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +dist/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8f32be --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Raspberry Pi Setup + +Repository to automatically setup raspberry pi img for the **Gowest**. + +## Requirements + +- [rpi-imager](https://www.raspberrypi.com/software/) Official Raspberry Pi + Imager. + + > Make sure `rpi-imager` is in your path + + ```sh + # linux + sudo apt install rpi-imager + + # windows + winget install --id=RaspberryPiFoundation.RaspberryPiImager + + # macos + brew install --cask raspberry-pi-imager + ``` + +- [deno](https://deno.land) Scripts and task runner. + ```sh + # linux + curl -fsSL https://deno.land/install.sh | sh + + # windows + winget install --id=DenoLand.Deno + + # macos + brew install deno + ``` + +## Usage + +1. Fill your `.env` file (see `.env.example`) or pass env through cli. +2. Check and/or edit the `config.json` file. +3. Run `deno task setup` to automatically flash drive for your **Gowest**. + +`setup` task run the following task in order: + +1. `setup:prepare`: Build img install script (create users, set hostname, set + wifi, set ssh, ...). +2. `setup:image`: Download and unzip img. +3. `setup:flash`: Flash boot drive with img and install script. +4. `setup:clean`: Clean all builds outputs. + +Additionally you can write task individually if you already have done one task. diff --git a/config.json b/config.json new file mode 100644 index 0000000..59bb263 --- /dev/null +++ b/config.json @@ -0,0 +1,29 @@ +{ + "image": { + "url": "https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2020-08-24/2020-08-20-raspios-buster-arm64-lite.zip", + "drive": "" + }, + "script": [ + "curl -fsSL https://deno.land/install.sh | sh", + "git clone https://git.cohabit.fr/gowest/gowest.git", + "cd gowest", + "/home/gowest/.deno/bin/deno task start" + ], + "config": { + "host": "gowest.local", + "user": "gowest", + "password": "{{USER_PASSWORD}}" + }, + "network": { + "ssid": "cohabit", + "password": "{{WIFI_PASSWORD}}" + }, + "local": { + "country": "FR", + "timezone": "Europe/Paris", + "keyboard": "fr" + }, + "service": { + "ssh_key": "{{SSH_KEY}}" + } +} diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..6f92528 --- /dev/null +++ b/deno.json @@ -0,0 +1,20 @@ +{ + "tasks": { + "setup": "deno task setup:prepare && deno task setup:image && deno task setup:flash && deno task setup:clean", + "setup:image": "deno run --allow-net --allow-read=dist --allow-write=dist ./scripts/image.ts", + "setup:prepare": "deno run --allow-read=. --allow-write=dist --env=.env --allow-env=USER_PASSWORD,WIFI_PASSWORD,SSH_KEY ./scripts/prepare.ts", + "setup:flash": "deno run --allow-run=rpi-imager ./scripts/flash.ts", + "setup:clean": "rm -rf ./dist" + }, + "fmt": { + "singleQuote": true, + "semiColons": false, + "useTabs": true + }, + "imports": { + "@std/fs": "jsr:@std/fs@^0.219.1", + "@std/json": "jsr:@std/json@^0.219.1", + "@zip-js/zip-js": "jsr:@zip-js/zip-js@^2.7.40" + }, + "lock": false +} diff --git a/scripts/flash.ts b/scripts/flash.ts new file mode 100644 index 0000000..4475dc4 --- /dev/null +++ b/scripts/flash.ts @@ -0,0 +1,39 @@ +import config from '../config.json' with { type: 'json' } + +console.log( + `%c[setup:flash]%c running rpi-imager`, + 'color: royalblue; font-weight: bold', + '', +) +const rpiImager = new Deno.Command('rpi-imager', { + args: [ + '--cli', + '--first-run-script', + './dist/prepare.sh', + './dist/rpi_os.img', + config.image.drive, + ], +}) + +try { + console.log( + `%c[setup:flash]%c flashing img to drive %c${config.image.drive}`, + 'color: royalblue; font-weight: bold', + '', + 'text-decoration: underline', + ) + await rpiImager.spawn().output() + console.log( + `%c[setup:flash]%c drive successfully flashed`, + 'color: green; font-weight: bold', + '', + ) +} catch (error) { + console.error( + `%c[setup:flash]%c can't rpi-imager or return error`, + 'color: red; font-weight: bold', + '', + ) + console.error(error) + Deno.exit(1) +} diff --git a/scripts/image.ts b/scripts/image.ts new file mode 100644 index 0000000..de086bd --- /dev/null +++ b/scripts/image.ts @@ -0,0 +1,66 @@ +import { ensureFile } from '@std/fs' +import { ZipReaderStream } from '@zip-js/zip-js' +import config from '../config.json' with { type: 'json' } + +const imageName = './dist/rpi_os.img' +await ensureFile(imageName) +const image = await Deno.open(imageName, { create: true, write: true }) + +console.log( + `%c[setup:image]%c getting img from %c${config.image.url}`, + 'color: royalblue; font-weight: bold', + '', + 'text-decoration: underline', +) +const archive = await fetch(config.image.url) + +if (archive.ok && archive.body) { + console.log( + `%c[setup:image]%c img found`, + 'color: green; font-weight: bold', + '', + ) +} else { + console.error( + `%c[setup:image]%c no img found or server doesn't respond with 200`, + 'color: red; font-weight: bold', + '', + ) + Deno.exit(1) +} + +console.log( + `%c[setup:image]%c decompressing archive`, + 'color: royalblue; font-weight: bold', + '', +) + +let imgInArchive = false +for await (const file of archive.body.pipeThrough(new ZipReaderStream())) { + if (file.filename.endsWith('.img')) { + imgInArchive = true + console.log( + `%c[setup:image]%c found img file %c${file.filename}`, + 'color: green; font-weight: bold', + '', + 'text-decoration: underline', + ) + console.log( + `%c[setup:image]%c writing file to %c${imageName}`, + 'color: royalblue; font-weight: bold', + '', + 'text-decoration: underline', + ) + await file.readable?.pipeTo(image.writable) + break + } +} + +if (!imgInArchive) { + console.error( + `%c[setup:image]%c no img found in archive`, + 'color: red; font-weight: bold', + '', + ) + Deno.exit(2) +} diff --git a/scripts/prepare.ts b/scripts/prepare.ts new file mode 100644 index 0000000..999337a --- /dev/null +++ b/scripts/prepare.ts @@ -0,0 +1,54 @@ +import { JsonValue } from '@std/json' +import { ensureFile } from '@std/fs' +import piConfig from '../config.json' with { type: 'json' } + +const { config, service, script, network, local } = injectEnv(piConfig) + +const sh = ` +#!/bin/sh + +# configure id +sudo raspi-config nonint do_hostname ${config.host} +sudo useradd -m ${config.user} +echo -e "${config.password}\\n${config.password}" | passwd ${config.user} + +# configure localization +sudo raspi-config nonint do_change_timezone ${local.timezone} +sudo raspi-config nonint do_configure_keyboard ${local.keyboard} +sudo raspi-config nonint do_wifi_country ${local.country} + +# configure network +sudo raspi-config nonint do_wifi_ssid_passphrase ${network.ssid} ${network.password} + +# configure services +sudo raspi-config nonint do_ssh 0 #(enable = 0, disable = 1) +echo -e "${service.ssh_key}" >> /home/${config.user}/.ssh/authorized_keys + +# custom scripts +${script.join('\n')} +` + +const fileName = './dist/prepare.sh' +await ensureFile(fileName) +await Deno.writeTextFile(fileName, sh) + +function injectEnv>(config: T): T { + for (const key in config) { + const value = config[key] + if (typeof value === 'object') { + config[key] = injectEnv( + value as Record, + ) as typeof value + } else if (typeof value === 'string') { + const variables = value.match(/{{\w+}}/g) ?? [] + for (const variable of variables) { + const env = Deno.env.get(variable.slice(2, -2)) + if (env === undefined) { + throw new Error(`env ${variable.slice(2, -2)} is not set`) + } + config[key] = value.replaceAll(variable, env) as typeof value + } + } + } + return config +}