raidutils.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. package raid
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "strconv"
  10. "strings"
  11. "imuslab.com/bokofs/bokofsd/mod/disktool/diskfs"
  12. )
  13. // Get the next avaible RAID array name
  14. func GetNextAvailableMDDevice() (string, error) {
  15. for i := 0; i < 100; i++ {
  16. mdDevice := fmt.Sprintf("/dev/md%d", i)
  17. if _, err := os.Stat(mdDevice); os.IsNotExist(err) {
  18. return mdDevice, nil
  19. }
  20. }
  21. return "", fmt.Errorf("no available /dev/mdX devices found")
  22. }
  23. // Check if the given device is safe to remove from the array without losing data
  24. func (m *Manager) IsSafeToRemove(mdDev string, sdXDev string) bool {
  25. targetRAIDVol, err := m.GetRAIDDeviceByDevicePath(mdDev)
  26. if err != nil {
  27. return false
  28. }
  29. //Trim off the /dev/ part if exists
  30. sdXDev = filepath.Base(sdXDev)
  31. //Check how many members left if this is removed
  32. remainingMemebers := 0
  33. for _, member := range targetRAIDVol.Members {
  34. if member.Name != sdXDev {
  35. remainingMemebers++
  36. }
  37. }
  38. //Check if removal of sdX will cause data loss
  39. if strings.EqualFold(targetRAIDVol.Level, "raid0") {
  40. return false
  41. } else if strings.EqualFold(targetRAIDVol.Level, "raid1") {
  42. //In raid1, you need at least 1 disk to hold data
  43. return remainingMemebers >= 1
  44. } else if strings.EqualFold(targetRAIDVol.Level, "raid5") {
  45. //In raid 5, at least 2 disk is needed before data loss
  46. return remainingMemebers >= 2
  47. } else if strings.EqualFold(targetRAIDVol.Level, "raid6") {
  48. //In raid 6, you need 6 disks with max loss = 2 disks
  49. return remainingMemebers >= 2
  50. }
  51. return true
  52. }
  53. // Check if the given disk (sdX) is currently used in any volume
  54. func (m *Manager) DiskIsUsedInAnotherRAIDVol(sdXDev string) (bool, error) {
  55. raidPools, err := m.GetRAIDDevicesFromProcMDStat()
  56. if err != nil {
  57. return false, errors.New("unable to access RAID controller state")
  58. }
  59. for _, md := range raidPools {
  60. for _, member := range md.Members {
  61. if member.Name == filepath.Base(sdXDev) {
  62. return true, nil
  63. }
  64. }
  65. }
  66. return false, nil
  67. }
  68. // Check if the given disk (sdX) is root drive (the disk that install the OS, aka /)
  69. func (m *Manager) DiskIsRoot(sdXDev string) (bool, error) {
  70. bdMeta, err := diskfs.GetBlockDeviceMeta(sdXDev)
  71. if err != nil {
  72. return false, err
  73. }
  74. for _, partition := range bdMeta.Children {
  75. if partition.Mountpoint == "/" {
  76. //Root
  77. return true, nil
  78. }
  79. }
  80. return false, nil
  81. }
  82. // ClearSuperblock clears the superblock of the specified disk so it can be used safely
  83. func (m *Manager) ClearSuperblock(devicePath string) error {
  84. isMounted, err := diskfs.DeviceIsMounted(devicePath)
  85. if err != nil {
  86. return errors.New("unable to validate if the device is unmounted: " + err.Error())
  87. }
  88. if isMounted {
  89. return errors.New("target device is mounted. Make sure it is unmounted before clearing")
  90. }
  91. //Make sure there are /dev/ in front of the device path
  92. if !strings.HasPrefix(devicePath, "/dev/") {
  93. devicePath = filepath.Join("/dev/", devicePath)
  94. }
  95. cmd := exec.Command("sudo", "mdadm", "--zero-superblock", devicePath)
  96. err = cmd.Run()
  97. if err != nil {
  98. return fmt.Errorf("error clearing superblock: %v", err)
  99. }
  100. return nil
  101. }
  102. // Use to restart any not-removed RAID device
  103. func (m *Manager) RestartRAIDService() error {
  104. cmd := exec.Command("sudo", "mdadm", "--assemble", "--scan")
  105. // Run the command
  106. output, err := cmd.CombinedOutput()
  107. if err != nil {
  108. if string(output) == "" {
  109. //Nothing updated in config.
  110. return nil
  111. }
  112. return fmt.Errorf("error restarting RAID device: %s", strings.TrimSpace(string(output)))
  113. }
  114. return nil
  115. }
  116. // Stop RAID device with given path
  117. func (m *Manager) StopRAIDDevice(devicePath string) error {
  118. cmd := exec.Command("sudo", "mdadm", "--stop", devicePath)
  119. // Run the command
  120. err := cmd.Run()
  121. if err != nil {
  122. return fmt.Errorf("error stopping RAID device: %v", err)
  123. }
  124. return nil
  125. }
  126. // RemoveRAIDDevice removes the specified RAID device member (disk).
  127. func (m *Manager) RemoveRAIDMember(devicePath string) error {
  128. // Construct the mdadm command to remove the RAID device
  129. cmd := exec.Command("sudo", "mdadm", "--remove", devicePath)
  130. // Run the command
  131. output, err := cmd.CombinedOutput()
  132. if err != nil {
  133. // If there was an error, return the combined output and the error message
  134. return fmt.Errorf("error removing RAID device: %s", strings.TrimSpace(string(output)))
  135. }
  136. return nil
  137. }
  138. // IsValidRAIDLevel checks if the given RAID level is valid.
  139. func IsValidRAIDLevel(level string) bool {
  140. // List of valid RAID levels
  141. validLevels := []string{"raid1", "raid0", "raid6", "raid5", "raid4", "raid10"}
  142. // Convert the RAID level to lowercase and remove any surrounding whitespace
  143. level = strings.TrimSpace(strings.ToLower(level))
  144. // Check if the level exists in the list of valid levels
  145. for _, validLevel := range validLevels {
  146. if level == validLevel {
  147. return true
  148. }
  149. }
  150. // Return false if the level is not found in the list of valid levels
  151. return false
  152. }
  153. // Get RAID device info from device path
  154. func (m *Manager) GetRAIDDeviceByDevicePath(devicePath string) (*RAIDDevice, error) {
  155. //Strip the /dev/ part if it was accidentally passed in
  156. devicePath = filepath.Base(devicePath)
  157. //Get all the raid devices
  158. rdevs, err := m.GetRAIDDevicesFromProcMDStat()
  159. if err != nil {
  160. return nil, err
  161. }
  162. //Check for match
  163. for _, rdev := range rdevs {
  164. if rdev.Name == devicePath {
  165. return &rdev, nil
  166. }
  167. }
  168. return nil, errors.New("target RAID device not found")
  169. }
  170. // Check if a RAID device exists, e.g. md0
  171. func (m *Manager) RAIDDeviceExists(devicePath string) bool {
  172. _, err := m.GetRAIDDeviceByDevicePath(devicePath)
  173. return err == nil
  174. }
  175. // Check if a RAID contain disk that failed or degraded given the devicePath, e.g. md0 or /dev/md0
  176. func (m *Manager) RAIDArrayContainsFailedDisks(devicePath string) (bool, error) {
  177. raidDeviceInfo, err := m.GetRAIDInfo(devicePath)
  178. if err != nil {
  179. return false, err
  180. }
  181. return strings.Contains(raidDeviceInfo.State, "degraded") || strings.Contains(raidDeviceInfo.State, "faulty"), nil
  182. }
  183. // GetRAIDPartitionSize returns the size of the RAID partition in bytes as an int64
  184. func GetRAIDPartitionSize(devicePath string) (int64, error) {
  185. // Ensure devicePath is formatted correctly
  186. if !strings.HasPrefix(devicePath, "/dev/") {
  187. devicePath = "/dev/" + devicePath
  188. }
  189. // Execute the df command with the device path
  190. cmd := exec.Command("df", "--block-size=1", devicePath)
  191. var out bytes.Buffer
  192. cmd.Stdout = &out
  193. if err := cmd.Run(); err != nil {
  194. return 0, fmt.Errorf("failed to execute df command: %v", err)
  195. }
  196. // Parse the output to find the size
  197. lines := strings.Split(out.String(), "\n")
  198. if len(lines) < 2 {
  199. return 0, fmt.Errorf("unexpected df output: %s", out.String())
  200. }
  201. // The second line should contain the relevant information
  202. fields := strings.Fields(lines[1])
  203. if len(fields) < 2 {
  204. return 0, fmt.Errorf("unexpected df output: %s", lines[1])
  205. }
  206. // The second field should be the size in bytes
  207. size, err := strconv.ParseInt(fields[1], 10, 64)
  208. if err != nil {
  209. return 0, fmt.Errorf("failed to parse size: %v", err)
  210. }
  211. return size, nil
  212. }
  213. // GetRAIDUsedSize returns the used size of the RAID array in bytes as an int64
  214. func GetRAIDUsedSize(devicePath string) (int64, error) {
  215. // Ensure devicePath is formatted correctly
  216. if !strings.HasPrefix(devicePath, "/dev/") {
  217. devicePath = "/dev/" + devicePath
  218. }
  219. // Execute the df command with the device path
  220. cmd := exec.Command("df", "--block-size=1", devicePath)
  221. var out bytes.Buffer
  222. cmd.Stdout = &out
  223. if err := cmd.Run(); err != nil {
  224. return 0, fmt.Errorf("failed to execute df command: %v", err)
  225. }
  226. // Parse the output to find the used size
  227. lines := strings.Split(out.String(), "\n")
  228. if len(lines) < 2 {
  229. return 0, fmt.Errorf("unexpected df output: %s", out.String())
  230. }
  231. // The second line should contain the relevant information
  232. fields := strings.Fields(lines[1])
  233. if len(fields) < 3 {
  234. return 0, fmt.Errorf("unexpected df output: %s", lines[1])
  235. }
  236. // The third field should be the used size in bytes
  237. usedSize, err := strconv.ParseInt(fields[2], 10, 64)
  238. if err != nil {
  239. return 0, fmt.Errorf("failed to parse used size: %v", err)
  240. }
  241. return usedSize, nil
  242. }