123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- package raid
- import (
- "errors"
- "fmt"
- "log"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "imuslab.com/bokofs/bokofsd/mod/utils"
- )
- /*
- mdadm manager
- This script handles the interaction with mdadm
- */
- // RAIDDevice represents information about a RAID device.
- type RAIDMember struct {
- Name string //sdX
- Seq int //Sequence in RAID arary
- Failed bool //If details output with (F) tag this will set to true
- }
- type RAIDDevice struct {
- Name string
- Status string
- Level string
- Members []*RAIDMember
- }
- // Return the uuid of the disk by its path name (e.g. /dev/sda)
- func (m *Manager) GetDiskUUIDByPath(devicePath string) (string, error) {
- cmd := exec.Command("sudo", "blkid", devicePath)
- output, err := cmd.CombinedOutput()
- if err != nil {
- return "", fmt.Errorf("blkid error: %v", err)
- }
- // Parse the output to extract the UUID
- fields := strings.Fields(string(output))
- for _, field := range fields {
- if strings.HasPrefix(field, "UUID=") {
- uuid := strings.TrimPrefix(field, "UUID=\"")
- uuid = strings.TrimSuffix(uuid, "\"")
- return uuid, nil
- }
- }
- return "", fmt.Errorf("UUID not found for device %s", devicePath)
- }
- // CreateRAIDDevice creates a RAID device using the mdadm command.
- func (m *Manager) CreateRAIDDevice(devName string, raidName string, raidLevel int, raidDeviceIds []string, spareDeviceIds []string) error {
- //Calculate the size of the raid devices
- raidDev := len(raidDeviceIds)
- spareDevice := len(spareDeviceIds)
- //Validate if raid level
- if !IsValidRAIDLevel("raid" + strconv.Itoa(raidLevel)) {
- return fmt.Errorf("invalid or unsupported raid level given: raid" + strconv.Itoa(raidLevel))
- }
- //Validate the number of disk is enough for the raid
- if raidLevel == 0 && raidDev < 2 {
- return fmt.Errorf("not enough disks for raid0")
- } else if raidLevel == 1 && raidDev < 2 {
- return fmt.Errorf("not enough disks for raid1")
- } else if raidLevel == 5 && raidDev < 3 {
- return fmt.Errorf("not enough disk for raid5")
- } else if raidLevel == 6 && raidDev < 4 {
- return fmt.Errorf("not enough disk for raid6")
- }
- //Append /dev to the name if missing
- if !strings.HasPrefix(devName, "/dev/") {
- devName = "/dev/" + devName
- }
- if utils.FileExists(devName) {
- //RAID device already exists
- return errors.New(devName + " already been used")
- }
- //Append /dev to the name of the raid device ids and spare device ids if missing
- for i, raidDev := range raidDeviceIds {
- if !strings.HasPrefix(raidDev, "/dev/") {
- raidDeviceIds[i] = filepath.Join("/dev/", raidDev)
- }
- }
- for i, spareDev := range spareDeviceIds {
- if !strings.HasPrefix(spareDev, "/dev/") {
- spareDeviceIds[i] = filepath.Join("/dev/", spareDev)
- }
- }
- // Concatenate RAID and spare device arrays
- allDeviceIds := append(raidDeviceIds, spareDeviceIds...)
- // Build the mdadm command
- 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, " "))
- if raidLevel == 0 {
- //raid0 cannot use --spare-device command as there is no failover
- mdadmCommand = fmt.Sprintf("yes | sudo mdadm --create %s --name %s --level=%d --raid-devices=%d %s", devName, raidName, raidLevel, raidDev, strings.Join(allDeviceIds, " "))
- }
- cmd := exec.Command("bash", "-c", mdadmCommand)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err := cmd.Run()
- if err != nil {
- return fmt.Errorf("error running mdadm command: %v", err)
- }
- return nil
- }
- // GetRAIDDevicesFromProcMDStat retrieves information about RAID devices from /proc/mdstat.
- // if your RAID array is in auto-read-only mode, it is (usually) brand new
- func (m *Manager) GetRAIDDevicesFromProcMDStat() ([]RAIDDevice, error) {
- // Execute the cat command to read /proc/mdstat
- cmd := exec.Command("cat", "/proc/mdstat")
- // Run the command and capture its output
- output, err := cmd.Output()
- if err != nil {
- return nil, fmt.Errorf("error running cat command: %v", err)
- }
- // Convert the output to a string and split it into lines
- lines := strings.Split(string(output), "\n")
- // Initialize an empty slice to store RAID devices
- raidDevices := make([]RAIDDevice, 0)
- // Iterate over the lines, skipping the first line (Personalities)
- // Lines usually looks like this
- // md0 : active raid1 sdc[1] sdb[0]
- for _, line := range lines[1:] {
- // Skip empty lines
- if line == "" {
- continue
- }
- // Split the line by colon (:)
- parts := strings.SplitN(line, " : ", 2)
- if len(parts) != 2 {
- continue
- }
- // Extract device name and status
- deviceName := parts[0]
- // Split the members string by space to get individual member devices
- info := strings.Fields(parts[1])
- if len(info) < 2 {
- //Malform output
- continue
- }
- deviceStatus := info[0]
- //Raid level usually appears at position 1 - 2, check both
- raidLevel := ""
- if strings.HasPrefix(info[1], "raid") {
- raidLevel = info[1]
- } else if strings.HasPrefix(info[2], "raid") {
- raidLevel = info[2]
- }
- //Get the members (disks) of the array
- members := []*RAIDMember{}
- for _, disk := range info[2:] {
- if !strings.HasPrefix(disk, "sd") {
- //Probably not a storage device
- continue
- }
- //In sda[0] format, we need to split out the number from the disk seq
- tmp := strings.Split(disk, "[")
- if len(tmp) != 2 {
- continue
- }
- //Convert the sequence to id
- diskFailed := false
- if strings.HasSuffix(strings.TrimSpace(tmp[1]), "(F)") {
- //Trim off the Fail label
- diskFailed = true
- tmp[1] = strings.TrimSuffix(strings.TrimSpace(tmp[1]), "(F)")
- }
- seqInt, err := strconv.Atoi(strings.TrimSuffix(strings.TrimSpace(tmp[1]), "]"))
- if err != nil {
- //Not an integer?
- log.Println("[RAID] Unable to parse " + disk + " sequence ID")
- continue
- }
- member := RAIDMember{
- Name: strings.TrimSpace(tmp[0]),
- Seq: seqInt,
- Failed: diskFailed,
- }
- members = append(members, &member)
- }
- //Sort the member disks
- sort.Slice(members[:], func(i, j int) bool {
- return members[i].Seq < members[j].Seq
- })
- // Create a RAIDDevice struct and append it to the slice
- raidDevice := RAIDDevice{
- Name: deviceName,
- Status: deviceStatus,
- Level: raidLevel,
- Members: members,
- }
- raidDevices = append(raidDevices, raidDevice)
- }
- return raidDevices, nil
- }
- // Check if a disk is failed in given array
- func (m *Manager) DiskIsFailed(mdDevice, diskPath string) (bool, error) {
- raidDevices, err := m.GetRAIDDeviceByDevicePath(mdDevice)
- if err != nil {
- return false, err
- }
- diskName := filepath.Base(diskPath)
- for _, disk := range raidDevices.Members {
- if disk.Name == diskName {
- return disk.Failed, nil
- }
- }
- return false, errors.New("target disk not found in this array")
- }
- // FailDisk label a disk as failed
- func (m *Manager) FailDisk(mdDevice, diskPath string) error {
- //mdadm commands require full path
- if !strings.HasPrefix(diskPath, "/dev/") {
- diskPath = filepath.Join("/dev/", diskPath)
- }
- if !strings.HasPrefix(mdDevice, "/dev/") {
- mdDevice = filepath.Join("/dev/", mdDevice)
- }
- cmd := exec.Command("sudo", "mdadm", mdDevice, "--fail", diskPath)
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to fail disk: %v", err)
- }
- return nil
- }
- // RemoveDisk removes a failed disk from the specified RAID array using mdadm.
- // must be failed before remove
- func (m *Manager) RemoveDisk(mdDevice, diskPath string) error {
- //mdadm commands require full path
- if !strings.HasPrefix(diskPath, "/dev/") {
- diskPath = filepath.Join("/dev/", diskPath)
- }
- if !strings.HasPrefix(mdDevice, "/dev/") {
- mdDevice = filepath.Join("/dev/", mdDevice)
- }
- cmd := exec.Command("sudo", "mdadm", mdDevice, "--remove", diskPath)
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to remove disk: %v", err)
- }
- return nil
- }
- // Add disk to a given RAID array, must be unmounted and not in-use
- func (m *Manager) AddDisk(mdDevice, diskPath string) error {
- //mdadm commands require full path
- if !strings.HasPrefix(diskPath, "/dev/") {
- diskPath = filepath.Join("/dev/", diskPath)
- }
- if !strings.HasPrefix(mdDevice, "/dev/") {
- mdDevice = filepath.Join("/dev/", mdDevice)
- }
- cmd := exec.Command("sudo", "mdadm", mdDevice, "--add", diskPath)
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to add disk: %v", err)
- }
- return nil
- }
- // GrowRAIDDevice grows the specified RAID device to its maximum size
- func (m *Manager) GrowRAIDDevice(deviceName string) error {
- //Prevent anyone passing /dev/md0 into the deviceName field
- deviceName = strings.TrimPrefix(deviceName, "/dev/")
- // Construct the mdadm command
- cmd := exec.Command("sudo", "mdadm", "--grow", fmt.Sprintf("/dev/%s", deviceName), "--size=max")
- // Execute the command
- output, err := cmd.CombinedOutput()
- if err != nil {
- return fmt.Errorf("failed to grow RAID device: %v, output: %s", err, string(output))
- }
- fmt.Printf("[RAID] Successfully grew RAID device %s. Output: %s\n", deviceName, string(output))
- return nil
- }
|