cem_acpt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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