Skip to content

Commit 28cf097

Browse files
committed
Merge pull request #99 from cyberious/TSqlRefacter
(FM-2577) - Change from sqlcmd.exe to win32ole connector
2 parents 9c4437e + 9f24c0c commit 28cf097

File tree

12 files changed

+262
-31
lines changed

12 files changed

+262
-31
lines changed

.fixtures.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@ fixtures:
22
forge_modules:
33
acl: "puppetlabs/acl"
44
stdlib: "puppetlabs/stdlib"
5-
acl: "puppetlabs/acl"
65
symlinks:
76
sqlserver: "#{source_dir}"

lib/puppet/provider/sqlserver.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def try_execute(command, msg = nil)
3030
##
3131
def self.run_authenticated_sqlcmd(query, opts)
3232
b = binding
33-
@sql_instance_config = "C:/Program Files/Microsoft SQL Server/.puppet/.#{opts[:instance_name]}.cfg"
33+
@sql_instance_config = File.join(Puppet[:vardir], "cache/sqlserver/.#{resource[:instance]}.cfg")
3434
if File.exists?(@sql_instance_config)
3535
@sql_instance_config = native_path(@sql_instance_config)
3636
else
Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
11
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'sqlserver'))
2+
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'puppet_x/sqlserver/sql_connection'))
23

34
Puppet::Type::type(:sqlserver_tsql).provide(:mssql, :parent => Puppet::Provider::Sqlserver) do
45

5-
def run(query, opts)
6-
debug("Running resource #{query} against #{resource[:instance]} with failonfail set to #{opts[:failonfail]}")
7-
opts[:instance_name] = resource[:instance]
8-
result = Puppet::Provider::Sqlserver.run_authenticated_sqlcmd(query, opts)
9-
return result
6+
7+
def run(query)
8+
debug("Running resource #{query} against #{resource[:instance]}")
9+
config = get_instance_config
10+
sqlconn = PuppetX::Sqlserver::SqlConnection.new
11+
12+
sqlconn.open_and_run_command(query, config)
1013
end
1114

12-
def run_update
13-
result = self.run(resource[:command], {:failonfail => true})
14-
return result
15+
def get_instance_config
16+
config_file = File.join(Puppet[:vardir], "cache/sqlserver/.#{resource[:instance]}.cfg")
17+
if !File.exists? (config_file)
18+
fail('Required config file missing, add the appropriate sqlserver::config and rerun')
19+
end
20+
if !File.readable?(config_file)
21+
fail('Unable to read config file, ensure proper permissions and please try again')
22+
end
23+
JSON.parse(File.read(config_file))
1524
end
1625

26+
def run_check
27+
return self.run(resource[:onlyif])
28+
end
29+
30+
def run_update
31+
return self.run(resource[:command])
32+
end
1733
end

lib/puppet/type/sqlserver_tsql.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ def self.checks
3232
newcheck(:onlyif, :parent => Puppet::Property::SqlserverTsql) do
3333
#Runs in the event that our TSQL exits with anything other than 0
3434
def check(value)
35-
begin
36-
output = provider.run(value, :failonfail => false)
37-
end
35+
output = provider.run(value)
3836
debug("OnlyIf returned exitstatus of #{output.exitstatus}")
3937
output.exitstatus != 0
4038
end
@@ -62,7 +60,10 @@ def output
6260

6361
def refresh
6462
if self.check_all_attributes(true)
65-
provider.run_update
63+
result = provider.run_update
64+
if result.has_errors
65+
fail("Unable to apply changes, failed with error message #{result.error_message}")
66+
end
6667
end
6768
end
6869

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
module PuppetX
2+
module Sqlserver
3+
4+
CONNECTION_CLOSED = 0
5+
6+
class SqlConnection
7+
attr_reader :exception_caught
8+
9+
10+
def initialize
11+
@connection = nil
12+
@data = nil
13+
end
14+
15+
def open_and_run_command(query, config)
16+
begin
17+
open(config)
18+
command(query)
19+
ensure
20+
close
21+
end
22+
23+
result
24+
end
25+
26+
private
27+
def connection
28+
@connection ||= create_connection
29+
end
30+
31+
def open(config)
32+
connection_string = get_connection_string(config)
33+
connection.Open(connection_string) unless !connection_closed?
34+
end
35+
36+
def get_connection_string(config)
37+
config = {'database' => 'master'}.merge(config)
38+
# Open ADO connection to the SQL Server database
39+
connection_string = "Provider=SQLOLEDB.1;"
40+
connection_string << "Persist Security Info=False;"
41+
connection_string << "User ID=#{config['admin']};"
42+
connection_string << "password=#{config['pass']};"
43+
connection_string << "Initial Catalog=#{config['database']};"
44+
connection_string << "Application Name=Puppet;"
45+
if config['instance'] !~ /^MSSQLSERVER$/
46+
connection_string << "Data Source=localhost\\#{config['instance']};"
47+
else
48+
connection_string << "Data Source=localhost;"
49+
end
50+
end
51+
52+
def command(sql)
53+
reset_instance
54+
begin
55+
r = execute(sql)
56+
yield(r) if block_given?
57+
rescue win32_exception => e
58+
@exception_caught = e
59+
end
60+
nil
61+
end
62+
63+
def result
64+
ResultOutput.new(has_errors, error_message)
65+
end
66+
67+
def has_errors
68+
@exception_caught != nil
69+
end
70+
71+
def error_message
72+
@exception_caught.message unless @exception_caught == nil
73+
end
74+
75+
def close
76+
begin
77+
connection.Close unless connection_closed?
78+
rescue win32_exception => e
79+
end
80+
end
81+
82+
def reset_instance
83+
@data = nil
84+
@fields = nil
85+
@exception_caught = nil
86+
end
87+
88+
def connection_closed?
89+
connection.State == CONNECTION_CLOSED
90+
end
91+
92+
def create_connection
93+
require 'win32ole'
94+
WIN32OLE.new('ADODB.Connection')
95+
end
96+
97+
def execute (sql)
98+
connection.Execute(sql, nil, nil)
99+
end
100+
101+
def parse_column_names(result)
102+
result.Fields.extend(Enumerable).map { |column| column.Name }
103+
end
104+
105+
# having as a method instead of hard coded allows us to stub and test outside of Windows
106+
def win32_exception
107+
::WIN32OLERuntimeError
108+
end
109+
110+
def connection=(conn)
111+
@connection = conn
112+
end
113+
end
114+
115+
class ResultOutput
116+
attr_reader :exitstatus, :error_message, :raw_error_message
117+
118+
def initialize(has_errors, error_message)
119+
@exitstatus = has_errors ? 1 : 0
120+
if error_message
121+
@raw_error_message = error_message
122+
@error_message = parse_for_error(error_message)
123+
end
124+
end
125+
126+
def has_errors
127+
@exitstatus != 0
128+
end
129+
130+
private
131+
def parse_for_error(result)
132+
match = result.match(/SQL Server\n\s+(.*)/i)
133+
match[1] unless match == nil
134+
end
135+
end
136+
end
137+
end

manifests/config.pp

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,10 @@
2424
$instance_name = $title,
2525
) {
2626
#possible future parameter if we do end up supporting different install directories
27-
$install_dir ='C:/Program Files/Microsoft SQL Server'
28-
$config_dir = "${install_dir}/.puppet"
27+
$config_dir = "${::puppet_vardir}/cache/sqlserver"
2928
$config_file = "${config_dir}/.${instance_name}.cfg"
30-
if !defined(File[$config_dir]){
31-
file{ $config_dir:
32-
ensure => directory
33-
}
34-
}
29+
ensure_resource('file', ["${::puppet_vardir}/cache",$config_dir], { 'ensure' => 'directory','recurse' => 'true' })
30+
3531
file{ $config_file:
3632
content => template('sqlserver/instance_config.erb'),
3733
require => File[$config_dir],

spec/defines/config_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
RSpec.describe 'sqlserver::config', :type => :define do
55
let(:title) { 'MSSQLSERVER' }
66
let(:params) { {
7-
:instance_name => 'MSSQLSERVER',
8-
:admin_user => 'sa',
9-
:admin_pass => 'Pupp3t1@',
7+
:instance_name => 'MSSQLSERVER',
8+
:admin_user => 'sa',
9+
:admin_pass => 'Pupp3t1@',
1010
} }
11-
let(:facts) { {:osfamily => 'windows', :platform => :windows} }
11+
let(:facts) { {:osfamily => 'windows', :platform => :windows, :puppet_vardir => 'C:/ProgramData/PuppetLabs/puppet/var'} }
1212
describe 'compile' do
1313
it {
14-
should contain_file('C:/Program Files/Microsoft SQL Server/.puppet/.MSSQLSERVER.cfg')
15-
should contain_file('C:/Program Files/Microsoft SQL Server/.puppet')
14+
should contain_file('C:/ProgramData/PuppetLabs/puppet/var/cache/sqlserver/.MSSQLSERVER.cfg')
15+
should contain_file('C:/ProgramData/PuppetLabs/puppet/var/cache/sqlserver')
1616
}
1717
end
1818
end

spec/functions/sqlserver_upcase_spec.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#! /usr/bin/env ruby -S rspec
21
require 'spec_helper'
32

43
describe "the sqlserver_upcase function" do

spec/functions/sqlserver_validate_hash_uniq_values_spec.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#! /usr/bin/env ruby -S rspec
21
require 'spec_helper'
32

43
describe "the sqlserver_validate_hash_uniq_values" do
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
require 'rspec'
2+
require 'spec_helper'
3+
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'lib/puppet_x/sqlserver/sql_connection'))
4+
5+
RSpec.describe PuppetX::Sqlserver::SqlConnection do
6+
let(:subject) { PuppetX::Sqlserver::SqlConnection.new }
7+
let(:config) { {'admin' => 'sa', 'pass' => 'Pupp3t1@', 'instance' => 'MSSQLSERVER'} }
8+
9+
def stub_connection
10+
@connection = mock()
11+
subject.stubs(:create_connection).returns(@connection)
12+
subject.stubs(:win32_exception).returns(Exception)
13+
end
14+
15+
def stub_no_errors
16+
subject.stubs(:has_errors).returns(false)
17+
subject.stubs(:error_message).returns(nil)
18+
end
19+
20+
describe 'open_and_run_command' do
21+
context 'command' do
22+
before :each do
23+
stub_connection
24+
@connection.stubs(:State).returns(0)
25+
@connection.stubs(:Open).with('Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;password=Pupp3t1@;Initial Catalog=master;Application Name=Puppet;Data Source=localhost;')
26+
end
27+
it 'should not raise an error but populate has_errors with message' do
28+
subject.stubs(:win32_exception).returns(Exception)
29+
subject.stubs(:execute).raises(Exception.new("SQL Server\n error has happened"))
30+
expect {
31+
result = subject.open_and_run_command('whacka whacka whacka', config)
32+
expect(result.exitstatus).to eq(1)
33+
expect(result.error_message).to eq('error has happened')
34+
}.to_not raise_error(Exception)
35+
36+
end
37+
it 'should yield when passed a block' do
38+
subject.stubs(:execute).returns('results')
39+
subject.open_and_run_command('myquery', config) do |r|
40+
expect(r).to eq('results')
41+
end
42+
end
43+
end
44+
context 'closed connection' do
45+
before :each do
46+
stub_connection
47+
stub_no_errors
48+
@connection.stubs(:State).returns(0)
49+
end
50+
it 'should not add MSSQLSERVER to connection string' do
51+
@connection.stubs(:Open).with('Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;password=Pupp3t1@;Initial Catalog=master;Application Name=Puppet;Data Source=localhost;')
52+
subject.open_and_run_command('query', {'admin' => 'sa', 'pass' => 'Pupp3t1@', 'instance' => 'MSSQLSERVER'})
53+
end
54+
it 'should add a non default instance to connection string' do
55+
@connection.stubs(:Open).with('Provider=SQLOLEDB.1;Persist Security Info=False;User ID=superuser;password=puppetTested;Initial Catalog=master;Application Name=Puppet;Data Source=localhost\LOGGING;')
56+
subject.open_and_run_command('query', {'admin' => 'superuser', 'pass' => 'puppetTested', 'instance' => 'LOGGING'})
57+
end
58+
end
59+
context 'open connection' do
60+
it 'should not reopen an existing connection' do
61+
stub_connection
62+
@connection.expects(:open).never
63+
@connection.stubs(:State).returns(1)
64+
@connection.expects(:Execute).with('query', nil, nil)
65+
subject.open_and_run_command('query', {'admin' => 'sa', 'pass' => 'Pupp3t1@', 'instance' => 'MSSQLSERVER'})
66+
end
67+
end
68+
context 'return result with errors' do
69+
it {
70+
subject.expects(:open).with({'admin' => 'sa', 'pass' => 'Pupp3t1@', 'instance' => 'MSSQLSERVER'})
71+
subject.expects(:command).with('SELECT * FROM sys.databases')
72+
subject.expects(:close).once
73+
subject.stubs(:has_errors).returns(:true)
74+
subject.stubs(:error_message).returns(
75+
'SQL Server
76+
invalid syntax provider')
77+
result =
78+
subject.open_and_run_command('SELECT * FROM sys.databases',
79+
{'instance' => 'MSSQLSERVER', 'admin' => 'sa', 'pass' => 'Pupp3t1@'})
80+
expect(result.exitstatus).to eq(1)
81+
expect(result.error_message).to eq('invalid syntax provider')
82+
}
83+
end
84+
end
85+
end

templates/create/login.sql.erb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,3 @@ BEGIN
4747
<% end -%>
4848
END
4949

50-

templates/instance_config.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ 'instance': '<%= @instance_name %>','admin':'<%= @admin_user %>','pass':'<%= @admin_pass %>' }
1+
{ "instance": "<%= @instance_name %>","admin":"<%= @admin_user %>","pass":"<%= @admin_pass %>" }

0 commit comments

Comments
 (0)