From 377a0bbe07d5bc4a49966a5a393a248002253395 Mon Sep 17 00:00:00 2001 From: Michael T Lombardi Date: Mon, 2 Oct 2017 14:50:41 -0500 Subject: [PATCH 1/2] (MODULES-5566) Discover instances via registry Without this commit, the logic to discover instances and features relies on WMI calls and fails to discover instances unless the SQL Engine is installed; this meant that Puppet would never discover instances with only analytics services or reporting services installed, causing each puppet run to attempt to reinstall them, even though they exist. This commit refactors the logic for discovering SQL instances to use registry instead of WMI. In order to do so, we use the Puppet Util Windows Registry module and ensure that SQL instances with valid names outside the current code page can be discovered. This ensures that we are able to discover SQL instances regardless of which features they have installed. This commit also removes code supporting calls to WMI to discover SQL instances. --- lib/puppet_x/sqlserver/features.rb | 127 ++++++++++++----------------- 1 file changed, 52 insertions(+), 75 deletions(-) diff --git a/lib/puppet_x/sqlserver/features.rb b/lib/puppet_x/sqlserver/features.rb index 17921226..7e20c498 100644 --- a/lib/puppet_x/sqlserver/features.rb +++ b/lib/puppet_x/sqlserver/features.rb @@ -1,3 +1,5 @@ +require 'puppet/util/windows' + SQL_2012 ||= 'SQL_2012' SQL_2014 ||= 'SQL_2014' SQL_2016 ||= 'SQL_2016' @@ -10,39 +12,26 @@ module Sqlserver class Features private + include Puppet::Util::Windows::Registry + extend Puppet::Util::Windows::Registry + SQL_CONFIGURATION ||= { SQL_2012 => { :major_version => 11, - :wmi_path => 'ComputerManagement11', :registry_path => '110', }, SQL_2014 => { :major_version => 12, - :wmi_path => 'ComputerManagement12', :registry_path => '120', }, SQL_2016 => { :major_version => 13, - :wmi_path => 'ComputerManagement13', :registry_path => '130', } } 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_CONFIGURATION[version][:wmi_path] - context = WIN32OLE.new('WbemScripting.SWbemNamedValueSet') - context.Add("__ProviderArchitecture", 64) - locator = WIN32OLE.new('WbemScripting.SWbemLocator') - # WMI Path must use backslashes. Ruby 2.3 can cause crashes with forward slashes - locator.ConnectServer(nil, "root\\Microsoft\\SqlServer\\#{ver}", nil, nil, nil, nil, nil, context) - end + HKLM ||= 'HKEY_LOCAL_MACHINE' def self.get_parent_path(key_path) # should be the same as SQL_REG_ROOT @@ -55,81 +44,66 @@ def self.get_reg_key_val(win32_reg_key, val_name, reg_type) nil end - def self.get_sql_reg_val_features(key_name, reg_val_feat_hash) - require 'win32/registry' + def self.key_exists?(path) + begin + open(HKLM, path, KEY_READ | KEY64) {} + return true + rescue + return false + end + end + def self.get_sql_reg_val_features(key_name, reg_val_feat_hash) vals = [] - begin - hklm = Win32::Registry::HKEY_LOCAL_MACHINE - vals = hklm.open(key_name, KEY_READ | KEY_WOW64_64KEY) do |key| + vals = open(HKLM, key_name, KEY_READ | KEY64) 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 + rescue Puppet::Util::Windows::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' + def self.get_reg_instance_info(friendly_version) + instance_root = 'SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names' + return [] unless key_exists?(instance_root) + discovered = {} + open(HKLM, "#{instance_root}", KEY_READ | KEY64) do |registry| + each_key(registry) do |instance_type, _| + open(HKLM, "#{instance_root}\\#{instance_type}", KEY_READ | KEY64) do |instance| + each_value(instance) do |short_name, _, long_name| + root = "Software\\Microsoft\\Microsoft SQL Server\\#{long_name}" + discovered[short_name] ||= { + 'name' => short_name, + 'reg_root' => [], + 'version' => open(HKLM, "#{root}\\MSSQLServer\\CurrentVersion", KEY_READ | KEY64) { |r| values(r)['CurrentVersion'] }, + 'version_friendly' => friendly_version + } + + discovered[short_name]['reg_root'].push(root) + end + end + end + end + discovered.values + end + def self.get_sql_reg_key_features(key_name, reg_key_feat_hash, instance_name) 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| + open(HKLM, "#{key_name}\\#{subkey}", KEY_READ | KEY64) do |feat_key| get_reg_key_val(feat_key, instance_name, Win32::Registry::REG_SZ) end - rescue Win32::Registry::Error # subkey doesn't exist + rescue Puppet::Util::Windows::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 @@ -215,8 +189,9 @@ def self.get_instances .map do |version| major_version = SQL_CONFIGURATION[version][:major_version] - instances = get_instance_names_by_ver(version) - .map { |name| [ name, get_instance_info(version, name) ] } + instances = get_reg_instance_info(version).map do |instance| + [instance['name'], get_instance_info(version,instance)] + end # Instance names are unique on a single host, but not for a particular SQL Server version therefore # it's possible to request information for a valid instance_name but not for version. In this case @@ -266,15 +241,17 @@ def self.get_features # "RS" # ] # } - def self.get_instance_info(version, instance_name) + def self.get_instance_info(version, sql_instance) return nil if version.nil? - sql_instance = get_wmi_instance_info(version, instance_name) # Instance names are unique on a single host, but not for a particular SQL Server version therefore # it's possible to request information for a valid instance_name but not for version. In this case # we just return nil. return nil if sql_instance['reg_root'].nil? - feats = get_instance_features(sql_instance['reg_root'], sql_instance['name']) - sql_instance.merge({'features' => feats}) + + feats = sql_instance['reg_root'].map do |reg_root| + get_instance_features(reg_root, sql_instance['name']) + end + sql_instance.merge({'features' => feats.uniq}) end end end From 5f7caec39828357e0fc80b9c902d4d0a22c93222 Mon Sep 17 00:00:00 2001 From: Michael T Lombardi Date: Thu, 2 Nov 2017 13:19:14 -0500 Subject: [PATCH 2/2] (MAINT) Remove extraneous logic from spec This commit removes logic from the features file in the acceptance tests suite which existed only to support Puppet 3. This logic is no longer needed as Puppet 3 is no longer supported. Additionally, the way the logic was written caused the tests designed for Puppet 3 to run against Puppet 5. This commit simplifies the maintenance and execution of the suite of acceptance tests. --- spec/acceptance/z_last_sqlserver_features_spec.rb | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/spec/acceptance/z_last_sqlserver_features_spec.rb b/spec/acceptance/z_last_sqlserver_features_spec.rb index f85142bc..667f77f6 100644 --- a/spec/acceptance/z_last_sqlserver_features_spec.rb +++ b/spec/acceptance/z_last_sqlserver_features_spec.rb @@ -204,17 +204,8 @@ def bind_and_apply_failing_manifest(features, ensure_val = 'present') before(:all) do puppet_version = (on host, puppet('--version')).stdout.chomp - - if puppet_version =~ /^4\.\d+\.\d+/ - json_result = JSON.parse((on host, puppet('facts --render-as json')).raw_output)["values"]["sqlserver_instances"] - names = json_result.collect { |k, v| json_result[k].keys }.flatten - else - # use agents fact to get instance names - distmoduledir = on(host, "echo #{host['distmoduledir']}").raw_output.chomp - facter_opts = {:environment => {'FACTERLIB' => "#{distmoduledir}/sqlserver/lib/facter"}} - - names = eval(fact_on(host, 'sqlserver_instances', facter_opts)).values.inject(:merge).keys - end + json_result = JSON.parse((on host, puppet('facts --render-as json')).raw_output)["values"]["sqlserver_instances"] + names = json_result.collect { |k, v| json_result[k].keys }.flatten remove_sql_instances(host, {:version => sql_version, :instance_names => names}) end