this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: basic install API

Khue Doan 699ff766 e30a5b14

+100 -18
+2 -2
README.md
··· 13 13 - [x] Simple, declarative JSON configuration 14 14 - [x] Build a custom NixOS installer from a flake 15 15 - [x] Built-in PXE server based on [Pixiecore](https://github.com/danderson/netboot/tree/main/pixiecore) to serve netboot components from the custom installer 16 + - [x] Install NixOS from a flake using [nixos-anywhere](https://nix-community.github.io/nixos-anywhere) 16 17 - [ ] Custom agent and API to manage the installation process 17 - - [ ] Install NixOS from a flake using [nixos-anywhere](https://nix-community.github.io/nixos-anywhere) 18 18 - [ ] Remote power-on with [Wake-on-LAN](https://en.wikipedia.org/wiki/Wake-on-LAN) 19 19 - [ ] Host status check with IP discovery 20 20 - [x] Stateless and ephemeral [^1] 21 - - [ ] Fast, under 2 minutes to install NixOS from empty hard drives [^2] 21 + - [x] Fast, under 2 minutes to install NixOS from empty hard drives [^2] 22 22 23 23 [^1]: No pre-configured PXE server is required to install other machines - you 24 24 only need to run Nixie on your laptop or workstation. This solves the
+37
cmd/nixie-agent/client.go
··· 1 1 package main 2 2 3 3 import ( 4 + "bytes" 5 + "encoding/json" 4 6 "fmt" 7 + "io" 5 8 "log" 6 9 "math/rand" 7 10 "net/http" 8 11 "time" 12 + 13 + "code.khuedoan.com/nixie/internal/serve" 9 14 ) 10 15 11 16 func ping(address string) error { ··· 39 44 } 40 45 } 41 46 } 47 + 48 + func install(address string, installRequest serve.InstallRequest) error { 49 + client := &http.Client{Timeout: 5 * time.Second} 50 + body, err := json.Marshal(&installRequest) 51 + if err != nil { 52 + return err 53 + } 54 + 55 + resp, err := client.Post( 56 + fmt.Sprintf("http://%s/install", address), 57 + "application/json", 58 + bytes.NewBuffer(body), 59 + ) 60 + 61 + if err != nil { 62 + return err 63 + } 64 + defer resp.Body.Close() 65 + 66 + if resp.StatusCode != http.StatusAccepted { 67 + return fmt.Errorf("install request failed: %s", resp.Status) 68 + } 69 + 70 + respBody, err := io.ReadAll(resp.Body) 71 + if err != nil { 72 + log.Printf("failed to read response body: %v", err) 73 + } else { 74 + log.Printf("successfully requested installation: %s", string(respBody)) 75 + } 76 + 77 + return err 78 + }
+8 -8
cmd/nixie-agent/kernel.go
··· 6 6 "strings" 7 7 ) 8 8 9 - type NixieParams struct { 10 - MAC string 11 - API string 9 + type AgentConfig struct { 10 + MACAddress string 11 + APIAddress string 12 12 } 13 13 14 - func getNixieParams() (*NixieParams, error) { 14 + func getAgentConfig() (*AgentConfig, error) { 15 15 data, err := os.ReadFile("/proc/cmdline") 16 16 if err != nil { 17 17 return nil, err 18 18 } 19 19 20 20 params := parseKernelParams(string(data)) 21 - nixieParams := &NixieParams{ 22 - MAC: params["nixie_mac_address"], 23 - API: params["nixie_api"], 21 + nixieParams := &AgentConfig{ 22 + MACAddress: params["nixie_mac_address"], 23 + APIAddress: params["nixie_api"], 24 24 } 25 - if nixieParams.MAC == "" || nixieParams.API == "" { 25 + if nixieParams.MACAddress == "" || nixieParams.APIAddress == "" { 26 26 return nil, errors.New("missing required kernel parameters: nixie_mac_address or nixie_api") 27 27 } 28 28
+12 -2
cmd/nixie-agent/main.go
··· 2 2 3 3 import ( 4 4 "log" 5 + 6 + "code.khuedoan.com/nixie/internal/serve" 5 7 ) 6 8 7 9 func main() { 8 - params, err := getNixieParams() 10 + params, err := getAgentConfig() 9 11 if err != nil { 10 12 log.Fatalf("failed to get Nixie params: %v", err) 11 13 } 12 14 log.Printf("nixie-agent params: %+v", params) 13 15 14 - if err = ping(params.API); err != nil { 16 + if err = ping(params.APIAddress); err != nil { 15 17 log.Fatalf("failed to ping Nixie API server: %v", err) 18 + } 19 + log.Printf("successfully sent ping to API server") 20 + 21 + installRequest := serve.InstallRequest{ 22 + MACAddress: params.MACAddress, 23 + } 24 + if err = install(params.APIAddress, installRequest); err != nil { 25 + log.Fatalf("failed to request for installation: %v", err) 16 26 } 17 27 }
-5
cmd/nixie/main.go
··· 77 77 } 78 78 }() 79 79 80 - // TODO IMPORTANT obviously, should be API call 81 - // TODO IMPORTANT support SSH key 82 - log.Info("installing NixOS", "host", "192.168.1.8", "flake", "./examples#machine1") 83 - nixos.Install(ctx, "./examples#machine1", "root", "192.168.1.8", "nixos-installer", flags.Debug) 84 - 85 80 sigCh := make(chan os.Signal, 1) 86 81 signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 87 82
+1
internal/nixos/installer.go
··· 38 38 } 39 39 40 40 func BuildInstaller(ctx context.Context, flakeRef string, debug bool) (InstallerComponents, error) { 41 + // TODO there might be some race condition here if we update the flake/installer content while an existing build is running, causing mismatch in init path and the actual one in the installer 41 42 kernelOut, err := nixBuild( 42 43 ctx, 43 44 flakeRef+".config.system.build.kernel",
+40 -1
internal/serve/api.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "encoding/json" 5 6 "io" 6 7 "net" 7 8 "net/http" 8 9 9 10 "code.khuedoan.com/nixie/internal/hosts" 11 + "code.khuedoan.com/nixie/internal/nixos" 10 12 "github.com/charmbracelet/log" 11 13 ) 12 14 13 15 type API struct { 16 + ctx context.Context 14 17 hostsConfig hosts.HostsConfig 18 + debug bool 19 + } 20 + 21 + type InstallRequest struct { 22 + MACAddress string 15 23 } 16 24 17 25 func (api *API) ping(w http.ResponseWriter, r *http.Request) { 18 - log.Info("received ping from agent", "ip", extractClientIP(r)) 26 + ip := extractClientIP(r) 27 + log.Info("received ping from agent", "ip", ip) 19 28 io.WriteString(w, "pong") 20 29 } 21 30 31 + func (api *API) install(w http.ResponseWriter, r *http.Request) { 32 + var installRequest InstallRequest 33 + if err := json.NewDecoder(r.Body).Decode(&installRequest); err != nil { 34 + http.Error(w, err.Error(), http.StatusBadRequest) 35 + return 36 + } 37 + defer r.Body.Close() 38 + 39 + ip := extractClientIP(r) 40 + log.Info("received install request from agent", "ip", ip, "request", installRequest) 41 + // TODO IMPORTANT lookup flake name based on mac address 42 + flake := "./examples#machine1" 43 + 44 + log.Info("installing NixOS", "host", ip, "flake", flake) 45 + go func() { 46 + // TODO IMPORTANT support SSH key 47 + if err := nixos.Install(api.ctx, "./examples#machine1", "root", ip, "nixos-installer", api.debug); err != nil { 48 + log.Error("failed to install NixOS", "ip", ip, "flake", flake, "error", err) 49 + } else { 50 + log.Info("successfully installed NixOS", "ip", ip, "flake", flake) 51 + } 52 + }() 53 + 54 + w.WriteHeader(http.StatusAccepted) 55 + io.WriteString(w, "installation started") 56 + } 57 + 22 58 func (api *API) router() http.Handler { 23 59 mux := http.NewServeMux() 24 60 mux.HandleFunc("GET /ping", api.ping) 61 + mux.HandleFunc("POST /install", api.install) 25 62 26 63 return mux 27 64 } 28 65 29 66 func StartAPIServer(ctx context.Context, hostsConfig hosts.HostsConfig, debug bool) error { 30 67 api := &API{ 68 + ctx: ctx, 31 69 hostsConfig: hostsConfig, 70 + debug: debug, 32 71 } 33 72 34 73 server := &http.Server{