Codzienne raporty o stanie maszyn wirtualnych za pomocą R i PowerShell

Codzienne raporty o stanie maszyn wirtualnych za pomocą R i PowerShell

Wprowadzenie

Dzień dobry. Już od pół roku działa u nas skrypt (a właściwie zestaw skryptów), generujący raporty dotyczące stanu maszyn wirtualnych (i nie tylko). Postanowiłem podzielić się doświadczeniem w tworzeniu i samym kodem. Liczę na krytykę i na to, że ten materiał może okazać się przydatny dla kogoś.

Tworzenie zapotrzebowania

Maszyn wirtualnych mamy wiele (około 1500 VM rozproszonych po 3 vCenter). Nowe są tworzone, a stare usuwane dość często. Aby zachować porządek, dodano kilka pól niestandardowych w vCenter, aby rozdzielać VM na Podsystemy, wskazywać, czy są to maszyny testowe, a także kto i kiedy je stworzył. Czynnik ludzki doprowadził do tego, że ponad połowa maszyn pozostała z niezapełnionymi polami, co utrudniało pracę. Co pół roku ktoś się denerwował, zaczynał pracę nad aktualizacją tych danych, ale wynik przestawał być aktualny już po około półtora tygodnia.
Od razu zaznaczam, że wszyscy rozumieją, że powinny być zgłoszenia na tworzenie maszyn, proces ich tworzenia itd. i tym podobne. I wszyscy nieustannie przestrzegają tego procesu i panuje pełen porządek. U nas, niestety, nie jest tak, ale to nie jest temat artykułu 🙂

W każdym razie podjęto decyzję o automatyzacji sprawdzania poprawności wypełnienia pól.
Postanowiono, że codzienny e-mail z listą niewłaściwie wypełnionych maszyn do wszystkich odpowiedzialnych inżynierów i ich przełożonych będzie dobrym początkiem.

W tym momencie jeden z kolegów już wdrożył skrypt w PowerShell, który codziennie zgodnie z harmonogramem zbierał informacje o wszystkich maszynach we wszystkich vCenter i tworzył 3 dokumenty CSV (każdy dla swojego vCenter), które były umieszczane na wspólnym dysku. Postanowiono wziąć ten skrypt jako bazę i uzupełnić go o kontrole za pomocą języka R, w którym miałem pewne doświadczenie.

W trakcie rozwoju rozwiązanie wzbogacono o informowanie przez e-mail, bazę danych z główną i historyczną tabelą (o tym później), a także analizę logów vSphere w celu znalezienia rzeczywistych twórców VM i czasu ich stworzenia.

Do rozwoju używano IDE RStudio Desktop i PowerShell ISE.

Skrypt uruchamiany jest z zwykłej wirtualnej maszyny Windows.

Opis ogólnej logiki.

Ogólna logika skryptów wygląda następująco.

  • Zbieramy dane o maszynach wirtualnych za pomocą skryptu PowerShell, który wywołujemy przez R, a wynik łączymy w jeden plik csv. Interakcja zwrotna między językami została zrealizowana w podobny sposób. (można byłoby przesyłać dane bezpośrednio z R do PowerShell w postaci zmiennych, ale jest to skomplikowane, a posiadanie pośrednich plików csv ułatwia debugowanie i dzielenie się z kimś pośrednimi wynikami).
  • Za pomocą R tworzymy dopuszczalne parametry dla pól, których wartości sprawdzamy. — Tworzymy dokument word, który będzie zawierał wartości tych pól do wstawienia w informacyjnym piśmie, które będzie odpowiedzią na pytania kolegów "No dobrze, ale jak to powinienem wypełnić?".
  • Wczytujemy dane ze wszystkich VM z pliku csv za pomocą R, tworzymy dataframe, usuwamy niepotrzebne pola i tworzymy informacyjny dokument xlsx, który będzie zawierał podsumowanie informacji o wszystkich VM i który publikujemy na wspólnym zasobie.
  • Na dataframe dotyczący wszystkich VM stosujemy wszystkie kontrole poprawności wypełniania pól i tworzymy tabelę, zawierającą tylko VM z błędnie wypełnionymi polami (i tylko te pola).
  • Otrzymaną listę VM wysyłamy do innego skryptu PowerShell, który będzie przeszukiwał logi vCenter w poszukiwaniu zdarzeń tworzenia VM, co pozwoli wskazać przypuszczalny czas utworzenia VM i przypuszczalnego twórcę. To na wypadek, gdyby nikt się nie przyznawał, czyja to maszyna. Ten skrypt nie działa szybko, szczególnie jeśli jest wiele logów, więc sprawdzamy tylko ostatnie 2 tygodnie, a także korzystamy z workflow, które umożliwia wyszukiwanie informacji dla kilku VM jednocześnie. W przykładzie skryptu znajdują się szczegółowe komentarze dotyczące tego mechanizmu. Wynik zapisujemy w pliku csv, który ponownie wczytujemy do R.
  • Tworzymy ładnie sformatowany dokument xlsx, w którym niepoprawnie wypełnione pola będą podświetlone na czerwono, zastosowane zostaną filtry do niektórych kolumn, a także pojawią się dodatkowe kolumny zawierające przypuszczalnych twórców i czas utworzenia VM.
  • Tworzymy wiadomość e-mail, do której dołączamy dokument opisujący dopuszczalne wartości pól, a także tabelę z błędnie wypełnionymi VM. W treści podajemy ogólną liczbę błędnie utworzonych VM, link do ogólnego zasobu i motywujące obrazek. Jeśli nie ma błędnie wypełnionych VM, wysyłamy inną wiadomość z bardziej radosnym motywującym obrazkiem.
  • Zapisujemy dane o wszystkich VM w bazie danych SQL Server z uwzględnieniem wdrożonego mechanizmu tabel historycznych (bardzo interesujący mechanizm - o którym więcej dalej).

Właściwie skrypty

Główny plik z kodem w 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)

# Sprawdzamy poprawność wypełnionych pól
full_df <- fullXslx_df %>%
  mutate(
    # Najpierw usuwamy wszystkie zbędne spacje i tabulatory, następnie uwzględniamy przecinek jako separator, potem sprawdzamy przynależność do dopuszczalnych wartości,
    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)
  )

# Sprawdzamy istnienie pliku ze wszystkimi VM i usuwamy, jeśli istnieje.
if (file.exists(filenameAll)) {file.remove(filenameAll)}

#### Tworzymy plik xslx z raportem ####
# Ogólne dane na osobnym arkuszu
full_df %&gt;% write.xlsx(file=filenameAll,
                       sheetName=names[1],
                       col.names=TRUE,
                       row.names=FALSE,
                       append=FALSE)

#### Tworzymy plik xslx z błędnie wypełnionymi polami ####
# Tworzymy 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)

# Sprawdzamy istnienie pliku ze wszystkimi VM i usuwamy, jeśli istnieje.
if (file.exists(filenameIncVM)) {file.remove(filenameIncVM)}

# Zapisujemy listę VM z niezapewnionymi polami w csv
incorrect_df %&gt;%
  select(VM.Name) %&gt;%
  write_csv2(path = filenameIncVM, append = FALSE)

# Filtrujemy do wstawienia w mailu
incorrect_df_filtered <- incorrect_df %>% 
  select(VM.Name, 
         IP.s, 
         Owner, 
         Subsystem, 
         Category,
         Creator,
         vCenter.Name,
         Creation.Date
  )

# Liczymy liczbę wierszy
numberOfRows <- nrow(incorrect_df)

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

if (numberOfRows > 0) {

  # Sprawdzamy istnienie pliku z twórcami i usuwamy, jeśli istnieje.
  if (file.exists(creatorsFilePath)) {file.remove(creatorsFilePath)}

  # Uruchamiamy skrypt PowerShell, który znajdzie twórców znalezionych VM. Na wyjściu otrzymamy csv.
  system(paste0("powershell -File ", getCreatorsPath))

  # Odczytujemy plik z twórcami
  creators_df <- creatorsFilePath %>%
    read.csv2(stringsAsFactors = FALSE)

  # Filtrujemy do wstawienia w mailu, dodajemy dane z tabeli z twórcami
  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(`Przypuszczalny twórca` = CreatedBy, 
           `Przypuszczalna data stworzenia` = CreatedOn)  

  # Tworzymy treść e-maila
  emailBody &lt;- paste0(
    &#039;<html>
                    <h3>Dzień dobry, szanowni koledzy.</h3>
                    <p>Pełne aktualne informacje o maszynach wirtualnych można zobaczyć na dysku H: tutaj:<p>
                    <p>\server.ruVM', sourceFileFormat, '</p>
                    <p>W załączeniu znajduje się lista VM z <strong>niepoprawnie wypełnionymi</strong> polami. Łącznie jest ich <strong>', numberOfRows, '</strong>.</p>
                    <p>W tabeli pojawiły się 2 dodatkowe kolumny. <strong>Przypuszczalny twórca</strong> i <strong>Przypuszczalna data utworzenia</strong>, które są pobierane z logów vCenter z ostatnich 2 tygodni</p>
                    <p>Prośba do twórców maszyn o doprecyzowanie danych i poprawne wypełnienie pól. Zasady wypełniania pól również w załączeniu</p>
                    <p><img src="data/meme.jpg"></p>
                    </html>'
  )

  # Sprawdzamy istnienie pliku
  if (file.exists(filenameIncorrect)) {file.remove(filenameIncorrect)}

  # Tworzymy ładną tabelę z formatami itp.
  source(file = "email.R", local = T, encoding = "utf-8")

  #### Tworzymy wiadomość z źle podpisanymi maszynami ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "VM z niepoprawnie wypełnionymi polami",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            attach.files = c(filenameIncorrect, filenameVmCreationRules),
            debug = FALSE)

  #### Dalej będzie blok, jeśli nie ma problemów z VM ####
} else {

  # Tworzymy treść wiadomości
  emailBody &lt;- paste0(
    &#039;<html>
    <h3>Dzień dobry, szanowni koledzy</h3>
   <p>Pełne aktualne informacje o maszynach wirtualnych można zobaczyć na dysku H: tutaj:<p>
    <p>\server.ruVM', sourceFileFormat, '</p>
    <p>W obecnej chwili wszystkie pola VM są poprawnie wypełnione</p>
    <p><img src="data/meme_correct.jpg"></p>
    </html>'
  )

  #### Tworzymy wiadomość bez źle wypełnionych VM ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "Podsumowanie",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            debug = FALSE)
}

####### Zapisujemy dane do bazy danych #####

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

Skrypt uzyskujący listę VM w 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
    }

Skrypt w PowerShell, który wyciąga z logów twórców maszyn wirtualnych oraz daty ich stworzenia.

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

Osobną uwagę zasługuje biblioteka xlsx, która umożliwiła przygotowanie załącznika do e-maila w czytelnej formie (jak lubi kierownictwo), a nie po prostu jako tabela csv.

Tworzenie estetycznego dokumentu xlsx z listą błędnie wypełnionych maszyn.

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

Na wyjściu mniej więcej wygląda to tak:

Codzienne raporty o stanie maszyn wirtualnych za pomocą R i PowerShell

Był także ciekawy aspekt dotyczący konfiguracji harmonogramu Windows. Nie mogłem znaleźć odpowiednich parametrów uprawnień i ustawień, aby wszystko działało jak należy. Ostatecznie znalazłem bibliotekę R, która sama tworzy zadanie do uruchamiania skryptu R i nie zapomina o pliku z logami. Potem można ręcznie poprawić zadanie.

Fragment kodu w R z dwoma przykładami, który tworzy zadanie w harmonogramie Windows.

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

## uruchamiamy skrypt za 62 sekundy
taskscheduler_create(taskname = "getAllVm", rscript = myscript, 
                     schedule = "ONCE", starttime = format(Sys.time() + 62, "%H:%M"))

## uruchamiamy skrypt codziennie o 09:10
taskscheduler_create(taskname = "getAllVmDaily", rscript = myscript, 
                     schedule = "WEEKLY", 
                     days = c("MON", "TUE", "WED", "THU", "FRI"),
                     starttime = "02:00")

## usuwamy zadania
taskscheduler_delete(taskname = "getAllVm")
taskscheduler_delete(taskname = "getAllVmDaily")

# Sprawdzamy logi (ostatnie 4 linie)
tail(readLines("all_vm.log"), sep ="n", n = 4)

Osobno o bazie danych.

Po skonfigurowaniu skryptu zaczęły pojawiać się inne pytania. Na przykład, chciałem znaleźć datę, kiedy VM została usunięta, a logi w vCenter już się skasowały. Ponieważ skrypt codziennie umieszcza pliki w folderze i ich nie czyści (czyścimy ręcznie, gdy sobie przypomnimy), można przeglądać stare pliki i znaleźć pierwszy plik, w którym danej VM już nie ma. Ale to nie jest zbyt wygodne.

Zdecydowałem się stworzyć historyczną bazę danych.

W tym pomogła funkcjonalność MS SQL SERVER — tabela tymczasowa wersjonowana w systemie. Zwykle tłumaczona jest jako tabeli wersjonowane (nie jako tabeli czasowe).

Szczegółowe informacje można znaleźć w oficjalnej dokumentacji Microsoftu.

Mówiąc krótko — tworzymy tabelę, wskazujemy, że ma być wersjonowana, a SQL Server automatycznie tworzy dwie dodatkowe kolumny typu datetime (datę utworzenia rekordu i datę zakończenia życia rekordu) oraz dodatkową tabelę, do której zapisywane będą zmiany. W rezultacie uzyskujemy aktualne informacje i, dzięki prostym zapytaniom, których przykłady podano w dokumentacji, możemy zobaczyć zarówno cykl życia konkretnej maszyny wirtualnej, jak i stan wszystkich VM w danym momencie.

Z perspektywy wydajności — transakcja zapisu do głównej tabeli nie zostanie zakończona, dopóki nie zakończy się transakcja zapisu do tabeli tymczasowej. Oznacza to, że w tabelach z dużą liczba operacji zapisu tę funkcjonalność należy wprowadzać ostrożnie, jednak w naszym przypadku to bardzo ciekawa rzecz.

Aby mechanizm działał prawidłowo, musiałem w R napisać mały fragment kodu, który porównywał nową tabelę z danymi o wszystkich VM z tymi, które są przechowywane w bazie danych, i zapisywał tylko zmienione wiersze. Kod nie jest szczególnie skomplikowany, korzysta z biblioteki compareDF, ale również go poniżej przedstawię.

Kod w R do zapisywania danych w bazie danych

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

Podsumowując

W wyniku wdrożenia skryptu, w ciągu kilku miesięcy zaprowadzono i utrzymano porządek. Czasami pojawiają się źle wypełnione VM, ale skrypt służy jako dobre przypomnienie i rzadko która VM pojawia się w zestawieniu dwa dni z rzędu.

Dodatkowo stworzono fundamenty do analizy danych historycznych.

Oczywiście wiele z tego można zrealizować nie "na kolanie", a w odpowiednim oprogramowaniu, ale zadanie było interesujące i można powiedzieć, że opcjonalne.

R po raz kolejny pokazał, że jest doskonałym uniwersalnym językiem, który świetnie nadaje się nie tylko do rozwiązywania problemów statystycznych, ale także pełni rolę doskonałego "interfejsu" między innymi źródłami danych.

Codzienne raporty o stanie maszyn wirtualnych za pomocą R i PowerShell

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster