Skip to content

Commit 65067c0

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_instances - returns a hash of all instance specific details for both SQL_2012 and SQL_2014 instances munged together given in a side by side SQL install, name collisions are not allowed - get_features - returns a hash of SQL_2012 / SQL_2014 shared features - get_instance_info(version, instance_name) - returns a hash for a given instance with a given version. Called by get_instances - 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_instances 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 shared feature data for one version of SQL will be installed, with SQL_2014 prioritized.
1 parent 18cddf0 commit 65067c0

File tree

4 files changed

+241
-46
lines changed

4 files changed

+241
-46
lines changed

lib/puppet/provider/sqlserver.rb

Lines changed: 7 additions & 36 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,14 @@ 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+
features = PuppetX::Sqlserver::Features.get_features
5244

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-
}
45+
instances = {
46+
# SQL instance names are unique over side-by-side installs
47+
:instances => PuppetX::Sqlserver::Features.get_instances.values.inject(:merge),
48+
# but features across versions are different
49+
:features => !features[SQL_2014].empty? ? features[SQL_2014] : features[SQL_2012]
7150
}
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)
8051
end
8152

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

0 commit comments

Comments
 (0)