installation.go
1 package system 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "log" 8 "os" 9 "os/exec" 10 "strings" 11 12 "github.com/dell/csi-baremetal/pkg/base/linuxutils/lsblk" 13 dogeboxd "github.com/dogeorg/dogeboxd/pkg" 14 "github.com/dogeorg/dogeboxd/pkg/utils" 15 "github.com/sirupsen/logrus" 16 ) 17 18 const DBXRootSecret = "yes-i-will-destroy-everything-on-this-disk" 19 20 func GetInstallationMode(dbxState dogeboxd.DogeboxState) (dogeboxd.BootstrapInstallationMode, error) { 21 // First, check if we're already installed. 22 if _, err := os.Stat("/opt/dbx-installed"); err == nil { 23 return dogeboxd.BootstrapInstallationModeIsInstalled, nil 24 } else if !os.IsNotExist(err) { 25 return "", fmt.Errorf("error checking for RO installation media: %v", err) 26 } 27 28 // If we're not already installed, but we've been configured, no install for you. 29 if dbxState.InitialState.HasFullyConfigured { 30 return dogeboxd.BootstrapInstallationModeCannotInstall, nil 31 } 32 33 // Check if we're running on RO installation media. If so, must install. 34 if _, err := os.Stat("/opt/ro-media"); err == nil { 35 return dogeboxd.BootstrapInstallationModeMustInstall, nil 36 } else if !os.IsNotExist(err) { 37 return "", fmt.Errorf("error checking for RO installation media: %v", err) 38 } 39 40 // Otherwise, the user can optionally install. 41 return dogeboxd.BootstrapInstallationModeCanInstalled, nil 42 } 43 44 const ( 45 one_gigabyte = 1024 * 1024 * 1024 46 ten_gigabytes = 10 * one_gigabyte 47 three_hundred_gigabytes = 300 * one_gigabyte 48 ) 49 50 func GetSystemDisks() ([]dogeboxd.SystemDisk, error) { 51 lsb := lsblk.NewLSBLK(logrus.New()) 52 53 devices, err := lsb.GetBlockDevices("") 54 if err != nil { 55 return []dogeboxd.SystemDisk{}, err 56 } 57 58 disks := []dogeboxd.SystemDisk{} 59 60 for _, device := range devices { 61 disk := dogeboxd.SystemDisk{ 62 Name: device.Name, 63 Size: device.Size.Int64, 64 SizePretty: utils.PrettyPrintDiskSize(device.Size.Int64), 65 } 66 67 // We will likely never see loop devices in the wild, 68 // but it's useful to support these for development. 69 isOKDevice := device.Type == "disk" || device.Type == "loop" 70 71 isMounted := device.MountPoint != "" 72 hasChildren := len(device.Children) > 0 73 74 isSuitableInstallSize := device.Size.Int64 >= ten_gigabytes 75 isSuitableStorageSize := device.Size.Int64 >= three_hundred_gigabytes 76 77 isSuitableDevice := isOKDevice && device.Size.Int64 > 0 78 isAlreadyUsed := isMounted || hasChildren 79 80 // This block package only seems to return a single mount point. 81 // So we need to check if we're mounted at either / or /nix/store 82 // to "reliably" determine if this is our boot media. 83 if device.MountPoint == "/" || device.MountPoint == "/nix/store" || device.MountPoint == "/nix/.ro-store" { 84 disk.BootMedia = true 85 } 86 87 // Check if any of our children are mounted as boot. 88 for _, child := range device.Children { 89 if child.MountPoint == "/" || child.MountPoint == "/nix/store" || device.MountPoint == "/nix/.ro-store" { 90 disk.BootMedia = true 91 } 92 } 93 94 // Even for devices we don't class as "usable" for storage, if we're 95 // booting off it, we need to let the user select it (ie. no external storage) 96 isUsableStorageDevice := isSuitableDevice || disk.BootMedia 97 98 disk.Suitability = dogeboxd.SystemDiskSuitability{ 99 Install: dogeboxd.SystemDiskSuitabilityEntry{ 100 Usable: isSuitableDevice, 101 SizeOK: isSuitableInstallSize, 102 }, 103 Storage: dogeboxd.SystemDiskSuitabilityEntry{ 104 Usable: isUsableStorageDevice, 105 SizeOK: isSuitableStorageSize, 106 }, 107 IsAlreadyUsed: isAlreadyUsed, 108 } 109 110 disks = append(disks, disk) 111 } 112 113 return disks, nil 114 } 115 116 func InitStorageDevice(dbxState dogeboxd.DogeboxState) (string, error) { 117 if dbxState.StorageDevice == "" || dbxState.InitialState.HasFullyConfigured { 118 return "", nil 119 } 120 121 cmd := exec.Command("sudo", "_dbxroot", "prepare-storage-device", "--print", "--disk", dbxState.StorageDevice, "--dbx-secret", DBXRootSecret) 122 123 var out bytes.Buffer 124 cmd.Stdout = io.MultiWriter(&out, os.Stdout) 125 cmd.Stderr = io.MultiWriter(&out, os.Stderr) 126 127 // Execute the command 128 err := cmd.Run() 129 if err != nil { 130 return "", fmt.Errorf("failed to execute _dbxroot prepare-storage-device: %w", err) 131 } 132 133 output := out.String() 134 135 lines := strings.Split(strings.TrimSpace(output), "\n") 136 partitionName := "" 137 if len(lines) > 0 { 138 lastLine := lines[len(lines)-1] 139 parts := strings.Split(lastLine, " ") 140 if len(parts) > 0 { 141 partitionName = parts[len(parts)-1] 142 } 143 } 144 145 if partitionName == "" { 146 return "", fmt.Errorf("failed to get partition name") 147 } 148 149 return partitionName, nil 150 } 151 152 func GetBuildType() (string, error) { 153 buildType, err := os.ReadFile("/opt/build-type") 154 if err != nil { 155 if os.IsNotExist(err) { 156 return "unknown", nil 157 } 158 return "", fmt.Errorf("failed to read build type: %w", err) 159 } 160 return strings.TrimSpace(string(buildType)), nil 161 } 162 163 func InstallToDisk(config dogeboxd.ServerConfig, dbxState dogeboxd.DogeboxState, name string) error { 164 if config.DevMode { 165 log.Printf("Dev mode enabled, skipping installation. You probably do not want to do this. re-run without dev mode if you do.") 166 return nil 167 } 168 169 if !config.Recovery { 170 return fmt.Errorf("installation can only be done in recovery mode") 171 } 172 173 installMode, err := GetInstallationMode(dbxState) 174 if err != nil { 175 return err 176 } 177 178 if installMode != dogeboxd.BootstrapInstallationModeMustInstall && installMode != dogeboxd.BootstrapInstallationModeCanInstalled { 179 return fmt.Errorf("installation is not possible with current system state: %s", installMode) 180 } 181 182 disks, err := GetSystemDisks() 183 if err != nil { 184 return err 185 } 186 187 // Check if the specified disk name exists in possibleDisks 188 diskExists := false 189 for _, disk := range disks { 190 if disk.Name == name && disk.Suitability.Install.Usable { 191 diskExists = true 192 break 193 } 194 } 195 196 if !diskExists { 197 return fmt.Errorf("specified disk '%s' not found in list of possible install disks", name) 198 } 199 200 buildType, err := GetBuildType() 201 if err != nil { 202 log.Printf("Failed to get build type: %v", err) 203 return err 204 } 205 206 log.Printf("Starting to install to disk %s", name) 207 208 var installFn func(string) error 209 210 installFn = dbxrootInstallToDisk 211 212 // For the T6, we need to write the root FS over the EMMC 213 // with DD, as we need all the arm-specific bootloaders and such. 214 if buildType == "nanopc-T6" { 215 installFn = dbxrootDDToDisk 216 } 217 218 if err := installFn(name); err != nil { 219 log.Printf("Failed to install to disk: %v", err) 220 return err 221 } 222 223 log.Printf("Installation completed successfully") 224 225 return nil 226 } 227 228 func dbxrootInstallToDisk(disk string) error { 229 cmd := exec.Command("sudo", "_dbxroot", "install-to-disk", "--disk", disk, "--dbx-secret", DBXRootSecret) 230 cmd.Stdout = os.Stdout 231 cmd.Stderr = os.Stderr 232 return cmd.Run() 233 } 234 235 func dbxrootDDToDisk(toDisk string) error { 236 cmd := exec.Command("sudo", "_dbxroot", "dd-to-disk", "--target-disk", toDisk, "--dbx-secret", DBXRootSecret) 237 cmd.Stdout = os.Stdout 238 cmd.Stderr = os.Stderr 239 return cmd.Run() 240 }