Rapoarte zilnice privind starea mașinilor virtuale cu ajutorul R și PowerShell

Rapoarte zilnice privind starea mașinilor virtuale cu ajutorul R și PowerShell

Introducere

Bună ziua. Deja de șase luni un script (mai exact, un set de scripturi) funcționează pentru a genera rapoarte despre starea mașinilor virtuale (și nu numai). Am decis să împărtășesc experiența de creație și codul în sine. Mă aștept la critici și sper că acest material poate fi util cuiva.

Formarea necesității

Avem multe mașini virtuale (aproximativ 1500 VM distribuite pe 3 vCenter-uri). Noi mașini sunt create și vechi sunt șterse destul de des. Pentru a menține ordinea, au fost adăugate câteva câmpuri personalizate în vCenter pentru a separa VM-urile pe subsisteme, a indica dacă sunt de testare, precum și cine și când le-a creat. Factorul uman a dus la situația în care mai mult de jumătate dintre mașini au rămas cu câmpuri necompletate, ceea ce complica munca. O dată la șase luni, cineva se enerva și începea să actualizeze aceste date, dar rezultatul devenea învechit după aproximativ o săptămână și jumătate.
Îmi clarific de la bun început că toată lumea înțelege că trebuie să existe cereri pentru crearea mașinilor, un proces pentru crearea lor etc. și așa mai departe. Și în același timp, toți respectă cu strictețe acest proces și totul este în ordine. Din păcate, la noi nu este așa, dar acesta nu este subiectul articolului 🙂

În general, s-a decis automatizarea verificării corectitudinii completării câmpurilor.
Am decis că un e-mail zilnic cu lista mașinilor completate greșit trimis tuturor inginerilor responsabili și șefilor lor va fi un bun început.

Până în acest moment, unul dintre colegi implementase deja un script în PowerShell, care în fiecare zi, conform unui program, colecta informațiile despre toate mașinile din toate vCenter-urile și genera 3 documente csv (câte unul pentru fiecare vCenter), care erau încărcate pe un disc comun. S-a decis să luăm acest script ca bază și să-l completăm cu verificări folosind limbajul R, cu care aveam o oarecare experiență.

În timpul dezvoltării, soluția a fost extinsă pentru a include informarea prin e-mail, o bază de date cu o tabelă principală și una istorică (despre asta mai târziu), precum și analiza jurnalelor vSphere pentru a găsi creatorii efectivi ai VM-urilor și timpul în care au fost create.

Pentru dezvoltare au fost folosite IDE-urile RStudio Desktop și PowerShell ISE.

Scriptul este lansat de o mașină virtuală Windows obișnuită.

Descrierea logicii generale.

Logica generală a scripturilor a fost următoarea.

  • Colectăm datele despre mașinile virtuale folosind un script PowerShell, pe care îl apelăm prin R, iar rezultatul este combinat într-un singur csv. Interacțiunea inversă între limbaje este realizată similar. (puteam să trimitem datele direct din R în PowerShell ca variabile, dar este complicat, iar având csv-uri intermediare este mai ușor de depanat și de distribuit cuiva rezultatele intermediare).
  • Folosind R, generăm parametrii valizi pentru câmpurile a căror valori le verificăm. - Generăm un document Word, care va conține valorile acestor câmpuri pentru a fi inserate în scrisoarea informativă, care va fi răspunsul la întrebările colegilor „Dar cum ar trebui să completez asta?”.
  • Încărcăm datele pentru toate VM-urile din csv utilizând R, formăm un dataframe, eliminăm câmpurile inutile și generăm un document informational xlsx, care va conține informații rezumate despre toate VM-urile, pe care le încărcăm pe o resursă comună.
  • Aplicăm toate verificările de corectitudine a completării câmpurilor la dataframe-ul cu toate VM-urile și generăm un tabel, care conține doar VM-urile cu câmpuri completate incorect (și doar aceste câmpuri).
  • Lista obținută a VM-urilor este trimisă către un alt script PowerShell, care va verifica jurnalele vCenter pentru evenimentele de creare a VM-urilor, ceea ce va permite specificarea timpului estimat de creare a VM-ului și a presupusului creator. Acest lucru este util în caz că nimeni nu recunoaște că aparatul este al său. Acest script nu funcționează rapid, în special dacă există multe jurnale, așa că analizăm doar ultimele 2 săptămâni și folosim un flux de lucru care permite căutarea informațiilor pentru mai multe VM-uri simultan. În exemplul scriptului sunt comentarii detaliate despre acest mecanism. Rezultatul este salvat într-un csv, care este din nou încărcat în R.
  • Generăm un document xlsx frumos formatat, în care vor fi evidențiate în roșu câmpurile completate incorect, se vor aplica filtre pe unele coloane și se vor specifica coloane suplimentare, conținând creatorii presupusi și timpul de creare a VM-ului.
  • Formăm un email în care atașăm un document care descrie valorile permise ale câmpurilor, precum și un tabel cu mașinile virtuale completate incorect. În text menționăm numărul total de VMs create incorect, un link către resursa generală și o imagine motivațională. Dacă nu sunt VMs completate incorect, trimitem un alt email cu o imagine motivațională mai veselă.
  • Înregistrăm datele pentru toate VMs în baza de date SQL Server ținând cont de mecanismul implementat al tabelelor istorice (un mecanism foarte interesant – despre care voi vorbi mai în detaliu mai jos).

Scripturi propriu-zise

Fișierul principal cu cod pe R

# Путь к рабочей директории (нужно для корректной работы через виндовый планировщик заданий)
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)

# Verificăm corectitudinea câmpurilor completate
full_df <- fullXslx_df %>%
  mutate(
    # Mai întâi eliminăm toate spațiile și tabulările inutile, apoi luăm în considerare separatorul virgulă, apoi verificăm includerea în valorile acceptate,
    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)
  )

# Verificăm existența fișierului cu toate VM-urile și îl ștergem, dacă există.
if (file.exists(filenameAll)) {file.remove(filenameAll)}

#### Formăm fișierul xslx cu raportul ####
# Datele generale pe o foaie separată
full_df %&gt;% write.xlsx(file=filenameAll,
                       sheetName=names[1],
                       col.names=TRUE,
                       row.names=FALSE,
                       append=FALSE)

#### Formăm fișierul xslx cu câmpurile completate greșit ####
# Formăm df
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)

# Verificăm existența fișierului cu toate VM-urile și îl ștergem, dacă există.
if (file.exists(filenameIncVM)) {file.remove(filenameIncVM)}

# Salvăm lista VM-urilor cu câmpuri necompletate în csv
incorrect_df %&gt;%
  select(VM.Name) %&gt;%
  write_csv2(path = filenameIncVM, append = FALSE)

# Filtrăm pentru a insera în email
incorrect_df_filtered <- incorrect_df %>% 
  select(VM.Name, 
         IP.s, 
         Owner, 
         Subsystem, 
         Category,
         Creator,
         vCenter.Name,
         Creation.Date
  )

# Numărăm liniile
numberOfRows <- nrow(incorrect_df)

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

if (numberOfRows > 0) {

  # Verificăm existența fișierului cu creatorii și îl ștergem, dacă există.
  if (file.exists(creatorsFilePath)) {file.remove(creatorsFilePath)}

  # Executăm scriptul PowerShell, care va găsi creatorii VM-urilor găsite. La ieșire vom obține csv.
  system(paste0("powershell -File ", getCreatorsPath))

  # Citim fișierul cu creatorii
  creators_df <- creatorsFilePath %>%
    read.csv2(stringsAsFactors = FALSE)

  # Filtrăm pentru a insera în email, adăugând date din tabelul cu creatorii
  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(`Creator presupus` = CreatedBy, 
           `Data presuspusă a creării` = CreatedOn)  

  # Formăm corpul emailului
  emailBody &lt;- paste0(
    &#039;<html>
                    <h3>Bună ziua, stimați colegi.</h3>
                    <p>Informații complete și actualizate despre mașinile virtuale le puteți vizualiza pe disk-ul H: aici:<p>
                    <p>\server.ruVM', sourceFileFormat, '</p>
                    <p>De asemenea, în atașament se află lista VM cu <strong>câmpurile completate incorect.</strong> În total sunt <strong>', numberOfRows,'</strong>.</p>
                    <p>În tabel au apărut 2 coloane suplimentare. <strong>Creatorul presupus</strong> și <strong>Data creației presupuse</strong>, care sunt extrase din jurnalele vCenter în ultimele 2 săptămâni.</p>
                    <p>Cerere către creatorii mașinilor să clarifice datele și să completeze câmpurile corect. Regulile de completare a câmpurilor se află de asemenea în atașament.</p>
                    <p><img src="data/meme.jpg"></p>
                    </html>'
  )

  # Verificăm existența fișierului
  if (file.exists(filenameIncorrect)) {file.remove(filenameIncorrect)}

  # Formăm un tabel elegant cu formate etc.
  source(file = "email.R", local = T, encoding = "utf-8")

  #### Formăm un email cu mașini prost semnate ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "VM cu câmpuri completate incorect",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            attach.files = c(filenameIncorrect, filenameVmCreationRules),
            debug = FALSE)

  #### Aici se va continua blocul, dacă nu sunt probleme cu VM ####
} else {

  # Formăm corpul emailului
  emailBody &lt;- paste0(
    &#039;<html>
    <h3>Bună ziua, dragi colegi</h3>
   <p>Informații complete și actualizate despre mașinile virtuale le puteți vizualiza pe disk-ul H: aici:<p>
    <p>\server.ruVM', sourceFileFormat, '</p>
    <p>De asemenea, în prezent, toate câmpurile VM sunt completate corect.</p>
    <p><img src="data/meme_correct.jpg"></p>
    </html>'
  )

  #### Formăm un email fără VM prost completate ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "Informații sumare",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            debug = FALSE)
}

####### Scriem datele în Bază de Date #####

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

Script pentru obținerea listei de vm în PowerShell

# Данные для подключения и другие переменные
$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
    }

Script pe PowerShell care extrage din jurnale creatorii mașinilor virtuale și datele lor de creare

# Путь к файлу, из которого будем доставать список 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"

Merită o atenție deosebită biblioteca xlsx, care a permis formatarea vizuală a atașamentului la email (așa cum îi place conducerii), și nu doar ca un tabel csv.

Generarea unui document xlsx frumos cu lista mașinilor completate incorect

# Создаём новую книгу
# Возможные значения : "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)

La ieșire, rezultatul arată cam așa:

Rapoarte zilnice privind starea mașinilor virtuale cu ajutorul R și PowerShell

De asemenea, a fost un aspect interesant legat de configurarea programatorului Windows. Nu se reușea să găsesc parametrii corecți pentru drepturi și setări, ca totul să se lanseze așa cum trebuie. În cele din urmă, am găsit o bibliotecă R care creează automat o sarcină pentru a rula scriptul R și nu uită nici de fișierul pentru jurnale. Apoi, se poate ajusta manual sarcina.

Un fragment de cod în R cu două exemple, care creează o sarcină în programatorul Windows

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

## lansăm scriptul peste 62 de secunde
taskscheduler_create(taskname = "getAllVm", rscript = myscript, 
                     schedule = "ONCE", starttime = format(Sys.time() + 62, "%H:%M"))

## lansăm scriptul în fiecare zi la 09:10
taskscheduler_create(taskname = "getAllVmDaily", rscript = myscript, 
                     schedule = "WEEKLY", 
                     days = c("MON", "TUE", "WED", "THU", "FRI"),
                     starttime = "02:00")

## ștergem sarcinile
taskscheduler_delete(taskname = "getAllVm")
taskscheduler_delete(taskname = "getAllVmDaily")

# Vizualizăm jurnalele (ultimele 4 linii)
tail(readLines("all_vm.log"), sep ="n", n = 4)

Despre Baza de Date în mod separat

După configurarea scriptului, au apărut alte întrebări. De exemplu, am dorit să găsesc data la care VM a fost ștearsă, dar jurnalele din vCenter s-au șters deja. Deoarece scriptul salvează fișiere în folder în fiecare zi și nu le curăță (le curățăm manual când ne amintim), putem vizualiza fișierele vechi și găsi primul fișier în care acel VM nu mai există. Dar nu este ideal.

Am dorit să creez o bază de date istorică.

Funcționalitatea MS SQL SERVER — tabel temporal cu versiuni de sistem a venit în ajutor. Acesta este de obicei tradus ca tabele temporale (nu tabele vremelnice).

Puteți citi detaliat în documentația oficială Microsoft.

Pe scurt — creăm un tabel, specificăm că va avea versiuni și SQL Server creează 2 coloane datetime suplimentare în acest tabel (data creării înregistrării și data expirării acesteia) și un tabel suplimentar în care vor fi scrise modificările. În rezultatul obținem informații actualizate și, prin interogări simple, exemplele cărora sunt date în documentație, putem vedea fie ciclul de viață al unei mașini virtuale specifice, fie starea tuturor VM-urilor într-un anumit moment în timp.

Din punct de vedere al performanței — tranzacția de scriere în tabelul principal nu se va finaliza până când nu se va finaliza tranzacția de scriere în tabelul temporar. Adică, la tabele cu un număr mare de operațiuni de scriere, această funcționalitate trebuie implementată cu precauție, dar în cazul nostru este pur și simplu o unealtă foarte interesantă.

Pentru ca mecanismul să funcționeze corect, a fost necesar să scriu un mic cod în R care să compare noul tabel cu datele despre toate VM-urile cu cea stocată în BDD și să scrie în ea doar rândurile care s-au modificat. Codul nu este foarte complex, folosește biblioteca compareDF, dar îl voi include și pe acesta mai jos.

Codul în R pentru scrierea datelor în BDD

# Подцепляем пакеты
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)

În concluzie

Ca urmare a implementării scriptului, în câteva luni a fost restabilită și este menținută ordinea. Uneori apar VM-uri completate greșit, dar scriptul servește ca o bună amintire și rar o VM apare pe lista timp de 2 zile consecutive.

De asemenea, a fost realizat un suport pentru analiza datelor istorice.

Este clar că multe dintre acestea pot fi realizate nu "pe genunchi", ci cu software specializat, dar sarcina a fost interesantă și, se poate spune, facultativă.

R s-a dovedit din nou a fi un limbaj universitar excelent, care se potrivește perfect nu doar pentru soluționarea problemelor statistice, ci și servește ca un "conector" excelent între alte surse de date.

Rapoarte zilnice privind starea mașinilor virtuale cu ajutorul R și PowerShell

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster