Всем привет!
В этой статье мы расскажем о том, как мы автоматизировали задачу по расширению дискового пространства на одном из наших серверов. А чего сложного в такой простой задаче, что пришлось ее автоматизировать — спросите вы? Ничего, если вы не используете каскадно-объединённое монтирование. Чувствую, вопросов стало больше!? Ну тогда поехали под кат.
Вначале расскажу, для чего мы используем каскадно-объединённое монтирование.
Есть у нас одна система, которой нужно хранилище для маленьких файлов (сканы документов и т.д.). Средний размер файла от 200кб до 1 мегабайта, данные статичны, не меняются. Файлов в нем — миллиарды и каждый день количество растет. Однажды, когда объем уже был более 6тб, мы поняли что скоро начнутся проблемы, одна из которых – время бэкапа и восстановления. Тогда мы решили дробить данные по дискам, а помочь нам в этом был призван UnionFS.
Алгоритм определили следующий: данные пишутся на диск не более 2ТБ, когда он заканчивается мы добавляем виртуальной машине новый диск, размечаем, добавляем его в UnionFS, старый переводим в ReadOnly, снимаем с него копию, пишем на ленту, снимаем с оперативного бэкапа.
Как Вы уже поняли, данный алгоритм достаточно требователен к вниманию администратора – любое неловкое движение и хранилище не доступно. Поэтому решили исключить человеческий фактор полностью и вспомнили что у нас есть ZABBIX, который вполне может справиться с этим сам если в алгоритм добавить немного магии PowerShell и Bash.
Теперь о том, как это сделано.
В Zabbix настроен триггер на свободное пространство и сделана кнопка для ручного режима:
При срабатывании триггера формируется задача в шедуллере сервера-робота на котором расположены все наши скрипты автоматизации:
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 нету, работает стабильно более двух лет.
Вопрос о том, почему так организовано хранение в целом, оставим риторическим, просто примите как есть.
Обращаю ваше внимание, что для разных систем должны использоваться разные типы сопоставления дисков. поэтому будьте внимательны и прибудет с вами сила.