setup.go
1 package web 2 3 import ( 4 "encoding/json" 5 "io" 6 "log" 7 "net/http" 8 "os" 9 "time" 10 11 dogeboxd "github.com/dogeorg/dogeboxd/pkg" 12 "github.com/dogeorg/dogeboxd/pkg/system" 13 "github.com/dogeorg/dogeboxd/pkg/utils" 14 "github.com/dogeorg/dogeboxd/pkg/version" 15 ) 16 17 type InitialSystemBootstrapRequestBody struct { 18 ReflectorToken string `json:"reflectorToken"` 19 ReflectorHost string `json:"reflectorHost"` 20 InitialSSHKey string `json:"initialSSHKey"` 21 } 22 23 type BootstrapFacts struct { 24 InstallationMode dogeboxd.BootstrapInstallationMode `json:"installationMode"` 25 HasGeneratedKey bool `json:"hasGeneratedKey"` 26 HasConfiguredNetwork bool `json:"hasConfiguredNetwork"` 27 HasCompletedInitialConfiguration bool `json:"hasCompletedInitialConfiguration"` 28 } 29 30 type BootstrapResponse struct { 31 Version *version.DBXVersionInfo `json:"version"` 32 DevMode bool `json:"devMode"` 33 Assets map[string]dogeboxd.PupAsset `json:"assets"` 34 States map[string]dogeboxd.PupState `json:"states"` 35 Stats map[string]dogeboxd.PupStats `json:"stats"` 36 SetupFacts BootstrapFacts `json:"setupFacts"` 37 } 38 39 func (t api) getRawBS() BootstrapResponse { 40 dbxState := t.sm.Get().Dogebox 41 42 installationMode, err := system.GetInstallationMode(dbxState) 43 if err != nil { 44 log.Printf("Could not determine installation mode: %v", err) 45 installationMode = dogeboxd.BootstrapInstallationModeCannotInstall 46 } 47 48 return BootstrapResponse{ 49 Version: version.GetDBXRelease(), 50 DevMode: t.config.DevMode, 51 Assets: t.pups.GetAssetsMap(), 52 States: t.pups.GetStateMap(), 53 Stats: t.pups.GetStatsMap(), 54 SetupFacts: BootstrapFacts{ 55 InstallationMode: installationMode, 56 HasGeneratedKey: dbxState.InitialState.HasGeneratedKey, 57 HasConfiguredNetwork: dbxState.InitialState.HasSetNetwork, 58 HasCompletedInitialConfiguration: dbxState.InitialState.HasFullyConfigured, 59 }, 60 } 61 } 62 63 func (t api) getBootstrap(w http.ResponseWriter, r *http.Request) { 64 sendResponse(w, t.getRawBS()) 65 } 66 67 func (t api) hostReboot(w http.ResponseWriter, r *http.Request) { 68 t.lifecycle.Reboot() 69 } 70 71 func (t api) hostShutdown(w http.ResponseWriter, r *http.Request) { 72 t.lifecycle.Shutdown() 73 } 74 75 func (t api) getKeymaps(w http.ResponseWriter, r *http.Request) { 76 keymaps, err := system.GetKeymaps() 77 if err != nil { 78 sendErrorResponse(w, http.StatusInternalServerError, "Error getting keymaps") 79 return 80 } 81 82 // Convert keymaps to the desired format 83 formattedKeymaps := make([]map[string]string, len(keymaps)) 84 for i, keymap := range keymaps { 85 formattedKeymaps[i] = map[string]string{ 86 "id": keymap.Name, 87 "label": keymap.Value, 88 } 89 } 90 91 sendResponse(w, formattedKeymaps) 92 } 93 94 type SetHostnameRequestBody struct { 95 Hostname string `json:"hostname"` 96 } 97 98 func (t api) setHostname(w http.ResponseWriter, r *http.Request) { 99 dbxState := t.sm.Get().Dogebox 100 101 body, err := io.ReadAll(r.Body) 102 if err != nil { 103 sendErrorResponse(w, http.StatusBadRequest, "Error reading request body") 104 return 105 } 106 defer r.Body.Close() 107 108 var requestBody SetHostnameRequestBody 109 if err := json.Unmarshal(body, &requestBody); err != nil { 110 http.Error(w, "Error parsing payload", http.StatusBadRequest) 111 return 112 } 113 114 dbxState = t.sm.Get().Dogebox 115 dbxState.Hostname = requestBody.Hostname 116 117 // TODO: If we've already configured our box, rebuild here? 118 119 if err := t.sm.SetDogebox(dbxState); err != nil { 120 sendErrorResponse(w, http.StatusInternalServerError, "Error saving state") 121 return 122 } 123 124 sendResponse(w, map[string]any{"status": "OK"}) 125 } 126 127 type SetKeyMapRequestBody struct { 128 KeyMap string `json:"keyMap"` 129 } 130 131 func (t api) setKeyMap(w http.ResponseWriter, r *http.Request) { 132 dbxState := t.sm.Get().Dogebox 133 134 body, err := io.ReadAll(r.Body) 135 if err != nil { 136 sendErrorResponse(w, http.StatusBadRequest, "Error reading request body") 137 return 138 } 139 defer r.Body.Close() 140 141 var requestBody SetKeyMapRequestBody 142 if err := json.Unmarshal(body, &requestBody); err != nil { 143 http.Error(w, "Error parsing payload", http.StatusBadRequest) 144 return 145 } 146 147 // Fetch available keymaps 148 keymaps, err := system.GetKeymaps() 149 if err != nil { 150 sendErrorResponse(w, http.StatusInternalServerError, "Error fetching keymaps") 151 return 152 } 153 154 // Check if the submitted keymap is valid 155 isValidKeymap := false 156 for _, keymap := range keymaps { 157 if keymap.Name == requestBody.KeyMap { 158 isValidKeymap = true 159 break 160 } 161 } 162 163 if !isValidKeymap { 164 sendErrorResponse(w, http.StatusBadRequest, "Invalid keymap") 165 return 166 } 167 168 dbxState = t.sm.Get().Dogebox 169 dbxState.KeyMap = requestBody.KeyMap 170 171 // TODO: If we've already configured our box, rebuild here? 172 173 if err := t.sm.SetDogebox(dbxState); err != nil { 174 sendErrorResponse(w, http.StatusInternalServerError, "Error saving state") 175 return 176 } 177 178 sendResponse(w, map[string]any{"status": "OK"}) 179 } 180 181 type SetStorageDeviceRequestBody struct { 182 StorageDevice string `json:"storageDevice"` 183 } 184 185 func (t api) setStorageDevice(w http.ResponseWriter, r *http.Request) { 186 dbxState := t.sm.Get().Dogebox 187 188 if dbxState.InitialState.HasFullyConfigured { 189 sendErrorResponse(w, http.StatusForbidden, "Cannot set storage device once initial setup has completed") 190 return 191 } 192 193 body, err := io.ReadAll(r.Body) 194 if err != nil { 195 sendErrorResponse(w, http.StatusBadRequest, "Error reading request body") 196 return 197 } 198 defer r.Body.Close() 199 200 var requestBody SetStorageDeviceRequestBody 201 if err := json.Unmarshal(body, &requestBody); err != nil { 202 http.Error(w, "Error parsing payload", http.StatusBadRequest) 203 return 204 } 205 206 disks, err := system.GetSystemDisks() 207 if err != nil { 208 sendErrorResponse(w, http.StatusInternalServerError, "Error getting system disks") 209 return 210 } 211 212 var foundDisk *dogeboxd.SystemDisk 213 214 // Ensure that the provided storage device can actually be used. 215 for _, disk := range disks { 216 if disk.Name == requestBody.StorageDevice && disk.Suitability.Storage.Usable { 217 foundDisk = &disk 218 break 219 } 220 } 221 222 // If the disk selected is actually our boot drive, allow it, and don't set StorageDevice. 223 if foundDisk != nil && foundDisk.BootMedia { 224 sendResponse(w, map[string]any{"status": "OK"}) 225 return 226 } 227 228 if foundDisk == nil { 229 sendErrorResponse(w, http.StatusBadRequest, "Invalid storage device") 230 return 231 } 232 233 dbxState = t.sm.Get().Dogebox 234 dbxState.StorageDevice = requestBody.StorageDevice 235 236 if err := t.sm.SetDogebox(dbxState); err != nil { 237 sendErrorResponse(w, http.StatusInternalServerError, "Error saving state") 238 return 239 } 240 241 sendResponse(w, map[string]any{"status": "OK"}) 242 } 243 244 func (t api) initialBootstrap(w http.ResponseWriter, r *http.Request) { 245 // Check a few things first. 246 if !t.config.Recovery { 247 sendErrorResponse(w, http.StatusForbidden, "Cannot initiate bootstrap in non-recovery mode.") 248 return 249 } 250 log := dogeboxd.NewConsoleSubLogger("internal", "initial setup") 251 dbxState := t.sm.Get().Dogebox 252 253 if dbxState.InitialState.HasFullyConfigured { 254 sendErrorResponse(w, http.StatusForbidden, "System has already been initialised") 255 return 256 } 257 258 if !dbxState.InitialState.HasGeneratedKey || !dbxState.InitialState.HasSetNetwork { 259 sendErrorResponse(w, http.StatusForbidden, "System not ready to initialise") 260 return 261 } 262 263 body, err := io.ReadAll(r.Body) 264 if err != nil { 265 sendErrorResponse(w, http.StatusBadRequest, "Error reading request body") 266 return 267 } 268 defer r.Body.Close() 269 270 var requestBody InitialSystemBootstrapRequestBody 271 if err := json.Unmarshal(body, &requestBody); err != nil { 272 http.Error(w, "Error parsing payload", http.StatusBadRequest) 273 return 274 } 275 276 if err := t.sm.SetDogebox(dbxState); err != nil { 277 sendErrorResponse(w, http.StatusInternalServerError, "Error saving state") 278 return 279 } 280 281 nixPatch := t.nix.NewPatch(log) 282 283 // This will try and connect to the pending network, and if 284 // that works, it will persist the network config to disk properly. 285 if err := t.dbx.NetworkManager.TryConnect(nixPatch); err != nil { 286 log.Errf("Error connecting to network: %v", err) 287 sendErrorResponse(w, http.StatusInternalServerError, "Error connecting to network") 288 return 289 } 290 291 t.nix.InitSystem(nixPatch, dbxState) 292 293 if err := nixPatch.Apply(); err != nil { 294 sendErrorResponse(w, http.StatusInternalServerError, "Error initialising system") 295 return 296 } 297 298 // This storage overlay stuff needs to happen _after_ we've init'd our system, as 299 // otherwise we end up in a position where we can't access the $datadir/nix/* files 300 // to copy back into our new overlay.. because the overlay is mounted as part of the 301 // system init. So we init, copy files, apply overlay, copy files back. 302 if dbxState.StorageDevice != "" { 303 // Before we do anything, close the DB so we don't have any 304 // issues with the overlay mount (ie. stuff not written yet) 305 if err := t.sm.CloseDB(); err != nil { 306 log.Errf("Error closing DB: %v", err) 307 sendErrorResponse(w, http.StatusInternalServerError, "Error closing DB") 308 return 309 } 310 311 tempDir, err := os.MkdirTemp("", "dbx-data-overlay") 312 if err != nil { 313 log.Errf("Error creating temporary directory: %v", err) 314 sendErrorResponse(w, http.StatusInternalServerError, "Error creating temporary directory") 315 return 316 } 317 log.Logf("Created temporary directory: %s", tempDir) 318 // defer os.RemoveAll(tempDir) 319 320 log.Logf("Initialising storage device: %s", dbxState.StorageDevice) 321 322 partitionName, err := system.InitStorageDevice(dbxState) 323 if err != nil { 324 log.Errf("Error initialising storage device: %v", err) 325 sendErrorResponse(w, http.StatusInternalServerError, "Error initialising storage device") 326 return 327 } 328 329 // Copy all our existing data to our temp dir so we don't lose everything created already. 330 if err := utils.CopyFiles(t.config.DataDir, tempDir); err != nil { 331 log.Errf("Error copying data to temp dir: %v", err) 332 sendErrorResponse(w, http.StatusInternalServerError, "Error copying data to temp dir") 333 return 334 } 335 336 // Apply our new overlay update. 337 overlayPatch := t.nix.NewPatch(log) 338 t.nix.UpdateStorageOverlay(overlayPatch, partitionName) 339 340 if err := overlayPatch.Apply(); err != nil { 341 log.Errf("Error applying overlay patch: %v", err) 342 sendErrorResponse(w, http.StatusInternalServerError, "Error applying overlay patch") 343 return 344 } 345 346 // Copy our data back from the temp dir to the new location. 347 if err := utils.CopyFiles(tempDir, t.config.DataDir); err != nil { 348 log.Errf("Error copying data back to %s: %v", t.config.DataDir, err) 349 sendErrorResponse(w, http.StatusInternalServerError, "Error copying data back to data dir") 350 return 351 } 352 353 // This sucks, but because we wrote our storage-overlay file during the last rebuild, 354 // we don't actually have that in the tempDir we backed up. So we have to re-save this 355 // file into the overlay we now have mounted, but we don't actually have to rebuild. 356 reoverlayPatch := t.nix.NewPatch(log) 357 t.nix.UpdateStorageOverlay(reoverlayPatch, partitionName) 358 if err := reoverlayPatch.ApplyCustom(dogeboxd.NixPatchApplyOptions{ 359 DangerousNoRebuild: true, 360 }); err != nil { 361 log.Errf("Error re-applying overlay patch: %v", err) 362 sendErrorResponse(w, http.StatusInternalServerError, "Error re-applying overlay patch") 363 return 364 } 365 366 if err := t.sm.OpenDB(); err != nil { 367 log.Errf("Error re-opening store manager: %v", err) 368 sendErrorResponse(w, http.StatusInternalServerError, "Error re-opening store manager") 369 return 370 } 371 } 372 373 if requestBody.ReflectorToken != "" && requestBody.ReflectorHost != "" { 374 if err := system.SaveReflectorTokenForReboot(t.config, requestBody.ReflectorHost, requestBody.ReflectorToken); err != nil { 375 log.Errf("Error saving reflector data: %v", err) 376 } 377 } 378 379 // Add our DogeOrg source in by default, for people to test things with. 380 if _, err := t.sources.AddSource("https://github.com/dogeorg/pups.git"); err != nil { 381 log.Errf("Error adding initial dogeorg source: %v", err) 382 sendErrorResponse(w, http.StatusInternalServerError, "Error adding dogeorg source") 383 return 384 } 385 386 // If the user has provided an SSH key, we should add it to the system and enable SSH. 387 if requestBody.InitialSSHKey != "" { 388 if err := t.dbx.SystemUpdater.AddSSHKey(requestBody.InitialSSHKey, log); err != nil { 389 log.Errf("Error adding initial SSH key: %v", err) 390 sendErrorResponse(w, http.StatusInternalServerError, "Error adding initial SSH key") 391 return 392 } 393 394 if err := t.dbx.SystemUpdater.EnableSSH(log); err != nil { 395 log.Errf("Error enabling SSH: %v", err) 396 sendErrorResponse(w, http.StatusInternalServerError, "Error enabling SSH") 397 return 398 } 399 } 400 401 dbxs := t.sm.Get().Dogebox 402 dbxs.InitialState.HasFullyConfigured = true 403 if err := t.sm.SetDogebox(dbxs); err != nil { 404 // What should we do here? We've already turned off AP mode so any errors 405 // won't get send back to the client. I guess we just reboot? 406 // That'll force recovery mode again. We can't even persist this error though. 407 sendErrorResponse(w, http.StatusInternalServerError, "Error persisting flags") 408 } 409 410 sendResponse(w, map[string]any{"status": "OK"}) 411 412 log.Log("Dogebox successfully bootstrapped, rebooting in 5 seconds so we can boot into normal mode.") 413 414 go func() { 415 time.Sleep(5 * time.Second) 416 t.lifecycle.Reboot() 417 }() 418 }