Introducción
Buenas tardes. Ya llevamos seis meses utilizando un script (más bien un conjunto de scripts) que genera informes sobre el estado de las máquinas virtuales (y no solo). Decidí compartir la experiencia de creación y el propio código. Espero críticas y que este material pueda ser útil para alguien.
Formación de necesidades
Tenemos muchas máquinas virtuales (alrededor de 1500 VM distribuidas en 3 vCenter). Se crean nuevas y se eliminan viejas con bastante frecuencia. Para mantener el orden, se añadieron varios campos personalizados en el vCenter para clasificar las VM en subsistemas, indicar si son de prueba, así como quién y cuándo fueron creadas. El factor humano llevó a que más de la mitad de las máquinas quedara con campos sin completar, lo que complicaba el trabajo. Cada seis meses, alguien se frustraba, iniciaba el trabajo de actualización de estos datos, pero el resultado dejaba de ser relevante en aproximadamente una semana y media.
Aclararé de inmediato que todos entienden que debe haber solicitudes para la creación de máquinas, un proceso para su creación, etc. Y al mismo tiempo, todos siguen estrictamente este proceso y todo está ordenado. Desafortunadamente, en nuestro caso no es así, pero ese no es el tema de este artículo 🙂
En resumen, se decidió automatizar la verificación de la correcta cumplimentación de los campos.
Se decidió que un correo diario con la lista de máquinas incorrectamente completadas a todos los ingenieros responsables y sus jefes sería un buen comienzo.
En este momento, uno de los colegas ya había implementado un script en PowerShell que recopilaba información sobre todas las máquinas de todos los vCenters a diario, según un horario, y generaba 3 documentos csv (cada uno para su vCenter), que se colocaban en un disco compartido. Se decidió tomar este script como base y complementarlo con verificaciones utilizando el lenguaje R, con el cual tenía cierta experiencia.
En el proceso de mejora, la solución creció con la notificación por correo electrónico, una base de datos con una tabla principal y una histórica (de esto hablaré más adelante), así como el análisis de los registros de vSphere para buscar a los creadores reales de las VM y el tiempo de su creación.
Para el desarrollo se utilizaron las IDE RStudio Desktop y PowerShell ISE.
El script se ejecuta desde una máquina virtual Windows estándar.
Descripción de la lógica general.
La lógica general de los scripts es la siguiente.
- Recopilamos datos sobre máquinas virtuales mediante un script de PowerShell, que invocamos a través de R, y combinamos el resultado en un solo csv. La interacción inversa entre los lenguajes se realiza de manera similar. (podría haber sido posible enviar los datos directamente de R a PowerShell como variables, pero eso es complicado, y tener csv intermedios facilita la depuración y el intercambio de resultados intermedios con otros).
- Usando R, generamos los parámetros permitidos para los campos cuyos valores verificamos. — Creamos un documento de Word que contendrá los valores de estos campos para insertar en un boletín informativo, que será la respuesta a las preguntas de los colegas "¿Pero cómo se supone que debo completar esto?".
- Cargamos los datos de todas las VM desde el csv usando R, formamos un dataframe, eliminamos los campos innecesarios y generamos un documento xlsx informativo que contendrá un resumen de toda la información sobre las VM, que colocamos en un recurso compartido.
- Aplicamos todas las verificaciones de corrección de los campos al dataframe de todas las VM y creamos una tabla que contiene solo las VM con campos incorrectamente completados (y solo esos campos).
- Enviamos la lista de VM obtenida a otro script de PowerShell, que revisará los registros de vCenter para eventos de creación de VM, lo que permitirá indicar el tiempo previsto de creación de la VM y el presunto creador. Esto es para el caso de que nadie reclame la propiedad de la máquina. Este script no funciona rápido, especialmente si hay muchos registros, por lo que solo revisamos las últimas 2 semanas, y también usamos un flujo de trabajo que permite buscar información sobre varias VM al mismo tiempo. En el ejemplo del script hay comentarios detallados sobre este mecanismo. El resultado se almacena en un csv, que nuevamente cargamos en R.
- Creamos un documento xlsx bellamente formateado, en el que se resaltarán en rojo los campos incorrectamente completados, se aplicarán filtros a algunas columnas, y también se incluirán columnas adicionales que contengan los presuntos creadores y el tiempo de creación de la VM.
- Formamos un correo electrónico al que adjuntamos un documento que describe los valores permitidos de los campos, así como una tabla con las máquinas virtuales (VM) mal llenadas. En el texto, indicamos el número total de VMs creadas incorrectamente, un enlace a un recurso general y una imagen motivacional. Si no hay VMs mal llenadas, enviamos otro correo con una imagen motivacional más alegre.
- Registramos los datos de todas las VMs en la base de datos SQL Server, teniendo en cuenta el mecanismo de tablas históricas implementado (un mecanismo muy interesante del que hablaremos más adelante).
Los scripts propiamente dichos.
El archivo principal con el código en 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)
# Verificamos la corrección de los campos llenados
full_df <- fullXslx_df %>%
mutate(
# Primero eliminamos todos los espacios y tabulaciones innecesarias, luego consideramos el separador coma, después verificamos la inclusión en los valores permitidos,
isSubsystemCorrect = Subsystem %>%
gsub("[[:space:]]", "", .) %>%
str_split(., ",") %>%
map(function(x) (all(x %in% AllowedValues$Subsystem))) %>%
as.logical(),
isOwnerCorrect = Owner %in% AllowedValues$Owner,
isCategoryCorrect = Category %in% AllowedValues$Category,
isCreatorCorrect = (!is.na(Creator) & Creator != ''),
isCreation.DateCorrect = map(Creation.Date, IsDate)
)
# Verificamos la existencia del archivo con todas las VM y lo eliminamos si existe.
if (file.exists(filenameAll)) {file.remove(filenameAll)}
#### Formamos el archivo xslx con el informe ####
# Datos generales en una hoja separada
full_df %>% write.xlsx(file=filenameAll,
sheetName=names[1],
col.names=TRUE,
row.names=FALSE,
append=FALSE)
#### Formamos el archivo xslx con Campos llenados incorrectamente ####
# Formamos df
incorrect_df <- full_df %>%
select(VM.Name,
IP.s,
Owner,
Subsystem,
Creator,
Category,
Creation.Date,
isOwnerCorrect,
isSubsystemCorrect,
isCategoryCorrect,
isCreatorCorrect,
vCenter.Name) %>%
filter(isSubsystemCorrect == F |
isOwnerCorrect == F |
isCategoryCorrect == F |
isCreatorCorrect == F)
# Verificamos la existencia del archivo con todas las VM y lo eliminamos si existe.
if (file.exists(filenameIncVM)) {file.remove(filenameIncVM)}
# Guardamos la lista de VM con campos incompletos en csv
incorrect_df %>%
select(VM.Name) %>%
write_csv2(path = filenameIncVM, append = FALSE)
# Filtramos para insertar en el correo
incorrect_df_filtered <- incorrect_df %>%
select(VM.Name,
IP.s,
Owner,
Subsystem,
Category,
Creator,
vCenter.Name,
Creation.Date
)
# Contamos el número de filas
numberOfRows <- nrow(incorrect_df)
#### Начало условия ####
# Дальше либо у нас есть неправильно заполненные поля, либо нет.
# Если есть - запускаем ещё один скрипт
if (numberOfRows > 0) {
# Verificamos la existencia del archivo con creadores y lo eliminamos si existe.
if (file.exists(creatorsFilePath)) {file.remove(creatorsFilePath)}
# Ejecutamos el script de PowerShell que encontrará los creadores de las VM encontradas. Obtendremos un csv como salida.
system(paste0("powershell -File ", getCreatorsPath))
# Leemos el archivo con creadores
creators_df <- creatorsFilePath %>%
read.csv2(stringsAsFactors = FALSE)
# Filtramos para insertar en el correo, agregamos datos de la tabla con creadores
incorrect_df_filtered <- incorrect_df_filtered %>%
select(VM.Name,
IP.s,
Owner,
Subsystem,
Category,
Creator,
vCenter.Name,
Creation.Date
) %>%
left_join(creators_df, by = "VM.Name") %>%
rename(`Creador sugerido` = CreatedBy,
`Fecha de creación sugerida` = CreatedOn)
# Formamos el cuerpo del correo
emailBody <- paste0(
'<html>
<h3>Buenos días, estimados colegas.</h3>
<p>La información completa y actualizada sobre las máquinas virtuales se puede ver en el disco H: aquí:<p>
<p>\server.ruVM', sourceFileFormat, '</p>
<p>También se adjunta una lista de máquinas virtuales con <strong>campos incorrectamente llenos.</strong> En total son <strong>', numberOfRows,'</strong>.</p>
<p>La tabla ha añadido 2 columnas adicionales. <strong>Creador presumible</strong> y <strong>Fecha de creación presumible</strong>, que se obtienen de los registros de vCenter de las últimas 2 semanas.</p>
<p>Se solicita a los creadores de las máquinas que aclaren los datos y completen los campos correctamente. Las reglas para completar los campos también están en el adjunto.</p>
<p><img src="data/meme.jpg"></p>
</html>'
)
# Comprobamos la existencia del archivo
if (file.exists(filenameIncorrect)) {file.remove(filenameIncorrect)}
# Formamos una tabla bonita con formatos, etc.
source(file = "email.R", local = T, encoding = "utf-8")
#### Formamos el correo con máquinas mal firmadas ####
send.mail(from = emailParams$from,
to = emailParams$to,
subject = "Máquinas virtuales con campos incorrectamente llenos",
body = emailBody,
encoding = "utf-8",
html = TRUE,
inline = TRUE,
smtp = emailParams$smtpParams,
authenticate = TRUE,
send = TRUE,
attach.files = c(filenameIncorrect, filenameVmCreationRules),
debug = FALSE)
#### A continuación, vendrá el bloque si no hay problemas con las máquinas virtuales ####
} else {
# Formamos el cuerpo del correo
emailBody <- paste0(
'<html>
<h3>Buen día, estimados colegas</h3>
<p>La información completa y actualizada sobre las máquinas virtuales se puede ver en el disco H: aquí:<p>
<p>\server.ruVM', sourceFileFormat, '</p>
<p>Además, en este momento, todos los campos de las máquinas virtuales están correctamente llenos.</p>
<p><img src="data/meme_correct.jpg"></p>
</html>'
)
#### Formamos el correo sin máquinas virtuales mal llenadas ####
send.mail(from = emailParams$from,
to = emailParams$to,
subject = "Información resumida",
body = emailBody,
encoding = "utf-8",
html = TRUE,
inline = TRUE,
smtp = emailParams$smtpParams,
authenticate = TRUE,
send = TRUE,
debug = FALSE)
}
####### Guardamos los datos en la base de datos #####
source(file = "DB.R", local = T, encoding = "utf-8")
Script para obtener la lista de VMs en PowerShell.
# Данные для подключения и другие переменные
$vCenterNames = @(
"vcenter01",
"vcenter02",
"vcenter03"
)
$vCenterUsername = "myusername"
$vCenterPassword = "mypassword"
$filename = "C:ScriptsgetVmdataallvmall-vm-$(get-date -f yyyy-MM-dd).csv"
$destinationSMB = "server.rumyfolder$vm"
$IP0=""
$IP1=""
$IP2=""
$IP3=""
$IP4=""
$IP5=""
# Подключение ко всем vCenter, что содержатся в переменной. Будет работать, если логин и пароль одинаковые (например, доменные)
Connect-VIServer -Server $vCenterNames -User $vCenterUsername -Password $vCenterPassword
write-host ""
# Создаём функцию с циклом по всем vCenter-ам
function Get-VMinventory {
# В этой переменной будет списко всех ВМ, как объектов
$AllVM = Get-VM | Sort Name
$cnt = $AllVM.Count
$count = 1
# Начинаем цикл по всем ВМ и собираем необходимые параметры каждого объекта
foreach ($vm in $AllVM) {
$StartTime = $(get-date)
$IP0 = $vm.Guest.IPAddress[0]
$IP1 = $vm.Guest.IPAddress[1]
$IP2 = $vm.Guest.IPAddress[2]
$IP3 = $vm.Guest.IPAddress[3]
$IP4 = $vm.Guest.IPAddress[4]
$IP5 = $vm.Guest.IPAddress[5]
If ($IP0 -ne $null) {If ($IP0.Contains(":") -ne 0) {$IP0=""}}
If ($IP1 -ne $null) {If ($IP1.Contains(":") -ne 0) {$IP1=""}}
If ($IP2 -ne $null) {If ($IP2.Contains(":") -ne 0) {$IP2=""}}
If ($IP3 -ne $null) {If ($IP3.Contains(":") -ne 0) {$IP3=""}}
If ($IP4 -ne $null) {If ($IP4.Contains(":") -ne 0) {$IP4=""}}
If ($IP5 -ne $null) {If ($IP5.Contains(":") -ne 0) {$IP5=""}}
$cluster = $vm | Get-Cluster | Select-Object -ExpandProperty name
$Bootime = $vm.ExtensionData.Runtime.BootTime
$TotalHDDs = $vm.ProvisionedSpaceGB -as [int]
$CreationDate = $vm.CustomFields.Item("CreationDate") -as [string]
$Creator = $vm.CustomFields.Item("Creator") -as [string]
$Category = $vm.CustomFields.Item("Category") -as [string]
$Owner = $vm.CustomFields.Item("Owner") -as [string]
$Subsystem = $vm.CustomFields.Item("Subsystem") -as [string]
$IPS = $vm.CustomFields.Item("IP") -as [string]
$vCPU = $vm.NumCpu
$CorePerSocket = $vm.ExtensionData.config.hardware.NumCoresPerSocket
$Sockets = $vCPU/$CorePerSocket
$Id = $vm.Id.Split('-')[2] -as [int]
# Собираем все параметры в один объект
$Vmresult = New-Object PSObject
$Vmresult | add-member -MemberType NoteProperty -Name "Id" -Value $Id
$Vmresult | add-member -MemberType NoteProperty -Name "VM Name" -Value $vm.Name
$Vmresult | add-member -MemberType NoteProperty -Name "Cluster" -Value $cluster
$Vmresult | add-member -MemberType NoteProperty -Name "Esxi Host" -Value $VM.VMHost
$Vmresult | add-member -MemberType NoteProperty -Name "IP Address 1" -Value $IP0
$Vmresult | add-member -MemberType NoteProperty -Name "IP Address 2" -Value $IP1
$Vmresult | add-member -MemberType NoteProperty -Name "IP Address 3" -Value $IP2
$Vmresult | add-member -MemberType NoteProperty -Name "IP Address 4" -Value $IP3
$Vmresult | add-member -MemberType NoteProperty -Name "IP Address 5" -Value $IP4
$Vmresult | add-member -MemberType NoteProperty -Name "IP Address 6" -Value $IP5
$Vmresult | add-member -MemberType NoteProperty -Name "vCPU" -Value $vCPU
$Vmresult | Add-Member -MemberType NoteProperty -Name "CPU Sockets" -Value $Sockets
$Vmresult | Add-Member -MemberType NoteProperty -Name "Core per Socket" -Value $CorePerSocket
$Vmresult | add-member -MemberType NoteProperty -Name "RAM (GB)" -Value $vm.MemoryGB
$Vmresult | add-member -MemberType NoteProperty -Name "Total-HDD (GB)" -Value $TotalHDDs
$Vmresult | add-member -MemberType NoteProperty -Name "Power State" -Value $vm.PowerState
$Vmresult | add-member -MemberType NoteProperty -Name "OS" -Value $VM.ExtensionData.summary.config.guestfullname
$Vmresult | Add-Member -MemberType NoteProperty -Name "Boot Time" -Value $Bootime
$Vmresult | add-member -MemberType NoteProperty -Name "VMTools Status" -Value $vm.ExtensionData.Guest.ToolsStatus
$Vmresult | add-member -MemberType NoteProperty -Name "VMTools Version" -Value $vm.ExtensionData.Guest.ToolsVersion
$Vmresult | add-member -MemberType NoteProperty -Name "VMTools Version Status" -Value $vm.ExtensionData.Guest.ToolsVersionStatus
$Vmresult | add-member -MemberType NoteProperty -Name "VMTools Running Status" -Value $vm.ExtensionData.Guest.ToolsRunningStatus
$Vmresult | add-member -MemberType NoteProperty -Name "Creation Date" -Value $CreationDate
$Vmresult | add-member -MemberType NoteProperty -Name "Creator" -Value $Creator
$Vmresult | add-member -MemberType NoteProperty -Name "Category" -Value $Category
$Vmresult | add-member -MemberType NoteProperty -Name "Owner" -Value $Owner
$Vmresult | add-member -MemberType NoteProperty -Name "Subsystem" -Value $Subsystem
$Vmresult | add-member -MemberType NoteProperty -Name "IP's" -Value $IPS
$Vmresult | add-member -MemberType NoteProperty -Name "vCenter Name" -Value $vm.Uid.Split('@')[1].Split(':')[0]
# Считаем общее и оставшееся время выполнения и выводим на экран результаты. Использовалось для тестирования, но по факту оказалось очень удобно.
$elapsedTime = $(get-date) - $StartTime
$totalTime = "{0:HH:mm:ss}" -f ([datetime]($elapsedTime.Ticks*($cnt - $count)))
clear-host
Write-Host "Processing" $count "from" $cnt
Write-host "Progress:" ([math]::Round($count/$cnt*100, 2)) "%"
Write-host "You have about " $totalTime "for cofee"
Write-host ""
$count++
# Выводим результат, чтобы цикл "знал" что является результатом выполнения одного прохода
$Vmresult
}
}
# Вызываем получившуюся функцию и сразу выгружаем результат в csv
$allVm = Get-VMinventory | Export-CSV -Path $filename -NoTypeInformation -UseCulture -Force
# Пытаемся выложить полученный файл в нужное нам место и, в случае ошибки, пишем лог.
try
{
Copy-Item $filename -Destination $destinationSMB -Force -ErrorAction SilentlyContinue
}
catch
{
$error | Export-CSV -Path $filename".error" -NoTypeInformation -UseCulture -Force
}Script en PowerShell que extrae de los registros el creador de las máquinas virtuales y las fechas en que fueron creadas.
# Путь к файлу, из которого будем доставать список 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"
La biblioteca merece una atención especial. , que permitió hacer que el archivo adjunto al correo estuviera visualmente formateado (como le gusta a la dirección), y no simplemente como una tabla csv.
Generación de un bonito documento xlsx con la lista de máquinas mal llenadas.
# Создаём новую книгу
# Возможные значения : "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)El resultado es más o menos así:
También hubo un detalle interesante sobre la configuración del programador de Windows. No lograba encontrar los parámetros correctos de derechos y configuraciones para que todo se ejecutara como debía. Al final, se encontró una biblioteca en R que crea automáticamente la tarea para ejecutar el script en R y no olvida el archivo de registros. Luego se puede ajustar manualmente la tarea.
Un fragmento de código en R con dos ejemplos que crea una tarea en el programador de Windows.
library(taskscheduleR)
myscript <- file.path(getwd(), "all_vm.R")
## ejecutamos el script después de 62 segundos
taskscheduler_create(taskname = "getAllVm", rscript = myscript,
schedule = "ONCE", starttime = format(Sys.time() + 62, "%H:%M"))
## ejecutamos el script cada día a las 09:10
taskscheduler_create(taskname = "getAllVmDaily", rscript = myscript,
schedule = "WEEKLY",
days = c("MON", "TUE", "WED", "THU", "FRI"),
starttime = "02:00")
## eliminamos tareas
taskscheduler_delete(taskname = "getAllVm")
taskscheduler_delete(taskname = "getAllVmDaily")
# Vemos los registros (las últimas 4 líneas)
tail(readLines("all_vm.log"), sep ="n", n = 4)Hablemos sobre la base de datos.
Después de configurar el script, empezaron a surgir otras preguntas. Por ejemplo, había interés en encontrar la fecha en que se eliminó una VM, y los registros en vCenter ya estaban borrados. Dado que el script guarda archivos en una carpeta todos los días y no limpia (limpiamos manualmente cuando recordamos), se pueden revisar los archivos antiguos y encontrar el primer archivo en el que no aparece la VM en cuestión. Pero eso no es lo ideal.
Quería crear una base de datos histórica.
El funcionalidad de MS SQL SERVER llegó al rescate: la tabla temporal versionada. Normalmente se traduce como tablas temporales (no como tablas de tiempo).
Se puede leer en detalle en .
En resumen, creamos una tabla, indicamos que tendrá versionado y SQL Server crea 2 columnas adicionales de datetime en esta tabla (una para la fecha de creación del registro y otra para la fecha de finalización de la vida del registro) y una tabla adicional donde se registrarán los cambios. Así obtenemos información actualizada y, mediante consultas sencillas, ejemplos de las cuales están en la documentación, podemos ver ya sea el ciclo de vida de una máquina virtual específica o el estado de todas las VM en un momento determinado.
Desde el punto de vista del rendimiento, la transacción de escritura en la tabla principal no se completará hasta que se complete la transacción de escritura en la tabla temporal. Es decir, en tablas con una gran cantidad de operaciones de escritura, esta funcionalidad debe implementarse con precaución, pero en nuestro caso es realmente algo interesante.
Para que el mecanismo funcionara correctamente, tuve que escribir un pequeño trozo de código en R que comparara la nueva tabla con los datos de todas las VM con la que se almacena en la base de datos y solo escribiera las filas que hayan cambiado. El código no es muy complicado, utiliza la biblioteca compareDF, pero también lo proporcionaré más abajo.
Código en R para escribir datos en la base de datos
# Подцепляем пакеты
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
Como resultado de la implementación del script, en unos meses se ha establecido y se mantiene el orden. A veces aparecen VM mal completadas, pero el script sirve como un buen recordatorio y rara vez una VM entra en la lista dos días seguidos.
También se realizó un trabajo preliminar para el análisis de datos históricos.
Es evidente que mucho de esto se puede realizar no 'a mano', sino con software especializado, pero la tarea fue interesante y, se puede decir, opcional.
R nuevamente demostró ser un excelente lenguaje universal, que es ideal no solo para resolver problemas estadísticos, sino que también actúa como un excelente 'puente' entre otras fuentes de datos.
Fuente: habr.com
