summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Dibowitz <phil@ipom.com>2020-10-01 22:25:33 -0700
committerPhil Dibowitz <phil@ipom.com>2020-10-02 20:24:49 -0700
commit75506cab7ccf6c4fb6c85c547d08ec2d0eadee4d (patch)
tree3b5eff7d297a12fd52221fdff146c3897519f1d3
parent0af23845276bd6ce226f475b3bdd577479a2fb77 (diff)
downloadohai-75506cab7ccf6c4fb6c85c547d08ec2d0eadee4d.tar.gz
Move this all to WMI
This cuts down the time an order of magnitude. Signed-off-by: Phil Dibowitz <phil@ipom.com>
-rw-r--r--RELEASE_NOTES.md9
-rw-r--r--lib/ohai/plugins/passwd.rb58
-rw-r--r--spec/unit/plugins/passwd_spec.rb276
3 files changed, 187 insertions, 156 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 5dfe416d..3b0dc2c5 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,3 +1,12 @@
+# Unreleased
+
+## etc Ohai Data on Windows
+
+Ohai's 'passwd' plugin that provides `node['etc']['passwd']` and `node['etc']['group']` now populates data on Windows. Data for all local users and groups is present. A few things to note:
+
+ * If you are on a domain controller, you will get all domain users as domain controllers see domain users as local
+ * If you are not on a domain controller you will only get actual local users
+ * Group members are not recursed, so you if groups are nested, you will simply see the group that is directly a member of the group.
# Ohai Release Notes 15.6
diff --git a/lib/ohai/plugins/passwd.rb b/lib/ohai/plugins/passwd.rb
index db26c610..781aba94 100644
--- a/lib/ohai/plugins/passwd.rb
+++ b/lib/ohai/plugins/passwd.rb
@@ -12,10 +12,6 @@ Ohai.plugin(:Passwd) do
str
end
- def powershell_out(ps)
- Mixlib::ShellOut.new("powershell.exe", "-c", ps).run_command
- end
-
collect_data do
require "etc" unless defined?(Etc)
@@ -46,42 +42,60 @@ Ohai.plugin(:Passwd) do
end
collect_data(:windows) do
+ require "wmi-lite/wmi" unless defined?(WmiLite::Wmi)
+
unless etc
etc Mash.new
+ wmi = WmiLite::Wmi.new
+
etc[:passwd] = Mash.new
- s = powershell_out("get-localuser | convertto-json")
- users = JSON.parse(s.stdout)
+ users = wmi.query("SELECT * FROM Win32_UserAccount WHERE LocalAccount = True")
users.each do |user|
uname = user["Name"].strip.downcase
Ohai::Log.debug("processing user #{uname}")
etc[:passwd][uname] = Mash.new
- user.each do |key, val|
- etc[:passwd][uname][key.downcase] = val
+ wmi_obj = user.wmi_ole_object
+ wmi_obj.properties_.each do |key|
+ etc[:passwd][uname][key.name.downcase] = user[key.name]
end
end
etc[:group] = Mash.new
- s = powershell_out("get-localgroup | convertto-json")
- groups = JSON.parse(s.stdout)
+ groups = wmi.query("SELECT * FROM Win32_Group WHERE LocalAccount = True")
groups.each do |group|
gname = group["Name"].strip.downcase
Ohai::Log.debug("processing group #{gname}")
etc[:group][gname] = Mash.new
- group.each do |key, val|
- etc[:group][gname][key.downcase] = val
+ wmi_obj = group.wmi_ole_object
+ wmi_obj.properties_.each do |key|
+ etc[:group][gname][key.name.downcase] = group[key.name]
end
- # calling this for each group is slow, but it requires
- # a specific group, soooooo....
- g = powershell_out(
- "get-localgroupmember -name '#{gname}' | convertto-json"
+
+ # This is the primary reason that we're using WMI instead of powershell
+ # cmdlets - the powershell start up cost is huge, and you *must* do this
+ # query for every. single. group. individually.
+
+ # The query returns nothing unless you specify domain *and* name, it's
+ # a path, not a set of queries.
+ subq = "Win32_Group.Domain='#{group["Domain"]}',Name='#{group["Name"]}'"
+ members = wmi.query(
+ "SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"#{subq}\""
)
- out = g.stdout
- if !out.empty?
- gmem = JSON.parse(g.stdout)
- etc[:group][gname]["members"] = gmem
- else
- etc[:group][gname]["members"] = []
+ etc[:group][gname]["members"] = members.map do |member|
+ mi = {}
+ info = Hash[
+ member["partcomponent"].split(",").map { |x| x.split("=") }.map { |a, b| [a, b.undump] }
+ ]
+ if info.keys.any? { |x| x.match?("Win32_UserAccount") }
+ mi["type"] = :user
+ else
+ # Note: the type here is actually Win32_SystemAccount, because,
+ # that's what groups are in the Windows universe.
+ mi["type"] = :group
+ end
+ mi["name"] = info["Name"]
+ mi
end
end
end
diff --git a/spec/unit/plugins/passwd_spec.rb b/spec/unit/plugins/passwd_spec.rb
index 482fba3a..c5cfcf69 100644
--- a/spec/unit/plugins/passwd_spec.rb
+++ b/spec/unit/plugins/passwd_spec.rb
@@ -73,102 +73,90 @@ describe Ohai::System, "plugin etc" do
end
end
- let(:user_info) do
- [
- {
- "Name" => "UserOne",
- "FullName" => "User One",
- "SID" => {
- "BinaryLength" => 28,
- "AccountDomainSid" => "bla",
- "Value" => "blabla",
- },
- "ObjectClass" => "User",
- "Enabled" => false,
- },
- {
- "Name" => "UserTwo",
- "FullName" => "User Two",
- "SID" => {
- "BinaryLength" => 28,
- "AccountDomainSid" => "doo",
- "Value" => "doodoo",
- },
- "ObjectClass" => "User",
- "Enabled" => true,
- },
- ]
- end
+ USERS = [
+ {
+ "AccountType" => 512,
+ "Disabled" => false,
+ "Name" => "userone",
+ "FullName" => "User One",
+ "SID" => "bla bla bla",
+ "SIDType" => 1,
+ "Status" => "OK",
+ },
+ {
+ "AccountType" => 512,
+ "Disabled" => false,
+ "FullName" => "User Two",
+ "Name" => "usertwo",
+ "SID" => "bla bla bla2",
+ "SIDType" => 1,
+ "Status" => "OK",
+ },
+ ].freeze
- let(:group_info) do
- [
- {
- "Description" => "Group One",
- "Name" => "GroupOne",
- "SID" => {
- "BinaryLength" => 16,
- "AccountDomainSid" => nil,
- "Value" => "foo",
- },
- "ObjectClass" => "Group",
- },
- {
- "Description" => "Group Two",
- "Name" => "GroupTwo",
- "SID" => {
- "BinaryLength" => 16,
- "AccountDomainSid" => nil,
- "Value" => "foo",
- },
- "ObjectClass" => "Group",
- },
- ]
- end
+ GROUPS = [
+ {
+ "Description" => "Group One",
+ "Domain" => "THIS-MACHINE",
+ "Name" => "GroupOne",
+ "SID" => "foo foo foo",
+ "SidType" => 4,
+ "Status" => "OK",
+ },
+ {
+ "Description" => "Group Two",
+ "Domain" => "THIS-MACHINE",
+ "Name" => "GroupTwo",
+ "SID" => "foo foo foo2",
+ "SidType" => 4,
+ "Status" => "OK",
+ },
+ ].freeze
- let(:group_one_info) do
- [
- {
- "Name" => "UserOne",
- "SID" => {
- "BinaryLength" => 28,
- "AccountDomainSid" => nil,
- "Value" => "bar",
- },
- "ObjectClass" => "User",
- },
- {
- "Name" => "UserTwo",
- "SID" => {
- "BinaryLength" => 28,
- "AccountDomainSid" => nil,
- "Value" => "bar",
- },
- "ObjectClass" => "User",
- },
- ]
- end
+ GROUP_ONE_MEMBERS = [
+ {
+ "groupcomponent" => "Win32_Group.Domain=\"THIS-MACHINE\",Name=\"GroupOne\"",
+ "partcomponent" => "\\\\VCRS-PRODWIN05\\root\\cimv2:Win32_UserAccount.Domain=\"THIS-MACHINE\",Name=\"UserOne\"",
+ },
+ {
+ "groupcomponent" => "Win32_Group.Domain=\"THIS-MACHINE\",Name=\"GroupOne\"",
+ "partcomponent" => "\\\\VCRS-PRODWIN05\\root\\cimv2:Win32_UserAccount.Domain=\"THIS-MACHINE\",Name=\"UserTwo\"",
+ },
+ ].freeze
- let(:group_two_info) do
- [
- {
- "Name" => "UserTwo",
- "SID" => {
- "BinaryLength" => 28,
- "AccountDomainSid" => nil,
- "Value" => "bar",
- },
- "ObjectClass" => "User",
- },
- ]
- end
+ GROUP_TWO_MEMBERS = [
+ {
+ "groupcomponent" => "Win32_Group.Domain=\"THIS-MACHINE\",Name=\"GroupOne\"",
+ "partcomponent" => "\\\\VCRS-PRODWIN05\\root\\cimv2:Win32_SystemAccount.Domain=\"THIS-MACHINE\",Name=\"GroupOne\"",
+ },
+ ].freeze
before do
- expect(plugin).to receive(:powershell_out)
- .with("get-localuser | convertto-json")
- .and_return(mock_shell_out(0, user_info.to_json, ""))
- expect(plugin).to receive(:powershell_out)
- .with("get-localgroup | convertto-json")
- .and_return(mock_shell_out(0, group_info.to_json, ""))
+ require "wmi-lite/wmi" unless defined?(WmiLite::Wmi)
+ properties = USERS[0].map { |k, v| double(name: k) }
+ wmi_user_list = USERS.map do |user|
+ wmi_ole_object = double properties_: properties
+ user.each do |key, val|
+ allow(wmi_ole_object).to receive(:invoke).with(key).and_return(val)
+ end
+ WmiLite::Wmi::Instance.new(wmi_ole_object)
+ end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_UserAccount WHERE LocalAccount = True")
+ .and_return(wmi_user_list)
+
+ properties = GROUPS[0].map { |k, v| double(name: k) }
+ wmi_group_list = GROUPS.map do |group|
+ wmi_ole_object = double properties_: properties
+ group.each do |key, val|
+ allow(wmi_ole_object).to receive(:invoke).with(key).and_return(val)
+ end
+ WmiLite::Wmi::Instance.new(wmi_ole_object)
+ end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_Group WHERE LocalAccount = True")
+ .and_return(wmi_group_list)
+
end
def transform(user_data)
@@ -180,71 +168,91 @@ describe Ohai::System, "plugin etc" do
end
it "returns lower-cased passwd keys for each local user" do
- {
- "groupone" => group_one_info.to_json,
- "grouptwo" => group_two_info.to_json,
- }.each do |gname, info|
- expect(plugin).to receive(:powershell_out)
- .with("get-localgroupmember -name '#{gname}' | convertto-json")
- .and_return(mock_shell_out(0, "[]", ""))
- end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return([])
+
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return([])
+
plugin.run
expect(plugin[:etc][:passwd].keys.sort).to eq(%w{userone usertwo}.sort)
end
it "returns preserved-case passwd entries for local users" do
- {
- "groupone" => group_one_info.to_json,
- "grouptwo" => group_two_info.to_json,
- }.each do |gname, info|
- expect(plugin).to receive(:powershell_out)
- .with("get-localgroupmember -name '#{gname}' | convertto-json")
- .and_return(mock_shell_out(0, "[]", ""))
- end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return([])
+
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return([])
+
plugin.run
- expect(plugin[:etc][:passwd]["userone"]).to eq(transform(user_info[0]))
+ expect(plugin[:etc][:passwd]["userone"]).to eq(transform(USERS[0]))
end
it "returns lower-cased group entries for each local group" do
- {
- "groupone" => group_one_info.to_json,
- "grouptwo" => group_two_info.to_json,
- }.each do |gname, info|
- expect(plugin).to receive(:powershell_out)
- .with("get-localgroupmember -name '#{gname}' | convertto-json")
- .and_return(mock_shell_out(0, "[]", ""))
- end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return([])
+
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return([])
+
plugin.run
expect(plugin[:etc][:group].keys.sort).to eq(%w{groupone grouptwo}.sort)
end
it "returns preserved-cased group entries for local groups" do
- {
- "groupone" => group_one_info.to_json,
- "grouptwo" => group_two_info.to_json,
- }.each do |gname, info|
- expect(plugin).to receive(:powershell_out)
- .with("get-localgroupmember -name '#{gname}' | convertto-json")
- .and_return(mock_shell_out(0, "[]", ""))
- end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return([])
+
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return([])
+
plugin.run
expect(plugin[:etc][:group]["grouptwo"]).to eq(
- transform(group_info[1]).merge({ "members" => [] })
+ transform(GROUPS[1]).merge({ "members" => [] })
)
end
it "returns members for groups" do
- {
- "groupone" => group_one_info.to_json,
- "grouptwo" => group_two_info.to_json,
- }.each do |gname, info|
- expect(plugin).to receive(:powershell_out)
- .with("get-localgroupmember -name '#{gname}' | convertto-json")
- .and_return(mock_shell_out(0, info, ""))
+ properties = GROUP_ONE_MEMBERS[0].map { |k, v| double(name: k) }
+ g1_members = GROUP_ONE_MEMBERS.map do |member|
+ wmi_ole_object = double properties_: properties
+ member.each do |key, val|
+ allow(wmi_ole_object).to receive(:invoke).with(key).and_return(val)
+ end
+ WmiLite::Wmi::Instance.new(wmi_ole_object)
end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupOne'\"")
+ .and_return(g1_members)
+
+ g2_members = GROUP_TWO_MEMBERS.map do |member|
+ wmi_ole_object = double properties_: properties
+ member.each do |key, val|
+ allow(wmi_ole_object).to receive(:invoke).with(key).and_return(val)
+ end
+ WmiLite::Wmi::Instance.new(wmi_ole_object)
+ end
+ allow_any_instance_of(WmiLite::Wmi).to receive(:query)
+ .with("SELECT * FROM Win32_GroupUser WHERE GroupComponent=\"Win32_Group.Domain='THIS-MACHINE',Name='GroupTwo'\"")
+ .and_return(g2_members)
+
plugin.run
- expect(plugin[:etc][:group]["groupone"]["members"]).to eq(group_one_info)
- expect(plugin[:etc][:group]["grouptwo"]["members"]).to eq(group_two_info)
+ expect(plugin[:etc][:group]["groupone"]["members"]).to eq([
+ { "name" => "UserOne", "type" => :user },
+ { "name" => "UserTwo", "type" => :user },
+ ])
+ expect(plugin[:etc][:group]["grouptwo"]["members"]).to eq([
+ { "name" => "GroupOne", "type" => :group },
+ ])
end
end
end