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.
@@ -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