Skip to content

Commit bbbd4f0

Browse files
committed
(FM-2303) WMI / Registry impl of Activity Report
- Provide a nearly drop-in replacement for the existing SQL discovery report based on sqlcmd.exe - but by using WMI queries and Registry spleunking. This code: - does not shell to PowerShell - does not call sqlcmd.exe - does not parse XML - runs all code in the Ruby process - As a result: - it's a bit quicker to execute - it doesn't require any heuristics to find files - it doesn't need to write / secure temporary files - it can use native Ruby symbols - Of note, the public API on PuppetX::SqlServer::Features is rather small and comprises only 3 public methods: - get_installations - returns a detailed hash for both SQL_2012 and SQL_2014 that contains instance specific and shared features - get_instance_names - returns a hash of SQL_2012 / SQL_2014 to the installed instance names - get_instance_info(version, instance_name) - returns a hash for a given instance with a given version. Called by get_installations - The shape of the data returned is slightly different than what run_discovery_report previously used, but it was close to a drop-in replacement. - Note that since the module doesn't specify a SQL version for install and the current code is not set to handle multiple versions. The Features.get_installations returns info for both SQL_2014 and SQL_2012 should they both be installed. For the sake of consumers of the discovery_script code path, only feature data for one version of SQL will be installed, and SQL_2014 is prioritized.
1 parent b6125fc commit bbbd4f0

File tree

4 files changed

+237
-47
lines changed

4 files changed

+237
-47
lines changed

lib/puppet/provider/sqlserver.rb

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'lib/puppet_x/sqlserver/server_helper'))
2+
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'lib/puppet_x/sqlserver/features'))
23
require File.expand_path(File.join(File.dirname(__FILE__), 'sqlserver'))
34
require 'tempfile'
45

@@ -39,44 +40,10 @@ def not_nil_and_not_empty?(obj)
3940
end
4041

4142
def self.run_discovery_script
42-
discovery = <<-DISCOVERY
43-
if(Test-Path 'C:\\Program Files\\Microsoft SQL Server\\120\\Setup Bootstrap\\SQLServer2014\\setup.exe'){
44-
pushd 'C:\\Program Files\\Microsoft SQL Server\\120\\Setup Bootstrap\\SQLServer2014\\'
45-
Start-Process -FilePath .\\setup.exe -ArgumentList @("/Action=RunDiscovery","/q") -Wait -WindowStyle Hidden
46-
popd
47-
}elseif(Test-Path 'C:\\Program Files\\Microsoft SQL Server\\110\\Setup Bootstrap\\SQLServer2012\\setup.exe'){
48-
pushd 'C:\\Program Files\\Microsoft SQL Server\\110\\Setup Bootstrap\\SQLServer2012\\'
49-
Start-Process -FilePath .\\setup.exe -ArgumentList @("/Action=RunDiscovery","/q") -Wait -WindowStyle Hidden
50-
popd
51-
}
43+
installs = PuppetX::Sqlserver::Features.get_installations
44+
return installs[:SQL_2014] if !installs[:SQL_2014].empty?
5245

53-
$file = gci 'C:\\Program Files\\Microsoft SQL Server\\*\\Setup Bootstrap\\Log\\*\\SqlDiscoveryReport.xml' -ErrorAction Ignore | sort -Descending | select -First 1
54-
if($file -ne $null) {
55-
[xml] $xml = cat $file
56-
$json = $xml.ArrayOfDiscoveryInformation.DiscoveryInformation
57-
$hash = @{"instances" = @();"TimeStamp"= ("{0:yyyy-MM-dd HH:mm:ss}" -f $file.CreationTime)}
58-
foreach($instance in ($json | % { $_.Instance } | Get-Unique )){
59-
$features = @()
60-
$json | %{
61-
if($_.instance -eq $instance){
62-
$features += $_.feature
63-
}
64-
}
65-
if($instance -eq "" ){
66-
$hash.Add("Generic Features",$features)
67-
}else{
68-
$hash["instances"] += $instance
69-
$hash.Add($instance,@{"features"=$features})
70-
}
71-
}
72-
$file.Directory.Delete($true)
73-
Write-Host (ConvertTo-Json $hash)
74-
}else{
75-
Write-host ("{}")
76-
}
77-
DISCOVERY
78-
result = powershell([discovery])
79-
JSON.parse(result)
46+
installs[:SQL_2012]
8047
end
8148

8249
def self.run_install_dot_net

lib/puppet/provider/sqlserver_features/mssql.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
Puppet::Type::type(:sqlserver_features).provide(:mssql, :parent => Puppet::Provider::Sqlserver) do
99
def self.instances
1010
instances = []
11-
jsonResult = Puppet::Provider::Sqlserver.run_discovery_script
12-
debug "Parsing json result #{jsonResult}"
13-
if jsonResult.has_key?('Generic Features')
11+
result = Puppet::Provider::Sqlserver.run_discovery_script
12+
debug "Parsing result #{result}"
13+
if result.has_key?(:features)
1414
existing_instance = {:name => "Generic Features",
1515
:ensure => :present,
1616
:features =>
1717
PuppetX::Sqlserver::ServerHelper.translate_features(
18-
jsonResult['Generic Features']).sort!
18+
result[:features]).sort!
1919
}
2020
debug "Parsed features = #{existing_instance[:features]}"
2121

lib/puppet/provider/sqlserver_instance/mssql.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,17 @@
99
Puppet::Type::type(:sqlserver_instance).provide(:mssql, :parent => Puppet::Provider::Sqlserver) do
1010
def self.instances
1111
instances = []
12-
jsonResult = Puppet::Provider::Sqlserver.run_discovery_script
13-
debug "Parsing json result #{jsonResult}"
14-
if jsonResult.has_key?('instances')
15-
jsonResult['instances'].each do |instance_name|
12+
result = Puppet::Provider::Sqlserver.run_discovery_script
13+
debug "Parsing result #{result}"
14+
result.keys.reject { |key| key == :features }.each do |instance_name|
1615
existing_instance = {:name => instance_name,
1716
:ensure => :present,
1817
:features =>
1918
PuppetX::Sqlserver::ServerHelper.translate_features(
20-
jsonResult[instance_name]['features']).sort!
19+
result[instance_name][:features]).sort!
2120
}
2221
instance = new(existing_instance)
2322
instances << instance
24-
end
2523
end
2624
instances
2725
end

lib/puppet_x/sqlserver/features.rb

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
SQL_2012 = 'SQL_2012'
2+
SQL_2014 = 'SQL_2014'
3+
4+
module PuppetX
5+
module Sqlserver
6+
# https://msdn.microsoft.com/en-us/library/ms143786.aspx basic feature docs
7+
class Features
8+
private
9+
10+
SQL_WMI_PATH = {
11+
SQL_2012 => 'ComputerManagement11',
12+
SQL_2014 => 'ComputerManagement12',
13+
}
14+
15+
def self.connect(version = SQL_2012)
16+
require 'win32ole'
17+
ver = SQL_WMI_PATH[version]
18+
WIN32OLE.connect("winmgmts://./root/Microsoft/SqlServer/#{ver}")
19+
end
20+
21+
def self.get_parent_path(key_path)
22+
key_path.slice(0, key_path.rindex('\\'))
23+
end
24+
25+
def self.get_reg_key_val(win32_reg_key, val_name, reg_type)
26+
win32_reg_key[val_name, reg_type]
27+
rescue
28+
nil
29+
end
30+
31+
def self.get_sql_reg_val_features(key_name, reg_val_feat_hash)
32+
require 'win32/registry'
33+
34+
Win32::Registry::HKEY_LOCAL_MACHINE.open(key_name) do |key|
35+
reg_val_feat_hash
36+
.select { |val_name, _| get_reg_key_val(key, val_name, Win32::Registry::REG_DWORD) == 1 }
37+
.map { |_, feat_name| feat_name }
38+
end
39+
end
40+
41+
def self.get_sql_reg_key_features(key_name, reg_key_feat_hash, instance_name)
42+
require 'win32/registry'
43+
44+
installed = reg_key_feat_hash.select do |subkey, feat_name|
45+
Win32::Registry::HKEY_LOCAL_MACHINE.open("#{key_name}\\#{subkey}") do |feat_key|
46+
get_reg_key_val(feat_key, instance_name, Win32::Registry::REG_SZ)
47+
end
48+
end
49+
50+
installed.values
51+
end
52+
53+
def self.get_wmi_property_values(wmi, query, prop_name = 'PropertyStrValue')
54+
vals = []
55+
56+
wmi.ExecQuery(query).each do |v|
57+
vals.push(v.Properties_(prop_name).Value)
58+
end
59+
60+
vals
61+
end
62+
63+
def self.get_instance_names_by_ver(version = SQL_2012)
64+
query = 'SELECT InstanceName FROM ServerSettings'
65+
get_wmi_property_values(connect(version), query, 'InstanceName')
66+
rescue # version doesn't exist
67+
[]
68+
end
69+
70+
def self.get_sql_property_values(version, instance_name, property_name)
71+
query = <<-END
72+
SELECT * FROM SqlServiceAdvancedProperty
73+
WHERE PropertyName='#{property_name}'
74+
AND SqlServiceType=1 AND ServiceName LIKE '%#{instance_name}'
75+
END
76+
# WMI LIKE query to substring match since ServiceName will be of the format
77+
# MSSQLSERVER (first install) or MSSQL$MSSQLSERVER (second install)
78+
79+
get_wmi_property_values(connect(version), query)
80+
end
81+
82+
def self.get_wmi_instance_info(version, instance_name)
83+
{
84+
:name => instance_name,
85+
:version_friendly => version,
86+
:version => get_sql_property_values(version, instance_name, 'VERSION').first,
87+
# typically Software\Microsoft\Microsoft SQL Server\MSSQL11.MSSQLSERVER
88+
:reg_root => get_sql_property_values(version, instance_name, 'REGROOT').first,
89+
}
90+
end
91+
92+
def self.get_instance_features(reg_root, instance_name)
93+
instance_features = {
94+
# also reg Replication/IsInstalled set to 1
95+
'SQL_Replication_Core_Inst' => 'SQL Server Replication',
96+
# also WMI: SqlService WHERE SQLServiceType = 1 # MSSQLSERVER
97+
'SQL_Engine_Core_Inst' => 'Database Engine Services',
98+
'SQL_FullText_Adv' => 'Full-Text and Semantic Extractions for Search',
99+
'SQL_DQ_Full' => 'Data Quality Services'
100+
}
101+
102+
feat_root = "#{reg_root}\\ConfigurationState"
103+
features = get_sql_reg_val_features(feat_root, instance_features)
104+
105+
# https://msdn.microsoft.com/en-us/library/ms179591.aspx
106+
# WMI equivalents require trickier name parsing
107+
parent_subkey_features = {
108+
# also WMI: SqlService WHERE SQLServiceType = 5 # MSSQLServerOLAPService
109+
'OLAP' => 'Analysis Services',
110+
# also WMI: SqlService WHERE SQLServiceType = 6 # ReportServer
111+
'RS' => 'Reporting Services - Native'
112+
}
113+
114+
# instance features found in non-parented reg keys
115+
feat_root = "#{get_parent_path(reg_root)}\\Instance Names"
116+
parent_features = get_sql_reg_key_features(feat_root, parent_subkey_features, instance_name)
117+
118+
features + parent_features
119+
end
120+
121+
def self.get_shared_features(version, reg_root)
122+
shared_features = {
123+
'Connectivity_Full' => 'Client Tools Connectivity',
124+
'SDK_Full' => 'Client Tools SDK',
125+
'MDSCoreFeature' => 'Master Data Services',
126+
'Tools_Legacy_Full' => 'Client Tools Backwards Compatibility',
127+
'SQL_SSMS_Full' => 'Management Tools - Complete',
128+
'SQL_SSMS_Adv' => 'Management Tools - Basic', # also SQL_PowerShell_Tools_ANS
129+
# also WMI: SqlService WHERE SQLServiceType = 4 # MsDtsServer
130+
'SQL_DTS_Full' => 'Integration Services'
131+
# currently ignoring Reporting Services Shared
132+
}
133+
134+
reg_ver = (version == SQL_2014 ? '120' : '110')
135+
reg_root = "#{reg_root}\\#{reg_ver}\\ConfigurationState"
136+
137+
get_sql_reg_val_features(reg_root, shared_features)
138+
end
139+
140+
public
141+
142+
# return a hash of version => instance info and shared features
143+
#
144+
# {
145+
# "SQL_2012" => {},
146+
# "SQL_2014" => {
147+
# "MSSQLSERVER" => {
148+
# :name => "MSSQLSERVER",
149+
# :version_friendly => "SQL_2014",
150+
# :version => "12.0.2000.8",
151+
# :reg_root => "Software\\Microsoft\\Microsoft SQL Server\\MSSQL12.MSSQLSERVER",
152+
# :features => [
153+
# "SQL Server Replication",
154+
# "Database Engine Services",
155+
# "Full-Text and Semantic Extractions for Search",
156+
# "Data Quality Services",
157+
# "Analysis Services",
158+
# "Reporting Services - Native"
159+
# ]
160+
# },
161+
# :features => [
162+
# "Client Tools Connectivity",
163+
# "Client Tools SDK",
164+
# "Master Data Services",
165+
# "Client Tools Backwards Compatibility",
166+
# "Management Tools - Complete",
167+
# "Management Tools - Basic",
168+
# "Integration Services"
169+
# ]
170+
# }
171+
# }
172+
def self.get_installations
173+
version_instance_map = get_instance_names
174+
.map do |version, instance_names|
175+
instances = instance_names
176+
.map { |name| [ name, get_instance_info(version, name) ] }
177+
178+
if !instance_names.empty?
179+
reg_root = get_parent_path(instances.first[1][:reg_root])
180+
instances.push([:features, get_shared_features(version, reg_root)])
181+
end
182+
183+
[ version, Hash[instances] ]
184+
end
185+
186+
Hash[version_instance_map]
187+
end
188+
189+
# return a hash of version => instance name array
190+
#
191+
# {
192+
# "SQL_2012" => ["MSSQLSERVER"],
193+
# "SQL_2014" => ["MSSQLSERVER", "MSSQLSERVER2"],
194+
# }
195+
def self.get_instance_names
196+
{
197+
SQL_2012 => get_instance_names_by_ver(SQL_2012),
198+
SQL_2014 => get_instance_names_by_ver(SQL_2014),
199+
}
200+
end
201+
202+
# returns a hash containing instance details
203+
#
204+
# {
205+
# :name => "MSSQLSERVER2",
206+
# :version_friendly => "SQL_2014",
207+
# :version => "12.0.2000.8",
208+
# :reg_root => "Software\\Microsoft\\Microsoft SQL Server\\MSSQL12.MSSQLSERVER2",
209+
# :features => [
210+
# "SQLServer Replication",
211+
# "Database Engine Services",
212+
# "Full-Text and Semantic Extractions for Search",
213+
# "Data Quality Services",
214+
# "Analysis Services",
215+
# "Reporting Services - Native"
216+
# ]
217+
# }
218+
def self.get_instance_info(version = SQL_2012, instance_name)
219+
sql_instance = get_wmi_instance_info(version, instance_name)
220+
feats = get_instance_features(sql_instance[:reg_root], sql_instance[:name])
221+
sql_instance.merge({:features => feats})
222+
end
223+
end
224+
end
225+
end

0 commit comments

Comments
 (0)