herd-rb 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84702715cdf7b1c28fe203c5c88a0b0f7a3c0a77bf5bfb237c027a82585be7c0
4
- data.tar.gz: d61aa1dcbe955f6ab0bd56c535612cf490891ed2589425af1ac23baf6e085636
3
+ metadata.gz: ad0f13503109d91f099f4b40a1c208bafd460fd6efbcfe8d15cb92c3d0bad66b
4
+ data.tar.gz: 2a793b3ff5246eabb9ea55cd67b507786b1226afc40ace46208737176f0bd4df
5
5
  SHA512:
6
- metadata.gz: c2f6c111802762b7c5e642a1a8f073d9bad60c138f5d607870a4989154777aa2691997ca514ef440237d73b4112107a72dffe17b1c3221aab0d7663440be70bf
7
- data.tar.gz: b65a40974cee1729989bfc9c65a95b937e101c71072daa2da4cf70475cd04ce8de65ab4483e1dfbf4301bcc27a4bcbe149a9c07119619190bde82f052c1eee1e
6
+ metadata.gz: 2a2b263aec47c102e8d60069352b46814032a90b546928bb5a7cb4ed5ee2cae93cfc05e2b153e71a0366249c4e59a8a84903a5f3e6c7efd34de1a2d7771023b0
7
+ data.tar.gz: 02e6e5e6bd999632457f7521ae443fa55847d8ef9d8a563cc01b5fcf5a9a3f033064e06b869b36962170ba134006decee2e104f73e9dbaeae863d35715b4d2e7
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.2
data/README.md CHANGED
@@ -4,9 +4,15 @@ Fast host configuration tool.
4
4
 
5
5
  ## TODO
6
6
 
7
- * [ ] Commands with arguments
8
- * [ ] Reading and writing files
9
- * [ ] Run with `sudo`
7
+ * [x] Run with `sudo`
8
+ * [x] Commands with arguments
9
+ * [x] Reading and writing files
10
+ * [x] Templates (ERB)
11
+ * [x] Copy dirs
12
+ * [ ] Compare with Rsync
13
+ * [x] Crontab
14
+ * [ ] Log all commands for all hosts
15
+ * [ ] Add user to group
10
16
 
11
17
  ## Installation
12
18
 
@@ -55,6 +61,89 @@ 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
+
79
+ ### Something more complex
80
+
81
+ ```ruby
82
+ public_key_path = File.expand_path("~/.ssh/id_ed25519.pub")
83
+ my_key = File.read(public_key_path).chomp
84
+
85
+ result = runner.exec do
86
+ h = hostname
87
+ keys = authorized_keys
88
+
89
+ if keys.include?(my_key)
90
+ puts "Key already in authorized_keys on host #{h}"
91
+ else
92
+ add_authorized_key my_key
93
+ puts "Added new key for host #{h}"
94
+ end
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
+
58
147
  ## Development
59
148
 
60
149
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/TODO.md ADDED
@@ -0,0 +1,20 @@
1
+ # TODO
2
+
3
+ - [x] Load session commands automatically from `lib/herd/session/commands`.
4
+ - [x] Extract existing `Session` command helpers (e.g., `add_authorized_key`) into the commands directory.
5
+ - [x] Ensure new command loading approach is covered by specs.
6
+ - [ ] Add explicit `logger` dependency to address net-ssh warning on Ruby 3.4.2.
7
+ - [ ] Document new command-extension flow with YARD annotations.
8
+
9
+ # Log
10
+
11
+ - Initialized planning file and seeded initial task list.
12
+ - Updated project configuration to target Ruby 3.4.2 (Gemfile, gemspec, .ruby-version, lockfile).
13
+ - Synced RuboCop config with Ruby 3.4.2 and confirmed clean lint run.
14
+ - Moved development dependencies from gemspec to Gemfile; bundle, lint, and tests still pass (with net-ssh logger warning).
15
+ - Fixed `Session#method_missing` to avoid extra spaces when building commands; specs now green.
16
+ - Implemented automatic session command loading via prepended modules, moved authorized key helpers, added YARD dependency, and expanded specs accordingly.
17
+
18
+ # Notes
19
+
20
+ - Keep command implementations in English and back them with tests.
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herd
4
+ module Commands
5
+ # Working with package managers, like apt for Ubuntu
6
+ module Packages
7
+ def install_packages(packages)
8
+ packages = [packages].flatten.join(" ")
9
+ echo %(-e '#{password}\n' | sudo -S apt install -qq -y #{packages})
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herd
4
+ module Commands
5
+ # Commands for python environment
6
+ module Python
7
+ def install_uv
8
+ run("curl -LsSf https://astral.sh/uv/install.sh | sh")
9
+ end
10
+ end
11
+ end
12
+ 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,33 +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
- attr_reader :host, :user, :ssh_options
10
+ include Herd::Log
9
11
 
10
- def initialize(host, user, port: 22, private_key_path: nil, password: nil)
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
- @ssh_options = { port: port, timeout: 10 }
15
- if private_key_path
16
- @ssh_options[:keys] = [private_key_path]
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
34
  end
21
35
 
22
- def exec(command = nil, &block)
36
+ def exec(command = nil, &)
23
37
  Net::SSH.start(host, user, ssh_options) do |ssh|
24
- session = Herd::Session.new(ssh)
38
+ session = Herd::Session.new(ssh, password, log)
25
39
 
26
- output = nil
27
40
  output = session.send(command) if command
28
- output = session.instance_exec(&block) if block_given?
41
+ output = session.instance_exec(vars, &) if block_given?
29
42
  output
30
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
31
58
  end
32
59
  end
33
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/runner.rb CHANGED
@@ -9,9 +9,9 @@ module Herd
9
9
  @hosts = hosts
10
10
  end
11
11
 
12
- def exec(command = nil, &block)
12
+ def exec(command = nil, &)
13
13
  threads = hosts.map do |host|
14
- Thread.new { host.exec(command, &block) }
14
+ Thread.new { host.exec(command, &) }
15
15
  end
16
16
 
17
17
  threads.each(&:join)
data/lib/herd/session.rb CHANGED
@@ -3,37 +3,101 @@
3
3
  module Herd
4
4
  # Session for executing commands on the remote host
5
5
  class Session
6
- COMMANDS = %i[cat chmod echo hostname touch].freeze
6
+ include Herd::Log
7
7
 
8
- attr_reader :ssh
8
+ OS_COMMANDS = %i[cat chmod echo hostname touch].freeze
9
+ CUSTOM_COMMANDS_DIR = File.expand_path("commands", __dir__)
9
10
 
10
- def initialize(ssh)
11
+ attr_reader :ssh, :password, :log
12
+
13
+ def initialize(ssh, password, log)
11
14
  @ssh = ssh
15
+ @password = password
16
+ @log = log
12
17
  end
13
18
 
14
- def authorized_keys
15
- cat("~/.ssh/authorized_keys")&.chomp&.split("\n") || []
19
+ def method_missing(cmd, *args)
20
+ command_parts = [cmd.to_s]
21
+ command_parts.concat(args.map(&:to_s)) if args.any?
22
+ command = command_parts.join(" ")
23
+
24
+ run(command)
16
25
  end
17
26
 
18
- def add_authorized_key(key)
19
- touch("~/.ssh/authorized_keys")
20
- chmod("600 ~/.ssh/authorized_keys")
21
- echo "'#{key}' >> ~/.ssh/authorized_keys"
27
+ def run(command)
28
+ result = []
29
+ ssh.open_channel do |channel|
30
+ channel.request_pty do |ch, success|
31
+ raise ::Herd::CommandError, "could not obtain pty" unless success
32
+
33
+ channel_run(ch, command, result, Time.now)
34
+ end
35
+ end
36
+ ssh.loop
37
+ result.join
22
38
  end
23
39
 
24
- def method_missing(cmd, *args)
25
- command = cmd.to_s
26
- command = "#{command} #{args.join(" ")}" if args
40
+ def respond_to_missing?(cmd)
41
+ OS_COMMANDS.include?(cmd) || super
42
+ end
43
+
44
+ class << self
45
+ def load_command_modules
46
+ command_files.each { |file| require file }
47
+
48
+ command_modules.each do |mod|
49
+ next if self <= mod
50
+
51
+ prepend mod
52
+ end
53
+ end
54
+
55
+ private
27
56
 
28
- ssh.exec! command do |_, stream, data|
29
- raise ::Herd::CommandError, data if stream == :stderr
57
+ def command_files
58
+ Dir[File.join(CUSTOM_COMMANDS_DIR, "*.rb")]
59
+ end
60
+
61
+ def command_modules
62
+ return [] unless defined?(Herd::Commands)
30
63
 
31
- return data
64
+ Herd::Commands.constants
65
+ .sort
66
+ .map { |const_name| Herd::Commands.const_get(const_name) }
67
+ .select { |value| value.is_a?(Module) }
32
68
  end
33
69
  end
34
70
 
35
- def respond_to_missing?(cmd)
36
- COMMANDS.include?(cmd) || super
71
+ private
72
+
73
+ def channel_run(channel, command, result, started_at)
74
+ log_command_start(started_at, command)
75
+
76
+ channel.exec(command) do |c, _|
77
+ c.on_data do |_, data|
78
+ process_output(c, command, started_at, data, result)
79
+ end
80
+
81
+ c.on_extended_data do |_, _, data|
82
+ process_error(command, started_at, data)
83
+ end
84
+ end
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
37
99
  end
38
100
  end
39
101
  end
102
+
103
+ Herd::Session.load_command_modules
data/lib/herd/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Herd
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/herd.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "herd/version"
4
+ require_relative "herd/log"
4
5
  require_relative "herd/host"
5
6
  require_relative "herd/runner"
6
7
  require_relative "herd/session"
data/log/.keep ADDED
File without changes
metadata CHANGED
@@ -1,70 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: herd-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yury Kotlyarov
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: rspec
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '3.0'
19
- type: :development
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '3.0'
26
- - !ruby/object:Gem::Dependency
27
- name: rubocop
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '1.21'
33
- type: :development
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '1.21'
40
- - !ruby/object:Gem::Dependency
41
- name: rubocop-rake
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
- type: :development
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- - !ruby/object:Gem::Dependency
55
- name: rubocop-rspec
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- type: :development
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
11
+ dependencies: []
68
12
  description: |
69
13
  Simple ruby DSL for fast host configuration. Supports Ubuntu and requires
70
14
  SSH server running on each targets host.
@@ -74,15 +18,25 @@ executables: []
74
18
  extensions: []
75
19
  extra_rdoc_files: []
76
20
  files:
21
+ - ".ruby-version"
77
22
  - CODE_OF_CONDUCT.md
78
23
  - LICENSE.txt
79
24
  - README.md
80
25
  - Rakefile
26
+ - TODO.md
81
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
82
34
  - lib/herd/host.rb
35
+ - lib/herd/log.rb
83
36
  - lib/herd/runner.rb
84
37
  - lib/herd/session.rb
85
38
  - lib/herd/version.rb
39
+ - log/.keep
86
40
  - sig/herd.rbs
87
41
  homepage: https://github.com/yura/herd
88
42
  licenses:
@@ -91,6 +45,7 @@ metadata:
91
45
  allowed_push_host: https://rubygems.org
92
46
  homepage_uri: https://github.com/yura/herd
93
47
  source_code_uri: https://github.com/yura/herd
48
+ rubygems_mfa_required: 'true'
94
49
  rdoc_options: []
95
50
  require_paths:
96
51
  - lib
@@ -98,7 +53,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
98
53
  requirements:
99
54
  - - ">="
100
55
  - !ruby/object:Gem::Version
101
- version: 3.2.0
56
+ version: 3.4.2
102
57
  required_rubygems_version: !ruby/object:Gem::Requirement
103
58
  requirements:
104
59
  - - ">="