initial commit
This commit is contained in:
commit
4b15a7bcab
5
.env.example
Normal file
5
.env.example
Normal 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
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
dist/
|
49
README.md
Normal file
49
README.md
Normal 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
29
config.json
Normal 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
20
deno.json
Normal 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
39
scripts/flash.ts
Normal 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
66
scripts/image.ts
Normal 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
54
scripts/prepare.ts
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue