mdadm.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. package raid
  2. import (
  3. "errors"
  4. "fmt"
  5. "log"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "sort"
  10. "strconv"
  11. "strings"
  12. "imuslab.com/bokofs/bokofsd/mod/utils"
  13. )
  14. /*
  15. mdadm manager
  16. This script handles the interaction with mdadm
  17. */
  18. // RAIDDevice represents information about a RAID device.
  19. type RAIDMember struct {
  20. Name string //sdX
  21. Seq int //Sequence in RAID arary
  22. Failed bool //If details output with (F) tag this will set to true
  23. }
  24. type RAIDDevice struct {
  25. Name string
  26. Status string
  27. Level string
  28. Members []*RAIDMember
  29. }
  30. // Return the uuid of the disk by its path name (e.g. /dev/sda)
  31. func (m *Manager) GetDiskUUIDByPath(devicePath string) (string, error) {
  32. cmd := exec.Command("sudo", "blkid", devicePath)
  33. output, err := cmd.CombinedOutput()
  34. if err != nil {
  35. return "", fmt.Errorf("blkid error: %v", err)
  36. }
  37. // Parse the output to extract the UUID
  38. fields := strings.Fields(string(output))
  39. for _, field := range fields {
  40. if strings.HasPrefix(field, "UUID=") {
  41. uuid := strings.TrimPrefix(field, "UUID=\"")
  42. uuid = strings.TrimSuffix(uuid, "\"")
  43. return uuid, nil
  44. }
  45. }
  46. return "", fmt.Errorf("UUID not found for device %s", devicePath)
  47. }
  48. // CreateRAIDDevice creates a RAID device using the mdadm command.
  49. func (m *Manager) CreateRAIDDevice(devName string, raidName string, raidLevel int, raidDeviceIds []string, spareDeviceIds []string) error {
  50. //Calculate the size of the raid devices
  51. raidDev := len(raidDeviceIds)
  52. spareDevice := len(spareDeviceIds)
  53. //Validate if raid level
  54. if !IsValidRAIDLevel("raid" + strconv.Itoa(raidLevel)) {
  55. return fmt.Errorf("invalid or unsupported raid level given: raid" + strconv.Itoa(raidLevel))
  56. }
  57. //Validate the number of disk is enough for the raid
  58. if raidLevel == 0 && raidDev < 2 {
  59. return fmt.Errorf("not enough disks for raid0")
  60. } else if raidLevel == 1 && raidDev < 2 {
  61. return fmt.Errorf("not enough disks for raid1")
  62. } else if raidLevel == 5 && raidDev < 3 {
  63. return fmt.Errorf("not enough disk for raid5")
  64. } else if raidLevel == 6 && raidDev < 4 {
  65. return fmt.Errorf("not enough disk for raid6")
  66. }
  67. //Append /dev to the name if missing
  68. if !strings.HasPrefix(devName, "/dev/") {
  69. devName = "/dev/" + devName
  70. }
  71. if utils.FileExists(devName) {
  72. //RAID device already exists
  73. return errors.New(devName + " already been used")
  74. }
  75. //Append /dev to the name of the raid device ids and spare device ids if missing
  76. for i, raidDev := range raidDeviceIds {
  77. if !strings.HasPrefix(raidDev, "/dev/") {
  78. raidDeviceIds[i] = filepath.Join("/dev/", raidDev)
  79. }
  80. }
  81. for i, spareDev := range spareDeviceIds {
  82. if !strings.HasPrefix(spareDev, "/dev/") {
  83. spareDeviceIds[i] = filepath.Join("/dev/", spareDev)
  84. }
  85. }
  86. // Concatenate RAID and spare device arrays
  87. allDeviceIds := append(raidDeviceIds, spareDeviceIds...)
  88. // Build the mdadm command
  89. mdadmCommand := fmt.Sprintf("yes | sudo mdadm --create %s --name %s --level=%d --raid-devices=%d --spare-devices=%d %s", devName, raidName, raidLevel, raidDev, spareDevice, strings.Join(allDeviceIds, " "))
  90. if raidLevel == 0 {
  91. //raid0 cannot use --spare-device command as there is no failover
  92. mdadmCommand = fmt.Sprintf("yes | sudo mdadm --create %s --name %s --level=%d --raid-devices=%d %s", devName, raidName, raidLevel, raidDev, strings.Join(allDeviceIds, " "))
  93. }
  94. cmd := exec.Command("bash", "-c", mdadmCommand)
  95. cmd.Stdout = os.Stdout
  96. cmd.Stderr = os.Stderr
  97. err := cmd.Run()
  98. if err != nil {
  99. return fmt.Errorf("error running mdadm command: %v", err)
  100. }
  101. return nil
  102. }
  103. // GetRAIDDevicesFromProcMDStat retrieves information about RAID devices from /proc/mdstat.
  104. // if your RAID array is in auto-read-only mode, it is (usually) brand new
  105. func (m *Manager) GetRAIDDevicesFromProcMDStat() ([]RAIDDevice, error) {
  106. // Execute the cat command to read /proc/mdstat
  107. cmd := exec.Command("cat", "/proc/mdstat")
  108. // Run the command and capture its output
  109. output, err := cmd.Output()
  110. if err != nil {
  111. return nil, fmt.Errorf("error running cat command: %v", err)
  112. }
  113. // Convert the output to a string and split it into lines
  114. lines := strings.Split(string(output), "\n")
  115. // Initialize an empty slice to store RAID devices
  116. raidDevices := make([]RAIDDevice, 0)
  117. // Iterate over the lines, skipping the first line (Personalities)
  118. // Lines usually looks like this
  119. // md0 : active raid1 sdc[1] sdb[0]
  120. for _, line := range lines[1:] {
  121. // Skip empty lines
  122. if line == "" {
  123. continue
  124. }
  125. // Split the line by colon (:)
  126. parts := strings.SplitN(line, " : ", 2)
  127. if len(parts) != 2 {
  128. continue
  129. }
  130. // Extract device name and status
  131. deviceName := parts[0]
  132. // Split the members string by space to get individual member devices
  133. info := strings.Fields(parts[1])
  134. if len(info) < 2 {
  135. //Malform output
  136. continue
  137. }
  138. deviceStatus := info[0]
  139. //Raid level usually appears at position 1 - 2, check both
  140. raidLevel := ""
  141. if strings.HasPrefix(info[1], "raid") {
  142. raidLevel = info[1]
  143. } else if strings.HasPrefix(info[2], "raid") {
  144. raidLevel = info[2]
  145. }
  146. //Get the members (disks) of the array
  147. members := []*RAIDMember{}
  148. for _, disk := range info[2:] {
  149. if !strings.HasPrefix(disk, "sd") {
  150. //Probably not a storage device
  151. continue
  152. }
  153. //In sda[0] format, we need to split out the number from the disk seq
  154. tmp := strings.Split(disk, "[")
  155. if len(tmp) != 2 {
  156. continue
  157. }
  158. //Convert the sequence to id
  159. diskFailed := false
  160. if strings.HasSuffix(strings.TrimSpace(tmp[1]), "(F)") {
  161. //Trim off the Fail label
  162. diskFailed = true
  163. tmp[1] = strings.TrimSuffix(strings.TrimSpace(tmp[1]), "(F)")
  164. }
  165. seqInt, err := strconv.Atoi(strings.TrimSuffix(strings.TrimSpace(tmp[1]), "]"))
  166. if err != nil {
  167. //Not an integer?
  168. log.Println("[RAID] Unable to parse " + disk + " sequence ID")
  169. continue
  170. }
  171. member := RAIDMember{
  172. Name: strings.TrimSpace(tmp[0]),
  173. Seq: seqInt,
  174. Failed: diskFailed,
  175. }
  176. members = append(members, &member)
  177. }
  178. //Sort the member disks
  179. sort.Slice(members[:], func(i, j int) bool {
  180. return members[i].Seq < members[j].Seq
  181. })
  182. // Create a RAIDDevice struct and append it to the slice
  183. raidDevice := RAIDDevice{
  184. Name: deviceName,
  185. Status: deviceStatus,
  186. Level: raidLevel,
  187. Members: members,
  188. }
  189. raidDevices = append(raidDevices, raidDevice)
  190. }
  191. return raidDevices, nil
  192. }
  193. // Check if a disk is failed in given array
  194. func (m *Manager) DiskIsFailed(mdDevice, diskPath string) (bool, error) {
  195. raidDevices, err := m.GetRAIDDeviceByDevicePath(mdDevice)
  196. if err != nil {
  197. return false, err
  198. }
  199. diskName := filepath.Base(diskPath)
  200. for _, disk := range raidDevices.Members {
  201. if disk.Name == diskName {
  202. return disk.Failed, nil
  203. }
  204. }
  205. return false, errors.New("target disk not found in this array")
  206. }
  207. // FailDisk label a disk as failed
  208. func (m *Manager) FailDisk(mdDevice, diskPath string) error {
  209. //mdadm commands require full path
  210. if !strings.HasPrefix(diskPath, "/dev/") {
  211. diskPath = filepath.Join("/dev/", diskPath)
  212. }
  213. if !strings.HasPrefix(mdDevice, "/dev/") {
  214. mdDevice = filepath.Join("/dev/", mdDevice)
  215. }
  216. cmd := exec.Command("sudo", "mdadm", mdDevice, "--fail", diskPath)
  217. if err := cmd.Run(); err != nil {
  218. return fmt.Errorf("failed to fail disk: %v", err)
  219. }
  220. return nil
  221. }
  222. // RemoveDisk removes a failed disk from the specified RAID array using mdadm.
  223. // must be failed before remove
  224. func (m *Manager) RemoveDisk(mdDevice, diskPath string) error {
  225. //mdadm commands require full path
  226. if !strings.HasPrefix(diskPath, "/dev/") {
  227. diskPath = filepath.Join("/dev/", diskPath)
  228. }
  229. if !strings.HasPrefix(mdDevice, "/dev/") {
  230. mdDevice = filepath.Join("/dev/", mdDevice)
  231. }
  232. cmd := exec.Command("sudo", "mdadm", mdDevice, "--remove", diskPath)
  233. if err := cmd.Run(); err != nil {
  234. return fmt.Errorf("failed to remove disk: %v", err)
  235. }
  236. return nil
  237. }
  238. // Add disk to a given RAID array, must be unmounted and not in-use
  239. func (m *Manager) AddDisk(mdDevice, diskPath string) error {
  240. //mdadm commands require full path
  241. if !strings.HasPrefix(diskPath, "/dev/") {
  242. diskPath = filepath.Join("/dev/", diskPath)
  243. }
  244. if !strings.HasPrefix(mdDevice, "/dev/") {
  245. mdDevice = filepath.Join("/dev/", mdDevice)
  246. }
  247. cmd := exec.Command("sudo", "mdadm", mdDevice, "--add", diskPath)
  248. if err := cmd.Run(); err != nil {
  249. return fmt.Errorf("failed to add disk: %v", err)
  250. }
  251. return nil
  252. }
  253. // GrowRAIDDevice grows the specified RAID device to its maximum size
  254. func (m *Manager) GrowRAIDDevice(deviceName string) error {
  255. //Prevent anyone passing /dev/md0 into the deviceName field
  256. deviceName = strings.TrimPrefix(deviceName, "/dev/")
  257. // Construct the mdadm command
  258. cmd := exec.Command("sudo", "mdadm", "--grow", fmt.Sprintf("/dev/%s", deviceName), "--size=max")
  259. // Execute the command
  260. output, err := cmd.CombinedOutput()
  261. if err != nil {
  262. return fmt.Errorf("failed to grow RAID device: %v, output: %s", err, string(output))
  263. }
  264. fmt.Printf("[RAID] Successfully grew RAID device %s. Output: %s\n", deviceName, string(output))
  265. return nil
  266. }