Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a plugin for Windows mimicing the Unix dmi plugin #1445

Merged
merged 18 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions lib/ohai/common/dmi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ def id_lookup(id)
id
end

SKIPPED_CONVENIENCE_KEYS = %w{
application_identifier
caption
creation_class_name
size
system_creation_class_name
record_id
}.freeze

# create simplified convenience access keys for each record type
# for single occurrences of one type, copy to top level all fields and values
# for multiple occurrences of same type, copy to top level all fields and values that are common to all records
Expand All @@ -122,13 +131,12 @@ def convenience_keys(dmi)

records[:all_records].each do |record|
record.each do |field, value|
next if value.is_a?(Mash)
next if field.to_s == "application_identifier"
next if field.to_s == "size"
next if field.to_s == "record_id"
next unless value.is_a?(String)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows API returns values other than strings, which seemed to break an expectation of the values coming from the existing dmi plugin. There is a call to value.strip a few lines down so it seemed like a test for String was more in line with the intent here.


translated = field.downcase.gsub(/[^a-z0-9]/, "_")
value = value.strip
next if SKIPPED_CONVENIENCE_KEYS.include?(translated.to_s)

value = value.strip
if in_common.key?(translated)
in_common[translated] = nil unless in_common[translated] == value
else
Expand Down
94 changes: 94 additions & 0 deletions lib/ohai/plugins/windows/dmi.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#
# Author:: Pete Higgins (pete@peterhiggins.org)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

Ohai.plugin(:DMI) do
provides "dmi"

# Map the linux component types to their rough Windows API equivalents
DMI_TO_WIN32OLE = {
chassis: "SystemEnclosure",
phiggins marked this conversation as resolved.
Show resolved Hide resolved
processor: "Processor",
bios: "Bios",
system: "ComputerSystemProduct",
base_board: "BaseBoard",
}.freeze

# This regex is in 3 parts for the different supported patterns in camel
# case names coming from the Windows API:
# * Typical camelcase, eg Depth, PartNumber, NumberOfPowerCords
# * Acronyms preceding camelcase, eg SMBIOSAssetTag
# * Acronyms that occur at the end of the name, eg SKU, DeviceID
#
# This cannot handle some property names, eg SMBIOSBIOSVersion.
# https://rubular.com/r/FBNtXod4wkZGAG
SPLIT_REGEX = /[A-Z][a-z0-9]+|[A-Z]{2,}(?=[A-Z][a-z0-9])|[A-Z]{2,}/.freeze

WINDOWS_TO_UNIX_KEYS = [
%w{vendor manufacturer},
%w{identifying_number serial_number},
%w{name family},
].freeze

collect_data(:windows) do
require "ohai/common/dmi"
require "wmi-lite/wmi"
wmi = WmiLite::Wmi.new

dmi Mash.new

# The Windows API returns property names in camel case, eg "SerialNumber",
# while `dmi` returns them as space separated strings, eg "Serial Number".
# `Ohai::Common::DMI.convenience_keys` expects property names in `dmi`'s
# format, so build two parallel hashes with the keys as they come from the
# Windows API and in a faked-out `dmi` version. After the call to
# `Ohai::Common::DMI.convenience_keys` replace the faked-out `dmi`
# collection with the one with the original property names.
DMI_TO_WIN32OLE.each do |dmi_key, ole_key|
wmi_objects = wmi.instances_of("Win32_#{ole_key}").map(&:wmi_ole_object)

split_name_properties = []
properties = []

wmi_objects.each do |wmi_object|
split_name_properties << Mash.new
properties << Mash.new

wmi_object.properties_.each do |property|
property_name = property.name
value = wmi_object.invoke(property_name)

split_name = property_name.scan(SPLIT_REGEX).join(" ")
split_name_properties.last[split_name] = value
properties.last[property_name] = value
end
end

dmi[dmi_key] = Mash.new(all_records: split_name_properties, _all_records: properties)
end

Ohai::Common::DMI.convenience_keys(dmi)

dmi.each_value do |records|
records[:all_records] = records.delete(:_all_records)

WINDOWS_TO_UNIX_KEYS.each do |windows_key, unix_key|
records[unix_key] = records.delete(windows_key) if records.key?(windows_key)
end
end
end
end
8 changes: 3 additions & 5 deletions lib/ohai/plugins/windows/system_enclosure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@

Ohai.plugin :SystemEnclosure do
provides "system_enclosure"
depends "dmi"

collect_data(:windows) do
require "wmi-lite/wmi"
system_enclosure Mash.new
wmi = WmiLite::Wmi.new
wmi_object = wmi.first_of("Win32_SystemEnclosure").wmi_ole_object
system_enclosure[:manufacturer] = wmi_object.invoke("manufacturer")
system_enclosure[:serialnumber] = wmi_object.invoke("serialnumber")
system_enclosure[:manufacturer] = get_attribute(:dmi, :chassis, :manufacturer)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should cut an issue in cookstyle to get people using node['system_enclosure']['manufacturer'] onto the DMI equiv in the future. That way we can eventually remove this plugin namespace

system_enclosure[:serialnumber] = get_attribute(:dmi, :chassis, :serial_number)
end
end
179 changes: 179 additions & 0 deletions spec/unit/plugins/windows/dmi_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#
# Author:: Pete Higgins (pete@peterhiggins.org)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "spec_helper"

describe Ohai::System, "DMI", :windows_only do
let(:plugin) { get_plugin("windows/dmi") }

before do
require "wmi-lite/wmi"

empty_wmi_object = WmiLite::Wmi::Instance.new(double(properties_: []))
%w{Processor Bios ComputerSystemProduct BaseBoard}.each do |type|
expect_any_instance_of(WmiLite::Wmi).to receive(:instances_of).with("Win32_#{type}").and_return([empty_wmi_object])
end
end

context "when property names are different types of camel casing" do
# Each test case has 3 elements:
# * The name of the property as it comes from the Windows APIs
# * The transformed snake-case version of the property name
# * A unique dummy value per test case
CASES = [
%w{Depth depth aaa},
%w{PartNumber part_number bbb},
%w{NumberOfPowerCords number_of_power_cords ccc},
%w{SKU sku ddd},
%w{SMBIOSAssetTag smbios_asset_tag eee},
%w{DeviceID device_id fff},
%w{L2CacheSize l2_cache_size ggg},
].freeze

before do
properties = CASES.map { |name, _, _| double(name: name) }
wmi_ole_object = double properties_: properties

CASES.each do |name, _, value|
allow(wmi_ole_object).to receive(:invoke).with(name).and_return(value)
end

wmi_object = WmiLite::Wmi::Instance.new(wmi_ole_object)
expect_any_instance_of(WmiLite::Wmi).to receive(:instances_of).with("Win32_SystemEnclosure").and_return([wmi_object])

plugin.run
end

CASES.each do |name, transformed_name, value|
it "adds #{name} to :all_records" do
expect(plugin[:dmi][:chassis][:all_records].first[name]).to eq(value)
end

it "adds #{transformed_name} to the root" do
expect(plugin[:dmi][:chassis][transformed_name]).to eq(value)
end
end
end

context "when multiple objects of one type are returned from the Windows API" do
before do
properties = [
double(name: "UniqueProperty"),
double(name: "SharedProperty"),
]

wmi_ole_objects = %w{tacos nachos}.map do |value|
object = double properties_: properties
allow(object).to receive(:invoke).with("UniqueProperty").and_return(value)
allow(object).to receive(:invoke).with("SharedProperty").and_return("Taco Bell")
object
end

wmi_objects = wmi_ole_objects.map { |o| WmiLite::Wmi::Instance.new(o) }
expect_any_instance_of(WmiLite::Wmi).to receive(:instances_of).with("Win32_SystemEnclosure").and_return(wmi_objects)

plugin.run
end

it "adds unique values to :all_records" do
values = plugin[:dmi][:chassis][:all_records].map { |r| r["UniqueProperty"] }
expect(values).to eq(%w{tacos nachos})
end

it "adds shared values to the root with snake case key" do
expect(plugin[:dmi][:chassis]["shared_property"]).to eq("Taco Bell")
end
end

context "with extra information that should be filtered out" do
# Each test case has 3 elements:
# * The name of the property as it comes from the Windows APIs
# * The transformed snake-case version of the property name
# * A unique dummy value per test case
FILTERED_KEYS = [
%w{Caption caption aaa},
%w{CreationClassName creation_class_name bbb},
%w{SystemCreationClassName system_creation_class_name ccc},
].freeze

before do
properties = FILTERED_KEYS.map { |name, _, _| double(name: name) }
wmi_ole_object = double properties_: properties

FILTERED_KEYS.each do |name, _, value|
allow(wmi_ole_object).to receive(:invoke).with(name).and_return(value)
end

wmi_object = WmiLite::Wmi::Instance.new(wmi_ole_object)
expect_any_instance_of(WmiLite::Wmi).to receive(:instances_of).with("Win32_SystemEnclosure").and_return([wmi_object])

plugin.run
end

FILTERED_KEYS.each do |name, transformed_name, value|
it "adds #{name} to :all_records" do
expect(plugin[:dmi][:chassis][:all_records].first[name]).to eq(value)
end

it "does not add #{transformed_name} to the root" do
expect(plugin[:dmi][:chassis]).not_to have_key(transformed_name)
end
end
end

context "with information that should be made to match other platforms" do
# Each test case has 4 elements:
# * The name of the property as it comes from the Windows APIs
# * The transformed snake-case version of the property name
# * The Unix equivalent of the property name
# * A unique dummy value per test case
RENAMED_KEYS = [
%w{Vendor vendor manufacturer aaa},
%w{IdentifyingNumber identifying_number serial_number bbb},
%w{Name name family ccc},
].freeze

before do
properties = RENAMED_KEYS.map { |name, _, _, _| double(name: name) }
wmi_ole_object = double properties_: properties

RENAMED_KEYS.each do |name, _, _, value|
allow(wmi_ole_object).to receive(:invoke).with(name).and_return(value)
end

wmi_object = WmiLite::Wmi::Instance.new(wmi_ole_object)
expect_any_instance_of(WmiLite::Wmi).to receive(:instances_of).with("Win32_SystemEnclosure").and_return([wmi_object])

plugin.run
end

RENAMED_KEYS.each do |name, transformed_name, renamed_name, value|
it "adds #{name} to :all_records" do
expect(plugin[:dmi][:chassis][:all_records].first[name]).to eq(value)
end

it "adds #{renamed_name} to the root" do
expect(plugin[:dmi][:chassis][renamed_name]).to eq(value)
end

it "does not add #{transformed_name} to the root" do
expect(plugin[:dmi][:chassis]).not_to have_key(transformed_name)
end
end
end
end
45 changes: 0 additions & 45 deletions spec/unit/plugins/windows/system_enclosure_spec.rb

This file was deleted.