auto-update-daily-20260202
  1{
  2  config,
  3  globals,
  4  libx,
  5  pkgs,
  6  ...
  7}:
  8{
  9
 10  imports = [
 11    ../common/hardware/laptop.nix
 12    ../common/programs/direnv.nix
 13    ../common/programs/git.nix
 14    ../common/programs/nix-ld.nix
 15    ../common/programs/tmux.nix
 16    # ../common/services/networkmanager.nix
 17    # ../common/services/fprint.nix # With yubikey I don't really need this to be honest
 18    ../common/services/ansible.nix
 19    ../common/services/containers.nix
 20    ../common/services/docker.nix
 21    ../common/services/libvirt.nix
 22    ../common/services/binfmt.nix
 23    # ../common/services/oci-image-mirroring.nixi
 24    # ../common/services/ollama.nix # TODO handle nvidia vs not ?
 25    ../common/services/prometheus-exporters-node.nix
 26    # ../common/services/gitea-runner
 27
 28    ../redhat
 29
 30    # OpenShift port forwarding
 31    ./openshift-port-forward.nix
 32
 33    # Remote build system
 34    ../../modules/job-notify
 35    ../../modules/nixpkgs-consolidate
 36    ../../modules/microshift
 37
 38    # Binary cache
 39    ../../modules/harmonia
 40
 41    # XMPP Research Bot
 42    ../../modules/xmpp-research-bot
 43
 44    # Automated flake updates
 45    ../../modules/nix-flake-updater
 46
 47    # MicroVMs for isolated Claude Code agents
 48    ./microvms.nix
 49  ];
 50
 51  # Firewall is enabled in openshift-port-forward.nix
 52  # networking.firewall.enable = false;
 53
 54  # Suppress malformed DHCP option 24 (MTU plateau) warnings from router
 55  networking.dhcpcd.extraConfig = ''
 56    nooption mtu_plateau
 57  '';
 58
 59  # OpenShift SNO endpoints
 60  networking.extraHosts = ''
 61    192.168.100.7 api.ocp4.lab.home
 62    192.168.100.7 api-int.ocp4.lab.home
 63    192.168.100.7 console-openshift-console.apps.ocp4.lab.home
 64    192.168.100.7 oauth-openshift.apps.ocp4.lab.home
 65  '';
 66
 67  # Age secrets
 68  age.secrets."ntfy-token" = {
 69    file = ../../secrets/sakhalin/ntfy-token.age;
 70    mode = "440";
 71    owner = "root";
 72    group = "users";
 73  };
 74  age.secrets."harmonia-aomi-signing-key" = {
 75    file = ../../secrets/harmonia/aomi-signing-key.age;
 76    mode = "440";
 77    owner = "root";
 78    group = "root";
 79  };
 80  age.secrets."xmpp-research-bot-password" = {
 81    file = ../../secrets/aomi/xmpp-research-bot-password.age;
 82    mode = "400";
 83    owner = "vincent";
 84    group = "users";
 85  };
 86  age.secrets."gemini-api-key" = {
 87    file = ../../secrets/aomi/gemini-api-key.age;
 88    mode = "400";
 89    owner = "vincent";
 90    group = "users";
 91  };
 92
 93  # TODO make it an option ? (otherwise I'll add it for all)
 94  users.users.vincent.linger = true;
 95
 96  # Binary cache server (x86_64-linux)
 97  services.harmonia-cache = {
 98    enable = true;
 99    signKeyPath = config.age.secrets."harmonia-aomi-signing-key".path;
100    port = 5000;
101    workers = 4;
102    priority = 30;
103
104    # Nightly cache pre-population
105    builder = {
106      enable = true;
107      systems = [
108        "aomi" # Self
109        "kyushu" # Work laptop
110        "sakhalin" # Server
111      ];
112      schedule = "02:00"; # 2 AM daily
113      notification = {
114        enable = true;
115        tokenFile = config.age.secrets."ntfy-token".path;
116      };
117    };
118  };
119
120  # Remote build system
121  services.job-notify = {
122    enable = true;
123    ntfyServer = "https://ntfy.sbr.pm";
124    ntfyTokenFile = config.age.secrets."ntfy-token".path;
125    defaultTopic = "builds";
126  };
127
128  # Nixpkgs WIP branch consolidation automation
129  services.nixpkgs-consolidate = {
130    enable = true;
131    user = "vincent";
132    schedule = "daily";
133    ntfyTokenFile = config.age.secrets."ntfy-token".path;
134    configFile = "/home/vincent/.config/nixpkgs-automation/branches.conf";
135  };
136
137  # XMPP Research Bot (uses Vertex AI for Claude, direct API for Gemini)
138  services.xmpp-research-bot = {
139    enable = true;
140    jid = "researchbot@xmpp.sbr.pm";
141    ownerJid = "vincent@xmpp.sbr.pm";
142    passwordFile = config.age.secrets."xmpp-research-bot-password".path;
143    geminiApiKeyFile = config.age.secrets."gemini-api-key".path;
144    vertexProjectId = "itpc-gcp-pnd-pe-eng-claude";
145    vertexRegion = "us-east5";
146    inboxPath = "/home/vincent/desktop/org/inbox.org";
147    commandsPath = "/home/vincent/.config/xmpp-research-bot/commands.yaml";
148    user = "vincent";
149    group = "users";
150  };
151
152  # Automated flake.lock updates with build verification
153  services.nix-flake-updater = {
154    # Weekly updates for all inputs
155    weekly = {
156      enable = true;
157      repoPath = "/home/vincent/src/home";
158
159      # Build systems across both architectures for verification
160      buildSystems = [
161        # x86_64-linux systems
162        "aomi" # Self (laptop/build server)
163        "kyushu" # Work laptop
164        "sakhalin" # Server
165        "kerkouane" # VPS server
166
167        # aarch64-linux systems
168        "rhea" # Main media server
169        "aion" # XMPP/podcast server
170        "athena" # Raspberry Pi 4
171        "demeter" # Raspberry Pi 4
172        "aix" # Raspberry Pi 4
173      ];
174
175      # Run weekly on Sunday at 2 AM
176      schedule = "Sun *-*-* 02:00:00";
177
178      # Notifications via ntfy
179      ntfyServer = "https://ntfy.sbr.pm";
180      ntfyTopic = "nix-updates";
181      ntfyTokenFile = config.age.secrets."ntfy-token".path;
182
183      # Git settings
184      gitRemote = "origin";
185      branchPrefix = "flake-update-";
186
187      # Run as vincent (has git push access)
188      user = "vincent";
189
190      # Add randomized delay to avoid conflicts
191      randomizedDelaySec = 1800; # 0-30 min delay
192    };
193
194    # Daily automated updates for chick-group and chapeau-rouge with auto-merge
195    daily = {
196      enable = true;
197      repoPath = "/home/vincent/src/home";
198
199      # Update only personal repos
200      flakeInputs = [
201        "chick-group"
202        "chapeau-rouge"
203      ];
204
205      # Auto-merge to main on successful build
206      autoMerge = true;
207
208      # Build fewer systems for faster daily updates
209      buildSystems = [
210        "aomi" # Self (x86_64-linux)
211        "kyushu" # Work laptop (x86_64-linux)
212      ];
213
214      # Run daily at 4 AM
215      schedule = "*-*-* 04:00:00";
216
217      # Notifications via ntfy (same topic as weekly)
218      ntfyServer = "https://ntfy.sbr.pm";
219      ntfyTopic = "nix-updates";
220      ntfyTokenFile = config.age.secrets."ntfy-token".path;
221
222      # Git settings
223      gitRemote = "origin";
224      mainBranch = "main";
225      branchPrefix = "auto-update-daily-";
226
227      # Org inbox for failure TODOs
228      inboxOrg = "/home/vincent/desktop/org/inbox.org";
229
230      # Run as vincent (has git push access)
231      user = "vincent";
232
233      # Smaller delay for daily updates
234      randomizedDelaySec = 600; # 0-10 min delay
235    };
236  };
237
238  services = {
239    logind.settings.Login = {
240      HandleLidSwitch = "ignore";
241      HandleLidSwitchExternalPower = "ignore";
242      HandleLidSwitchDocked = "ignore";
243    };
244    wireguard = {
245      enable = true;
246      ips = libx.wg-ips globals.machines.aomi.net.vpn.ips;
247      endpoint = "${globals.net.vpn.endpoint}";
248      endpointPublicKey = "${globals.machines.kerkouane.net.vpn.pubkey}";
249    };
250    ollama = {
251      enable = true;
252      # acceleration = "cuda"; # no nvidia :D
253      host = "0.0.0.0"; # Listen on all interfaces for network access
254      port = 11434;
255      loadModels = [
256        # Coding Models
257        "qwen2.5-coder:7b" # Best coding: 88.4% HumanEval, Apache 2.0 (~4-5GB, 10-15 tok/s)
258        "codestral" # Latest coding (Jan 2025): 86.6% HumanEval, #1 LMsys (~14GB, 8-10 tok/s)
259
260        # Reasoning Models
261        "phi4-reasoning" # Best 14B reasoning: outperforms 70B distillation, MIT (~9GB, 6-10 tok/s)
262        "deepseek-r1:7b" # Lightweight reasoning: MIT license (~4.5GB, 8-12 tok/s)
263
264        # Multimodal
265        "qwen2.5vl:7b" # Best vision: beats Llama 3.2 11B, Apache 2.0 (~6GB, 5-8 tok/s)
266
267        # Quick Tasks
268        "phi3.5:3.8b" # Ultra-fast all-rounder: MIT license (~2.4GB, 15-25 tok/s)
269
270        # Tool Calling / OpenCode Support
271        "llama3.1:8b" # Native tool support, good for OpenCode (~4.7GB)
272        "mistral-nemo" # Fast tool calling support (~7.7GB)
273
274        # Legacy (keeping for compatibility)
275        "mistral:7b-instruct-q4_K_M" # General purpose fallback
276      ];
277      environmentVariables = {
278        OLLAMA_MODELS = "/var/lib/ollama/models";
279        OLLAMA_NUM_PARALLEL = "1"; # CPU-only, keep it simple
280        OLLAMA_KEEP_ALIVE = "10m";
281      };
282    };
283    smartd = {
284      enable = true;
285      devices = [ { device = "/dev/nvme0n1"; } ];
286    };
287    hardware.bolt.enable = true;
288    # gitea-actions-runner = {
289    #   instances = {
290    #     "aomi-codeberg" = {
291    #       name = "aomi";
292    #       enable = true;
293    #       url = "https://codeberg.org";
294    #       # tokenFile = "/home/vincent/sync/codeberg.token";
295    #       tokenFile = "/etc/codeberg.token";
296    #       labels = [
297    #         # "local:host"
298    #         "nixos-${pkgs.system}:host"
299    #         "native:host"
300    #         "docker:docker://gitea/runner-images:ubuntu-latest"
301    #         "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
302    #         "ubuntu-24.04:docker://gitea/runner-images:ubuntu-24.04"
303    #         "ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04"
304    #         "ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04"
305    #         # "nix:docker://localhost:5921/nix-runner"
306    #       ];
307    #       hostPackages = with pkgs; [
308    #         bash
309    #         direnv
310    #         coreutils
311    #         curl
312    #         gawk
313    #         nixVersions.stable
314    #         gitFull
315    #         gnused
316    #         docker
317    #         openssh
318    #         wget
319    #       ];
320    #     };
321    #   };
322    # };
323  };
324
325  # Ollama Prometheus Exporter (Docker-based, built locally)
326  systemd.tmpfiles.rules = [
327    "d /var/lib/ollama-exporter 0755 root root -"
328    "d /var/lib/git-builds 0755 builder users -"
329  ];
330
331  systemd.services.ollama-exporter = {
332    description = "Ollama Prometheus Exporter";
333    after = [
334      "docker.service"
335      "ollama.service"
336    ];
337    requires = [ "docker.service" ];
338    wantedBy = [ "multi-user.target" ];
339
340    serviceConfig = {
341      Type = "simple";
342      Restart = "always";
343      RestartSec = "10s";
344
345      ExecStartPre = [
346        # Stop and remove existing container
347        "-${pkgs.docker}/bin/docker stop ollama-exporter"
348        "-${pkgs.docker}/bin/docker rm ollama-exporter"
349        # Copy source files to build directory (for future manual rebuilds if needed)
350        "${pkgs.coreutils}/bin/cp ${../../tools/ollama-exporter/Dockerfile} /var/lib/ollama-exporter/Dockerfile"
351        "${pkgs.coreutils}/bin/cp ${../../tools/ollama-exporter/ollama_exporter.py} /var/lib/ollama-exporter/ollama_exporter.py"
352        # Build image locally only if it doesn't exist (to avoid DNS timeout issues)
353        "-${pkgs.bash}/bin/bash -c '${pkgs.docker}/bin/docker image inspect ollama-exporter:local >/dev/null 2>&1 || ${pkgs.docker}/bin/docker build -t ollama-exporter:local /var/lib/ollama-exporter'"
354      ];
355
356      ExecStart = ''
357        ${pkgs.docker}/bin/docker run --rm --name ollama-exporter \
358          -p 8000:8000 \
359          -e OLLAMA_HOST=http://localhost:11434 \
360          --network host \
361          ollama-exporter:local
362      '';
363
364      ExecStop = "${pkgs.docker}/bin/docker stop ollama-exporter";
365    };
366  };
367
368  # Open firewall for Ollama exporter
369  networking.firewall.allowedTCPPorts = [ 8000 ];
370
371  # Builder user for remote builds
372  users.users.builder = {
373    isSystemUser = true;
374    group = "users";
375    home = "/var/lib/git-builds";
376    createHome = true;
377    openssh.authorizedKeys.keys = [
378      # This will be populated with kerkouane's SSH key for build triggering
379    ];
380  };
381
382  # Build execution script
383  environment.etc."git-builds/execute-build.sh" = {
384    text = ''
385      #!${pkgs.bash}/bin/bash
386      set -euo pipefail
387
388      REPO_NAME="$1"
389      BUILD_TYPE="''${2:-auto}"
390      REPO_URL="vincent@kerkouane.sbr.pm:git/public/$REPO_NAME.git"
391
392      # Cache directory and worktree
393      CACHE_DIR="/var/lib/git-builds/$REPO_NAME.git"
394      BUILD_DIR="/var/lib/git-builds/$REPO_NAME-build-$(${pkgs.coreutils}/bin/date +%s)"
395
396      echo "Building $REPO_NAME (type: $BUILD_TYPE)"
397      echo "Cache: $CACHE_DIR"
398      echo "Build directory: $BUILD_DIR"
399
400      # Fetch or clone
401      if [ -d "$CACHE_DIR" ]; then
402        echo "Fetching updates for $REPO_NAME..."
403        cd "$CACHE_DIR" && ${pkgs.git}/bin/git fetch origin
404      else
405        echo "Cloning $REPO_NAME..."
406        ${pkgs.git}/bin/git clone --bare "$REPO_URL" "$CACHE_DIR"
407      fi
408
409      # Create worktree for isolated build
410      echo "Creating worktree at $BUILD_DIR..."
411      cd "$CACHE_DIR"
412      ${pkgs.git}/bin/git worktree add "$BUILD_DIR" main
413
414      cd "$BUILD_DIR"
415      echo "Working directory: $(pwd)"
416
417      # Execute build based on type
418      case "$BUILD_TYPE" in
419        nixos)
420          echo "Running NixOS build..."
421          ${pkgs.nixos-rebuild}/bin/nixos-rebuild build --flake .#aomi
422          ;;
423        make)
424          echo "Running make build..."
425          ${pkgs.gnumake}/bin/make build
426          ;;
427        docker)
428          echo "Running Docker build..."
429          ${pkgs.docker}/bin/docker build -t "$REPO_NAME:latest" .
430          ;;
431        go)
432          echo "Running Go build..."
433          ${pkgs.go}/bin/go build ./...
434          ;;
435        custom)
436          if [ -x .git-build.sh ]; then
437            echo "Running custom build script..."
438            ./.git-build.sh
439          else
440            echo "ERROR: .git-build.sh not found or not executable"
441            exit 1
442          fi
443          ;;
444        auto)
445          echo "Auto-detecting build type..."
446          if [ -f flake.nix ]; then
447            echo "Detected NixOS flake"
448            ${pkgs.nixos-rebuild}/bin/nixos-rebuild build --flake .#aomi
449          elif [ -f Makefile ]; then
450            echo "Detected Makefile"
451            ${pkgs.gnumake}/bin/make build
452          elif [ -f Dockerfile ]; then
453            echo "Detected Dockerfile"
454            ${pkgs.docker}/bin/docker build -t "$REPO_NAME:latest" .
455          elif [ -f go.mod ]; then
456            echo "Detected Go module"
457            ${pkgs.go}/bin/go build ./...
458          else
459            echo "ERROR: Could not auto-detect build type"
460            exit 1
461          fi
462          ;;
463        *)
464          echo "ERROR: Unknown build type: $BUILD_TYPE"
465          exit 1
466          ;;
467      esac
468
469      echo "Build completed successfully!"
470
471      # Cleanup worktree (keep cache)
472      echo "Cleaning up worktree..."
473      cd /
474      ${pkgs.git}/bin/git -C "$CACHE_DIR" worktree remove "$BUILD_DIR"
475      echo "Done!"
476    '';
477    mode = "0755";
478  };
479
480  # Allow builder to run systemd-run without password
481  security.sudo.extraRules = [
482    {
483      users = [ "builder" ];
484      commands = [
485        {
486          command = "${pkgs.systemd}/bin/systemd-run";
487          options = [ "NOPASSWD" ];
488        }
489      ];
490    }
491  ];
492
493  # MicroShift via CRC (disabled for now)
494  services.microshift = {
495    enable = false;
496    cpus = 4;
497    memory = 8192; # 8GB
498    diskSize = 40;
499    user = "vincent";
500    # pullSecret will be configured via agenix later
501  };
502
503}