kdeploy 0.1.0 → 0.3.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.
@@ -1,182 +0,0 @@
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
@@ -1,83 +0,0 @@
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
data/lib/kdeploy/host.rb DELETED
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Host class for managing remote host configuration and connection details
5
- class Host
6
- attr_reader :hostname, :user, :port, :ssh_options, :roles, :vars
7
-
8
- def initialize(hostname, user: nil, port: nil, ssh_options: {}, roles: [], vars: {})
9
- @hostname = hostname
10
- @user = user || Kdeploy.configuration&.default_user || ENV.fetch('USER', nil)
11
- @port = port || Kdeploy.configuration&.default_port || 22
12
- @ssh_options = ssh_options
13
- @roles = Array(roles)
14
- @vars = vars || {}
15
- end
16
-
17
- # Check if host has specific role
18
- # @param role [String, Symbol] Role to check
19
- # @return [Boolean] True if host has role
20
- def has_role?(role)
21
- @roles.include?(role.to_s) || @roles.include?(role.to_sym)
22
- end
23
-
24
- # Get variable value
25
- # @param key [String, Symbol] Variable key
26
- # @return [Object] Variable value
27
- def var(key)
28
- @vars[key.to_s] || @vars[key.to_sym]
29
- end
30
-
31
- # Set variable value
32
- # @param key [String, Symbol] Variable key
33
- # @param value [Object] Variable value
34
- # @return [Object] Set value
35
- def set_var(key, value)
36
- @vars[key.to_s] = value
37
- end
38
-
39
- # Get connection string for display
40
- # @return [String] Connection string
41
- def connection_string
42
- "#{@user}@#{@hostname}:#{@port}"
43
- end
44
-
45
- # Get SSH connection options
46
- # @return [Hash] SSH options
47
- def connection_options
48
- base_options = Kdeploy.configuration&.merged_ssh_options(@ssh_options) || @ssh_options
49
- base_options.merge(
50
- timeout: Kdeploy.configuration&.ssh_timeout || 30
51
- )
52
- end
53
-
54
- # String representation of the host
55
- # @return [String] Connection string
56
- def to_s
57
- connection_string
58
- end
59
-
60
- # Detailed string representation of the host
61
- # @return [String] Host details
62
- def inspect
63
- "#<Kdeploy::Host #{connection_string} roles=#{@roles} vars=#{@vars.keys}>"
64
- end
65
-
66
- # Compare hosts for equality
67
- # @param other [Host] Host to compare with
68
- # @return [Boolean] True if hosts are equal
69
- def ==(other)
70
- return false unless other.is_a?(Host)
71
-
72
- hostname == other.hostname &&
73
- user == other.user &&
74
- port == other.port
75
- end
76
-
77
- alias eql? ==
78
-
79
- # Generate hash code for host
80
- # @return [Integer] Hash code
81
- def hash
82
- [hostname, user, port].hash
83
- end
84
- end
85
- end
@@ -1,243 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Inventory class for managing host inventory and configuration
5
- class Inventory
6
- attr_reader :hosts, :groups, :vars
7
-
8
- def initialize(inventory_file = nil)
9
- @hosts = {}
10
- @groups = {}
11
- @vars = {}
12
- load_from_file(inventory_file) if inventory_file && File.exist?(inventory_file)
13
- end
14
-
15
- # Load inventory from YAML file
16
- # @param inventory_file [String] Path to inventory file
17
- # @raise [ConfigurationError] If inventory file is invalid
18
- def load_from_file(inventory_file)
19
- inventory_data = YAML.load_file(inventory_file)
20
- parse_inventory(inventory_data)
21
- rescue Psych::SyntaxError => e
22
- raise ConfigurationError, "Invalid YAML syntax in inventory file: #{e.message}"
23
- rescue StandardError => e
24
- raise ConfigurationError, "Failed to load inventory file: #{e.message}"
25
- end
26
-
27
- # Get all hosts in a group
28
- # @param group_name [String, Symbol] Group name
29
- # @return [Array<Host>] Hosts in the group
30
- def hosts_in_group(group_name)
31
- group_name = group_name.to_s
32
- return [] unless @groups[group_name]
33
-
34
- @groups[group_name][:hosts].map { |hostname| @hosts[hostname] }.compact
35
- end
36
-
37
- # Get all hosts with specific role
38
- # @param role [String, Symbol] Role name
39
- # @return [Array<Host>] Hosts with the role
40
- def hosts_with_role(role)
41
- @hosts.values.select { |host| host.has_role?(role) }
42
- end
43
-
44
- # Get all hosts
45
- # @return [Array<Host>] All hosts
46
- def all_hosts
47
- @hosts.values
48
- end
49
-
50
- # Get host by hostname
51
- # @param hostname [String] Hostname
52
- # @return [Host, nil] Host object or nil
53
- def host(hostname)
54
- @hosts[hostname]
55
- end
56
-
57
- # Get group variable
58
- # @param group_name [String, Symbol] Group name
59
- # @param var_name [String, Symbol] Variable name
60
- # @return [Object] Variable value
61
- def group_var(group_name, var_name)
62
- group_name = group_name.to_s
63
- return nil unless @groups[group_name]
64
-
65
- @groups[group_name][:vars][var_name.to_s] || @groups[group_name][:vars][var_name.to_sym]
66
- end
67
-
68
- # Get global variable
69
- # @param var_name [String, Symbol] Variable name
70
- # @return [Object] Variable value
71
- def global_var(var_name)
72
- @vars[var_name.to_s] || @vars[var_name.to_sym]
73
- end
74
-
75
- # Export inventory summary
76
- # @return [Hash] Inventory summary
77
- def summary
78
- {
79
- total_hosts: @hosts.size,
80
- total_groups: @groups.size,
81
- hosts: @hosts.keys,
82
- groups: @groups.keys
83
- }
84
- end
85
-
86
- private
87
-
88
- def parse_inventory(inventory_data)
89
- return unless inventory_data.is_a?(Hash)
90
-
91
- @vars = extract_vars(inventory_data)
92
- parse_groups(inventory_data)
93
- parse_hosts(inventory_data)
94
- apply_group_variables
95
- end
96
-
97
- def extract_vars(data)
98
- data['vars'] || data[:vars] || {}
99
- end
100
-
101
- def parse_groups(inventory_data)
102
- groups_data = inventory_data['groups'] || inventory_data[:groups] || {}
103
-
104
- groups_data.each do |group_name, group_config|
105
- process_group(group_name.to_s, group_config || {})
106
- end
107
-
108
- resolve_group_children
109
- end
110
-
111
- def process_group(group_name, group_config)
112
- @groups[group_name] = {
113
- hosts: extract_group_hosts(group_config),
114
- vars: extract_group_vars(group_config),
115
- children: extract_group_children(group_config)
116
- }
117
- end
118
-
119
- def extract_group_hosts(config)
120
- Array(config['hosts'] || config[:hosts] || [])
121
- end
122
-
123
- def extract_group_vars(config)
124
- config['vars'] || config[:vars] || {}
125
- end
126
-
127
- def extract_group_children(config)
128
- Array(config['children'] || config[:children] || [])
129
- end
130
-
131
- def parse_hosts(inventory_data)
132
- hosts_data = inventory_data['hosts'] || inventory_data[:hosts] || {}
133
-
134
- hosts_data.each do |hostname, host_config|
135
- process_host(hostname, host_config || {})
136
- end
137
- end
138
-
139
- def process_host(hostname, host_config)
140
- host_groups = find_host_groups(hostname)
141
- host_roles = Array(host_config['roles'] || host_config[:roles] || host_groups)
142
-
143
- @hosts[hostname] = create_host(hostname, host_config, host_roles)
144
- end
145
-
146
- def create_host(hostname, config, roles)
147
- Host.new(
148
- hostname,
149
- user: config['user'] || config[:user],
150
- port: config['port'] || config[:port],
151
- ssh_options: parse_ssh_options(config),
152
- roles: roles,
153
- vars: config['vars'] || config[:vars] || {}
154
- )
155
- end
156
-
157
- def parse_ssh_options(host_config)
158
- ssh_config = host_config['ssh'] || host_config[:ssh] || {}
159
- options = {}
160
-
161
- process_ssh_key_options(ssh_config, options)
162
- process_ssh_auth_options(ssh_config, options)
163
- process_ssh_verification_options(ssh_config, options)
164
- process_ssh_timeout_option(ssh_config, options)
165
-
166
- options
167
- end
168
-
169
- def process_ssh_key_options(ssh_config, options)
170
- if ssh_config['key_file'] || ssh_config[:key_file]
171
- key_file = ssh_config['key_file'] || ssh_config[:key_file]
172
- options[:keys] = [File.expand_path(key_file)]
173
- end
174
-
175
- return unless ssh_config['key_data'] || ssh_config[:key_data]
176
-
177
- options[:key_data] = Array(ssh_config['key_data'] || ssh_config[:key_data])
178
- end
179
-
180
- def process_ssh_auth_options(ssh_config, options)
181
- options[:password] = ssh_config['password'] || ssh_config[:password] if ssh_config['password'] || ssh_config[:password]
182
- return unless ssh_config['passphrase'] || ssh_config[:passphrase]
183
-
184
- options[:passphrase] = ssh_config['passphrase'] || ssh_config[:passphrase]
185
- end
186
-
187
- def process_ssh_verification_options(ssh_config, options)
188
- return unless ssh_config.key?('verify_host_key') || ssh_config.key?(:verify_host_key)
189
-
190
- verify_host_key = ssh_config['verify_host_key'] || ssh_config[:verify_host_key]
191
- options[:verify_host_key] = verify_host_key ? :always : :never
192
- end
193
-
194
- def process_ssh_timeout_option(ssh_config, options)
195
- options[:timeout] = ssh_config['timeout'] || ssh_config[:timeout] if ssh_config['timeout'] || ssh_config[:timeout]
196
- end
197
-
198
- def find_host_groups(hostname)
199
- @groups.each_with_object([]) do |(group_name, group_config), groups|
200
- groups << group_name if group_config[:hosts].include?(hostname)
201
- end
202
- end
203
-
204
- def resolve_group_children
205
- @groups.each do |group_name, group_config|
206
- process_group_children(group_name, group_config)
207
- end
208
- end
209
-
210
- def process_group_children(group_name, group_config)
211
- group_config[:children].each do |child_group|
212
- next unless @groups[child_group]
213
-
214
- @groups[group_name][:hosts].concat(@groups[child_group][:hosts])
215
- end
216
-
217
- @groups[group_name][:hosts].uniq!
218
- end
219
-
220
- def apply_group_variables
221
- @hosts.each do |hostname, host|
222
- host_groups = find_host_groups(hostname)
223
- apply_group_vars_to_host(host, host_groups)
224
- apply_global_vars_to_host(host)
225
- end
226
- end
227
-
228
- def apply_group_vars_to_host(host, host_groups)
229
- host_groups.each do |group_name|
230
- group_vars = @groups[group_name][:vars] || {}
231
- group_vars.each do |key, value|
232
- host.set_var(key, value) unless host.var(key)
233
- end
234
- end
235
- end
236
-
237
- def apply_global_vars_to_host(host)
238
- @vars.each do |key, value|
239
- host.set_var(key, value) unless host.var(key)
240
- end
241
- end
242
- end
243
- end
@@ -1,100 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Custom logger class for Kdeploy with colorized output
5
- class KdeployLogger
6
- class << self
7
- attr_accessor :instance
8
-
9
- # Set up logger instance with specified level and output file
10
- # @param level [Symbol] Log level (:debug, :info, :warn, :error, :fatal)
11
- # @param file [String, IO] Output file or IO stream
12
- # @return [KdeployLogger] Logger instance
13
- def setup(level: :info, file: nil)
14
- @instance = new(level: level, file: file)
15
- end
16
-
17
- def method_missing(method_name, ...)
18
- return super unless respond_to_missing?(method_name, false)
19
-
20
- @instance ||= new
21
- @instance.send(method_name, ...)
22
- end
23
-
24
- def respond_to_missing?(method_name, include_private = false)
25
- return true if %i[debug info warn error fatal].include?(method_name)
26
-
27
- super
28
- end
29
- end
30
-
31
- # Initialize logger with specified level and output file
32
- # @param level [Symbol] Log level (:debug, :info, :warn, :error, :fatal)
33
- # @param file [String, IO] Output file or IO stream
34
- def initialize(level: :info, file: nil)
35
- @logger = Logger.new(file || $stdout)
36
- @logger.level = logger_level(level)
37
- @logger.formatter = method(:format_message)
38
- end
39
-
40
- # Log debug message
41
- # @param message [String] Message to log
42
- def debug(message)
43
- @logger.debug(message)
44
- end
45
-
46
- # Log info message
47
- # @param message [String] Message to log
48
- def info(message)
49
- @logger.info(message)
50
- end
51
-
52
- # Log warning message
53
- # @param message [String] Message to log
54
- def warn(message)
55
- @logger.warn(message)
56
- end
57
-
58
- # Log error message
59
- # @param message [String] Message to log
60
- def error(message)
61
- @logger.error(message)
62
- end
63
-
64
- # Log fatal message
65
- # @param message [String] Message to log
66
- def fatal(message)
67
- @logger.fatal(message)
68
- end
69
-
70
- private
71
-
72
- def format_message(severity, datetime, _progname, msg)
73
- timestamp = datetime.strftime('%Y-%m-%d %H:%M:%S')
74
- colored_msg = colorize_message(severity, msg)
75
- "[#{timestamp}] #{severity}: #{colored_msg}\n"
76
- end
77
-
78
- def logger_level(level)
79
- case level.to_sym
80
- when :debug then Logger::DEBUG
81
- when :info then Logger::INFO
82
- when :warn then Logger::WARN
83
- when :error then Logger::ERROR
84
- when :fatal then Logger::FATAL
85
- else Logger::INFO
86
- end
87
- end
88
-
89
- def colorize_message(severity, message)
90
- case severity
91
- when 'DEBUG' then message.colorize(:light_black)
92
- when 'INFO' then message.colorize(:green)
93
- when 'WARN' then message.colorize(:yellow)
94
- when 'ERROR' then message.colorize(:red)
95
- when 'FATAL' then message.colorize(:light_red)
96
- else message
97
- end
98
- end
99
- end
100
- end