Skip to content

Commit 837170f

Browse files
committed
(FM-2303) WMI / Registry impl of Discovery 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 a1cf183 commit 837170f

File tree

4 files changed

+242
-47
lines changed

4 files changed

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

0 commit comments

Comments
 (0)