Всем привет!


В этой статье мы расскажем о том, как мы автоматизировали задачу по расширению дискового пространства на одном из наших серверов. А чего сложного в такой простой задаче, что пришлось ее автоматизировать — спросите вы? Ничего, если вы не используете каскадно-объединённое монтирование. Чувствую, вопросов стало больше!? Ну тогда поехали под кат.

Вначале расскажу, для чего мы используем каскадно-объединённое монтирование.

Есть у нас одна система, которой нужно хранилище для маленьких файлов (сканы документов и т.д.). Средний размер файла от 200кб до 1 мегабайта, данные статичны, не меняются. Файлов в нем — миллиарды и каждый день количество растет. Однажды, когда объем уже был более 6тб, мы поняли что скоро начнутся проблемы, одна из которых – время бэкапа и восстановления. Тогда мы решили дробить данные по дискам, а помочь нам в этом был призван UnionFS.

Алгоритм определили следующий: данные пишутся на диск не более 2ТБ, когда он заканчивается мы добавляем виртуальной машине новый диск, размечаем, добавляем его в UnionFS, старый переводим в ReadOnly, снимаем с него копию, пишем на ленту, снимаем с оперативного бэкапа.

Как Вы уже поняли, данный алгоритм достаточно требователен к вниманию администратора – любое неловкое движение и хранилище не доступно. Поэтому решили исключить человеческий фактор полностью и вспомнили что у нас есть ZABBIX, который вполне может справиться с этим сам если в алгоритм добавить немного магии PowerShell и Bash.

Теперь о том, как это сделано.

В Zabbix настроен триггер на свободное пространство и сделана кнопка для ручного режима:

image

При срабатывании триггера формируется задача в шедуллере сервера-робота на котором расположены все наши скрипты автоматизации:

Powershell.exe "Enable-ScheduledTask \PROD_TASKS\Add_HDD_OS0226”

В назначенное время на сервере запускается скрипт который:

Добавляет диск нужной ВМ:
(при этом он выбирает самый свободный том)

$vm = Get-VM -Name $vmName
New-HardDisk -VM $vm -CapacityGB $newHDDCapacity -Datastore $datastoreName –ThinProvisioned

Ищет реквизиты доступа к серверу:
ОФФТоп
У нас используется кастомизированное хранилище реквизитов доступа на базе TeamPass, поэтому скрип находит нужный сервер в системе и получает его реквизиты автоматически. Так сделано потому что каждый месяц у нас происходит автоматическая смена всех паролей, но это тема отдельной статьи

#Generate TeamPass API request string
$vmTPReq = "Строка запроса к TeamPass"
#Send request to TeamPass
$vmCreds = Invoke-WebRequest($vmTPReq) -UseBasicParsing | ConvertFrom-Json
#Convert credentials
$credential = New-Object System.Management.Automation.PSCredential($vmCreds.login,(ConvertTo-SecureString $vmCreds.pw -asPlainText -Force)) 

Заходит по SSH:

#Create partition & FS, mount disk to directory, edit fstab...etc.
New-SSHSession -ComputerName $vmCreds.url -Credential $credential -Verbose -AcceptKey:$true
$results = Invoke-SSHCommand -SessionId 0 -Command "/mnt/autodoit.sh"
Remove-SSHSession -Index 0 -Verbose

Размечает его и добавляет в монтирование UnionFS:
(autodoit.sh)
#!/bin/bash

fstab="/etc/fstab"

newdisk=$((
(
parted -lm  >&1
) 1>/tmp/gethddlog
) 2>&1)

newdisk=$(echo $newdisk | cut -d ':' -f 2)

if [[ $newdisk == "" ]] ;
then
    printf "New disk not found! Exit\n".
    exit
fi

printf "New disk found: $newdisk\n"
echo

    #Create new partition
    echo Create new partition ...
    parted $newdisk mklabel gpt unit TB mkpart primary 0.00TB 2.00TB print

    sleep 10

    #Create filesystem
    echo Create filesystem xfs ...
    newdisk="$newdisk$((1))"
    mkfs.xfs $newdisk

    #Create new DATA directory
    lastdata=$(ls /mnt/ | grep 'data[0-9]\+$' | cut -c5- | sort -n | tail -n1)
    lastdatamount="/mnt/data$((lastdata))"
    newdata="/mnt/data$((lastdata+1))"
    printf "Create new direcory: $newdata\n"
    mkdir $newdata
    chown -R nobody:nobody $newdata
    chmod -R 755 $newdata

    #Mount new partition to new directory
    printf "Mount new partition to $newdata...\n"
    mount -t xfs ${newdisk} ${newdata}

    #Get UUID of new partition
    uuid=$(blkid $newdisk -o value -s UUID)
    printf "New disk UUID: $uuid\n"

    #Add mountpoint for new partition
    printf "Add mountpoint for new disk to fstab...\n"
    lastdatamount=$(cat $fstab | grep "$lastdatamount\s")
    newdatamount="UUID=$uuid $newdata xfs defaults,nofail 0 0"
    ldm=$(echo $lastdatamount | sed -r 's/[\/]/\\\//g')
    ndm=$(echo $newdatamount | sed -r 's/[\/]/\\\//g')
    sed -i "/$ldm/a $ndm" $fstab

    #Change UnionFS mountpoint string
    printf "Modify mountpoint for UnionFS in fstab...\n"
    prevunion=$(cat $fstab | grep fuse.unionfs)
    newunion=$(echo $prevunion | sed -e "s/=rw/=ro/")
    newunion=$(echo $newdata=rw:$newunion)
    sed -i "s|$prevunion|$newunion|" $fstab

    #Remount UnionFS
    printf "Remount UnionFS...\n"
    service smb stop
    sleep 0.6
    umount /mnt/unionfs
    mount /mnt/unionfs
    service smb start

    printf "Done!\n\n"

rm /tmp/gethddlog


К сожалению, на момент написания статьи мы не решили несколько вопросов, связанных с автоматическим созданием заданий в VEEAM для архивации старого диска и записи его на ленту, поэтому пока это происходит вручную. Но мы обязательно обновим скрипт как только решим пару задачек.

Автор — Виталий Розман (PBCVIT).

Кусочек кода по склеиванию массивов был честно позаимствован, ссылки в коде на автора сохранены.

Полный скрипт
#Set VM name
$vmName = "OS0226"
#Set TeamPass ID of linux server
$vmTPId = "1161"
#Set capacity of new HDD in GB
$newHDDCapacity = 2048
#Set Log file
$logFile = "C:\SCRIPTS\Log\NewHardDisk-OS0226.log"

#Import module for SSH connections
Import-Module Posh-SSH
#Add VEEAM Snap-In
Add-PSSnapin VeeamPSSnapin

#Initialize VMWare PowerCLI
& 'C:\Program Files (x86)\VMware\Infrastructure\PowerCLI\Scripts\Initialize-PowerCLIEnvironment.ps1'

#Add function for array join
Function Join-Object {                                          # https://powersnippets.com/join-object/
    [CmdletBinding()]Param (                                    # Version 02.02.00, by iRon
        [Object[]]$RightTable, [Alias("Using")]$On, $Merge = @{}, [Parameter(ValueFromPipeLine = $True)][Object[]]$LeftTable, [String]$Equals
    )
    $Type = ($MyInvocation.InvocationName -Split "-")[0]
    $PipeLine = $Input | ForEach {$_}; If ($PipeLine) {$LeftTable = $PipeLine}
    If ($LeftTable -eq $Null) {If ($RightTable[0] -is [Array]) {$LeftTable = $RightTable[0]; $RightTable = $RightTable[-1]} Else {$LeftTable = $RightTable}}
    $DefaultMerge = If ($Merge -is [ScriptBlock]) {$Merge; $Merge = @{}} ElseIf ($Merge."") {$Merge.""} Else {{$Left.$_, $Right.$_}}
    If ($Equals) {$Merge.$Equals = {If ($Left.$Equals -ne $Null) {$Left.$Equals} Else {$Right.$Equals}}}
    ElseIf ($On -is [String] -or $On -is [Array]) {@($On) | ForEach {If (!$Merge.$_) {$Merge.$_ = {$Left.$_}}}}
    $LeftKeys  = @($LeftTable[0].PSObject.Properties  | ForEach {$_.Name})
    $RightKeys = @($RightTable[0].PSObject.Properties | ForEach {$_.Name})
    $Keys = $LeftKeys + $RightKeys | Select -Unique
    $Keys | Where {!$Merge.$_} | ForEach {$Merge.$_ = $DefaultMerge}
    $Properties = @{}; $LeftOut = @($True) * @($LeftTable).Length; $RightOut = @($True) * @($RightTable).Length
    For ($LeftIndex = 0; $LeftIndex -lt $LeftOut.Length; $LeftIndex++) {$Left = $LeftTable[$LeftIndex]
        For ($RightIndex = 0; $RightIndex -lt $RightOut.Length; $RightIndex++) {$Right = $RightTable[$RightIndex]
            $Select = If ($On -is [String]) {If ($Equals) {$Left.$On -eq $Right.$Equals} Else {$Left.$On -eq $Right.$On}}
            ElseIf ($On -is [Array]) {($On | Where {!($Left.$_ -eq $Right.$_)}) -eq $Null} ElseIf ($On -is [ScriptBlock]) {&$On} Else {$True}
            If ($Select) {$Keys | ForEach {$Properties.$_ = 
                    If ($LeftKeys -NotContains $_) {$Right.$_} ElseIf ($RightKeys -NotContains $_) {$Left.$_} Else {&$Merge.$_}
                }; New-Object PSObject -Property $Properties; $LeftOut[$LeftIndex], $RightOut[$RightIndex] = $Null
    }   }   }
    If ("LeftJoin",  "FullJoin" -Contains $Type) {
        For ($LeftIndex = 0; $LeftIndex -lt $LeftOut.Length; $LeftIndex++) {
            If ($LeftOut[$LeftIndex]) {$Keys | ForEach {$Properties.$_ = $LeftTable[$LeftIndex].$_}; New-Object PSObject -Property $Properties}
    }   }
    If ("RightJoin", "FullJoin" -Contains $Type) {
        For ($RightIndex = 0; $RightIndex -lt $RightOut.Length; $RightIndex++) {
            If ($RightOut[$RightIndex]) {$Keys | ForEach {$Properties.$_ = $RightTable[$RightIndex].$_}; New-Object PSObject -Property $Properties}
    }   }
};
Set-Alias Join   Join-Object
Set-Alias InnerJoin Join-Object; Set-Alias InnerJoin-Object Join-Object -Description "Returns records that have matching values in both tables"
Set-Alias LeftJoin  Join-Object; Set-Alias LeftJoin-Object  Join-Object -Description "Returns all records from the left table and the matched records from the right table"
Set-Alias RightJoin Join-Object; Set-Alias RightJoin-Object Join-Object -Description "Returns all records from the right table and the matched records from the left table"
Set-Alias FullJoin  Join-Object; Set-Alias FullJoin-Object  Join-Object -Description "Returns all records when there is a match in either left or right table"


#Connect to vCenter
Connect-VIServer vcenter.mmc.local

#Get datastore
$datastores = get-datastore | where-object Name -like "*TIERED_VM_PROD*"

if ($datastores.Count -gt 0) {
    if (($datastores | Sort -Descending {$_.FreeSpaceGB})[0].FreeSpaceGB -gt 2048) {
        $datastoreName = ($datastores | Sort -Descending {$_.FreeSpaceGB})[0].Name
    } else {
        Write-Host("ERROR: No enought space on datastore for new HDD!")
        break
    }
} else {
    Write-Host("ERROR: No Datastores found!")
    break
}


#Generate TeamPass API request string
$vmTPReq = "строка запроса к TeamPass"

#Send request to TeamPass
$vmCreds = Invoke-WebRequest($vmTPReq) -UseBasicParsing | ConvertFrom-Json
#Convert credentials
$credential = New-Object System.Management.Automation.PSCredential($vmCreds.login,(ConvertTo-SecureString $vmCreds.pw -asPlainText -Force))


if ((Test-Connection $vmCreds.url -Count 1 -Quiet) -eq $false) { $err = $error[0].FullyQualifiedErrorId }
try
{
    # Get disks information from Linux
    New-SSHSession -ComputerName $vmCreds.url -Credential $credential -Verbose -AcceptKey:$true
    $linuxCommand1 = 'ls -dl  /sys/block/sd*/device/scsi_device/*'
    $linuxDisksData1 = Invoke-SSHCommand -SessionId 0 -Command $linuxCommand1
    $linuxCommand2 = 'lsblk -l | grep /mnt'
    $linuxDisksData2 = Invoke-SSHCommand -SessionId 0 -Command $linuxCommand2
    Remove-SSHSession -Index 0 -Verbose

    $linuxMounts = $linuxDisksData2.Output -replace '\s+', ' ' |
    Select  @{N='Partition';E={($_.split(" ")[0])}},
            @{N='linuxMount';E={($_.split(" ")[6])}}

    $linuxDisks = $linuxDisksData1.Output -replace '\s+', ' ' |
    Select  @{N='Partition';E={($_.split(" ")[8]).split('/')[3]+'1'}},
            @{N='SCSIAddr';E={(($_.split(" ")[8]).split('/')[6]).split(':')[1]+':'+(($_.split(" ")[8]).split('/')[6]).split(':')[2]}}

    $linuxDisks = $linuxDisks | sort SCSIAddr
}
catch
{
    $err = $error[0].FullyQualifiedErrorId
}

#Get disks information from vmware
$vmDisks = Get-VM -Name $vmName | Get-HardDisk |
Select @{N='vmwareHardDisk';E={$_.Name}},
       @{N='vSCSI';E={$_.uid.split("/")[3].split("=")[1]}},
       @{N='SCSIAddr';E={[string]([math]::truncate((($_.uid.split("/")[3].split("=")[1])-2000)/16))+':'+[string]((($_.uid.split("/")[3].split("=")[1])-2000)%16)}}
$vmDisks = $vmDisks | sort SCSIAddr

#Get total information about VM Disks
$OLAYtotalEffects = $vmDisks | InnerJoin $linuxDisks SCSIAddr -eq SCSIAddr | InnerJoin $linuxMounts Partition -eq Partition| sort vmwareHardDisk

#Display total information about VM Disks
$OLAYtotalEffects | ft
$OLAYtotalEffects | ft 2>$logFile

#Get latest mount
$linuxLatestDiskMount = [string](($OLAYtotalEffects | select linuxMount | where linuxMount -like "/mnt/data*" | % {[int](($_.linuxMount.Split("/")[2]).Replace("data",""))} | Measure -Maximum).Maximum)
#Get latest vSCSI number
$latestDiskvSCSI = ($OLAYtotalEffects | where {$_.linuxMount -eq "/mnt/data$linuxLatestDiskMount"}).vSCSI


#Add HDD to VM
$vm = Get-VM -Name $vmName
New-HardDisk -VM $vm -CapacityGB $newHDDCapacity -Datastore $datastoreName -ThinProvisioned

#Let the disk takes root
Write-Host("Let the disk takes root...")
sleep 10

if ((Test-Connection $vmCreds.url -Count 1 -Quiet) -eq $false) { $err = $error[0].FullyQualifiedErrorId }
try
{
    #Create partition & FS, mount disk to directory, edit fstab...etc.
    New-SSHSession -ComputerName $vmCreds.url -Credential $credential -Verbose -AcceptKey:$true
    $results = Invoke-SSHCommand -SessionId 0 -Command "/mnt/autodoit.sh"
    Remove-SSHSession -Index 0 -Verbose
    $results.Output
}
catch
{
    $err = $error[0].FullyQualifiedErrorId
}


#After adding a new disk, some checks are just performed
if ((Test-Connection $vmCreds.url -Count 1 -Quiet) -eq $false) { $err = $error[0].FullyQualifiedErrorId }
try
{
    # Get disks information from Linux
    New-SSHSession -ComputerName $vmCreds.url -Credential $credential -Verbose -AcceptKey:$true
    $linuxCommand1 = 'ls -dl  /sys/block/sd*/device/scsi_device/*'
    $linuxDisksData1 = Invoke-SSHCommand -SessionId 0 -Command $linuxCommand1
    $linuxCommand2 = 'lsblk -l | grep /mnt'
    $linuxDisksData2 = Invoke-SSHCommand -SessionId 0 -Command $linuxCommand2
    Remove-SSHSession -Index 0 -Verbose

    $linuxMounts = $linuxDisksData2.Output -replace '\s+', ' ' |
    Select  @{N='Partition';E={($_.split(" ")[0])}},
            @{N='linuxMount';E={($_.split(" ")[6])}}

    $linuxDisks = $linuxDisksData1.Output -replace '\s+', ' ' |
    Select  @{N='Partition';E={($_.split(" ")[8]).split('/')[3]+'1'}},
            @{N='SCSIAddr';E={(($_.split(" ")[8]).split('/')[6]).split(':')[1]+':'+(($_.split(" ")[8]).split('/')[6]).split(':')[2]}}

    $linuxDisks = $linuxDisks | sort SCSIAddr
}
catch
{
    $err = $error[0].FullyQualifiedErrorId
}

#Get disks information from vmware
$vmDisks = Get-VM -Name $vmName | Get-HardDisk |
Select @{N='vmwareHardDisk';E={$_.Name}},
       @{N='vSCSI';E={$_.uid.split("/")[3].split("=")[1]}},
       @{N='SCSIAddr';E={[string]([math]::truncate((($_.uid.split("/")[3].split("=")[1])-2000)/16))+':'+[string]((($_.uid.split("/")[3].split("=")[1])-2000)%16)}}
$vmDisks = $vmDisks | sort SCSIAddr

#Get total information about VM Disks
$OLAYtotalEffects = $vmDisks | InnerJoin $linuxDisks SCSIAddr -eq SCSIAddr | InnerJoin $linuxMounts Partition -eq Partition| sort vmwareHardDisk

#Display total information about VM Disks
$OLAYtotalEffects | ft
$OLAYtotalEffects | ft 2>$logFile

Disconnect-VIServer -Confirm:$false
Disable-ScheduledTask \PROD_TASKS\Add_HDD_OS0226


Претензий к UnionFS нету, работает стабильно более двух лет.

Вопрос о том, почему так организовано хранение в целом, оставим риторическим, просто примите как есть.

Обращаю ваше внимание, что для разных систем должны использоваться разные типы сопоставления дисков. поэтому будьте внимательны и прибудет с вами сила.

Комментарии (2)


  1. amarao
    13.02.2019 17:34

    А почему вы для этого используете локальные файлы в локальных дисках, а не объектное хранилище?


  1. Hardened
    15.02.2019 20:53

    Предположу. Простой бэкенд. Не нужно допиливать поддержку ещё одного протокола доступа.