initial commit

This commit is contained in:
Julien Oculi 2024-03-13 14:46:29 +01:00
commit 4b15a7bcab
8 changed files with 264 additions and 0 deletions

5
.env.example Normal file
View file

@ -0,0 +1,5 @@
USER_PASSWORD="gowest user password on the raspberry pi"
WIFI_PASSWORD="wifi password"
SSH_KEY="gowest user ssh public key"

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.env
dist/

49
README.md Normal file
View file

@ -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.

29
config.json Normal file
View file

@ -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}}"
}
}

20
deno.json Normal file
View file

@ -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
}

39
scripts/flash.ts Normal file
View file

@ -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)
}

66
scripts/image.ts Normal file
View file

@ -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)
}

54
scripts/prepare.ts Normal file
View file

@ -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<T extends Record<string, JsonValue>>(config: T): T {
for (const key in config) {
const value = config[key]
if (typeof value === 'object') {
config[key] = injectEnv(
value as Record<string, JsonValue>,
) 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
}