Skip to content

(FM-2303, FM-2790, FM-2445) WMI / Registry impl of Discovery Report #116

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

Merged
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
9 changes: 9 additions & 0 deletions lib/facter/sqlserver_features.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'puppet_x/sqlserver/features'))

Facter.add(:sqlserver_features) do
confine :osfamily => :windows

setcode do
PuppetX::Sqlserver::Features.get_features
end
end
9 changes: 9 additions & 0 deletions lib/facter/sqlserver_instances.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'puppet_x/sqlserver/features'))

Facter.add(:sqlserver_instances) do
confine :osfamily => :windows

setcode do
PuppetX::Sqlserver::Features.get_instances
end
end
42 changes: 1 addition & 41 deletions lib/puppet/provider/sqlserver.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'lib/puppet_x/sqlserver/server_helper'))
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'lib/puppet_x/sqlserver/features'))
require File.expand_path(File.join(File.dirname(__FILE__), 'sqlserver'))
require 'tempfile'

Expand Down Expand Up @@ -38,47 +39,6 @@ def not_nil_and_not_empty?(obj)
!obj.nil? and !obj.empty?
end

def self.run_discovery_script
discovery = <<-DISCOVERY
if(Test-Path 'C:\\Program Files\\Microsoft SQL Server\\120\\Setup Bootstrap\\SQLServer2014\\setup.exe'){
pushd 'C:\\Program Files\\Microsoft SQL Server\\120\\Setup Bootstrap\\SQLServer2014\\'
Start-Process -FilePath .\\setup.exe -ArgumentList @("/Action=RunDiscovery","/q") -Wait -WindowStyle Hidden
popd
}elseif(Test-Path 'C:\\Program Files\\Microsoft SQL Server\\110\\Setup Bootstrap\\SQLServer2012\\setup.exe'){
pushd 'C:\\Program Files\\Microsoft SQL Server\\110\\Setup Bootstrap\\SQLServer2012\\'
Start-Process -FilePath .\\setup.exe -ArgumentList @("/Action=RunDiscovery","/q") -Wait -WindowStyle Hidden
popd
}

$file = gci 'C:\\Program Files\\Microsoft SQL Server\\*\\Setup Bootstrap\\Log\\*\\SqlDiscoveryReport.xml' -ErrorAction Ignore | sort -Descending | select -First 1
if($file -ne $null) {
[xml] $xml = cat $file
$json = $xml.ArrayOfDiscoveryInformation.DiscoveryInformation
$hash = @{"instances" = @();"TimeStamp"= ("{0:yyyy-MM-dd HH:mm:ss}" -f $file.CreationTime)}
foreach($instance in ($json | % { $_.Instance } | Get-Unique )){
$features = @()
$json | %{
if($_.instance -eq $instance){
$features += $_.feature
}
}
if($instance -eq "" ){
$hash.Add("Generic Features",$features)
}else{
$hash["instances"] += $instance
$hash.Add($instance,@{"features"=$features})
}
}
$file.Directory.Delete($true)
Write-Host (ConvertTo-Json $hash)
}else{
Write-host ("{}")
}
DISCOVERY
result = powershell([discovery])
JSON.parse(result)
end

def self.run_install_dot_net
install_dot_net = <<-DOTNET
Install-WindowsFeature NET-Framework-Core
Expand Down
11 changes: 5 additions & 6 deletions lib/puppet/provider/sqlserver_features/mssql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
Puppet::Type::type(:sqlserver_features).provide(:mssql, :parent => Puppet::Provider::Sqlserver) do
def self.instances
instances = []
jsonResult = Puppet::Provider::Sqlserver.run_discovery_script
debug "Parsing json result #{jsonResult}"
if jsonResult.has_key?('Generic Features')
result = Facter.value(:sqlserver_features)
debug "Parsing result #{result}"
result = !result[SQL_2014].empty? ? result[SQL_2014] : result[SQL_2012]
if !result.empty?
existing_instance = {:name => "Generic Features",
:ensure => :present,
:features =>
PuppetX::Sqlserver::ServerHelper.translate_features(
jsonResult['Generic Features']).sort!
:features => result.sort
}
debug "Parsed features = #{existing_instance[:features]}"

Expand Down
13 changes: 5 additions & 8 deletions lib/puppet/provider/sqlserver_instance/mssql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,16 @@
Puppet::Type::type(:sqlserver_instance).provide(:mssql, :parent => Puppet::Provider::Sqlserver) do
def self.instances
instances = []
jsonResult = Puppet::Provider::Sqlserver.run_discovery_script
debug "Parsing json result #{jsonResult}"
if jsonResult.has_key?('instances')
jsonResult['instances'].each do |instance_name|
result = Facter.value(:sqlserver_instances)
debug "Parsing result #{result}"
result = result.values.inject(:merge)
result.keys.each do |instance_name|
existing_instance = {:name => instance_name,
:ensure => :present,
:features =>
PuppetX::Sqlserver::ServerHelper.translate_features(
jsonResult[instance_name]['features']).sort!
:features => result[instance_name]['features'].sort
}
instance = new(existing_instance)
instances << instance
end
end
instances
end
Expand Down
235 changes: 235 additions & 0 deletions lib/puppet_x/sqlserver/features.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
SQL_2012 ||= 'SQL_2012'
SQL_2014 ||= 'SQL_2014'

module PuppetX
module Sqlserver
# https://msdn.microsoft.com/en-us/library/ms143786.aspx basic feature docs
class Features
private

SQL_WMI_PATH ||= {
SQL_2012 => 'ComputerManagement11',
SQL_2014 => 'ComputerManagement12',
}

SQL_REG_ROOT ||= 'Software\Microsoft\Microsoft SQL Server'

# http://msdn.microsoft.com/en-us/library/windows/desktop/aa384129(v=vs.85).aspx
KEY_WOW64_64KEY ||= 0x100
KEY_READ ||= 0x20019

def self.connect(version)
require 'win32ole'
ver = SQL_WMI_PATH[version]
context = WIN32OLE.new('WbemScripting.SWbemNamedValueSet')
context.Add("__ProviderArchitecture", 64)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@cyberious this is the solution for the 32-bit Puppet agent not seeing the 64-bit SQL servers. WMI has redirection as well (since it maps over the registry), so the solution was to always force it look at 64-bit data, since we only support SQL 64-bit.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice find 👍 fixed issue on 32, testing further to see if anything else shakes out but I think we are good to go now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for testing with the 32-bit installer - otherwise this would have never been caught.

locator = WIN32OLE.new('WbemScripting.SWbemLocator')
locator.ConnectServer('', "root/Microsoft/SqlServer/#{ver}", '', '', nil, nil, nil, context)
end

def self.get_parent_path(key_path)
# should be the same as SQL_REG_ROOT
key_path.slice(0, key_path.rindex('\\'))
end

def self.get_reg_key_val(win32_reg_key, val_name, reg_type)
win32_reg_key[val_name, reg_type]
rescue
nil
end

def self.get_sql_reg_val_features(key_name, reg_val_feat_hash)
require 'win32/registry'

vals = []

begin
hklm = Win32::Registry::HKEY_LOCAL_MACHINE
vals = hklm.open(key_name, KEY_READ | KEY_WOW64_64KEY) do |key|
reg_val_feat_hash
.select { |val_name, _| get_reg_key_val(key, val_name, Win32::Registry::REG_DWORD) == 1 }
.map { |_, feat_name| feat_name }
end
rescue Win32::Registry::Error # subkey doesn't exist
end

vals
end

def self.get_sql_reg_key_features(key_name, reg_key_feat_hash, instance_name)
require 'win32/registry'

installed = reg_key_feat_hash.select do |subkey, feat_name|
begin
hklm = Win32::Registry::HKEY_LOCAL_MACHINE
hklm.open("#{key_name}\\#{subkey}", KEY_READ | KEY_WOW64_64KEY) do |feat_key|
get_reg_key_val(feat_key, instance_name, Win32::Registry::REG_SZ)
end
rescue Win32::Registry::Error # subkey doesn't exist
end
end

installed.values
end

def self.get_wmi_property_values(wmi, query, prop_name = 'PropertyStrValue')
vals = []

wmi.ExecQuery(query).each do |v|
vals.push(v.Properties_(prop_name).Value)
end

vals
end

def self.get_instance_names_by_ver(version)
query = 'SELECT InstanceName FROM ServerSettings'
get_wmi_property_values(connect(version), query, 'InstanceName')
rescue WIN32OLERuntimeError => e # version doesn't exist
# WBEM_E_INVALID_NAMESPACE from wbemcli.h
return [] if e.message =~ /8004100e/im
raise
end

def self.get_sql_property_values(version, instance_name, property_name)
query = <<-END
SELECT * FROM SqlServiceAdvancedProperty
WHERE PropertyName='#{property_name}'
AND SqlServiceType=1 AND ServiceName LIKE '%#{instance_name}'
END
# WMI LIKE query to substring match since ServiceName will be of the format
# MSSQLSERVER (first install) or MSSQL$MSSQLSERVER (second install)

get_wmi_property_values(connect(version), query)
end

def self.get_wmi_instance_info(version, instance_name)
{
'name' => instance_name,
'version_friendly' => version,
'version' => get_sql_property_values(version, instance_name, 'VERSION').first,
# typically Software\Microsoft\Microsoft SQL Server\MSSQL11.MSSQLSERVER
'reg_root' => get_sql_property_values(version, instance_name, 'REGROOT').first,
}
end

def self.get_instance_features(reg_root, instance_name)
instance_features = {
# also reg Replication/IsInstalled set to 1
'SQL_Replication_Core_Inst' => 'Replication', # SQL Server Replication
# also WMI: SqlService WHERE SQLServiceType = 1 # MSSQLSERVER
'SQL_Engine_Core_Inst' => 'SQLEngine', # Database Engine Services
'SQL_FullText_Adv' => 'FullText', # Full-Text and Semantic Extractions for Search
'SQL_DQ_Full' => 'DQ', # Data Quality Services
}

feat_root = "#{reg_root}\\ConfigurationState"
features = get_sql_reg_val_features(feat_root, instance_features)

# https://msdn.microsoft.com/en-us/library/ms179591.aspx
# WMI equivalents require trickier name parsing
parent_subkey_features = {
# also WMI: SqlService WHERE SQLServiceType = 5 # MSSQLServerOLAPService
'OLAP' => 'AS', # Analysis Services,
# also WMI: SqlService WHERE SQLServiceType = 6 # ReportServer
'RS' => 'RS' # Reporting Services - Native
}

# instance features found in non-parented reg keys
feat_root = "#{get_parent_path(reg_root)}\\Instance Names"
parent_features = get_sql_reg_key_features(feat_root, parent_subkey_features, instance_name)

features + parent_features
end

def self.get_shared_features(version)
shared_features = {
'Connectivity_Full' => 'Conn', # Client Tools Connectivity
'SDK_Full' => 'SDK', # Client Tools SDK
'MDSCoreFeature' => 'MDS', # Master Data Services
'Tools_Legacy_Full' => 'BC', # Client Tools Backwards Compatibility
'SQL_SSMS_Full' => 'ADV_SSMS', # Management Tools - Complete
'SQL_SSMS_Adv' => 'SSMS', # Management Tools - Basic
# also WMI: SqlService WHERE SQLServiceType = 4 # MsDtsServer
'SQL_DTS_Full' => 'IS', # Integration Services
# currently ignoring Reporting Services Shared
}

reg_ver = (version == SQL_2014 ? '120' : '110')
reg_root = "#{SQL_REG_ROOT}\\#{reg_ver}\\ConfigurationState"

get_sql_reg_val_features(reg_root, shared_features)
end

public

# return a hash of version => instance info
#
# {
# "SQL_2012" => {},
# "SQL_2014" => {
# "MSSQLSERVER" => {
# "name" => "MSSQLSERVER",
# "version_friendly" => "SQL_2014",
# "version" => "12.0.2000.8",
# "reg_root" => "Software\\Microsoft\\Microsoft SQL Server\\MSSQL12.MSSQLSERVER",
# "features" => [
# "Replication",
# "SQLEngine",
# "FullText",
# "DQ",
# "AS",
# "RS"
# ]
# }
# }
# }
def self.get_instances
version_instance_map = [SQL_2012, SQL_2014]
.map do |version|
instances = get_instance_names_by_ver(version)
.map { |name| [ name, get_instance_info(version, name) ] }

[ version, Hash[instances] ]
end

Hash[version_instance_map]
end

# return a hash of version => shared features array
#
# {
# "SQL_2012" => ["Conn", "SDK", "MDS", "BC", "SSMS", "ADV_SSMS", "IS"],
# "SQL_2014" => []
# }
def self.get_features
{
SQL_2012 => get_shared_features(SQL_2012),
SQL_2014 => get_shared_features(SQL_2014),
}
end

# returns a hash containing instance details
#
# {
# "name" => "MSSQLSERVER2",
# "version_friendly" => "SQL_2014",
# "version" => "12.0.2000.8",
# "reg_root" => "Software\\Microsoft\\Microsoft SQL Server\\MSSQL12.MSSQLSERVER2",
# "features" =>[
# "Replication",
# "SQLEngine",
# "FullText",
# "DQ",
# "AS",
# "RS"
# ]
# }
def self.get_instance_info(version = SQL_2014, instance_name)
sql_instance = get_wmi_instance_info(version, instance_name)
feats = get_instance_features(sql_instance['reg_root'], sql_instance['name'])
sql_instance.merge({'features' => feats})
end
end
end
end
28 changes: 1 addition & 27 deletions lib/puppet_x/sqlserver/server_helper.rb
Original file line number Diff line number Diff line change
@@ -1,37 +1,11 @@
module PuppetX
module Sqlserver
class ServerHelper
@features_hash = {
:AS => 'Analysis Services',
:RS => 'Reporting Services - Native',
:SQLEngine => 'Database Engine Services',
:Replication => 'SQL Server Replication',
:FullText => 'Full-Text and Semantic Extractions for Search',
:DQ => 'Data Quality Services',
:BC => 'Client Tools Backwards Compatibility',
:SSMS => 'Management Tools - Basic',
:ADV_SSMS => 'Management Tools - Complete',
:Conn => 'Client Tools Connectivity',
:SDK => 'Client Tools SDK',
:IS => 'Integration Services',
:MDS => 'Master Data Services',
}
@super_feature_hash = {
:SQL => [:DQ, :FullText, :Replication, :SQLEngine],
:Tools => [:SSMS, :ADV_SSMS, :Conn]
:Tools => [:BC, :SSMS, :ADV_SSMS, :Conn, :SDK]
}


def self.translate_features(features)
translated = []
Array.new(features).each do |feature|
if @features_hash.has_value?(feature)
translated << @features_hash.key(feature).to_s
end
end
translated
end

def self.get_sub_features(super_feature)
@super_feature_hash[super_feature.to_sym]
end
Expand Down
Loading