Report giornalieri sullo stato delle macchine virtuali tramite R e PowerShell

Report giornalieri sullo stato delle macchine virtuali tramite R e PowerShell

Introduzione

Buongiorno. Da sei mesi abbiamo in funzione uno script (più precisamente un insieme di script) che genera rapporti sullo stato delle macchine virtuali (e non solo). Ho deciso di condividere l'esperienza di creazione e il codice stesso. Conto su critiche e spero che questo materiale possa essere utile a qualcuno.

Formazione del bisogno

Abbiamo molte macchine virtuali (circa 1500 VM distribuite su 3 vCenter). Vengono create nuove e rimosse vecchie abbastanza spesso. Per mantenere l'ordine, sono stati aggiunti alcuni campi personalizzati nel vCenter per separare le VM in Sottosistemi, indicare se sono di test e chi le ha create e quando. Il fattore umano ha portato al fatto che più della metà delle macchine sono rimaste con i campi vuoti, il che ha complicato il lavoro. Ogni sei mesi, qualcuno si stressava e iniziava a lavorare per aggiornare questi dati, ma il risultato smetteva di essere attuale già dopo una settimana e mezzo.
Preciso subito che tutti comprendono che devono esserci richieste per la creazione delle macchine, il processo della loro creazione, ecc. e così via. E tutti seguono rigorosamente questo processo e c'è ordine in tutto. Purtroppo da noi non è così, ma non è oggetto dell'articolo 🙂

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

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

Durante lo sviluppo, la soluzione è stata arricchita con l'invio di email, una base di dati con la tabella principale e quella storica (di cui parleremo più avanti), oltre all'analisi dei log di vSphere per individuare i creatori reali delle vm e il loro orario di 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 invochiamo tramite R, e uniamo il risultato in un unico file csv. L'interazione tra i linguaggi è stata realizzata in modo analogo. (avremmo potuto passare i dati direttamente da R a PowerShell come variabili, ma è complicato e avere file csv intermedi rende più facile il debug e la condivisione dei risultati con qualcuno).
  • Utilizziamo R per generare i parametri validi per i campi i cui valori verifichiamo. — Creiamo un documento Word che conterrà i valori di questi campi da inserire in una comunicazione informativa, che risponderà alle domande dei colleghi "Ma come dovrei compilare questo?".
  • Carichiamo i dati di tutte le VM da csv usando R, creiamo un dataframe, rimuoviamo i campi non necessari e generiamo un documento informativo in formato xlsx, che conterrà informazioni riassuntive su tutte le VM, e che pubblichiamo su una risorsa condivisa.
  • Applichiamo a dataframe di tutte le VM tutte le verifiche di correttezza nella compilazione dei campi e creiamo una tabella, contenente solo le VM con campi compilati in modo errato (e solo quei campi).
  • Inviamo l'elenco di VM a un altro script PowerShell, che controllerà i registri di vCenter per eventi di creazione delle VM, il che ci permetterà di specificare il presunto tempo di creazione della VM e il presunto creatore. Questo è nel caso in cui nessuno si assuma la responsabilità della macchina. Questo script non funziona rapidamente, specialmente se ci sono molti registri, quindi controlliamo solo le ultime 2 settimane e utilizziamo un workflow che consente di cercare informazioni su più VM contemporaneamente. Nel esempio di script ci sono commenti dettagliati su questo meccanismo. I risultati vengono messi in un file csv, che carichiamo di nuovo in R.
  • Creiamo un documento xlsx ben formattato, in cui i campi compilati in modo errato sono evidenziati in rosso, vengono applicati filtri su alcune colonne e vengono indicate colonne aggiuntive contenenti i presunti creatori e il tempo di creazione delle VM.
  • Stiamo preparando un'email a cui alleghiamo un documento che descrive i valori consentiti per i campi, insieme a una tabella con le macchine virtuali (VM) compilate in modo errato. Nel testo indichiamo il numero totale di VM create in modo errato, un link al risorsa generale e un'immagine motivazionale. Se non ci sono VM compilate in modo errato, inviamo un'altra email con un'immagine motivazionale più allegra.
  • Registriamo i dati di 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 e propri script.

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

# Controlliamo la correttezza dei campi compilati
full_df <- fullXslx_df %>%
  mutate(
    # Prima di tutto rimuoviamo spazi e tabulazioni superflui, poi consideriamo il separatore come la virgola, infine controlliamo l'appartenenza ai 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)
  )

# Controlliamo l'esistenza del file con tutte le VM e lo rimuoviamo, se presente.
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 con 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 nella email
incorrect_df_filtered <- incorrect_df %>% 
  select(VM.Name, 
         IP.s, 
         Owner, 
         Subsystem, 
         Category,
         Creator,
         vCenter.Name,
         Creation.Date
  )

# Contiamo 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 trovate. In uscita 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 nella email, aggiungiamo 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(`Creatore Sospettato` = CreatedBy, 
           `Data di Creazione Sospettata` = CreatedOn)  

  # Formiamo il corpo della mail
  emailBody &lt;- paste0(
    &#039;<html>
                    <h3>Buongiorno, cari colleghi.</h3>
                    <p>Potete visualizzare informazioni complete e aggiornate sulle macchine virtuali sul disco H: qui:<p>
                    <p>\server.ruVM', sourceFileFormat, '</p>
                    <p>In allegato anche un elenco delle VM con <strong>dati non correttamente compilati,</strong> in totale sono <strong>', numberOfRows,'</strong>.</p>
                    <p>La tabella ha aggiunto 2 colonne supplementari. <strong>Creatore Sospettato</strong> e <strong>Data di Creazione Sospettata</strong>, ottenute dai log di vCenter delle ultime 2 settimane.</p>
                    <p>Si prega ai creatori delle macchine di confermare i dati e completare i campi correttamente. Le regole per la compilazione dei campi sono anch'esse 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 l'email con le VM mal 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)

  #### Successivamente 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, gentili colleghi</h3>
   <p>Potete visualizzare informazioni complete e aggiornate sulle macchine virtuali sul disco H: qui:<p>
    <p>\server.ruVM', sourceFileFormat, '</p>
    <p>Al momento, tutti i campi delle VM sono compilati correttamente</p>
    <p><img src="data/meme_correct.jpg"></p>
    </html>'
  )

  #### Creiamo l'email senza VM mal compilate ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "Informazioni aggregate",
            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 in PowerShell che estrae dai log i creatori delle macchine virtuali e le date di 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 particolare attenzione la libreria. xlsx, che ha permesso di rendere l'allegato all'email visivamente formattato (come ama la dirigenza), e non semplicemente 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)

Il risultato finale è circa così:

Report giornalieri sullo stato delle macchine virtuali tramite R e PowerShell

C'era anche un interessante aspetto riguardo la configurazione del pianificatore di Windows. Non riuscivo proprio a trovare i parametri giusti di permessi e impostazioni affinché tutto si avviasse come doveva. Alla fine, è stata trovata una libreria R che crea automaticamente un'attività per l'esecuzione di uno 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")

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

Separatamente riguardo al database

Dopo aver configurato lo script, sono emerse altre questioni. Ad esempio, avrei voluto trovare la data in cui la VM è stata eliminata, ma i log in vCenter sono già scomparsi. Poiché lo script salva i file in una cartella ogni giorno e non li pulisce (puliamo manualmente quando ce ne ricordiamo), posso visualizzare i file precedenti e trovare il primo file in cui non è presente la VM. Ma non è l'ideale.

Ho voluto creare un database storico.

In aiuto è venuta la funzionalità di MS SQL SERVER — la tabella temporale versionata dal sistema. Di solito viene tradotta come "tabelle temporali" (non "tabelle temporanee").

Puoi leggerne in dettaglio nella documentazione ufficiale di Microsoft..

In breve, creiamo una tabella, diciamo che avrà versioni, 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 saranno scritte le modifiche. Di conseguenza, otteniamo informazioni aggiornate e, tramite query non complesse, esempi delle quali sono forniti nella documentazione, possiamo vedere sia il ciclo di vita di una specifica 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 fino a quando non sarà completata la transazione di scrittura nella tabella temporanea. Pertanto, su tabelle con un alto 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 rispetto a quella memorizzata nel database e registrasse solo le righe modificate. Il codice non è particolarmente complesso, utilizza la libreria compareDF, ma lo riporterò di seguito.

Codice in 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, nel corso di alcuni mesi è stato messo ordine e viene mantenuto. A volte appaiono VM compilate in modo errato, ma lo script funge da buon promemoria e difficilmente una VM entra nella lista per due giorni consecutivi.

È stata anche fatta una predisposizione per l'analisi dei dati storici.

È chiaro che molte di queste cose possono essere realizzate non "alla buona", ma con software specializzati, ma il compito era interessante e, si potrebbe dire, facoltativo.

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

Report giornalieri sullo stato delle macchine virtuali tramite 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