Дневни отчети за състоянието на виртуалните машини със средства на R и PowerShell

Дневни отчети за състоянието на виртуалните машини със средства на R и PowerShell

Въведение

Добър ден. Вече половин година при нас работи скрипт (по-точно набор от скриптов), генериращ отчети за състоянието на виртуалните машини (и не само). Реших да споделя опита си в създаването и самия код. Разчитам на критика и на това, че този материал може да бъде полезен за някого.

Формулиране на нуждата

Виртуалните машини при нас са много (около 1500 ВМ, разпределени в 3 vCenter). Нови се създават, а стари се изтриват доста често. За запазване на реда бяха добавени няколко custom полета в vCenter, за разделяне на ВМ на Подсистеми, за указване дали са тестови, както и от кого и кога са създадени. Човешкият фактор доведе до факта, че повече от половината машини останаха с незапълнени полета, което усложняваше работата. На всеки шест месеца някой се ядосваше, започваше работа по актуализиране на тези данни, но резултатът спираше да бъде актуален още след около две седмици.
Веднага уточнявам, че всички разбират, че трябва да има заявки за създаване на машини, процес по тяхното създаване и т.н. И в същото време всички безусловно следват този процес и всичко е в ред. При нас, за съжаление, не е така, но това не е предмет на статията 🙂

В общи линии, бе взето решение да се автоматизира проверката за правилността на попълването на полета.
Решихме, че ежедневен имейл със списък на неправилно попълнените машини до всички отговорни инженери и техните началници ще бъде добро начало.

В момента един от колегите вече бе внедрил скрипт на PowerShell, който всеки ден по график събира информация за всички машини от всичките vCenter-и и формира 3 csv документа (всеки по свой vCenter), които се поставяха на общ диск. Бе взето решение да вземем този скрипт за основа и да го допълним с проверки, с помощта на езика R, по отношение на който имаше известен опит.

В процеса на разработка решението се обогати с уведомления по имейл, база данни с основна и историческа таблица (за това по-късно), както и анализ на логовете vSphere за търсене на фактическите създатели на vm и времето на тяхното създаване.

За разработката се използваха IDE RStudio Desktop и PowerShell ISE.

Скриптът се стартира от обикновена Windows виртуална машина.

Описание на общата логика.

Общата логика на скриптовете се оказа следната.

  • Събираме данни за виртуалните машини с помощта на PowerShell скрипт, който извикваме чрез R, а резултата обединяваме в един csv файл. Взаимодействието между езиците е направено по аналогичен начин. (можеше да се предават данни директно от R в PowerShell под формата на променливи, но това е сложно и при наличието на междинни csv файлове е по-лесно да се дебъгва и да се споделят междинни резултати).
  • С помощта на R формулираме допустимите параметри за полетата, чиито стойности проверяваме. — Формулираме Word документ, който ще съдържа стойностите на тези полета за вмъкване в информационно писмо, което ще бъде отговор на въпросите на колегите "Не, как трябва да го попълня?".
  • Зареждаме данните за всички ВМ от csv с помощта на R, формулираме dataframe, премахваме ненужните полета и създаваме информационен xlsx документ, който ще съдържа обобщена информация за всички ВМ и го публикуваме на общия ресурс.
  • К dataframe за всички ВМ прилагаме всички проверки за правилност на попълването на полета и формулираме таблица, съдържаща само ВМ с неправилно попълнени полета (и само тези полета).
  • Полученият списък с ВМ изпращаме на друг PowerShell скрипт, който ще проверява логовете на vCenter за събития за създаване на ВМ, което ще позволи да се посочи предполагаемото време за създаване на ВМ и предполагаемия създател. Това е в случай, че никой не признае чия е машината. Този скрипт не работи бързо, особено ако има много логове, затова разглеждаме само последните 2 седмици и използваме workflow, който позволява търсене на информация за няколко ВМ едновременно. В примера на скрипта има подробни коментари относно този механизъм. Резултатът съхраняваме в csv, който отново зареждаме в R.
  • Създаваме красиво форматиран xlsx документ, в който ще бъдат обозначени с червен цвят неправилно попълнените полета, приложени филтри към някои колони, а също така указани допълнителни колони, съдържащи предполагаемите създатели и времето за създаване на ВМ.
  • Създаваме електронно писмо, в което прикачваме документ, описващ допустимите стойности на полетата, както и таблица с неправилно попълнени ВМ. В текста посочваме общото количество неправилно създадени ВМ, връзка към общия ресурс и мотивираща картинка. Ако няма неправилно попълнени ВМ, изпращаме друго писмо с по-радостна мотивираща картинка.
  • Записваме данните за всички ВМ в БД SQL Server, като вземаме предвид внедрения механизъм за исторически таблици (много интересен механизъм — за който по-подробно по-долу).

Собствено скриптове

Основен файл с код на 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)

# Проверяваме правилността на въведените полета
full_df <- fullXslx_df %>%
  mutate(
    # Първо премахваме всички ненужни интервали и табулации, после взимаме под внимание разделителя запетая, след това проверяваме за допустимите стойности,
    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)
  )

# Проверяваме съществуването на файла с всички ВМ и го изтриваме, ако съществува.
if (file.exists(filenameAll)) {file.remove(filenameAll)}

#### Създаваме xslx файл с отчета ####
# Общи данни на отделен лист
full_df %&gt;% write.xlsx(file=filenameAll,
                       sheetName=names[1],
                       col.names=TRUE,
                       row.names=FALSE,
                       append=FALSE)

#### Създаваме xslx файл с неправилно попълнени полета ####
# Създаваме 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)

# Проверяваме съществуването на файла с всички ВМ и го изтриваме, ако съществува.
if (file.exists(filenameIncVM)) {file.remove(filenameIncVM)}

# Запазваме списъка с ВМ с незапълнени полета в csv
incorrect_df %&gt;%
  select(VM.Name) %&gt;%
  write_csv2(path = filenameIncVM, append = FALSE)

# Филтрираме за вмъкване в имейла
incorrect_df_filtered <- incorrect_df %>% 
  select(VM.Name, 
         IP.s, 
         Owner, 
         Subsystem, 
         Category,
         Creator,
         vCenter.Name,
         Creation.Date
  )

# Броим броя на редовете
numberOfRows <- nrow(incorrect_df)

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

if (numberOfRows > 0) {

  # Проверяваме съществуването на файла с творците и го изтриваме, ако съществува.
  if (file.exists(creatorsFilePath)) {file.remove(creatorsFilePath)}

  # Стартираме PowerShell скрипт, който ще намери създателите на откритите ВМ. В изхода ще получим csv.
  system(paste0("powershell -File ", getCreatorsPath))

  # Четем файла със създателите
  creators_df <- creatorsFilePath %>%
    read.csv2(stringsAsFactors = FALSE)

  # Филтрираме за вмъкване в имейла, добавяйки данни от таблицата със създателите
  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(`Предполагаем създател` = CreatedBy, 
           `Предполагаема дата на създаване` = CreatedOn)  

  # Създаваме тялото на имейла
  emailBody &lt;- paste0(
    &#039;<html>
                    <h3>Добър ден, уважаеми колеги.</h3>
                    <p>Пълната актуална информация относно виртуалните машини можете да видите на диск H: тук:<p>
                    <p>\server.ruVM', sourceFileFormat, '</p>
                    <p>Също така в приложението е списък с <strong>неправилно попълнени</strong> полета. Общият брой е <strong>', numberOfRows, '</strong>.</p>
                    <p>В таблицата бяха добавени 2 допълнителни колони. <strong>Предполагаем създател</strong> и <strong>Предполагаема дата на създаване</strong>, които се извличат от логовете на vCenter за последните 2 седмици</p>
                    <p>Моля, създателите на машините да уточнят данните и да попълнят полетата коректно. Правилата за попълване на полетата също са в приложението</p>
                    <p><img src="data/meme.jpg"></p>
                    </html>'
  )

  # Проверяваме дали файлът съществува
  if (file.exists(filenameIncorrect)) {file.remove(filenameIncorrect)}

  # Форматираме красивата таблица с формати и т.н.
  source(file = "email.R", local = T, encoding = "utf-8")

  #### Форматираме писмо с неправилно подписани машини ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "ВМ с неправилно попълнени полета",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            attach.files = c(filenameIncorrect, filenameVmCreationRules),
            debug = FALSE)

  #### По-късно ще последва блок, ако няма проблеми с ВМ ####
} else {

  # Форматираме тялото на писмото
  emailBody &lt;- paste0(
    &#039;<html>
    <h3>Добър ден, уважаеми колеги</h3>
   <p>Пълната актуална информация относно виртуалните машини можете да видите на диск H: тук:<p>
    <p>\server.ruVM', sourceFileFormat, '</p>
    <p>Също така, в момента, всички полета на ВМ са коректно попълнени</p>
    <p><img src="data/meme_correct.jpg"></p>
    </html>'
  )

  #### Форматираме писмо без неправилно попълнени ВМ ####
  send.mail(from = emailParams$from,
            to = emailParams$to,
            subject = "Сводна информация",
            body = emailBody,
            encoding = "utf-8",
            html = TRUE,
            inline = TRUE,
            smtp = emailParams$smtpParams,
            authenticate = TRUE,
            send = TRUE,
            debug = FALSE)
}

####### Записваме данни в БД #####

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

Скрипт за получаване на списък с ВМ на 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
    }

Скрипт на PowerShell, извличащ от логовете създателите на виртуалните машини и датите на тяхното създаване

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

Отделно внимание заслужава библиотеката xlsx, която позволи направата на вложение към писмото в изряден формат (както е предпочитано от ръководството), а не просто csv таблица.

Създаване на красив xlsx документ със списък на неправилно попълнени машини

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

На изхода получаваме нещо подобно:

Дневни отчети за състоянието на виртуалните машини със средства на R и PowerShell

Също така имаше интересен нюанс при настройването на Windows scheduler. Никак не успявахме да подберем правилните параметри на правата и настройките, за да се стартира всичко, както трябва. В крайна сметка беше намерена библиотека R, която сама създава задача за стартиране на R скрипт и дори не забравя за файла за логовете. После може ръчно да се коригира задачата.

Парче код на R с два примера, което създава задача в планирача на Windows

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

## стартираме скрипта след 62 секунди
taskscheduler_create(taskname = "getAllVm", rscript = myscript, 
                     schedule = "ONCE", starttime = format(Sys.time() + 62, "%H:%M"))

## стартираме скрипта всеки ден в 09:10
taskscheduler_create(taskname = "getAllVmDaily", rscript = myscript, 
                     schedule = "WEEKLY", 
                     days = c("MON", "TUE", "WED", "THU", "FRI"),
                     starttime = "02:00")

## изтриваме задачи
taskscheduler_delete(taskname = "getAllVm")
taskscheduler_delete(taskname = "getAllVmDaily")

# Гледаме логовете (последните 4 реда)
tail(readLines("all_vm.log"), sep = "n", n = 4)

Отделно за БД

След настройването на скрипта започнаха да се появяват други въпроси. Например, искахме да намерим датата, когато ВМ е била изтрита, а логовете във vCenter вече са изтрити. Тъй като скриптът съхранява файловете в папка всеки ден и не прави почистване (почистваме ръчно, когато си спомним), можем да разгледаме стари файлове и да намерим първия файл, в който съответната ВМ я няма. Но това не е идеално.

Появи се желание да се създаде историческа база данни.

На помощ дойде функционалността на MS SQL SERVER — времево версионирано таблично хранилище. Обикновено се превежда като временни (не времеви) таблици.

Може да се прочете подробно в официалната документация на Microsoft.

Ако накратко — създаваме таблица, указваме, че тя ще бъде с версии и SQL Server генерира 2 допълнителни колони с тип datetime в тази таблица (дата на създаване на записа и дата на приключване на живота на записа) и допълнителна таблица, в която ще се записват промените. В резултат получаваме актуална информация и, чрез не толкова сложни заявки, примери за които са дадени в документацията, можем да видим или жизнения цикъл на конкретна виртуална машина, или състоянието на всички ВМ в определен момент.

От гледна точка на производителността — транзакцията за запис в основната таблица няма да бъде завършена, докато не завърши транзакцията за запис в времевата таблица. Тоест, на таблици с голям обем операция по запис, тази функционалност трябва да бъде внедрена внимателно, но в нашия случай това е наистина много интересно решение.

За да функционира механизмът коректно, трябваше да напиша малко код на R, който сравнява новата таблица с данните за всички ВМ от тази, която се съхранява в БД и записва в нея само променените редове. Кодът не е особено сложен, използва библиотеката compareDF, но ще го предоставя по-долу.

Код на R за запис на данни в БД

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

Итого

В резултат на внедряването на скрипта, през последните месеци беше поддържан ред. Понякога неправилно попълнени ВМ се появяват, но скриптът служи като добро напомняне и рядко ВМ остава в списъка 2 дни подред.

Също така беше направено предварително проучване за анализ на исторически данни.

Разбира се, че много от това може да бъде реализирано не "на коленке", а с профилиран софтуер, но задачата беше интересна и, може да се каже, факултативна.

R отново показа, че е чудесен универсален език, който е отличен не само за решаване на статистически задачи, но също така служи като прекрасен "мост" между различни източници на данни.

Дневни отчети за състоянието на виртуалните машини със средства на R и PowerShell

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster