Переписка с владельцем HWINFO, Мартином Маликом (mm@hwinfo.com), в качестве предисловия:

Я:
Hi Martin, my name is Dmitry.
I am working system administrator in Russia.
I want to do auto hardware reporting from all computers of my company.
Idea is run bach file (or something like that) and get text file with current system configuration (or may be sent the report on email).
I think you're SDK of HWINFO can do that.
Can you help me? Thanks!

Мартин:
It’s possible to do that with the HWiNFO SDK, but it’s not freeware and the price starts from 1000 EUR per year (including updates&support). So I’m not sure if this would be a choice for you.


В жизни наверно любого IT отдела наступает момент, когда необходимо провести инвентаризацию парка ПК. Одно дело, когда все они включены в домен, находятся в этом же здании и их не много, и совсем другое, когда компьютеров несколько сотен и находятся они по городам и весям…
HWINFO за SDK просит 1000 Евро за год…
Нафиг надо :), напишу сам, на Ruby, тем более что у Windows есть такой замечательный инструмент, как Windows Management Instrumentation (WMI).

Шаг 1. Класс для получения данных из WMI


На самом деле класс для этого создавать не обязательно, но в качестве учебных целей работы с классами — думаю не повредит.
Итак, напишем вот такой код:

require 'win32ole'
require 'ruby-wmi'

class GetWMIData
def initialize(debug=false)
	@debug=debug
end
def getWMI(st)
	wmi = WIN32OLE.connect("winmgmts://")
	res = wmi.ExecQuery("select * from #{st}")
	return res
end
def getCPU
	res=[]
	puts "DEBUG: Центральный процессор" if @debug
	res << "============================"
	res << "====Центральный процессор==="
	res << "============================"
	for get in self.getWMI("Win32_Processor") do
		res << "CPU Имя: #{get.Caption}" rescue nil
		res << "CPU Производитель: #{get.Manufacturer}" rescue nil
		res << "CPU Модель: #{get.Name}" rescue nil
		res << "CPU Макс. частота: #{get.MaxClockSpeed}" rescue nil
		res << "CPU Количество ядер: #{get.NumberOfCores}" rescue nil
		res << "CPU S/n: #{get.ProcessorId}" rescue nil
		res << "CPU Ревизия: #{get.Revision}" rescue nil
		res << " "	
	end
	return res
end
end

Итак, если этот класс инициализировать как:
w=GetWMIData.new
puts w.getCPU

Интерпретатор выдаст нам значения параметров центрального процессора:
ruby>============================
ruby>====Центральный процессор===
ruby>============================
ruby>CPU Имя: x86 Family 15 Model 2 Stepping 7
ruby>CPU Производитель: GenuineIntel
ruby>CPU Модель: Intel(R) Celeron(R) CPU 2.00GHz
ruby>CPU Макс. частота: 2040
ruby>CPU Количество ядер: 1
ruby>CPU S/n: BFEBFBFF00000F27
ruby>CPU Ревизия: 519

Уже неплохо. Подробно на каждом блоке данных я останавливаться не буду, все они (процессор, ОС, программы, материнка и т.д.) будут описаны в полном коде.

Шаг 2. Отправка отчета на почту


require 'mail'
def mailing(data)	
Mail.defaults do
  delivery_method :smtp, address: mail_server, port: mail_port, user_name: mail_user, password: mail_psw
end
mail = Mail.new
mail.from ="mail_from@myil.ru"
mail.to = "mail_to@admin.ru"
#Здесь к теме я добавляю параметры из массива, имя ПК и Серийник операционки
#(чтобы письма можно было удобнее сортировать)
mail.subject = "Тема письма"
mail.body	= "Текст письма"+data.join("\n")
mail.charset = "UTF-8"
	
#Отправляю
if mail.deliver!
	puts "Почта отправлена"
	return true 
else
	puts "ОШИБКА! Сбой отправки почты!"
	return false
end
end

Если данную функцию запустить с нашим предыдущим кодом как:
w=GetWMIData.new
mailing(w.getCPU)

то в случае успеха, на адрес администратора mail_to@admin.ru придет письмо с конфигурацией процессора текущего компьютера, а интерпретатор выдаст нам строку «Почта отправлена».

Шаг 3. Шифрование отчета


Согласитесь, что часто в организации есть некоторая часть ПК, не подключенных к общей сети. Тем не менее данные с них тоже нужно получить, и для того чтобы отчет с характеристиками не был «подделан» или «в шутку исправлен» сотрудниками — мы его зашифруем.
Поможет нам в этом 7zip:
require 'seven_zip_ruby'

def zipFile(file_to_zip)
begin
File.open("Отчет.7z", "wb") do |file|
	SevenZipRuby::Writer.open(file, { password: "superpassowrd" }) do |szr|
		szr.add_file file_to_zip
		end
	end	
puts "Процедура сжатия завершена успешно"
return true
rescue => error
	puts "Ошибка сжатия файла"
	puts error
	return false
end	
end

Если в процедуру zipFile передать файл, то на выходе появится «Отчет.7z», зашифрованный паролем «superpassowrd». Дальше мы просим оператора отправить данный файл нам например почтой.

Вот и вся логика. Добавлю к этому маркеры, которыми мы будем помечать письмо, информационные сообщения для пользователей, дебаги, и так по мелочи, и получим:

Полный код


Под спойлером
require 'win32ole'
require 'ruby-wmi'
require 'mail'
require 'socket'
require 'seven_zip_ruby'

class GetWMIData

	def initialize(debug=false)
		@prgrs=0
		@markers=[]
		@debug=debug
	end
	
	def marker(mark="")
		@markers << mark
		return @markers
	end

	def getWMI(st)
		wmi = WIN32OLE.connect("winmgmts://")
		res = wmi.ExecQuery("select * from #{st}")
		return res
	end

	def getMB
		res=[]
		@prgrs+=10
		puts "DEBUG: Материнская плата" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "====Материнская плата======="
		res << "============================"
		for get in self.getWMI("Win32_BaseBoard") do
			res << "MB Имя: #{get.Description}" rescue nil
			res << "MB Производитель: #{get.Manufacturer}" rescue nil
			res << "MB Модель: #{get.Model}" rescue nil
			res << "MB Модель2: #{get.Product}" rescue nil
			res << "MB Модель3: #{get.ProductMB}" rescue nil
			res << "MB Серийый №: #{get.SerialNumber}" rescue nil
			res << "MB Ревизия: #{get.Version}" rescue nil
			res << " "	
		end
		return res
	end

	def getOS
		res=[]
		@prgrs+=10
		puts "DEBUG: Операционная система" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "====Операционная система===="
		res << "============================"
		for get in self.getWMI("Win32_OperatingSystem") do
			#Добавляю нужные елементы в Маркер, затем буду применять это в теме письма
			self.marker << "#{get.CSName}" rescue nil
			#self.marker << "#{get.SerialNumber}" rescue nil
			res << "OS ПК: #{get.CSName}" rescue nil
			res << "OS Имя: #{get.Caption}" rescue nil
			res << "OS Сборка: #{get.BuildNumber}" rescue nil
			res << "OS Обновление: #{get.CSDVersion}" rescue nil		
			res << "OS Пользователей: #{get.NumberOfUsers}" rescue nil
			res << "OS Организация: #{get.Organization}" rescue nil
			res << "OS Пользователь: #{get.RegisteredUser}" rescue nil
			res << "OS S/n: #{get.SerialNumber}" rescue nil
			mem=get.TotalVisibleMemorySize.to_i/1024/1024 rescue nil
			res << "OS Всего RAM (Gb): "+mem.to_s rescue nil
			res << " "
		end
		return res
	end

	def getCPU
		res=[]
		@prgrs+=10
		puts "DEBUG: Центральный процессор" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "====Центральный процессор==="
		res << "============================"
		for get in self.getWMI("Win32_Processor") do
			#Добавляю нужные елементы в Маркер, затем буду применять это в теме письма
			self.marker << "#{get.ProcessorId}" rescue nil
			res << "CPU Имя: #{get.Caption}" rescue nil
			res << "CPU Производитель: #{get.Manufacturer}" rescue nil
			res << "CPU Модель: #{get.Name}" rescue nil
			res << "CPU Макс. частота: #{get.MaxClockSpeed}" rescue nil
			res << "CPU Количество ядер: #{get.NumberOfCores}" rescue nil
			res << "CPU S/n: #{get.ProcessorId}" rescue nil
			res << "CPU Ревизия: #{get.Revision}" rescue nil
			res << " "	
		end
		return res
	end

	def getMEM
		res=[]
		@prgrs+=10
		puts "DEBUG: Оперативная память" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "=====Оперативная память====="
		res << "============================"
		for get in self.getWMI("Win32_PhysicalMemory") do
			res << "MEM Слот: #{get.BankLabel}" rescue nil
			res << "MEM ID Производителя: #{get.Manufacturer}" rescue nil
			res << "MEM Модель: #{get.Model}" rescue nil
			res << "MEM Частота шины: #{get.Speed}" rescue nil			
			mem=get.Capacity.to_i/1024/1024 rescue nil
			res << "MEM Объем (Gb): "+mem.to_s rescue nil
			res << " "	
		end
		return res
	end

	def getVID
		res=[]
		@prgrs+=10
		puts "DEBUG: Видео система" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "=======Видео система========"
		res << "============================"
		for get in self.getWMI("Win32_VideoController") do
			res << "VIDEO Адаптер: #{get.DeviceID}" rescue nil
			res << "VIDEO Модель: #{get.Description}" rescue nil
			res << "VIDEO Производитель: #{get.AdapterCompatibility}" rescue nil
			res << "VIDEO Чип: #{get.VideoProcessor}" rescue nil			
			mem=get.AdapterRam.to_i/1024/1024 rescue nil
			res << "VIDEO Объем (Gb): "+mem.to_s rescue nil
			res << " "	
		end
		return res
	end

	def getHDD
		res=[]
		@prgrs+=10
		puts "DEBUG: Жесткие диски" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "=======Жесткие диски========"
		res << "============================"
		for get in self.getWMI("Win32_DiskDrive") do
			res << "HDD Описание: #{get.Caption}" rescue nil
			res << "HDD В системе: #{get.DeviceID}" rescue nil			
			res << "HDD Модель: #{get.model}" rescue nil	
			mem=get.Size.to_i/1024/1024 rescue nil
			res << "HDD Объем (Gb): "+mem.to_s rescue nil
			res << " "	
		end
		return res
	end

	def getPRINT
		res=[]
		@prgrs+=10
		puts "DEBUG: Установленные принтеры" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "===Установленные принтеры==="
		res << "============================"
		for get in self.getWMI("Win32_Printer") do
			res << "PRNTR Описание: #{get.Caption}" rescue nil		
			res << "PRNTR Драйвер: #{get.DriverName}" rescue nil			
			res << "PRNTR Порт: #{get.PortName}" rescue nil	
			res << " "	
		end
		return res
	end

	def getMON
		res=[]
		@prgrs+=10
		puts "DEBUG: Подключенные мониторы" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "===Подключенные мониторы===="
		res << "============================"
		for get in self.getWMI("Win32_DesktopMonitor") do
			res << "MONITOR Модель: #{get.Description}" rescue nil	
			res << "MONITOR Производитель: #{get.MonitorManufacturer}" rescue nil			
			res << " "	
		end
		return res
	end

	def getSOFT
		res=[]
		@prgrs+=10
		puts "DEBUG: Установленные программы" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "==Установленные программы==="
		res << "============================"
		for get in self.getWMI("Win32_Product") do
			res << "SOFT Название: #{get.Caption}" rescue nil	
			res << "SOFT Версия: #{get.Version}" rescue nil	
			res << "SOFT Дата установки: #{get.InstallDate}" rescue nil	
			res << " "	
		end
		return res
	end

	def getPROC
		res=[]
		@prgrs+=10
		puts "DEBUG: Запущенные процессы" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "====Запущенные процессы====="
		res << "============================"
		for get in self.getWMI("Win32_process") do
			res << "PROCS Имя: #{get.Name}" rescue nil	
		end
		return res
	end
	
	def getIP
		res=[]
		@prgrs+=5
		puts "DEBUG: IP Адреса" if @debug
		puts ("Сбор информации: "+@prgrs.to_s+" %")
		res << "============================"
		res << "==========IP Адреса========="
		res << "============================"
		for get in self.getWMI("Win32_NetworkAdapterConfiguration") do
			#Здесь начинаем записывать, только если адрес не пустой
			a=get.IPAddress rescue nil
			m=get.MACAddress rescue nil
			if a 
			res << "IP Addr: "+a.join(" ")
			end
			if m
			res << "IP MAC: "+m.to_s
			res << " "
			end
		end
		return res
	end
end

def avalable?(debug,server,port)
	sock = Socket.new(:INET, :STREAM)
	return true if sock.connect(Socket.sockaddr_in(port, server))
    rescue => error
		puts "DEBUG: Ошибка проверки доступности сервера" if debug
		puts error if debug
	return false
end

def mailing(debug,data,mail_server,mail_port,mail_user,mail_psw,mail_from,mail_to,mail_subj,mail_text,markers)	
	Mail.defaults do
	  delivery_method :smtp, address: mail_server, port: mail_port, user_name: mail_user, password: mail_psw
	end
	mail = Mail.new
	mail.from		=	mail_from
	mail.to			=	mail_to
	#Здесь к теме я добавляю параметры из массива, имя ПК и Серийник операционки
	#(чтобы письма можно было удобнее сортировать)
	mail.subject	=	mail_subj+markers.join(". ")
	mail.body		=	mail_text+data.join("\n")
	mail.charset	=	"UTF-8"
	
	puts "DEBUG: Попытка отправить почту" if debug
	if mail.deliver!
		puts "DEBUG: Почта отправлена" if debug
		return true 
	else
		puts "DEBUG: ОШИБКА! Сбой отправки почты!" if debug
		return false
	end
end

def deleteFile(debug,file)
	begin
	File.delete(file)
	puts "DEBUG: Процедура удаления завершена успешно" if debug	
	return true
	rescue => error
		puts "DEBUG: Ошибка удаления файла" if debug
		puts error if debug
		return false
	end	
end

def zipFile(debug,file_zip,file_psw,file_temp)
	begin
	File.open(file_zip, "wb") do |file|
		SevenZipRuby::Writer.open(file, { password: file_psw }) do |szr|
			szr.add_file file_temp
			end
		end	
	puts "DEBUG: Процедура сжатия завершена успешно" if debug	
	return true
	rescue => error
		puts "DEBUG: Ошибка сжатия файла" if debug
		puts error if debug
		return false
	end	
end

def writeFile(debug,data,file_temp)	
	begin
		File.open(file_temp, 'w+') do |f|				
			f.puts(data)
		end		
	puts "DEBUG: Процедура записи завершена успешно" if debug		
	return true	
	rescue => error
		puts "DEBUG: Ошибка создания файла" if debug
		puts error if debug
		return false
	end	
end

def writeANDzip(debug=false,data,file_temp,file_zip,file_psw)
	begin
		if writeFile(debug,data,file_temp)
			puts "DEBUG: Записано" if debug
			if zipFile(debug,file_zip,file_psw,file_temp)
				puts "DEBUG: сжато" if debug
				if deleteFile(debug,file_temp)
					puts "DEBUG: Удалено" if debug
					return true
				end
			end
		end
	rescue => error
		puts "DEBUG: Ошибка процедуры записи и сжатия" if debug
		puts error if debug
		return false
	end
end

def beginning(debug=false,file_name,file_exetemp,file_exezip,file_psw,mail_server,mail_port,mail_user,mail_psw,mail_from,mail_to,mail_subj,mail_text)	
	thread=[]
	result=[]
	markers=[]
	w=GetWMIData.new(debug)
	wMI=[w.getOS,w.getMB,w.getCPU,w.getMEM,w.getVID,w.getHDD,w.getPRINT,w.getMON,w.getSOFT,w.getPROC]
	#wMI=[w.getOS,w.getMB,w.getCPU]
	wMI.each do |wmi|
		thread << Thread.new {result << wmi}		
	end.each(&:join)
	markers=w.marker
	
	f_name			=file_name+markers.join(".")
	f_zip			=f_name+file_exezip
	f_temp			=f_name+file_exetemp

#Теперь пробуем отправить то что мы собрали
#Сначала проверяем доступен ли почтовый	сервер (ПК может оказаться не подключен к интернету)
#Если доступен - отправляем почту
#Если не доступен, шифруем, записываем файл на рабочий стол и просим оператора отправить его нам	
	puts "\nDEBUG: Все данные собраны.Подготовка данных к отправке" if debug
	puts "DEBUG: Проверяю доступность почтового сервера" if debug
	#Проверка доступности почты	
	if avalable?(debug,mail_server,mail_port)	
		puts "DEBUG: Почтовый сервер доступен." if debug
		#Если доступна, отправляем почту
		if mailing(debug,result,mail_server,mail_port,mail_user,mail_psw,mail_from,mail_to,mail_subj,mail_text,markers)
			puts "DEBUG: Письмо отправлено" if debug
			puts "\nДанные успешно отправлены!\n\nДля выхода из программы нажмите < Enter >"
			choice = gets.chomp.downcase
			if choice
			end
		else
		#Если во время отправки возникли ошибки,
			puts "\nОШИБКА! Сбой отправки почты!"
			puts "DEBUG: Пробую создать файл: "+f_zip if debug
			#Пробуем записать файл на рабочий стол
			if writeANDzip(debug,result,f_temp,f_zip,file_psw)		
				puts "DEBUG: Файл записан в "+f_zip if debug
				puts "В папке с программой создан файл: "+f_zip+"\nПожалуйста, самостоятельно отправьте его на "+mail_to+"\n\nДля выхода из программы нажмите < Enter >"
				choice = gets.chomp.downcase
				if choice
				end
			#Если не можем записать - выходим
			else
				puts "DEBUG: Не могу создать файл отчета!"+f_zip if debug
				puts "Ошибка создания файла отчета!\nПожалуйста обратитесь к системному администратору "+mail_to+"\n\nДля выхода из программы нажмите < Enter >"
				choice = gets.chomp.downcase
				if choice
				end
			end #if writeFile(result,file_path,file_psw)
		end	#if mailing
	#Если почта не доступна
	else
		puts "\nОШИБКА! Почтовый сервер не доступен!"
		#Пробуем создать файл
		puts "DEBUG: Пробую создать файл: "+f_zip if debug	
		#Если можно создавать файлы, записываем результат в темп
		if writeANDzip(debug,result,f_temp,f_zip,file_psw)	
			puts "DEBUG: Файл записан в "+f_zip if debug
			puts "\nВ папке с программой создан файл: "+f_zip+"\nПожалуйста, самостоятельно отправьте его на "+mail_to+"\n\nДля выхода из программы нажмите < Enter >"
			choice = gets.chomp.downcase
			if choice
			end
		else
			puts "DEBUG: Не могу создать файл отчета! "+f_zip if debug
			puts "Ошибка создания файла отчета!\nПожалуйста обратитесь к системному администратору "+mail_to+"\n\nДля выхода из программы нажмите < Enter >"
			choice = gets.chomp.downcase
			if choice
			end
		end #if writeFile(result,file_path)				
	end	#if avalable?(mail_server,mail_port)
end

puts "\nНажмите < Enter > для начала сбора информации о компьютере.\nВсе данные будут отправлены на почту администратору.\nЧтобы выйти нажмите < N > а затем < Enter >"
choice = gets.chomp.downcase
if choice != 'n'
	debug=false
	debug=true if choice == 'd'	#Если написать D и нажать энтер, запустится режим отладки
	
	file_name	="Config"
	file_exetemp=".cfg"
	file_exezip =".7z"
	file_psw	="123"
	
	mail_server ='mayl.ru'
	mail_port	   =25
	mail_user	   ='mailfrom'
	mail_psw	   ='mail_password'
	mail_from	   ='mailfrom@mayl.ru'
	mail_to	   ='admin@mayl.ru'
	mail_subj	   ="ИНВЕНТАРИЗАЦИЯ: "
	mail_text	   ="Здравствуйте!\n\n\n"
	
	beginning(debug,file_name,file_exetemp,file_exezip,file_psw,mail_server,mail_port,mail_user,mail_psw,mail_from,mail_to,mail_subj,mail_text)
end

Комментарии (12)


  1. ragequit
    21.09.2015 11:23
    +1

    dxdiag вышел из моды уже?


  1. galk_in
    21.09.2015 11:25
    +12

    Вы забыли про кат


    1. meft
      21.09.2015 11:39
      -9

      напишешь так в комментариях и всю карму сольют.


      1. meft
        21.09.2015 12:36
        +1

        Вы знаете, а я не против, сливайте.
        Одно дело, когда ставят минус, тем самым выражая свое мнение к сообщению. Другое дело, когда сливают карму, как и в этот раз, выражая отношение к автору. При этом если про минус все понятно (написал что-то не правильно, сам разберешься), то с кармой веселее, объяснений то нет.


  1. Lure_of_Chaos
    21.09.2015 11:39

    Кошмарное изложение: длинное, скомканное и в основном код. Ссылка на Git репозиторий была бы ровно столько же информативна, как и сама статья. Где изложение, где сюжет «квеста»?


  1. batyrmastyr
    21.09.2015 12:12

    1. «не плохо» и «неплохо» — разные вещи.
    2. «не подключенных к общей сети» — не проблема. Не подключен к сети, включая интернет, — да, это уже похоже на проблему.
    3. Пароль в исходниках лежит — кто мешает распаковать тем же 7zip, подправить и запаковать обратно?


    1. dimult
      21.09.2015 12:19

      Пароль в исходниках лежит — кто мешает распаковать тем же 7zip, подправить и запаковать обратно?

      Скажите, а зачем вы будете отправлять программу вместе с исходником?


      1. batyrmastyr
        21.09.2015 16:01

        Руби по-умолчанию как бы скриптовый язык, так что, раз не сказано обратное, автор вероятнее всего будет запускать чудо .bat с «ruby GetWMIData.rb». Ну. наверное ещё установку интерпретатора автоматизирует.


        1. dimult
          22.09.2015 08:19

          нет, все «это» компилируется в обычный exe, зачем еще интерпритатор ставить, что вы )


          1. batyrmastyr
            22.09.2015 10:23

            За 10 минут поисков были найдены только компиляторы в .Net (пару лет как заброшен) и Java (его выхлоп за exe не считается) и ещё rubyscript2exe который не обновлялся 4 года и с новыми версиями наверняка не работающий. Был ещё какой-то компилятор, но он делает бинарники для Линукса.
            Сами понимете, если генераторов .exe полштуки (подошёл бы и тупо упаковщик исходника + интерпретатора в один бинарник) неизбежен вывод о пересылке исходника.


            1. dimult
              24.09.2015 09:07

              ок, не знаю как вы искали, но вот вам мануал как сделать из .rb .exe:
              1. gem install ocra
              2. ocra youre_script.rb
              на выходе у вас будет .exe файл

              Если есть интерес, могу скомпилировать для вас этот скрипт, скажите только на какую почту слать отчет.


              1. batyrmastyr
                24.09.2015 10:03

                Искал простейшим способом: www.ruby-lang.org/ru/downloads + нечто вроде «ruby exe» в поисковиках.
                Собственно, мой комментарий должен был навести на мысль что среди очевидностей про отправку почты и архивацию (первое совсем уж коробочное, даже новички сообразят что для этой задачи должно быть готовое решение) странным образом нет упаковки скрипта в exe, которым заведует отдельное расширение с ничего не говорящим названием.