Dagelijkse rapporten over de status van virtuele machines met behulp van R en PowerShell

Dagelijkse rapporten over de status van virtuele machines met behulp van R en PowerShell

Inleiding

Goedendag. Al een half jaar draait bij ons een script (meer precies een set scripts) die rapporten genereert over de status van virtuele machines (en niet alleen dat). Ik besloot mijn ervaring te delen en de code zelf. Ik rekende op kritiek en hoop dat dit materiaal nuttig kan zijn voor iemand.

Vorming van behoefte

We hebben veel virtuele machines (ongeveer 1500 VM's verspreid over 3 vCenters). Nieuwe machines worden vaak aangemaakt en oude machines worden regelmatig verwijderd. Om orde te behouden zijn er verschillende custom velden toegevoegd in vCenter om de VM's in subsystemen te verdelen, aan te geven of ze voor testdoeleinden zijn, en wie en wanneer ze zijn aangemaakt. Het menselijke element heeft ertoe geleid dat meer dan de helft van de machines ongebruikte velden heeft, wat het werk bemoeilijkte. Om de zes maanden raakte iemand gefrustreerd, startte een werkproces om deze gegevens te actualiseren, maar de resultaten waren al na anderhalve week weer verouderd.
Ik wil meteen verduidelijken dat iedereen begrijpt dat er aanvragen moeten zijn voor het aanmaken van machines, het proces voor hun creatie, enzovoort. En tegelijkertijd volgen iedereen dit proces strikt en is alles in orde. Helaas is het bij ons niet zo, maar dat is niet het onderwerp van het artikel 🙂

Uiteindelijk werd besloten om de controle op het juist invullen van de velden te automatiseren.
We besloten dat een dagelijkse e-mail met een lijst van verkeerd ingevulde machines naar alle verantwoordelijke ingenieurs en hun leidinggevenden een goed begin zou zijn.

Op dat moment had een van de collega's al een PowerShell-script geïmplementeerd dat elke dag volgens een schema informatie verzamelde over alle machines van alle vCenters en drie csv-documenten (elk voor zijn eigen vCenter) aanmaakte, die op een netwerkstation werden geplaatst. Er werd besloten om dit script als basis te nemen en uit te breiden met controles met behulp van de R-taal, waar we enige ervaring mee hadden.

In het proces van verbeteren groeide de oplossing uit tot het informeren via e-mail, een database met een hoofdtabel en een historische tabel (daarover later), evenals analyses van de vSphere-logs om de feitelijke makers van de VM's en de tijd van hun creatie te vinden.

Voor de ontwikkeling werden de IDE's RStudio Desktop en PowerShell ISE gebruikt.

Het script wordt uitgevoerd vanaf een standaard Windows virtuele machine.

Omschrijving van de algemene logica.

De algemene logica van de scripts is als volgt.

  • We verzamelen gegevens over virtuele machines met behulp van een PowerShell-script, dat we via R aanroepen, en we combineren het resultaat in één csv-bestand. De interactie tussen de talen verloopt op een vergelijkbare manier. (We hadden de gegevens rechtstreeks vanuit R in PowerShell als variabelen kunnen verzenden, maar dat is ingewikkeld, en met tussentijdse csv’s is het gemakkelijker om te debuggen en tussenresultaten te delen met anderen).
  • Met R stellen we de geldige parameters voor de velden op, waarvan we de waarden controleren. — We creëren een word-document dat de waarden van deze velden bevat voor opname in de informatiebrief, die als antwoord op de vragen van collega's zal dienen: "Maar hoe moet ik dit invullen?".
  • We laden de gegevens van alle VM's in vanuit csv met behulp van R, maken een dataframe aan, verwijderen onnodige velden en creëren een informatief xlsx-document dat samenvattende informatie over alle VM's bevat, welke we op een gemeenschappelijke resource publiceren.
  • We passen alle controles voor de juiste invulling van de velden toe op de dataframe van alle VM's en creëren een tabel die alleen de VM's bevat met verkeerd ingevulde velden (en alleen deze velden).
  • De verkregen lijst met VM's sturen we naar een ander PowerShell-script, dat de vCenter-logs bekijkt op zoek naar evenementen voor het creëren van VM's, wat ons in staat stelt om de geschatte tijd van de creatie van VM's en de vermoedelijke maker aan te geven. Dit voor het geval niemand toegeeft van wie de machine is. Dit script werkt niet snel, vooral als er veel logs zijn, daarom bekijken we alleen de laatste 2 weken en gebruiken we een workflow die het mogelijk maakt om informatie over meerdere VM's tegelijk te doorzoeken. In het scriptvoorbeeld zijn gedetailleerde opmerkingen over dit mechanisme opgenomen. We slaan het resultaat op in csv, dat we weer in R laden.
  • We creëren een mooi opgemaakt xlsx-document, waarin verkeerd ingevulde velden in het rood zijn gemarkeerd, filters zijn toegepast op sommige kolommen, en bovendien extra kolommen zijn opgenomen met de vermoedelijke makers en de tijd van creatie van de VM.
  • We create an email that includes a document describing acceptable field values and a table with incorrectly filled VMs. In the text, we specify the total number of incorrectly created VMs, a link to the overall resource, and a motivational image. If there are no incorrectly filled VMs, we send a different email with a more cheerful motivational image.
  • We record data for all VMs in the SQL Server database, taking into account the implemented mechanism of historical tables (a very interesting mechanism — which we will discuss in more detail later).

Here are the scripts.

The main file with R code.

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

# Controleer de juistheid van de ingevulde velden
full_df <- fullXslx_df %>%
  mutate(
    # Eerst verwijderen we alle overbodige spaties en tabs, daarna houden we rekening met het scheidingsteken komma, en vervolgens controleren we op geldige waarden,
    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)
  )

# Controleer of het bestand met alle VM's bestaat en verwijder het indien nodig.
if (file.exists(filenameAll)) {file.remove(filenameAll)}

#### Genereer xslx-bestand met rapport ####
# Algemene gegevens op een apart blad
full_df %&gt;% write.xlsx(file=filenameAll,
                       sheetName=names[1],
                       col.names=TRUE,
                       row.names=FALSE,
                       append=FALSE)

#### Genereer xslx-bestand met verkeerd ingevulde velden ####
# Genereer 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)

# Controleer of het bestand met alle VM's bestaat en verwijder het indien nodig.
if (file.exists(filenameIncVM)) {file.remove(filenameIncVM)}

# Sla de lijst met VM's met open velden op in csv
incorrect_df %&gt;%
  select(VM.Name) %&gt;%
  write_csv2(path = filenameIncVM, append = FALSE)

# Filter voor invoeging in de e-mail
incorrect_df_filtered <- incorrect_df %>% 
  select(VM.Name, 
         IP.s, 
         Owner, 
         Subsystem, 
         Category,
         Creator,
         vCenter.Name,
         Creation.Date
  )

# Tel het aantal rijen
numberOfRows <- nrow(incorrect_df)

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

if (numberOfRows > 0) {

  # Controleer of het bestand met makers bestaat en verwijder het indien nodig.
  if (file.exists(creatorsFilePath)) {file.remove(creatorsFilePath)}

  # Voer het PowerShell-script uit dat de makers van de gevonden VM's vindt. We krijgen een csv als output.
  system(paste0("powershell -File ", getCreatorsPath))

  # Lees het bestand met makers
  creators_df <- creatorsFilePath %>%
    read.csv2(stringsAsFactors = FALSE)

  # Filter voor invoeging in de e-mail, voeg gegevens van de tabel met makers toe
  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(`Veronderstelde maker` = CreatedBy, 
           `Veronderstelde creatiedatum` = CreatedOn)  

  # Genereer de e-mailinhoud
  emailBody &lt;- paste0(
    &#039;<html>
                    <h3>Goedendag, geachte collega's.</h3>
                    <p>U kunt de volledige actuele informatie over de virtuele machines bekijken op schijf H: hier:<p>
                    <p>\server.ruVM', sourceFileFormat, '</p>
                    <p>Ook in de bijlage een lijst van VM's met <strong>incorrect ingevulde</strong> velden. In totaal zijn er <strong>', numberOfRows, '</strong>.</p>
                    <p>De tabel bevat 2 extra kolommen. <strong>Veronderstelde maker</strong> en <strong>Veronderstelde aanmaakdatum</strong>, die uit de vCenter-logboeken van de afgelopen 2 weken zijn gehaald</p>
                    <p>Verzoek aan de makers van machines om de gegevens te verduidelijken en de velden correct in te vullen. De regels voor het invullen van velden zijn ook in de bijlage.</p>
                    <p><img src="data/meme.jpg"></p>
                    </html>'
  )

  # Controleer of het bestand bestaat
  if (file.exists(filenameIncorrect)) {file.remove(filenameIncorrect)}

  # Maak een mooie tabel met formaten enz.
  source(file = "email.R", local = T, encoding = "utf-8")

  #### Maak een e-mail met slecht ondertekende machines ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "VM's met incorrect ingevulde velden",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            attach.files = c(filenameIncorrect, filenameVmCreationRules),
            debug = FALSE)

  #### Hierna volgt een blok als er geen problemen zijn met de VM's ####
} else {

  # Maak de inhoud van de e-mail
  emailBody &lt;- paste0(
    &#039;<html>
    <h3>Goedemiddag, geachte collega's</h3>
   <p>U kunt de volledige actuele informatie over de virtuele machines bekijken op schijf H: hier:<p>
    <p>\server.ruVM', sourceFileFormat, '</p>
    <p>Daarnaast zijn op dit moment alle velden van de VM correct ingevuld</p>
    <p><img src="data/meme_correct.jpg"></p>
    </html>'
  )

  #### Maak een e-mail zonder slecht ingevulde VM's ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "Samenvattende informatie",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            debug = FALSE)
}

####### Gegevens opslaan in de DB #####

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

A script for getting a list of VMs in 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
    }

A PowerShell script that extracts the creators of virtual machines and their creation dates from the logs.

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

Particular attention should be given to the library. xlsx, which allowed the attachment to the email to be neatly formatted (as the management likes), rather than just a csv table.

Creating a nice xlsx document with a list of incorrectly filled machines.

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

The output looks approximately like this:

Dagelijkse rapporten over de status van virtuele machines met behulp van R en PowerShell

There was also an interesting nuance regarding the setup of the Windows scheduler. It was difficult to find the right parameters for permissions and settings so that everything would run as needed. Eventually, an R library was found that automatically creates a task to run the R script and even remembers the log file. Afterward, the task can be manually adjusted.

A snippet of R code with two examples that creates a task in the Windows scheduler.

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

## run the script after 62 seconds
taskscheduler_create(taskname = "getAllVm", rscript = myscript, 
                     schedule = "ONCE", starttime = format(Sys.time() + 62, "%H:%M"))

## run the script daily at 09:10
taskscheduler_create(taskname = "getAllVmDaily", rscript = myscript, 
                     schedule = "WEEKLY", 
                     days = c("MON", "TUE", "WED", "THU", "FRI"),
                     starttime = "02:00")

## delete tasks
taskscheduler_delete(taskname = "getAllVm")
taskscheduler_delete(taskname = "getAllVmDaily")

# Check the logs (last 4 lines)
tail(readLines("all_vm.log"), sep ="n", n = 4)

Separate discussion about the database.

After setting up the script, other questions began to arise. For example, I wanted to find the date when the VM was deleted, but the logs in vCenter had already been cleared. Since the script places files in a folder every day and doesn't clear them (we clear them manually when we remember), it is possible to look at the old files and find the first file in which the VM is absent. But that’s not ideal.

Ik wilde een historische database creëren.

De functionaliteit van MS SQL SERVER kwam van pas — system-versioned temporal table. Dit wordt meestal vertaald als temporele (niet tijdelijke) tabellen.

Je kunt er in detail over lezen in de officiële documentatie van Microsoft.

Kort samengevat — we maken een tabel, geven aan dat deze versiebeheer zal hebben en SQL Server creëert 2 extra datetime kolommen in deze tabel (de aanmaakdatum van het record en de einddatum van het record) en een extra tabel waarin de wijzigingen worden vastgelegd. Als resultaat krijgen we actuele informatie en, door middel van eenvoudige queries, waarvan voorbeelden in de documentatie staan, kunnen we of de levenscyclus van een specifieke virtuele machine bekijken of de status van alle VM's op een bepaald moment.

Wat betreft prestaties — de transactie voor het schrijven naar de hoofd tabel zal niet worden voltooid totdat de transactie voor het schrijven naar de tijdelijke tabel is voltooid. Dat wil zeggen, bij tabellen met een groot aantal schrijfbewerkingen moet deze functionaliteit voorzichtig worden geïmplementeerd, maar in ons geval is het echt een coole functie.

Om ervoor te zorgen dat het mechanisme correct werkt, moest ik een klein stuk code in R schrijven dat de nieuwe tabel zou vergelijken met de gegevens van alle VM's met degene die in de database is opgeslagen en alleen de gewijzigde rijen daar naar zou schrijven. De code is niet bijzonder ingewikkeld, maakt gebruik van de library compareDF, maar die zal ik ook hieronder laten zien.

R-code voor het schrijven van gegevens naar de 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)

Total

Als resultaat van de implementatie van het script is er gedurende enkele maanden orde geschapen en wordt dit onderhouden. Soms verschijnen er verkeerd ingevulde VM's, maar het script dient als een goede herinnering en zeldzaam komt een VM twee dagen achter elkaar in de lijst.

Er is ook een basis gelegd voor de analyse van historische gegevens.

Het is duidelijk dat veel hiervan niet 'met de hand' kan worden gerealiseerd, maar met gespecialiseerde software, maar de taak was interessant en kan als facultatief worden beschouwd.

R heeft zich opnieuw bewezen als een uitstekende universele taal, die niet alleen perfect geschikt is voor het oplossen van statistische taken, maar ook een uitstekende 'schakel' is tussen andere gegevensbronnen.

Dagelijkse rapporten over de status van virtuele machines met behulp van R en PowerShell

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster