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