Report giornalieri sullo stato delle macchine virtuali con R e PowerShell

Report giornalieri sullo stato delle macchine virtuali con R e PowerShell

Introduzione

Buongiorno. Da sei mesi abbiamo un script (in realtà un insieme di script) che genera report sullo stato delle macchine virtuali (e non solo). Ho deciso di condividere l'esperienza di creazione e il codice stesso. Mi aspetto critiche e confido che questo materiale possa essere utile a qualcuno.

Formazione della domanda

Abbiamo molte macchine virtuali (circa 1500 VM distribuite su 3 vCenter). Nuove macchine vengono create e quelle vecchie eliminate abbastanza frequentemente. Per mantenere l'ordine sono stati aggiunti alcuni campi personalizzati nel vCenter per distinguere le VM in Sottosistemi, indicare se sono test e chi e quando le ha create. Il fattore umano ha portato a lasciare oltre metà delle macchine con campi vuoti, il che ha complicato il lavoro. Ogni sei mesi qualcuno si innervosiva e iniziava il lavoro di aggiornamento di questi dati, ma il risultato smetteva di essere valido già dopo un paio di settimane.
Chiarisco subito che tutti comprendono che dovrebbero esserci richieste per la creazione di macchine, un processo per la loro creazione, ecc. e così via. E tutti seguono rigorosamente questo processo e tutto è in ordine. Purtroppo da noi non è così, ma questo non è l'argomento dell'articolo 🙂

In generale, è stata presa la decisione di automatizzare il controllo della correttezza della compilazione dei campi.
Abbiamo deciso che una email quotidiana con l'elenco delle macchine compilate in modo errato inviata a tutti gli ingegneri responsabili e ai loro superiori sarebbe un buon inizio.

Fino a quel momento uno dei colleghi aveva già implementato uno script in PowerShell che raccoglieva quotidianamente informazioni su tutte le macchine di tutti i vCenter secondo un programma prestabilito e generava 3 documenti csv (ognuno per il proprio vCenter), che venivano pubblicati su un'unità condivisa. È stata presa la decisione di utilizzare questo script come base e di arricchirlo con controlli utilizzando il linguaggio R, con il quale avevamo una certa esperienza.

Nel processo di sviluppo, la soluzione è stata ampliata con notifiche via email, un database con una tabella principale e storica (ne parlerò più avanti), e un'analisi dei log di vSphere per cercare i veri creatori delle VM e il momento della loro creazione.

Per lo sviluppo sono stati utilizzati gli IDE RStudio Desktop e PowerShell ISE.

Lo script viene eseguito da una normale macchina virtuale Windows.

Descrizione della logica generale.

La logica generale degli script è risultata la seguente.

  • Raccogliamo dati sulle macchine virtuali utilizzando uno script PowerShell, che chiamiamo tramite R, e uniamo i risultati in un unico csv. L'interazione inversa tra i linguaggi è realizzata in modo simile. (si potevano inviare dati direttamente da R a PowerShell come variabili, ma è complicato e, avendo csv intermedi, è più facile fare debug e condividere i risultati intermedi con qualcuno).
  • Con R definiamo i parametri ammissibili per i campi i cui valori controlliamo. — Creiamo un documento word che conterrà i valori di questi campi da inserire in una lettera informativa, che sarà una risposta alle domande dei colleghi "Non so come dovrei compilarlo?".
  • Carichiamo i dati di tutte le VM da csv utilizzando R, creiamo un dataframe, rimuoviamo i campi non necessari e creiamo un documento xlsx informativo, che conterrà un riepilogo delle informazioni su tutte le VM, che pubblichiamo su una risorsa condivisa.
  • Applichiamo al dataframe di tutte le VM tutti i controlli di correttezza dei campi e creiamo una tabella contenente solo le VM con campi compilati in modo errato (e solo questi campi).
  • Inviamo l'elenco delle VM ottenuto a un altro script PowerShell, che verificherà i log di vCenter in cerca di eventi di creazione delle VM, il che ci permetterà di indicare il presunto orario di creazione della VM e il presunto creatore. Questo nel caso in cui nessuno si assuma la responsabilità per la macchina. Questo script non è veloce, soprattutto se ci sono molti log, quindi controlliamo solo le ultime 2 settimane, e utilizziamo anche un workflow che consente di cercare informazioni su più VM contemporaneamente. Nel esempio di script ci sono commenti dettagliati su questo meccanismo. Uniamo i risultati in un csv, che carichiamo di nuovo in R.
  • Creiamo un documento xlsx ben formattato, in cui i campi compilati in modo errato saranno evidenziati in rosso, saranno applicati filtri a alcune colonne e saranno indicate colonne aggiuntive contenenti i presunti creatori e il tempo di creazione delle VM.
  • Stiamo preparando un'email in cui alleghiamo un documento che descrive i valori consentiti dei campi, oltre a una tabella con le VM compilate in modo errato. Nel testo indichiamo il numero totale di VM create in modo errato, il link a una risorsa generale e un'immagine motivazionale. Se non ci sono VM compilate in modo errato, inviamo un'altra email con un'immagine motivazionale più felice.
  • Registriamo i dati su tutte le VM nel database SQL Server tenendo conto del meccanismo delle tabelle storiche implementato (un meccanismo molto interessante di cui parleremo più avanti).

I veri script

Il file principale con codice 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)

# Verifichiamo la correttezza dei campi compilati
full_df <- fullXslx_df %>%
  mutate(
    # Prima rimuoviamo tutti gli spazi e le tabulazioni superflue, poi consideriamo il separatore virgola, poi verifichiamo l'inclusione nei valori consentiti,
    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)
  )

# Verifichiamo l'esistenza del file contenente tutte le VM e lo rimuoviamo, se esiste.
if (file.exists(filenameAll)) {file.remove(filenameAll)}

#### Creiamo il file xslx con il rapporto ####
# Dati generali su un foglio separato
full_df %&gt;% write.xlsx(file=filenameAll,
                       sheetName=names[1],
                       col.names=TRUE,
                       row.names=FALSE,
                       append=FALSE)

#### Creiamo il file xslx con i campi compilati in modo errato ####
# Creiamo 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)

# Verifichiamo l'esistenza del file contenente tutte le VM e lo rimuoviamo, se esiste.
if (file.exists(filenameIncVM)) {file.remove(filenameIncVM)}

# Salviamo l'elenco delle VM con campi non compilati in csv
incorrect_df %&gt;%
  select(VM.Name) %&gt;%
  write_csv2(path = filenameIncVM, append = FALSE)

# Filtriamo per l'inserimento nell'email
incorrect_df_filtered <- incorrect_df %>% 
  select(VM.Name, 
         IP.s, 
         Owner, 
         Subsystem, 
         Category,
         Creator,
         vCenter.Name,
         Creation.Date
  )

# Calcoliamo il numero di righe
numberOfRows <- nrow(incorrect_df)

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

if (numberOfRows > 0) {

  # Verifichiamo l'esistenza del file con i creatori e lo rimuoviamo, se esiste.
  if (file.exists(creatorsFilePath)) {file.remove(creatorsFilePath)}

  # Eseguiamo lo script PowerShell che troverà i creatori delle VM individuate. Come risultato otterremo un csv.
  system(paste0("powershell -File ", getCreatorsPath))

  # Leggiamo il file con i creatori
  creators_df <- creatorsFilePath %>%
    read.csv2(stringsAsFactors = FALSE)

  # Filtriamo per l'inserimento nell'email, aggiungiamo i dati dalla tabella con i creatori
  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(`Presunto creatore` = CreatedBy, 
           `Data di creazione presunta` = CreatedOn)  

  # Creiamo il corpo dell'email
  emailBody &lt;- paste0(
    &#039;<html>
                    <h3>Buongiorno, cari colleghi.</h3>
                    <p>Potete vedere tutte le informazioni aggiornate sulle macchine virtuali sul disco H: qui:<p>
                    <p>\server.ruVM', sourceFileFormat, '</p>
                    <p>In allegato anche l'elenco delle VM con <strong>compilazioni errate</strong> dei campi. In totale sono <strong>', numberOfRows,'</strong>.</p>
                    <p>La tabella ha ora 2 colonne aggiuntive. <strong>Creatore previsto</strong> e <strong>Data di creazione prevista</strong>, che vengono recuperati dai log di vCenter delle ultime 2 settimane</p>
                    <p>Si prega ai creatori delle macchine di chiarire i dati e di compilare i campi correttamente. Le regole di compilazione dei campi sono anche in allegato</p>
                    <p><img src="data/meme.jpg"></p>
                    </html>'
  )

  # Verifichiamo l'esistenza del file
  if (file.exists(filenameIncorrect)) {file.remove(filenameIncorrect)}

  # Creiamo una bella tabella con formati ecc.
  source(file = "email.R", local = T, encoding = "utf-8")

  #### Creiamo un'email con macchine poco firmate ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "VM con campi compilati in modo errato",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            attach.files = c(filenameIncorrect, filenameVmCreationRules),
            debug = FALSE)

  #### Poi ci sarà un blocco, se non ci sono problemi con le VM ####
} else {

  # Creiamo il corpo dell'email
  emailBody &lt;- paste0(
    &#039;<html>
    <h3>Buongiorno, cari colleghi</h3>
   <p>Potete vedere tutte le informazioni aggiornate sulle macchine virtuali sul disco H: qui:<p>
    <p>\server.ruVM', sourceFileFormat, '</p>
    <p>Inoltre, al momento, tutti i campi delle VM sono compilati correttamente</p>
    <p><img src="data/meme_correct.jpg"></p>
    </html>'
  )

  #### Creiamo un'email senza VM mal compilate ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "Informazioni riassuntive",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            debug = FALSE)
}

####### Registriamo i dati nel DB #####

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

Script per ottenere l'elenco delle VM su 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 PowerShell che estrae dai log i creatori delle macchine virtuali e le date della loro creazione

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

Merita attenzione la libreria xlsx, che ha permesso di formattare l'allegato all'email in modo chiaro (come piace alla direzione), e non semplicemente come una tabella csv.

Creazione di un bel documento xlsx con l'elenco delle macchine compilate in modo errato

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

In output risulta più o meno così:

Report giornalieri sullo stato delle macchine virtuali con R e PowerShell

C'è stato anche un interessante problema con la configurazione del pianificatore di Windows. Non siamo riusciti a trovare i parametri giusti per diritti e impostazioni affinché tutto si avviasse come previsto. Alla fine, è stata trovata una libreria R che crea automaticamente un'attività per l'esecuzione dello script R e non dimentica nemmeno il file per i log. Poi si può modificare manualmente l'attività.

Un frammento di codice R con due esempi che crea un'attività nel pianificatore di Windows

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

## avviamo lo script dopo 62 secondi
taskscheduler_create(taskname = "getAllVm", rscript = myscript, 
                     schedule = "ONCE", starttime = format(Sys.time() + 62, "%H:%M"))

## avviamo lo script ogni giorno alle 09:10
taskscheduler_create(taskname = "getAllVmDaily", rscript = myscript, 
                     schedule = "WEEKLY", 
                     days = c("MON", "TUE", "WED", "THU", "FRI"),
                     starttime = "02:00")

## eliminiamo le attività
taskscheduler_delete(taskname = "getAllVm")
taskscheduler_delete(taskname = "getAllVmDaily")

# Guardiamo i log (ultime 4 righe)
tail(readLines("all_vm.log"), sep = "n", n = 4)

Separatamente riguardo al database

Dopo che lo script è stato configurato, sono emersi altri quesiti. Ad esempio, volevamo trovare la data in cui la VM è stata eliminata, ma i log in vCenter erano già stati persi. Poiché lo script salva i file in una cartella ogni giorno e non li elimina (li puliamo a mano quando ce ne ricordiamo), è possibile esaminare i file più vecchi e trovare il primo file in cui non è presente la VM in questione. Ma non è affatto una buona soluzione.

Ho voluto creare un database storico.

A supporto è arrivata la funzionalità di MS SQL SERVER — la tabella temporale versionata. Di solito viene tradotta come tabelle temporali (non tabelle provvisorie).

Puoi leggere in dettaglio in documentazione ufficiale Microsoft.

In breve, creiamo una tabella, indichiamo che sarà versione e SQL Server crea 2 colonne datetime aggiuntive in questa tabella (la data di creazione della registrazione e la data di scadenza della registrazione) e una tabella aggiuntiva in cui verranno registrate le modifiche. Di conseguenza otteniamo informazioni aggiornate e, attraverso semplici query, i cui esempi sono forniti nella documentazione, possiamo vedere sia il ciclo di vita di una singola macchina virtuale, sia lo stato di tutte le VM in un determinato momento.

Dal punto di vista delle prestazioni, la transazione di scrittura nella tabella principale non sarà completata finché non sarà completata la transazione di scrittura nella tabella temporale. Cioè, su tabelle con un gran numero di operazioni di scrittura, questa funzionalità deve essere implementata con cautela, ma nel nostro caso è davvero una cosa molto interessante.

Per far funzionare correttamente il meccanismo, è stato necessario scrivere un piccolo pezzo di codice in R che confrontasse la nuova tabella con i dati di tutte le VM di quella memorizzata nel database e registrasse solo le righe che erano cambiate. Il codice non è particolarmente complicato, utilizza la libreria compareDF, ma lo riporterò anche di seguito.

Codice R per la registrazione dei dati nel database

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

Totale

A seguito dell'implementazione dello script, nell'arco di pochi mesi è stato stabilito e mantenuto un ordine. A volte compaiono VM compilate in modo errato, ma lo script funge da buon promemoria e raramente una VM finisce nella lista per 2 giorni di fila.

È stato inoltre fatto un preliminare per l'analisi dei dati storici.

È chiaro che molto di questo può essere realizzato non "alla buona", ma con software specializzato, ma l'attività è stata interessante e, si può dire, facoltativa.

R si è dimostrato ancora una volta un linguaggio universale eccellente, perfetto non solo per risolvere problemi statistici, ma anche come ottimo "ponte" tra altre fonti di dati.

Report giornalieri sullo stato delle macchine virtuali con R e PowerShell

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster