Napi jelentések a virtuális gépek állapotáról R és PowerShell segítségével

Napi jelentések a virtuális gépek állapotáról R és PowerShell segítségével

Bevezetés

Jó napot! Már fél éve fut egy szkriptünk (pontosabban szkriptek gyűjteménye), amely virtuális gépek (és nem csak) állapotjelentéseket generál. Úgy döntöttem, hogy megosztom a tapasztalataimat a létrehozásról és a kódról. Számítok a kritikára, és bízom benne, hogy ez az anyag valakinek hasznos lehet.

Igény kialakítása

Sok virtuális gépünk van (körülbelül 1500 VM, amelyek 3 vCenter-en oszlanak meg). Új gépek jönnek létre és régiek törlődnek elég gyakran. A rend fenntartása érdekében néhány egyedi mezőt adtunk hozzá a vCenterhez, hogy elkülönítsük a VM-eket az Alrendszerek szerint, jelezzük, hogy tesztelés alatt állnak-e, valamint, hogy ki és mikor hozta létre őket. Az emberi tényező oda vezetett, hogy a gépek több mint fele üres mezőkkel maradt, ami megnehezítette a munkát. Félévente valaki dühös lett, és elkezdte az adatok aktualizálását, de az eredmény másfél hét múlva már nem volt aktuális.
Közvetlenül tisztázom, hogy mindenki tudja, hogy létezniük kellene a gépek létrehozására vonatkozó kéréseknek, a folyamatnak stb. És közben mindenki maradéktalanul követi ezt a folyamatot és rendben van minden. Nálunk sajnos nem így van, de ez nem a cikk tárgya 🙂

Összességében úgy döntöttünk, hogy automatizáljuk a mezők helyességének ellenőrzését.
Úgy határoztunk, hogy napi e-mailt küldünk a helytelenül kitöltött gépek listájával minden érintett mérnöknek és azok feletteseinek, ami jó kezdet lenne.

Jelen pillanatban egy kollégánk már bevezetett egy PowerShell szkriptet, amely naponta ütemezve gyűjtötte az információt az összes gépről az összes vCenterben, és 3 csv dokumentumot készített (mindegyik saját vCenteréhez), amelyeket megosztottak egy közös meghajtón. Úgy döntöttek, hogy ezt a szkriptet alapként veszik és R nyelven kiegészítik ellenőrzésekkel, amiből volt némi tapasztalat.

A fejlesztés folyamán a megoldás kiegészült e-mail értesítésekkel, egy adatbázissal, amely tartalmazza az alap és a történeti táblát (erről később), valamint a vSphere naplók elemzésével, hogy megtalálják a VM tényleges létrehozóit és azok létrehozásának időpontját.

A fejlesztéshez az RStudio Desktop és a PowerShell ISE IDE-ket használták.

A szkript egy szokványos Windows virtuális gépről indítható.

Áttekintés a logika általános felépítéséről.

A szkriptek általános logikája a következőképpen alakult.

  • Virtuális gépekkel kapcsolatos adatokat gyűjtünk egy PowerShell szkript segítségével, amelyet R-en keresztül hívunk meg, és az eredményeket egy csv fájlba egyesítjük. A két nyelv közötti visszacsatolás hasonló módon működik. (Az adatok R-ből közvetlenül PowerShell-be is átkonvertálhatók változókként, de ez bonyolultabb, és a köztes csv-k használata megkönnyíti a hibakeresést és az eredmények megosztását másokkal).
  • Az R segítségével képezzük a mezők érvényes paramétereit, amelyek értékeit ellenőrizzük. — Word dokumentumot készítünk, amely az említett mezők értékeit tartalmazza a munkatársak kérdéseire adott válasz információs levélbe történő beillesztéshez: "De hát hogy kell ezt kitöltenem?".
  • Az összes virtuális gép adatait csv-ből tölthetjük be R segítségével, létrehozunk egy dataframe-et, eltávolítjuk a felesleges mezőket, és készítünk egy információs xlsx dokumentumot, amely az összes virtuális gépről összesített információt tartalmaz, és közzétesszük egy közös erőforráson.
  • A dataframe-et az összes virtuális gép esetén alkalmazzuk az összes mező kitöltésének helyességi ellenőrzésére, és létrehozunk egy táblázatot, amely csak a helytelenül kitöltött mezőket tartalmazó virtuális gépeket tartalmazza (és csak ezeket a mezőket).
  • A kapott virtuális gépek listáját egy másik PowerShell szkriptnek küldjük, amely a vCenter naplókban keres a virtuális gépek létrehozásának eseményei után, lehetővé téve a virtuális gép létrehozásának előrejelzett időpontjának és az előrejelzett alkotónak a megadását. Arra az esetre, ha senki sem ismeri el, hogy kié a gép. Ez a szkript nem fut gyorsan, különösen, ha sok napló áll rendelkezésre, ezért csak az utolsó 2 hetet nézzük, és workflow-t is használunk, amely lehetővé teszi, hogy párhuzamosan keressünk információkat több virtuális gép esetén. A szkript példájában részletes megjegyzések találhatóak e mechanizmusról. Az eredményt egy csv fájlba helyezzük, amelyet ismét R-be töltünk.
  • Szép formázott xlsx dokumentumot készítünk, amelyben piros színnel kiemeljük a helytelenül kitöltött mezőket, szűrőket alkalmazunk néhány oszlopra, és további oszlopok is szerepelnek, amelyek a feltételezett alkotókat és a virtuális gépek létrehozásának időpontját tartalmazzák.
  • Egy elektronikus levelet készítünk, amelyhez csatolunk egy dokumentumot, amely a mezők elfogadható értékeit írja le, valamint egy táblázatot a hibásan kitöltött VM-ekről. A szövegben megadjuk a hibásan létrehozott VM-ek összes számát, egy linket az általános erőforráshoz és egy motiváló képet. Ha nincsenek hibásan kitöltött VM-ek, akkor egy másik levelet küldünk egy kedvezőbb motiváló képpel.
  • Rögzítjük az összes VM adatait az SQL Server adatbázisban, figyelembe véve a történeti táblák korszerűsített mechanizmusát (nagyon érdekes mechanizmus — amiről részletesebben később).

Tulajdonképpen a szkriptek.

Fő fájl R kódra.

# Путь к рабочей директории (нужно для корректной работы через виндовый планировщик заданий)
setwd("C:ScriptsgetVm")

#### Подгружаем необходимые пакеты ####
library(tidyverse)
library(xlsx)
library(mailR)
library(rmarkdown)

##### Определяем пути к исходным файлам и другие переменные #####
source(file = "const.R", local = T, encoding = "utf-8")

# Проверяем существование файла со всеми ВМ и удаляем, если есть.
if (file.exists(filenameVmCreationRules)) {file.remove(filenameVmCreationRules)}

#### Создаём вордовский документ с допустимыми полями
render("VM_name_rules.Rmd",
       output_format = word_document(),
       output_file = filenameVmCreationRules)

# Проверяем существование файла со всеми ВМ и удаляем, если есть
if (file.exists(allVmXlsxPath)) {file.remove(allVmXlsxPath)}

#### Забираем данные по всем машинам через PowerShell скрипт. На выходе получим csv.
system(paste0("powershell -File ", getVmPsPath))

# Полный df
fullXslx_df <- allVmXlsxPath %>% 
  read.csv2(stringsAsFactors = FALSE)

# Ellenőrizzük a kitöltött mezők helyességét
full_df <- fullXslx_df %>%
  mutate(
    # Először eltávolítjuk az összes felesleges szóközt és tabulációt, majd figyelembe vesszük a vesszőt mint elválasztót, végül ellenőrizzük a megengedett értékekbe való tartozást,
    isSubsystemCorrect = Subsystem %&gt;% 
      gsub("[[:space:]]", "", .) %&gt;% 
      str_split(., ",") %&gt;% 
      map(function(x) (all(x %in% AllowedValues$Subsystem))) %&gt;%
      as.logical(),
    isOwnerCorrect = Owner %in% AllowedValues$Owner,
    isCategoryCorrect = Category %in% AllowedValues$Category,
    isCreatorCorrect = (!is.na(Creator) &amp; Creator != ''),
    isCreation.DateCorrect = map(Creation.Date, IsDate)
  )

# Ellenőrizzük, hogy létezik-e a fájl az összes VM-mel, és töröljük, ha létezik.
if (file.exists(filenameAll)) {file.remove(filenameAll)}

#### XSLX fájl készítése a jelentéshez ####
# Általános adatok külön lapra
full_df %&gt;% write.xlsx(file=filenameAll,
                       sheetName=names[1],
                       col.names=TRUE,
                       row.names=FALSE,
                       append=FALSE)

#### XSLX fájl készítése a hibásan kitöltött mezőkről ####
# DF kialakítása
incorrect_df <- full_df %>%
  select(VM.Name, 
         IP.s, 
         Owner,
         Subsystem,
         Creator,
         Category,
         Creation.Date,
         isOwnerCorrect, 
         isSubsystemCorrect, 
         isCategoryCorrect,
         isCreatorCorrect,
         vCenter.Name) %&gt;%
  filter(isSubsystemCorrect == F | 
           isOwnerCorrect == F |
           isCategoryCorrect == F |
           isCreatorCorrect == F)

# Ellenőrizzük, hogy létezik-e a fájl az összes VM-mel, és töröljük, ha létezik.
if (file.exists(filenameIncVM)) {file.remove(filenameIncVM)}

# A hiányos mezőkkel rendelkező VM-ek lista elmentése CSV-be
incorrect_df %&gt;%
  select(VM.Name) %&gt;%
  write_csv2(path = filenameIncVM, append = FALSE)

# Szűrés beillesztéshez az emailbe
incorrect_df_filtered <- incorrect_df %>% 
  select(VM.Name, 
         IP.s, 
         Owner, 
         Subsystem, 
         Category,
         Creator,
         vCenter.Name,
         Creation.Date
  )

# Sorok számának kiszámítása
numberOfRows <- nrow(incorrect_df)

#### Начало условия ####
# Дальше либо у нас есть неправильно заполненные поля, либо нет.
# Если есть - запускаем ещё один скрипт

if (numberOfRows > 0) {

  # Ellenőrizzük, hogy létezik-e a fájl a készítőkről, és töröljük, ha létezik.
  if (file.exists(creatorsFilePath)) {file.remove(creatorsFilePath)}

  # PowerShell szkript indítása, amely megkeresi a megtalált VM-ek készítőit. A kimenet egy CSV lesz.
  system(paste0("powershell -File ", getCreatorsPath))

  # Készítők fájl beolvasása
  creators_df <- creatorsFilePath %>%
    read.csv2(stringsAsFactors = FALSE)

  # Szűrés beillesztéshez az emailbe, adatokat adunk hozzá a készítők táblázatából
  incorrect_df_filtered <- incorrect_df_filtered %>% 
    select(VM.Name, 
           IP.s, 
           Owner, 
           Subsystem, 
           Category,
           Creator,
           vCenter.Name,
           Creation.Date
    ) %&gt;% 
    left_join(creators_df, by = "VM.Name") %&gt;% 
    rename(`Előrelátható készítő` = CreatedBy, 
           `Előrelátható létrehozási dátum` = CreatedOn)  

  # Az email törzsének kialakítása
  emailBody &lt;- paste0(
    &#039;<html>
                    <h3>Jó napot kívánok, tisztelt kollégák.</h3>
                    <p>A virtuális gépekkel kapcsolatos teljes és aktuális információt a H: meghajtón találhatják itt:<p>
                    <p>\server.ruVM', sourceFileFormat, '</p>
                    <p>A mellékletben található a virtuális gépek listája, <strong>amelyek hibásan vannak kitöltve.</strong> Összesen <strong>', numberOfRows, '</strong>.</p>
                    <p>A táblázatban két további oszlop jelent meg. <strong>Valószínűsíthető készítő</strong> és <strong>Valószínűsíthető létrehozási dátum</strong>, amelyeket az utolsó két hét vCenter naplójából nyerünk ki.</p>
                    <p>Kérjük a gépek készítőit, hogy pontosítsák az adatokat és helyesen töltsék ki a mezőket. A mezők kitöltésének szabályai szintén a mellékletben találhatók.</p>
                    <p><img src="data/meme.jpg"></p>
                    </html>'
  )

  # Ellenőrizzük a fájl létezését
  if (file.exists(filenameIncorrect)) {file.remove(filenameIncorrect)}

  # Készítünk egy szép táblázatot formátumokkal stb.
  source(file = "email.R", local = T, encoding = "utf-8")

  #### Készítünk egy levelet a rosszul aláírt gépekről ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "VM-ek hibásan kitöltött mezőkkel",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            attach.files = c(filenameIncorrect, filenameVmCreationRules),
            debug = FALSE)

  #### Továbbiakban következik blokk, ha nincs probléma a VM-ekkel ####
} else {

  # Készítjük a levél testét
  emailBody &lt;- paste0(
    &#039;<html>
    <h3>Jó napot kívánok, kedves kollégák.</h3>
   <p>A virtuális gépekkel kapcsolatos teljes és aktuális információt a H: meghajtón találhatják itt:<p>
    <p>\server.ruVM', sourceFileFormat, '</p>
    <p>Jelenleg az összes mező a VM-en helyesen van kitöltve.</p>
    <p><img src="data/meme_correct.jpg"></p>
    </html>'
  )

  #### Készítünk egy levelet rosszul kitöltött VM-ek nélkül ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "Összesítő információ",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            debug = FALSE)
}

####### Adatok rögzítése az adatbázisba #####

source(file = "DB.R", local = T, encoding = "utf-8")

VM lista lekérdezése PowerShell-en keresztül.

# Данные для подключения и другие переменные
$vCenterNames = @(
                    "vcenter01", 
                    "vcenter02", 
                    "vcenter03"
                    )
$vCenterUsername = "myusername"
$vCenterPassword = "mypassword"

$filename = "C:ScriptsgetVmdataallvmall-vm-$(get-date -f yyyy-MM-dd).csv"

$destinationSMB = "server.rumyfolder$vm"
$IP0=""
$IP1=""
$IP2=""
$IP3=""
$IP4=""
$IP5=""

# Подключение ко всем vCenter, что содержатся в переменной. Будет работать, если логин и пароль одинаковые (например, доменные)
Connect-VIServer -Server $vCenterNames -User $vCenterUsername -Password $vCenterPassword

write-host ""

# Создаём функцию с циклом по всем vCenter-ам
function Get-VMinventory {

# В этой переменной будет списко всех ВМ, как объектов
$AllVM = Get-VM | Sort Name
$cnt = $AllVM.Count
$count = 1

# Начинаем цикл по всем ВМ и собираем необходимые параметры каждого объекта
   foreach ($vm in $AllVM) {
   $StartTime = $(get-date)

     $IP0 = $vm.Guest.IPAddress[0]
     $IP1 = $vm.Guest.IPAddress[1]
     $IP2 = $vm.Guest.IPAddress[2]
     $IP3 = $vm.Guest.IPAddress[3]
     $IP4 = $vm.Guest.IPAddress[4]
     $IP5 = $vm.Guest.IPAddress[5]

     If ($IP0 -ne $null) {If ($IP0.Contains(":") -ne 0) {$IP0=""}}
     If ($IP1 -ne $null) {If ($IP1.Contains(":") -ne 0) {$IP1=""}}
     If ($IP2 -ne $null) {If ($IP2.Contains(":") -ne 0) {$IP2=""}}
     If ($IP3 -ne $null) {If ($IP3.Contains(":") -ne 0) {$IP3=""}}
     If ($IP4 -ne $null) {If ($IP4.Contains(":") -ne 0) {$IP4=""}}
     If ($IP5 -ne $null) {If ($IP5.Contains(":") -ne 0) {$IP5=""}}

     $cluster = $vm | Get-Cluster | Select-Object -ExpandProperty name  
     $Bootime = $vm.ExtensionData.Runtime.BootTime
     $TotalHDDs = $vm.ProvisionedSpaceGB -as [int]
     $CreationDate = $vm.CustomFields.Item("CreationDate") -as [string]
     $Creator = $vm.CustomFields.Item("Creator") -as [string]
     $Category = $vm.CustomFields.Item("Category") -as [string]
     $Owner = $vm.CustomFields.Item("Owner") -as [string]
     $Subsystem = $vm.CustomFields.Item("Subsystem") -as [string]

     $IPS = $vm.CustomFields.Item("IP") -as [string]

     $vCPU = $vm.NumCpu
     $CorePerSocket = $vm.ExtensionData.config.hardware.NumCoresPerSocket
     $Sockets = $vCPU/$CorePerSocket

     $Id = $vm.Id.Split('-')[2] -as [int]

     # Собираем все параметры в один объект
     $Vmresult = New-Object PSObject
     $Vmresult | add-member -MemberType NoteProperty -Name "Id" -Value $Id   
     $Vmresult | add-member -MemberType NoteProperty -Name "VM Name" -Value $vm.Name  
     $Vmresult | add-member -MemberType NoteProperty -Name "Cluster" -Value $cluster  
     $Vmresult | add-member -MemberType NoteProperty -Name "Esxi Host" -Value $VM.VMHost  
     $Vmresult | add-member -MemberType NoteProperty -Name "IP Address 1" -Value $IP0
     $Vmresult | add-member -MemberType NoteProperty -Name "IP Address 2" -Value $IP1
     $Vmresult | add-member -MemberType NoteProperty -Name "IP Address 3" -Value $IP2
     $Vmresult | add-member -MemberType NoteProperty -Name "IP Address 4" -Value $IP3
     $Vmresult | add-member -MemberType NoteProperty -Name "IP Address 5" -Value $IP4
     $Vmresult | add-member -MemberType NoteProperty -Name "IP Address 6" -Value $IP5
     $Vmresult | add-member -MemberType NoteProperty -Name "vCPU" -Value $vCPU
     $Vmresult | Add-Member -MemberType NoteProperty -Name "CPU Sockets" -Value $Sockets
     $Vmresult | Add-Member -MemberType NoteProperty -Name "Core per Socket" -Value $CorePerSocket
     $Vmresult | add-member -MemberType NoteProperty -Name "RAM (GB)" -Value $vm.MemoryGB
     $Vmresult | add-member -MemberType NoteProperty -Name "Total-HDD (GB)" -Value $TotalHDDs
     $Vmresult | add-member -MemberType NoteProperty -Name "Power State" -Value $vm.PowerState
     $Vmresult | add-member -MemberType NoteProperty -Name "OS" -Value $VM.ExtensionData.summary.config.guestfullname  
     $Vmresult | Add-Member -MemberType NoteProperty -Name "Boot Time" -Value $Bootime
     $Vmresult | add-member -MemberType NoteProperty -Name "VMTools Status" -Value $vm.ExtensionData.Guest.ToolsStatus  
     $Vmresult | add-member -MemberType NoteProperty -Name "VMTools Version" -Value $vm.ExtensionData.Guest.ToolsVersion  
     $Vmresult | add-member -MemberType NoteProperty -Name "VMTools Version Status" -Value $vm.ExtensionData.Guest.ToolsVersionStatus  
     $Vmresult | add-member -MemberType NoteProperty -Name "VMTools Running Status" -Value $vm.ExtensionData.Guest.ToolsRunningStatus  
     $Vmresult | add-member -MemberType NoteProperty -Name "Creation Date" -Value $CreationDate
     $Vmresult | add-member -MemberType NoteProperty -Name "Creator" -Value $Creator
     $Vmresult | add-member -MemberType NoteProperty -Name "Category" -Value $Category
     $Vmresult | add-member -MemberType NoteProperty -Name "Owner" -Value $Owner
     $Vmresult | add-member -MemberType NoteProperty -Name "Subsystem" -Value $Subsystem
     $Vmresult | add-member -MemberType NoteProperty -Name "IP's" -Value $IPS
     $Vmresult | add-member -MemberType NoteProperty -Name "vCenter Name" -Value $vm.Uid.Split('@')[1].Split(':')[0]  

# Считаем общее и оставшееся время выполнения и выводим на экран результаты. Использовалось для тестирования, но по факту оказалось очень удобно.
     $elapsedTime = $(get-date) - $StartTime
     $totalTime = "{0:HH:mm:ss}" -f ([datetime]($elapsedTime.Ticks*($cnt - $count)))

     clear-host
     Write-Host "Processing" $count "from" $cnt 
     Write-host "Progress:" ([math]::Round($count/$cnt*100, 2)) "%" 
     Write-host "You have about " $totalTime "for cofee"
     Write-host ""

     $count++

# Выводим результат, чтобы цикл "знал" что является результатом выполнения одного прохода
     $Vmresult
   }

}

# Вызываем получившуюся функцию и сразу выгружаем результат в csv
$allVm = Get-VMinventory | Export-CSV -Path $filename -NoTypeInformation -UseCulture -Force

# Пытаемся выложить полученный файл в нужное нам место и, в случае ошибки, пишем лог.
try
    {
        Copy-Item $filename -Destination $destinationSMB -Force -ErrorAction SilentlyContinue
    }
catch
    {
        $error | Export-CSV -Path $filename".error" -NoTypeInformation -UseCulture -Force
    }

PowerShell szkript, amely kiemeli a virtuális gépek létrehozóit és létrehozásuk dátumát a naplókból.

# Путь к файлу, из которого будем доставать список VM
$VMfilePath = "C:ScriptsgetVmcreators_VMcreators_VM_$(get-date -f yyyy-MM-dd).csv"
# Путь к файлу, в который будем записывать результат
$filePath = "C:ScriptsgetVmdatacreatorscreators-$(get-date -f yyyy-MM-dd).csv"

# Создаём вокрфлоу
Workflow GetCreators-Wf
{
    # Параметры, которые можно будет передать при вызове скрипта
    param([string[]]$VMfilePath)

# Параметры, которые доступны только внутри workflow
$vCenterUsername = "myusername"
$vCenterPassword = "mypassword"
$daysToLook = 14
$start = (get-date).AddDays(-$daysToLook)
$finish = get-date
# Значения, которые будут вписаны в csv для машин, по которым не будет ничего найдено
$UnknownUser = "UNKNOWN"
$UnknownCreatedTime = "0000-00-00"

# Определяем параметры подключения и выводной файл, которые будут доступны во всём скрипте.
$vCenterNames = @(
                    "vcenter01", 
                    "vcenter02", 
                    "vcenter03"
                    )

# Получаем список VM из csv и загружаем соответствующие объекты
$list = Import-Csv $VMfilePath -UseCulture | select -ExpandProperty VM.Name

# Цикл, который будет выполняться параллельно (по 5 машин за раз)
foreach -parallel ($row in $list)
  {
    # Это скрипт, который видит только свои переменные и те, которые ему переданы через $Using
    InlineScript {

    # Время начала выполнения отдельного блока
        $StartTime = $(get-date)

        Write-Host ""
        Write-Host "Processing $Using:row started at $StartTime"
        Write-Host ""

        # Подключение оборачиваем в переменную, чтобы информация о нём не мешалась в консоли
        $con = Connect-VIServer -Server $Using:vCenterNames -User $Using:vCenterUsername -Password $Using:vCenterPassword

        # Получаем объект vm
        $vm = Get-VM -Name $Using:row

      # Ниже 2 одинаковые команды. Одна с фильтром по времени, вторая - без. Можно пользоваться тем,
      $Event = $vm | Get-VIEvent -Start $Using:start -Finish $Using:finish -Types Info | Where { $_.Gettype().Name -eq "VmBeingDeployedEvent" -or $_.Gettype().Name -eq "VmCreatedEvent" -or $_.Gettype().Name -eq "VmRegisteredEvent" -or $_.Gettype().Name -eq "VmClonedEvent"}
      # $Event = $vm | Get-VIEvent -Types Info | Where { $_.Gettype().Name -eq "VmBeingDeployedEvent" -or $_.Gettype().Name -eq "VmCreatedEvent" -or $_.Gettype().Name -eq "VmRegisteredEvent" -or $_.Gettype().Name -eq "VmClonedEvent"}

      # Заполняем параметры в зависимости от того, удалось ли в логах найти что-то
      If (($Event | Measure-Object).Count -eq 0){
         $User = $Using:UnknownUser
         $Created = $Using:UnknownCreatedTime
         $CreatedFormat = $Using:UnknownCreatedTime
      } Else {
         If ($Event.Username -eq "" -or $Event.Username -eq $null) {
            $User = $Using:UnknownUser
         } Else {
         $User = $Event.Username
         } # Else
            $CreatedFormat = $Event.CreatedTime
            # Один из коллег отдельно просил, чтобы время было в таком формате, поэтому дублируем его. А в БД пойдёт нормальный формат.
            $Created = $Event.CreatedTime.ToString('yyyy-MM-dd')
         } # Else

      Write-Host "Creator for $vm is $User. Creating object."

      # Создаём объект. Добавляем параметры.
      $Vmresult = New-Object PSObject
      $Vmresult | add-member -MemberType NoteProperty -Name "VM Name" -Value $vm.Name  
      $Vmresult | add-member -MemberType NoteProperty -Name "CreatedBy" -Value $User
      $Vmresult | add-member -MemberType NoteProperty -Name "CreatedOn" -Value $CreatedFormat
      $Vmresult | add-member -MemberType NoteProperty -Name "CreatedOnFormat" -Value $Created           
      # Выводим результаты
      $Vmresult

    } # Inline

} # ForEach

}

$Creators = GetCreators-Wf $VMfilePath
# Записываем результат в файл
$Creators | select 'VM Name', CreatedBy, CreatedOn | Export-Csv -Path $filePath -NoTypeInformation -UseCulture -Force

Write-Host "CSV generetion finisghed at $(get-date). PROFIT"

Külön figyelmet érdemel a könyvtár. xlsx, amely lehetővé tette, hogy a levélhez csatolt dokumentum szemléletesen legyen formázva (ahogy a vezetőség szereti), és ne csak egy csv táblázatként.

Szép xlsx dokumentum készítése a hibásan kitöltött gépek listájával.

# Создаём новую книгу
# Возможные значения : "xls" и "xlsx"
wb<-createWorkbook(type="xlsx")

# Стили для имён рядов и колонок в таблицах
TABLE_ROWNAMES_STYLE <- CellStyle(wb) + Font(wb, isBold=TRUE)
TABLE_COLNAMES_STYLE <- CellStyle(wb) + Font(wb, isBold=TRUE) +
  Alignment(wrapText=TRUE, horizontal="ALIGN_CENTER") +
  Border(color="black", position=c("TOP", "BOTTOM"), 
         pen=c("BORDER_THIN", "BORDER_THICK"))

# Создаём новый лист
sheet <- createSheet(wb, sheetName = names[2])

# Добавляем таблицу
addDataFrame(incorrect_df_filtered, 
             sheet, startRow=1, startColumn=1,  row.names=FALSE, byrow=FALSE,
             colnamesStyle = TABLE_COLNAMES_STYLE,
             rownamesStyle = TABLE_ROWNAMES_STYLE)

# Меняем ширину, чтобы форматирование было автоматическим
autoSizeColumn(sheet = sheet, colIndex=c(1:ncol(incorrect_df)))

# Добавляем фильтры
addAutoFilter(sheet, cellRange = "C1:G1")

# Определяем стиль
fo2 <- Fill(foregroundColor="red")
cs2 <- CellStyle(wb, 
                 fill = fo2, 
                 dataFormat = DataFormat("@"))

# Находим ряды с неверно заполненным полем Владельца и применяем к ним определённый стиль
rowsOwner <- getRows(sheet, rowIndex = (which(!incorrect_df$isOwnerCorrect) + 1))
cellsOwner <- getCells(rowsOwner, colIndex = which( colnames(incorrect_df_filtered) == "Owner" )) 
lapply(names(cellsOwner), function(x) setCellStyle(cellsOwner[[x]], cs2))

# Находим ряды с неверно заполненным полем Подсистемы и применяем к ним определённый стиль
rowsSubsystem <- getRows(sheet, rowIndex = (which(!incorrect_df$isSubsystemCorrect) + 1))
cellsSubsystem <- getCells(rowsSubsystem, colIndex = which( colnames(incorrect_df_filtered) == "Subsystem" )) 
lapply(names(cellsSubsystem), function(x) setCellStyle(cellsSubsystem[[x]], cs2))

# Аналогично по Категории
rowsCategory <- getRows(sheet, rowIndex = (which(!incorrect_df$isCategoryCorrect) + 1))
cellsCategory <- getCells(rowsCategory, colIndex = which( colnames(incorrect_df_filtered) == "Category" )) 
lapply(names(cellsCategory), function(x) setCellStyle(cellsCategory[[x]], cs2))

# Создатель
rowsCreator <- getRows(sheet, rowIndex = (which(!incorrect_df$isCreatorCorrect) + 1))
cellsCreator <- getCells(rowsCreator, colIndex = which( colnames(incorrect_df_filtered) == "Creator" )) 
lapply(names(cellsCreator), function(x) setCellStyle(cellsCreator[[x]], cs2))

# Сохраняем файл
saveWorkbook(wb, filenameIncorrect)

A kimenet körülbelül így néz ki:

Napi jelentések a virtuális gépek állapotáról R és PowerShell segítségével

Volt egy érdekes árnyalat a Windows ütemező beállításánál. Nem sikerült megtalálni a megfelelő jog- és beállítási paramétereket, hogy minden úgy fusson, ahogy kell. Végül találtunk egy R könyvtárat, amely automatikusan létrehozza a R szkript futtatásához szükséges ütemezést, és még a naplófájlra is emlékszik. Később lehet kézzel módosítani a feladatot.

Egy kis R kód két példával, amely létrehoz egy feladatot a Windows ütemezőben.

library(taskscheduleR)
myscript <- file.path(getwd(), "all_vm.R")

## A szkript futtatásának indítása 62 másodperc múlva
taskscheduler_create(taskname = "getAllVm", rscript = myscript, 
                     schedule = "ONCE", starttime = format(Sys.time() + 62, "%H:%M"))

## A szkript napi futtatása 09:10-kor
taskscheduler_create(taskname = "getAllVmDaily", rscript = myscript, 
                     schedule = "WEEKLY", 
                     days = c("MON", "TUE", "WED", "THU", "FRI"),
                     starttime = "02:00")

## Feladatok törlése
taskscheduler_delete(taskname = "getAllVm")
taskscheduler_delete(taskname = "getAllVmDaily")

# Naplók megtekintése (az utolsó 4 sor)
tail(readLines("all_vm.log"), sep ="n", n = 4)

Külön a DB-ről.

A szkript beállítása után más kérdések merültek fel. Például, szerettem volna megtalálni a dátumot, amikor a VM törölve lett, de a vCenter naplói már eltűntek. Mivel a szkript minden nap fájlokat ment egy mappába és nem tisztít (kézzel tisztítunk, amikor eszünkbe jut), ezért át lehet nézni a régi fájlokat és megtalálni az első fájlt, amiben a VM már nincs. De ez nem túl jó.

Szerettem volna létrehozni egy történeti adatbázist.

A segítségemre sietett a MS SQL SERVER funkciója — a rendszerverziózott időbeli táblázat. Ezt általában időbeli (nem idős) táblázatokként fordítják.

Részletesebben olvashatunk a Microsoft hivatalos dokumentációjában.

Röviden: létrehozunk egy táblázatot, megmondjuk, hogy verziózással szeretnénk, és az SQL Server létrehoz 2 további datetime oszlopot ebben a táblázatban (a rekord létrehozásának dátumát és a rekord élettartamának végét), valamint egy további táblázatot, ahová a módosítások kerülnek. Ennek eredményeként aktuális információt kapunk, és néhány egyszerű lekérdezéssel, amelyek példáit a dokumentáció tartalmazza, láthatjuk vagy egy konkrét virtuális gép életciklusát, vagy az összes VM állapotát egy adott időpontban.

Teljesítmény szempontjából - a fő táblázatba történő írás tranzakciója nem fejeződik be, amíg a ideiglenes táblázatba történő írás tranzakciója nem zárul le. Tehát olyan táblázatok esetében, ahol sok írási művelet történik, ezt a funkciót óvatosan kell bevezetni, de a mi esetünkben ez egy nagyon izgalmas dolog.

Annak érdekében, hogy a mechanizmus helyesen működjön, az R-hez írtam egy kis kódot, amely összehasonlította az új táblázatot az összes VM adataival, amelyek az adatbázisban vannak, és csak a megváltozott sorokat írt be. A kód nem olyan bonyolult, a compareDF könyvtárat használja, de ezt is meg fogom adni lent.

R kód az adatok adatbázisba való írásához

# Подцепляем пакеты
library(odbc)
library(compareDF)

# Формируем коннект
con <- dbConnect(odbc(),
                 Driver = "ODBC Driver 13 for SQL Server",
                 Server = DBParams$server,
                 Database = DBParams$database,
                 UID = DBParams$UID,
                 PWD = DBParams$PWD,
                 Port = 1433)

#### Проверяем есть ли таблица. Если нет - создаём. ####

if (!dbExistsTable(con, DBParams$TblName)) {
  #### Создаём таблицу ####
  create <- dbSendStatement(
    con,
    paste0(
      'CREATE TABLE ',
      DBParams$TblName,
      '(
    [Id] [int] NOT NULL PRIMARY KEY CLUSTERED,
    [VM.Name] [varchar](255) NULL,
    [Cluster] [varchar](255) NULL,
    [Esxi.Host] [varchar](255) NULL,
    [IP.Address.1] [varchar](255) NULL,
    [IP.Address.2] [varchar](255) NULL,
    [IP.Address.3] [varchar](255) NULL,
    [IP.Address.4] [varchar](255) NULL,
    [IP.Address.5] [varchar](255) NULL,
    [IP.Address.6] [varchar](255) NULL,
    [vCPU] [int] NULL,
    [CPU.Sockets] [int] NULL,
    [Core.per.Socket] [int] NULL,
    [RAM..GB.] [int] NULL,
    [Total.HDD..GB.] [int] NULL,
    [Power.State] [varchar](255) NULL,
    [OS] [varchar](255) NULL,
    [Boot.Time] [varchar](255) NULL,
    [VMTools.Status] [varchar](255) NULL,
    [VMTools.Version] [int] NULL,
    [VMTools.Version.Status] [varchar](255) NULL,
    [VMTools.Running.Status] [varchar](255) NULL,
    [Creation.Date] [varchar](255) NULL,
    [Creator] [varchar](255) NULL,
    [Category] [varchar](255) NULL,
    [Owner] [varchar](255) NULL,
    [Subsystem] [varchar](255) NULL,
    [IP.s] [varchar](255) NULL,
    [vCenter.Name] [varchar](255) NULL,
    DateFrom datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    DateTo datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    PERIOD FOR SYSTEM_TIME (DateFrom, DateTo)
        ) ON [PRIMARY]
        WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ', DBParams$TblHistName,'));'
    )
  )

  # Отправляем подготовленный запрос
  dbClearResult(create)

} # if

#### Начало работы с таблицей ####

# Обозначаем таблицу, с которой будем работать
allVM_db_con <- tbl(con, DBParams$TblName) 

#### Сравниваем таблицы ####

# Собираем данные с таблицы (убираем служебные временные поля)
allVM_db <- allVM_db_con %>% 
  select(c(-"DateTo", -"DateFrom")) %>% 
  collect()

# Создаём таблицу со сравнением объектов. Сравниваем по Id
# Удалённые объекты там будут помечены через -, созданные через +, изменённые через - и +
ctable_VM <- fullXslx_df %>% 
  compare_df(allVM_db, 
             c("Id"))

#### Удаление строк ####

# Выдираем Id виртуалок, записи о которых надо удалить 
remove_Id <- ctable_VM$comparison_df %>% 
  filter(chng_type == "-") %>%
  select(Id)

# Проверяем, что есть записи (если записей нет - и удалять ничего не нужно)
if (remove_Id %>% nrow() > 0) {

  # Конструируем шаблон для запроса на удаление данных
  delete <- dbSendStatement(con, 
                        paste0('
                               DELETE 
                               FROM ',
                               DBParams$TblName,
                               ' WHERE "Id"=?
                               ') # paste
                        ) # send

  # Создаём запрос на удаление данных
  dbBind(delete, remove_Id)

  # Отправляем подготовленный запрос
  dbClearResult(delete)

} # if

#### Добавление строк ####

# Выделяем таблицу, содержащую строки, которые нужно добавить.
allVM_add <- ctable_VM$comparison_df %>% 
  filter(chng_type == "+") %>% 
  select(-chng_type)

# Проверяем, есть ли строки, которые нужно добавить и добавляем (если нет - не добавляем)
if (allVM_add %>% nrow() > 0) {
  # Пишем таблицу со всеми необходимыми данными
  dbWriteTable(con,
               DBParams$TblName,
               allVM_add,
               overwrite = FALSE,
               append = TRUE)

} # if

#### Не забываем сделать дисконнект ####
dbDisconnect(con)

Összesen

A szkript bevezetésének eredményeként néhány hónap alatt rendet teremtettünk és fenntartjuk. Néha helytelenül kitöltött VM-ek jelennek meg, de a szkript jól emlékeztet, és ritkán fordul elő, hogy egy VM egymás után 2 napig bekerül a listába.

Továbbá elkészült egy előkészítés a történeti adatok elemzésére.

Nyilvánvaló, hogy sok mindent meg lehet valósítani nem "kézzel", hanem megfelelő szoftverrel, de a feladat érdekes és mondhatni fakultatív volt.

Az R ismét megmutatta, hogy kiváló univerzális nyelv, amely nemcsak statisztikai problémák megoldására alkalmas, hanem kiváló "hidat" is jelent más adatforrások között.

Napi jelentések a virtuális gépek állapotáról R és PowerShell segítségével

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster