dopi 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +322 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +177 -0
- data/README.md +309 -0
- data/Rakefile +44 -0
- data/Vagrantfile +64 -0
- data/bin/dopi +4 -0
- data/doc/getting_started.md +247 -0
- data/doc/getting_started_examples/001_hello_world.yaml +17 -0
- data/doc/getting_started_examples/002_connecting_over_ssh.yaml +35 -0
- data/doc/plugins/custom.md +88 -0
- data/doc/plugins/mco/rpc.md +82 -0
- data/doc/plugins/ssh/custom.md +141 -0
- data/doc/plugins/ssh/file_contains.md +37 -0
- data/doc/plugins/ssh/file_deploy.md +52 -0
- data/doc/plugins/ssh/file_exists.md +31 -0
- data/doc/plugins/ssh/file_replace.md +37 -0
- data/doc/plugins/ssh/puppet_agent_run.md +50 -0
- data/doc/plugins/ssh/reboot.md +22 -0
- data/doc/plugins/ssh/wait_for_login.md +53 -0
- data/doc/plugins/winrm/cmd.md +161 -0
- data/doc/plugins/winrm/file_contains.md +39 -0
- data/doc/plugins/winrm/file_exists.md +31 -0
- data/doc/plugins/winrm/powershell.md +27 -0
- data/doc/plugins/winrm/puppet_agent_run.md +49 -0
- data/doc/plugins/winrm/reboot.md +17 -0
- data/doc/plugins/winrm/wait_for_login.md +55 -0
- data/dopi.gemspec +42 -0
- data/lib/dopi/cli/command_add.rb +35 -0
- data/lib/dopi/cli/command_list.rb +19 -0
- data/lib/dopi/cli/command_remove.rb +31 -0
- data/lib/dopi/cli/command_reset.rb +27 -0
- data/lib/dopi/cli/command_run.rb +68 -0
- data/lib/dopi/cli/command_show.rb +109 -0
- data/lib/dopi/cli/command_update.rb +37 -0
- data/lib/dopi/cli/command_validate.rb +27 -0
- data/lib/dopi/cli/global_options.rb +55 -0
- data/lib/dopi/cli/log.rb +33 -0
- data/lib/dopi/cli.rb +57 -0
- data/lib/dopi/command/custom.rb +52 -0
- data/lib/dopi/command/dummy.rb +27 -0
- data/lib/dopi/command/mco/rpc.rb +158 -0
- data/lib/dopi/command/ssh/custom.rb +48 -0
- data/lib/dopi/command/ssh/file_contains.rb +70 -0
- data/lib/dopi/command/ssh/file_deploy.rb +71 -0
- data/lib/dopi/command/ssh/file_exists.rb +54 -0
- data/lib/dopi/command/ssh/file_replace.rb +96 -0
- data/lib/dopi/command/ssh/puppet_agent_run.rb +63 -0
- data/lib/dopi/command/ssh/reboot.rb +50 -0
- data/lib/dopi/command/ssh/wait_for_login.rb +68 -0
- data/lib/dopi/command/winrm/cmd.rb +44 -0
- data/lib/dopi/command/winrm/file_contains.rb +66 -0
- data/lib/dopi/command/winrm/file_exists.rb +51 -0
- data/lib/dopi/command/winrm/powershell.rb +16 -0
- data/lib/dopi/command/winrm/puppet_agent_run.rb +61 -0
- data/lib/dopi/command/winrm/reboot.rb +33 -0
- data/lib/dopi/command/winrm/wait_for_login.rb +49 -0
- data/lib/dopi/command.rb +239 -0
- data/lib/dopi/command_parser/arguments.rb +38 -0
- data/lib/dopi/command_parser/credentials.rb +59 -0
- data/lib/dopi/command_parser/env.rb +37 -0
- data/lib/dopi/command_parser/exec.rb +27 -0
- data/lib/dopi/command_parser/exit_code.rb +73 -0
- data/lib/dopi/command_parser/output.rb +126 -0
- data/lib/dopi/command_set.rb +66 -0
- data/lib/dopi/connector/local.rb +77 -0
- data/lib/dopi/connector/ssh.rb +170 -0
- data/lib/dopi/connector/winrm.rb +167 -0
- data/lib/dopi/error.rb +43 -0
- data/lib/dopi/log.rb +18 -0
- data/lib/dopi/node.rb +70 -0
- data/lib/dopi/plan.rb +99 -0
- data/lib/dopi/pluginmanager.rb +62 -0
- data/lib/dopi/state.rb +226 -0
- data/lib/dopi/state_store.rb +155 -0
- data/lib/dopi/step.rb +227 -0
- data/lib/dopi/step_set.rb +70 -0
- data/lib/dopi/version.rb +3 -0
- data/lib/dopi.rb +165 -0
- data/spec/command_helper.rb +11 -0
- data/spec/fixtures/mco_client.cfg +26 -0
- data/spec/fixtures/plans/fail_on_timeout.yaml +20 -0
- data/spec/fixtures/plans/hello_world.yaml +34 -0
- data/spec/fixtures/plans/non_existing_node.yaml +26 -0
- data/spec/fixtures/plans/test_role_variable.yaml +29 -0
- data/spec/fixtures/puppet/Puppetfile +8 -0
- data/spec/fixtures/puppet/Puppetfile.lock +57 -0
- data/spec/fixtures/puppet/hiera.yaml +6 -0
- data/spec/fixtures/puppet/manifests/site.pp +52 -0
- data/spec/fixtures/test_configuration.yaml +54 -0
- data/spec/fixtures/test_credentials.yaml +11 -0
- data/spec/fixtures/test_deloyed_file.txt +5 -0
- data/spec/fixtures/test_infrastructure.yaml +12 -0
- data/spec/fixtures/test_nodes.yaml +45 -0
- data/spec/fixtures/testenv_plan.yaml +159 -0
- data/spec/integration/dopi/addrun_spec.rb +31 -0
- data/spec/integration/dopi/cli/command_run_spec.rb +38 -0
- data/spec/integration/dopi/cli/global_options_spec.rb +128 -0
- data/spec/integration/dopi/command_spec.rb +66 -0
- data/spec/integration/dopi/fail_check_plans/file_exists_fails.yaml +38 -0
- data/spec/integration/dopi/fail_check_plans/output_parser.yaml +39 -0
- data/spec/integration/dopi/fail_check_plans/powershell_fail.yaml +25 -0
- data/spec/integration/dopi/fail_check_plans/timeout.yaml +29 -0
- data/spec/integration/dopi/fail_check_plans/verify_commands.yaml +33 -0
- data/spec/integration/dopi/failplan.rb +27 -0
- data/spec/integration/dopi/plan.rb +27 -0
- data/spec/integration/dopi/plans/dummy.yaml +29 -0
- data/spec/integration/dopi/plans/max_per_role.yaml +55 -0
- data/spec/integration/dopi/plans/no_timeout.yaml +29 -0
- data/spec/integration/dopi/plans/node_and_role_patterns.yaml +58 -0
- data/spec/integration/dopi/plans/node_by_config.yaml +116 -0
- data/spec/integration/dopi/plans/plugin_defaults.yaml +86 -0
- data/spec/integration/dopi/plans/plugins/mco/rpc.yaml +33 -0
- data/spec/integration/dopi/plans/plugins/ssh/custom.yaml +97 -0
- data/spec/integration/dopi/plans/plugins/ssh/file_contains.yaml +51 -0
- data/spec/integration/dopi/plans/plugins/ssh/file_deploy.yaml +82 -0
- data/spec/integration/dopi/plans/plugins/ssh/file_exists.yaml +69 -0
- data/spec/integration/dopi/plans/plugins/ssh/file_replace.yaml +55 -0
- data/spec/integration/dopi/plans/plugins/ssh/puppet_agent_run.yaml +45 -0
- data/spec/integration/dopi/plans/plugins/ssh/reboot.yaml +43 -0
- data/spec/integration/dopi/plans/plugins/ssh/wait_for_login.yaml +45 -0
- data/spec/integration/dopi/plans/plugins/winrm/cmd.yaml +39 -0
- data/spec/integration/dopi/plans/plugins/winrm/file_contains.yaml +51 -0
- data/spec/integration/dopi/plans/plugins/winrm/file_exists.yaml +69 -0
- data/spec/integration/dopi/plans/plugins/winrm/reboot.yaml +31 -0
- data/spec/integration/dopi/plans/resolve_roles_on_validate.yaml +23 -0
- data/spec/integration/dopi/plans/ssh_parallel.yaml +37 -0
- data/spec/integration/dopi/plans/verify_commands.yaml +49 -0
- data/spec/spec_helper.rb +104 -0
- data/spec/unit/dopi/command/custom_spec.rb +58 -0
- data/spec/unit/dopi/command/mco/rpc_spec.rb +157 -0
- data/spec/unit/dopi/command/ssh/custom_spec.rb +30 -0
- data/spec/unit/dopi/command/ssh/file_deploy_spec.rb +42 -0
- data/spec/unit/dopi/command/ssh/file_replace_spec.rb +35 -0
- data/spec/unit/dopi/command_parser/credentials_spec.rb +53 -0
- data/spec/unit/dopi/command_parser/exit_code_spec.rb +63 -0
- data/spec/unit/dopi/command_parser/output_spec.rb +129 -0
- data/spec/unit/dopi/command_spec.rb +14 -0
- data/spec/unit/dopi/connector/winrm_spec.rb +111 -0
- data/spec/unit/dopi/node_spec.rb +24 -0
- data/spec/unit/dopi/plan_spec.rb +31 -0
- data/spec/unit/dopi/state_spec.rb +109 -0
- data/spec/unit/dopi/step_spec.rb +13 -0
- metadata +448 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#
|
|
2
|
+
#
|
|
3
|
+
# This class represents the set commands for a
|
|
4
|
+
# specific node and step.
|
|
5
|
+
#
|
|
6
|
+
# It will also manage the run group restrictions
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
module Dopi
|
|
10
|
+
class CommandSet
|
|
11
|
+
include Dopi::State
|
|
12
|
+
attr_reader :plan, :step, :node
|
|
13
|
+
|
|
14
|
+
def initialize(step_parser, step, node)
|
|
15
|
+
@step_parser = step_parser
|
|
16
|
+
@step = step
|
|
17
|
+
@plan = step.plan
|
|
18
|
+
@node = node
|
|
19
|
+
|
|
20
|
+
commands.each{|command| state_add_child(command)}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def name
|
|
24
|
+
@node.name
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def commands
|
|
28
|
+
@commands ||= @step_parser.commands.map do |command|
|
|
29
|
+
Dopi::Command.create_plugin_instance(command, @step, node)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid?
|
|
34
|
+
begin
|
|
35
|
+
commands.all?{|command| command.meta_valid?}
|
|
36
|
+
rescue PluginLoaderError => e
|
|
37
|
+
Dopi.log.error("Step '#{name}': Can't load plugin : #{e.message}")
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run(noop)
|
|
43
|
+
commands.each do |command|
|
|
44
|
+
break if state_failed? or signals[:stop]
|
|
45
|
+
command.meta_run(noop)
|
|
46
|
+
break unless command.state_done?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load_state(state_hash)
|
|
51
|
+
return if state_hash.empty?
|
|
52
|
+
commands.each_with_index do |command, i|
|
|
53
|
+
command.load_state(state_hash[i])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def state_hash
|
|
58
|
+
commands.map do |command|
|
|
59
|
+
command.state_hash
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This connector simply executes commands on the local
|
|
3
|
+
# machine. It does all the signal handling and makes
|
|
4
|
+
# sure processes are stopped or killed.
|
|
5
|
+
#
|
|
6
|
+
require 'pty'
|
|
7
|
+
require 'open3'
|
|
8
|
+
|
|
9
|
+
module Dopi
|
|
10
|
+
module Connector
|
|
11
|
+
module Local
|
|
12
|
+
|
|
13
|
+
# The command method executes the command of the step.
|
|
14
|
+
# Returns an array with stdio, sterror and exit code.
|
|
15
|
+
def local_command(env, command_string)
|
|
16
|
+
master, slave = PTY.open
|
|
17
|
+
stdout_r, stdout_w = IO.pipe
|
|
18
|
+
stderr_r, stderr_w = IO.pipe
|
|
19
|
+
cmd_stdout = ''
|
|
20
|
+
cmd_stderr = ''
|
|
21
|
+
options = {
|
|
22
|
+
:pgroup => true,
|
|
23
|
+
:unsetenv_others => true,
|
|
24
|
+
:in => slave,
|
|
25
|
+
:out => stdout_w,
|
|
26
|
+
:err => stderr_w,
|
|
27
|
+
}
|
|
28
|
+
log(:debug, "Executing #{command_string} for command #{name}")
|
|
29
|
+
log(:debug, "Environment: #{env.to_s}")
|
|
30
|
+
|
|
31
|
+
pid = Process.spawn(merged_env(env), command_string, options)
|
|
32
|
+
slave.close
|
|
33
|
+
stdout_w.close
|
|
34
|
+
stderr_w.close
|
|
35
|
+
|
|
36
|
+
signal_handler = Proc.new do |signal|
|
|
37
|
+
case signal
|
|
38
|
+
when :abort then Process.kill(:TERM, pid)
|
|
39
|
+
when :kill then Process.kill(:KILL, pid)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
on_signal(signal_handler)
|
|
44
|
+
stdout_thread = Thread.new do
|
|
45
|
+
until ( line = stdout_r.gets ).nil? do
|
|
46
|
+
cmd_stdout << line
|
|
47
|
+
log(:debug, line.gsub("\n", '').gsub("\r", ''))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
stderr_thread = Thread.new do
|
|
52
|
+
until ( line = stderr_r.gets ).nil? do
|
|
53
|
+
cmd_stderr << line
|
|
54
|
+
log(:error, line.gsub("\n", '').gsub("\r", ''))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
_, status = Process.wait2(pid)
|
|
59
|
+
stdout_thread.join
|
|
60
|
+
stderr_thread.join
|
|
61
|
+
delete_on_signal(signal_handler)
|
|
62
|
+
[ cmd_stdout, cmd_stderr, status.exitstatus ]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def merged_env(env)
|
|
68
|
+
{
|
|
69
|
+
'HOME' => ENV['HOME'],
|
|
70
|
+
'PATH' => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
|
|
71
|
+
}.merge(env)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This connector will execute commands over ssh
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
module Dopi
|
|
6
|
+
module Connector
|
|
7
|
+
module Ssh
|
|
8
|
+
include Dopi::Connector::Local
|
|
9
|
+
include Dopi::CommandParser::Credentials
|
|
10
|
+
|
|
11
|
+
public
|
|
12
|
+
|
|
13
|
+
def validate_ssh
|
|
14
|
+
log_validation_method(:port_valid?, CommandParsingError)
|
|
15
|
+
log_validation_method(:quiet_valid?, CommandParsingError)
|
|
16
|
+
log_validation_method(:check_host_key_valid?, CommandParsingError)
|
|
17
|
+
log_validation_method(:base64_valid?, CommandParsingError)
|
|
18
|
+
log_validation_method(:ssh_options_valid?, CommandParsingError)
|
|
19
|
+
validate_credentials
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ssh_command(env, command_string)
|
|
23
|
+
credential = working_ssh_credential
|
|
24
|
+
ssh_command_string = create_ssh_command_string(credential, env, command_string)
|
|
25
|
+
local_env = create_local_env(credential)
|
|
26
|
+
local_env.merge!(env) unless base64 # keep old behaviour for escaping
|
|
27
|
+
local_command(local_env, ssh_command_string)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def port
|
|
31
|
+
@port ||= port_valid? ? hash[:port].to_s : '22'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def quiet
|
|
35
|
+
@quiet || quiet_valid? ? hash[:quiet] : true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def check_host_key
|
|
39
|
+
@check_host_key || check_host_key_valid? ? hash[:check_host_key] : false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def base64
|
|
43
|
+
@base64 || base64_valid? ? hash[:base64] : true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ssh_options
|
|
47
|
+
#TBD
|
|
48
|
+
[]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def supported_credential_types
|
|
52
|
+
[:username_password, :ssh_key]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# this method checks for every credential if a login is possible
|
|
58
|
+
# and will return the first one where it is possible.
|
|
59
|
+
# If none of the credentials work it will raise a CommandConnectionError.
|
|
60
|
+
def working_ssh_credential
|
|
61
|
+
credentials.find do |credential|
|
|
62
|
+
ssh_command_string = create_ssh_command_string(credential, {}, 'exit')
|
|
63
|
+
local_env = create_local_env(credential)
|
|
64
|
+
local_command(local_env, ssh_command_string)[2] == 0
|
|
65
|
+
end or raise CommandConnectionError,
|
|
66
|
+
"Can't establish connection with node #{@node.name} with any of the given" +
|
|
67
|
+
"credentials #{credentials.map{|c| c.name}.join(', ')}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create_local_env(credential)
|
|
71
|
+
if credential.type == :username_password
|
|
72
|
+
{ 'SSHPASS' => credential.password }
|
|
73
|
+
else
|
|
74
|
+
{}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def create_ssh_command_string(credential, env, command_string)
|
|
79
|
+
address = @node.address(port)
|
|
80
|
+
opts = options(credential)
|
|
81
|
+
prefix = credential.type == :username_password ? sshpass_cmd : ''
|
|
82
|
+
cmd = "#{ssh_env_string(env)} #{command_string}"
|
|
83
|
+
real_cmd = if base64
|
|
84
|
+
log(:debug, "Unencoded command: '#{cmd}'")
|
|
85
|
+
ssh_encode_command(cmd)
|
|
86
|
+
else
|
|
87
|
+
ssh_escape_command(cmd)
|
|
88
|
+
end
|
|
89
|
+
"#{prefix}ssh #{opts} #{credential.username}@#{address} \"#{real_cmd}\""
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def options(credential)
|
|
93
|
+
opts = []
|
|
94
|
+
opts << "-p #{port}"
|
|
95
|
+
opts << '-q' if quiet
|
|
96
|
+
opts << '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' unless check_host_key
|
|
97
|
+
if credential.type == :ssh_key
|
|
98
|
+
opts << '-o ChallengeResponseAuthentication=no'
|
|
99
|
+
opts << '-o PasswordAuthentication=no'
|
|
100
|
+
opts << "-i #{credential.private_key}"
|
|
101
|
+
end
|
|
102
|
+
# Force allocation of tty, needed to propagate signals to remotely
|
|
103
|
+
# spawned processes
|
|
104
|
+
opts << '-tt'
|
|
105
|
+
opts += ssh_options
|
|
106
|
+
opts += ssh_options_defaults if respond_to?(:ssh_options_defaults)
|
|
107
|
+
opts.join(' ')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def sshpass_bin
|
|
111
|
+
@sshpass_bin ||= ENV['PATH'].split(':').map{|p| File.join(p, 'sshpass')}.find do |f|
|
|
112
|
+
File.exists?(f) && File.executable?(f)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def sshpass_cmd
|
|
117
|
+
@sshpass_cmd ||= sshpass_bin ? sshpass_bin + ' -e ' : ""
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ssh_env_string(env)
|
|
121
|
+
env.map{|variable,value| "export #{variable}=#{value};" }.join(' ')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ssh_escape_command(cmd)
|
|
125
|
+
cmd.gsub("\\", "\\\\\\\\").gsub('"', '\\"')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def ssh_encode_command(cmd)
|
|
129
|
+
"echo -n #{Base64.strict_encode64(cmd)} | base64 -d | bash"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def port_valid?
|
|
133
|
+
return false unless hash.kind_of?(Hash)
|
|
134
|
+
return false if hash[:port].nil? # is optional
|
|
135
|
+
hash[:port].kind_of?(Fixnum) or
|
|
136
|
+
raise CommandParsingError, "Plugin #{name}: The value for port must be a number"
|
|
137
|
+
hash[:port].between?(0, 65536) or
|
|
138
|
+
raise CommandParsingError, "Plugin #{name}: The value for port must bigger than 0 and below 65536"
|
|
139
|
+
true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def quiet_valid?
|
|
143
|
+
return false unless hash.kind_of?(Hash)
|
|
144
|
+
return false if hash[:quiet].nil? # is optional
|
|
145
|
+
hash[:quiet].kind_of?(TrueClass) or hash[:quiet].kind_of?(FalseClass) or
|
|
146
|
+
raise CommandParsingError, "Plugin #{name}: The value for 'quiet' must be boolean"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def check_host_key_valid?
|
|
150
|
+
return false unless hash.kind_of?(Hash)
|
|
151
|
+
return false if hash[:check_host_key].nil? # is optional
|
|
152
|
+
hash[:check_host_key].kind_of?(TrueClass) or hash[:check_host_key].kind_of?(FalseClass) or
|
|
153
|
+
raise CommandParsingError, "Plugin #{name}: The value for 'check_host_key' must be boolean"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def base64_valid?
|
|
157
|
+
return false unless hash.kind_of?(Hash)
|
|
158
|
+
return false if hash[:base64].nil? # is optional
|
|
159
|
+
hash[:base64].kind_of?(TrueClass) or hash[:base64].kind_of?(FalseClass) or
|
|
160
|
+
raise CommandParsingError, "Plugin #{name}: The value for 'base64' must be boolean"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ssh_options_valid?
|
|
164
|
+
#TBD
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#
|
|
2
|
+
# DOPi Plugin: WinRM Command
|
|
3
|
+
#
|
|
4
|
+
require 'winrm'
|
|
5
|
+
require 'gssapi'
|
|
6
|
+
|
|
7
|
+
module Dopi
|
|
8
|
+
module Connector
|
|
9
|
+
module Winrm
|
|
10
|
+
include Dopi::CommandParser::Credentials
|
|
11
|
+
|
|
12
|
+
def validate_winrm
|
|
13
|
+
log_validation_method(:port_valid?)
|
|
14
|
+
log_validation_method(:ssl_valid?)
|
|
15
|
+
log_validation_method(:ca_trust_path_valid?)
|
|
16
|
+
log_validation_method(:disable_sspi_valid?)
|
|
17
|
+
log_validation_method(:basic_auth_only_valid?)
|
|
18
|
+
validate_credentials
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def winrm_command(command_string)
|
|
22
|
+
cmd_stdout = ""
|
|
23
|
+
cmd_stderr = ""
|
|
24
|
+
log(:debug, "Executing '#{command_string}' for command #{name}")
|
|
25
|
+
result = winrm.run_cmd(command_string) do |stdout, stderr|
|
|
26
|
+
unless stdout.nil? or stdout.empty?
|
|
27
|
+
cmd_stdout << stdout
|
|
28
|
+
log(:debug, stdout)
|
|
29
|
+
end
|
|
30
|
+
unless stderr.nil? or stderr.empty?
|
|
31
|
+
cmd_stderr << stderr
|
|
32
|
+
log(:error, stderr)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
winrm.close
|
|
36
|
+
[cmd_stdout, cmd_stderr, result[:exitcode]]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def winrm_powershell_command(command_string)
|
|
40
|
+
log(:debug, "Unencoded Powershell command '#{command_string}'")
|
|
41
|
+
script = WinRM::PowershellScript.new(command_string)
|
|
42
|
+
winrm_command("powershell -encodedCommand #{script.encoded()}")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def winrm
|
|
46
|
+
working_command_executor = nil
|
|
47
|
+
credentials.each do |credential|
|
|
48
|
+
begin
|
|
49
|
+
winrm_web_service = WinRM::WinRMWebService.new(
|
|
50
|
+
endpoint,
|
|
51
|
+
auth_method(credential),
|
|
52
|
+
:realm => credential.realm,
|
|
53
|
+
:service => credential.service,
|
|
54
|
+
:keytab => credential.keytab,
|
|
55
|
+
:user => credential.username,
|
|
56
|
+
:pass => credential.password,
|
|
57
|
+
:disable_sspi => disable_sspi,
|
|
58
|
+
:basic_auth_only => basic_auth_only,
|
|
59
|
+
:ca_trust_path => ca_trust_path
|
|
60
|
+
)
|
|
61
|
+
winrm_web_service.set_timeout(operation_timeout)
|
|
62
|
+
command_executor = WinRM::CommandExecutor.new(winrm_web_service)
|
|
63
|
+
command_executor.open
|
|
64
|
+
command_executor.run_cmd('exit') # test connection
|
|
65
|
+
rescue WinRM::WinRMAuthorizationError, GSSAPI::GssApiError => e
|
|
66
|
+
log(:warn, "Unable to login with credential #{credential.name} : #{e.message}")
|
|
67
|
+
rescue SocketError => e
|
|
68
|
+
raise CommandConnectionError,
|
|
69
|
+
"A problem occurred while trying to connect to node #{@node.name} : #{e.message}"
|
|
70
|
+
else working_command_executor = command_executor
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
working_command_executor or
|
|
74
|
+
raise CommandExecutionError,
|
|
75
|
+
"Unable to login with any of the given credentials: #{credentials.map{|c| c.name}.join(', ')}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def endpoint
|
|
79
|
+
"http://#{@node.address(port)}:#{port}/wsman"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def port
|
|
83
|
+
port_valid? ? hash[:port] : 5985
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def ssl
|
|
87
|
+
ssl_valid? ? hash[:ssl] : true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def ca_trust_path
|
|
91
|
+
ca_trust_path_valid? ? hash[:ca_trust_path] : nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def disable_sspi
|
|
95
|
+
disable_sspi_valid? ? hash[:disable_sspi] : nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def basic_auth_only
|
|
99
|
+
basic_auth_only_valid? ? hash[:basic_auth_only] : nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def operation_timeout
|
|
103
|
+
operation_timeout_valid? ? hash[:operation_timeout] : ( plugin_timeout - 5 )
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def supported_credential_types
|
|
107
|
+
[:username_password, :kerberos]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def auth_method(credential)
|
|
113
|
+
case credential.type
|
|
114
|
+
when :kerberos then :kerberos
|
|
115
|
+
when :username_password then ssl ? :ssl : :plaintext
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def port_valid?
|
|
120
|
+
return false if hash.nil?
|
|
121
|
+
return false if hash[:port].nil?
|
|
122
|
+
hash[:port].kind_of?(Fixnum) and (hash[:port] > 0) and (hash[:port] < 65536) or
|
|
123
|
+
raise CommandParsingError, "The value for 'port' has to be a number in the range of 1-65535"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def ssl_valid?
|
|
127
|
+
return false if hash.nil?
|
|
128
|
+
return false if hash[:ssl].nil?
|
|
129
|
+
hash[:ssl].kind_of?(TrueClass) or hash[:ssl].kind_of?(FalseClass) or
|
|
130
|
+
raise CommandParsingError, "The value for 'ssl_valid' has to be true or false"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def ca_trust_path_valid?
|
|
134
|
+
return false if hash.nil?
|
|
135
|
+
return false if hash[:ca_trust_path].nil?
|
|
136
|
+
hash[:ca_trust_path].kind_of?(String) or
|
|
137
|
+
raise CommandParsingError, "The value for ca_trust_path has to be a string"
|
|
138
|
+
File.directory?(hash[:ca_trust_path]) or
|
|
139
|
+
raise CommandParsingError, "The directory in 'ca_trust_path' does not exist"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def disable_sspi_valid?
|
|
143
|
+
return false if hash.nil?
|
|
144
|
+
return false if hash[:disable_sspi].nil?
|
|
145
|
+
hash[:disable_sspi].kind_of?(TrueClass) or hash[:disable_sspi].kind_of?(FalseClass) or
|
|
146
|
+
raise CommandParsingError, "The value for 'disable_sspi' has to be true or false"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def basic_auth_only_valid?
|
|
150
|
+
return false if hash.nil?
|
|
151
|
+
return false if hash[:basic_auth_only].nil?
|
|
152
|
+
hash[:basic_auth_only].kind_of?(TrueClass) or hash[:basic_auth_only].kind_of?(FalseClass) or
|
|
153
|
+
raise CommandParsingError, "The value for 'basic_auth_only' has to be true or false"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def operation_timeout_valid?
|
|
157
|
+
return false if hash.nil?
|
|
158
|
+
return false if hash[:operation_timeout].nil?
|
|
159
|
+
hash[:operation_timeout].kind_of?(Fixnum) or
|
|
160
|
+
raise CommandParsingError, 'The value for operation_timeout has to be a number'
|
|
161
|
+
hash[:operation_timeout] >= 0 or
|
|
162
|
+
raise CommandParsingError, 'The value for operation_timeout has to be positive number'
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/dopi/error.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Various error classes for DOPi
|
|
3
|
+
#
|
|
4
|
+
# Error hierarchy:
|
|
5
|
+
#
|
|
6
|
+
# PluginLoaderError
|
|
7
|
+
# StateTransitionError
|
|
8
|
+
# NoRoleFoundError
|
|
9
|
+
# CommandParsingError
|
|
10
|
+
# CommandExecutionError
|
|
11
|
+
# CommandExecutionError
|
|
12
|
+
# ConnectionError
|
|
13
|
+
# NodeConnectionError
|
|
14
|
+
# CommandConnectionError
|
|
15
|
+
#
|
|
16
|
+
module Dopi
|
|
17
|
+
class PluginLoaderError < StandardError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class StateTransitionError < StandardError
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class NoRoleFoundError < StandardError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class CommandParsingError < StandardError
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class CommandExecutionError < StandardError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class GracefulExit < StandardError
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class ConnectionError < CommandExecutionError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class NodeConnectionError < ConnectionError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class CommandConnectionError < ConnectionError
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/dopi/log.rb
ADDED
data/lib/dopi/node.rb
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This class loades a deployment plan
|
|
3
|
+
#
|
|
4
|
+
require 'hiera'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
require 'socket'
|
|
7
|
+
require 'timeout'
|
|
8
|
+
require 'forwardable'
|
|
9
|
+
|
|
10
|
+
module Dopi
|
|
11
|
+
class Node
|
|
12
|
+
extend Forwardable
|
|
13
|
+
|
|
14
|
+
attr_accessor :node_info
|
|
15
|
+
|
|
16
|
+
def initialize(node_parser, plan)
|
|
17
|
+
@node_parser = node_parser
|
|
18
|
+
@plan = plan
|
|
19
|
+
@addresses = {}
|
|
20
|
+
@node_info = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def_delegators :@node_parser,
|
|
24
|
+
:name,
|
|
25
|
+
:fqdn,
|
|
26
|
+
:has_name?,
|
|
27
|
+
:config,
|
|
28
|
+
:has_config?,
|
|
29
|
+
:config_includes?,
|
|
30
|
+
:fact,
|
|
31
|
+
:has_fact?,
|
|
32
|
+
:role,
|
|
33
|
+
:has_role?
|
|
34
|
+
|
|
35
|
+
def addresses
|
|
36
|
+
[ fqdn, plan_ip_addresses, node_info_ip_addresses ].flatten.uniq
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def address(port)
|
|
40
|
+
@addresses[port] ||= addresses.find {|addr| connection_possible?(addr,port)} or
|
|
41
|
+
raise NodeConnectionError, "Unable to establish a connection for node #{name} on port #{port} over #{addresses.join(', ')}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reset_address(port = nil)
|
|
45
|
+
port.nil? ? @addresses = {} : @addresses.delete(port)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def plan_ip_addresses
|
|
51
|
+
@node_parser.interfaces.map{|i| [:dhcp, :none].include?(i.ip) ? nil : i.ip}.compact
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def node_info_ip_addresses
|
|
55
|
+
node_info[:ip_addresses] || []
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def connection_possible?(address, port)
|
|
59
|
+
Timeout::timeout(DopCommon.config.connection_check_timeout.to_i) do
|
|
60
|
+
TCPSocket.new(address, port).close
|
|
61
|
+
end
|
|
62
|
+
Dopi.log.debug("Connection test with #{address}:#{port} for node #{name} ok")
|
|
63
|
+
true
|
|
64
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Timeout::Error, SocketError
|
|
65
|
+
Dopi.log.debug("Connection test with #{address}:#{port} for node #{name} failed")
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
end
|
|
70
|
+
end
|