kdeploy 0.1.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/.editorconfig +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +100 -0
- data/LICENSE +21 -0
- data/README.md +1030 -0
- data/Rakefile +45 -0
- data/bin/kdeploy +7 -0
- data/kdeploy.gemspec +49 -0
- data/lib/kdeploy/banner.rb +28 -0
- data/lib/kdeploy/cli.rb +1452 -0
- data/lib/kdeploy/command.rb +182 -0
- data/lib/kdeploy/configuration.rb +83 -0
- data/lib/kdeploy/dsl.rb +566 -0
- data/lib/kdeploy/host.rb +85 -0
- data/lib/kdeploy/inventory.rb +243 -0
- data/lib/kdeploy/logger.rb +100 -0
- data/lib/kdeploy/pipeline.rb +249 -0
- data/lib/kdeploy/runner.rb +190 -0
- data/lib/kdeploy/ssh_connection.rb +187 -0
- data/lib/kdeploy/statistics.rb +439 -0
- data/lib/kdeploy/task.rb +240 -0
- data/lib/kdeploy/template.rb +173 -0
- data/lib/kdeploy/version.rb +6 -0
- data/lib/kdeploy.rb +106 -0
- data/scripts/common_tasks.rb +218 -0
- data/scripts/deploy.rb +50 -0
- metadata +178 -0
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kdeploy
|
4
|
+
# Command class for executing commands on remote hosts
|
5
|
+
class Command
|
6
|
+
attr_reader :name, :command, :options, :result
|
7
|
+
|
8
|
+
def initialize(name, command, options = {})
|
9
|
+
@name = name
|
10
|
+
@command = command
|
11
|
+
@options = default_options.merge(options)
|
12
|
+
@global_variables = options.delete(:global_variables) || {}
|
13
|
+
@result = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Execute command on specified host
|
17
|
+
# @param host [Host] Target host
|
18
|
+
# @param connection [SSHConnection] SSH connection
|
19
|
+
# @return [Boolean] True if command succeeded
|
20
|
+
def execute(host, connection)
|
21
|
+
start_time = Time.now
|
22
|
+
processed_command = process_command_template(host)
|
23
|
+
|
24
|
+
log_command_start(host, processed_command)
|
25
|
+
@result = execute_with_retry(connection, processed_command)
|
26
|
+
duration = Time.now - start_time
|
27
|
+
|
28
|
+
log_result(host, duration)
|
29
|
+
record_statistics(host.hostname, duration, @result[:success])
|
30
|
+
|
31
|
+
@result[:success]
|
32
|
+
rescue StandardError => e
|
33
|
+
handle_execution_error(host, e, start_time)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check if command should run on host
|
37
|
+
# @param host [Host] Target host
|
38
|
+
# @return [Boolean] True if command should run
|
39
|
+
def should_run_on?(host)
|
40
|
+
return true unless @options[:only] || @options[:except]
|
41
|
+
|
42
|
+
if @options[:only]
|
43
|
+
roles = Array(@options[:only])
|
44
|
+
return roles.any? { |role| host.has_role?(role) }
|
45
|
+
end
|
46
|
+
|
47
|
+
if @options[:except]
|
48
|
+
roles = Array(@options[:except])
|
49
|
+
return roles.none? { |role| host.has_role?(role) }
|
50
|
+
end
|
51
|
+
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def default_options
|
58
|
+
{
|
59
|
+
timeout: nil,
|
60
|
+
retry_count: nil,
|
61
|
+
retry_delay: nil,
|
62
|
+
ignore_errors: false,
|
63
|
+
only: nil,
|
64
|
+
except: nil
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def process_command_template(host)
|
69
|
+
processed = @command.dup
|
70
|
+
process_global_variables(processed)
|
71
|
+
process_host_variables(processed, host)
|
72
|
+
process_host_info(processed, host)
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_global_variables(command)
|
76
|
+
@global_variables.each_with_object(command) do |(key, value), cmd|
|
77
|
+
cmd.gsub!("{{#{key}}}", value.to_s)
|
78
|
+
cmd.gsub!("${#{key}}", value.to_s)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def process_host_variables(command, host)
|
83
|
+
host.vars.each_with_object(command) do |(key, value), cmd|
|
84
|
+
cmd.gsub!("{{#{key}}}", value.to_s)
|
85
|
+
cmd.gsub!("${#{key}}", value.to_s)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def process_host_info(command, host)
|
90
|
+
command.gsub('{{hostname}}', host.hostname)
|
91
|
+
.gsub('{{user}}', host.user)
|
92
|
+
.gsub('{{port}}', host.port.to_s)
|
93
|
+
end
|
94
|
+
|
95
|
+
def execute_with_retry(connection, command)
|
96
|
+
retry_count = @options[:retry_count] || Kdeploy.configuration&.retry_count || 0
|
97
|
+
retry_delay = @options[:retry_delay] || Kdeploy.configuration&.retry_delay || 1
|
98
|
+
|
99
|
+
result = nil
|
100
|
+
attempts = 0
|
101
|
+
|
102
|
+
loop do
|
103
|
+
attempts += 1
|
104
|
+
result = connection.execute(command, timeout: @options[:timeout])
|
105
|
+
|
106
|
+
break if result[:success] || attempts > retry_count
|
107
|
+
|
108
|
+
if attempts <= retry_count
|
109
|
+
log_retry_attempt(attempts, retry_count, retry_delay)
|
110
|
+
sleep(retry_delay)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
result
|
115
|
+
end
|
116
|
+
|
117
|
+
def log_command_start(host, command)
|
118
|
+
KdeployLogger.info("🚀 Executing '#{@name}' on #{host}")
|
119
|
+
KdeployLogger.debug(" Command: #{command}")
|
120
|
+
end
|
121
|
+
|
122
|
+
def log_retry_attempt(attempts, retry_count, retry_delay)
|
123
|
+
KdeployLogger.warn(
|
124
|
+
"Command '#{@name}' failed (attempt #{attempts}/#{retry_count + 1}), " \
|
125
|
+
"retrying in #{retry_delay}s..."
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
def log_result(host, duration)
|
130
|
+
if @result[:success]
|
131
|
+
log_success(host, duration)
|
132
|
+
else
|
133
|
+
log_failure(host, duration)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def log_success(host, duration)
|
138
|
+
KdeployLogger.info("✅ Command '#{@name}' completed on #{host} in #{duration.round(2)}s")
|
139
|
+
return if @result[:stdout].strip.empty?
|
140
|
+
|
141
|
+
KdeployLogger.info('📤 Output:')
|
142
|
+
@result[:stdout].strip.split("\n").each do |line|
|
143
|
+
KdeployLogger.info(" #{line}")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def log_failure(host, duration)
|
148
|
+
level = @options[:ignore_errors] ? :warn : :error
|
149
|
+
icon = @options[:ignore_errors] ? '⚠️' : '❌'
|
150
|
+
|
151
|
+
KdeployLogger.send(
|
152
|
+
level,
|
153
|
+
"#{icon} Command '#{@name}' failed on #{host} in #{duration.round(2)}s " \
|
154
|
+
"(exit code: #{@result[:exit_code]})"
|
155
|
+
)
|
156
|
+
|
157
|
+
KdeployLogger.send(level, "📤 STDERR: #{@result[:stderr]}") unless @result[:stderr].empty?
|
158
|
+
KdeployLogger.send(level, "📤 STDOUT: #{@result[:stdout]}") unless @result[:stdout].strip.empty?
|
159
|
+
end
|
160
|
+
|
161
|
+
def handle_execution_error(host, error, start_time)
|
162
|
+
duration = Time.now - start_time
|
163
|
+
KdeployLogger.error(
|
164
|
+
"Command '#{@name}' failed on #{host} after #{duration.round(2)}s: #{error.message}"
|
165
|
+
)
|
166
|
+
|
167
|
+
@result = {
|
168
|
+
stdout: '',
|
169
|
+
stderr: error.message,
|
170
|
+
exit_code: 1,
|
171
|
+
success: false
|
172
|
+
}
|
173
|
+
|
174
|
+
record_statistics(host.hostname, duration, false)
|
175
|
+
false
|
176
|
+
end
|
177
|
+
|
178
|
+
def record_statistics(hostname, duration, success)
|
179
|
+
Kdeploy.statistics.record_command(@name, hostname, success, duration)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kdeploy
|
4
|
+
# Configuration class for managing global settings
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :max_concurrent_tasks, :ssh_timeout, :command_timeout,
|
7
|
+
:retry_count, :retry_delay, :log_level, :log_file,
|
8
|
+
:default_user, :default_port, :ssh_options, :inventory_file,
|
9
|
+
:template_dir
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
set_default_values
|
13
|
+
end
|
14
|
+
|
15
|
+
# Load configuration from YAML file
|
16
|
+
# @param config_file [String] Path to configuration file
|
17
|
+
# @return [void]
|
18
|
+
def load_from_file(config_file)
|
19
|
+
return unless File.exist?(config_file)
|
20
|
+
|
21
|
+
config = YAML.load_file(config_file)
|
22
|
+
return unless config.is_a?(Hash)
|
23
|
+
|
24
|
+
apply_configuration(config)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Merge SSH options with defaults
|
28
|
+
# @param options [Hash] SSH options to merge
|
29
|
+
# @return [Hash] Merged SSH options
|
30
|
+
def merged_ssh_options(options = {})
|
31
|
+
ssh_options.merge(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def set_default_values
|
37
|
+
set_default_timeouts
|
38
|
+
set_default_retry_settings
|
39
|
+
set_default_logging
|
40
|
+
set_default_ssh_settings
|
41
|
+
set_default_paths
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_default_timeouts
|
45
|
+
@max_concurrent_tasks = 10
|
46
|
+
@ssh_timeout = 30
|
47
|
+
@command_timeout = 300
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_default_retry_settings
|
51
|
+
@retry_count = 3
|
52
|
+
@retry_delay = 1
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_default_logging
|
56
|
+
@log_level = :info
|
57
|
+
@log_file = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_default_ssh_settings
|
61
|
+
@default_user = ENV.fetch('USER', 'root')
|
62
|
+
@default_port = 22
|
63
|
+
@ssh_options = {
|
64
|
+
verify_host_key: :never,
|
65
|
+
non_interactive: true,
|
66
|
+
use_agent: true,
|
67
|
+
forward_agent: false
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_default_paths
|
72
|
+
@inventory_file = 'inventory.yml'
|
73
|
+
@template_dir = 'templates'
|
74
|
+
end
|
75
|
+
|
76
|
+
def apply_configuration(config)
|
77
|
+
config.each do |key, value|
|
78
|
+
method_name = "#{key}="
|
79
|
+
send(method_name, value) if respond_to?(method_name)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|