diff --git a/lib/facter/sqlserver_features.rb b/lib/facter/sqlserver_features.rb new file mode 100644 index 00000000..391871dd --- /dev/null +++ b/lib/facter/sqlserver_features.rb @@ -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 diff --git a/lib/facter/sqlserver_instances.rb b/lib/facter/sqlserver_instances.rb new file mode 100644 index 00000000..8ba8cfdb --- /dev/null +++ b/lib/facter/sqlserver_instances.rb @@ -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 diff --git a/lib/puppet/provider/sqlserver.rb b/lib/puppet/provider/sqlserver.rb index e196a30a..040993fa 100644 --- a/lib/puppet/provider/sqlserver.rb +++ b/lib/puppet/provider/sqlserver.rb @@ -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' @@ -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 diff --git a/lib/puppet/provider/sqlserver_features/mssql.rb b/lib/puppet/provider/sqlserver_features/mssql.rb index 5cb4c516..1280f54b 100644 --- a/lib/puppet/provider/sqlserver_features/mssql.rb +++ b/lib/puppet/provider/sqlserver_features/mssql.rb @@ -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]}" diff --git a/lib/puppet/provider/sqlserver_instance/mssql.rb b/lib/puppet/provider/sqlserver_instance/mssql.rb index ceeb5d01..edf4dff3 100644 --- a/lib/puppet/provider/sqlserver_instance/mssql.rb +++ b/lib/puppet/provider/sqlserver_instance/mssql.rb @@ -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 diff --git a/lib/puppet_x/sqlserver/features.rb b/lib/puppet_x/sqlserver/features.rb new file mode 100644 index 00000000..bd3a3f02 --- /dev/null +++ b/lib/puppet_x/sqlserver/features.rb @@ -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) + 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 diff --git a/lib/puppet_x/sqlserver/server_helper.rb b/lib/puppet_x/sqlserver/server_helper.rb index 886bf674..38eca0b4 100644 --- a/lib/puppet_x/sqlserver/server_helper.rb +++ b/lib/puppet_x/sqlserver/server_helper.rb @@ -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 diff --git a/spec/acceptance/sqlserver_features_spec.rb b/spec/acceptance/sqlserver_features_spec.rb index f9e843c1..8d61dbc6 100644 --- a/spec/acceptance/sqlserver_features_spec.rb +++ b/spec/acceptance/sqlserver_features_spec.rb @@ -233,4 +233,39 @@ def bind_and_apply_failing_manifest(host, features, ensure_val = 'present') ensure_sql_features(host, features) end end + + context 'with no installed instances' do + + context 'can install' do + + features = ['Tools', 'BC', 'Conn', 'SSMS', 'ADV_SSMS', 'SDK', 'IS', 'MDS'] + + before(:all) do + # 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 + remove_sql_instances(host, {:version => version, :instance_names => names}) + end + + after(:all) do + remove_sql_features(host, {:features => features, :version => version}) + end + + it 'all possible features' do + ensure_sql_features(host, features) + + validate_sql_install(host, {:version => version}) do |r| + expect(r.stdout).to match(/Management Tools - Basic/) + expect(r.stdout).to match(/Management Tools - Complete/) + expect(r.stdout).to match(/Client Tools Connectivity/) + expect(r.stdout).to match(/Client Tools Backwards Compatibility/) + expect(r.stdout).to match(/Client Tools SDK/) + expect(r.stdout).to match(/Integration Services/) + expect(r.stdout).to match(/Master Data Services/) + end + end + end + end end diff --git a/spec/sql_testing_helpers.rb b/spec/sql_testing_helpers.rb index ffb3f865..e6f31bb6 100644 --- a/spec/sql_testing_helpers.rb +++ b/spec/sql_testing_helpers.rb @@ -123,6 +123,14 @@ def remove_sql_features(host, opts = {}) on(host, "cmd.exe /c \"#{cmd}\"", {:acceptable_exit_codes => [0, 1, 2]}) end +def remove_sql_instances(host, opts = {}) + bootstrap_dir, setup_dir = get_install_paths(opts[:version]) + opts[:instance_names].each do |instance_name| + cmd = "cd \\\"#{setup_dir}\\\" && setup.exe /Action=uninstall /Q /IACCEPTSQLSERVERLICENSETERMS /FEATURES=SQL,AS,RS /INSTANCENAME=#{instance_name}" + on(host, "cmd.exe /c \"#{cmd}\"", {:acceptable_exit_codes => [0]}) + end +end + def get_install_paths(version) vers = { '2012' => '110', '2014' => '120' } diff --git a/spec/unit/puppet/provider/sqlserver_features_spec.rb b/spec/unit/puppet/provider/sqlserver_features_spec.rb index f4aa21da..69455359 100644 --- a/spec/unit/puppet/provider/sqlserver_features_spec.rb +++ b/spec/unit/puppet/provider/sqlserver_features_spec.rb @@ -61,7 +61,7 @@ context 'it should expand the superset for features' do include_context 'features' let(:additional_params) { {:features => %w(Tools)} } - let(:munged_args) { {:features => %w(ADV_SSMS Conn SSMS)} } + let(:munged_args) { {:features => %w(ADV_SSMS BC Conn SDK SSMS)} } it_should_behave_like 'create' end @@ -109,7 +109,7 @@ context 'it should install the expanded tools set' do include_context 'features' @feature_params[:features] = %w(Tools) - let(:feature_add) { %w(ADV_SSMS Conn SSMS) } + let(:feature_add) { %w(ADV_SSMS BC Conn SDK SSMS) } it_should_behave_like 'features=', @feature_params end diff --git a/spec/unit/puppet_x/server_helper_spec.rb b/spec/unit/puppet_x/server_helper_spec.rb index 2f1543ec..53fc8976 100644 --- a/spec/unit/puppet_x/server_helper_spec.rb +++ b/spec/unit/puppet_x/server_helper_spec.rb @@ -12,32 +12,6 @@ end end - shared_examples 'translate_features' do - it { - expect(PuppetX::Sqlserver::ServerHelper.translate_features(features)).to eq(translated) - } - end - - describe 'features parser' do - {: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'}.each do |k, v| - it_behaves_like 'translate_features' do - let(:features) { [v] } - let(:translated) { [k.to_s] } - end - end - end describe 'when calling with a local user' do ['mysillyuser', 'mybox\localuser'].each do |user| it_should_behave_like 'when calling with', user, false