herd-rb 0.3.0 → 0.5.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/README.md +84 -1
- data/lib/herd/commands/authorized_keys.rb +48 -0
- data/lib/herd/commands/crontab.rb +16 -0
- data/lib/herd/commands/files.rb +134 -0
- data/lib/herd/{session/commands → commands}/packages.rb +1 -1
- data/lib/herd/commands/python.rb +12 -0
- data/lib/herd/commands/templates.rb +18 -0
- data/lib/herd/host.rb +36 -12
- data/lib/herd/log.rb +54 -0
- data/lib/herd/session.rb +33 -14
- data/lib/herd/version.rb +1 -1
- data/lib/herd.rb +1 -0
- data/log/.keep +0 -0
- metadata +9 -3
- data/lib/herd/session/commands/authorized_keys.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ded7f78ffd316333cc6e7177d7ec84ad9ac91ec6cd9b10a9b3e55c7851fa6e22
|
|
4
|
+
data.tar.gz: fb2aed4ff5ae882238b6a38905d0685af26a03d11149ae3e1cfe2d4846287e08
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccaa31336a819b2e845f0a713696303900a049f764d5eb7b1233e554f15fcf497a2d74109a2bf293e3d309a82bc711ad07cd9d38acfac18811f32e9c29869250
|
|
7
|
+
data.tar.gz: 1088e0d0613804db1ef7a1c2f098bdc63bc01aadf391ab4e712019f65461f4fc8d10ba65d84fd556f6065816c41213e7b5b6aed54b1db29028a2aa1555521d87
|
data/README.md
CHANGED
|
@@ -6,7 +6,13 @@ Fast host configuration tool.
|
|
|
6
6
|
|
|
7
7
|
* [x] Run with `sudo`
|
|
8
8
|
* [x] Commands with arguments
|
|
9
|
-
* [
|
|
9
|
+
* [x] Reading and writing files
|
|
10
|
+
* [x] Templates (ERB)
|
|
11
|
+
* [x] Copy dirs
|
|
12
|
+
* [ ] Compare with Rsync
|
|
13
|
+
* [x] Crontab
|
|
14
|
+
* [x] Log all commands for all hosts
|
|
15
|
+
* [ ] Add user to group
|
|
10
16
|
|
|
11
17
|
## Installation
|
|
12
18
|
|
|
@@ -55,6 +61,21 @@ runner.exec("hostname") # ["alpha001\n", "omega001\n"]
|
|
|
55
61
|
runner.exec { hostname + uptime } # ["alpha001\n2000 years\n", "omega001\2500 years\n"]
|
|
56
62
|
```
|
|
57
63
|
|
|
64
|
+
List of hosts can be loaded from the CSV file:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# hosts.csv
|
|
68
|
+
host,port,user,password,some_param1,some_param2
|
|
69
|
+
alpha.tesla.com,2022,elon,T0pS3kr3t,value1,value2
|
|
70
|
+
omega.tesla.com,2023,elon,T0pS3kr3t2,value3,value4
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
hosts = Herd::Host.from_csv("hosts.csv")
|
|
75
|
+
runner = Herd::Runner.new(hosts)
|
|
76
|
+
...
|
|
77
|
+
```
|
|
78
|
+
|
|
58
79
|
### Something more complex
|
|
59
80
|
|
|
60
81
|
```ruby
|
|
@@ -72,6 +93,68 @@ result = runner.exec do
|
|
|
72
93
|
puts "Added new key for host #{h}"
|
|
73
94
|
end
|
|
74
95
|
end
|
|
96
|
+
|
|
97
|
+
# or even simpler
|
|
98
|
+
my_key2 = "ssh-ed25519 ..."
|
|
99
|
+
|
|
100
|
+
result = runner.exec do
|
|
101
|
+
authorized_keys_contains_exactly([my_key, my_key2])
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Files and directories
|
|
106
|
+
|
|
107
|
+
Following example takes file from the `./files/etc/sudoers.d/50-elon`
|
|
108
|
+
and copy content to the remote host with required permissions.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
result = runner.exec do
|
|
112
|
+
file("/etc/sudoers.d/50-elon", "root", "root", 440)
|
|
113
|
+
|
|
114
|
+
# or copy dirs
|
|
115
|
+
dir("/home/elon/projects", "elon", "elon")
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Templates
|
|
120
|
+
|
|
121
|
+
Following example takes ERB template from the `./templates/home/elon/.env.erb`
|
|
122
|
+
and renders using additional `Herd::Host` values and copies content to the remote host
|
|
123
|
+
`/home/elon/.env`:
|
|
124
|
+
|
|
125
|
+
```erb
|
|
126
|
+
# File: ./templates/home/elon/.env.erb
|
|
127
|
+
export ALIAS=<%= alias %>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
host = Herd::Host.new("tesla.com", "elon", password: "T0pS3kr3t", alias: "alpha001")
|
|
132
|
+
runner = Runner.new([host])
|
|
133
|
+
runner.exec do |values|
|
|
134
|
+
# values contain named arguments (except password and public_key_path)
|
|
135
|
+
# from the host constructor:
|
|
136
|
+
# { host: "tesla.com", port: 22, user: "elon", alias: "alpha001" }
|
|
137
|
+
template("/home/elon/.env", "elon", "wheels", values: values)
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Crontab
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
crontab("* * * * * /some-script.sh")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Logs
|
|
148
|
+
Herd logs all commands, outputs and errors into the `log/<host>_<port>_<user>/<timestamp>.json` files:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
{"vars":{"alias":"alpha001","port":22,"host":"tesla.com","user":"elon"}},
|
|
153
|
+
{"timestamp":"2025-11-09 18:10:21.134","command":"test -a /home/elon/.herd-version; echo $?"},
|
|
154
|
+
{"timestamp":"2025-11-09 18:10:21.395","command":"test -a /home/elon/.herd-version; echo $?","output":"4\r\n","time":0.261358},
|
|
155
|
+
{"timestamp":"2025-11-09 18:10:22.013","command":"cat /home/home/.herd-version"},
|
|
156
|
+
{"timestamp":"2025-11-09 18:10:22.314","command":"cat /home/home/.herd-version","output":"4\r\n","time":0.301}
|
|
157
|
+
}
|
|
75
158
|
```
|
|
76
159
|
|
|
77
160
|
## Development
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herd
|
|
4
|
+
module Commands
|
|
5
|
+
# Commands for inspecting and managing authorized SSH keys.
|
|
6
|
+
module AuthorizedKeys
|
|
7
|
+
AUTHORIZED_KEYS_FILE = "~/.ssh/authorized_keys"
|
|
8
|
+
|
|
9
|
+
def authorized_keys_contains_exactly(required_keys)
|
|
10
|
+
required_keys = [required_keys].flatten
|
|
11
|
+
diff = keys_diff(authorized_keys, required_keys)
|
|
12
|
+
self.authorized_keys = required_keys
|
|
13
|
+
|
|
14
|
+
diff
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def authorized_keys
|
|
18
|
+
read_file(AUTHORIZED_KEYS_FILE)&.split(/\r\n|\r|\n/) || []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def authorized_keys=(keys)
|
|
22
|
+
touch(AUTHORIZED_KEYS_FILE)
|
|
23
|
+
write_to_file(AUTHORIZED_KEYS_FILE, [keys].flatten.join("\n"))
|
|
24
|
+
file_permissions(AUTHORIZED_KEYS_FILE, 600)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_authorized_key(key)
|
|
28
|
+
touch(AUTHORIZED_KEYS_FILE)
|
|
29
|
+
append_to_file(AUTHORIZED_KEYS_FILE, key)
|
|
30
|
+
file_permissions(AUTHORIZED_KEYS_FILE, 600)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def keys_diff(actual_keys, required_keys)
|
|
36
|
+
result = Hash.new { |h, k| h[k] = [] }
|
|
37
|
+
actual_keys.each do |actual_key|
|
|
38
|
+
if required_keys.include?(actual_key)
|
|
39
|
+
result[:existing] << actual_key
|
|
40
|
+
else
|
|
41
|
+
result[:obsolete] << actual_key
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
result.merge({ added: required_keys - actual_keys })
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herd
|
|
4
|
+
module Commands
|
|
5
|
+
# Commands for adding new crontab tasks or replace with given one.
|
|
6
|
+
module Crontab
|
|
7
|
+
def add_cron(entry)
|
|
8
|
+
run(%(crontab -l | { cat; echo "#{entry}"; } | crontab -))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def crontab(entry)
|
|
12
|
+
run(%(echo "#{entry}" | crontab -))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "diff/lcs"
|
|
4
|
+
require "diff/lcs/hunk"
|
|
5
|
+
|
|
6
|
+
module Herd
|
|
7
|
+
module Commands
|
|
8
|
+
FILES = "files"
|
|
9
|
+
|
|
10
|
+
# Create, read, write, remove files, check permssions.
|
|
11
|
+
module Files
|
|
12
|
+
class PermissionDeniedError < StandardError; end
|
|
13
|
+
|
|
14
|
+
def file_exists?(path)
|
|
15
|
+
run("test -a #{path}; echo $?").chomp == "0"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def file_readable?(path)
|
|
19
|
+
run("test -r #{path}; echo $?").chomp == "0"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def file_writable?(path)
|
|
23
|
+
run("test -w #{path}; echo $?").chomp == "0"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def dir(path, user, group, sudo: false)
|
|
27
|
+
Dir.glob(File.join(FILES, path, "**/*"), File::FNM_DOTMATCH).each do |f|
|
|
28
|
+
remote_path = f.sub(FILES, "")
|
|
29
|
+
if File.directory?(f)
|
|
30
|
+
mkdir_p(remote_path, user, group, sudo: sudo)
|
|
31
|
+
else
|
|
32
|
+
file(remote_path, user, group)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def mkdir_p(path, user, group, sudo: false)
|
|
38
|
+
if sudo
|
|
39
|
+
run("sudo mkdir -p #{path}")
|
|
40
|
+
else
|
|
41
|
+
run("mkdir -p #{path}")
|
|
42
|
+
end
|
|
43
|
+
file_user_and_group(path, user, group)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def file(path, user, group, content: nil, mode: nil)
|
|
47
|
+
required_content = if content.nil?
|
|
48
|
+
File.read(File.join(FILES, path))
|
|
49
|
+
else
|
|
50
|
+
content
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
expect_file_content_equals(path, required_content)
|
|
54
|
+
|
|
55
|
+
file_user_and_group(path, user, group)
|
|
56
|
+
file_permissions(path, mode) if mode
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def expect_file_content_equals(path, required_content)
|
|
60
|
+
if file_exists?(path)
|
|
61
|
+
actual_content = read_file!(path, sudo: true)
|
|
62
|
+
unless actual_content.lines(chomp: true) == required_content.lines(chomp: true)
|
|
63
|
+
# "File has been replaced with diff:\n\n#{diff(actual_content, required_content)}"
|
|
64
|
+
write_to_file!(path, required_content, sudo: true)
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
write_to_file!(path, required_content, sudo: true)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def read_file!(path, sudo: false)
|
|
72
|
+
if file_readable?(path)
|
|
73
|
+
read_file(path)
|
|
74
|
+
elsif sudo
|
|
75
|
+
read_file(path, sudo: true)
|
|
76
|
+
else
|
|
77
|
+
raise PermissionDeniedError, "'#{path}' is not readable"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def read_file(path, sudo: false)
|
|
82
|
+
command = "cat #{path}"
|
|
83
|
+
command = "sudo #{command}" if sudo
|
|
84
|
+
|
|
85
|
+
result = run(command)&.chomp
|
|
86
|
+
result = result.sub(/\A(\r\n|\r|\n)/, "") if sudo
|
|
87
|
+
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def write_to_file!(path, content, sudo: false)
|
|
92
|
+
if file_writable?(path)
|
|
93
|
+
write_to_file(path, content)
|
|
94
|
+
elsif sudo
|
|
95
|
+
write_to_file(path, content, sudo: true)
|
|
96
|
+
else
|
|
97
|
+
raise PermissionDeniedError, "'#{path}' is not writable"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def write_to_file(path, content, sudo: false)
|
|
102
|
+
command = "tee"
|
|
103
|
+
command = "sudo #{command}" if sudo
|
|
104
|
+
run(%(#{command} #{path} << EOF
|
|
105
|
+
#{content}
|
|
106
|
+
EOF))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def append_to_file(path, content, sudo: false)
|
|
110
|
+
command = "tee -a"
|
|
111
|
+
command = "sudo #{command}" if sudo
|
|
112
|
+
run(%(#{command} #{path} << EOF
|
|
113
|
+
#{content}
|
|
114
|
+
EOF))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def file_user_and_group(path, user, group)
|
|
118
|
+
sudo("chown #{user}:#{group} #{path}")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def file_permissions(path, mode)
|
|
122
|
+
sudo("chmod #{mode} #{path}")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def diff(actual, required)
|
|
126
|
+
actual = actual.lines(chomp: true)
|
|
127
|
+
required = required.lines(chomp: true)
|
|
128
|
+
|
|
129
|
+
diffs = Diff::LCS.diff actual, required
|
|
130
|
+
Diff::LCS::Hunk.new(actual, required, diffs[0], 3, 0).diff(:unified)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module Herd
|
|
6
|
+
module Commands
|
|
7
|
+
TEMPLATES = "templates"
|
|
8
|
+
|
|
9
|
+
# Render templates using ERB
|
|
10
|
+
module Templates
|
|
11
|
+
def template(path, user, group, mode: nil, values: {})
|
|
12
|
+
erb = ERB.new(File.read(File.join(TEMPLATES, "#{path}.erb")))
|
|
13
|
+
content = erb.result_with_hash(values)
|
|
14
|
+
file(path, user, group, mode: mode, content: content)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/herd/host.rb
CHANGED
|
@@ -1,36 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "csv"
|
|
4
|
+
require "json"
|
|
3
5
|
require "net/ssh"
|
|
4
6
|
|
|
5
7
|
module Herd
|
|
6
8
|
# Target host
|
|
7
9
|
class Host
|
|
8
|
-
|
|
10
|
+
include Herd::Log
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
attr_reader :host, :user, :ssh_options, :password, :vars, :log
|
|
13
|
+
|
|
14
|
+
# port, private_key_path, password are for the ssh connection
|
|
15
|
+
def initialize(host, user, options)
|
|
11
16
|
@host = host
|
|
12
17
|
@user = user
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
create_ssh_options(options)
|
|
20
|
+
|
|
21
|
+
@password = options.delete(:password)
|
|
22
|
+
@vars = options.merge(host: host, user: user, port: ssh_options[:port])
|
|
23
|
+
|
|
24
|
+
open_log
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_ssh_options(options)
|
|
28
|
+
@ssh_options = { port: options[:port] || 22, timeout: 10 }
|
|
29
|
+
if options[:private_key_path]
|
|
30
|
+
@ssh_options[:keys] = [options.delete(:private_key_path)]
|
|
17
31
|
else
|
|
18
|
-
@ssh_options[:password] = password
|
|
32
|
+
@ssh_options[:password] = options[:password]
|
|
19
33
|
end
|
|
20
|
-
|
|
21
|
-
@password = password
|
|
22
34
|
end
|
|
23
35
|
|
|
24
36
|
def exec(command = nil, &)
|
|
25
37
|
Net::SSH.start(host, user, ssh_options) do |ssh|
|
|
26
|
-
session = Herd::Session.new(ssh, password)
|
|
38
|
+
session = Herd::Session.new(ssh, password, log)
|
|
27
39
|
|
|
28
|
-
output = nil
|
|
29
40
|
output = session.send(command) if command
|
|
30
|
-
output = session.instance_exec(&) if block_given?
|
|
31
|
-
|
|
41
|
+
output = session.instance_exec(vars, &) if block_given?
|
|
32
42
|
output
|
|
33
43
|
end
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
log_connection_error(e)
|
|
46
|
+
ensure
|
|
47
|
+
close_log
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.from_csv(file = "hosts.csv")
|
|
51
|
+
CSV.read(file, headers: true).map do |csv|
|
|
52
|
+
h = csv.to_h.transform_keys(&:to_sym)
|
|
53
|
+
host = h.delete(:host)
|
|
54
|
+
user = h.delete(:user)
|
|
55
|
+
port = h.delete(:port).to_i
|
|
56
|
+
new(host, user, h.merge(port: port))
|
|
57
|
+
end
|
|
34
58
|
end
|
|
35
59
|
end
|
|
36
60
|
end
|
data/lib/herd/log.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Herd
|
|
7
|
+
# Methods for logging commands, outputs and errors.
|
|
8
|
+
module Log
|
|
9
|
+
def log_file_path
|
|
10
|
+
dir = "log/#{vars[:host]}_#{vars[:port]}_#{vars[:user]}"
|
|
11
|
+
FileUtils.mkdir_p(dir)
|
|
12
|
+
"#{File.join(dir, Time.now.strftime("%Y%m%d_%H%M%S"))}.json"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def open_log
|
|
16
|
+
@log = File.open(log_file_path, "w")
|
|
17
|
+
|
|
18
|
+
log.puts "{"
|
|
19
|
+
log.print({ vars: vars }.to_json)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def close_log
|
|
23
|
+
log.puts "\n}"
|
|
24
|
+
log.close
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def log_connection_error(error)
|
|
28
|
+
puts "#{vars.inspect}: #{error.message}"
|
|
29
|
+
log.puts ","
|
|
30
|
+
log.print({ error: error.message, error_trace: error.backtrace }.to_json)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def log_command_start(timestamp, command)
|
|
34
|
+
log.puts(",")
|
|
35
|
+
log.print({ timestamp: time(timestamp), command: command }.to_json)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def log_command_output(command, output, started_at)
|
|
39
|
+
now = Time.now
|
|
40
|
+
log.puts(",")
|
|
41
|
+
log.print({ timestamp: time(now), command: command, output: output, time: now - started_at }.to_json)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def log_command_error(command, error, started_at)
|
|
45
|
+
now = Time.now
|
|
46
|
+
log.puts(",")
|
|
47
|
+
log.print({ timestamp: time(now), command: command, error: error, time: now - started_at }.to_json)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def time(timestamp = Time.now)
|
|
51
|
+
timestamp.strftime("%Y-%m-%d %H:%M:%S.%L")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/herd/session.rb
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
module Herd
|
|
4
4
|
# Session for executing commands on the remote host
|
|
5
5
|
class Session
|
|
6
|
+
include Herd::Log
|
|
7
|
+
|
|
6
8
|
OS_COMMANDS = %i[cat chmod echo hostname touch].freeze
|
|
7
|
-
CUSTOM_COMMANDS_DIR = File.expand_path("
|
|
9
|
+
CUSTOM_COMMANDS_DIR = File.expand_path("commands", __dir__)
|
|
8
10
|
|
|
9
|
-
attr_reader :ssh, :password
|
|
11
|
+
attr_reader :ssh, :password, :log
|
|
10
12
|
|
|
11
|
-
def initialize(ssh, password
|
|
13
|
+
def initialize(ssh, password, log)
|
|
12
14
|
@ssh = ssh
|
|
13
15
|
@password = password
|
|
16
|
+
@log = log
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def method_missing(cmd, *args)
|
|
@@ -27,7 +30,7 @@ module Herd
|
|
|
27
30
|
channel.request_pty do |ch, success|
|
|
28
31
|
raise ::Herd::CommandError, "could not obtain pty" unless success
|
|
29
32
|
|
|
30
|
-
channel_run(ch, command, result)
|
|
33
|
+
channel_run(ch, command, result, Time.now)
|
|
31
34
|
end
|
|
32
35
|
end
|
|
33
36
|
ssh.loop
|
|
@@ -42,7 +45,7 @@ module Herd
|
|
|
42
45
|
def load_command_modules
|
|
43
46
|
command_files.each { |file| require file }
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
command_modules.each do |mod|
|
|
46
49
|
next if self <= mod
|
|
47
50
|
|
|
48
51
|
prepend mod
|
|
@@ -55,29 +58,45 @@ module Herd
|
|
|
55
58
|
Dir[File.join(CUSTOM_COMMANDS_DIR, "*.rb")]
|
|
56
59
|
end
|
|
57
60
|
|
|
58
|
-
def
|
|
59
|
-
return [] unless defined?(Herd::
|
|
61
|
+
def command_modules
|
|
62
|
+
return [] unless defined?(Herd::Commands)
|
|
60
63
|
|
|
61
|
-
Herd::
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
Herd::Commands.constants
|
|
65
|
+
.sort
|
|
66
|
+
.map { |const_name| Herd::Commands.const_get(const_name) }
|
|
67
|
+
.select { |value| value.is_a?(Module) }
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
private
|
|
69
72
|
|
|
70
|
-
def channel_run(channel, command, result)
|
|
73
|
+
def channel_run(channel, command, result, started_at)
|
|
74
|
+
log_command_start(started_at, command)
|
|
75
|
+
|
|
71
76
|
channel.exec(command) do |c, _|
|
|
72
77
|
c.on_data do |_, data|
|
|
73
|
-
|
|
78
|
+
process_output(c, command, started_at, data, result)
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
c.on_extended_data do |_, _, data|
|
|
77
|
-
|
|
82
|
+
process_error(command, started_at, data)
|
|
78
83
|
end
|
|
79
84
|
end
|
|
80
85
|
end
|
|
86
|
+
|
|
87
|
+
def process_output(channel, command, started_at, data, result)
|
|
88
|
+
if data.include?("[sudo] password for")
|
|
89
|
+
channel.send_data "#{password}\n"
|
|
90
|
+
else
|
|
91
|
+
log_command_output(command, data, started_at)
|
|
92
|
+
result << data
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def process_error(command, started_at, data)
|
|
97
|
+
log_command_error(command, data, started_at)
|
|
98
|
+
raise ::Herd::CommandError, data
|
|
99
|
+
end
|
|
81
100
|
end
|
|
82
101
|
end
|
|
83
102
|
|
data/lib/herd/version.rb
CHANGED
data/lib/herd.rb
CHANGED
data/log/.keep
ADDED
|
File without changes
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: herd-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yury Kotlyarov
|
|
@@ -25,12 +25,18 @@ files:
|
|
|
25
25
|
- Rakefile
|
|
26
26
|
- TODO.md
|
|
27
27
|
- lib/herd.rb
|
|
28
|
+
- lib/herd/commands/authorized_keys.rb
|
|
29
|
+
- lib/herd/commands/crontab.rb
|
|
30
|
+
- lib/herd/commands/files.rb
|
|
31
|
+
- lib/herd/commands/packages.rb
|
|
32
|
+
- lib/herd/commands/python.rb
|
|
33
|
+
- lib/herd/commands/templates.rb
|
|
28
34
|
- lib/herd/host.rb
|
|
35
|
+
- lib/herd/log.rb
|
|
29
36
|
- lib/herd/runner.rb
|
|
30
37
|
- lib/herd/session.rb
|
|
31
|
-
- lib/herd/session/commands/authorized_keys.rb
|
|
32
|
-
- lib/herd/session/commands/packages.rb
|
|
33
38
|
- lib/herd/version.rb
|
|
39
|
+
- log/.keep
|
|
34
40
|
- sig/herd.rbs
|
|
35
41
|
homepage: https://github.com/yura/herd
|
|
36
42
|
licenses:
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Herd
|
|
4
|
-
module SessionCommands
|
|
5
|
-
# Commands for inspecting and managing authorized SSH keys.
|
|
6
|
-
module AuthorizedKeys
|
|
7
|
-
def authorized_keys
|
|
8
|
-
cat("~/.ssh/authorized_keys")&.chomp&.split("\n") || []
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def add_authorized_key(key)
|
|
12
|
-
touch("~/.ssh/authorized_keys")
|
|
13
|
-
chmod("600 ~/.ssh/authorized_keys")
|
|
14
|
-
echo("'#{key}' >> ~/.ssh/authorized_keys")
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|