Parcourir la source

Added more RAID function

TC il y a 1 semaine
Parent
commit
12e89d91bc
7 fichiers modifiés avec 471 ajouts et 19 suppressions
  1. 5 0
      mod/disktool/raid/handler.go
  2. 21 14
      mod/disktool/raid/mdadmConf.go
  3. 5 0
      raid.go
  4. 109 4
      web/components/raid.html
  5. 313 0
      web/components/raid_new.html
  6. 15 0
      web/index.html
  7. 3 1
      web/js/locale.js

+ 5 - 0
mod/disktool/raid/handler.go

@@ -149,6 +149,11 @@ func (m *Manager) HandleAddDiskToRAIDVol(w http.ResponseWriter, r *http.Request)
 
 // Handle force flush reloading mdadm to solve the md0 become md127 problem
 func (m *Manager) HandleMdadmFlushReload(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodPost {
+		//This force the request to pass through the csrf check
+		utils.SendErrorResponse(w, "invalid method")
+		return
+	}
 	err := m.FlushReload()
 	if err != nil {
 		utils.SendErrorResponse(w, "reload failed: "+strings.ReplaceAll(err.Error(), "\n", " "))

+ 21 - 14
mod/disktool/raid/mdadmConf.go

@@ -1,6 +1,7 @@
 package raid
 
 import (
+	"errors"
 	"fmt"
 	"log"
 	"os"
@@ -32,21 +33,9 @@ func (m *Manager) FlushReload() error {
 
 	//Stop all of the running RAID devices
 	for _, rd := range raidDevices {
-
-		//Check if it is mounted. If yes, skip this
-		devMounted, err := diskfs.DeviceIsMounted("/dev/" + rd.Name)
-		if devMounted || err != nil {
-			log.Println("[RAID] " + "/dev/" + rd.Name + " is in use. Skipping.")
-			continue
-		}
-		log.Println("[RAID] Stopping " + rd.Name)
-
-		cmdMdadm := exec.Command("sudo", "mdadm", "--stop", "/dev/"+rd.Name)
-
-		// Run the command and capture its output
-		_, err = cmdMdadm.Output()
+		err = m.FlushReloadDev(&rd)
 		if err != nil {
-			log.Println("[RAID] Unable to stop " + rd.Name + ". Skipping")
+			log.Println("[RAID] Unable to stop " + rd.Name + ": " + err.Error())
 			continue
 		}
 	}
@@ -62,6 +51,24 @@ func (m *Manager) FlushReload() error {
 	return nil
 }
 
+// FlushReloadDev stop a single RAID device and remove it from mdadm config
+func (m *Manager) FlushReloadDev(targetDev *RAIDDevice) error {
+	//Check if it is mounted. If yes, skip this
+	devMounted, err := diskfs.DeviceIsMounted("/dev/" + targetDev.Name)
+	if devMounted || err != nil {
+		return errors.New("device is in use")
+	}
+
+	cmdMdadm := exec.Command("sudo", "mdadm", "--stop", "/dev/"+targetDev.Name)
+
+	// Run the command and capture its output
+	_, err = cmdMdadm.Output()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 // removeDevicesEntry remove device hardcode from mdadm config file
 func removeDevicesEntry(configLine string) string {
 	// Split the config line by space character

+ 5 - 0
raid.go

@@ -40,6 +40,11 @@ func HandleRAIDCalls() http.Handler {
 			// Activate a RAID device, require "dev=md0" as a query parameter
 			raidManager.HandleSyncPendingToReadWrite(w, r)
 			return
+		case "reassemble":
+			// Reassemble all RAID devices
+			raidManager.HandleForceAssembleReload(w, r)
+			return
+
 		case "test":
 			devname, err := utils.GetPara(r, "dev")
 			if err != nil {

+ 109 - 4
web/components/raid.html

@@ -18,6 +18,22 @@
         margin-top: -3px !important;
         margin-left: 0.15em !important;
     }
+
+    .new_raid_disk_selected{
+        position: absolute;
+        top: 0;
+        right: 0.4em;
+        color: var(--ts-positive-500);
+        display: none;
+    }
+
+    .new-raid-disk-info.selected .new_raid_disk_selected{
+        display: block;
+    }   
+
+    .new_raid_disk_selected span.ts-icon{
+        font-size: 2em !important;
+    }
 </style>
 <div class="ts-content">
     <div class="ts-container is-padded">
@@ -39,7 +55,7 @@
                         // 新增陣列
                         </span>
                     </button>
-                    <button class="ts-button is-start-icon is-positive is-circular">
+                    <button onclick="showForceAssembleWarning();" class="ts-button is-start-icon is-positive is-circular">
                         <span class="ts-icon is-rotate-icon" style="color: var(--ts-primary-500);"></span>
                         <span i18n>Assemble
                         // 重組陣列
@@ -54,8 +70,45 @@
             </div>
         </div>
     </div>
+    <style>
+        
+    </style>
+    <dialog id="raid_assemble_warning" class="ts-modal">
+        <div class="content">
+            <div class="ts-content">
+                <div class="ts-header" i18n>Confirm force stop & reload RAID from mdadm.conf? 
+                    // 確認強制停止並從 mdadm.conf 重新載入 RAID 陣列?
+                </div>
+            </div>
+            <div class="ts-divider"></div>
+            <div class="ts-content">
+                <div class="ts-text is-description">
+                    <span i18n> This will stop all RAID arrays and reload all configs from mdadm.conf. Mounted RAID partitions will not be unmounted.
+                        // 這將停止所有 RAID 陣列並從 mdadm.conf 重新載入設定,已掛載的 RAID 分割區不會被卸載。
+                    </span>
+                </div>
+            </div>
+            <div class="ts-divider"></div>
+            <div class="ts-content is-tertiary">
+                <div class="ts-wrap is-end-aligned">
+                    <button class="ts-button is-negative" onclick="confirmApplyReassemble();" i18n>Confirm
+                        // 確認
+                    </button>
+                    <button class="ts-button" onclick="cancelForceAssemble();" i18n>Cancel
+                        // 取消
+                    </button>
+                </div>
+            </div>
+        </div>
+    </dialog>
+    <dialog id="raid_new" class="ts-modal is-big mobile:is-fullscreen" open>
+
+    </dialog>
 </div>
 <script>
+    $("#raid_new").load("./components/raid_new.html", function(){
+        
+    });
     function initRAIDDeviceList(){
         $.ajax({
             url: './api/raid/list',
@@ -356,18 +409,35 @@
 
                         </div>
                     </div>
+                    <!-- Child Disks -->
                     <div class="has-top-spaced-small">
                         ${getRAIDChildDiskElement(raid.DeviceInfo)}
                     </div>
                     <!-- Operations -->
+                    <div class="has-top-spaced-small">
+                        <div class="ts-box">
+                            <div class="ts-content">
+                                <button class="ts-button is-start-icon is-positive is-circular" onclick="" >
+                                    <span class="ts-icon is-rotate-icon" style="color: var(--ts-primary-500);"></span>
+                                    <span i18n> Assemble
+                                        // 重組陣列
+                                    </span>
+                                </button>
+                                <button class="ts-button is-circular is-start-icon is-negative"> 
+                                    <span class="ts-icon is-trash-icon"></span>
+                                    <span i18n> Delete RAID
+                                        // 刪除陣列
+                                    </span>
+                                </button>
+                            </div>
+                        </div>
+                    </div>
+                    <!-- Operations -->
                     <div class="raid-array-opr-btns">
                         <div class="ts-content">
                             <button class="ts-button is-circular is-icon" onclick="refreshRAIDArrayStatus('${mdX}');">
                                 <span class="ts-icon is-arrows-rotate-icon"></span>
                             </button>
-                            <button class="ts-button is-circular is-icon is-negative" onclick="deleteRAIDArray('${mdX}');">
-                                <span class="ts-icon is-trash-icon"></span>
-                            </button>
                         </div>
                     </div>  
                 </div>
@@ -555,5 +625,40 @@
         $(`#raid_menu_${index}`).addClass('is-active'); // Add active class to the selected menu item
         relocale(); // Recalculate layout
     }
+
+    /* Assemble RAID */
+    function cancelForceAssemble(){
+        $('#raid_assemble_warning')[0].close();
+    }
+
+    function showForceAssembleWarning(){
+        $('#raid_assemble_warning')[0].showModal();
+    }
+
+    function AssembleAllRAID(){
+        $.cjax({
+            url: './api/raid/reassemble',
+            method: 'POST',
+            success: function(data) {
+                if (data.error != undefined){
+                    // Handle error response
+                    console.error('Error reassembling RAID device:', data.error);
+                }else{
+                    // Successfully activated the device
+                    console.log('RAID device reassemble started successfully:', data);
+                    msgbox(i18nc("raid_reassemble_started_succ"));
+                    setTimeout(function() {
+                        // Refresh the RAID device list after a short delay
+                        initRAIDDeviceList();
+                    }, 300);
+                }
+            },
+        });
+    }
+
+    function confirmApplyReassemble(){
+        $('#raid_assemble_warning')[0].close();
+        AssembleAllRAID();
+    }
 </script>
 

+ 313 - 0
web/components/raid_new.html

@@ -0,0 +1,313 @@
+<!-- This will be shown in a ts-modal in the raid.html-->
+<style>
+    .new-raid-modal-content{
+        max-height: 70vh;
+        overflow-y: auto;
+    }
+    @media screen and (max-width: 767px) {
+        .new-raid-modal-content {
+            max-height: none;
+        }
+    }
+
+    .new-raid-disk-info{
+        cursor: pointer;
+        
+    }
+
+    .new-raid-disk-info:hover{
+        opacity: 0.5;
+    }
+</style>
+<div class="content">
+<div class="ts-content">
+    <div class="ts-header" i18n>Create New RAID Array
+        // 建立 RAID 陣列
+    </div>
+</div>
+<div class="ts-divider"></div>
+<div class="ts-content new-raid-modal-content">
+    <div class="ts-grid mobile:is-stacked" >
+        <div class="column is-3-wide">
+            <div class="ts-procedure is-vertical has-top-spaced-large" style="position: sticky; top: 0;">
+                <a step="1" class="item is-active">
+                    <div class="content">
+                        <div class="indicator"></div>
+                        <div class="label" i18n>RAID Name
+                            // 陣列名稱
+                        </div>
+                    </div>
+                </a>
+                <a step="2" class="item">
+                    <div class="content">
+                        <div class="indicator"></div>
+                        <div class="label" i18n>Select Disks
+                            // 選擇磁碟
+                        </div>
+                    </div>
+                </a>
+                <a step="3" class="item">
+                    <div class="content">
+                        <div class="indicator"></div>
+                        <div class="label" i18n>Raid Type
+                            // 陣列類型
+                        </div>
+                    </div>
+                </a>
+                <a step="4" class="item">
+                    <div class="content">
+                        <div class="indicator"></div>
+                        <div class="label" i18n>Confirm
+                            // 確認設定
+                        </div>
+                    </div>
+                </a>
+            </div>
+        </div>
+        <div class="column is-13-wide" style="overflow-y: auto;">
+            <div class="ts-content">
+                <!-- Array Name -->
+                <div class="ts-text" i18n>RAID Name
+                    // 陣列名稱
+                </div>
+                <div class="ts-input has-top-spaced-small">
+                    <input type="text" placeholder="my-raid" id="raid_name" i18n>
+                    <span class="ts-icon is-circle-check-icon raid_name_valid_icon" style="color: var(--ts-positive-400); display:none;"></span>
+                </div>
+                <div class="ts-text is-description" i18n>Only alphabet, digits, _ (underscore) and - (hyphen) are allowed
+                    // 只允許字母、數字、_ (底線) 和 - (連字符)
+                </div>
+
+                <!-- Select Disks -->
+                <div class="ts-divider has-top-spaced-small has-bottom-spaced-large"></div>
+                <div id="new_raid_disk_select">
+                    <div class="ts-content">
+                        <div class="ts-text is-description" i18n>Loading disks, please wait...
+                            // 正在載入磁碟,請稍候...
+                        </div>
+                    </div>
+                </div>
+    
+                <div class="ts-text is-description has-top-spaced-small" i18n> Tips: For any extra disks selected, it will be used as spare disks.
+                    // 提示:選擇的任何額外磁碟將用作備用磁碟。
+                </div>
+                <div class="ts-wrap is-end-aligned">
+                    <button class="ts-button is-outlined" i18n>Refresh
+                        // 重新整理
+                    </button>
+                </div>
+
+                <!-- Array Type -->
+                <div class="ts-divider has-top-spaced-small has-bottom-spaced-small"></div>
+                <div class="ts-text" i18n>RAID Type
+                    // 陣列類型
+                </div>
+                <button class="ts-button is-start-icon has-top-spaced-small" data-dropdown="new_raid_type_select">
+                    <span class="ts-icon is-chevron-down-icon"></span>
+                    <span class="raid_type_selected" i18n>Select RAID Type
+                        // 選擇陣列類型
+                    </span>
+                </button>
+                <div class="ts-dropdown is-end-icon" id="new_raid_type_select">
+                    <button class="item raid_type" value="raid1">RAID 1 <span class="ts-icon is-star-icon" style="color: var(--ts-warning-400);"></span></button>
+                    <button class="item raid_type" value="raid6">RAID 6 <span class="ts-icon is-star-icon" style="color: var(--ts-warning-400);"></span></button>
+                    <button class="item raid_type" value="raid5">RAID 5 </button>
+                    <button class="item raid_type" value="raid0">RAID 0</button>
+                </div>
+            </div>
+            
+        </div>
+        
+    </div>
+</div>
+<div class="ts-divider"></div>
+<div class="ts-content is-tertiary">
+    <div class="ts-wrap is-end-aligned">
+        <button class="ts-button" i18n>Confirm
+            // 確認
+        </button>
+        <button class="ts-button is-outlined" i18n>Cancel
+            // 取消
+        </button>
+    </div>
+</div>
+
+<script>
+
+    /* RAID name validation */
+    $("#raid_name").on("keydown", function() {
+        validateRaidName();
+    });
+
+    $("#raid_name").on("change", function() {
+        validateRaidName();
+    });
+
+    function validateRaidName(){
+        var raidName = $("#raid_name").val().trim();
+        var isValid = /^[a-zA-Z0-9_-]+$/.test(raidName) && raidName.trim() !== "";
+
+        if (raidName == ""){
+            $("#raid_name").parent().removeClass("is-end-icon");
+            $("a[step='1']").removeClass("is-completed").removeClass("is-active");
+            $("a[step='2']").removeClass("is-active");
+            $(".ts-icon.raid_name_valid_icon").hide();
+        } else if (isValid) {
+            $("#raid_name").parent().removeClass("is-negative");
+            $("a[step='1']").removeClass("is-active").addClass("is-completed");
+            $("a[step='2']").addClass("is-active");
+            $("#raid_name").parent().addClass("is-end-icon");
+            $(".ts-icon.raid_name_valid_icon").show();
+        } else {
+            $("#raid_name").parent().addClass("is-negative");
+            $("a[step='1']").removeClass("is-completed").addClass("is-active");
+            $("a[step='2']").removeClass("is-active");
+            $("#raid_name").parent().removeClass("is-end-icon");
+            $(".ts-icon.raid_name_valid_icon").hide();
+        }
+    }
+
+    /* Disk selection */
+    function initNewRAIDDiskList(){
+        $.get("./api/info/list", function(data) {
+            if (data) {
+                var disks = data;
+                var diskList = $("#new_raid_disk_select");
+                diskList.empty();
+
+                for (var i = 0; i < disks.length; i++) {
+                    let disk = disks[i];
+                    let partitionTable = "";
+                    let diskIsMounted = false;
+                    let diskIsAlreadyRAID = false; //The disk already belongs to another RAID array
+                    // Render the partition table
+                    if (disk.partitions.length > 0) {
+                        partitionTable += `<table class="ts-table is-bordered is-striped">
+                                            <thead>
+                                                <tr>
+                                                    <th i18n>Partition Name
+                                                        // 分割區    
+                                                    </th>
+                                                    <th i18n>Size
+                                                        // 大小    
+                                                    </th>
+                                                    <th i18n>Type
+                                                        // 類型
+                                                    </th>
+                                                    <th i18n>Mount Point
+                                                        // 掛載點
+                                                    </th>
+                                                </tr>
+                                            </thead>
+                                            <tbody>`;
+                        for (var j = 0; j < disk.partitions.length; j++) {
+                            let partition = disk.partitions[j];
+                            partitionTable += `<tr>
+                                                    <td>${partition.name}</td>
+                                                    <td>${humanFileSize(partition.size)}</td>
+                                                    <td>${partition.fstype || partition.blocktype}</td>
+                                                    <td>${partition.mountpoint || ""}</td>
+                                               </tr>`;
+                            if (partition.mountpoint) {
+                                diskIsMounted = true;
+                            }
+                            if (partition.blocktype.includes("raid")) {
+                                diskIsAlreadyRAID = true;
+                            }
+                        }
+                        partitionTable += `</tbody></table>`;
+                    } else {
+                        partitionTable = `<div class="ts-text is-description" i18n>No Partitions
+                                            // 無分割區
+                                          </div>`;
+                    }
+                   
+                    let warningClass = "";
+                    let warningMessage = `<div class="ts-text is-negative" i18n> 
+                                            This disk is mounted and might be in use.
+                                            // 此磁碟已被掛載,可能正在使用中。
+                                        </div>`;
+                    if (diskIsMounted) {
+                        warningClass = "is-negative is-start-indicated";
+                    }
+
+                    let disabledClass = "";
+                    if (diskIsAlreadyRAID) {
+                        disabledClass = "is-disabled";
+                    } 
+
+                    // Append disk information with partition table
+                    diskList.append(`<div class="ts-box ts-content has-top-spaced-small new-raid-disk-info ${warningClass} ${disabledClass}">
+                                        ${diskIsMounted ? warningMessage : ""}
+                                        <div class="ts-item">
+                                            <div class="ts-header">${disk.model} <span class="ts-badge has-start-spaced-small">${humanFileSize(disk.size)}</span></div>
+                                            <div class="ts-text is-description">
+                                                /dev/${disk.name}
+                                            </div>
+                                        </div>
+                                        <div class="has-top-spaced-small">
+                                            ${partitionTable}
+                                        </div>
+                                        <div class="new_raid_disk_selected">
+                                            <span class="ts-icon is-circle-check-icon" style="color: var(--ts-positive-400); font-size: 1.5rem;"></span>
+                                        </div>
+                                    </div>`);
+                }
+            } else {
+                console.error("Failed to load disk info: " + data.message);
+            }
+
+            //Bind click event to each disk info
+            $(".new-raid-disk-info").off("click").on("click", function() {
+                var selectedDisk = $(this);
+                var selectedIcon = selectedDisk.find(".new_raid_disk_selected");
+                var isSelected = $(this).hasClass("selected");
+                if (isSelected) {
+                    selectedDisk.removeClass("selected");
+                    selectedIcon.hide();
+                } else {
+                    selectedDisk.addClass("selected");
+                    selectedIcon.show();
+                }
+
+                // Check if any disk is selected
+                if ($(".new-raid-disk-info.selected").length > 0) {
+                    // Mark step 2 as completed and step 3 as active
+                    $("a[step='2']").removeClass("is-active").addClass("is-completed");
+                    $("a[step='3']").addClass("is-active");
+                } else {
+                    // Restore the not complete state of the steps
+                    $("a[step='2']").removeClass("is-completed").addClass("is-active");
+                    $("a[step='3']").removeClass("is-active");
+                }
+            });
+            relocale();
+        });
+    }
+    initNewRAIDDiskList();
+
+    /* RAID type selection */
+    $(".raid_type").click(function() {
+        var raid_type = $(this).text();
+        var raid_type_value = $(this).val();
+        $(".raid_type_selected").text(raid_type);
+        $(".raid_type_selected").val(raid_type_value);
+        $("#new_raid_type_select").removeClass("is-active");
+        $("#new_raid_type_select").removeAttr("data-dropdown");
+        $("#new_raid_type_select").attr("data-dropdown", "new_raid_type_select");
+        $(".raid_type").removeClass("is-active");
+        $(this).addClass("is-active");
+        $(".raid_type").removeAttr("aria-pressed");
+        $(this).attr("aria-pressed", "true");
+        $(".raid_type").removeAttr("aria-selected");
+        $(this).attr("aria-selected", "true");
+        $(".raid_type").removeAttr("aria-expanded");
+        $(this).attr("aria-expanded", "true");
+        $(".raid_type").removeAttr("aria-hidden");
+        $(this).attr("aria-hidden", "false");
+
+        // Mark step 3 as completed and step 2 as active
+        $("a[step='3']").removeClass("is-active").addClass("is-completed");
+        $("a[step='2']").addClass("is-active");
+    });
+</script>

+ 15 - 0
web/index.html

@@ -27,6 +27,21 @@
             right: 1em;
             z-index: 9999;
         }
+
+        @keyframes fadeIn {
+            from {
+                opacity: 0;
+            }
+            to {
+                opacity: 1;
+            }
+        }
+
+        dialog[open] {
+            animation: fadeIn 0.3s ease-in-out;
+        }
+
+        
     </style>
     <script>
         //Add a new function to jquery for ajax override with csrf token injected

+ 3 - 1
web/js/locale.js

@@ -58,12 +58,14 @@ function i18nc(key, language=undefined){
 let translatedMessages = {
     'en': {
         'disk_info_refreshed': 'Disk information reloaded',
-        "raid_resync_started_succ": 'RAID resync started successfully',
+        "raid_resync_started_succ": 'RAID resync started',
         "raid_device_updated_succ": 'RAID device status reloaded',
+        "raid_reassemble_started_succ": 'RAID config reloaded',
     },
     'zh': {
         'disk_info_refreshed': '磁碟資訊已重新載入',
         "raid_resync_started_succ": 'RAID 重建已成功啟動',
         "raid_device_updated_succ": 'RAID 裝置狀態已重新載入',
+        "raid_reassemble_started_succ": 'RAID 配置已重新載入',
     }
 };