├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── build-and-deploy.yml │ └── update-flake-lock.yml ├── .gitignore ├── LICENSE ├── README.md ├── default.nix ├── flake.lock ├── flake.nix ├── hardware ├── nuc.nix ├── rp3.nix ├── rp4.nix └── x230.nix ├── host-nuc.nix ├── host-rp3.nix ├── host-rp4.nix ├── installer ├── configuration.nix ├── install.sh └── iso.nix ├── modules ├── alertmanager.nix ├── arcade.nix ├── backup.nix ├── buildMachines.nix ├── cachix.nix ├── camera-rpi-v1 │ ├── default.nix │ ├── overlays │ │ ├── apply-overlays-dtmerge.nix │ │ └── libcamera.nix │ └── rpicam-apps.nix ├── common.nix ├── consul-catalog.nix ├── consul │ ├── base.nix │ ├── client.nix │ └── server.nix ├── git.nix ├── grafana │ ├── dashboard-linter.nix │ ├── dashboards │ │ ├── nixos.json │ │ ├── node-exporter.json │ │ ├── openwrt.json │ │ └── room-temperature-humidity.json │ └── default.nix ├── hydra.nix ├── keys.nix ├── kodi.nix ├── lib │ └── traefik.nix ├── loki.nix ├── mqtt.nix ├── nas.nix ├── nats.nix ├── node-exporter.nix ├── prometheus.nix ├── promtail.nix ├── push-notifications.nix ├── remote-builder │ ├── default.nix │ └── remote-builder.pub ├── server.nix ├── traefik.nix ├── vpn.nix └── webhook.nix ├── nodemcu ├── mqtt-dash.json ├── provision.nix ├── shell.nix └── tasmota.nix ├── overview.svg ├── router ├── config ├── config-repeater ├── setup-repeater.sh └── setup.sh ├── scripts ├── listen-mqtt.sh ├── push-room-humidity.sh ├── push-sunrise-sunset.sh ├── reboot_all.sh ├── switch.sh └── test-alert.sh ├── treefmt.nix ├── x1.nix └── x230.nix /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # git config blame.ignoreRevsFile .git-blame-ignore-revs 2 | # 3 | # After running the above, commits listed in this file will be 4 | # ignored by git blame. The blame will be shifted to the person 5 | # who edited the line(s) before the ignored commit. 6 | # 7 | # To disable this ignorance for a command, run as follows 8 | # git blame --ignore-revs-file="" 9 | # 10 | # https://git-scm.com/docs/git-blame/2.23.0 11 | 12 | # Run nixpkgs-fmt 13 | dca13c594213206feb3e794242c75cffa69a9a50 14 | d262360fef17cd1a22f6f92a8787f15fdcfd0dc1 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | # yamllint disable rule:line-length 2 | --- 3 | name: "Build and Deploy" 4 | on: # yamllint disable-line rule:truthy 5 | workflow_dispatch: # allows manual triggering 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | environment: 16 | name: Homelab 17 | url: "https://app.cachix.org/deploy/workspace/lab.thewagner.home/" 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | with: 21 | fetch-depth: 0 22 | - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 23 | - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31.4.0 24 | with: 25 | nix_path: nixpkgs=channel:nixos-unstable 26 | extra_nix_config: "extra-platforms = aarch64-linux" 27 | - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 28 | with: 29 | name: wagdav 30 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 31 | - name: Check 32 | run: nix flake check 33 | - name: Build 34 | run: nix build --print-build-logs .#cachix-deploy-spec 35 | - name: Deploy 36 | if: github.ref == 'refs/heads/master' 37 | env: 38 | CACHIX_ACTIVATE_TOKEN: "${{ secrets.CACHIX_ACTIVATE_TOKEN }}" 39 | run: | 40 | cachix push wagdav ./result 41 | cachix deploy activate --async ./result 42 | -------------------------------------------------------------------------------- /.github/workflows/update-flake-lock.yml: -------------------------------------------------------------------------------- 1 | # yamllint disable rule:line-length 2 | --- 3 | name: update-flake-lock 4 | on: # yamllint disable-line rule:truthy 5 | workflow_dispatch: # allows manual triggering 6 | schedule: 7 | - cron: '0 0 1 * *' # runs monthly at 00:00 8 | 9 | jobs: 10 | lockfile: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31.4.0 15 | with: 16 | extra_nix_config: | 17 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 18 | - uses: DeterminateSystems/update-flake-lock@428c2b58a4b7414dabd372acb6a03dba1084d3ab # v25 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result 2 | installer/result 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 David Wagner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Homelab 2 | 3 | The configuration of my home infrastructure. 4 | 5 | ![Architectural overview](./overview.svg) 6 | 7 | ## Laptop 8 | 9 | My main laptop, a Lenovo Carbon X1, runs [NixOS](https://nixos.org/). 10 | 11 | Its configuration is specified in `x1.nix` using the [experimental flakes 12 | feature](https://www.tweag.io/blog/2020-07-31-nixos-flakes/). Modify this file 13 | and switch to the new configuration: 14 | 15 | ``` 16 | ./scripts/switch.sh x1 17 | ``` 18 | 19 | By default, this configuration is stored in `/etc/nixos/configuration.nix`. 20 | 21 | For testing purposes you can build a QEMU virtual machine from the configuration: 22 | 23 | ``` 24 | nixos-rebuild build-vm --flake .#x1 && ./result/bin/run-*-vm 25 | ``` 26 | 27 | To update the lock files: 28 | 29 | ``` 30 | nix flake update --update-input nixpkgs --commit-lock-file 31 | ``` 32 | 33 | My [rcfiles](https://github.com/wagdav/rcfiles) repository completes the 34 | configuration of my laptop. Those files live in a separate repository because 35 | I also use them on my work computer which doesn't run NixOS. 36 | 37 | A periodic job backs up my home directory. The remote connection uses an SSH 38 | key which I store in `/root/keys`: 39 | 40 | ``` 41 | sudo ssh-keygen -N '' -t ed25519 -f /root/keys/id_ed25519-borg-x1 42 | ``` 43 | 44 | The [backup server configuration](./modules/backup.nix) references the public 45 | part of this key. 46 | 47 | ## Servers 48 | 49 | The server's configuration is in the `nixosConfigurations` attribute of 50 | [flake.nix](flake.nix). Use [this script](./scripts/switch.sh), a thin 51 | wrapper around `nixos-rebuild`, to build and activate a server's configuration: 52 | 53 | ``` 54 | ./scripts/switch.sh nuc # redeploy the server nuc 55 | ``` 56 | 57 | ### Logs 58 | 59 | All server send their logs to [Loki](https://grafana.com/oss/loki/). To see 60 | all logs live: 61 | 62 | ``` 63 | export LOKI_ADDR=http://loki.thewagner.home 64 | nix shell nixpkgs#grafana-loki --command \ 65 | logcli query '{job="systemd-journald"}' --tail 66 | ``` 67 | 68 | The `query` command takes a 69 | [LogQL](https://grafana.com/docs/loki/latest/logql/) expression as an argument. 70 | 71 | ### Continuous deployment 72 | 73 | Each commit on the master branch is automatically deployed using [Cachix 74 | Deploy](https://blog.cachix.org/posts/2022-07-29-cachix-deploy-public-beta/). 75 | For a detailed description see [this blog 76 | post](https://thewagner.net/blog/2023/11/25/homelab-deployment/). 77 | 78 | To prepare a machine for automatic deployment: 79 | 80 | 1. Add the system's derivation to the `cachix-deploy` package in 81 | [flake.nix](./flake.nix) 82 | 1. Install `cachix-agent` by including [this module](./modules/cachix.nix) 83 | 1. In the Cachix Deploy console follow the "Add Agent" steps 84 | 1. Save the generated agent token in `/etc/cachix-agent.token` using the format 85 | `CACHIX_AGENT_TOKEN=` 86 | 87 | The deployment steps are defined in [this 88 | file](.github/workflows/build-and-deploy.yml). The Cachix Deploy documentation 89 | describe how to configure GitHub Actions. The pipeline uses the following 90 | values as [action 91 | secrets](https://github.com/wagdav/homelab/settings/secrets/actions): 92 | 93 | * `CACHIX_AUTH_TOKEN` 94 | * `CACHIX_ACTIVATE_TOKEN` 95 | 96 | ## Installing a new NixOS system 97 | 98 | Installing a new system takes only a few manual steps. 99 | 100 | Create a customized installer ISO image using the command mentioned at the top 101 | of [installer/iso.nix](installer/iso.nix). 102 | 103 | Copy the ISO image to a USB stick and boot the computer from it. Connect to 104 | the installer using SSH: 105 | 106 | ``` 107 | ssh root@nixos -o StrictHostKeyChecking=no -o 'UserKnownHostsFile /dev/null' 108 | ``` 109 | 110 | Execute the relevant lines from [/etc/install.sh](installer/install.sh) to 111 | partition the disk and create file systems. 112 | 113 | Use the basic configuration from 114 | [/etc/configuration.nix](installer/configuration.nix) as default and set the 115 | hostname. 116 | 117 | Run the installer then reboot the machine. The installation of the basic 118 | system is done. 119 | 120 | Continue the system's management using NixOps. 121 | 122 | ## Useful commands 123 | 124 | The configuration.nix(5) man page documents all the available options for 125 | configuring the system: 126 | 127 | ``` 128 | man configuration.nix 129 | ``` 130 | 131 | All supported options are searchable [online](https://nixos.org/nixos/options.html). 132 | 133 | Query available packages: 134 | 135 | ``` 136 | nix search nixpkgs wget 137 | ``` 138 | 139 | [Install a package](https://nixos.wiki/wiki/Nix_command/profile_install) into 140 | the user's profile 141 | 142 | ``` 143 | nix profile install nixpkgs#firefox 144 | ``` 145 | 146 | Remove old, unreferenced packages, system-wide: 147 | 148 | ``` 149 | sudo nix-collect-garbage 150 | sudo nix-collect-garbage -d # also delete old system old configurations 151 | ``` 152 | 153 | This is documented in the [Cleaning the Nix Store](https://nixos.org/nixos/manual/index.html#sec-nix-gc) 154 | section of the NixOS manual. 155 | 156 | The builtin functions of the Nix evaulator are listed 157 | [here](https://nixos.org/nix/manual/#ssec-builtins). 158 | 159 | See the version of this repository from which the system's configuration was 160 | built: 161 | 162 | ``` 163 | nixos-version --json 164 | ``` 165 | 166 | See which version of a given package will be installed: 167 | 168 | ``` 169 | $ nix eval .#nixosConfigurations.nuc.pkgs.grafana.version 170 | "10.2.4" 171 | ``` 172 | 173 | Evaluate configuration parameters: 174 | 175 | ``` 176 | $ nix eval .#nixosConfigurations.nuc.config.networking.firewall.allowedTCPPorts 177 | [ 22 80 1883 3000 3100 8022 8080 8081 8300 8301 8302 8500 8600 9000 9080 9090 178 | 9093 9100 9883 ] 179 | ``` 180 | 181 | Push a [runtime closure](https://docs.cachix.org/pushing#pushing-runtime-closure) 182 | of a locally built derivation to Cachix: 183 | 184 | ``` 185 | export CACHIX_AUTH_TOKEN=... 186 | nix build -L --json .#nixosConfigurations.rp3.config.system.build.toplevel \ 187 | | nix run nixpkgs#jq -- -r '.[].outputs | to_entries[].value' \ 188 | | nix run nixpkgs#cachix -- push wagdav 189 | ``` 190 | 191 | ## Router 192 | 193 | Linksys WRT ACM-3200 running OpenWRT. 194 | 195 | ### First time setup 196 | 197 | Connect to the router with an Ethernet cable. 198 | 199 | Download and install the [OpenWRT 200 | firmware](https://openwrt.org/toh/linksys/wrt3200acm) then run: 201 | 202 | ``` 203 | router/setup.sh first-time 204 | ``` 205 | 206 | Reboot the router. 207 | 208 | ### Customizations 209 | 210 | Change the settings in `router/config` and run 211 | 212 | ``` 213 | router/setup.sh 214 | ``` 215 | 216 | ## Raspberry Pi 3 Model B 217 | 218 | ### SD card image 219 | 220 | Build the Raspberry Pi's SD card image using QEMU's aarch64 emulator. 221 | 222 | On `x230`, because `nuc` [is configured](./hardware/nuc.nix) as a remote builder 223 | for `aarch64` packages, just run: 224 | 225 | ``` 226 | nix build .#packages.aarch64-linux.sdcard 227 | ``` 228 | 229 | On other hosts, specify `nuc` explicitly as a remote builder: 230 | 231 | ``` 232 | nix build -L .#packages.aarch64-linux.sdcard \ 233 | --builders "ssh://root@nuc aarch64-linux $HOME/.ssh/remote-builder 4 1 - - c3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSUlLYUV0YzhQTnFoeEFRMjRnWTV0MjVZLzhIVTZTdFVCNmttVTF4bVZ0YTcgcm9vdEBudWMK" 234 | ``` 235 | 236 | The elements of `--builders` argument are described [here][NixOSRemoteBuilds]. 237 | 238 | Uncompress the built image and write it to an SD card: 239 | 240 | ``` 241 | unzstd ./result/sd-image/nixos-sd-image*.zst -o nixos-sd-image.img 242 | sudo dd if=nixos-sd-image.img of=/dev/mmcblk0 bs=4096 conv=fsync status=progress 243 | ``` 244 | 245 | Insert the SD card in the Raspberry Pi and power it up. The system is 246 | configured as defined in [host-rp3.nix](./host-rp3.nix). 247 | 248 | ### Secrets 249 | 250 | If the SD card is build from scratch, change or provision the following 251 | secrets: 252 | 253 | * Host's identity (automatically generated on first boot) 254 | * WiFi SSID and password in `/etc/secrets/wireless.env` 255 | * Tailscale authentication token 256 | * Cachix authentication token 257 | 258 | If this is a complete reinstall, update the host's public key in 259 | [program.ssh.knownHosts](./modules/buildMachines.nix). Run `ssh-keyscan rp3` to 260 | obtain the host key's signature. 261 | 262 | Store the WIFI SSID and password in the file `/etc/secrets/wireless.env` with 263 | the following format: 264 | 265 | ``` 266 | WIFI_SSID=... 267 | WIFI_KEY=... 268 | ``` 269 | 270 | Connect the host to the tailnet with `tailscale login`. 271 | 272 | To connect Cachix, follow [these instructions](#continuous-deployment). 273 | 274 | ### Raspberry Pi Camera 1.3 275 | 276 | [This article](https://thewagner.net/blog/2024/07/31/raspberry-pi-camera-on-nixos/), 277 | describes how I configured my Raspberry Pi v1 camera module on my Raspberry Pi 278 | 3 running NixOS. 279 | 280 | ### Reference 281 | 282 | I found the following links useful: 283 | 284 | * [nix.dev](https://nix.dev/tutorials/nixos/installing-nixos-on-a-raspberry-pi.html) 285 | on installing NixOS on the Raspberry Pi. 286 | * [Hydra](https://hydra.nixos.org/search?query=sd_image) hosts the official 287 | NixOS SD card images. 288 | 289 | ## NodeMCU 290 | 291 | I have a couple of NodeMCU boards which can be configured using the scripts in 292 | the [nodemcu](nodemcu) directory. 293 | 294 | Enter a Nix shell 295 | 296 | ``` 297 | cd nodemcu 298 | nix-shell 299 | ``` 300 | 301 | In this shell the following helper functions are available. 302 | 303 | Erase everything from the device and start from scratch: 304 | 305 | * `flash_erase`: Perform Chip Erase on SPI flash 306 | * `flash_write`: Write the [Tasmota firmware]( 307 | https://github.com/arendst/Tasmota) to the device 308 | 309 | Open an interactive serial terminal: 310 | 311 | ``` 312 | serial_terminal 313 | ``` 314 | 315 | Restore the firmware's factory settings: 316 | 317 | ``` 318 | device_reset | commit 319 | ``` 320 | 321 | Configure a device 322 | 323 | ``` 324 | device_config | commit 325 | ``` 326 | 327 | These commands are defined as [shell hooks in shell.nix](./nodemcu/shell.nix) 328 | 329 | ### Provisioning 330 | 331 | The best way I found to provision the ESP8266 systems with custom firmware is 332 | through MQTT because it's not always easy to get access to a serial terminal. 333 | 334 | Use the serial console or the web interface to connect the device to the WiFi 335 | and to the MQTT broker. 336 | 337 | Build and run any of the [provisioning scripts](nodemcu/provision.nix): 338 | 339 | ```shell 340 | nix build .#sensors && ./result/tasmota_082320.sh 341 | ``` 342 | 343 | This will reconfigure the specified sensor by sending 344 | [commands](https://tasmota.github.io/docs/Commands/) over MQTT. 345 | 346 | ### Dashboard 347 | 348 | On my mobile I created a dashboard using [MQTT Dash](https://play.google.com/store/apps/details?id=net.routix.mqttdash&gl=US). 349 | 350 | To update the [dashboard configuration](nodemcu/mqtt-dash.json) file, use the 351 | Import/Export functionality of the app and publish the dashboard state to an 352 | MQTT topic (the default is `metrics/exchange`). 353 | 354 | The following command listens to the published configuration and updates the 355 | dashboard configuration in this repository: 356 | 357 | ``` 358 | nix run .#mqtt-dash-listen > nodemcu/mqtt-dash.json 359 | ``` 360 | 361 | ### Troubleshooting 362 | 363 | The device `tasmota_0E63DE` couldn't connect to the Wi-Fi network. I believe 364 | this is because the 2.4GHz and 5GHz networks share the same SSID. I flashed 365 | the latest tasmota firmware (14.1.0) on the device and selected the 2.4GHz 366 | connection with the command: 367 | 368 | ``` 369 | Wifi 3 370 | ``` 371 | 372 | which corresponds to the Wi-Fi mode 802.11b/g (2.4 GHz). 373 | 374 | [NixOSBootWifi]: https://nixos.org/manual/nixos/stable/#sec-installation-booting-networking 375 | [NixOSRemoteBuilds]: https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds.html?highlight=builders#remote-builds 376 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in 4 | fetchTarball { 5 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 6 | sha256 = lock.nodes.flake-compat.locked.narHash; 7 | } 8 | ) 9 | { src = ./.; } 10 | ).defaultNix 11 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "cachix-deploy": { 4 | "inputs": { 5 | "darwin": "darwin", 6 | "disko": "disko", 7 | "home-manager": "home-manager", 8 | "nixos-anywhere": "nixos-anywhere", 9 | "nixpkgs": "nixpkgs" 10 | }, 11 | "locked": { 12 | "lastModified": 1728048122, 13 | "narHash": "sha256-2P7BjsQHpAjp+zjftGXSGwo0gepR79KJbBNRKJxsUyk=", 14 | "owner": "cachix", 15 | "repo": "cachix-deploy-flake", 16 | "rev": "f363e7ba6661f0e342707b98224c85599fdfb1cc", 17 | "type": "github" 18 | }, 19 | "original": { 20 | "owner": "cachix", 21 | "repo": "cachix-deploy-flake", 22 | "type": "github" 23 | } 24 | }, 25 | "darwin": { 26 | "inputs": { 27 | "nixpkgs": [ 28 | "cachix-deploy", 29 | "nixpkgs" 30 | ] 31 | }, 32 | "locked": { 33 | "lastModified": 1727999297, 34 | "narHash": "sha256-LTJuQPCsSItZ/8TieFeP30iY+uaLoD0mT0tAj1gLeyQ=", 35 | "owner": "LnL7", 36 | "repo": "nix-darwin", 37 | "rev": "8c8388ade72e58efdeae71b4cbb79e872c23a56b", 38 | "type": "github" 39 | }, 40 | "original": { 41 | "owner": "LnL7", 42 | "repo": "nix-darwin", 43 | "type": "github" 44 | } 45 | }, 46 | "disko": { 47 | "inputs": { 48 | "nixpkgs": [ 49 | "cachix-deploy", 50 | "nixpkgs" 51 | ] 52 | }, 53 | "locked": { 54 | "lastModified": 1727977578, 55 | "narHash": "sha256-DBORKcmQ7ZjA4qE1MsnF1MmZSokOGrw4W9vTCioOv2U=", 56 | "owner": "nix-community", 57 | "repo": "disko", 58 | "rev": "574400001b3ffe555c7a21e0ff846230759be2ed", 59 | "type": "github" 60 | }, 61 | "original": { 62 | "owner": "nix-community", 63 | "repo": "disko", 64 | "type": "github" 65 | } 66 | }, 67 | "disko_2": { 68 | "inputs": { 69 | "nixpkgs": [ 70 | "nixpkgs" 71 | ] 72 | }, 73 | "locked": { 74 | "lastModified": 1748225455, 75 | "narHash": "sha256-AzlJCKaM4wbEyEpV3I/PUq5mHnib2ryEy32c+qfj6xk=", 76 | "owner": "nix-community", 77 | "repo": "disko", 78 | "rev": "a894f2811e1ee8d10c50560551e50d6ab3c392ba", 79 | "type": "github" 80 | }, 81 | "original": { 82 | "owner": "nix-community", 83 | "repo": "disko", 84 | "type": "github" 85 | } 86 | }, 87 | "flake-compat": { 88 | "flake": false, 89 | "locked": { 90 | "lastModified": 1747046372, 91 | "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", 92 | "owner": "edolstra", 93 | "repo": "flake-compat", 94 | "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", 95 | "type": "github" 96 | }, 97 | "original": { 98 | "owner": "edolstra", 99 | "repo": "flake-compat", 100 | "type": "github" 101 | } 102 | }, 103 | "flake-parts": { 104 | "inputs": { 105 | "nixpkgs-lib": [ 106 | "cachix-deploy", 107 | "nixos-anywhere", 108 | "nixpkgs" 109 | ] 110 | }, 111 | "locked": { 112 | "lastModified": 1726153070, 113 | "narHash": "sha256-HO4zgY0ekfwO5bX0QH/3kJ/h4KvUDFZg8YpkNwIbg1U=", 114 | "owner": "hercules-ci", 115 | "repo": "flake-parts", 116 | "rev": "bcef6817a8b2aa20a5a6dbb19b43e63c5bf8619a", 117 | "type": "github" 118 | }, 119 | "original": { 120 | "owner": "hercules-ci", 121 | "repo": "flake-parts", 122 | "type": "github" 123 | } 124 | }, 125 | "home-manager": { 126 | "inputs": { 127 | "nixpkgs": [ 128 | "cachix-deploy", 129 | "nixpkgs" 130 | ] 131 | }, 132 | "locked": { 133 | "lastModified": 1728041527, 134 | "narHash": "sha256-03liqiJtk9UP7YQHW4r8MduKCK242FQzud8iWvvlK+o=", 135 | "owner": "nix-community", 136 | "repo": "home-manager", 137 | "rev": "509dbf8d45606b618e9ec3bbe4e936b7c5bc6c1e", 138 | "type": "github" 139 | }, 140 | "original": { 141 | "owner": "nix-community", 142 | "repo": "home-manager", 143 | "type": "github" 144 | } 145 | }, 146 | "nixlib": { 147 | "locked": { 148 | "lastModified": 1736643958, 149 | "narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=", 150 | "owner": "nix-community", 151 | "repo": "nixpkgs.lib", 152 | "rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181", 153 | "type": "github" 154 | }, 155 | "original": { 156 | "owner": "nix-community", 157 | "repo": "nixpkgs.lib", 158 | "type": "github" 159 | } 160 | }, 161 | "nixos-anywhere": { 162 | "inputs": { 163 | "disko": [ 164 | "cachix-deploy", 165 | "disko" 166 | ], 167 | "flake-parts": "flake-parts", 168 | "nixos-images": "nixos-images", 169 | "nixos-stable": "nixos-stable", 170 | "nixpkgs": [ 171 | "cachix-deploy", 172 | "nixpkgs" 173 | ], 174 | "treefmt-nix": "treefmt-nix" 175 | }, 176 | "locked": { 177 | "lastModified": 1727531568, 178 | "narHash": "sha256-lt8fmizvl6iRDNz7/Yqor1MmU5fcUyv3oajtUsUmthA=", 179 | "owner": "numtide", 180 | "repo": "nixos-anywhere", 181 | "rev": "b6168ba67a8fad0636b5111a906dfbdf3abe2dee", 182 | "type": "github" 183 | }, 184 | "original": { 185 | "owner": "numtide", 186 | "repo": "nixos-anywhere", 187 | "type": "github" 188 | } 189 | }, 190 | "nixos-generators": { 191 | "inputs": { 192 | "nixlib": "nixlib", 193 | "nixpkgs": [ 194 | "nixpkgs" 195 | ] 196 | }, 197 | "locked": { 198 | "lastModified": 1747663185, 199 | "narHash": "sha256-Obh50J+O9jhUM/FgXtI3he/QRNiV9+J53+l+RlKSaAk=", 200 | "owner": "nix-community", 201 | "repo": "nixos-generators", 202 | "rev": "ee07ba0d36c38e9915c55d2ac5a8fb0f05f2afcc", 203 | "type": "github" 204 | }, 205 | "original": { 206 | "owner": "nix-community", 207 | "repo": "nixos-generators", 208 | "type": "github" 209 | } 210 | }, 211 | "nixos-hardware": { 212 | "locked": { 213 | "lastModified": 1748634340, 214 | "narHash": "sha256-pZH4bqbOd8S+si6UcfjHovWDiWKiIGRNRMpmRWaDIms=", 215 | "owner": "NixOS", 216 | "repo": "nixos-hardware", 217 | "rev": "daa628a725ab4948e0e2b795e8fb6f4c3e289a7a", 218 | "type": "github" 219 | }, 220 | "original": { 221 | "owner": "NixOS", 222 | "repo": "nixos-hardware", 223 | "type": "github" 224 | } 225 | }, 226 | "nixos-images": { 227 | "inputs": { 228 | "nixos-stable": [ 229 | "cachix-deploy", 230 | "nixos-anywhere", 231 | "nixos-stable" 232 | ], 233 | "nixos-unstable": [ 234 | "cachix-deploy", 235 | "nixos-anywhere", 236 | "nixpkgs" 237 | ] 238 | }, 239 | "locked": { 240 | "lastModified": 1727367213, 241 | "narHash": "sha256-7O4pi8MmcJpA0nYUQkdolvKGyu6zNjf2gFYD1Q0xppc=", 242 | "owner": "nix-community", 243 | "repo": "nixos-images", 244 | "rev": "3e7978bab153f39f3fc329ad346d35a8871420f7", 245 | "type": "github" 246 | }, 247 | "original": { 248 | "owner": "nix-community", 249 | "repo": "nixos-images", 250 | "type": "github" 251 | } 252 | }, 253 | "nixos-stable": { 254 | "locked": { 255 | "lastModified": 1727264057, 256 | "narHash": "sha256-KQPI8CTTnB9CrJ7LrmLC4VWbKZfljEPBXOFGZFRpxao=", 257 | "owner": "NixOS", 258 | "repo": "nixpkgs", 259 | "rev": "759537f06e6999e141588ff1c9be7f3a5c060106", 260 | "type": "github" 261 | }, 262 | "original": { 263 | "owner": "NixOS", 264 | "ref": "nixos-24.05", 265 | "repo": "nixpkgs", 266 | "type": "github" 267 | } 268 | }, 269 | "nixpkgs": { 270 | "locked": { 271 | "lastModified": 1727998858, 272 | "narHash": "sha256-IeBVJ75Bd7yWz8i3m225x5Q25O1Wk8cBWi8DI7bCgSo=", 273 | "owner": "NixOS", 274 | "repo": "nixpkgs", 275 | "rev": "73bed75dbd3de6d4fca3f81ce25a0cc7766afff6", 276 | "type": "github" 277 | }, 278 | "original": { 279 | "id": "nixpkgs", 280 | "type": "indirect" 281 | } 282 | }, 283 | "nixpkgs_2": { 284 | "locked": { 285 | "lastModified": 1748437600, 286 | "narHash": "sha256-hYKMs3ilp09anGO7xzfGs3JqEgUqFMnZ8GMAqI6/k04=", 287 | "owner": "NixOS", 288 | "repo": "nixpkgs", 289 | "rev": "7282cb574e0607e65224d33be8241eae7cfe0979", 290 | "type": "github" 291 | }, 292 | "original": { 293 | "owner": "NixOS", 294 | "ref": "nixos-25.05", 295 | "repo": "nixpkgs", 296 | "type": "github" 297 | } 298 | }, 299 | "root": { 300 | "inputs": { 301 | "cachix-deploy": "cachix-deploy", 302 | "disko": "disko_2", 303 | "flake-compat": "flake-compat", 304 | "nixos-generators": "nixos-generators", 305 | "nixos-hardware": "nixos-hardware", 306 | "nixpkgs": "nixpkgs_2", 307 | "treefmt-nix": "treefmt-nix_2" 308 | } 309 | }, 310 | "treefmt-nix": { 311 | "inputs": { 312 | "nixpkgs": [ 313 | "cachix-deploy", 314 | "nixos-anywhere", 315 | "nixpkgs" 316 | ] 317 | }, 318 | "locked": { 319 | "lastModified": 1727252110, 320 | "narHash": "sha256-3O7RWiXpvqBcCl84Mvqa8dXudZ1Bol1ubNdSmQt7nF4=", 321 | "owner": "numtide", 322 | "repo": "treefmt-nix", 323 | "rev": "1bff2ba6ec22bc90e9ad3f7e94cca0d37870afa3", 324 | "type": "github" 325 | }, 326 | "original": { 327 | "owner": "numtide", 328 | "repo": "treefmt-nix", 329 | "type": "github" 330 | } 331 | }, 332 | "treefmt-nix_2": { 333 | "inputs": { 334 | "nixpkgs": [ 335 | "nixpkgs" 336 | ] 337 | }, 338 | "locked": { 339 | "lastModified": 1748243702, 340 | "narHash": "sha256-9YzfeN8CB6SzNPyPm2XjRRqSixDopTapaRsnTpXUEY8=", 341 | "owner": "numtide", 342 | "repo": "treefmt-nix", 343 | "rev": "1f3f7b784643d488ba4bf315638b2b0a4c5fb007", 344 | "type": "github" 345 | }, 346 | "original": { 347 | "owner": "numtide", 348 | "repo": "treefmt-nix", 349 | "type": "github" 350 | } 351 | } 352 | }, 353 | "root": "root", 354 | "version": 7 355 | } 356 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.flake-compat = { 3 | url = "github:edolstra/flake-compat"; 4 | flake = false; 5 | }; 6 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; 7 | inputs.nixos-hardware.url = "github:NixOS/nixos-hardware"; 8 | inputs.disko = { 9 | url = "github:nix-community/disko"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | }; 12 | inputs.cachix-deploy.url = "github:cachix/cachix-deploy-flake"; 13 | inputs.nixos-generators = { 14 | url = "github:nix-community/nixos-generators"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | }; 17 | inputs.treefmt-nix = { 18 | url = "github:numtide/treefmt-nix"; 19 | inputs.nixpkgs.follows = "nixpkgs"; 20 | }; 21 | 22 | outputs = { self, disko, flake-compat, nixpkgs, nixos-generators, nixos-hardware, cachix-deploy, treefmt-nix }@attrs: 23 | let 24 | system = "x86_64-linux"; 25 | 26 | pkgs = nixpkgs.legacyPackages.${system}; 27 | 28 | cachix-deploy-lib = cachix-deploy.lib pkgs; 29 | 30 | revision = "${self.lastModifiedDate}-${self.shortRev or "dirty"}"; 31 | 32 | mkMachine = system: modules: nixpkgs.lib.nixosSystem { 33 | inherit system; 34 | modules = modules ++ [ ./modules/common.nix ]; 35 | specialArgs = attrs; 36 | }; 37 | 38 | mqtt-dash-listen = pkgs.writeScriptBin "mqtt-dash-listen" '' 39 | echo 1>&2 "Press Publish Metrics in the MQTT Dash app..." 40 | ${pkgs.mosquitto}/bin/mosquitto_sub -h mqtt -t 'metrics/exchange' -C 1 | ${pkgs.jq}/bin/jq -r . 41 | ''; 42 | 43 | dashboard-linter = pkgs.callPackage ./modules/grafana/dashboard-linter.nix { }; 44 | 45 | treefmtEval = treefmt-nix.lib.evalModule pkgs ./treefmt.nix; 46 | 47 | in 48 | { 49 | nixosConfigurations = { 50 | x1 = mkMachine "x86_64-linux" [ ./x1.nix ]; 51 | x230 = mkMachine "x86_64-linux" [ ./x230.nix ]; 52 | nuc = mkMachine "x86_64-linux" [ ./host-nuc.nix ]; 53 | rp3 = mkMachine "aarch64-linux" [ ./host-rp3.nix ]; 54 | rp4 = mkMachine "aarch64-linux" [ ./host-rp4.nix ]; 55 | }; 56 | 57 | formatter.${system} = treefmtEval.config.build.wrapper; 58 | 59 | apps.${system} = { 60 | mqtt-dash-listen = { 61 | type = "app"; 62 | program = "${mqtt-dash-listen}/bin/mqtt-dash-listen"; 63 | }; 64 | 65 | dashboard-linter = { 66 | type = "app"; 67 | program = "${dashboard-linter}/bin/dashboard-linter"; 68 | }; 69 | }; 70 | 71 | packages.${system} = with pkgs; { 72 | sensors = callPackage ./nodemcu/provision.nix { }; 73 | cachix-deploy-spec = cachix-deploy-lib.spec { 74 | agents = { 75 | nuc = self.nixosConfigurations.nuc.config.system.build.toplevel; 76 | x1 = self.nixosConfigurations.x1.config.system.build.toplevel; 77 | x230 = self.nixosConfigurations.x230.config.system.build.toplevel; 78 | rp3 = self.nixosConfigurations.rp3.config.system.build.toplevel; 79 | rp4 = self.nixosConfigurations.rp4.config.system.build.toplevel; 80 | }; 81 | }; 82 | }; 83 | 84 | packages.aarch64-linux = { 85 | sdcard = nixos-generators.nixosGenerate { 86 | system = "aarch64-linux"; 87 | format = "sd-aarch64"; 88 | specialArgs = attrs; 89 | modules = [ ./host-rp3.nix ]; 90 | }; 91 | 92 | sdcard-rp4 = nixos-generators.nixosGenerate { 93 | system = "aarch64-linux"; 94 | format = "sd-aarch64"; 95 | specialArgs = attrs; 96 | modules = [ ./host-rp4.nix ]; 97 | }; 98 | }; 99 | 100 | checks.${system} = with pkgs; { 101 | formatting = treefmtEval.config.build.check self; 102 | 103 | markdownlint = runCommand "mdl" 104 | { 105 | buildInputs = [ mdl ]; 106 | } 107 | '' 108 | mkdir $out 109 | mdl ${./README.md} 110 | ''; 111 | 112 | shellcheck = runCommand "shellcheck" 113 | { 114 | buildInputs = [ shellcheck ]; 115 | } 116 | '' 117 | mkdir $out 118 | shellcheck --shell bash ${./scripts}/* 119 | ''; 120 | }; 121 | }; 122 | } 123 | -------------------------------------------------------------------------------- /hardware/nuc.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | let 3 | 4 | name = "nuc"; 5 | 6 | in 7 | { 8 | nix.settings.max-jobs = lib.mkDefault 4; 9 | 10 | boot = { 11 | initrd = { 12 | availableKernelModules = [ 13 | "ahci" 14 | "rtsx_pci_sdmmc" 15 | "sd_mod" 16 | "usb_storage" 17 | "xhci_pci" 18 | ]; 19 | 20 | kernelModules = [ ]; 21 | }; 22 | 23 | kernelModules = [ "kvm-intel" ]; 24 | 25 | extraModulePackages = [ ]; 26 | 27 | # Use the systemd-boot EFI boot loader. 28 | loader = { 29 | systemd-boot.enable = true; 30 | efi.canTouchEfiVariables = true; 31 | }; 32 | 33 | binfmt.emulatedSystems = [ "aarch64-linux" ]; 34 | }; 35 | 36 | fileSystems = { 37 | "/boot" = 38 | { 39 | device = "/dev/disk/by-label/boot"; 40 | fsType = "vfat"; 41 | }; 42 | 43 | "/" = 44 | { 45 | device = "/dev/disk/by-label/nixos"; 46 | fsType = "ext4"; 47 | }; 48 | }; 49 | 50 | swapDevices = [{ device = "/dev/disk/by-label/swap"; }]; 51 | 52 | networking = { 53 | hostName = name; 54 | useDHCP = false; 55 | 56 | interfaces = { 57 | eno1.useDHCP = true; 58 | wlp58s0.useDHCP = true; 59 | }; 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /hardware/rp3.nix: -------------------------------------------------------------------------------- 1 | # Raspberry Pi 3 2 | { config, lib, pkgs, ... }: 3 | let 4 | 5 | name = "rp3"; 6 | 7 | in 8 | { 9 | nix.settings.max-jobs = lib.mkDefault 4; 10 | nixpkgs.system = "aarch64-linux"; 11 | 12 | networking.wireless = { 13 | enable = true; 14 | secretsFile = "/etc/secrets/wireless.env"; 15 | networks."Eat-Knit-Code-Repeat".pskRaw = "ext:WIFI_KEY"; 16 | interfaces = [ "wlan0" ]; 17 | }; 18 | 19 | hardware.enableRedistributableFirmware = true; 20 | 21 | services.journald.extraConfig = '' 22 | Storage = volatile 23 | RuntimeMaxFileSize = 10M 24 | ''; 25 | 26 | fileSystems."/" = 27 | { 28 | device = "/dev/disk/by-label/NIXOS_SD"; 29 | fsType = "ext4"; 30 | }; 31 | 32 | networking = { 33 | hostName = name; 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /hardware/rp4.nix: -------------------------------------------------------------------------------- 1 | # Raspberry Pi 4 2 | { config, lib, pkgs, ... }: 3 | let 4 | 5 | name = "rp4"; 6 | 7 | in 8 | { 9 | nixpkgs.system = "aarch64-linux"; 10 | 11 | services.journald.extraConfig = '' 12 | Storage = volatile 13 | RuntimeMaxFileSize = 10M 14 | ''; 15 | 16 | fileSystems."/" = 17 | { 18 | device = "/dev/disk/by-label/NIXOS_SD"; 19 | fsType = "ext4"; 20 | }; 21 | 22 | services.pulseaudio.enable = true; 23 | 24 | networking = { 25 | hostName = name; 26 | }; 27 | 28 | networking.wireless = { 29 | enable = true; 30 | secretsFile = "/etc/secrets/wireless.env"; 31 | networks."Eat-Knit-Code-Repeat".pskRaw = "ext:WIFI_KEY"; 32 | interfaces = [ "wlan0" ]; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /hardware/x230.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | { 4 | boot.initrd.availableKernelModules = [ "xhci_pci" "ehci_pci" "ahci" "usb_storage" "sd_mod" "sdhci_pci" ]; 5 | boot.initrd.kernelModules = [ ]; 6 | boot.blacklistedKernelModules = [ ]; 7 | boot.kernelModules = [ "kvm-intel" ]; 8 | boot.extraModulePackages = [ ]; 9 | 10 | fileSystems."/" = 11 | { 12 | device = "/dev/disk/by-uuid/03c05626-c162-4ad7-94ca-657b0c8b8e3f"; 13 | fsType = "ext4"; 14 | }; 15 | 16 | fileSystems."/boot" = 17 | { 18 | device = "/dev/disk/by-uuid/54FA-AB2F"; 19 | fsType = "vfat"; 20 | }; 21 | 22 | swapDevices = 23 | [ 24 | { device = "/dev/disk/by-uuid/335bd258-d037-45d4-ab0d-44bca8a1c31b"; } 25 | ]; 26 | 27 | nix.settings.max-jobs = lib.mkDefault 4; 28 | powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; 29 | } 30 | -------------------------------------------------------------------------------- /host-nuc.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | imports = [ 5 | ./hardware/nuc.nix 6 | ./modules/alertmanager.nix 7 | ./modules/backup.nix 8 | ./modules/cachix.nix 9 | ./modules/consul/server.nix 10 | ./modules/grafana 11 | ./modules/loki.nix 12 | ./modules/mqtt.nix 13 | ./modules/prometheus.nix 14 | ./modules/push-notifications.nix 15 | ./modules/remote-builder 16 | ./modules/server.nix 17 | ./modules/traefik.nix 18 | ./modules/vpn.nix 19 | ./modules/webhook.nix 20 | ]; 21 | 22 | services.tailscale = { 23 | useRoutingFeatures = "server"; 24 | extraUpFlags = "--advertise-exit-node"; 25 | }; 26 | 27 | containers.git = { 28 | autoStart = true; 29 | macvlans = [ "eno1" ]; 30 | bindMounts = { 31 | "/srv/git" = { 32 | hostPath = "/srv/git"; 33 | isReadOnly = false; 34 | }; 35 | }; 36 | config = 37 | { config, lib, ... }: 38 | { 39 | imports = [ 40 | ./modules/git.nix 41 | ./modules/vpn.nix 42 | ]; 43 | networking.useDHCP = lib.mkForce true; 44 | system.stateVersion = "24.05"; 45 | services.tailscale.interfaceName = "userspace-networking"; 46 | }; 47 | }; 48 | 49 | containers.nats = { 50 | autoStart = true; 51 | macvlans = [ "eno1" ]; 52 | config = 53 | { config, lib, ... }: 54 | { 55 | imports = [ 56 | ./modules/nats.nix 57 | ]; 58 | networking.useDHCP = lib.mkForce true; 59 | system.stateVersion = "24.05"; 60 | }; 61 | }; 62 | 63 | services.borgbackup.jobs.git = { 64 | paths = "/srv/git"; 65 | repo = "borg@nuc:."; 66 | environment = { BORG_RSH = "ssh -i /root/keys/id_ed25519-borg-git"; }; 67 | encryption.mode = "none"; 68 | doInit = false; 69 | startAt = "daily"; 70 | prune.keep = { 71 | within = "1d"; 72 | daily = 7; 73 | weekly = 4; 74 | monthly = 12; 75 | yearly = 10; 76 | }; 77 | }; 78 | 79 | system.stateVersion = "22.05"; 80 | } 81 | -------------------------------------------------------------------------------- /host-rp3.nix: -------------------------------------------------------------------------------- 1 | { config, nixos-hardware, ... }: 2 | 3 | { 4 | imports = [ 5 | nixos-hardware.nixosModules.raspberry-pi-3 6 | ./hardware/rp3.nix 7 | ./modules/cachix.nix 8 | ./modules/camera-rpi-v1 9 | ./modules/consul/client.nix 10 | ./modules/remote-builder 11 | ./modules/server.nix 12 | ./modules/vpn.nix 13 | ]; 14 | 15 | system.stateVersion = "23.11"; 16 | } 17 | -------------------------------------------------------------------------------- /host-rp4.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, nixos-hardware, ... }: 2 | 3 | { 4 | imports = [ 5 | nixos-hardware.nixosModules.raspberry-pi-4 6 | ./hardware/rp4.nix 7 | ./modules/cachix.nix 8 | ./modules/consul/client.nix 9 | ./modules/remote-builder 10 | ./modules/server.nix 11 | ./modules/vpn.nix 12 | ]; 13 | 14 | system.stateVersion = "23.11"; 15 | 16 | hardware = { 17 | raspberry-pi."4".apply-overlays-dtmerge.enable = true; 18 | deviceTree.enable = true; 19 | }; 20 | 21 | nixpkgs.overlays = [ 22 | (final: super: { 23 | makeModulesClosure = x: 24 | super.makeModulesClosure (x // { allowMissing = true; }); 25 | }) 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /installer/configuration.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | 3 | { 4 | imports = 5 | [ 6 | # Include the results of the hardware scan. 7 | ./hardware-configuration.nix 8 | ]; 9 | 10 | # Use the systemd-boot EFI boot loader. 11 | boot.loader.systemd-boot.enable = true; 12 | boot.loader.efi.canTouchEfiVariables = true; 13 | 14 | services.openssh.enable = true; 15 | users.users.root.openssh.authorizedKeys.keyFiles = [ /etc/ssh/authorized_keys.d/root ]; 16 | 17 | networking.hostName = "nixos"; # Define your hostname. 18 | } 19 | -------------------------------------------------------------------------------- /installer/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | parted /dev/sda -- mklabel gpt 6 | parted /dev/sda -- mkpart primary 512MiB -8GiB 7 | parted /dev/sda -- mkpart primary linux-swap -8GiB 100% 8 | parted /dev/sda -- mkpart ESP fat32 1MiB 512MiB 9 | parted /dev/sda -- set 3 boot on 10 | 11 | mkfs.ext4 -L nixos /dev/sda1 12 | mkswap -L swap /dev/sda2 13 | swapon /dev/sda2 14 | mkfs.fat -F 32 -n boot /dev/sda3 # (for UEFI systems only) 15 | mount /dev/disk/by-label/nixos /mnt 16 | mkdir -p /mnt/boot # (for UEFI systems only) 17 | mount /dev/disk/by-label/boot /mnt/boot # (for UEFI systems only) 18 | 19 | #nixos-generate-config --root /mnt 20 | 21 | cp --no-clobber /etc/configuration.nix /mnt/etc/nixos/configuration.nix 22 | 23 | # Edit the configuration 24 | 25 | #vi /mnt/etc/nixos/configuration.nix 26 | 27 | # When ready, install the system and reboot 28 | 29 | #nixos-install 30 | #reboot 31 | -------------------------------------------------------------------------------- /installer/iso.nix: -------------------------------------------------------------------------------- 1 | # To build the installer for your system's architecture: 2 | # 3 | # nix-build '' -A config.system.build.isoImage -I nixos-config=iso.nix 4 | # 5 | # To build a 32-bit installer, overrride the value of the `system` parameter: 6 | # 7 | # nix-build --argStr system i686-linux 8 | # 9 | 10 | { config, pkgs, system ? builtins.currentSystem, ... }: 11 | 12 | { 13 | imports = [ 14 | # https://nixos.wiki/wiki/Creating_a_NixOS_live_CD 15 | 16 | 17 | ]; 18 | 19 | systemd.services.sshd.wantedBy = pkgs.lib.mkForce [ "multi-user.target" ]; 20 | users = { 21 | mutableUsers = false; 22 | users.root.openssh.authorizedKeys.keyFiles = [ ~/.ssh/id_rsa.pub ]; 23 | }; 24 | 25 | environment.etc = { 26 | "install.sh" = { 27 | source = ./install.sh; 28 | mode = "0700"; 29 | }; 30 | 31 | "configuration.nix" = { 32 | source = ./configuration.nix; 33 | mode = "0600"; 34 | }; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /modules/alertmanager.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | imports = [ ./consul-catalog.nix ]; 5 | 6 | services.prometheus.alertmanager = { 7 | enable = true; 8 | configuration = { 9 | route.receiver = "webhook"; 10 | receivers = [ 11 | { 12 | name = "webhook"; 13 | webhook_configs = [ 14 | { 15 | url = "http://localhost:${toString config.services.webhook.port}/hooks/alertmanager"; 16 | } 17 | ]; 18 | } 19 | ]; 20 | }; 21 | }; 22 | 23 | services.consul.catalog = [ 24 | { 25 | name = "alertmanager"; 26 | port = config.services.prometheus.alertmanager.port; 27 | tags = (import ./lib/traefik.nix).tagsForHost "alertmanager"; 28 | check = { 29 | name = "Health endpoint"; 30 | http = "http://localhost:${toString config.services.prometheus.alertmanager.port}/-/healthy"; 31 | interval = "10s"; 32 | }; 33 | } 34 | ]; 35 | 36 | networking.firewall.allowedTCPPorts = [ config.services.prometheus.alertmanager.port ]; 37 | } 38 | -------------------------------------------------------------------------------- /modules/arcade.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, ... }: 2 | 3 | { 4 | imports = [ ./consul-catalog.nix ]; 5 | 6 | sound.enable = true; 7 | 8 | services.xserver.enable = true; 9 | services.xserver.displayManager.autoLogin.enable = true; 10 | services.xserver.displayManager.autoLogin.user = "zsnes"; 11 | services.xserver.desktopManager.retroarch.enable = true; 12 | services.xserver.desktopManager.retroarch.package = pkgs.retroarchFull; 13 | 14 | nixpkgs.overlays = [ 15 | (self: super: { 16 | retroarchFull = super.retroarch.override { 17 | cores = [ super.libretro.snes9x2010 ]; 18 | }; 19 | }) 20 | ]; 21 | 22 | users.users.zsnes = { 23 | isNormalUser = true; 24 | extraGroups = [ "video" ]; 25 | }; 26 | 27 | nixpkgs.overlays = [ 28 | (self: super: { 29 | libcec = super.libcec.override { inherit (super) libraspberrypi; }; 30 | }) 31 | ]; 32 | 33 | services.udev.extraRules = '' 34 | SUBSYSTEM=="vchiq",GROUP="video",MODE="0660" 35 | ''; 36 | } 37 | -------------------------------------------------------------------------------- /modules/backup.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | 3 | { 4 | imports = [ 5 | ./nas.nix 6 | ]; 7 | 8 | users.users.borg = { 9 | isNormalUser = lib.mkForce true; 10 | isSystemUser = lib.mkForce false; 11 | uid = 1000; 12 | extraGroups = [ "git" ]; 13 | }; 14 | users.groups.borg = { 15 | gid = 1000; 16 | }; 17 | 18 | systemd.services.borgbackup-repo-git.serviceConfig = { 19 | User = "borg"; 20 | }; 21 | systemd.services.borgbackup-repo-x1.serviceConfig = { 22 | User = "borg"; 23 | }; 24 | 25 | services.borgbackup.repos = { 26 | git = { 27 | authorizedKeys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBpy7MNdMJUmrhlKaZEfm4GsoZWDZQUSTuUrRRlKCqRT root@git" ]; 28 | path = "/mnt/nas/backup/borg/git"; 29 | }; 30 | x1 = { 31 | authorizedKeys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDYh9g6mgiD2ckSeeZ+eXhEYSnFPo1/jNKpmhTX5U5i3 root@x1" ]; 32 | path = "/mnt/nas/backup/borg/x1"; 33 | }; 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /modules/buildMachines.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | 3 | { 4 | programs.ssh.knownHosts = { 5 | nuc = { 6 | hostNames = [ "nuc" "nuc.thewagner.home" ]; 7 | publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIKaEtc8PNqhxAQ24gY5t25Y/8HU6StUB6kmU1xmVta7"; 8 | }; 9 | 10 | rp3 = { 11 | hostNames = [ "rp3" "rp3.thewagner.home" ]; 12 | publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC7PpM3BlMNoiS1RtdAPPktucd2USaYaifLaE5Hd63RA"; 13 | }; 14 | 15 | rp4 = { 16 | hostNames = [ "rp4" "rp4.thewagner.home" ]; 17 | publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID/z/WxE1OsrpXsv+NtHv5jkZtKRF9RtrFGVZyXWzMhm"; 18 | }; 19 | }; 20 | 21 | nix = { 22 | distributedBuilds = true; 23 | buildMachines = 24 | let 25 | sshUser = "root"; 26 | sshKey = "/root/remote-builder"; 27 | in 28 | lib.filter (m: m.hostName != "${config.networking.hostName}") [ 29 | { 30 | hostName = "nuc"; 31 | systems = [ "x86_64-linux" "i686-linux" "aarch64-linux" ]; 32 | maxJobs = 4; 33 | supportedFeatures = [ "kvm" "nixos-test" "big-parallel" "benchmark" ]; 34 | inherit sshUser sshKey; 35 | } 36 | { 37 | hostName = "rp4"; 38 | systems = [ "aarch64-linux" ]; 39 | maxJobs = 2; 40 | supportedFeatures = [ "kvm" "nixos-test" "big-parallel" "benchmark" ]; 41 | inherit sshUser sshKey; 42 | speedFactor = 2; 43 | } 44 | ]; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /modules/cachix.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | services.cachix-agent.enable = true; 5 | } 6 | -------------------------------------------------------------------------------- /modules/camera-rpi-v1/default.nix: -------------------------------------------------------------------------------- 1 | # Raspberry Pi V1 camera module (OV5647) 2 | { config, pkgs, ... }: 3 | 4 | let 5 | 6 | # Workaround from https://github.com/NixOS/nixos-hardware/blob/master/raspberry-pi/4/apply-overlays-dtmerge.nix 7 | deviceTree_overlay = _final: prev: { 8 | deviceTree = { 9 | applyOverlays = prev.callPackage ./overlays/apply-overlays-dtmerge.nix { }; 10 | compileDTS = prev.deviceTree.compileDTS; 11 | }; 12 | }; 13 | 14 | libcamera_overlay = (import ./overlays/libcamera.nix); 15 | 16 | rpicam-apps_overlay = _final: prev: { 17 | rpicam-apps = prev.callPackage ./rpicam-apps.nix { }; 18 | }; 19 | 20 | in 21 | 22 | { 23 | nixpkgs.overlays = [ 24 | deviceTree_overlay 25 | libcamera_overlay 26 | rpicam-apps_overlay 27 | ]; 28 | 29 | environment.systemPackages = with pkgs; [ 30 | rpicam-apps 31 | ]; 32 | 33 | hardware.deviceTree.filter = "bcm2837-rpi-3*"; 34 | hardware.deviceTree.overlays = [ 35 | # Equivalent to: 36 | # https://github.com/raspberrypi/linux/blob/rpi-6.1.y/arch/arm/boot/dts/overlays/ov5647-overlay.dts 37 | # 38 | # As proposed in https://github.com/NixOS/nixpkgs/issues/125354, the "compatible" attribute is changed to bcm2837. 39 | { 40 | name = "ov5647-overlay"; 41 | dtsText = '' 42 | // SPDX-License-Identifier: GPL-2.0-only 43 | // Definitions for OV5647 camera module on VC I2C bus 44 | /dts-v1/; 45 | /plugin/; 46 | 47 | /{ 48 | compatible = "brcm,bcm2837"; 49 | 50 | i2c_frag: fragment@0 { 51 | target = <&i2c_csi_dsi>; 52 | __overlay__ { 53 | #address-cells = <1>; 54 | #size-cells = <0>; 55 | status = "okay"; 56 | 57 | cam_node: ov5647@36 { 58 | compatible = "ovti,ov5647"; 59 | reg = <0x36>; 60 | status = "disabled"; 61 | 62 | clocks = <&cam1_clk>; 63 | 64 | avdd-supply = <&cam1_reg>; 65 | dovdd-supply = <&cam_dummy_reg>; 66 | dvdd-supply = <&cam_dummy_reg>; 67 | 68 | rotation = <0>; 69 | orientation = <2>; 70 | 71 | port { 72 | cam_endpoint: endpoint { 73 | clock-lanes = <0>; 74 | data-lanes = <1 2>; 75 | clock-noncontinuous; 76 | link-frequencies = 77 | /bits/ 64 <297000000>; 78 | }; 79 | }; 80 | }; 81 | 82 | vcm_node: ad5398@c { 83 | compatible = "adi,ad5398"; 84 | reg = <0x0c>; 85 | status = "disabled"; 86 | VANA-supply = <&cam1_reg>; 87 | }; 88 | }; 89 | }; 90 | 91 | csi_frag: fragment@1 { 92 | target = <&csi1>; 93 | csi: __overlay__ { 94 | status = "okay"; 95 | brcm,media-controller; 96 | 97 | port { 98 | csi_ep: endpoint { 99 | remote-endpoint = <&cam_endpoint>; 100 | data-lanes = <1 2>; 101 | }; 102 | }; 103 | }; 104 | }; 105 | 106 | fragment@2 { 107 | target = <&i2c0if>; 108 | __overlay__ { 109 | status = "okay"; 110 | }; 111 | }; 112 | 113 | fragment@3 { 114 | target = <&i2c0mux>; 115 | __overlay__ { 116 | status = "okay"; 117 | }; 118 | }; 119 | 120 | reg_frag: fragment@4 { 121 | target = <&cam1_reg>; 122 | __overlay__ { 123 | startup-delay-us = <20000>; 124 | }; 125 | }; 126 | 127 | clk_frag: fragment@5 { 128 | target = <&cam1_clk>; 129 | __overlay__ { 130 | status = "okay"; 131 | clock-frequency = <25000000>; 132 | }; 133 | }; 134 | 135 | __overrides__ { 136 | rotation = <&cam_node>,"rotation:0"; 137 | orientation = <&cam_node>,"orientation:0"; 138 | media-controller = <&csi>,"brcm,media-controller?"; 139 | cam0 = <&i2c_frag>, "target:0=",<&i2c_csi_dsi0>, 140 | <&csi_frag>, "target:0=",<&csi0>, 141 | <®_frag>, "target:0=",<&cam0_reg>, 142 | <&clk_frag>, "target:0=",<&cam0_clk>, 143 | <&cam_node>, "clocks:0=",<&cam0_clk>, 144 | <&cam_node>, "avdd-supply:0=",<&cam0_reg>, 145 | <&vcm_node>, "VANA-supply:0=",<&cam0_reg>; 146 | vcm = <&vcm_node>, "status=okay", 147 | <&cam_node>,"lens-focus:0=", <&vcm_node>; 148 | }; 149 | }; 150 | 151 | &cam_node { 152 | status = "okay"; 153 | }; 154 | 155 | &cam_endpoint { 156 | remote-endpoint = <&csi_ep>; 157 | }; 158 | ''; 159 | } 160 | ]; 161 | } 162 | -------------------------------------------------------------------------------- /modules/camera-rpi-v1/overlays/apply-overlays-dtmerge.nix: -------------------------------------------------------------------------------- 1 | # modification of nixpkgs deviceTree.applyOverlays to resolve https://github.com/NixOS/nixpkgs/issues/125354 2 | # derived from https://github.com/NixOS/nixpkgs/blob/916ca8f2b0c208def051f8ea9760c534a40309db/pkgs/os-specific/linux/device-tree/default.nix 3 | { lib, stdenvNoCC, dtc, libraspberrypi }: 4 | 5 | with lib; (base: overlays': stdenvNoCC.mkDerivation { 6 | name = "device-tree-overlays"; 7 | nativeBuildInputs = [ dtc libraspberrypi ]; 8 | buildCommand = 9 | let 10 | overlays = toList overlays'; 11 | in 12 | '' 13 | mkdir -p $out 14 | cd "${base}" 15 | find . -type f -name '*.dtb' -print0 \ 16 | | xargs -0 cp -v --no-preserve=mode --target-directory "$out" --parents 17 | 18 | for dtb in $(find "$out" -type f -name '*.dtb'); do 19 | dtbCompat=$(fdtget -t s "$dtb" / compatible 2>/dev/null || true) 20 | # skip files without `compatible` string 21 | test -z "$dtbCompat" && continue 22 | 23 | ${flip (concatMapStringsSep "\n") overlays (o: '' 24 | overlayCompat="$(fdtget -t s "${o.dtboFile}" / compatible)" 25 | 26 | # skip incompatible and non-matching overlays 27 | if [[ ! "$dtbCompat" =~ "$overlayCompat" ]]; then 28 | echo "Skipping overlay ${o.name}: incompatible with $(basename "$dtb")" 29 | elif ${if ((o.filter or null) == null) then "false" else '' 30 | [[ "''${dtb//${o.filter}/}" == "$dtb" ]] 31 | ''} 32 | then 33 | echo "Skipping overlay ${o.name}: filter does not match $(basename "$dtb")" 34 | else 35 | echo -n "Applying overlay ${o.name} to $(basename "$dtb")... " 36 | mv "$dtb"{,.in} 37 | 38 | # dtmerge requires a .dtbo ext for dtbo files, otherwise it adds it to the given file implicitly 39 | dtboWithExt="$TMPDIR/$(basename "${o.dtboFile}").dtbo" 40 | cp -r ${o.dtboFile} "$dtboWithExt" 41 | 42 | dtmerge "$dtb.in" "$dtb" "$dtboWithExt" 43 | 44 | echo "ok" 45 | rm "$dtb.in" "$dtboWithExt" 46 | fi 47 | '')} 48 | 49 | done''; 50 | }) 51 | -------------------------------------------------------------------------------- /modules/camera-rpi-v1/overlays/libcamera.nix: -------------------------------------------------------------------------------- 1 | final: prev: { 2 | libcamera = prev.libcamera.overrideAttrs (old: { 3 | buildInputs = old.buildInputs ++ [ prev.boost prev.nlohmann_json ]; 4 | nativeBuildInputs = old.nativeBuildInputs ++ [ prev.python3Packages.pybind11 ]; 5 | 6 | BOOST_INCLUDEDIR = "${prev.lib.getDev prev.boost}/include"; 7 | BOOST_LIBRARYDIR = "${prev.lib.getLib prev.boost}/lib"; 8 | 9 | postPatch = old.postPatch + '' 10 | patchShebangs src/py/libcamera 11 | ''; 12 | 13 | mesonFlags = old.mesonFlags ++ [ 14 | "-Dcam=disabled" 15 | "-Dgstreamer=disabled" 16 | "-Dipas=rpi/vc4,rpi/pisp" 17 | "-Dpipelines=rpi/vc4,rpi/pisp" 18 | ]; 19 | 20 | src = prev.fetchFromGitHub { 21 | owner = "raspberrypi"; 22 | repo = "libcamera"; 23 | rev = "d83ff0a4ae4503bc56b7ed48cd142c3dd423ad3b"; 24 | sha256 = "sha256-VP0s1jOON9J3gn81aiemsChvGeqx0PPivQF5rmSga6M="; 25 | 26 | nativeBuildInputs = [ prev.git ]; 27 | 28 | postFetch = '' 29 | cd "$out" 30 | 31 | export NIX_SSL_CERT_FILE=${prev.cacert}/etc/ssl/certs/ca-bundle.crt 32 | 33 | ${prev.lib.getExe prev.meson} subprojects download \ 34 | libpisp 35 | 36 | find subprojects -type d -name .git -prune -execdir rm -r {} + 37 | ''; 38 | }; 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /modules/camera-rpi-v1/rpicam-apps.nix: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/NixOS/nixpkgs/pull/281803 2 | { stdenv 3 | , fetchFromGitHub 4 | , lib 5 | , makeWrapper 6 | , meson 7 | , ninja 8 | , pkg-config 9 | , boost 10 | , ffmpeg-headless 11 | , libcamera 12 | , libdrm 13 | , libepoxy 14 | , libexif 15 | , libjpeg 16 | , libpng 17 | , libtiff 18 | , libX11 19 | }: 20 | 21 | stdenv.mkDerivation (finalAttrs: { 22 | pname = "rpicam-apps"; 23 | version = "1.7.0"; 24 | 25 | src = fetchFromGitHub { 26 | owner = "raspberrypi"; 27 | repo = "rpicam-apps"; 28 | rev = "v${finalAttrs.version}"; 29 | hash = "sha256-79qpAfY83YOZdM5ZPyIOkg3s7x75hvjG6Cc96UAIdb0="; 30 | }; 31 | 32 | buildInputs = [ 33 | boost 34 | ffmpeg-headless 35 | libcamera 36 | libdrm 37 | libepoxy # GLES/EGL preview window 38 | libexif 39 | libjpeg 40 | libpng 41 | libtiff 42 | libX11 43 | ]; 44 | 45 | nativeBuildInputs = [ 46 | makeWrapper 47 | meson 48 | ninja 49 | pkg-config 50 | ]; 51 | 52 | # Meson is no longer able to pick up Boost automatically. 53 | # https://github.com/NixOS/nixpkgs/issues/86131 54 | BOOST_INCLUDEDIR = "${lib.getDev boost}/include"; 55 | BOOST_LIBRARYDIR = "${lib.getLib boost}/lib"; 56 | 57 | env.NIX_CFLAGS_COMPILE = toString [ 58 | "-Wno-error=deprecated-declarations" 59 | ]; 60 | 61 | # See all options here: https://github.com/raspberrypi/rpicam-apps/blob/main/meson_options.txt 62 | mesonFlags = [ 63 | "-Denable_drm=disabled" 64 | "-Denable_egl=disabled" 65 | "-Denable_hailo=disabled" 66 | "-Denable_qt=disabled" 67 | ]; 68 | 69 | postInstall = '' 70 | for f in rpicam-hello rpicam-jpeg rpicam-raw rpicam-still rpicam-vid 71 | do 72 | wrapProgram $out/bin/$f --set-default LIBCAMERA_IPA_PROXY_PATH ${libcamera}/libexec/libcamera 73 | done 74 | ''; 75 | }) 76 | -------------------------------------------------------------------------------- /modules/common.nix: -------------------------------------------------------------------------------- 1 | /* Configuration applied on _all_ machines, that is, desktops and servers. */ 2 | { config, lib, pkgs, nixpkgs, ... }: 3 | 4 | { 5 | nix = { 6 | extraOptions = '' 7 | builders-use-substitutes = true 8 | experimental-features = nix-command flakes 9 | ''; 10 | 11 | registry.nixpkgs.flake = nixpkgs; 12 | 13 | gc = { 14 | automatic = true; 15 | options = ''--delete-older-than 30d''; 16 | dates = "weekly"; 17 | randomizedDelaySec = "15min"; 18 | }; 19 | }; 20 | 21 | i18n.defaultLocale = "en_US.UTF-8"; 22 | 23 | time.timeZone = "Europe/Zurich"; 24 | 25 | services = { 26 | avahi = { 27 | enable = true; 28 | nssmdns4 = true; 29 | openFirewall = true; 30 | }; 31 | 32 | openssh = { 33 | enable = true; 34 | settings.PasswordAuthentication = false; 35 | }; 36 | }; 37 | 38 | # Machine configuration to be added for the vm script produced by ‘nixos-rebuild build-vm’. 39 | virtualisation.vmVariant = { 40 | services.qemuGuest.enable = true; 41 | users.users.dwagner = { 42 | initialHashedPassword = ""; 43 | isNormalUser = true; 44 | }; 45 | }; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /modules/consul-catalog.nix: -------------------------------------------------------------------------------- 1 | # Add entries to the Consul Service catalog 2 | { config, lib, pkgs, ... }: 3 | let 4 | 5 | cfg = config.services.consul; 6 | 7 | in 8 | { 9 | 10 | options.services.consul = { 11 | catalog = lib.mkOption { 12 | default = [ ]; 13 | description = '' 14 | The provided sets are converted to JSON as specified here: 15 | https://www.consul.io/docs/agent/services 16 | ''; 17 | }; 18 | }; 19 | 20 | config = 21 | let 22 | toServiceDefinition = config: 23 | pkgs.writeText "${config.name}.json" (builtins.toJSON { service = config; }); 24 | 25 | allServices = builtins.map toServiceDefinition cfg.catalog; 26 | 27 | in 28 | { 29 | 30 | services.consul.extraConfigFiles = builtins.map toString allServices; 31 | 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /modules/consul/base.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | 3 | { 4 | services.consul = { 5 | enable = true; 6 | 7 | extraConfig = { 8 | bind_addr = "{{ GetPrivateInterfaces | include \"network\" \"192.168.1.0/24\" | attr \"address\" }}"; 9 | 10 | telemetry = { 11 | disable_hostname = true; 12 | prometheus_retention_time = "2m"; 13 | }; 14 | 15 | disable_update_check = true; 16 | }; 17 | }; 18 | 19 | nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ 20 | "consul" 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /modules/consul/client.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | imports = [ 5 | ./base.nix 6 | ]; 7 | 8 | services.consul = { 9 | extraConfig = { 10 | retry_join = [ "nuc.sunrise.box" ]; 11 | server = false; 12 | }; 13 | }; 14 | 15 | # https://www.consul.io/docs/install/ports#consul-clients 16 | networking.firewall.allowedTCPPorts = [ 8301 8500 8600 ]; 17 | networking.firewall.allowedUDPPorts = [ 8301 8600 ]; 18 | } 19 | -------------------------------------------------------------------------------- /modules/consul/server.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | imports = [ 5 | ./base.nix 6 | ]; 7 | 8 | services.consul = { 9 | interface.bind = "eno1"; 10 | extraConfig = { 11 | server = true; 12 | bootstrap_expect = 1; 13 | }; 14 | }; 15 | 16 | # https://www.consul.io/docs/install/ports#consul-servers 17 | networking.firewall.allowedTCPPorts = [ 8300 8301 8302 8503 8500 8600 ]; 18 | networking.firewall.allowedUDPPorts = [ 8600 8301 8302 ]; 19 | } 20 | -------------------------------------------------------------------------------- /modules/git.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | let 3 | httpPort = 80; 4 | 5 | in 6 | { 7 | users.users.git = { 8 | isSystemUser = true; 9 | group = "git"; 10 | shell = "${pkgs.git}/bin/git-shell"; 11 | openssh.authorizedKeys.keys = (import ./keys.nix).dwagner; 12 | }; 13 | users.groups.git = { }; 14 | 15 | services = { 16 | cgit.git = { 17 | enable = true; 18 | user = "cgit"; 19 | group = "git"; 20 | scanPath = "/srv/git"; 21 | settings = { 22 | enable-git-config = true; 23 | clone-url = "git@git:/srv/git/$CGIT_REPO_URL"; 24 | source-filter = "${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py"; 25 | about-filter = "${pkgs.cgit}/lib/cgit/filters/about-formatting.sh"; 26 | readme = ":README.md"; 27 | root-title = "Homelab git repositories"; 28 | root-desc = "Git repositories hosted at home"; 29 | section-from-path = 1; 30 | }; 31 | }; 32 | 33 | nginx.virtualHosts.git.listen = [ 34 | { 35 | addr = "0.0.0.0"; 36 | port = httpPort; 37 | } 38 | { 39 | addr = "[::]"; 40 | port = httpPort; 41 | } 42 | ]; 43 | 44 | openssh = { 45 | enable = true; 46 | settings.PasswordAuthentication = false; 47 | settings.PermitRootLogin = "no"; 48 | }; 49 | 50 | }; 51 | 52 | networking.firewall.allowedTCPPorts = [ httpPort ]; 53 | } 54 | -------------------------------------------------------------------------------- /modules/grafana/dashboard-linter.nix: -------------------------------------------------------------------------------- 1 | { fetchFromGitHub, buildGoModule }: 2 | 3 | buildGoModule rec { 4 | pname = "dashboard-linter"; 5 | version = "0.0.1"; 6 | 7 | src = fetchFromGitHub { 8 | owner = "grafana"; 9 | repo = "dashboard-linter"; 10 | rev = "b1f5eb2cca53b30525eeca68d65e5ba017e90df2"; 11 | sha256 = "0a1gf6y5vlyrbhmqsgqh91s74wp2saylaqr3rmag7yjhjpi4s4gq"; 12 | }; 13 | 14 | vendorHash = "sha256-BRe+I1tZZw0YXLhidLbapIrqP55vuSx/gSADLW0PXL0="; 15 | 16 | meta = { 17 | description = " A tool to lint Grafana dashboards "; 18 | homepage = "https://github.com/grafana/dashboard-linter"; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /modules/grafana/dashboards/nixos.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "datasource", 8 | "uid": "grafana" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | }, 22 | { 23 | "datasource": { 24 | "type": "prometheus", 25 | "uid": "bRc0IA0Mz" 26 | }, 27 | "enable": true, 28 | "expr": "changes(nixos_system_version[5m])", 29 | "iconColor": "orange", 30 | "name": "Deployments", 31 | "titleFormat": "Host {{ hostname }} deployed" 32 | } 33 | ] 34 | }, 35 | "editable": true, 36 | "fiscalYearStartMonth": 0, 37 | "graphTooltip": 0, 38 | "links": [], 39 | "liveNow": false, 40 | "panels": [ 41 | { 42 | "datasource": { 43 | "type": "prometheus", 44 | "uid": "bRc0IA0Mz" 45 | }, 46 | "gridPos": { 47 | "h": 9, 48 | "w": 4, 49 | "x": 0, 50 | "y": 0 51 | }, 52 | "id": 8, 53 | "options": { 54 | "code": { 55 | "language": "plaintext", 56 | "showLineNumbers": false, 57 | "showMiniMap": false 58 | }, 59 | "content": "* [Alertmanager](http://nuc:9093)\n* [Grafana](http://nuc:3000)\n* [Git](http://git)\n* [Node Exporter (nuc)](http://nuc:9100)\n* [Ntfy (nuc)](http://nuc:8080)\n* [Prometheus](http://nuc:9090)", 60 | "mode": "markdown" 61 | }, 62 | "pluginVersion": "9.4.9", 63 | "title": "Homelab", 64 | "type": "text" 65 | }, 66 | { 67 | "datasource": { 68 | "type": "loki", 69 | "uid": "Lg50IAAMz" 70 | }, 71 | "description": "", 72 | "fieldConfig": { 73 | "defaults": { 74 | "color": { 75 | "mode": "palette-classic" 76 | }, 77 | "custom": { 78 | "axisCenteredZero": false, 79 | "axisColorMode": "text", 80 | "axisLabel": "", 81 | "axisPlacement": "auto", 82 | "barAlignment": 0, 83 | "drawStyle": "line", 84 | "fillOpacity": 10, 85 | "gradientMode": "none", 86 | "hideFrom": { 87 | "legend": false, 88 | "tooltip": false, 89 | "viz": false 90 | }, 91 | "lineInterpolation": "linear", 92 | "lineWidth": 1, 93 | "pointSize": 5, 94 | "scaleDistribution": { 95 | "type": "linear" 96 | }, 97 | "showPoints": "never", 98 | "spanNulls": false, 99 | "stacking": { 100 | "group": "A", 101 | "mode": "none" 102 | }, 103 | "thresholdsStyle": { 104 | "mode": "off" 105 | } 106 | }, 107 | "mappings": [], 108 | "thresholds": { 109 | "mode": "absolute", 110 | "steps": [ 111 | { 112 | "color": "green", 113 | "value": null 114 | }, 115 | { 116 | "color": "red", 117 | "value": 80 118 | } 119 | ] 120 | }, 121 | "unit": "cpm" 122 | }, 123 | "overrides": [] 124 | }, 125 | "gridPos": { 126 | "h": 9, 127 | "w": 20, 128 | "x": 4, 129 | "y": 0 130 | }, 131 | "id": 4, 132 | "options": { 133 | "legend": { 134 | "calcs": [], 135 | "displayMode": "list", 136 | "placement": "bottom", 137 | "showLegend": true 138 | }, 139 | "tooltip": { 140 | "mode": "multi", 141 | "sort": "none" 142 | } 143 | }, 144 | "pluginVersion": "9.4.9", 145 | "targets": [ 146 | { 147 | "expr": "sum by (hostname) (rate({job=\"systemd-journal\"}[5m])) * 60", 148 | "legendFormat": "{{ hostname }}", 149 | "refId": "B" 150 | } 151 | ], 152 | "title": "Number of messages in the systemd journal", 153 | "type": "timeseries" 154 | }, 155 | { 156 | "datasource": { 157 | "type": "loki", 158 | "uid": "Lg50IAAMz" 159 | }, 160 | "gridPos": { 161 | "h": 9, 162 | "w": 24, 163 | "x": 0, 164 | "y": 9 165 | }, 166 | "id": 6, 167 | "options": { 168 | "dedupStrategy": "signature", 169 | "enableLogDetails": true, 170 | "prettifyLogMessage": false, 171 | "showCommonLabels": true, 172 | "showLabels": false, 173 | "showTime": true, 174 | "sortOrder": "Descending", 175 | "wrapLogMessage": false 176 | }, 177 | "pluginVersion": "8.5.15", 178 | "targets": [ 179 | { 180 | "datasource": { 181 | "type": "loki", 182 | "uid": "Lg50IAAMz" 183 | }, 184 | "expr": "{service=\"cachix-agent\"} | pattern `[<_>][<_>][][<_>][<_>][<_>][<_>] ` | line_format \"{{.msg}}\"", 185 | "queryType": "range", 186 | "refId": "A" 187 | } 188 | ], 189 | "title": "Cachix Agent Logs", 190 | "type": "logs" 191 | } 192 | ], 193 | "refresh": "30s", 194 | "revision": 1, 195 | "schemaVersion": 38, 196 | "style": "dark", 197 | "tags": [], 198 | "templating": { 199 | "list": [] 200 | }, 201 | "time": { 202 | "from": "now-12h", 203 | "to": "now" 204 | }, 205 | "timepicker": {}, 206 | "timezone": "", 207 | "title": "NixOS", 208 | "uid": "IvnY58oMz", 209 | "version": 7, 210 | "weekStart": "" 211 | } 212 | -------------------------------------------------------------------------------- /modules/grafana/dashboards/room-temperature-humidity.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "$$hashKey": "object:205", 6 | "builtIn": 1, 7 | "datasource": { 8 | "type": "datasource", 9 | "uid": "grafana" 10 | }, 11 | "enable": true, 12 | "hide": true, 13 | "iconColor": "rgba(0, 211, 255, 1)", 14 | "name": "Annotations & Alerts", 15 | "target": { 16 | "limit": 100, 17 | "matchAny": false, 18 | "tags": [], 19 | "type": "dashboard" 20 | }, 21 | "type": "dashboard" 22 | } 23 | ] 24 | }, 25 | "editable": false, 26 | "fiscalYearStartMonth": 0, 27 | "graphTooltip": 1, 28 | "links": [], 29 | "liveNow": false, 30 | "panels": [ 31 | { 32 | "datasource": { 33 | "type": "prometheus", 34 | "uid": "bRc0IA0Mz" 35 | }, 36 | "fieldConfig": { 37 | "defaults": { 38 | "color": { 39 | "mode": "palette-classic" 40 | }, 41 | "custom": { 42 | "axisLabel": "", 43 | "axisPlacement": "auto", 44 | "barAlignment": 0, 45 | "drawStyle": "line", 46 | "fillOpacity": 0, 47 | "gradientMode": "none", 48 | "hideFrom": { 49 | "graph": false, 50 | "legend": false, 51 | "tooltip": false, 52 | "viz": false 53 | }, 54 | "lineInterpolation": "stepAfter", 55 | "lineWidth": 1, 56 | "pointSize": 5, 57 | "scaleDistribution": { 58 | "type": "linear" 59 | }, 60 | "showPoints": "never", 61 | "spanNulls": false, 62 | "stacking": { 63 | "group": "A", 64 | "mode": "none" 65 | }, 66 | "thresholdsStyle": { 67 | "mode": "off" 68 | } 69 | }, 70 | "decimals": 1, 71 | "mappings": [], 72 | "thresholds": { 73 | "mode": "absolute", 74 | "steps": [ 75 | { 76 | "color": "green", 77 | "value": null 78 | }, 79 | { 80 | "color": "red", 81 | "value": 80 82 | } 83 | ] 84 | }, 85 | "unit": "celsius" 86 | }, 87 | "overrides": [] 88 | }, 89 | "gridPos": { 90 | "h": 7, 91 | "w": 8, 92 | "x": 0, 93 | "y": 0 94 | }, 95 | "id": 19, 96 | "options": { 97 | "graph": {}, 98 | "legend": { 99 | "calcs": [], 100 | "displayMode": "list", 101 | "placement": "bottom" 102 | }, 103 | "tooltip": { 104 | "mode": "single", 105 | "sort": "none" 106 | } 107 | }, 108 | "pluginVersion": "7.4.1", 109 | "targets": [ 110 | { 111 | "datasource": { 112 | "type": "prometheus", 113 | "uid": "bRc0IA0Mz" 114 | }, 115 | "editorMode": "code", 116 | "expr": "mqtt_consumer_AM2301_Temperature", 117 | "instant": false, 118 | "interval": "", 119 | "legendFormat": "{{ room }}", 120 | "refId": "A" 121 | }, 122 | { 123 | "datasource": { 124 | "type": "prometheus", 125 | "uid": "bRc0IA0Mz" 126 | }, 127 | "editorMode": "code", 128 | "expr": "mqtt_consumer_SI7021_Temperature", 129 | "hide": false, 130 | "legendFormat": "{{ room }}", 131 | "range": true, 132 | "refId": "B" 133 | } 134 | ], 135 | "title": "Room temperature", 136 | "type": "timeseries" 137 | }, 138 | { 139 | "datasource": { 140 | "type": "prometheus", 141 | "uid": "bRc0IA0Mz" 142 | }, 143 | "fieldConfig": { 144 | "defaults": { 145 | "color": { 146 | "mode": "palette-classic" 147 | }, 148 | "custom": { 149 | "axisLabel": "", 150 | "axisPlacement": "auto", 151 | "barAlignment": 0, 152 | "drawStyle": "line", 153 | "fillOpacity": 0, 154 | "gradientMode": "none", 155 | "hideFrom": { 156 | "graph": false, 157 | "legend": false, 158 | "tooltip": false, 159 | "viz": false 160 | }, 161 | "lineInterpolation": "stepAfter", 162 | "lineWidth": 1, 163 | "pointSize": 5, 164 | "scaleDistribution": { 165 | "type": "linear" 166 | }, 167 | "showPoints": "never", 168 | "spanNulls": false, 169 | "stacking": { 170 | "group": "A", 171 | "mode": "none" 172 | }, 173 | "thresholdsStyle": { 174 | "mode": "dashed" 175 | } 176 | }, 177 | "decimals": 1, 178 | "mappings": [], 179 | "thresholds": { 180 | "mode": "absolute", 181 | "steps": [ 182 | { 183 | "color": "green", 184 | "value": null 185 | }, 186 | { 187 | "color": "#EAB839", 188 | "value": 60 189 | }, 190 | { 191 | "color": "red", 192 | "value": 70 193 | } 194 | ] 195 | }, 196 | "unit": "percent" 197 | }, 198 | "overrides": [] 199 | }, 200 | "gridPos": { 201 | "h": 7, 202 | "w": 8, 203 | "x": 8, 204 | "y": 0 205 | }, 206 | "id": 20, 207 | "options": { 208 | "graph": {}, 209 | "legend": { 210 | "calcs": [], 211 | "displayMode": "list", 212 | "placement": "bottom" 213 | }, 214 | "tooltip": { 215 | "mode": "single", 216 | "sort": "none" 217 | } 218 | }, 219 | "pluginVersion": "7.4.1", 220 | "targets": [ 221 | { 222 | "datasource": { 223 | "type": "prometheus", 224 | "uid": "bRc0IA0Mz" 225 | }, 226 | "editorMode": "code", 227 | "expr": "mqtt_consumer_AM2301_Humidity", 228 | "instant": false, 229 | "interval": "", 230 | "legendFormat": "{{ room }}", 231 | "refId": "A" 232 | }, 233 | { 234 | "datasource": { 235 | "type": "prometheus", 236 | "uid": "bRc0IA0Mz" 237 | }, 238 | "editorMode": "code", 239 | "expr": "mqtt_consumer_SI7021_Humidity", 240 | "hide": false, 241 | "legendFormat": "{{ room }}", 242 | "range": true, 243 | "refId": "B" 244 | } 245 | ], 246 | "title": "Room humidity", 247 | "type": "timeseries" 248 | }, 249 | { 250 | "datasource": { 251 | "type": "prometheus", 252 | "uid": "bRc0IA0Mz" 253 | }, 254 | "fieldConfig": { 255 | "defaults": { 256 | "color": { 257 | "mode": "palette-classic" 258 | }, 259 | "custom": { 260 | "axisLabel": "", 261 | "axisPlacement": "auto", 262 | "barAlignment": 0, 263 | "drawStyle": "line", 264 | "fillOpacity": 0, 265 | "gradientMode": "none", 266 | "hideFrom": { 267 | "graph": false, 268 | "legend": false, 269 | "tooltip": false, 270 | "viz": false 271 | }, 272 | "lineInterpolation": "stepAfter", 273 | "lineWidth": 1, 274 | "pointSize": 5, 275 | "scaleDistribution": { 276 | "type": "linear" 277 | }, 278 | "showPoints": "never", 279 | "spanNulls": false, 280 | "stacking": { 281 | "group": "A", 282 | "mode": "none" 283 | }, 284 | "thresholdsStyle": { 285 | "mode": "off" 286 | } 287 | }, 288 | "decimals": 1, 289 | "mappings": [], 290 | "thresholds": { 291 | "mode": "absolute", 292 | "steps": [ 293 | { 294 | "color": "green", 295 | "value": null 296 | }, 297 | { 298 | "color": "red", 299 | "value": 80 300 | } 301 | ] 302 | }, 303 | "unit": "celsius" 304 | }, 305 | "overrides": [] 306 | }, 307 | "gridPos": { 308 | "h": 7, 309 | "w": 8, 310 | "x": 16, 311 | "y": 0 312 | }, 313 | "id": 21, 314 | "options": { 315 | "graph": {}, 316 | "legend": { 317 | "calcs": [], 318 | "displayMode": "list", 319 | "placement": "bottom" 320 | }, 321 | "tooltip": { 322 | "mode": "single", 323 | "sort": "none" 324 | } 325 | }, 326 | "pluginVersion": "7.4.1", 327 | "targets": [ 328 | { 329 | "datasource": { 330 | "type": "prometheus", 331 | "uid": "bRc0IA0Mz" 332 | }, 333 | "editorMode": "code", 334 | "expr": "mqtt_consumer_AM2301_DewPoint", 335 | "instant": false, 336 | "interval": "", 337 | "legendFormat": "{{ room }}", 338 | "refId": "A" 339 | }, 340 | { 341 | "datasource": { 342 | "type": "prometheus", 343 | "uid": "bRc0IA0Mz" 344 | }, 345 | "editorMode": "code", 346 | "expr": "mqtt_consumer_SI7021_DewPoint", 347 | "hide": false, 348 | "legendFormat": "{{ room }}", 349 | "range": true, 350 | "refId": "B" 351 | } 352 | ], 353 | "title": "Room Dew Point", 354 | "type": "timeseries" 355 | }, 356 | { 357 | "datasource": { 358 | "type": "prometheus", 359 | "uid": "bRc0IA0Mz" 360 | }, 361 | "fieldConfig": { 362 | "defaults": { 363 | "color": { 364 | "mode": "fixed" 365 | }, 366 | "custom": { 367 | "fillOpacity": 70, 368 | "lineWidth": 0, 369 | "spanNulls": false 370 | }, 371 | "mappings": [ 372 | { 373 | "options": { 374 | "0": { 375 | "color": "red", 376 | "index": 0, 377 | "text": "OFF" 378 | }, 379 | "1": { 380 | "color": "green", 381 | "index": 1, 382 | "text": "ON" 383 | } 384 | }, 385 | "type": "value" 386 | } 387 | ], 388 | "thresholds": { 389 | "mode": "absolute", 390 | "steps": [ 391 | { 392 | "color": "green", 393 | "value": null 394 | }, 395 | { 396 | "color": "red", 397 | "value": 80 398 | } 399 | ] 400 | } 401 | }, 402 | "overrides": [] 403 | }, 404 | "gridPos": { 405 | "h": 7, 406 | "w": 8, 407 | "x": 0, 408 | "y": 7 409 | }, 410 | "id": 23, 411 | "options": { 412 | "alignValue": "left", 413 | "legend": { 414 | "displayMode": "list", 415 | "placement": "bottom" 416 | }, 417 | "mergeValues": true, 418 | "rowHeight": 0.9, 419 | "showValue": "auto", 420 | "tooltip": { 421 | "mode": "single", 422 | "sort": "none" 423 | } 424 | }, 425 | "targets": [ 426 | { 427 | "datasource": { 428 | "type": "prometheus", 429 | "uid": "bRc0IA0Mz" 430 | }, 431 | "editorMode": "code", 432 | "expr": "mqtt_consumer_POWER", 433 | "legendFormat": "{{ room }}", 434 | "range": true, 435 | "refId": "A" 436 | } 437 | ], 438 | "title": "ON/OFF status", 439 | "type": "state-timeline" 440 | } 441 | ], 442 | "refresh": "30s", 443 | "schemaVersion": 36, 444 | "style": "dark", 445 | "tags": [], 446 | "templating": { 447 | "list": [] 448 | }, 449 | "time": { 450 | "from": "now-24h", 451 | "to": "now" 452 | }, 453 | "timepicker": { 454 | "refresh_intervals": [ 455 | "5s", 456 | "10s", 457 | "30s", 458 | "1m", 459 | "5m", 460 | "15m" 461 | ] 462 | }, 463 | "timezone": "", 464 | "title": "Room temperature & humidity", 465 | "uid": "-Jz7HnRMz", 466 | "version": 6, 467 | "weekStart": "" 468 | } 469 | -------------------------------------------------------------------------------- /modules/grafana/default.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | imports = [ ../consul-catalog.nix ]; 5 | 6 | services.grafana = { 7 | enable = true; 8 | settings.server.http_addr = "0.0.0.0"; 9 | settings."auth.anonymous" = { 10 | enabled = true; 11 | org_role = "Editor"; 12 | }; 13 | 14 | provision = { 15 | enable = true; 16 | datasources.settings.datasources = [ 17 | { 18 | name = "Prometheus"; 19 | isDefault = true; 20 | type = "prometheus"; 21 | url = "http://nuc:9090"; 22 | } 23 | { 24 | name = "Loki"; 25 | type = "loki"; 26 | url = "http://nuc:3100"; 27 | } 28 | { 29 | name = "Alertmanager"; 30 | type = "alertmanager"; 31 | url = "http://nuc:9093"; 32 | jsonData.implementation = "prometheus"; 33 | jsonData.handleGrafanaManagedAlerts = true; 34 | } 35 | ]; 36 | 37 | dashboards.settings.providers = [ 38 | { 39 | options.path = "/etc/dashboards"; 40 | } 41 | ]; 42 | }; 43 | }; 44 | 45 | services.grafana-image-renderer = { 46 | enable = true; 47 | provisionGrafana = true; 48 | settings.service.metrics.enabled = true; 49 | }; 50 | 51 | # Provision each dashboard in /etc/dashboard 52 | environment.etc = builtins.mapAttrs 53 | ( 54 | name: _: { 55 | target = "dashboards/${name}"; 56 | source = ./. + "/dashboards/${name}"; 57 | } 58 | ) 59 | (builtins.readDir ./dashboards); 60 | 61 | networking.firewall.allowedTCPPorts = [ 62 | config.services.grafana.settings.server.http_port 63 | config.services.grafana-image-renderer.settings.service.port 64 | ]; 65 | 66 | services.consul.catalog = [ 67 | { 68 | name = "grafana"; 69 | port = config.services.grafana.settings.server.http_port; 70 | tags = (import ../lib/traefik.nix).tagsForHost "metrics"; 71 | } 72 | { 73 | name = "grafana-image-renderer"; 74 | port = config.services.grafana-image-renderer.settings.service.port; 75 | } 76 | ]; 77 | } 78 | -------------------------------------------------------------------------------- /modules/hydra.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | { 3 | imports = [ 4 | ./consul-catalog.nix 5 | ./buildMachines.nix 6 | ]; 7 | 8 | services.hydra = { 9 | enable = true; 10 | hydraURL = "http://hydra.thewagner.home"; 11 | port = 3300; 12 | notificationSender = "hydra@localhost"; 13 | buildMachinesFiles = [ ]; 14 | useSubstitutes = true; 15 | }; 16 | 17 | nix.buildMachines = [ 18 | { 19 | hostName = "localhost"; 20 | system = "x86_64-linux"; 21 | supportedFeatures = [ "kvm" "nixos-test" "big-parallel" "benchmark" ]; 22 | maxJobs = 4; 23 | } 24 | ]; 25 | 26 | services.consul.catalog = [ 27 | { 28 | name = "hydra"; 29 | port = config.services.hydra.port; 30 | tags = (import ./lib/traefik.nix).tagsForHost "hydra"; 31 | } 32 | ]; 33 | 34 | networking.firewall.allowedTCPPorts = [ config.services.hydra.port ]; 35 | } 36 | -------------------------------------------------------------------------------- /modules/keys.nix: -------------------------------------------------------------------------------- 1 | { 2 | dwagner = [ 3 | "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDK3SuOKZRMotTbXMaArK+ano/QUKkttcn/+q1v4roBnOQXfPpG16P4NHrSA6SdIwCb2I70thiegCrPRDm0VyZ4VHICxwkmoznmsti6ZlQTvFeHvE22xxYOqPM9FzKGtHe/lPdqrVArzwAFlRpwRAT4TZxMDTGjSnVr3C+N5yBG5ACERStmqs+3phYN0ISYz5usVahhZqh3tKfjb4+UvXiNEC6qGXWpH2ZqT6sYgajmRAeB8Ke9+E1OTL32A0eP7Zri+aWU8+jg62MjH4zhq7bEwYXF7AhUVoi2skLTHv8W3u+7uWhtrSgWg2N7Z4ip6OPAcboCrEUQ+5/yFvhhs1hP wagdav@gmail.com" 4 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBhA5DZmQeYDmlrbBt7Tpng3NqNx6upyGQk+A/DNySdk dwagner@x1" 5 | ]; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /modules/kodi.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | 3 | { 4 | imports = [ ./consul-catalog.nix ]; 5 | 6 | sound.enable = true; 7 | 8 | services.xserver.enable = true; 9 | services.xserver.desktopManager.kodi.enable = true; 10 | services.xserver.desktopManager.kodi.package = pkgs.kodi.withPackages (p: with p; [ kodi-platform youtube ]); 11 | services.xserver.displayManager.autoLogin.enable = true; 12 | services.xserver.displayManager.autoLogin.user = "kodi"; 13 | 14 | users.users.kodi = { 15 | isNormalUser = true; 16 | extraGroups = [ "video" ]; 17 | }; 18 | 19 | networking.firewall = { 20 | allowedTCPPorts = [ 8080 ]; 21 | allowedUDPPorts = [ 8080 ]; 22 | }; 23 | 24 | services.consul.catalog = [ 25 | { 26 | name = "kodi"; 27 | port = 8080; 28 | tags = (import lib/traefik.nix).tagsForHost "tv"; 29 | } 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /modules/lib/traefik.nix: -------------------------------------------------------------------------------- 1 | let 2 | 3 | domain = "thewagner.home"; 4 | 5 | in 6 | { 7 | tagsForHost = host: [ 8 | "traefik.enable=true" 9 | "traefik.http.routers.${host}.rule=Host(`${host}`) || Host(`${host}.${domain}`)" 10 | "traefik.http.routers.${host}.middlewares=${host}-canonical-name" 11 | "traefik.http.middlewares.${host}-canonical-name.redirectregex.permanent=true" 12 | "traefik.http.middlewares.${host}-canonical-name.redirectregex.regex=^http://${host}/(.*)" 13 | "traefik.http.middlewares.${host}-canonical-name.redirectregex.replacement=http://${host}.${domain}/\${1}" 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /modules/loki.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | let 3 | httpPort = 3100; 4 | 5 | # https://grafana.com/docs/loki/latest/configuration/examples/#complete-local-config 6 | configuration = { 7 | auth_enabled = false; 8 | 9 | server.http_listen_port = httpPort; 10 | server.log_level = "warn"; 11 | 12 | common = { 13 | ring = { 14 | instance_addr = "127.0.0.1"; 15 | kvstore.store = "inmemory"; 16 | }; 17 | replication_factor = 1; 18 | path_prefix = "/var/lib/loki"; 19 | }; 20 | 21 | schema_config = { 22 | configs = [ 23 | { 24 | from = "2024-07-01"; 25 | store = "tsdb"; 26 | object_store = "filesystem"; 27 | schema = "v13"; 28 | index = { 29 | prefix = "index_"; 30 | period = "24h"; 31 | }; 32 | } 33 | ]; 34 | }; 35 | 36 | storage_config = { 37 | filesystem.directory = "/var/lib/loki/chunks"; 38 | tsdb_shipper.active_index_directory = "/var/lib/loki/tsdb-index"; 39 | tsdb_shipper.cache_location = "/var/lib/loki/tsdb-cache"; 40 | }; 41 | 42 | limits_config = { 43 | reject_old_samples = true; 44 | reject_old_samples_max_age = "168h"; 45 | }; 46 | 47 | ruler = { 48 | storage = { 49 | type = "local"; 50 | local.directory = "/tmp/rules"; 51 | }; 52 | rule_path = "/tmp/scratch"; 53 | alertmanager_url = "http://nuc:9093"; 54 | ring.kvstore.store = "inmemory"; 55 | enable_api = true; 56 | }; 57 | 58 | query_scheduler = { 59 | max_outstanding_requests_per_tenant = 2048; 60 | }; 61 | }; 62 | 63 | in 64 | { 65 | imports = [ ./consul-catalog.nix ]; 66 | 67 | services.loki = { 68 | enable = true; 69 | inherit configuration; 70 | }; 71 | 72 | services.consul.catalog = [ 73 | { 74 | name = "loki"; 75 | port = httpPort; 76 | tags = (import ./lib/traefik.nix).tagsForHost "loki"; 77 | } 78 | ]; 79 | 80 | networking.firewall.allowedTCPPorts = [ httpPort ]; 81 | } 82 | -------------------------------------------------------------------------------- /modules/mqtt.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | let 3 | 4 | prometheus_client_port = 9883; 5 | mqtt_port = 1883; 6 | 7 | in 8 | { 9 | imports = [ ./consul-catalog.nix ]; 10 | 11 | services.mosquitto = { 12 | enable = true; 13 | listeners = [ 14 | { 15 | acl = [ "pattern readwrite #" ]; 16 | omitPasswordAuth = true; 17 | settings.allow_anonymous = true; 18 | } 19 | ]; 20 | }; 21 | 22 | services.telegraf = { 23 | enable = true; 24 | 25 | extraConfig = { 26 | inputs.mqtt_consumer = { 27 | servers = [ "tcp://127.0.0.1:${toString mqtt_port}" ]; 28 | topics = [ "tele/+/SENSOR" "tele/+/STATE" ]; 29 | data_format = "json"; 30 | json_string_fields = [ "POWER" ]; 31 | topic_parsing = [ 32 | { 33 | topic = "tele/+/+"; 34 | fields = "_/device/_"; 35 | } 36 | ]; 37 | }; 38 | 39 | processors.enum = [ 40 | { } 41 | { 42 | mapping = [ 43 | { 44 | field = "POWER"; 45 | value_mappings = { 46 | ON = 1; 47 | OFF = 0; 48 | }; 49 | } 50 | ]; 51 | } 52 | { 53 | mapping = [ 54 | { 55 | field = "device"; 56 | dest = "room"; 57 | value_mappings = { 58 | "tasmota_082320" = "Living room"; 59 | "tasmota_0E63DE" = "Bedroom"; 60 | "tasmota_96804E" = "Living room"; 61 | "tasmota_D892EA" = "Study"; 62 | "tasmota_D8A2DD" = "Kitchen"; 63 | }; 64 | } 65 | ]; 66 | } 67 | ]; 68 | 69 | outputs.prometheus_client = { 70 | listen = ":${toString prometheus_client_port}"; 71 | metric_version = 2; 72 | export_timestamp = true; 73 | expiration_interval = "5m"; 74 | }; 75 | }; 76 | }; 77 | 78 | services.consul.catalog = [ 79 | { 80 | name = "mosquitto"; 81 | port = mqtt_port; 82 | } 83 | { 84 | name = "telegraf"; 85 | port = prometheus_client_port; 86 | } 87 | ]; 88 | 89 | networking.firewall.allowedTCPPorts = [ 90 | mqtt_port 91 | prometheus_client_port 92 | ]; 93 | } 94 | -------------------------------------------------------------------------------- /modules/nas.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | fileSystems = { 5 | "/mnt/nas" = { 6 | device = "dns-320.local:/mnt/HD/HD_a2/Ajaxpf"; 7 | fsType = "nfs"; 8 | options = [ "x-systemd.automount" "noauto" "_netdev" ]; 9 | }; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /modules/nats.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | services.nats = { 5 | enable = true; 6 | jetstream = true; 7 | }; 8 | 9 | networking.firewall.allowedTCPPorts = [ config.services.nats.port ]; 10 | } 11 | -------------------------------------------------------------------------------- /modules/node-exporter.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | imports = [ ./consul-catalog.nix ]; 5 | 6 | services.prometheus.exporters.node = { 7 | enable = true; 8 | enabledCollectors = [ "cpu" "filesystem" "loadavg" "systemd" ]; 9 | disabledCollectors = [ "rapl" ]; 10 | extraFlags = [ 11 | "--collector.textfile.directory=/var/lib/prometheus-node-exporter-text-files" 12 | ]; 13 | }; 14 | 15 | networking.firewall.allowedTCPPorts = [ 16 | config.services.prometheus.exporters.node.port 17 | ]; 18 | 19 | services.consul.catalog = [ 20 | { 21 | name = "node-exporter"; 22 | port = config.services.prometheus.exporters.node.port; 23 | } 24 | ]; 25 | 26 | system.activationScripts.node-exporter-system-version = '' 27 | mkdir -pm 0775 /var/lib/prometheus-node-exporter-text-files 28 | ( 29 | cd /var/lib/prometheus-node-exporter-text-files 30 | ( 31 | echo -n "nixos_system_version "; 32 | readlink /nix/var/nix/profiles/system | cut -d- -f2 33 | ) > system-version.prom.next 34 | mv system-version.prom.next system-version.prom 35 | ) 36 | ''; 37 | } 38 | -------------------------------------------------------------------------------- /modules/prometheus.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | let 3 | consulAgent = "localhost:8500"; 4 | 5 | scrapeConfigs = [ 6 | { 7 | job_name = "consul"; 8 | static_configs = [ 9 | { 10 | targets = [ 11 | "nuc:8500" 12 | "rp3:8500" 13 | ]; 14 | } 15 | ]; 16 | metrics_path = "/v1/agent/metrics"; 17 | params.format = [ "prometheus" ]; 18 | } 19 | { 20 | job_name = "consul_catalog"; 21 | consul_sd_configs = [ 22 | { 23 | server = consulAgent; 24 | services = [ 25 | "grafana" 26 | "grafana-image-renderer" 27 | "hydra" 28 | "loki" 29 | "node-exporter" 30 | "prometheus" 31 | "promtail" 32 | "telegraf" 33 | ]; 34 | } 35 | ]; 36 | relabel_configs = [ 37 | { 38 | source_labels = [ "__meta_consul_node" ]; 39 | target_label = "hostname"; 40 | } 41 | { 42 | source_labels = [ "__meta_consul_service" ]; 43 | target_label = "service"; 44 | } 45 | ]; 46 | } 47 | { 48 | job_name = "node"; 49 | static_configs = [ 50 | { 51 | targets = [ 52 | "wrt:9100" 53 | ]; 54 | } 55 | ]; 56 | } 57 | ]; 58 | 59 | alertmanagers = [ 60 | { 61 | static_configs = [ 62 | { 63 | targets = [ "nuc:9093" ]; 64 | } 65 | ]; 66 | } 67 | ]; 68 | 69 | in 70 | { 71 | imports = [ ./consul-catalog.nix ]; 72 | 73 | services.prometheus = { 74 | enable = true; 75 | inherit alertmanagers scrapeConfigs; 76 | }; 77 | 78 | services.consul.catalog = [ 79 | { 80 | name = "prometheus"; 81 | port = config.services.prometheus.port; 82 | tags = (import ./lib/traefik.nix).tagsForHost "prometheus"; 83 | check = { 84 | name = "Health endpoint"; 85 | http = "http://localhost:${toString config.services.prometheus.port}/-/healthy"; 86 | interval = "10s"; 87 | }; 88 | } 89 | ]; 90 | 91 | networking.firewall.allowedTCPPorts = [ config.services.prometheus.port ]; 92 | } 93 | -------------------------------------------------------------------------------- /modules/promtail.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | let 3 | 4 | httpPort = 9080; 5 | 6 | in 7 | { 8 | imports = [ ./consul-catalog.nix ]; 9 | 10 | services.promtail = { 11 | enable = true; 12 | configuration = { 13 | server.http_listen_port = httpPort; 14 | server.grpc_listen_port = 0; 15 | clients = [ 16 | { 17 | url = "http://nuc:3100/loki/api/v1/push"; 18 | } 19 | ]; 20 | 21 | scrape_configs = [ 22 | { 23 | job_name = "journal"; 24 | journal = { 25 | max_age = "12h"; 26 | labels = { 27 | job = "systemd-journal"; 28 | }; 29 | }; 30 | relabel_configs = 31 | [ 32 | { 33 | source_labels = [ "__journal__systemd_unit" ]; 34 | regex = "(.*)\\.service"; 35 | target_label = "service"; 36 | } 37 | { 38 | source_labels = [ "__journal__hostname" ]; 39 | target_label = "hostname"; 40 | } 41 | ]; 42 | } 43 | ]; 44 | }; 45 | }; 46 | 47 | services.consul.catalog = [ 48 | { 49 | name = "promtail"; 50 | port = httpPort; 51 | } 52 | ]; 53 | 54 | networking.firewall.allowedTCPPorts = [ httpPort ]; 55 | } 56 | -------------------------------------------------------------------------------- /modules/push-notifications.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | 3 | { 4 | services.ntfy-sh = { 5 | enable = true; 6 | settings = { 7 | base-url = "http://nuc"; 8 | listen-http = ":8080"; 9 | }; 10 | }; 11 | 12 | networking.firewall.allowedTCPPorts = [ 8080 ]; 13 | 14 | users.users = { 15 | ntfy = { 16 | isSystemUser = true; 17 | group = "ntfy"; 18 | }; 19 | }; 20 | 21 | users.groups."ntfy" = { }; 22 | 23 | systemd.services."send-room-humidity" = { 24 | path = [ pkgs.curl ]; 25 | script = '' 26 | set -eu 27 | ${../scripts/push-room-humidity.sh} 28 | ''; 29 | serviceConfig = { 30 | Type = "oneshot"; 31 | User = "ntfy"; 32 | }; 33 | }; 34 | 35 | systemd.timers."send-room-humidity" = { 36 | wantedBy = [ "timers.target" ]; 37 | wants = [ "network-online.target" ]; 38 | after = [ "network-online.target" ]; 39 | timerConfig = { 40 | OnCalendar = [ "*-*-* 8:00" "*-*-* 22:00" ]; 41 | Unit = "send-room-humidity.service"; 42 | }; 43 | }; 44 | 45 | systemd.services."sunrise-sunset" = { 46 | path = [ pkgs.sunwait pkgs.curl ]; 47 | script = '' 48 | set -eu 49 | ${../scripts/push-sunrise-sunset.sh} 50 | ''; 51 | serviceConfig = { 52 | Type = "oneshot"; 53 | User = "ntfy"; 54 | }; 55 | }; 56 | 57 | systemd.timers."sunrise-sunset" = { 58 | wantedBy = [ "timers.target" ]; 59 | wants = [ "network-online.target" ]; 60 | after = [ "network-online.target" ]; 61 | timerConfig = { 62 | OnCalendar = [ "*-*-* 5:30" "*-*-* 16:30" ]; 63 | Unit = "sunrise-sunset.service"; 64 | }; 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /modules/remote-builder/default.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | users = { 5 | # The private part of this key is manually provisioned in 6 | # 7 | # /root/remote-builder 8 | # 9 | # See the configuration option `nix.buildMachines.sshKey` in x230.nix 10 | users.root.openssh.authorizedKeys.keyFiles = [ ./remote-builder.pub ]; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /modules/remote-builder/remote-builder.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDJ+oqSFQ0uY2ncLyIgSCvHfMMxMGCblHbVLuzXVY3tf99HY50pGQp4FG6aDXo9TtPZ2pB4FscxDqCLkyd5WdWgGdIii9WYvHz3LDkJkr66H7d9NooIau6RFQFAw80La+FOBX6PgmAKkjwFfHMxs2J9paDb7pW8y0hrtoFyIAPEaSqO1VetuZK1K5pcZSYAOya0j4E3v1MQ/MIBHntLbHGtWP1gFep7GzLJ/4L5JHpPbmqZtsyy6GyRqUVs+ArCRqR79d021ZFXfGFTWW/psnQ+wByQpfzZn2/KroaVxmc4gO4eHbbOJ56Q2BTo+hOEAPD8St3DSkmbGnAnCkkJEif8MWm1xyJXOfy18BSc6gAtR7RZv/STU8ApF/Y/LZ3R/YRtFeaLBYLw5/+l7BibaYJwvNoJU9/uMjDxJhgM058H8hQtUfbYO5+Rn1k0DbeTcDnKicAsNy1OzgCkpW7sTe1lGp9nZodUE0Ey7ZYGvC+tZhBFzR4sSB8ihGoB61+YKXs= dwagner@x230 2 | -------------------------------------------------------------------------------- /modules/server.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | { 4 | imports = [ 5 | ./node-exporter.nix 6 | ./promtail.nix 7 | ]; 8 | 9 | documentation.enable = false; 10 | 11 | environment.systemPackages = with pkgs; [ 12 | vim 13 | ]; 14 | 15 | users = { 16 | mutableUsers = false; 17 | users.root.openssh.authorizedKeys.keys = (import ./keys.nix).dwagner; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /modules/traefik.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | { 4 | 5 | services.traefik = { 6 | enable = true; 7 | staticConfigOptions = { 8 | providers.consulCatalog = { 9 | exposedByDefault = false; 10 | prefix = "traefik"; 11 | }; 12 | }; 13 | }; 14 | 15 | networking.firewall.allowedTCPPorts = [ 80 ]; 16 | } 17 | -------------------------------------------------------------------------------- /modules/vpn.nix: -------------------------------------------------------------------------------- 1 | { config, ... }: 2 | 3 | { 4 | services.tailscale.enable = true; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /modules/webhook.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, ... }: 2 | 3 | let 4 | 5 | command = pkgs.writeShellScriptBin "ntfy" '' 6 | externalUrl=$1 7 | status=$2 8 | summary=$3 9 | description=$4 10 | 11 | if [ "$status" = "firing" ]; then 12 | icon=rotating_light 13 | else 14 | icon=tada 15 | fi 16 | 17 | ${pkgs.curl}/bin/curl \ 18 | -H "X-Tags: $icon" \ 19 | -H "Title: $summary" \ 20 | -H "Click: $externalUrl" \ 21 | -d "$description" \ 22 | "http://nuc:8080/home-thewagner-ec1" 23 | ''; 24 | 25 | in 26 | 27 | { 28 | imports = [ ./consul-catalog.nix ]; 29 | 30 | services.webhook = { 31 | enable = true; 32 | hooks = { 33 | alertmanager = { 34 | execute-command = "${command}/bin/ntfy"; 35 | incoming-payload-content-type = "application/json"; 36 | pass-arguments-to-command = [ 37 | { 38 | source = "payload"; 39 | name = "externalURL"; 40 | } 41 | { 42 | source = "payload"; 43 | name = "alerts.0.status"; 44 | } 45 | { 46 | source = "payload"; 47 | name = "alerts.0.annotations.summary"; 48 | } 49 | { 50 | source = "payload"; 51 | name = "alerts.0.annotations.description"; 52 | } 53 | ]; 54 | }; 55 | }; 56 | }; 57 | 58 | services.consul.catalog = [ 59 | { 60 | name = "webhook"; 61 | port = config.services.webhook.port; 62 | tags = (import ./lib/traefik.nix).tagsForHost "webhook"; 63 | } 64 | ]; 65 | 66 | networking.firewall.allowedTCPPorts = [ config.services.webhook.port ]; 67 | } 68 | -------------------------------------------------------------------------------- /nodemcu/mqtt-dash.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mainTextSize": "LARGE", 4 | "postfix": "°C", 5 | "prefix": "", 6 | "textColor": -1, 7 | "enableIntermediateState": true, 8 | "enablePub": false, 9 | "enteredIntermediateStateAt": 0, 10 | "intermediateStateTimeout": 0, 11 | "jsOnReceive": "", 12 | "jsonPath": "$.AM2301.Temperature", 13 | "lastJsonPathValue": "29.3", 14 | "lastPayload": "{\"Time\":\"2022-06-20T19:02:08\",\"AM2301\":{\"Temperature\":29.3,\"Humidity\":44.4,\"DewPoint\":15.9},\"TempUnit\":\"C\"}", 15 | "qos": 0, 16 | "retained": false, 17 | "topic": "tele/tasmota_0E63DE/SENSOR", 18 | "topicPub": "", 19 | "updateLastPayloadOnPub": true, 20 | "id": "5b270896-2f9e-4d0b-b1dd-9b0ea9a27b51", 21 | "jsBlinkExpression": "", 22 | "jsOnDisplay": "", 23 | "jsOnTap": "", 24 | "lastActivity": 1655748127, 25 | "longId": 4, 26 | "name": "Bedroom temperature", 27 | "type": 1 28 | }, 29 | { 30 | "mainTextSize": "LARGE", 31 | "postfix": "%", 32 | "prefix": "", 33 | "textColor": -1, 34 | "enableIntermediateState": true, 35 | "enablePub": false, 36 | "enteredIntermediateStateAt": 0, 37 | "intermediateStateTimeout": 0, 38 | "jsOnReceive": "", 39 | "jsonPath": "$.AM2301.Humidity", 40 | "lastJsonPathValue": "44.4", 41 | "lastPayload": "{\"Time\":\"2022-06-20T19:02:08\",\"AM2301\":{\"Temperature\":29.3,\"Humidity\":44.4,\"DewPoint\":15.9},\"TempUnit\":\"C\"}", 42 | "qos": 0, 43 | "retained": false, 44 | "topic": "tele/tasmota_0E63DE/SENSOR", 45 | "topicPub": "", 46 | "updateLastPayloadOnPub": true, 47 | "id": "3c2b2a53-48a5-4e4c-8181-4184d3bff7a3", 48 | "jsBlinkExpression": "", 49 | "jsOnDisplay": "", 50 | "jsOnTap": "", 51 | "lastActivity": 1655748127, 52 | "longId": 5, 53 | "name": "Bedroom humidity", 54 | "type": 1 55 | }, 56 | { 57 | "mainTextSize": "LARGE", 58 | "postfix": "°C", 59 | "prefix": "", 60 | "textColor": -1, 61 | "enableIntermediateState": true, 62 | "enablePub": false, 63 | "enteredIntermediateStateAt": 0, 64 | "intermediateStateTimeout": 0, 65 | "jsOnReceive": "", 66 | "jsonPath": "$.AM2301.Temperature", 67 | "lastJsonPathValue": "28.0", 68 | "lastPayload": "{\"Time\":\"2022-06-20T19:01:49\",\"AM2301\":{\"Temperature\":28.0,\"Humidity\":42.8,\"DewPoint\":14.2},\"TempUnit\":\"C\"}", 69 | "qos": 0, 70 | "retained": false, 71 | "topic": "tele/tasmota_082320/SENSOR", 72 | "topicPub": "", 73 | "updateLastPayloadOnPub": true, 74 | "id": "f8815028-51e6-4ded-b495-a99335016e67", 75 | "jsBlinkExpression": "", 76 | "jsOnDisplay": "", 77 | "jsOnTap": "", 78 | "lastActivity": 1655748109, 79 | "longId": 6, 80 | "name": "Living room temperature", 81 | "type": 1 82 | }, 83 | { 84 | "mainTextSize": "LARGE", 85 | "postfix": "%", 86 | "prefix": "", 87 | "textColor": -1, 88 | "enableIntermediateState": true, 89 | "enablePub": false, 90 | "enteredIntermediateStateAt": 0, 91 | "intermediateStateTimeout": 0, 92 | "jsOnReceive": "", 93 | "jsonPath": "$.AM2301.Humidity", 94 | "lastJsonPathValue": "42.8", 95 | "lastPayload": "{\"Time\":\"2022-06-20T19:01:49\",\"AM2301\":{\"Temperature\":28.0,\"Humidity\":42.8,\"DewPoint\":14.2},\"TempUnit\":\"C\"}", 96 | "qos": 0, 97 | "retained": false, 98 | "topic": "tele/tasmota_082320/SENSOR", 99 | "topicPub": "", 100 | "updateLastPayloadOnPub": true, 101 | "id": "62a80370-5cbd-423e-9c90-f499961af3e0", 102 | "jsBlinkExpression": "", 103 | "jsOnDisplay": "", 104 | "jsOnTap": "", 105 | "lastActivity": 1655748109, 106 | "longId": 7, 107 | "name": "Living room humidity", 108 | "type": 1 109 | }, 110 | { 111 | "mainTextSize": "LARGE", 112 | "postfix": "°C", 113 | "prefix": "", 114 | "textColor": -1, 115 | "enableIntermediateState": true, 116 | "enablePub": false, 117 | "enteredIntermediateStateAt": 0, 118 | "intermediateStateTimeout": 0, 119 | "jsOnReceive": "", 120 | "jsonPath": "$.SI7021.Temperature", 121 | "lastJsonPathValue": "29.0", 122 | "lastPayload": "{\"Time\":\"2022-06-20T19:02:18\",\"SI7021\":{\"Temperature\":29.0,\"Humidity\":40.6,\"DewPoint\":14.2},\"TempUnit\":\"C\"}", 123 | "qos": 0, 124 | "retained": false, 125 | "topic": "tele/tasmota_D892EA/SENSOR", 126 | "topicPub": "", 127 | "updateLastPayloadOnPub": true, 128 | "id": "907077d4-18ca-4423-bfc4-17eb274ef10f", 129 | "jsBlinkExpression": "", 130 | "jsOnDisplay": "", 131 | "jsOnTap": "", 132 | "lastActivity": 1655748138, 133 | "longId": 8, 134 | "name": "Study temperature", 135 | "type": 1 136 | }, 137 | { 138 | "mainTextSize": "LARGE", 139 | "postfix": "%", 140 | "prefix": "", 141 | "textColor": -1, 142 | "enableIntermediateState": true, 143 | "enablePub": false, 144 | "enteredIntermediateStateAt": 0, 145 | "intermediateStateTimeout": 0, 146 | "jsOnReceive": "", 147 | "jsonPath": "$.SI7021.Humidity", 148 | "lastJsonPathValue": "40.6", 149 | "lastPayload": "{\"Time\":\"2022-06-20T19:02:18\",\"SI7021\":{\"Temperature\":29.0,\"Humidity\":40.6,\"DewPoint\":14.2},\"TempUnit\":\"C\"}", 150 | "qos": 0, 151 | "retained": false, 152 | "topic": "tele/tasmota_D892EA/SENSOR", 153 | "topicPub": "", 154 | "updateLastPayloadOnPub": true, 155 | "id": "a8b4c2c2-a48a-4a63-ad9e-3916d62cf25c", 156 | "jsBlinkExpression": "", 157 | "jsOnDisplay": "", 158 | "jsOnTap": "", 159 | "lastActivity": 1655748138, 160 | "longId": 9, 161 | "name": "Study humidity", 162 | "type": 1 163 | }, 164 | { 165 | "iconOff": "ic_light3_off", 166 | "iconOn": "ic_light3_on", 167 | "offColor": -1, 168 | "onColor": -1, 169 | "payloadOff": "OFF", 170 | "payloadOn": "ON", 171 | "enableIntermediateState": true, 172 | "enablePub": true, 173 | "enteredIntermediateStateAt": 0, 174 | "intermediateStateTimeout": 10, 175 | "jsOnReceive": "", 176 | "jsonPath": "", 177 | "lastPayload": "ON", 178 | "qos": 0, 179 | "retained": false, 180 | "topic": "stat/tasmota_96804E/POWER", 181 | "topicPub": "cmnd/tasmota_96804E/POWER", 182 | "updateLastPayloadOnPub": false, 183 | "id": "861cfd65-e4c3-4991-98d9-5f9d5251f295", 184 | "jsBlinkExpression": "", 185 | "jsOnDisplay": "", 186 | "jsOnTap": "", 187 | "lastActivity": 1651267094, 188 | "longId": 3, 189 | "name": "Window LED", 190 | "type": 2 191 | }, 192 | { 193 | "iconOff": "ic_light3_off", 194 | "iconOn": "ic_light3_on", 195 | "offColor": -1, 196 | "onColor": -1, 197 | "payloadOff": "OFF", 198 | "payloadOn": "ON", 199 | "enableIntermediateState": true, 200 | "enablePub": true, 201 | "enteredIntermediateStateAt": 0, 202 | "intermediateStateTimeout": 10, 203 | "jsOnReceive": "", 204 | "jsonPath": "", 205 | "lastPayload": "ON", 206 | "qos": 0, 207 | "retained": false, 208 | "topic": "stat/tasmota_68C818/POWER", 209 | "topicPub": "cmnd/tasmota_68C818/POWER", 210 | "updateLastPayloadOnPub": false, 211 | "id": "e9c3a9e0-faca-45b1-bc50-8a40949be40b", 212 | "jsBlinkExpression": "", 213 | "jsOnDisplay": "", 214 | "jsOnTap": "", 215 | "lastActivity": 1651267094, 216 | "longId": 1, 217 | "name": "TV lamp", 218 | "type": 2 219 | }, 220 | { 221 | "format": 0, 222 | "icon": "ic_sun", 223 | "enableIntermediateState": true, 224 | "enablePub": true, 225 | "enteredIntermediateStateAt": 0, 226 | "intermediateStateTimeout": 0, 227 | "jsOnReceive": "", 228 | "jsonPath": "$.Color", 229 | "lastJsonPathValue": "#EE7D03", 230 | "lastPayload": "", 231 | "qos": 0, 232 | "retained": false, 233 | "topic": "", 234 | "topicPub": "cmnd/tasmota_96804E/COLOR", 235 | "updateLastPayloadOnPub": true, 236 | "id": "d195cf7f-f3be-4da1-8370-d6b8889ee250", 237 | "jsBlinkExpression": "", 238 | "jsOnDisplay": "", 239 | "jsOnTap": "", 240 | "lastActivity": 1651264995, 241 | "longId": 2, 242 | "name": "Window LED color", 243 | "type": 6 244 | } 245 | ] 246 | -------------------------------------------------------------------------------- /nodemcu/provision.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | let 3 | tasmota = import ./tasmota.nix; 4 | 5 | # https://tasmota.github.io/docs/Commands 6 | 7 | # Custom NodeMCU board with an AM-2301 temperature and humidity sensor and an 8 | # HC-SR501 PIR motion sensor 9 | # https://tasmota.github.io/docs/PIR-Motion-Sensors/ 10 | config-2c-3a-e8-0e-63-de = [ 11 | { 12 | cmnd = "Wifi"; 13 | value = 3; # select explicitly the Wi-Fi mode 802.11b/g (2.4 GHz) for better connectivity 14 | } 15 | { 16 | cmnd = "Template"; 17 | value = tasmota.template { 18 | name = "MySensor"; 19 | gpio = { 20 | GPIO14 = tasmota.component.AM2301; 21 | }; 22 | }; 23 | } 24 | { 25 | cmnd = "SwitchMode1"; 26 | value = 1; 27 | } 28 | { 29 | cmnd = "SwitchTopic"; 30 | value = 0; 31 | } 32 | { 33 | cmnd = "Rule1"; 34 | value = "on Switch1#state=1 do publish stat/%topic%/PIR1 ON endon on Switch1#state=0 do Publish stat/%topic%/PIR1 OFF endon"; 35 | } 36 | { 37 | cmnd = "Rule1"; 38 | value = 0; 39 | } 40 | { 41 | cmnd = "SensorRetain"; 42 | value = "on"; 43 | } 44 | { 45 | cmnd = "Module"; 46 | value = 0; 47 | } 48 | ]; 49 | 50 | # Custom NodeMCU board with an AM-2301 temperature and humidity sensor on GPIO14 51 | config-2c-3a-e8-08-23-20 = [ 52 | { 53 | cmnd = "Template"; 54 | value = tasmota.template { 55 | name = "MySensor"; 56 | gpio = { 57 | GPIO14 = tasmota.component.AM2301; 58 | }; 59 | }; 60 | } 61 | { 62 | cmnd = "SetOption56"; 63 | value = "1"; # Wi-Fi network scan to select strongest signal on restart. 64 | } 65 | { 66 | cmnd = "SensorRetain"; 67 | value = "on"; 68 | } 69 | { 70 | cmnd = "Module"; 71 | value = 0; 72 | } 73 | ]; 74 | 75 | # MagicHome LED controller 76 | config-60-01-94-96-80-4e = [ 77 | { 78 | cmnd = "Template"; 79 | value = tasmota.template { 80 | name = "ZJ-ESP-IR-B-v2.3"; 81 | gpio = with tasmota.component; { 82 | GPIO4 = IRrecv; 83 | GPIO5 = PWM1; 84 | GPIO12 = PWM3; 85 | GPIO13 = PWM2; 86 | }; 87 | }; 88 | } 89 | { 90 | cmnd = "Fade"; 91 | value = 1; 92 | } 93 | { 94 | cmnd = "Speed"; 95 | value = 1; 96 | } 97 | { 98 | cmnd = "PowerRetain"; 99 | value = "on"; 100 | } 101 | { 102 | cmnd = "Module"; 103 | value = 0; 104 | } 105 | ]; 106 | 107 | # Sonoff Basic with a switch on GPIO14 108 | config-60-01-94-68-c8-18 = [ 109 | { 110 | cmnd = "Template"; 111 | value = tasmota.template { 112 | name = "Sonoff Basic"; 113 | gpio = with tasmota.component; { 114 | GPIO0 = Button1; 115 | GPIO1 = User; 116 | GPIO2 = User; 117 | GPIO3 = User; 118 | GPIO4 = User; 119 | GPIO12 = Relay1; 120 | GPIO13 = Led1i; 121 | GPIO14 = Switch1; 122 | }; 123 | base = tasmota.base.SonoffBasic; 124 | }; 125 | } 126 | { 127 | cmnd = "PowerRetain"; 128 | value = "on"; 129 | } 130 | { 131 | cmnd = "Module"; 132 | value = 0; 133 | } 134 | ]; 135 | 136 | # Sonoff TH16 with an Si7021 temperature and humidity sensor 137 | config-a4-cf-12-d8-92-ea = [ 138 | { 139 | cmnd = "Template"; 140 | value = tasmota.template { 141 | name = "Sonoff TH"; 142 | gpio = with tasmota.component; { 143 | GPIO0 = Button1; 144 | GPIO1 = User; 145 | GPIO2 = User; 146 | GPIO3 = User; 147 | GPIO4 = User; 148 | GPIO12 = Relay1; 149 | GPIO13 = Led1i; 150 | GPIO14 = SI7021; 151 | }; 152 | base = tasmota.base.SonoffTH; 153 | }; 154 | } 155 | { 156 | cmnd = "SetOption56"; 157 | value = "1"; # Wi-Fi network scan to select strongest signal on restart. 158 | } 159 | { 160 | cmnd = "SensorRetain"; 161 | value = "on"; 162 | } 163 | { 164 | cmnd = "Module"; 165 | value = 0; 166 | } 167 | ]; 168 | 169 | # Sonoff TH16 with an Si7021 temperature and humidity sensor 170 | config-a4-cf-12-d8-a2-dd = [ 171 | { 172 | cmnd = "Template"; 173 | value = tasmota.template { 174 | name = "Sonoff TH"; 175 | gpio = with tasmota.component; { 176 | GPIO0 = Button1; 177 | GPIO1 = User; 178 | GPIO2 = User; 179 | GPIO3 = User; 180 | GPIO4 = User; 181 | GPIO12 = Relay1; 182 | GPIO13 = Led1i; 183 | GPIO14 = SI7021; 184 | }; 185 | base = tasmota.base.SonoffTH; 186 | }; 187 | } 188 | { 189 | cmnd = "SensorRetain"; 190 | value = "on"; 191 | } 192 | { 193 | cmnd = "Module"; 194 | value = 0; 195 | } 196 | ]; 197 | 198 | backlogMessage = with builtins; config: 199 | let 200 | mkCommand = { cmnd, value }: "${cmnd} ${toString value}"; 201 | in 202 | assert length config <= 30; 203 | concatStringsSep ";" (map mkCommand config); 204 | 205 | IrRemote1 = { 206 | # See the command list https://tasmota.github.io/docs/Commands/#light 207 | "0x00FF906F" = "Dimmer +"; 208 | "0x00FFB847" = "Dimmer -"; 209 | "0x00FFF807" = "Power OFF"; 210 | "0x00FFB04F" = "Power ON"; 211 | # White 212 | "0x00FFA857" = "Color ffffff"; # W 213 | # Red shades 214 | "0x00FF9867" = "Color ff0000"; # R 215 | "0x00FFE817" = "Color ff5500"; 216 | "0x00FF02FD" = "Color ff8000"; 217 | "0x00FF50AF" = "Color ffd500"; 218 | "0x00FF38C7" = "Color ffff00"; 219 | }; 220 | 221 | IrRemote2 = { 222 | # Green shades 223 | "0x00FFD827" = "Color 00ff00"; # G 224 | "0x00FF48B7" = "Color 00ff55"; 225 | "0x00FF32CD" = "Color 00ff80"; 226 | "0x00FF7887" = "Color 00ffd5"; 227 | "0x00FF28D7" = "Color 00d5ff"; 228 | }; 229 | 230 | IrRemote3 = { 231 | # Blue shades 232 | "0x00FF8877" = "Color 0000ff"; # B 233 | "0x00FF6897" = "Color 3333ff"; 234 | "0x00FF20DF" = "Color 5500ff"; 235 | "0x00FF708F" = "Color aa00ff"; 236 | "0x00FFF00F" = "Color ff00d4"; 237 | # Schemes 238 | "0x00FFB24D" = ""; # FLASH 239 | "0x00FF00FF" = ""; # STROBE 240 | "0x00FF58A7" = "Scheme 2"; # FADE 241 | "0x00FF30CF" = "Scheme 4"; # SMOOTH 242 | }; 243 | 244 | ruleMessage = with builtins; keyMap: 245 | let 246 | lengthLimit = 511; # https://tasmota.github.io/docs/Rules/#rule-syntax 247 | 248 | mkRule = code: command: "ON IrReceived#Data=${code} DO ${command} ENDON"; 249 | 250 | rules = attrValues (mapAttrs mkRule keyMap); 251 | message = concatStringsSep " " rules; 252 | 253 | in 254 | assert stringLength message <= lengthLimit; message; 255 | 256 | 257 | send = device: command: value: 258 | "${pkgs.mosquitto}/bin/mosquitto_pub --host nuc --topic cmnd/${device}/${command} --message '${toString value}'"; 259 | 260 | tasmota_0E63DE = pkgs.writeScript "tasmota_0E63DE" '' 261 | ${send "tasmota_0E63DE" "Backlog" (backlogMessage config-2c-3a-e8-0e-63-de)} 262 | ''; 263 | 264 | tasmota_082320 = pkgs.writeScript "tasmota_082320" '' 265 | ${send "tasmota_082320" "Backlog" (backlogMessage config-2c-3a-e8-08-23-20)} 266 | ''; 267 | 268 | tasmota_68C818 = pkgs.writeScript "tasmota_68C818" '' 269 | ${send "tasmota_68C818" "Backlog" (backlogMessage config-60-01-94-68-c8-18)} 270 | ''; 271 | 272 | tasmota_D892EA = pkgs.writeScript "tasmota_D892EA" '' 273 | ${send "tasmota_D892EA" "Backlog" (backlogMessage config-a4-cf-12-d8-92-ea)} 274 | ''; 275 | 276 | tasmota_D8A2DD = pkgs.writeScript "tasmota_D8A2DD" '' 277 | ${send "tasmota_D8A2DD" "Backlog" (backlogMessage config-a4-cf-12-d8-a2-dd)} 278 | ''; 279 | 280 | tasmota_96804E = pkgs.writeScript "tasmota_96804E" '' 281 | ${send "tasmota_96804E" "Rule1" (ruleMessage IrRemote1)} 282 | ${send "tasmota_96804E" "Rule1" 1} 283 | ${send "tasmota_96804E" "Rule2" (ruleMessage IrRemote2)} 284 | ${send "tasmota_96804E" "Rule2" 1} 285 | ${send "tasmota_96804E" "Rule3" (ruleMessage IrRemote3)} 286 | ${send "tasmota_96804E" "Rule3" 1} 287 | ${send "tasmota_96804E" "Backlog" (backlogMessage config-60-01-94-96-80-4e)} 288 | ''; 289 | in 290 | pkgs.runCommand "provisioning_scripts" { } '' 291 | mkdir $out 292 | cp ${tasmota_0E63DE} $out/tasmota_0E63DE.sh 293 | cp ${tasmota_082320} $out/tasmota_082320.sh 294 | cp ${tasmota_68C818} $out/tasmota_68C818.sh 295 | cp ${tasmota_D892EA} $out/tasmota_D892EA.sh 296 | cp ${tasmota_D8A2DD} $out/tasmota_D8A2DD.sh 297 | cp ${tasmota_96804E} $out/tasmota_96804E.sh 298 | '' 299 | -------------------------------------------------------------------------------- /nodemcu/shell.nix: -------------------------------------------------------------------------------- 1 | with import { }; 2 | let 3 | 4 | firmware = fetchurl { 5 | url = "https://github.com/arendst/Tasmota/releases/download/v14.1.0/tasmota.bin"; 6 | sha256 = "sha256-tcy79NlSnmJ3foLfYIDLPbR5fpW9ItVkuApO9+MS9n0="; 7 | }; 8 | 9 | in 10 | mkShell { 11 | buildInputs = [ esptool picocom ]; 12 | 13 | shellHook = '' 14 | PORT=/dev/ttyUSB0 15 | BAUD=115200 16 | 17 | flash_backup() { 18 | esptool.py --baud $BAUD --port $PORT read_flash 0x00000 0x100000 image1M.bin 19 | } 20 | 21 | flash_erase() { 22 | esptool.py --baud $BAUD --port $PORT erase_flash 23 | } 24 | 25 | flash_write() { 26 | esptool.py --baud $BAUD --port $PORT write_flash -fs 1MB -fm dout 0x0 ${firmware} 27 | } 28 | 29 | serial_terminal() { 30 | picocom --baud $BAUD --omap crcrlf --echo $PORT 31 | } 32 | 33 | commit() { 34 | picocom --baud $BAUD --quiet --exit-after 1000 $PORT 35 | } 36 | 37 | device_reset() { 38 | echo "Reset 1" 39 | } 40 | 41 | device_config() { 42 | if [ "$#" -ne 2 ]; then 43 | echo "Usage: device_config WIFI_SSID WIFI_KEY" >&2 44 | exit 1 45 | fi 46 | 47 | WIFI_SSID="$1" 48 | WIFI_KEY="$2" 49 | 50 | echo -n "Backlog " 51 | 52 | echo -n "SSID1 $WIFI_SSID;" 53 | echo -n "Password1 $WIFI_KEY;" 54 | 55 | echo -n "MqttHost nuc;" 56 | echo -n "MqttUser 0;" 57 | echo -n "MqttPassword 0;" 58 | 59 | echo "" 60 | } 61 | ''; 62 | } 63 | -------------------------------------------------------------------------------- /nodemcu/tasmota.nix: -------------------------------------------------------------------------------- 1 | # Generate Tasmota JSON templates 2 | # https://tasmota.github.io/docs/Templates/ 3 | let 4 | Generic' = 18; 5 | 6 | template = { name, gpio, base ? Generic', flag ? 0 }: 7 | let 8 | 9 | # https://tasmota.github.io/docs/Templates/#gpio 10 | toList = 11 | { GPIO0 ? component.None 12 | , GPIO1 ? component.None 13 | , GPIO2 ? component.None 14 | , GPIO3 ? component.None 15 | , GPIO4 ? component.None 16 | , GPIO5 ? component.None 17 | , GPIO9 ? component.None 18 | , GPIO10 ? component.None 19 | , GPIO12 ? component.None 20 | , GPIO13 ? component.None 21 | , GPIO14 ? component.None 22 | , GPIO15 ? component.None 23 | , GPIO16 ? component.None 24 | , 25 | }: [ 26 | GPIO0 27 | GPIO1 28 | GPIO2 29 | GPIO3 30 | GPIO4 31 | GPIO5 32 | GPIO9 33 | GPIO10 34 | GPIO12 35 | GPIO13 36 | GPIO14 37 | GPIO15 38 | GPIO16 39 | ]; 40 | 41 | in 42 | builtins.toJSON { 43 | BASE = base; 44 | FLAG = flag; 45 | GPIO = toList gpio; 46 | NAME = name; 47 | }; 48 | 49 | # https://tasmota.github.io/docs/Modules/ 50 | base = { 51 | SonoffBasic = 1; 52 | SonoffTH = 4; 53 | Generic = Generic'; 54 | }; 55 | 56 | # https://tasmota.github.io/docs/Components/ 57 | component = { 58 | AM2301 = 2; 59 | Button1 = 17; 60 | IRrecv = 51; 61 | Led1i = 56; 62 | None = 0; 63 | PWM1 = 37; 64 | PWM2 = 38; 65 | PWM3 = 39; 66 | Relay1 = 21; 67 | SI7021 = 3; 68 | Switch1 = 9; 69 | User = 255; 70 | }; 71 | 72 | in 73 | { 74 | inherit component template base; 75 | } 76 | -------------------------------------------------------------------------------- /overview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
cache artifacts
cache artifacts
GitHub
GitHub
Home network
ESP8266-based
sensors
ESP8266-based...
hosts
hosts
Intel NUC
Intel NUC
Raspberry Pi 4
Raspberry P...
Raspberry Pi 3
Raspberry P...
Camera
v1
Camera...
ISP Internet Box
(DHCP, DNS)
ISP Internet Box...
NixOS containers
NixOS containers
services
services
VPN access
VPN access
build pipeline
build pipeline
Internet
Internet
continous deployment
continous deployment
Cachix
Cachix
Tailscale
Tailscale
External service
External service
Managed hardware
Managed hardware
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /router/config: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -o errexit 3 | 4 | if [ "$#" -ne 2 ]; then 5 | echo "Usage: $0 WIFI_SSID WIFI_KEY" >&2 6 | exit 1 7 | fi 8 | 9 | WIFI_SSID="$1" 10 | WIFI_KEY="$2" 11 | 12 | uci set system.@system[0].hostname="wrt" 13 | uci set system.@system[0].zonename="Europe/Zurich" 14 | 15 | # LAN 16 | uci set network.lan.ipaddr=172.16.0.1 17 | 18 | # WIRELESS 19 | 20 | # https://openwrt.org/toh/linksys/wrt3200acm#wifi_driver_bug_and_workaround 21 | opkg remove kmod-mwifiex-sdio 22 | 23 | wifi config > /etc/config/wireless 24 | 25 | # 5 GHz 26 | uci set wireless.radio0.disabled="0" 27 | uci set wireless.radio0.country="CH" 28 | uci set wireless.radio0.cell_density="0" 29 | uci set wireless.default_radio0.ssid="${WIFI_SSID}" 30 | uci set wireless.default_radio0.encryption='psk2' 31 | uci set wireless.default_radio0.key="$WIFI_KEY" 32 | 33 | # 2.4 GHz 34 | uci set wireless.radio1.disabled="0" 35 | uci set wireless.radio1.country="CH" 36 | uci set wireless.radio1.cell_density="0" 37 | uci set wireless.default_radio1.ssid="${WIFI_SSID}" 38 | uci set wireless.default_radio1.encryption='psk2' 39 | uci set wireless.default_radio1.key="$WIFI_KEY" 40 | 41 | # Local domain 42 | uci set dhcp.@dnsmasq[0].domain='thewagner.home' 43 | uci set dhcp.@dnsmasq[0].local='/thewagner.home/' 44 | 45 | # CNAME records 46 | while uci -q get dhcp.@cname[0]; do 47 | uci delete dhcp.@cname[0] 48 | done 49 | 50 | uci add dhcp cname 51 | uci set dhcp.@cname[-1].cname="alertmanager.thewagner.home" 52 | uci set dhcp.@cname[-1].target="nuc.thewagner.home" 53 | 54 | uci add dhcp cname 55 | uci set dhcp.@cname[-1].cname="git.thewagner.home" 56 | uci set dhcp.@cname[-1].target="nuc.thewagner.home" 57 | 58 | uci add dhcp cname 59 | uci set dhcp.@cname[-1].cname="hydra.thewagner.home" 60 | uci set dhcp.@cname[-1].target="nuc.thewagner.home" 61 | 62 | uci add dhcp cname 63 | uci set dhcp.@cname[-1].cname="loki.thewagner.home" 64 | uci set dhcp.@cname[-1].target="nuc.thewagner.home" 65 | 66 | uci add dhcp cname 67 | uci set dhcp.@cname[-1].cname="metrics.thewagner.home" 68 | uci set dhcp.@cname[-1].target="nuc.thewagner.home" 69 | 70 | uci add dhcp cname 71 | uci set dhcp.@cname[-1].cname="prometheus.thewagner.home" 72 | uci set dhcp.@cname[-1].target="nuc.thewagner.home" 73 | 74 | uci add dhcp cname 75 | uci set dhcp.@cname[-1].cname="tv.thewagner.home" 76 | uci set dhcp.@cname[-1].target="nuc.thewagner.home" 77 | 78 | uci add dhcp cname 79 | uci set dhcp.@cname[-1].cname="mqtt.thewagner.home" 80 | uci set dhcp.@cname[-1].target="nuc.thewagner.home" 81 | 82 | # Node exporter 83 | opkg install \ 84 | libubus-lua \ 85 | prometheus-node-exporter-lua \ 86 | prometheus-node-exporter-lua-nat_traffic \ 87 | prometheus-node-exporter-lua-netstat \ 88 | prometheus-node-exporter-lua-openwrt \ 89 | prometheus-node-exporter-lua-wifi \ 90 | prometheus-node-exporter-lua-wifi_stations 91 | uci set prometheus-node-exporter-lua.main.listen_interface='*' 92 | 93 | # Tailscale 94 | opkg install iptables tailscale 95 | 96 | CHANGES=$(uci changes) 97 | if [ -n "$CHANGES" ]; then 98 | echo "Changes:" 99 | uci changes 100 | echo "" 101 | echo "" 102 | echo "This script doesn't restart any services. You may need to run:" 103 | echo "- wifi" 104 | echo "- service network restart" 105 | echo "- service dnsmasq restart" 106 | echo "" 107 | echo "Or just reboot the device" 108 | else 109 | echo "No changes. Configuration is up-to-date!" 110 | fi 111 | 112 | uci commit 113 | -------------------------------------------------------------------------------- /router/config-repeater: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Setup Wi-Fi Repeater with relayd 3 | # Reference: https://openwrt.org/docs/guide-user/network/wifi/relay_configuration 4 | set -o errexit 5 | 6 | if [ "$#" -ne 3 ]; then 7 | echo "Usage: $0 FIRST_TIME WIFI_SSID WIFI_KEY" >&2 8 | exit 1 9 | fi 10 | 11 | FIRST_TIME="$1" 12 | WIFI_SSID="$2" 13 | WIFI_KEY="$3" 14 | 15 | uci set system.@system[0].hostname="wrt" 16 | uci set system.@system[0].zonename="Europe/Zurich" 17 | 18 | # LAN 19 | uci set network.lan.ipaddr=192.168.2.1 20 | 21 | # WIRELESS 22 | 23 | # https://openwrt.org/toh/linksys/wrt3200acm#wifi_driver_bug_and_workaround 24 | opkg remove kmod-mwifiex-sdio 25 | 26 | wifi config > /etc/config/wireless 27 | 28 | uci set wireless.radio0.disabled="0" 29 | uci set wireless.radio0.country="CH" 30 | uci set wireless.radio0.cell_density="0" 31 | uci set wireless.default_radio0.ssid="${WIFI_SSID}" 32 | uci set wireless.default_radio0.encryption='psk2' 33 | uci set wireless.default_radio0.key="$WIFI_KEY" 34 | 35 | uci set wireless.radio1.disabled="0" 36 | uci set wireless.radio1.country="CH" 37 | uci set wireless.radio1.cell_density="0" 38 | uci set wireless.default_radio1.ssid="${WIFI_SSID}" 39 | uci set wireless.default_radio1.encryption='psk2' 40 | uci set wireless.default_radio1.key="$WIFI_KEY" 41 | 42 | uci set dhcp.lan.ignore='1' 43 | 44 | # Configure Wi-Fi uplink. It obtains a DHCP address from the upstream hotspot. 45 | uci set network.wwan="interface" 46 | uci set network.wwan.proto="dhcp" 47 | 48 | # Add the Wi-Fi interface for the uplink. 49 | uci set wireless.wwan="wifi-iface" 50 | uci set wireless.wwan.device="radio0" 51 | uci set wireless.wwan.network="wwan" 52 | uci set wireless.wwan.mode="sta" 53 | uci set wireless.wwan.encryption="psk2" 54 | uci set wireless.wwan.ssid="$WIFI_SSID" 55 | uci set wireless.wwan.key="$WIFI_KEY" 56 | 57 | if $FIRST_TIME ; then 58 | uci commit 59 | echo "First time setup finished. After rebooting, the device's new address is 192.168.2.1" 60 | reboot 61 | exit 0 62 | fi 63 | 64 | # Add Relay interface 65 | if ! opkg list-installed | grep -q luci-proto-relay; then 66 | opkg update 67 | opkg install luci-proto-relay 68 | fi 69 | 70 | uci set network.repeater_bridge=interface 71 | uci set network.repeater_bridge.proto="relay" 72 | uci set network.repeater_bridge.network="lan wwan" 73 | 74 | # Firewall zone 75 | uci set firewall.@zone[0].network='lan repeater_bridge wwan' 76 | 77 | # Remove reduntant WAN interface and firewall zones 78 | if uci -q get network.wan; then 79 | uci delete network.wan 80 | fi 81 | 82 | if uci -q get network.wan6; then 83 | uci delete network.wan6 84 | fi 85 | 86 | if uci -q get firewall.@zone[1]; then 87 | uci delete firewall.@zone[1] 88 | fi 89 | 90 | # Install packages 91 | if [ "$(uname -m)" != "mips" ]; then 92 | opkg install \ 93 | libubus-lua \ 94 | prometheus-node-exporter-lua \ 95 | prometheus-node-exporter-lua-nat_traffic \ 96 | prometheus-node-exporter-lua-netstat \ 97 | prometheus-node-exporter-lua-openwrt \ 98 | prometheus-node-exporter-lua-wifi \ 99 | prometheus-node-exporter-lua-wifi_stations 100 | 101 | # Tailscale 102 | opkg install iptables tailscale 103 | fi 104 | 105 | uci commit 106 | wifi 107 | -------------------------------------------------------------------------------- /router/setup-repeater.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | if [ "$1" = "create-network-connections" ]; then 5 | nmcli connection delete id openwrt-repeater || true 6 | 7 | nmcli connection add \ 8 | type ethernet \ 9 | con-name openwrt-repeater \ 10 | autoconnect false \ 11 | ipv4.ignore-auto-routes yes \ 12 | ipv4.ignore-auto-dns yes \ 13 | ip4 192.168.2.2/24 14 | 15 | exit 0 16 | fi 17 | 18 | if [ "$1" = "first-time" ]; then 19 | SSH_COMMAND="ssh root@192.168.1.1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 20 | FIRST_TIME=true 21 | else 22 | SSH_COMMAND="ssh root@192.168.2.1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 23 | FIRST_TIME=false 24 | fi 25 | 26 | HERE=$(dirname "$(readlink -f "$0")") 27 | $SSH_COMMAND sh -s \ 28 | "$(printf "%q" $FIRST_TIME)" \ 29 | "$(printf "%q" "$(pass Home/wifi_ssid)")" \ 30 | "$(printf "%q" "$(pass Home/wifi_key)")" \ 31 | < "$HERE"/config-repeater 32 | -------------------------------------------------------------------------------- /router/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if [ "$1" = "first-time" ]; then 5 | SSH_COMMAND="ssh root@192.168.1.1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 6 | else 7 | SSH_COMMAND="ssh root@wrt" 8 | fi 9 | 10 | HERE=$(dirname "$(readlink -f "$0")") 11 | $SSH_COMMAND sh -s \ 12 | "$(printf "%q" "$(pass Home/wifi_ssid)")" \ 13 | "$(printf "%q" "$(pass Home/wifi_key)")" \ 14 | < "$HERE"/config 15 | -------------------------------------------------------------------------------- /scripts/listen-mqtt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | nix shell nixpkgs#mosquitto --command mosquitto_sub -h nuc -p 1883 -t '#' -v 4 | -------------------------------------------------------------------------------- /scripts/push-room-humidity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NTFY_ADDRESS=http://nuc:8080/home-thewagner-ec1 4 | GRAFANA_DASHBOARD="http://nuc:3000/d/-Jz7HnRMz/room-temperature-and-humidity?orgId=1&refresh=30s#" 5 | GRAFANA_ROOM_HUMIDITY="http://nuc:3000/render/d-solo/-Jz7HnRMz/room-temperature-and-humidity?orgId=1&refresh=30s&panelId=20&width=1000&height=500&tz=Europe%2FZurich" 6 | 7 | curl \ 8 | -H "Title: Grafana" \ 9 | -H "Click: $GRAFANA_DASHBOARD" \ 10 | -H "Attach: $GRAFANA_ROOM_HUMIDITY" \ 11 | -d "Room humidity for the past 24 hours" \ 12 | "$NTFY_ADDRESS" 13 | -------------------------------------------------------------------------------- /scripts/push-sunrise-sunset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NTFY_ADDRESS=http://nuc:8080/home-thewagner-ec1 4 | LAT="46.519833N" 5 | LON="6.6335E" 6 | 7 | now=$(sunwait poll $LAT $LON) 8 | 9 | if [ "$now" = "NIGHT" ]; then 10 | echo "Waiting for sunrise" 11 | HEADER="Sunrise!" 12 | MESSAGE="Good morning! 🌅" 13 | else 14 | echo "Waiting for sunset" 15 | HEADER="Sunset!" 16 | MESSAGE="Good evening! 🌇" 17 | fi 18 | 19 | sunwait wait $LAT $LON 20 | 21 | curl \ 22 | -H "Title: $HEADER" \ 23 | -d "$MESSAGE" \ 24 | "$NTFY_ADDRESS" 25 | -------------------------------------------------------------------------------- /scripts/reboot_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | wait_for_consul() { 5 | echo "Waiting for the Consul agent to re-connect" 6 | sleep 30 7 | } 8 | 9 | 10 | nixops reboot --include nuc 11 | wait_for_consul 12 | nixops reboot --include rp3 13 | -------------------------------------------------------------------------------- /scripts/switch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ "$#" -ne 1 ]; then 5 | echo "Usage: $0 HOST" >&2 6 | exit 1 7 | fi 8 | 9 | host="$1" 10 | 11 | if [ "$host" = "$(hostname)" ]; then 12 | sudo nixos-rebuild switch \ 13 | --flake ".#$host" 14 | else 15 | nix run 'nixpkgs#nixos-rebuild' -- switch \ 16 | --flake ".#$host" \ 17 | --target-host "root@$host" \ 18 | --build-host "root@$host" \ 19 | --fast 20 | fi 21 | -------------------------------------------------------------------------------- /scripts/test-alert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | nix shell nixpkgs#prometheus-alertmanager --command amtool \ 4 | --alertmanager.url http://nuc:9093 \ 5 | alert add \ 6 | --annotation=summary="Test alert" \ 7 | --annotation=description="This is a test alert sent from $(hostname) by $USER" \ 8 | instance="$(hostname)" 9 | -------------------------------------------------------------------------------- /treefmt.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | { 3 | projectRootFile = "flake.nix"; 4 | programs.actionlint.enable = true; 5 | programs.nixpkgs-fmt.enable = true; 6 | programs.shellcheck.enable = true; 7 | } 8 | -------------------------------------------------------------------------------- /x1.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, nixpkgs, disko, nixos-hardware, ... }: 2 | 3 | { 4 | imports = [ 5 | disko.nixosModules.disko 6 | nixpkgs.nixosModules.notDetected 7 | nixos-hardware.nixosModules.lenovo-thinkpad-x1-12th-gen 8 | ./modules/buildMachines.nix 9 | ./modules/cachix.nix 10 | ./modules/nas.nix 11 | ./modules/vpn.nix 12 | ]; 13 | 14 | boot = { 15 | initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" "usb_storage" "sd_mod" ]; 16 | loader.systemd-boot.enable = true; 17 | kernelModules = [ "kvm-intel" ]; 18 | }; 19 | 20 | swapDevices = [ ]; 21 | 22 | hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; 23 | 24 | networking = { 25 | hostName = "x1"; 26 | networkmanager.enable = true; 27 | hostId = builtins.substring 0 8 ( 28 | builtins.hashString "sha256" config.networking.hostName 29 | ); 30 | }; 31 | 32 | # Workaround for " Failed to start Network Manager Wait Online." after `nixos-rebuild switch` 33 | # See https://github.com/NixOS/nixpkgs/issues/180175 34 | systemd.services.NetworkManager-wait-online.enable = lib.mkForce false; 35 | 36 | nixpkgs.config.allowUnfree = true; 37 | 38 | powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; 39 | powerManagement.powertop.enable = true; 40 | 41 | # List packages installed in system profile. 42 | environment.systemPackages = with pkgs; [ 43 | acpi 44 | drawio 45 | curl 46 | #dropbox-cli 47 | fd 48 | file 49 | firefox-wayland 50 | fzf 51 | git 52 | gh 53 | httpie 54 | moreutils 55 | mosh 56 | mpv 57 | ntfs3g 58 | pass 59 | pwvucontrol 60 | pmount 61 | ripgrep 62 | tree 63 | trash-cli 64 | unzip 65 | vcsh 66 | wget 67 | wl-clipboard 68 | xdg-utils 69 | zathura 70 | zoom-us 71 | ]; 72 | 73 | fonts.packages = with pkgs; [ 74 | noto-fonts 75 | noto-fonts-emoji 76 | ]; 77 | 78 | security.wrappers = { 79 | pmount.source = "${pkgs.pmount}/bin/pmount"; 80 | pmount.owner = "root"; 81 | pmount.group = "root"; 82 | pmount.setuid = true; 83 | pumount.source = "${pkgs.pmount}/bin/pumount"; 84 | pumount.owner = "root"; 85 | pumount.group = "root"; 86 | pumount.setuid = true; 87 | }; 88 | 89 | programs = { 90 | autojump.enable = true; 91 | 92 | gnupg.agent = { enable = true; enableSSHSupport = true; }; 93 | 94 | sway = { 95 | enable = true; 96 | extraPackages = with pkgs; [ swaylock swayidle swayimg pulseaudio foot sway-contrib.grimshot wmenu ]; 97 | }; 98 | 99 | neovim = { 100 | enable = true; 101 | viAlias = true; 102 | vimAlias = true; 103 | }; 104 | 105 | zsh = { 106 | enable = true; 107 | ohMyZsh = { 108 | enable = true; 109 | theme = "robbyrussell"; 110 | plugins = [ "autojump" "dirhistory" "fzf" "git" "pass" "sudo" ]; 111 | }; 112 | }; 113 | }; 114 | 115 | xdg.portal.wlr.enable = true; 116 | 117 | hardware = { 118 | enableRedistributableFirmware = true; 119 | enableAllFirmware = true; 120 | 121 | bluetooth = { 122 | enable = true; 123 | }; 124 | 125 | trackpoint = { 126 | enable = true; 127 | }; 128 | }; 129 | 130 | services = { 131 | illum.enable = true; # Enable the brightness buttons 132 | 133 | openssh.enable = true; 134 | 135 | pipewire.pulse.enable = true; 136 | 137 | printing.enable = true; 138 | 139 | fprintd.enable = true; 140 | 141 | tailscale.useRoutingFeatures = "client"; 142 | 143 | tlp.enable = true; 144 | 145 | greetd = { 146 | enable = true; 147 | settings = { 148 | default_session = { 149 | command = "${pkgs.greetd.greetd}/bin/agreety --cmd sway"; 150 | }; 151 | }; 152 | }; 153 | 154 | upower.enable = true; 155 | }; 156 | 157 | virtualisation.podman.enable = true; 158 | virtualisation.virtualbox.host.enable = true; 159 | 160 | # Define a user account. Don't forget to set a password with ‘passwd’. 161 | users.users.dwagner = { 162 | isNormalUser = true; 163 | shell = pkgs.zsh; 164 | extraGroups = [ "dialout" "networkmanager" "vboxusers" "wheel" ]; 165 | }; 166 | 167 | nix.settings.trusted-users = [ "root" "@wheel" ]; 168 | 169 | # This value determines the NixOS release with which your system is to be 170 | # compatible, in order to avoid breaking some software such as database 171 | # servers. You should change this only after NixOS release notes say you 172 | # should. 173 | system.stateVersion = "24.05"; # Did you read the comment? 174 | 175 | # Backup 176 | services.borgbackup.jobs.home = { 177 | paths = "/home"; 178 | patterns = [ 179 | "+ /home/*/documents" 180 | "+ /home/*/projects" 181 | "+ /home/*/.password-store" 182 | "+ /home/*/.ssh/id_*" 183 | "+ /home/*/.gnupg/**" 184 | "- **/*.o" 185 | "- **/.pyc" 186 | "- **/.swp" 187 | "- **" 188 | ]; 189 | repo = "borg@nuc:."; 190 | environment = { BORG_RSH = "ssh -i /root/keys/id_ed25519-borg-x1"; }; 191 | encryption.mode = "none"; 192 | doInit = false; 193 | startAt = "daily"; 194 | persistentTimer = true; 195 | prune.keep = { 196 | within = "1d"; 197 | daily = 7; 198 | weekly = 4; 199 | monthly = 12; 200 | yearly = 10; 201 | }; 202 | }; 203 | 204 | disko.devices = { 205 | disk = { 206 | system = { 207 | type = "disk"; 208 | device = "/dev/disk/by-id/nvme-eui.ace42e003b022f602ee4ac0000000001"; 209 | content = { 210 | type = "gpt"; 211 | partitions = { 212 | ESP = { 213 | size = "512M"; 214 | type = "EF00"; 215 | content = { 216 | type = "filesystem"; 217 | format = "vfat"; 218 | mountpoint = "/boot"; 219 | # Fix world-accessible /boot/loader/random-seed 220 | # https://github.com/nix-community/disko/issues/527#issuecomment-1924076948 221 | mountOptions = [ "umask=0077" ]; 222 | }; 223 | }; 224 | zfs = { 225 | size = "100%"; 226 | content = { 227 | type = "zfs"; 228 | pool = "rpool"; 229 | }; 230 | }; 231 | }; # partitions 232 | }; # content 233 | }; # system 234 | }; # disk 235 | zpool = { 236 | rpool = { 237 | type = "zpool"; 238 | rootFsOptions = { 239 | acltype = "posixacl"; 240 | atime = "off"; 241 | canmount = "off"; 242 | compression = "on"; 243 | mountpoint = "none"; 244 | xattr = "sa"; 245 | "com.sun:auto-snapshot" = "false"; 246 | }; 247 | 248 | datasets = { 249 | local = { 250 | type = "zfs_fs"; 251 | options.mountpoint = "none"; 252 | }; 253 | safe = { 254 | type = "zfs_fs"; 255 | options.mountpoint = "none"; 256 | }; 257 | "local/log" = { 258 | type = "zfs_fs"; 259 | mountpoint = "/var/log"; 260 | options = { 261 | mountpoint = "legacy"; 262 | "com.sun:auto-snapshot" = "true"; 263 | }; 264 | }; 265 | "local/nix" = { 266 | type = "zfs_fs"; 267 | mountpoint = "/nix"; 268 | options = { 269 | atime = "off"; 270 | canmount = "on"; 271 | mountpoint = "legacy"; 272 | "com.sun:auto-snapshot" = "true"; 273 | }; 274 | }; 275 | "local/root" = { 276 | type = "zfs_fs"; 277 | mountpoint = "/"; 278 | options.mountpoint = "legacy"; 279 | postCreateHook = '' 280 | zfs snapshot rpool/local/root@blank 281 | ''; 282 | }; 283 | "safe/home" = { 284 | type = "zfs_fs"; 285 | mountpoint = "/home"; 286 | options = { 287 | mountpoint = "legacy"; 288 | "com.sun:auto-snapshot" = "true"; 289 | }; 290 | }; 291 | "safe/persist" = { 292 | type = "zfs_fs"; 293 | mountpoint = "/persist"; 294 | options = { 295 | mountpoint = "legacy"; 296 | "com.sun:auto-snapshot" = "true"; 297 | }; 298 | }; 299 | }; # datasets 300 | }; # rpool 301 | }; # zpool 302 | }; # disko.devices 303 | } 304 | -------------------------------------------------------------------------------- /x230.nix: -------------------------------------------------------------------------------- 1 | # Edit this configuration file to define what should be installed on 2 | # your system. Help is available in the configuration.nix(5) man page 3 | # and in the NixOS manual (accessible by running ‘nixos-help’). 4 | 5 | { config, lib, pkgs, nixpkgs, nixos-hardware, ... }: 6 | 7 | { 8 | imports = [ 9 | nixpkgs.nixosModules.notDetected 10 | nixos-hardware.nixosModules.lenovo-thinkpad-x230 11 | ./hardware/x230.nix 12 | ./modules/buildMachines.nix 13 | ./modules/cachix.nix 14 | ./modules/nas.nix 15 | ./modules/vpn.nix 16 | ]; 17 | 18 | # Use the systemd-boot EFI boot loader. 19 | boot.loader.systemd-boot.enable = true; 20 | boot.loader.efi.canTouchEfiVariables = true; 21 | 22 | networking = { 23 | hostName = "x230"; 24 | networkmanager.enable = true; 25 | }; 26 | 27 | # Workaround for " Failed to start Network Manager Wait Online." after `nixos-rebuild switch` 28 | # See https://github.com/NixOS/nixpkgs/issues/180175 29 | systemd.services.NetworkManager-wait-online.enable = lib.mkForce false; 30 | 31 | nixpkgs.config.allowUnfree = true; 32 | 33 | powerManagement.powertop.enable = true; 34 | 35 | # List packages installed in system profile. 36 | environment.systemPackages = with pkgs; [ 37 | acpi 38 | bat 39 | curl 40 | #dropbox-cli 41 | fd 42 | file 43 | firefox-wayland 44 | fzf 45 | git 46 | gh 47 | httpie 48 | moreutils 49 | mosh 50 | mpv 51 | ntfs3g 52 | pass 53 | pavucontrol 54 | pmount 55 | ripgrep 56 | tree 57 | unzip 58 | vcsh 59 | wget 60 | wl-clipboard 61 | xdg-utils 62 | zathura 63 | zoom-us 64 | ]; 65 | 66 | fonts.packages = with pkgs; [ 67 | noto-fonts 68 | noto-fonts-emoji 69 | ]; 70 | 71 | security.wrappers = { 72 | pmount.source = "${pkgs.pmount}/bin/pmount"; 73 | pmount.owner = "root"; 74 | pmount.group = "root"; 75 | pmount.setuid = true; 76 | pumount.source = "${pkgs.pmount}/bin/pumount"; 77 | pumount.owner = "root"; 78 | pumount.group = "root"; 79 | pumount.setuid = true; 80 | }; 81 | 82 | programs = { 83 | autojump.enable = true; 84 | 85 | gnupg.agent = { enable = true; enableSSHSupport = true; }; 86 | 87 | sway = { 88 | enable = true; 89 | extraPackages = with pkgs; [ swaylock swayidle swayimg foot sway-contrib.grimshot wmenu ]; 90 | }; 91 | 92 | neovim = { 93 | enable = true; 94 | viAlias = true; 95 | vimAlias = true; 96 | }; 97 | 98 | zsh = { 99 | enable = true; 100 | ohMyZsh = { 101 | enable = true; 102 | theme = "robbyrussell"; 103 | plugins = [ "autojump" "dirhistory" "fzf" "git" "pass" "sudo" ]; 104 | }; 105 | }; 106 | }; 107 | 108 | xdg.portal.wlr.enable = true; 109 | 110 | hardware = { 111 | enableRedistributableFirmware = true; 112 | enableAllFirmware = true; 113 | 114 | bluetooth = { 115 | enable = true; 116 | }; 117 | 118 | trackpoint = { 119 | enable = true; 120 | speed = 200; 121 | sensitivity = 200; 122 | }; 123 | }; 124 | 125 | services = { 126 | avahi = { 127 | enable = true; 128 | nssmdns4 = true; 129 | openFirewall = true; 130 | }; 131 | 132 | illum.enable = true; # Enable the brightness buttons 133 | 134 | openssh.enable = true; 135 | 136 | printing.enable = true; 137 | 138 | tailscale.useRoutingFeatures = "client"; 139 | 140 | greetd = { 141 | enable = true; 142 | settings = { 143 | default_session = { 144 | command = "${pkgs.greetd.greetd}/bin/agreety --cmd sway"; 145 | }; 146 | }; 147 | }; 148 | }; 149 | 150 | virtualisation.podman.enable = true; 151 | virtualisation.virtualbox.host.enable = true; 152 | 153 | # Define a user account. Don't forget to set a password with ‘passwd’. 154 | users.users.dwagner = { 155 | isNormalUser = true; 156 | shell = pkgs.zsh; 157 | extraGroups = [ "dialout" "networkmanager" "vboxusers" "wheel" ]; 158 | }; 159 | 160 | nix.settings.trusted-users = [ "root" "@wheel" ]; 161 | 162 | # This value determines the NixOS release with which your system is to be 163 | # compatible, in order to avoid breaking some software such as database 164 | # servers. You should change this only after NixOS release notes say you 165 | # should. 166 | system.stateVersion = "22.05"; # Did you read the comment? 167 | } 168 | --------------------------------------------------------------------------------