Pārlūkot izejas kodu

Added disk remove function

aroz 1 gadu atpakaļ
vecāks
revīzija
8f7357a494

+ 34 - 0
disk.go

@@ -161,9 +161,43 @@ func DiskServiceInit() {
 				adminRouter.HandleFunc("/system/disk/raid/format", raidManager.HandleFormatRaidDevice)
 				adminRouter.HandleFunc("/system/disk/raid/detail", raidManager.HandleLoadArrayDetail)
 				adminRouter.HandleFunc("/system/disk/raid/devinfo", raidManager.HandlListChildrenDeviceInfo)
+				adminRouter.HandleFunc("/system/disk/raid/removeMemeber", func(w http.ResponseWriter, r *http.Request) {
+					/*
+						Remove a member disk from RAID array
+						Require double check for credentials
+					*/
+					userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+					if err != nil {
+						w.WriteHeader(http.StatusUnauthorized)
+						w.Write([]byte("401 Unauthorized"))
+						return
+					}
+
+					if !userinfo.IsAdmin() {
+						utils.SendErrorResponse(w, "Permission Denied")
+						return
+					}
+
+					//Double check password for this user
+					password, err := utils.PostPara(r, "password")
+					if err != nil {
+						utils.SendErrorResponse(w, "Password Incorrect")
+						return
+					}
+
+					passwordCorrect, rejectionReason := authAgent.ValidateUsernameAndPasswordWithReason(userinfo.Username, password)
+					if !passwordCorrect {
+						utils.SendErrorResponse(w, rejectionReason)
+						return
+					}
+
+					//OK! Handle the remaining operations
+					raidManager.HandleRemoveDiskFromRAIDVol(w, r)
+				})
 
 				/* Device Management functions */
 				adminRouter.HandleFunc("/system/disk/devices/list", raidManager.HandleListUsableDevices)
+				adminRouter.HandleFunc("/system/disk/devices/model", raidManager.HandleResolveDiskModelLabel)
 
 				/* Advance functions*/
 				adminRouter.HandleFunc("/system/disk/raid/assemble", raidManager.HandleRaidDevicesAssemble)

+ 48 - 0
mod/disk/diskfs/disklb.go

@@ -0,0 +1,48 @@
+package diskfs
+
+import (
+	"encoding/json"
+	"fmt"
+	"os/exec"
+	"strings"
+)
+
+type BlockDeviceModelInfo struct {
+	Name     string                 `json:"name"`
+	Size     string                 `json:"size"`
+	Model    string                 `json:"model"`
+	Children []BlockDeviceModelInfo `json:"children"`
+}
+
+// Get disk model name by disk name (sdX, not /dev/sdX), return the model name (if any) and expected size (not actual)
+// return device labeled size, model and error if any
+func GetDiskModelByName(name string) (string, string, error) {
+	cmd := exec.Command("sudo", "lsblk", "--json", "-o", "NAME,SIZE,MODEL")
+
+	output, err := cmd.Output()
+	if err != nil {
+		return "", "", fmt.Errorf("error running lsblk: %v", err)
+	}
+
+	var blockDevices struct {
+		BlockDevices []BlockDeviceModelInfo `json:"blockdevices"`
+	}
+
+	if err := json.Unmarshal(output, &blockDevices); err != nil {
+		return "", "", fmt.Errorf("error parsing lsblk output: %v", err)
+	}
+
+	return findDiskInfo(blockDevices.BlockDevices, name)
+}
+
+func findDiskInfo(blockDevices []BlockDeviceModelInfo, name string) (string, string, error) {
+	for _, device := range blockDevices {
+		if device.Name == name {
+			return device.Size, device.Model, nil
+		}
+		if strings.HasPrefix(name, device.Name) {
+			return findDiskInfo(device.Children, name)
+		}
+	}
+	return "", "", fmt.Errorf("disk not found: %s", name)
+}

+ 76 - 0
mod/disk/raid/handler.go

@@ -7,6 +7,7 @@ import (
 	"path/filepath"
 	"strconv"
 	"strings"
+	"time"
 
 	"imuslab.com/arozos/mod/disk/diskfs"
 	"imuslab.com/arozos/mod/utils"
@@ -18,6 +19,60 @@ import (
 	This module handle api call to the raid module
 */
 
+func (m *Manager) HandleRemoveDiskFromRAIDVol(w http.ResponseWriter, r *http.Request) {
+	//mdadm --remove /dev/md0 /dev/sdb1
+	mdDev, err := utils.PostPara(r, "raidDev")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid raid device given")
+		return
+	}
+
+	sdXDev, err := utils.PostPara(r, "memDev")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid member device given")
+		return
+	}
+
+	//Check if target array exists
+	if !m.RAIDDeviceExists(mdDev) {
+		utils.SendErrorResponse(w, "target RAID array not exists")
+		return
+	}
+
+	//Check if the disk is already failed
+	diskAlreadyFailed, err := m.DiskIsFailed(mdDev, sdXDev)
+	if err != nil {
+		log.Println("[RAID] Unable to validate if disk failed: " + err.Error())
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+	//Disk not failed. Mark it as failed
+	if !diskAlreadyFailed {
+		err = m.FailDisk(mdDev, sdXDev)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+	}
+
+	//Add some delay for OS level to handle IO closing
+	time.Sleep(300 * time.Millisecond)
+
+	//Done. Remove the device from array
+	err = m.RemoveDisk(mdDev, sdXDev)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+	log.Println("[RAID] Memeber disk " + sdXDev + " removed from RAID volume " + mdDev)
+	utils.SendOK(w)
+}
+
+func (m *Manager) HandleAddDiskToRAIDVol(w http.ResponseWriter, r *http.Request) {
+
+	//mdadm --add /dev/md0 /dev/sdb1
+}
+
 // Handle force flush reloading mdadm to solve the md0 become md127 problem
 func (m *Manager) HandleMdadmFlushReload(w http.ResponseWriter, r *http.Request) {
 	err := m.FlushReload()
@@ -28,6 +83,27 @@ func (m *Manager) HandleMdadmFlushReload(w http.ResponseWriter, r *http.Request)
 	utils.SendOK(w)
 }
 
+// Handle resolving the disk model label, might return null
+func (m *Manager) HandleResolveDiskModelLabel(w http.ResponseWriter, r *http.Request) {
+	devName, err := utils.GetPara(r, "devName")
+	if err != nil {
+		utils.SendErrorResponse(w, "invalid device name given")
+		return
+	}
+
+	//Function only accept sdX not /dev/sdX
+	devName = filepath.Base(devName)
+
+	labelSize, labelModel, err := diskfs.GetDiskModelByName(devName)
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	js, _ := json.Marshal([]string{labelModel, labelSize})
+	utils.SendJSONResponse(w, string(js))
+}
+
 // Handle force flush reloading mdadm to solve the md0 become md127 problem
 func (m *Manager) HandlListChildrenDeviceInfo(w http.ResponseWriter, r *http.Request) {
 	devName, err := utils.GetPara(r, "devName")

+ 67 - 5
mod/disk/raid/mdadm.go

@@ -6,6 +6,7 @@ import (
 	"log"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"sort"
 	"strconv"
 	"strings"
@@ -21,8 +22,9 @@ import (
 
 // RAIDDevice represents information about a RAID device.
 type RAIDMember struct {
-	Name string
-	Seq  int
+	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 {
@@ -142,7 +144,7 @@ func (m *Manager) GetRAIDDevicesFromProcMDStat() ([]RAIDDevice, error) {
 
 		// Split the members string by space to get individual member devices
 		info := strings.Fields(parts[1])
-		if len(info) < 4 {
+		if len(info) < 2 {
 			//Malform output
 			continue
 		}
@@ -172,6 +174,12 @@ func (m *Manager) GetRAIDDevicesFromProcMDStat() ([]RAIDDevice, error) {
 			}
 
 			//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?
@@ -179,8 +187,9 @@ func (m *Manager) GetRAIDDevicesFromProcMDStat() ([]RAIDDevice, error) {
 				continue
 			}
 			member := RAIDMember{
-				Name: strings.TrimSpace(tmp[0]),
-				Seq:  seqInt,
+				Name:   strings.TrimSpace(tmp[0]),
+				Seq:    seqInt,
+				Failed: diskFailed,
 			}
 
 			members = append(members, &member)
@@ -203,3 +212,56 @@ func (m *Manager) GetRAIDDevicesFromProcMDStat() ([]RAIDDevice, error) {
 
 	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
+}

+ 21 - 0
web/SystemAO/disk/raid/img/raid.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="128px"
+	 height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<g id="圖層_1">
+	<rect x="12.563" y="32.823" fill="#727171" width="102.834" height="86.829"/>
+	<path fill="#3E3A39" d="M113.23,114.962c0,1.729-1.357,3.127-3.035,3.127H17.64c-1.676,0-3.035-1.398-3.035-3.127V95.153
+		c0-1.729,1.359-3.127,3.035-3.127h92.556c1.678,0,3.035,1.398,3.035,3.127V114.962z"/>
+	<circle fill="#00A0E9" cx="100.48" cy="105.066" r="3.708"/>
+</g>
+<g id="圖層_2">
+	<polygon fill="#DCDDDD" points="96.438,14.002 30.271,14.002 12.604,32.823 115.438,32.823 	"/>
+	<path fill="#3E3A39" d="M113.023,85.822c0,1.729-1.357,3.127-3.035,3.127H17.433c-1.676,0-3.035-1.398-3.035-3.127V66.014
+		c0-1.729,1.359-3.127,3.035-3.127h92.556c1.678,0,3.035,1.398,3.035,3.127V85.822z"/>
+	<circle fill="#00A0E9" cx="100.273" cy="75.929" r="3.708"/>
+	<path fill="#3E3A39" d="M112.92,57.3c0,1.729-1.357,3.127-3.035,3.127H17.329c-1.676,0-3.035-1.398-3.035-3.127V37.492
+		c0-1.729,1.359-3.127,3.035-3.127h92.556c1.678,0,3.035,1.398,3.035,3.127V57.3z"/>
+	<circle fill="#00A0E9" cx="100.17" cy="47.406" r="3.708"/>
+</g>
+</svg>

+ 28 - 0
web/SystemAO/disk/raid/img/remove-warning.svg

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<g id="圖層_1_1_">
+	<rect x="12.563" y="32.823" fill="#727171" width="102.833" height="86.83"/>
+	<path fill="#3E3A39" d="M113.23,114.962c0,1.729-1.357,3.127-3.035,3.127H17.64c-1.676,0-3.035-1.397-3.035-3.127v-19.81
+		c0-1.729,1.359-3.127,3.035-3.127h92.555c1.679,0,3.035,1.398,3.035,3.127V114.962L113.23,114.962z"/>
+	<circle fill="#00A0E9" cx="100.48" cy="105.065" r="3.708"/>
+</g>
+<g id="圖層_2">
+	<polygon fill="#DCDDDD" points="96.438,14.002 30.271,14.002 12.604,32.823 115.438,32.823 	"/>
+	<path fill="#3E3A39" d="M113.023,85.822c0,1.729-1.357,3.127-3.035,3.127H17.433c-1.676,0-3.035-1.398-3.035-3.127V66.014
+		c0-1.729,1.359-3.126,3.035-3.126h92.555c1.679,0,3.035,1.398,3.035,3.126V85.822L113.023,85.822z"/>
+	<circle fill="#00A0E9" cx="100.273" cy="75.929" r="3.708"/>
+	<path fill="#3E3A39" d="M112.92,57.3c0,1.729-1.357,3.127-3.035,3.127H17.329c-1.676,0-3.035-1.398-3.035-3.127V37.492
+		c0-1.729,1.359-3.127,3.035-3.127h92.556c1.678,0,3.035,1.398,3.035,3.127V57.3z"/>
+	<circle fill="#00A0E9" cx="100.17" cy="47.406" r="3.708"/>
+</g>
+<circle fill="#FF1D1D" cx="99.582" cy="98.738" r="23.667"/>
+<g>
+	<path fill="#FFFFFF" d="M99.582,116.816c-0.956,0-1.774-0.337-2.456-1.011c-0.682-0.673-1.022-1.512-1.022-2.515
+		c0-0.971,0.333-1.786,0.999-2.444s1.492-0.986,2.479-0.986s1.813,0.328,2.479,0.986s0.999,1.474,0.999,2.444
+		c0,1.003-0.333,1.842-0.999,2.515C101.396,116.479,100.569,116.816,99.582,116.816z M97.89,104.783l-0.563-22.984h4.418
+		l-0.563,22.984H97.89z"/>
+</g>
+</svg>

+ 210 - 21
web/SystemAO/disk/raid/index.html

@@ -82,7 +82,7 @@
                 border-radius: 0.4em !important;
                 margin-bottom: 0 !important;
                 margin-top: 0 !important;
-                cursor: pointer;
+                
                 position: relative;
             }
             .raiddevice:hover{
@@ -103,6 +103,21 @@
                 padding-right: 5px;
             }
 
+            /* RAID memeber options */
+            .raidDeviceOptions{
+                width: 100%;
+                min-height: 2em;
+            }
+
+            .raidDeviceOptions .content{
+                float :right;
+            }
+
+            .notMountedLabel{
+                color:rgb(194, 194, 194);
+                font-weight: 300;
+            }
+
             /* Danger zone css */
             .dangerzone{
                 border: 1px solid #dedede;
@@ -126,13 +141,16 @@
         </style>
     </head>
     <body>
+        <br>
         <div class="ui container">
+            <!-- 
             <div class="ui basic segment">
                 <h3 class="ui header">
                     <span id="RAIDhealthyState"></span>
                     <div class="sub header"><span id="RAIDvolCount">0</span> RAID Storage Volume Detected</div>
                 </h3>
             </div>
+            -->
             <div class="ui stackable grid">
                 <div class="six wide column" style="border-right: 1px solid #e0e0e0;">
                     <div id="raidVolList">
@@ -143,13 +161,16 @@
                         <button title="Add new RAID volume" onclick="addNewRaidVolume()" class="circular basic green large ui icon button">
                             <i class="green add icon"></i>
                         </button>
+                        <button title="Assemble RAID Pools" onclick="assembleRaidVolumes()" class="circular basic orange large ui icon button">
+                            <i class="orange refresh icon"></i>
+                        </button>
                     </div>
                 </div>
                 <div class="ten wide column">
                     <div id="volumeDetail">
                         <div id="raidDiskOverview">
                             <h3 class="ui header">
-                                <i class="ui green check circle icon"></i>
+                                <i id="RAIDOverviewStateIcon" class="ui green check circle icon"></i>
                                 <div class="content">
                                     <span id="RAIDOverviewDiskpath">Loading device details</span>
                                     <div class="sub header" id="RAIDOverviewDetails"></div>
@@ -199,17 +220,36 @@
                         </div>
                         <br>
                         <div id="raidDiskList">
-                            <div class="ui basic segment">
-                                <h4 class="ui header">
-                                    <img src="../disk/raid/img/drive-working.svg">
-                                    <div class="content">
-                                      /dev/sdb
-                                      <div class="sub header">Check out our plug-in marketplace</div>
-                                    </div>
-                                  </h4>
-                            </div>
+                            
                         </div>
                         <div class="ui divider"></div>
+                        <table class="ui very basic compact celled table" style="padding-left: 1em; padding-right: 1em;">
+                            <tbody>
+                                <tr>
+                                    <td>
+                                        <h4><i class="green add icon"></i> Add New Disk</h4>
+                                        <p>Add a new disk to the array. If your array have an empty slot (aka removed disk), the disk will be added as working disk, otherwise, it will be added as a spare disk.</p>
+                                    </td>
+                                    <td>
+                                        <button onclick="" class="circular small basic ui button">
+                                            <i class="green add icon"></i> Add Disk
+                                        </button>
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <td>
+                                        <h4><i class="purple expand icon"></i> Expand RAID Volume</h4>
+                                        <p>If you have upgraded all disks in this RAID storage pool, you can grow the RAID volume and its file system to match the new size of your disks. </p>
+                                    </td>
+                                    <td>
+                                        <button onclick="" class="circular small basic ui button">
+                                            <i class="purple expand icon"></i> Expand Volume
+                                        </button>
+                                    </td>
+                                </tr>
+                            </tbody>
+                        </table> 
+
                         <div class="dangerzone">
                             <div class="header">
                                 <h4>Danger Zone</h4>
@@ -246,8 +286,87 @@
                     </div>
                 </div>
             </div>
+
+            <!-- Disk Remove Confirmation -->
+            <div id="confirmDiskRemove" class="ui mini modal">
+                <i class="close icon"></i>
+                <div class="header">
+                  Confirm Disk Remove
+                </div>
+                <div class="image content">
+                  <div class="ui medium image">
+                    <img src="../disk/raid/img/remove-warning.svg">
+                  </div>
+                  <div class="description">
+                    <p id="oprconfirm"></p>
+                    <p><b>Removing a disk from RAID volume might risk data loss.</b> Make sure you know what you are doing.<br></p>
+                  </div>
+                </div>
+                <div class="actions">
+                  <div class="ui black deny button">
+                    Cancel
+                  </div>
+                  <div class="ui negative left labeled icon button" onclick="confirmRemoveDisk();">
+                    <i class="trash icon"></i>
+                    REMOVE
+                  </div>
+                </div>
+              </div>
+
         <script>
 
+            var raidManager = {
+                editingArray: "",
+                removePendingDisk: "",
+                removePendingSourceVol: ""
+            };
+
+            function removeDisk(arrayName, diskPath){
+                console.log(arrayName, diskPath);
+                $("#oprconfirm").html("Remove <b>" + diskPath + "</b> from <b>" + arrayName + "</b>");
+                $("#confirmDiskRemove").modal('show');
+
+                raidManager.removePendingDisk = diskPath;
+                raidManager.removePendingSourceVol = arrayName;
+            }
+            
+
+            function confirmRemoveDisk(){
+                let raidName = raidManager.removePendingSourceVol.split("/").pop();
+                var apiObject = {
+                    api: "../system/disk/raid/removeMemeber",
+                    data: {
+                        raidDev:  raidManager.removePendingSourceVol,
+                        memDev: raidManager.removePendingDisk
+                    },
+                    title: `<i class='yellow exclamation triangle icon'></i> REMOVE DISK FROM RAID VOLUME <i class='yellow exclamation triangle icon'></i>`,
+                    desc: `Confirm remove ${raidManager.removePendingDisk} from ${raidManager.removePendingSourceVol}`,
+                    thisuser: true, //This username as default, set to false for entering other user
+                    method: "POST",
+                    success: undefined
+                }
+                apiObject = encodeURIComponent(JSON.stringify(apiObject));
+                
+               
+                parent.newFloatWindow({
+                    url: "SystemAO/security/authreq.html#" + apiObject,
+                    width: 480,
+                    height: 300,
+                    appicon: "SystemAO/security/img/lock.svg",
+                    title: `Confirm Disk Remove`,
+                    parent: ao_module_windowID,
+                    callback: "handleRemoveDiskCallback"
+                });
+            }
+
+            //Remove disk completed
+            window.handleRemoveDiskCallback = function(succ){
+                console.log(data);
+                setTimeout(function(){
+                    reloadRAIDVolDetail();
+                }, 300);
+            }
+
             //Quick function to check if the disk is healthy
             function isHealthy(stateText) {
                 // Check if the stateText contains any unhealthy states
@@ -269,7 +388,7 @@
                 } else if (stateText.includes('recovering')) {
                     return 'ui yellow loading sync icon';
                 } else if (stateText.includes('degraded')) {
-                    return 'ui yellow exclamation triangle';
+                    return 'ui yellow exclamation circle icon';
                 } else if (stateText.includes('inactive')) {
                     return 'ui grey check sync icon';
                 } else if (stateText.includes('active')) {
@@ -287,6 +406,13 @@
                 }
             }
 
+            //Reload the current raid volume detail
+            function reloadRAIDVolDetail(){
+                if (raidManager.editingArray != undefined){
+                    loadRAIDVolDetail(raidManager.editingArray);
+                } 
+            }
+
             function loadRAIDVolDetail(deviceName){
                 function capitalize(string) {
                     return string.charAt(0).toUpperCase() + string.slice(1);
@@ -300,10 +426,19 @@
                     return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
                 };
 
+                //Highlight the vol selected
+                $(".raidVol").removeClass('active');
+                $(".raidVol").each(function(){
+                    if ($(this).attr("devpath") == deviceName){
+                        $(this).addClass('active');
+                    }
+                })
+
                 let deiviceSizeMap = {};
                 //Load the device size map
                 $.get("../../system/disk/raid/devinfo?devName=" + deviceName, function(sizemap){
                     deiviceSizeMap = sizemap;
+                    raidManager.editingArray = deviceName;
                     //Get the information of the device
                     $.get("../../system/disk/raid/detail?devName=" + deviceName, function(data){
                         if (data.error != undefined){
@@ -318,8 +453,6 @@
                             return;
                         }
 
-                    
-
                         //Update the active disks info
                         $("#RAIDOverviewDiskpath").html(data.DevicePath + ` <span class="mdevice">(${data.RaidLevel.toUpperCase()})</span>`);
                         $("#RAIDOverviewDetails").text(data.UUID + " | State: " + capitalize(data.State));
@@ -327,6 +460,7 @@
                         $("#RAIDWorkingDevices").text(data.WorkingDevices);
                         $("#RAIDFailedDevices").text(data.FailedDevices);
                         $("#RAIDSpareDevices").text(data.SpareDevices);
+                        $("#RAIDOverviewStateIcon").attr("class", getStateIconFromStateText(data.State));
 
                         //Render the disk list
                         $("#raidDiskList").html("");
@@ -336,34 +470,89 @@
                             let drivePath = thisDeviceInfo.DevicePath
                             let driveName = drivePath.split("/").pop();
                             let driveIcon = "drive-working.svg";
+                            let driveSize = "Unknown";
+                            let driveRWState = "Read Write";
                             if (thisDeviceInfo.DevicePath = ""){
                                 driveIcon = "drive-notfound.svg"
                                 drivePath = "(Drive unplugged or damaged)"
                             }
 
+                            let diskIsFailed = false;
+                            if (thisDeviceInfo.State.includes("faulty")){
+                                //Disk labeled as failed
+                                driveIcon = "drive-failed.svg"
+                                diskIsFailed = true;
+                            }else if (thisDeviceInfo.State.includes("removed")){
+                                driveIcon = "drive-notfound.svg"
+                            }else if (thisDeviceInfo.State.includes("rebuilding")){
+                                driveIcon = "drive-spare.svg"
+                            }
+
+                            if (deiviceSizeMap[driveName] == undefined){
+                                //Disk not found. Is it failed?
+                                driveSize = "";
+                                driveRWState = "";
+                            }else{
+                                driveSize = bytesToSize(deiviceSizeMap[driveName].size);
+                                driveRWState = deiviceSizeMap[driveName].ro?"<span class='readonlytag'>Read Only</span>":"Read Write";
+                            }
 
-                            $("#raidDiskList").append(`<div class="ui basic segment raiddevice">
+                            //Handle case where raidDeviceNo is -1 (Not mounted / failed)
+                            if (raidDeviceNo == -1){
+                                raidDeviceNo = "Failed";
+                            }
+
+                            //A random UID for this DOM element to make handling easier
+                            let raiddeviceUID = Date.now();
+                            $("#raidDiskList").append(`<div class="ui basic segment raiddevice ${raiddeviceUID}" domid="${raiddeviceUID}">
                                 <h4 class="ui header">
                                     <img src="../disk/raid/img/${driveIcon}">
                                     <div class="content">
-                                        ${raidDeviceNo}: ${drivePath}
+                                        ${raidDeviceNo}: <span class="raidMemberLabel ${driveName}"></span><br>
+                                        <small><i class="ui yellow folder icon" style="margin-right: 0.3em;"></i>${drivePath!=""?drivePath:`<span class="notMountedLabel">[Not Mounted]</span>`}</small>
                                         <div class="sub header">${thisDeviceInfo.State.join(" | ")}</div>
                                     </div>
                                 </h4>
                                 <div class="capinfo">
-                                    ${bytesToSize(deiviceSizeMap[driveName].size)}<br>
-                                    <span style="font-size: 0.8em;">${deiviceSizeMap[driveName].ro?"<span class='readonlytag'>Read Only</span>":"Read Write"}</span>
+                                    ${driveSize}<br>
+                                    <span style="font-size: 0.8em;">${driveRWState}</span>
                                 </div>
                                 <div class="raidDeviceOptions">
-
+                                    <div class="content">
+                                        <button class="ui tiny basic button"><i class="ui info circle grey icon"></i> SMART</button>
+                                        <button class="ui tiny basic button" onclick="removeDisk('${deviceName}','${driveName}');"><i class="ui red trash icon"></i> Remove Disk from Volume</button>
+                                    </div>
                                 </div>
+                                <div class="ui divider"></div>
                             </div>`);
+
+                            if (driveName != "" && drivePath != ""){
+                                $(".raidMemberLabel." + driveName).html(`<i class="ui loading circle notch icon"></i> Resolving disk label`);
+                                resolveDiskLabelToDOM(drivePath, ".raidMemberLabel." + driveName);
+                            }else{
+                                //Invalid disk, e.g. Removed
+                                $(".raiddevice." + raiddeviceUID).find(".raidDeviceOptions").hide();
+                            }
+                            
+
                         }
 
                     });
                 });
             }
 
+            function resolveDiskLabelToDOM(diskPath, domSelector){
+                $.get("/system/disk/devices/model?devName=" + diskPath, function(data){
+                    let diskLabelName = ""
+                    if (data.error == undefined){
+                        //[0] is disk labeled name
+                        //[1] is disk labeled size
+                        diskLabelName = data[0];
+                    }
+                    $(domSelector).html(diskLabelName);
+                });
+            }
+
             //Initialize the RAID volume list on this system
             function initRAIDVolList(){
                 $("#raidVolList").html(`<p style="text-align: center;"><i class="ui loading spinner icon"></i></p>`);
@@ -377,9 +566,9 @@
                     let containUnhealthyDisk = false;
                     for (var i = 0; i < data.length; i++){
                         let thisDiskInfo = data[i];
-                        $("#raidVolList").append(`<div class="ui segment raidVol">
+                        $("#raidVolList").append(`<div class="ui segment raidVol" devpath="${thisDiskInfo.DevicePath}">
                             <h4 class="ui header">
-                                <img src="../../img/system/raid.svg">
+                                <img src="../disk/raid/img/raid.svg">
                                 <div class="healthIcon"><i class="${getStateIconFromStateText(thisDiskInfo.State)}"></i></div>
                                 <div class="content" style="margin-left: 0.6em;">
                                     <span>${thisDiskInfo.DevicePath}</span> <span class="mdevice">(${thisDiskInfo.RaidLevel.toUpperCase()})</span>