assemblr 0.1.0 → 0.2.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 +4 -4
- data/.gitignore +2 -0
- data/README.md +19 -31
- data/lib/assemblr.rb +72 -241
- data/lib/assemblr/network.rb +57 -0
- data/lib/assemblr/remote.rb +195 -0
- data/lib/assemblr/shell.rb +52 -0
- data/lib/assemblr/version.rb +1 -1
- metadata +5 -4
- data/lib/assemblr/logging.rb +0 -48
- data/lib/assemblr/utilities.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 864016a190bb0067cd305a9824003a3cca4be4dd5eab9eb92b4f1e88ea388789
|
4
|
+
data.tar.gz: '08f75aa401dd8863a95897c07eacfffa5364b8cbefff9f39711167ff803867c9'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d35cb5c921b0da7ffd6440aafe890e14c113dbee161be2351161c79c56f9ac10d228688c3bcbfe70181068ce159da0a7349032b6697b88d3eb57a71f1e3506f
|
7
|
+
data.tar.gz: 2b59abd44694032c93d6018b5698d9eb227fe5019c4aad8914805f32992bd7dfd60ab0015f4923e207b29f085bc44eafbc44c319057fa5286e8abe0260306ff5
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -22,45 +22,33 @@ Or install it yourself as:
|
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
|
-
|
25
|
+
Assemblr is separated into modules. The core module contains simple logging
|
26
|
+
methods since they're needed by all other modules. It gets automatically
|
27
|
+
imported when requiring any sub-modules.
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
require 'assemblr'
|
31
|
-
|
32
|
-
# Configuration options. The values listed below are the defaults for assemblr.
|
33
|
-
set :default_user, 'root' # set the default user nodes are assigned to
|
34
|
-
set :default_group, 'default' # set the default group nodes are assigned to
|
35
|
-
set :log, true # turn on or off built-in logging.
|
36
|
-
set :quit_on_error, true # quit if anything goes wrong.
|
29
|
+
Here are the currently existing modules (although none of them are close to
|
30
|
+
finished):
|
37
31
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
# group will be restored for any other top-level node definitions.
|
42
|
-
node '10.0.0.173'
|
43
|
-
node '10.0.0.332'
|
44
|
-
node '10.0.1.22'
|
45
|
-
end
|
32
|
+
- `assemblr/shell`
|
33
|
+
- `assemblr/network`
|
34
|
+
- `assemblr/remote`
|
46
35
|
|
47
|
-
|
48
|
-
|
36
|
+
There are currently two styles available to call the methods defined in these
|
37
|
+
modules:
|
49
38
|
|
50
|
-
|
51
|
-
|
52
|
-
upload 'lib', '/tmp',
|
53
|
-
recursive: true # upload a directory to all nodes in a group
|
54
|
-
|
55
|
-
remote_exec 'echo "Hello, world!"'
|
56
|
-
end
|
39
|
+
```ruby
|
40
|
+
require 'assemblr/shell'
|
57
41
|
|
58
|
-
|
59
|
-
|
42
|
+
# Style 1
|
43
|
+
Shell.exec 'my_command'
|
60
44
|
|
61
|
-
|
45
|
+
# Style 2
|
46
|
+
shell_exec 'my_command'
|
62
47
|
```
|
63
48
|
|
49
|
+
Both styles are automatically imported into the main scope when the file is
|
50
|
+
required.
|
51
|
+
|
64
52
|
## Development
|
65
53
|
|
66
54
|
After checking out the repo, run `bin/setup` to install dependencies. You can
|
data/lib/assemblr.rb
CHANGED
@@ -1,275 +1,106 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require 'assemblr/logging'
|
5
|
-
require 'assemblr/utilities'
|
3
|
+
$VERBOSE = nil
|
6
4
|
|
7
|
-
require '
|
8
|
-
require 'net/ssh'
|
9
|
-
require 'net/scp'
|
5
|
+
require 'tty-logger'
|
10
6
|
|
7
|
+
##
|
8
|
+
# Define the core Assemblr methods.
|
11
9
|
module Assemblr
|
12
|
-
|
10
|
+
@@options = []
|
11
|
+
@@error_hook = -> {}
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
m_nodes = []
|
17
|
-
m_current_group = 'default'
|
18
|
-
m_current_user = 'root'
|
13
|
+
def set_option(key, value)
|
14
|
+
return unless @@options.include?(key.to_sym)
|
19
15
|
|
20
|
-
|
21
|
-
# Set a configuration option.
|
22
|
-
#
|
23
|
-
# Available options are:
|
24
|
-
#
|
25
|
-
# - *default_user* - the default user to assign nodes to
|
26
|
-
# - *default_group* - the default group to assign nodes to
|
27
|
-
# - *log* - turn on or off logging
|
28
|
-
# - *quit_on_error* - quit if any error occur
|
29
|
-
#
|
30
|
-
# @return [void]
|
31
|
-
define_method :set do |key, value|
|
32
|
-
options = %i[log quit_on_error default_user default_group]
|
33
|
-
unless options.include?(key)
|
34
|
-
raise ArgumentError, "#{key} is not a valid option"
|
35
|
-
end
|
36
|
-
|
37
|
-
key = :current_user if key == :default_user
|
38
|
-
key = :current_group if key == :default_group
|
39
|
-
|
40
|
-
info %(config: set "#{key}" to "#{value}")
|
41
|
-
eval "m_#{key} = value", binding, __FILE__, __LINE__
|
16
|
+
Assemblr.class_variable_set("@@#{key}", value)
|
42
17
|
end
|
43
18
|
|
44
|
-
|
45
|
-
|
46
|
-
# Execute a command locally.
|
47
|
-
#
|
48
|
-
# @param cmd [String] the command to run locally
|
49
|
-
# @param inject [Array<String>] strings to inject into stdin
|
50
|
-
#
|
51
|
-
# @return [Array(String, Process::Status)]
|
52
|
-
define_method :local_exec do |cmd, inject: []|
|
53
|
-
log_string = "running `#{cmd}` locally"
|
54
|
-
log_string += " with these inputs: #{inject}" unless inject.empty?
|
55
|
-
info log_string
|
56
|
-
|
57
|
-
result = ''
|
58
|
-
status = nil
|
59
|
-
Open3.popen2e(cmd) do |i, o, wait|
|
60
|
-
inject.each do |line|
|
61
|
-
line = line.strip
|
62
|
-
i.write line + "\n"
|
63
|
-
end
|
64
|
-
i.close
|
65
|
-
result = o.read
|
66
|
-
status = wait.value
|
67
|
-
end
|
68
|
-
|
69
|
-
if status.exitstatus != 0
|
70
|
-
code = status.exitstatus
|
71
|
-
error "local command `#{cmd}` exited with a status of #{code}"
|
72
|
-
else
|
73
|
-
success "local command `#{cmd}` executed successfully"
|
74
|
-
end
|
75
|
-
|
76
|
-
return result, status
|
19
|
+
def get_option(key)
|
20
|
+
Assemblr.class_variable_get("@@#{key}")
|
77
21
|
end
|
78
22
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
#
|
83
|
-
# @param ip [String] the ip of the node to run the command on
|
84
|
-
# @param user [String] the user to log in as
|
85
|
-
# @param cmd [String] the command to execute remotely
|
86
|
-
# @param inject [Array<String>] strings to inject into stdin
|
87
|
-
#
|
88
|
-
# @return [String]
|
89
|
-
# @return [Array<String>]
|
90
|
-
def remote_exec_on(ip, user, cmd, inject: [])
|
91
|
-
info "attempting to execute `#{cmd}` on #{user}@#{ip}"
|
92
|
-
|
93
|
-
result = ''
|
94
|
-
Net::SSH.start(ip, user) do |ssh|
|
95
|
-
ch = ssh.open_channel do |ch|
|
96
|
-
ch.exec(cmd) do |ch, success|
|
97
|
-
error "unable to execute `#{cmd}` on #{user}@#{ip}" unless success
|
98
|
-
|
99
|
-
# Collect stdout data.
|
100
|
-
ch.on_data do |_, data|
|
101
|
-
result += data if data.is_a?(String)
|
102
|
-
end
|
103
|
-
|
104
|
-
# Collect stderr data.
|
105
|
-
ch.on_extended_data do |_, _, data|
|
106
|
-
result += data if data.is_a?(String)
|
107
|
-
end
|
108
|
-
|
109
|
-
inject.each do |line|
|
110
|
-
ch.send_data(line + "\n")
|
111
|
-
end
|
112
|
-
|
113
|
-
ch.on_request 'exit-status' do |_, data|
|
114
|
-
code = data.read_long
|
115
|
-
if code.zero?
|
116
|
-
success "`#{cmd}` executed successfully on #{user}@#{ip}"
|
117
|
-
else
|
118
|
-
error "`#{cmd}` returned status code #{code} on #{user}@#{ip}"
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
ch.wait
|
125
|
-
end
|
126
|
-
result
|
23
|
+
def define_option(name, value)
|
24
|
+
Assemblr.class_variable_set("@@#{name}", value)
|
25
|
+
@@options << name.to_sym
|
127
26
|
end
|
128
27
|
|
129
|
-
|
130
|
-
|
131
|
-
#
|
132
|
-
# @param ip [String] the node to upload to
|
133
|
-
# @param user [String] the user to authenticate as
|
134
|
-
# @param src [String] the file to upload
|
135
|
-
# @param dest [String] the location to upload to
|
136
|
-
# @param recursive [Bool] recursively upload files in a directory
|
137
|
-
# @param timeout [Integer] how many seconds to wait before throwing an error
|
138
|
-
#
|
139
|
-
# @return [void]
|
140
|
-
def upload_to(ip, user, src, dest, recursive: false, timeout: 5)
|
141
|
-
info %(attempting to upload "#{src}" to #{user}@#{ip}:#{dest})
|
142
|
-
Timeout.timeout timeout do
|
143
|
-
Net::SCP.upload!(ip, user, src, dest, recursive: recursive)
|
144
|
-
end
|
145
|
-
rescue StandardError
|
146
|
-
error %(failed to upload "#{src}" to #{user}@#{ip}:#{dest})
|
147
|
-
rescue Timeout::Error
|
148
|
-
error %(failed to upload "#{src}" to #{user}@#{ip}:#{dest} within #{timeout} seconds)
|
149
|
-
else
|
150
|
-
success %(uploaded "#{src}" to #{user}@#{ip}:#{dest})
|
28
|
+
def options
|
29
|
+
Assemblr.class_variable_get('@@options')
|
151
30
|
end
|
152
31
|
|
153
|
-
|
154
|
-
|
155
|
-
# Get all nodes in the current group.
|
156
|
-
#
|
157
|
-
# @return [void]
|
158
|
-
define_method :current_nodes do
|
159
|
-
return nodes if m_current_group == 'all'
|
160
|
-
|
161
|
-
nodes.select { |n| n[:group] == m_current_group }
|
32
|
+
def error_hook
|
33
|
+
@@on_error
|
162
34
|
end
|
163
35
|
|
164
|
-
|
165
|
-
|
166
|
-
#
|
167
|
-
# @param cmd [String] the command to execute
|
168
|
-
# @param inject [Array<String>] strings to inject into stdin
|
169
|
-
#
|
170
|
-
# @return [String] the combined output of the command
|
171
|
-
def remote_exec(cmd, inject: [])
|
172
|
-
results = []
|
173
|
-
current_nodes.each do |n|
|
174
|
-
result = remote_exec_on(n[:ip], n[:user], cmd, inject: inject)
|
175
|
-
results << result
|
176
|
-
end
|
177
|
-
results
|
36
|
+
def on_error(&block)
|
37
|
+
@@on_error = block
|
178
38
|
end
|
179
39
|
|
180
40
|
##
|
181
|
-
#
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
# @param timeout [Integer] how many seconds to wait before throwing an error
|
187
|
-
#
|
188
|
-
# @return [void]
|
189
|
-
def upload(src, dest, recursive: false, timeout: 5)
|
190
|
-
current_nodes.each do |n|
|
191
|
-
upload_to(n[:ip], n[:user], src, dest, recursive: recursive)
|
192
|
-
end
|
41
|
+
# Generates method definitions in all supported styles.
|
42
|
+
def expose_method(name)
|
43
|
+
prefix = to_s.downcase.gsub('::', '_').delete_prefix('assemblr_')
|
44
|
+
eval "#{self}.define_method(:#{prefix}_#{name}, &#{self}.method(:#{name}))"
|
45
|
+
eval "#{self}.define_singleton_method(:#{name}, &#{self}.method(:#{name}))"
|
193
46
|
end
|
47
|
+
end
|
194
48
|
|
195
|
-
|
196
|
-
# @!method group(group, &block)
|
197
|
-
# Temporarily change current_group to refer to the specified group while
|
198
|
-
# within the passed block.
|
199
|
-
#
|
200
|
-
# @param group [Symbol] the group to temporarily switch to
|
201
|
-
#
|
202
|
-
# @return [void]
|
203
|
-
define_method :group do |group, &block|
|
204
|
-
info %(entered group block with group "#{group}")
|
205
|
-
|
206
|
-
# Temporarily switch to the target group.
|
207
|
-
old_current_group = m_current_group
|
208
|
-
m_current_group = group.to_s
|
209
|
-
|
210
|
-
block.call
|
211
|
-
|
212
|
-
m_current_group = old_current_group
|
213
|
-
|
214
|
-
info %(exited group block with group "#{group}")
|
215
|
-
end
|
49
|
+
include Assemblr
|
216
50
|
|
51
|
+
module Assemblr
|
217
52
|
##
|
218
|
-
#
|
219
|
-
|
220
|
-
|
221
|
-
|
53
|
+
# Define some quick-and-easy logging methods.
|
54
|
+
module Log
|
55
|
+
define_option :log, true
|
56
|
+
define_option :log_error_hook, true
|
222
57
|
|
223
|
-
|
224
|
-
|
225
|
-
# Get the current default user.
|
226
|
-
#
|
227
|
-
# @return [String]
|
228
|
-
%w[group user].each do |item|
|
229
|
-
define_method "current_#{item}" do
|
230
|
-
eval "m_current_#{item}"
|
58
|
+
TTYLogger = TTY::Logger.new do |config|
|
59
|
+
config.metadata = %i[time date]
|
231
60
|
end
|
232
|
-
end
|
233
61
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
# @param group [Symbol] the group to assign the node to
|
240
|
-
# @param user [String] the user to associate with the node
|
241
|
-
#
|
242
|
-
# @return [Hash{ip=>String, group=>String, user=>String}] the added node
|
243
|
-
define_method :node do |ip, group: m_current_group, user: m_current_user|
|
244
|
-
group = group.to_s
|
245
|
-
if group == 'all'
|
246
|
-
error = ArgumentError.new('nodes can not be assigned to the "all" group')
|
247
|
-
raise error
|
62
|
+
def self.included(_)
|
63
|
+
expose_method :info
|
64
|
+
expose_method :success
|
65
|
+
expose_method :warn
|
66
|
+
expose_method :error
|
248
67
|
end
|
249
68
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
69
|
+
class << self
|
70
|
+
##
|
71
|
+
# @!method info(message)
|
72
|
+
# Logs an info message.
|
73
|
+
#
|
74
|
+
# @return [void]
|
75
|
+
|
76
|
+
##
|
77
|
+
# @!method success(message)
|
78
|
+
# Logs a success message.
|
79
|
+
#
|
80
|
+
# @return [void]
|
81
|
+
|
82
|
+
##
|
83
|
+
# @!method warn(message)
|
84
|
+
# Logs a warning message.
|
85
|
+
#
|
86
|
+
# @return [void]
|
87
|
+
%i[info success warn].each do |level|
|
88
|
+
define_method level do |*args, &block|
|
89
|
+
TTYLogger.send(level, *args, &block) if get_option(:log)
|
90
|
+
end
|
91
|
+
end
|
265
92
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
93
|
+
##
|
94
|
+
# @!method error(message)
|
95
|
+
# Logs a error message. If configured, this will call the on_error proc
|
96
|
+
#
|
97
|
+
# @return [void]
|
98
|
+
define_method :error do |*args, &block|
|
99
|
+
TTYLogger.send(:error, *args, &block) if get_option(:log)
|
100
|
+
error_hook.call if get_option(:log_error_hook)
|
101
|
+
end
|
270
102
|
end
|
271
|
-
reachable
|
272
103
|
end
|
273
104
|
end
|
274
105
|
|
275
|
-
|
106
|
+
include Assemblr::Log
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$VERBOSE = nil
|
4
|
+
|
5
|
+
require 'assemblr'
|
6
|
+
|
7
|
+
require 'socket'
|
8
|
+
require 'net/ping'
|
9
|
+
|
10
|
+
module Assemblr
|
11
|
+
##
|
12
|
+
# Defines methods for common network functions.
|
13
|
+
module Network
|
14
|
+
def self.included(_)
|
15
|
+
expose_method :local_ip
|
16
|
+
expose_method :ip_reachable?
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
##
|
21
|
+
# Retrieve the first ip address that is not '127.0.0.1' on the local
|
22
|
+
# machine.
|
23
|
+
#
|
24
|
+
# @param pattern [Regexp] a pattern to test the IPs against
|
25
|
+
#
|
26
|
+
# @return [String]
|
27
|
+
def local_ip(pattern = //)
|
28
|
+
Socket.ip_address_list.each do |ip|
|
29
|
+
address = ip.ip_address
|
30
|
+
next if address == '127.0.0.1'
|
31
|
+
return address if address.match?(pattern)
|
32
|
+
end
|
33
|
+
''
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Check if a remote ip can be contacted.
|
38
|
+
#
|
39
|
+
# @return [Boolean]
|
40
|
+
def ip_reachable?(ip)
|
41
|
+
external = Net::Ping::External.new(ip)
|
42
|
+
|
43
|
+
log_info %(attempting to contact host "#{ip}")
|
44
|
+
reachable = external.ping || external.ping6
|
45
|
+
if reachable
|
46
|
+
log_success %(host "#{ip}" is reachable)
|
47
|
+
else
|
48
|
+
log_error %(unable to contact host "#{ip}")
|
49
|
+
end
|
50
|
+
|
51
|
+
reachable
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
include Assemblr::Network
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'assemblr'
|
4
|
+
|
5
|
+
require 'timeout'
|
6
|
+
require 'net/ssh'
|
7
|
+
require 'net/scp'
|
8
|
+
|
9
|
+
module Assemblr
|
10
|
+
##
|
11
|
+
# Defines methods to handle interacting with remote nodes.
|
12
|
+
module Remote
|
13
|
+
@@nodes = []
|
14
|
+
|
15
|
+
define_option :remote_default_group, :default
|
16
|
+
define_option :remote_default_user, 'root'
|
17
|
+
|
18
|
+
def self.included(_)
|
19
|
+
expose_method :group
|
20
|
+
expose_method :upload
|
21
|
+
expose_method :exec
|
22
|
+
expose_method :current_nodes
|
23
|
+
expose_method :node
|
24
|
+
expose_method :nodes
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
##
|
29
|
+
# Defines methods availabile only while in a group block.
|
30
|
+
module Group
|
31
|
+
def self.remote_upload(src, dest, recursive: false, timeout: 5)
|
32
|
+
Remote.current_nodes.each do |n|
|
33
|
+
Remote.upload(n[:ip], n[:user], src, dest,
|
34
|
+
recursive: recursive,
|
35
|
+
timeout: timeout)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.remote_exec(cmd, inject: [])
|
40
|
+
results = []
|
41
|
+
Remote.current_nodes.each do |n|
|
42
|
+
result = Remote.remote_exec(n[:ip], n[:user], cmd, inject: inject)
|
43
|
+
results << result
|
44
|
+
end
|
45
|
+
results
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Execute a command on a remote machine using SSH. It returns the
|
50
|
+
# combined stdout and stderr data.
|
51
|
+
#
|
52
|
+
# @param ip [String] the ip of the node to run the command on
|
53
|
+
# @param user [String] the user to log in as
|
54
|
+
# @param cmd [String] the command to execute remotely
|
55
|
+
# @param inject [Array<String>] strings to inject into stdin
|
56
|
+
#
|
57
|
+
# @return [String]
|
58
|
+
def exec(ip, user, cmd, inject: [])
|
59
|
+
log_info "attempting to execute `#{cmd}` on #{user}@#{ip}"
|
60
|
+
|
61
|
+
result = ''
|
62
|
+
exit_code = 0
|
63
|
+
Net::SSH.start(ip, user) do |ssh|
|
64
|
+
ch = ssh.open_channel do |ch|
|
65
|
+
ch.exec(cmd) do |ch, success|
|
66
|
+
log_error "unable to execute `#{cmd}` on #{user}@#{ip}" unless success
|
67
|
+
|
68
|
+
# Collect stdout data.
|
69
|
+
ch.on_data do |_, data|
|
70
|
+
result += data if data.is_a?(String)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Collect stderr data.
|
74
|
+
ch.on_extended_data do |_, _, data|
|
75
|
+
result += data if data.is_a?(String)
|
76
|
+
end
|
77
|
+
|
78
|
+
inject.each do |line|
|
79
|
+
ch.send_data(line + "\n")
|
80
|
+
end
|
81
|
+
|
82
|
+
ch.on_request 'exit-status' do |_, data|
|
83
|
+
exit_code = data.read_long
|
84
|
+
if exit_code.zero?
|
85
|
+
log_success "`#{cmd}` executed successfully on #{user}@#{ip}"
|
86
|
+
else
|
87
|
+
log_error "`#{cmd}` returned status code #{exit_code} on #{user}@#{ip}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
ch.wait
|
94
|
+
end
|
95
|
+
[result, exit_code]
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Return all nodes within the current default group.
|
100
|
+
#
|
101
|
+
# @return [Array<Hash{ip=>String, group=>String, user=>String}>]
|
102
|
+
def current_nodes
|
103
|
+
return nodes if get_option(:remote_default_group) == 'all'
|
104
|
+
|
105
|
+
@@nodes.select { |n| n[:group] == get_option(:remote_default_group).to_s }
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Temporarily change the default group to refer to the specified group
|
110
|
+
# while within the passed block.
|
111
|
+
#
|
112
|
+
# This method also changes the behavior of `remote_exec` and
|
113
|
+
# `remote_upload` while within the given block. Both functions don't take
|
114
|
+
# an IP # and a user, as they collect the information for all nodes
|
115
|
+
# within the group. In that case, the return value is an array of the
|
116
|
+
# results given by each node.
|
117
|
+
#
|
118
|
+
# @param group [Symbol] the group to temporarily switch to
|
119
|
+
#
|
120
|
+
# @return [void]
|
121
|
+
def group(group, &block)
|
122
|
+
log_info %(entered group block with group "#{group}")
|
123
|
+
|
124
|
+
# Temporarily switch to the target group.
|
125
|
+
old_default_group = get_option(:remote_default_group)
|
126
|
+
set_option(:remote_default_group, group.to_s)
|
127
|
+
|
128
|
+
Group.class_eval(&block)
|
129
|
+
|
130
|
+
set_option(:remote_default_group, old_default_group)
|
131
|
+
|
132
|
+
log_info %(exited group block with group "#{group}")
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Upload a file or directory to a node.
|
137
|
+
#
|
138
|
+
# @param ip [String] the node to upload to
|
139
|
+
# @param user [String] the user to authenticate as
|
140
|
+
# @param src [String] the file to upload
|
141
|
+
# @param dest [String] the location to upload to
|
142
|
+
# @param recursive [Bool] recursively upload files in a directory
|
143
|
+
# @param timeout [Integer] how many seconds to wait before throwing an error
|
144
|
+
#
|
145
|
+
# @return [void]
|
146
|
+
def upload(ip, user, src, dest, recursive: false, timeout: 5)
|
147
|
+
log_info %(attempting to upload "#{src}" to #{user}@#{ip}:#{dest})
|
148
|
+
Timeout.timeout timeout do
|
149
|
+
Net::SCP.upload!(ip, user, src, dest, recursive: recursive)
|
150
|
+
end
|
151
|
+
rescue Timeout::Error
|
152
|
+
log_error %(failed to upload "#{src}" to #{user}@#{ip}:#{dest} within #{timeout} seconds)
|
153
|
+
rescue StandardError
|
154
|
+
log_error %(failed to upload "#{src}" to #{user}@#{ip}:#{dest})
|
155
|
+
else
|
156
|
+
log_success %(uploaded "#{src}" to #{user}@#{ip}:#{dest})
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# Add a node to the list of nodes.
|
161
|
+
#
|
162
|
+
# @param ip [String] the ip address of the node
|
163
|
+
# @param group [Symbol] the group to assign the node to
|
164
|
+
# @param user [String] the user to associate with the node
|
165
|
+
#
|
166
|
+
# @return [Hash{ip=>String, group=>String, user=>String}] the added node
|
167
|
+
def node(ip,
|
168
|
+
group: get_option(:remote_default_group),
|
169
|
+
user: get_option(:remote_default_user))
|
170
|
+
group = group.to_s
|
171
|
+
if group == 'all'
|
172
|
+
message = 'nodes can not be assigned to the "all" group'
|
173
|
+
error = ArgumentError.new(message)
|
174
|
+
raise error
|
175
|
+
end
|
176
|
+
|
177
|
+
l_node = { ip: ip, group: group, user: user }
|
178
|
+
@@nodes << l_node
|
179
|
+
log_info "added node: #{l_node}"
|
180
|
+
|
181
|
+
l_node
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Get all registered nodes.
|
186
|
+
#
|
187
|
+
# @return [Array<Hash{ip=>String, group=>String, user=>String}>]<Paste>
|
188
|
+
def nodes
|
189
|
+
@@nodes
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
include Assemblr::Remote
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'assemblr'
|
4
|
+
|
5
|
+
require 'open3'
|
6
|
+
|
7
|
+
module Assemblr
|
8
|
+
##
|
9
|
+
# Defines methods for common shell commands.
|
10
|
+
module Shell
|
11
|
+
def self.included(_)
|
12
|
+
expose_method :exec
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Execute a command locally.
|
17
|
+
#
|
18
|
+
# @param cmd [String] the command to run locally
|
19
|
+
# @param inject [Array<String>] strings to inject into stdin
|
20
|
+
#
|
21
|
+
# @return [Array(String, Process::Status)]
|
22
|
+
def exec(cmd, inject: [])
|
23
|
+
log_string = "running `#{cmd}` locally"
|
24
|
+
log_string += " with these inputs: #{inject}" unless inject.empty?
|
25
|
+
log_info log_string
|
26
|
+
|
27
|
+
result = ''
|
28
|
+
status = nil
|
29
|
+
Open3.popen2e(cmd) do |i, o, wait|
|
30
|
+
inject.each do |line|
|
31
|
+
line = line.strip
|
32
|
+
i.write line + "\n"
|
33
|
+
end
|
34
|
+
i.close
|
35
|
+
result = o.read
|
36
|
+
status = wait.value
|
37
|
+
end
|
38
|
+
|
39
|
+
if status.exitstatus != 0
|
40
|
+
code = status.exitstatus
|
41
|
+
log_error "local command `#{cmd}` exited with a status of #{code}"
|
42
|
+
else
|
43
|
+
log_success "local command `#{cmd}` executed successfully"
|
44
|
+
end
|
45
|
+
|
46
|
+
[result, status]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
include Assemblr::Shell
|
data/lib/assemblr/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: assemblr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Burmeister-Morrison
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-03-
|
11
|
+
date: 2020-03-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -110,8 +110,9 @@ files:
|
|
110
110
|
- bin/console
|
111
111
|
- bin/setup
|
112
112
|
- lib/assemblr.rb
|
113
|
-
- lib/assemblr/
|
114
|
-
- lib/assemblr/
|
113
|
+
- lib/assemblr/network.rb
|
114
|
+
- lib/assemblr/remote.rb
|
115
|
+
- lib/assemblr/shell.rb
|
115
116
|
- lib/assemblr/version.rb
|
116
117
|
homepage: https://github.com/rburmorrison/assemblr
|
117
118
|
licenses:
|
data/lib/assemblr/logging.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'tty-logger'
|
4
|
-
|
5
|
-
module Assemblr
|
6
|
-
Logger = TTY::Logger.new do |config|
|
7
|
-
config.metadata = %i[time date]
|
8
|
-
end
|
9
|
-
|
10
|
-
m_log = true
|
11
|
-
m_quit_on_error = true
|
12
|
-
|
13
|
-
##
|
14
|
-
# @!method info(message)
|
15
|
-
# Logs an info message.
|
16
|
-
#
|
17
|
-
# @return [void]
|
18
|
-
|
19
|
-
##
|
20
|
-
# @!method success(message)
|
21
|
-
# Logs a success message.
|
22
|
-
#
|
23
|
-
# @return [void]
|
24
|
-
|
25
|
-
##
|
26
|
-
# @!method warn(message)
|
27
|
-
# Logs a warning message.
|
28
|
-
#
|
29
|
-
# @return [void]
|
30
|
-
%i[info success warn].each do |level|
|
31
|
-
define_method level do |*args, &block|
|
32
|
-
return unless m_log
|
33
|
-
|
34
|
-
Logger.send(level, *args, &block)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
##
|
39
|
-
# @!method error(message)
|
40
|
-
# Logs a error message. If configured, this will end the task with error code
|
41
|
-
# 1.
|
42
|
-
#
|
43
|
-
# @return [void]
|
44
|
-
define_method :error do |*args, &block|
|
45
|
-
Logger.send(:error, *args, &block) if m_log
|
46
|
-
exit 1 if m_quit_on_error
|
47
|
-
end
|
48
|
-
end
|
data/lib/assemblr/utilities.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'socket'
|
4
|
-
require 'net/ping'
|
5
|
-
|
6
|
-
##
|
7
|
-
# A collection of utility functions for assemblr.
|
8
|
-
module Assemblr
|
9
|
-
##
|
10
|
-
# Retrieve the first ip address that is not '127.0.0.1' on the local machine.
|
11
|
-
#
|
12
|
-
# @param pattern [Regexp] a pattern to test the ips against
|
13
|
-
def get_local_ip(pattern = //)
|
14
|
-
Socket.ip_address_list.each do |ip|
|
15
|
-
address = ip.ip_address
|
16
|
-
next if address == '127.0.0.1'
|
17
|
-
return address if address.match?(pattern)
|
18
|
-
end
|
19
|
-
''
|
20
|
-
end
|
21
|
-
|
22
|
-
##
|
23
|
-
# Check if a remote machine can be contacted.
|
24
|
-
#
|
25
|
-
# @return [Bool]
|
26
|
-
def reachable?(ip)
|
27
|
-
external = Net::Ping::External.new(ip)
|
28
|
-
|
29
|
-
info %(attempting to contact host "#{ip}"...)
|
30
|
-
reachable = external.ping || external.ping6
|
31
|
-
if reachable
|
32
|
-
success %(host "#{ip}" is reachable)
|
33
|
-
else
|
34
|
-
error %(unable to contact host "#{ip}")
|
35
|
-
end
|
36
|
-
|
37
|
-
reachable
|
38
|
-
end
|
39
|
-
end
|