/ pkg / web / setup.go
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  }