cem_acpt 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,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Currently unused in this implementation.
4
+ module CemAcpt::Bootstrap::OperatingSystem
5
+ # This module holds methods used by Bootstrap for RHEL-family operating systems
6
+ module RhelFamily
7
+ def puppet_agent_repo
8
+ "https://yum.puppet.com/#{@collection}-release-el-#{@major_version}.noarch.rpm"
9
+ end
10
+
11
+ def package_manager
12
+ return 'dnf' if @major_version.to_i >= 8
13
+
14
+ 'yum'
15
+ end
16
+
17
+ def rvm_deps
18
+ [
19
+ 'kernel-devel',
20
+ 'gcc',
21
+ 'gcc-c++',
22
+ 'make',
23
+ 'augeas',
24
+ 'augeas-devel',
25
+ 'patch',
26
+ 'readline',
27
+ 'readline-devel',
28
+ 'zlib',
29
+ 'zlib-devel',
30
+ 'libffi-devel',
31
+ 'openssl-devel',
32
+ 'bzip2',
33
+ 'autoconf',
34
+ 'automake',
35
+ 'libtool',
36
+ 'bison',
37
+ 'sqlite-devel',
38
+ ]
39
+ end
40
+
41
+ def repo_installs(repos, sudo: true)
42
+ return if repos.nil? || repos.empty?
43
+
44
+ cmd = [
45
+ repo_install_cmd(repos, sudo: sudo),
46
+ repo_install_verify_cmd(repos, sudo: sudo),
47
+ ].join(' && ')
48
+ "#{cmd} | sudo tee -ai #{@log_file}"
49
+ end
50
+
51
+ def package_installs(packages, sudo: true)
52
+ return if packages.nil? || packages.empty?
53
+
54
+ packages += rvm_deps
55
+ cmd = [
56
+ package_install_cmd(packages, sudo: sudo),
57
+ package_install_verify_cmd(packages, sudo: sudo),
58
+ ].join(' && ')
59
+ "#{cmd} | sudo tee -ai #{@log_file}"
60
+ end
61
+
62
+ def service_starts(services, sudo: true)
63
+ return if services.nil? || services.empty?
64
+
65
+ cmd = [
66
+ #service_start_verify_existance_cmd(services, sudo: sudo),
67
+ service_start_cmd(services, sudo: sudo),
68
+ ].join(' && ')
69
+ "#{cmd} | sudo tee -ai #{@log_file}"
70
+ end
71
+
72
+ def repo_install_cmd(repos, sudo: true)
73
+ cmd = []
74
+ cmd << 'sudo' if sudo
75
+ cmd << 'rpm -Uv'
76
+ repos.each { |r| cmd << r }
77
+ cmd.join(' ')
78
+ end
79
+
80
+ def repo_install_verify_cmd(repos, sudo: true)
81
+ cmd = []
82
+ cmd << 'sudo' if sudo
83
+ cmd << 'rpm -q'
84
+ repos.each { |r| cmd << r }
85
+ cmd.join(' ')
86
+ end
87
+
88
+ def package_install_cmd(packages, sudo: true)
89
+ cmd = []
90
+ cmd << 'sudo' if sudo
91
+ cmd << "#{package_manager} install #{package_install_cmd_opts}"
92
+ packages.each { |p| cmd << p }
93
+ cmd << package_install_cmd_output_format
94
+ cmd.join(' ')
95
+ end
96
+
97
+ def package_install_verify_cmd(packages, sudo: true)
98
+ cmd = []
99
+ cmd << 'sudo' if sudo
100
+ cmd << "#{package_manager} list installed"
101
+ packages.each { |p| cmd << p }
102
+ cmd.join(' ')
103
+ end
104
+
105
+ def package_install_cmd_opts
106
+ '-y'
107
+ end
108
+
109
+ def package_install_cmd_output_format
110
+ '| tr "\n" "#" | sed -e \'s/# / /g\' | tr "#" "\n"'
111
+ end
112
+
113
+ def service_start_cmd(services, sudo: true)
114
+ cmd = []
115
+ cmd << 'sudo' if sudo
116
+ cmd << 'systemctl start'
117
+ services.each { |s| cmd << s }
118
+ cmd.join(' ')
119
+ end
120
+
121
+ def service_start_verify_existance_cmd(services, sudo: true)
122
+ cmd = []
123
+ cmd << 'sudo' if sudo
124
+ cmd << 'systemctl list-units --type=service'
125
+ services.each { |s| cmd << s }
126
+ cmd.join(' ')
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Currently unused in this implementation.
4
+ module CemAcpt::Bootstrap::OperatingSystem
5
+ class Error < StandardError; end
6
+
7
+ # Currently unused in this implementation.
8
+ def use_os(os)
9
+ case os
10
+ when %r{^(centos|rhel)$}
11
+ require_relative 'operating_system/rhel_family'
12
+ self.class.include CemAcpt::Bootstrap::OperatingSystem::RhelFamily
13
+ else
14
+ raise Error, "Operating system #{os} is not supported"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bootstrap provides a method for bootstrapping test nodes.
4
+ # Currently unused in this implementation.
5
+ module CemAcpt::Bootstrap
6
+ require_relative 'bootstrap/bootstrapper'
7
+
8
+ def self.run(instance_name, instance_image, cmd_provider, collection: 'puppet7', repos: [], packages: [], services: [], commands: [], cmd_provider_args: {})
9
+ bootstrapper = CemAcpt::Bootstrap::Bootstrapper.new(instance_name, instance_image, cmd_provider, collection: 'puppet7', repos: [], packages: [], services: [], commands: [], cmd_provider_args: {})
10
+ bootstrapper.run
11
+ end
12
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent-ruby'
4
+ module CemAcpt
5
+ # Context provides the context in which the RunHandler creates and starts Runners.
6
+ module Context
7
+ require_relative 'platform'
8
+ require_relative 'shared_objects'
9
+ require_relative 'test_data'
10
+ require_relative 'logging'
11
+
12
+ class << self
13
+ include CemAcpt::Logging
14
+ end
15
+
16
+ # Creates a context for the RunHandler to create and start Runners.
17
+ # Provides the following objects for the Runners: a config object,
18
+ # the test data hash, the node inventory, and the local port allocator.
19
+ # Additionally, it creates the platform-specific node objects for each
20
+ # test suite in the test data. It then calls the provided block with
21
+ # the context objects nodes, config, test_data, and node_inventory.
22
+ # @param config_opts [Hash] the config options
23
+ # @param config_file [String] the config file
24
+ def self.with(config_opts: nil, config_file: nil, &block)
25
+ logger.debug('In context with:')
26
+ logger.debug("config_opts: #{config_opts}")
27
+ logger.debug("config_file: #{config_file}")
28
+
29
+ config = CemAcpt::Config.new.load(opts: config_opts, config_file: config_file)
30
+ logger.debug("Loaded config: #{config.to_h}")
31
+
32
+ test_data = CemAcpt::TestData.acceptance_test_data(config)
33
+ logger.debug("Loaded test data: #{test_data}")
34
+
35
+ node_inventory = CemAcpt::NodeInventory.new
36
+ logger.debug("Initialized new node inventory: #{node_inventory}")
37
+
38
+ local_port_allocator = CemAcpt::LocalPortAllocator.new
39
+ logger.debug("Initialized new local port allocator: #{local_port_allocator}")
40
+
41
+ if config.has?('platform')
42
+ logger.debug("Using platform: #{config.get('platform')}")
43
+ nodes = CemAcpt::Platform.use(config.get('platform'), config, test_data, local_port_allocator)
44
+ block.call(nodes, config, test_data, node_inventory)
45
+ elsif config.has?('platforms')
46
+ config.get('platforms').each do |platform|
47
+ logger.debug("Using platform: #{platform}")
48
+ nodes = CemAcpt::Platform.use(platform, config, test_data, local_port_allocator)
49
+ block.call(nodes, config, test_data, node_inventory)
50
+ end
51
+ else
52
+ raise CemAcpt::Error, 'No platform(s) specified'
53
+ end
54
+ rescue StandardError => e
55
+ logger.fatal("Fatal error: #{e.message}")
56
+ logger.debug(e.backtrace.join('; '))
57
+ exit(1)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module holds extensions to Ruby and the Ruby stdlib
4
+ # Extensions related to deep_freeze were pulled from: https://gist.github.com/steakknife/1a37057b3b8539f4aca3
5
+ module CemAcpt::CoreExtensions
6
+ # DeepFreeze recursively freezes all keys and values in a hash
7
+ # Currently unused, but was used at one point and may be useful again
8
+ module DeepFreeze
9
+ # Holds deep_freeze extensions to Kernel
10
+ module Kernel
11
+ alias deep_freeze freeze
12
+ alias deep_frozen? frozen?
13
+ end
14
+
15
+ # Holds deep_freeze extensions to Enumerable
16
+ module Enumerable
17
+ def deep_freeze
18
+ unless @deep_frozen
19
+ each(&:deep_freeze)
20
+ @deep_frozen = true
21
+ end
22
+ freeze
23
+ end
24
+
25
+ def deep_frozen?
26
+ !!@deep_frozen
27
+ end
28
+ end
29
+
30
+ # Holds deep_freeze extensions to Hash
31
+ module Hash
32
+ def deep_freeze
33
+ transform_values! do |value|
34
+ value.respond_to?(:deep_freeze) ? value.deep_freeze : value.freeze
35
+ end
36
+ freeze
37
+ @deep_frozen = true
38
+ end
39
+
40
+ def deep_frozen?
41
+ !!@deep_frozen
42
+ end
43
+ end
44
+
45
+ # Holds deep_freeze extensions to OpenStruct
46
+ module OpenStruct
47
+ def deep_freeze
48
+ return if deep_frozen?
49
+
50
+ @table.reduce({}) do |h, (key, value)|
51
+ fkey = key.respond_to?(:deep_freeze) ? key.deep_freeze : key
52
+ fval = value.respond_to?(:deep_freeze) ? value.deep_freeze : value
53
+ h.merge(fkey => fval)
54
+ end.freeze
55
+ @deep_frozen = true
56
+ end
57
+
58
+ def deep_frozen?
59
+ !!@deep_frozen
60
+ end
61
+ end
62
+ end
63
+
64
+ # Refines the Hash class with some convenience methods.
65
+ # Must call `using CemAcpt::CoreExtensions::HashExtensions`
66
+ # before these methods will be available.
67
+ module ExtendedHash
68
+ refine Hash do
69
+ # Formats a hash by converting all keys to symbols.
70
+ # If any value is a hash, it will be recursively
71
+ # extend and formatted.
72
+ def format!
73
+ transform_keys!(&:to_sym)
74
+ transform_values! do |value|
75
+ if value.is_a?(Hash)
76
+ value.format!
77
+ else
78
+ value
79
+ end
80
+ end
81
+ end
82
+
83
+ def has?(path)
84
+ !!dot_dig(path)
85
+ end
86
+
87
+ # Digs into a Hash using a dot-separated path.
88
+ # If the path is not found, returns nil.
89
+ # Example:
90
+ # hash = {a: {b: {c: 1}}}
91
+ # hash.dot_dig('a.b.c') # => 1
92
+ def dot_dig(path)
93
+ result = dig(*path.split('.').map(&:to_sym))
94
+ return result unless result.nil?
95
+
96
+ dig(*path.split('.'))
97
+ end
98
+
99
+ # Stores a value in a nested Hash using a dot-separated path
100
+ # to dig through keys.
101
+ # Example:
102
+ # hash = {a: {b: {c: 1}}}
103
+ # hash.dot_store('a.b.c', 2)
104
+ # hash #=> {a: {b: {c: 2}}}
105
+ def dot_store(path, value)
106
+ *key, last = path.split('.').map(&:to_sym)
107
+ key.inject(self, :fetch)[last] = value
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ require_relative 'core_extensions'
5
+ require_relative 'logging'
6
+
7
+ # Dynamically builds an image name based on parameters specified in the
8
+ # config. The config is expected to have a key 'image_name_builder' with
9
+ # the following options:
10
+ # - 'parts' - (Required) An array of strings to be joined together to form the image name.
11
+ # If the strings begin with a '$', they will be replaced with the corresponding
12
+ # value from current test data. To specify nested keys, use '.' to separate
13
+ # the keys. Example: '$name.pattern.framework' will be replaced with the
14
+ # value of the name pattern key framework in the current test data.
15
+ # - 'join_with' - (Optional) The string to join the parts with. Defaults to ''.
16
+ # - 'character_substitutions' - (Optional) An array of of 2-item arrays. The first item
17
+ # is the character to replace, the second is the replacement.
18
+ # Example: [[' ', '-'], ['_', '-']] will replace all spaces
19
+ # and underscores with dashes.
20
+ # - 'validation_pattern' - (Optional) A regex pattern to validate the image name against.
21
+ class ImageNameBuilder
22
+ include CemAcpt::Logging
23
+ using CemAcpt::CoreExtensions::ExtendedHash
24
+
25
+ # Initializes the ImageNameBuilder.
26
+ # @param config [CemAcpt::Config] The config to use.
27
+ def initialize(config)
28
+ unless config.has?('image_name_builder')
29
+ raise ArgumentError, 'Configuration does not have an image_name_builder key'
30
+ end
31
+
32
+ @config = config.get('image_name_builder')
33
+ end
34
+
35
+ # Builds an image name based on the config. It does so in three steps:
36
+ # 1. Resolve variables in the parts array.
37
+ # 2. Join the parts together with the join_with string if specified
38
+ # and validate the image name against the validation_pattern if
39
+ # specified.
40
+ # 3. Perform any specified character substitutions on the image name.
41
+ # @param test_data [Hash] The test data to use to build the image name.
42
+ # @return [String] The image name.
43
+ def build(test_data)
44
+ logger.debug 'Building image name...'
45
+ logger.debug "Using config: #{@config.to_h}"
46
+ logger.debug "Test data: #{test_data}"
47
+ parts = resolve_parts(test_data)
48
+ logger.debug "Resolved parts: #{parts}"
49
+ image_name = create_image_name(parts)
50
+ logger.debug "Created image name: #{image_name}"
51
+ final_image_name = character_substitutions(image_name)
52
+ logger.debug "Final image name: #{final_image_name}"
53
+ final_image_name
54
+ end
55
+
56
+ private
57
+
58
+ # Resolves variables in the parts array by replacing them with the
59
+ # corresponding value from the test data.
60
+ # @param test_data [Hash] The test data to use to build the image name.
61
+ # @return [Array] The parts array with variables resolved.
62
+ def resolve_parts(test_data)
63
+ @config[:parts].each_with_object([]) do |part, ary|
64
+ logger.debug "Resolving part: #{part}"
65
+ if part.start_with?('$')
66
+ var_path = part[1..-1]
67
+ logger.debug "Resolving variable path: #{var_path}"
68
+ ary << test_data.dot_dig(var_path)
69
+ else
70
+ ary << part
71
+ end
72
+ end
73
+ end
74
+
75
+ # Creates an image name based on the parts array.
76
+ # @param parts [Array] The parts array to use to build the image name.
77
+ # @return [String] The image name.
78
+ def create_image_name(parts)
79
+ image_name = @config[:join_with] ? parts.join(@config[:join_with]) : parts.join
80
+ logger.debug("Final image name: #{image_name}")
81
+
82
+ if @config[:validation_pattern]
83
+ logger.debug "Validating image name: #{image_name}..."
84
+ return image_name if image_name.match?(@config[:validation_pattern])
85
+
86
+ raise "Image name #{image_name} does not match validation pattern #{@config[:validation_pattern]}"
87
+ end
88
+ image_name
89
+ end
90
+
91
+ # Performs character substitutions on the image name.
92
+ # @param name [String] The image name to perform substitutions on.
93
+ # @return [String] The image name with substitutions performed.
94
+ def character_substitutions(name)
95
+ return name unless @config[:character_substitutions]
96
+
97
+ subbed_name = name
98
+ @config[:character_substitutions].each do |char_sub|
99
+ subbed_name.gsub!(char_sub[0], char_sub[1])
100
+ end
101
+ subbed_name
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module CemAcpt
6
+ # Logging for CemAcpt
7
+ module Logging
8
+ class << self
9
+ attr_writer :logger
10
+
11
+ # Exposes a logger instance. Will either use the currently set logger or
12
+ # create a new one.
13
+ # @return [Logger]
14
+ def logger
15
+ @logger ||= Logger.new(
16
+ current_log_config[:logdev],
17
+ current_log_config[:shift_age],
18
+ current_log_config[:shift_size],
19
+ **current_log_config.reject { |k, _| %i[logdev shift_age shift_size].include?(k) }
20
+ )
21
+ end
22
+
23
+ # Shortcut method for logger.level
24
+ # @return [Logger::Severity]
25
+ def current_log_level
26
+ logger.level
27
+ end
28
+
29
+ # Shortcut method to set logger.level
30
+ # @param level [String] the log level to set
31
+ def new_log_level(level)
32
+ case level.downcase
33
+ when 'debug'
34
+ @logger.level = Logger::DEBUG
35
+ when 'info'
36
+ @logger.level = Logger::INFO
37
+ when 'warn'
38
+ @logger.level = Logger::WARN
39
+ when 'error'
40
+ @logger.level = Logger::ERROR
41
+ when 'fatal'
42
+ @logger.level = Logger::FATAL
43
+ when 'unknown'
44
+ @logger.level = Logger::UNKNOWN
45
+ else
46
+ raise "Cannot set log level #{level}: invalid level"
47
+ end
48
+ end
49
+
50
+ # Shows the current log format style if set, or the default if not.
51
+ # @return [Symbol] the current log format style
52
+ def current_log_format
53
+ @current_log_format ||= :text
54
+ end
55
+
56
+ # Sets the current log format style and returns a proc to be passed to
57
+ # Logger#formatter=
58
+ # @param f [Symbol] the log format style to set
59
+ # @return [Proc] the proc to be passed to Logger#formatter=
60
+ def new_log_formatter(f)
61
+ case f.downcase.to_sym
62
+ when :json
63
+ require 'json'
64
+ @current_log_format = :json
65
+ proc do |severity, datetime, progname, msg|
66
+ {
67
+ timestamp: datetime.iso8601(3),
68
+ progname: progname,
69
+ severity: severity,
70
+ message: msg,
71
+ }.to_json + "\n"
72
+ end
73
+ when :text
74
+ @current_log_format = :text
75
+ proc do |severity, _datetime, _progname, msg|
76
+ "#{severity} - #{msg}\n"
77
+ end
78
+ else
79
+ @current_log_format = :file
80
+ proc do |severity, datetime, _progname, msg|
81
+ "[#{datetime.iso8601(3)}] #{severity}: #{msg}\n"
82
+ end
83
+ end
84
+ end
85
+
86
+ # Returns the current log config if set, or the default if not.
87
+ # @return [Hash] the current log config
88
+ def current_log_config
89
+ return @log_config if @log_config
90
+
91
+ formatter = new_log_formatter(current_log_format)
92
+ @log_config = {
93
+ logdev: $stdout,
94
+ shift_age: 'o',
95
+ shift_size: 1_048_576,
96
+ level: Logger::INFO,
97
+ progname: 'CemAcpt',
98
+ datetime_format: '%Y%m%dT%H%M%S%z',
99
+ formatter: formatter,
100
+ }
101
+ @log_config
102
+ end
103
+
104
+ # Creates a new log config hash and a new Logger instance using that config and sets
105
+ # the new Logger instance as the current logger. NO DEFAULT VALUES ARE SET.
106
+ # @param logdev [String, IO] the log device to use. If 'stdout' or 'stderr' are passed,
107
+ # the respective IO object will be used. Otherwise, Strings will be treated as file paths.
108
+ # If an IO object is passed, it will be used directly.
109
+ # If no logdev is passed, or an invalid logdev is passed, the default is $stdout.
110
+ # @param shift_age [String] the log rotation age
111
+ # @param shift_size [Integer] the log rotation size
112
+ # @param level [Logger::Severity] the log level
113
+ # @param formatter [Proc] the log formatter
114
+ # @param datetime_format [String] the datetime format
115
+ def new_log_config(logdev: nil, shift_age: nil, shift_size: nil, level: nil, formatter: nil, datetime_format: nil)
116
+ @log_config[:shift_age] = shift_age if shift_age
117
+ @log_config[:shift_size] = shift_size if shift_size
118
+ @log_config[:level] = level if level
119
+ @log_config[:formatter] = formatter if formatter
120
+ @log_config[:datetime_format] = datetime_format if datetime_format
121
+ case logdev
122
+ when 'stdout'
123
+ @log_config[:logdev] = $stdout
124
+ when 'stderr'
125
+ @log_config[:logdev] = $stderr
126
+ when String
127
+ @log_config[:logdev] = target
128
+ when IO
129
+ @log_config[:logdev] = logdev
130
+ else
131
+ @log_config[:logdev] = $stdout
132
+ logger.warn("Unknown log target: #{logdev.inspect}, using STDOUT")
133
+ end
134
+ @logger = Logger.new(
135
+ @log_config[:logdev],
136
+ @log_config[:shift_age],
137
+ @log_config[:shift_size],
138
+ **@log_config.reject { |k, _| %i[logdev shift_age shift_size].include?(k) },
139
+ )
140
+ end
141
+ end
142
+
143
+ # Provides class method wrappers for logging methods
144
+ def self.included(base)
145
+ class << base
146
+ def logger
147
+ CemAcpt::Logging.logger
148
+ end
149
+
150
+ def current_log_level
151
+ CemAcpt::Logging.current_log_level
152
+ end
153
+
154
+ def new_log_level(level)
155
+ CemAcpt::Logging.new_log_level(level)
156
+ end
157
+
158
+ def current_log_config
159
+ CemAcpt::Logging.current_log_config
160
+ end
161
+
162
+ def new_log_config(logdev: nil, shift_age: nil, shift_size: nil, level: nil, formatter: nil, datetime_format: nil)
163
+ CemAcpt::Logging.new_log_config(logdev: logdev, shift_age: shift_age, shift_size: shift_size, level: level, formatter: formatter, datetime_format: datetime_format)
164
+ end
165
+ end
166
+ end
167
+
168
+ # Exposes the logger instance
169
+ def logger
170
+ CemAcpt::Logging.logger
171
+ end
172
+
173
+ # Exposes the current log level
174
+ def current_log_level
175
+ CemAcpt::Logging.current_log_level
176
+ end
177
+
178
+ # Exposes setting the log level
179
+ def new_log_level(level)
180
+ CemAcpt::Logging.new_log_level(level)
181
+ end
182
+
183
+ # Exposes the current log config
184
+ def current_log_config
185
+ CemAcpt::Logging.current_log_config
186
+ end
187
+
188
+ # Exposes setting a new log config
189
+ def new_log_config(logdev: nil, shift_age: nil, shift_size: nil, level: nil, formatter: nil, datetime_format: nil)
190
+ CemAcpt::Logging.new_log_config(logdev: logdev, shift_age: shift_age, shift_size: shift_size, level: level, formatter: formatter, datetime_format: datetime_format)
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt::Platform
4
+ require_relative File.join(__dir__, '..', '..', 'logging.rb')
5
+
6
+ # Base class for command providers. Provides an API for subclasses to implement.
7
+ class CmdBase
8
+ include CemAcpt::Logging
9
+
10
+ def initialize(*args, **kwargs); end
11
+
12
+ def local_exec(*_args, **_kwargs)
13
+ raise NotImplementedError, '#local_exec must be implemented by a subclass'
14
+ end
15
+
16
+ def ssh(_instance_name, _command, _ignore_command_errors: false, _opts: {})
17
+ raise NotImplementedError, '#ssh must be implemented by a subclass'
18
+ end
19
+
20
+ def scp_upload(_instance_name, _local, _remote, _scp_opts: {}, _opts: {})
21
+ raise NotImplementedError, '#scp_upload must be implemented by a subclass'
22
+ end
23
+
24
+ def scp_download(_instance_name, _local, _remote, _scp_opts: {}, _opts: {})
25
+ raise NotImplementedError, '#scp_download must be implemented by a subclass'
26
+ end
27
+
28
+ def ssh_ready?(_instance_name, _timeout = 300, _opts: {})
29
+ raise NotImplementedError, '#ssh_ready? must be implemented by a subclass'
30
+ end
31
+
32
+ def apply_manifest(_instance_name, _manifest, _opts: {})
33
+ raise NotImplementedError, '#create_manifest_on_node must be implemented by a subclass'
34
+ end
35
+
36
+ def run_shell(_instance_name, _command, _opts: {})
37
+ raise NotImplementedError, '#run_shell must be implemented by a subclass'
38
+ end
39
+
40
+ def trim_output(output)
41
+ if output.include?("\n")
42
+ output.split("\n").map(&:strip).reject(&:empty?).join("\n")
43
+ else
44
+ output.strip
45
+ end
46
+ end
47
+
48
+ def with_timed_retry(timeout = 300)
49
+ return unless block_given?
50
+
51
+ last_error = nil
52
+ start_time = Time.now
53
+ while Time.now - start_time < timeout
54
+ begin
55
+ output = yield
56
+ return output
57
+ rescue StandardError => e
58
+ last_error = e
59
+ sleep(5)
60
+ end
61
+ end
62
+ raise last_error
63
+ end
64
+ end
65
+ end