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