kanrisuru 0.19.4 → 1.0.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: ef72387ee3392fab30d54254e7226e066d943757b686bd1e66fea0fc1bd58181
4
- data.tar.gz: eb829fe3e5d90968c17cfa9922161505f4fbdce4a44da5524f35ad23967c7335
3
+ metadata.gz: 2c8f6cf4fd27ca458feef7d89dd49617d3e4aba1058b203c297c0206bf5d91f0
4
+ data.tar.gz: c0f03db6bee208f1bcd6ed667f3e50db85e56482ded88023e91efa0025c2a7ad
5
5
  SHA512:
6
- metadata.gz: 9e6b315320713f79f5693b6a21269dcadbf6327efae5ab8dc4287165ebfd6f0c05cc221cb351911f669f71b1812ac3a43884ed978ef64422f07a3beab835ec4a
7
- data.tar.gz: 21e425b548076e932aa9770f4a0827e3252b6dba3869e90d219bbf3e70d983fadb87f14b7ed1747f3c4c569c109901e4da708211816573c963c96bd3977bee24
6
+ metadata.gz: 004c36ab67130dcb5a2bc8ff3dd60a28a15ef9c48b64a523b6386ee05a656f92f8af9492cc75400b16c5c26006c9ba9134c8605c6b3f87c185a79624ccf5af93
7
+ data.tar.gz: a2c3d25e09d91bc8da26198e9bfffe986270671f6e81ace0f11710a328cada0ac341d5e40d097cf204a93b091d7c3d400c9caf8b160064ca087da9dcdddec4b7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## Kanrisuru 1.0.0.beta1 (February 27, 2022) ##
2
+ * Add parallel mode for cluster. Allows hosts to run commands concurrently, reducing time it takes due to the high I/O blocking nature of the network requests.
3
+ * Add test cases for cluster parallel mode.
4
+ * Clean up methods on cluster class to use map and each methods in a simplified manner.
5
+ * Remove `/spec` dir from codecoverage.
6
+
7
+ ## Kanrisuru 0.20.0 (February 21, 2022) ##
8
+ * Allow hosts to be connected via proxy host. This is much like using a bastion / jump server.
9
+ * Add integration test cases for proxy host connection.
10
+
1
11
  ## Kanrisuru 0.19.4 (February 19, 2022) ##
2
12
  * Add additional family types to `ss` command.
3
13
 
data/README.md CHANGED
@@ -54,6 +54,27 @@ host = Kanrisuru::Remote::Host.new(
54
54
  )
55
55
  ```
56
56
 
57
+ #### Connect with a Jump / Bastion Host
58
+ To connect to a host behind a firewall through a jump / bastion host, pass either an instance of another Kanrisuru::Remote::Host, or a hash of host config values
59
+
60
+ ```ruby
61
+ proxy = Kanrisuru::Remote::Host.new(
62
+ host: 'proxy-host',
63
+ username: 'ubuntu',
64
+ keys: ['~/.ssh/proxy.pem']
65
+ )
66
+
67
+ host = Kanrisuru::Remote::Host.new(
68
+ host: '1.2.3.4',
69
+ username: 'ubuntu',
70
+ keys: ['~/.ssh/id_rsa'],
71
+ proxy: proxy
72
+ )
73
+
74
+ host.whoami
75
+ 'ubuntu'
76
+ ```
77
+
57
78
  #### run a simple echo command on the remote host
58
79
  ```ruby
59
80
  host.env['VAR'] = 'world'
@@ -124,7 +145,24 @@ host = Kanrisuru::Remote::Host.new(host: 'remote-host-4', username: 'rhel', keys
124
145
  cluster << host
125
146
  ```
126
147
 
127
- Kanrisuru at this point only runs commands sequentially. We plan on creating a parallel run mode in a future release.
148
+ #### Run cluster in parallel mode to reduce time waiting on blocking IO
149
+ ```ruby
150
+ Benchmark.measure do
151
+ cluster.each do |host|
152
+ puts cluster.pwd
153
+ end
154
+ end
155
+ # => 0.198980 0.029681 0.228661 ( 5.258496)
156
+
157
+ cluster.parallel = true
158
+
159
+ Benchmark.measure do
160
+ cluster.each do |host|
161
+ puts cluster.pwd
162
+ end
163
+ end
164
+ # => 0.016478 0.007956 0.024434 ( 0.120066)
165
+ ```
128
166
 
129
167
  #### To run across all hosts with a single command, cluster will return a array of result hashes
130
168
  ```ruby
data/kanrisuru.gemspec CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |gem|
27
27
  gem.add_runtime_dependency 'net-ping', '~> 2.0'
28
28
  gem.add_runtime_dependency 'net-scp', '~> 3.0'
29
29
  gem.add_runtime_dependency 'net-ssh', '~> 6.1'
30
+ gem.add_runtime_dependency 'net-ssh-gateway', '~> 2.0'
30
31
 
31
32
  gem.files = `git ls-files`.split("\n")
32
33
  gem.require_paths = ['lib']
@@ -14,7 +14,7 @@ module Kanrisuru
14
14
  else
15
15
  command.append_flag('--all')
16
16
  end
17
-
17
+
18
18
  execute_shell(command)
19
19
 
20
20
  Kanrisuru::Result.new(command) do |cmd|
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'ostruct'
3
4
 
4
5
  module Kanrisuru
5
6
  module Core
6
7
  module System
7
8
  module Parser
8
- class Sysctl
9
+ class Sysctl
9
10
  class << self
10
11
  def parse(command)
11
12
  result = {}
12
13
 
13
14
  lines = command.to_a
14
15
  lines.each do |line|
15
- next if line.include?("permission denied on key")
16
+ next if line.include?('permission denied on key')
16
17
 
17
18
  keys, value = parse_line(line)
18
19
  next if Kanrisuru::Util.blank?(value)
@@ -27,7 +28,7 @@ module Kanrisuru
27
28
  def build_struct(hash)
28
29
  struct = Struct.new(*hash.keys).new
29
30
 
30
- hash.keys.each do |key|
31
+ hash.each_key do |key|
31
32
  struct[key] = hash[key].is_a?(Hash) ? build_struct(hash[key]) : hash[key]
32
33
  end
33
34
 
@@ -35,7 +36,7 @@ module Kanrisuru
35
36
  end
36
37
 
37
38
  def merge_recursively(h1, h2)
38
- h1.merge(h2) do |k, v1, v2|
39
+ h1.merge(h2) do |_k, v1, v2|
39
40
  if v1.is_a?(Hash) && v2.is_a?(Hash)
40
41
  merge_recursively(v1, v2)
41
42
  else
@@ -64,7 +65,6 @@ module Kanrisuru
64
65
 
65
66
  [string.split('.').map(&:to_sym), value]
66
67
  end
67
-
68
68
  end
69
69
  end
70
70
  end
@@ -34,9 +34,8 @@ module Kanrisuru
34
34
  os_method_names.each do |method_name|
35
35
  define_method method_name do |*args, &block|
36
36
  cluster = namespace_instance.instance_variable_get(:@cluster)
37
- hosts = cluster.instance_variable_get(:@hosts)
38
- hosts.map do |host_addr, host|
39
- { host: host_addr, result: host.send(namespace).send(method_name, *args, &block) }
37
+ cluster.map do |host|
38
+ host.send(namespace).send(method_name, *args, &block)
40
39
  end
41
40
  end
42
41
  end
@@ -45,8 +44,8 @@ module Kanrisuru
45
44
  class_eval do
46
45
  os_method_names.each do |method_name|
47
46
  define_method method_name do |*args, &block|
48
- @hosts.map do |host_addr, host|
49
- { host: host_addr, result: host.send(method_name, *args, &block) }
47
+ map do |host|
48
+ host.send(method_name, *args, &block)
50
49
  end
51
50
  end
52
51
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ require 'etc'
3
+
4
+ module Kanrisuru
5
+ # https://github.com/grosser/parallel/blob/master/lib/parallel/processor_count.rb
6
+ module ProcessorCount
7
+ # Number of processors seen by the OS, used for process scheduling
8
+ def self.processor_count
9
+ @processor_count ||= Integer(ENV['PARALLEL_PROCESSOR_COUNT'] || Etc.nprocessors)
10
+ end
11
+
12
+ # Number of physical processor cores on the current system.
13
+ def self.physical_processor_count
14
+ @physical_processor_count ||= begin
15
+ ppc =
16
+ case RbConfig::CONFIG["target_os"]
17
+ when /darwin[12]/
18
+ IO.popen("/usr/sbin/sysctl -n hw.physicalcpu").read.to_i
19
+ when /linux/
20
+ cores = {} # unique physical ID / core ID combinations
21
+ phy = 0
22
+ IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
23
+ if ln.start_with?("physical")
24
+ phy = ln[/\d+/]
25
+ elsif ln.start_with?("core")
26
+ cid = "#{phy}:#{ln[/\d+/]}"
27
+ cores[cid] = true unless cores[cid]
28
+ end
29
+ end
30
+ cores.count
31
+ when /mswin|mingw/
32
+ require 'win32ole'
33
+ result_set = WIN32OLE.connect("winmgmts://").ExecQuery(
34
+ "select NumberOfCores from Win32_Processor"
35
+ )
36
+ result_set.to_enum.collect(&:NumberOfCores).reduce(:+)
37
+ else
38
+ processor_count
39
+ end
40
+ # fall back to logical count if physical info is invalid
41
+ ppc > 0 ? ppc : processor_count
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'thread'
2
3
 
3
4
  module Kanrisuru
4
5
  module Remote
@@ -6,7 +7,12 @@ module Kanrisuru
6
7
  extend OsPackage::Collection
7
8
  include Enumerable
8
9
 
10
+ attr_accessor :parallel, :concurrency
11
+
9
12
  def initialize(*hosts)
13
+ @parallel = false
14
+ @concurrency = local_concurrency
15
+
10
16
  @hosts = {}
11
17
  hosts.each do |host_opts|
12
18
  add_host(host_opts)
@@ -30,39 +36,23 @@ module Kanrisuru
30
36
  end
31
37
 
32
38
  def execute(command)
33
- @hosts.map do |host_addr, host|
34
- ## Need to evaluate each host independently for the command.
35
- cmd = create_command(command)
36
-
37
- { host: host_addr, result: host.execute(cmd) }
38
- end
39
+ map { |host| host.execute(create_command(command)) }
39
40
  end
40
41
 
41
42
  def execute_shell(command)
42
- @hosts.map do |host_addr, host|
43
- ## Need to evaluate each host independently for the command.
44
- cmd = create_command(command)
45
-
46
- { host: host_addr, result: host.execute_shell(cmd) }
47
- end
48
- end
49
-
50
- def each(&block)
51
- @hosts.each { |_host_addr, host| block.call(host) }
43
+ map { |host| host.execute_shell(create_command(command)) }
52
44
  end
53
45
 
54
46
  def hostname
55
- map_host_results(:hostname)
47
+ map { |host| host.send(:hostname) }
56
48
  end
57
49
 
58
50
  def ping?
59
- map_host_results(:ping?)
51
+ map { |host| host.send(:ping?) }
60
52
  end
61
53
 
62
54
  def su(user)
63
- @hosts.each do |_host_addr, host|
64
- host.su(user)
65
- end
55
+ each { |host| host.su(user) }
66
56
  end
67
57
 
68
58
  def chdir(path = '~')
@@ -70,19 +60,70 @@ module Kanrisuru
70
60
  end
71
61
 
72
62
  def cd(path = '~')
73
- @hosts.each do |_host_addr, host|
74
- host.cd(path)
75
- end
63
+ each { |host| host.cd(path) }
76
64
  end
77
65
 
78
66
  def disconnect
79
- @hosts.each do |_host_addr, host|
80
- host.disconnect
81
- end
67
+ each { |host| host.disconnect }
68
+ end
69
+
70
+ def map(&block)
71
+ parallel? ? each_parallel(preserve: true, &block) : each_sequential(preserve: true, &block)
72
+ end
73
+
74
+ def each(&block)
75
+ parallel? ? each_parallel(preserve: false, &block) : each_sequential(preserve: false, &block)
76
+ end
77
+
78
+ def parallel?
79
+ @parallel
80
+ end
81
+
82
+ def sequential?
83
+ !parallel?
82
84
  end
83
85
 
84
86
  private
85
87
 
88
+ def each_sequential(opts = {}, &block)
89
+ results = @hosts.map do |host_addr, host|
90
+ { host: host_addr, result: block.call(host) }
91
+ end
92
+
93
+ opts[:preserve] ? results : self
94
+ end
95
+
96
+ def each_parallel(opts = {}, &block)
97
+ queue = Queue.new.tap do |q|
98
+ @hosts.each { |_, host| q << host }
99
+ end
100
+
101
+ threads = []
102
+ results = []
103
+ mutex = Mutex.new
104
+
105
+ ## No need to spawn more threads then number of hosts in cluster
106
+ concurrency = queue.length < @concurrency ? queue.length : @concurrency
107
+ concurrency.times do
108
+ threads << Thread.new do
109
+ loop do
110
+ host = queue.pop(true) rescue Thread.exit
111
+
112
+ begin
113
+ result = block.call(host)
114
+ mutex.synchronize { results.push({ host: host.host, result: result }) }
115
+ rescue Exception => exception
116
+ mutex.synchronize { results.push({ host: host.host, result: exception }) }
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ threads.each(&:join)
123
+
124
+ opts[:preserve] ? results : self
125
+ end
126
+
86
127
  def create_command(command)
87
128
  case command
88
129
  when String
@@ -94,12 +135,6 @@ module Kanrisuru
94
135
  end
95
136
  end
96
137
 
97
- def map_host_results(action)
98
- @hosts.map do |host_addr, host|
99
- { host: host_addr, result: host.send(action) }
100
- end
101
- end
102
-
103
138
  def remove_host(host)
104
139
  if host.instance_of?(Kanrisuru::Remote::Host)
105
140
  removed = false
@@ -125,14 +160,22 @@ module Kanrisuru
125
160
  end
126
161
 
127
162
  def add_host(host_opts)
128
- if host_opts.instance_of?(Hash)
163
+ case host_opts
164
+ when Hash
129
165
  @hosts[host_opts[:host]] = Kanrisuru::Remote::Host.new(host_opts)
130
- elsif host_opts.instance_of?(Kanrisuru::Remote::Host)
166
+ when Kanrisuru::Remote::Host
131
167
  @hosts[host_opts.host] = host_opts
168
+ when Kanrisuru::Remote::Cluster
169
+ host_opts.send(:each_sequential) { |host| @hosts[host.host] = host }
132
170
  else
133
171
  raise ArgumentError, 'Invalid host option'
134
172
  end
135
173
  end
174
+
175
+ def local_concurrency
176
+ ProcessorCount.physical_processor_count
177
+ end
178
+
136
179
  end
137
180
  end
138
181
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'net/ssh'
4
+ require 'net/ssh/gateway'
4
5
  require 'net/scp'
5
6
  require 'net/ping'
6
7
 
@@ -12,6 +13,8 @@ module Kanrisuru
12
13
  attr_reader :host, :username, :password, :port, :keys
13
14
 
14
15
  def initialize(opts = {})
16
+ @opts = opts
17
+
15
18
  @host = opts[:host]
16
19
  @username = opts[:username]
17
20
  @login_user = @username
@@ -84,7 +87,11 @@ module Kanrisuru
84
87
  end
85
88
 
86
89
  def ssh
87
- @ssh ||= Net::SSH.start(@host, @username, keys: @keys, password: @password, port: @port)
90
+ @ssh ||= init_ssh
91
+ end
92
+
93
+ def proxy
94
+ @proxy ||= init_proxy
88
95
  end
89
96
 
90
97
  def ping?
@@ -110,6 +117,7 @@ module Kanrisuru
110
117
 
111
118
  def disconnect
112
119
  ssh.close
120
+ proxy&.close(ssh.transport.port)
113
121
  end
114
122
 
115
123
  private
@@ -158,6 +166,35 @@ module Kanrisuru
158
166
  end
159
167
  end
160
168
 
169
+ def init_ssh
170
+ if proxy&.active?
171
+ proxy.ssh(@host, @username,
172
+ keys: @keys, password: @password, port: @port)
173
+ else
174
+ Net::SSH.start(@host, @username,
175
+ keys: @keys, password: @password, port: @port)
176
+ end
177
+ end
178
+
179
+ def init_proxy
180
+ return unless @opts[:proxy]
181
+
182
+ proxy = @opts[:proxy]
183
+
184
+ case proxy
185
+ when Hash
186
+ Net::SSH::Gateway.new(proxy[:host], proxy[:username],
187
+ keys: proxy[:keys], password: proxy[:password], port: proxy[:port])
188
+ when Kanrisuru::Remote::Host
189
+ Net::SSH::Gateway.new(proxy.host, proxy.username,
190
+ keys: proxy.keys, password: proxy.password, port: proxy.port)
191
+ when Net::SSH::Gateway
192
+ proxy
193
+ else
194
+ raise ArgumentError, 'Invalid proxy type'
195
+ end
196
+ end
197
+
161
198
  def init_hostname
162
199
  command = Kanrisuru::Command.new('hostname')
163
200
  execute(command)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kanrisuru
4
- VERSION = '0.19.4'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/kanrisuru.rb CHANGED
@@ -29,6 +29,7 @@ require_relative 'kanrisuru/util'
29
29
  require_relative 'kanrisuru/mode'
30
30
  require_relative 'kanrisuru/os_package'
31
31
  require_relative 'kanrisuru/command'
32
+ require_relative 'kanrisuru/processor_count'
32
33
  require_relative 'kanrisuru/remote'
33
34
  require_relative 'kanrisuru/result'
34
35
  require_relative 'kanrisuru/core'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Kanrisuru::ProcessorCount do
6
+ it 'gets processor count' do
7
+ expect(described_class.processor_count).to be > 0
8
+ end
9
+
10
+ it 'gets physical processor count' do
11
+ expect(described_class.physical_processor_count).to be > 0
12
+ end
13
+
14
+ it "is even factor of logical cpus" do
15
+ expect(described_class.processor_count % described_class.physical_processor_count).to be == 0
16
+ end
17
+ end
@@ -27,6 +27,14 @@ RSpec.describe Kanrisuru::Remote::Cluster do
27
27
  )
28
28
  end
29
29
 
30
+ let(:host3) do
31
+ Kanrisuru::Remote::Host.new(
32
+ host: 'centos-host',
33
+ username: 'centos',
34
+ keys: ['id_rsa']
35
+ )
36
+ end
37
+
30
38
  it 'adds host to a cluster' do
31
39
  cluster = described_class.new(host1)
32
40
  expect(cluster.hosts.length).to eq(1)
@@ -46,7 +54,7 @@ RSpec.describe Kanrisuru::Remote::Cluster do
46
54
  username: 'centos',
47
55
  keys: ['id_rsa']
48
56
  }
49
-
57
+
50
58
  expect(cluster.hosts.length).to eq(3)
51
59
  expect(cluster.count).to eq(3)
52
60
  expect(cluster.count).to eq(3)
@@ -55,6 +63,19 @@ RSpec.describe Kanrisuru::Remote::Cluster do
55
63
  expect(cluster.hosts).to include(host2)
56
64
  end
57
65
 
66
+ it 'adds a cluster to another cluster' do
67
+ cluster1 = described_class.new(host1, host2)
68
+ cluster2 = described_class.new(host3)
69
+
70
+ cluster1 << cluster2
71
+ expect(cluster1.hosts.length).to eq(3)
72
+ expect(cluster1.count).to eq(3)
73
+ expect(cluster1.count).to eq(3)
74
+ expect(cluster1['centos-host'].username).to eq('centos')
75
+ expect(cluster1.hosts).to include(host1)
76
+ expect(cluster1.hosts).to include(host2)
77
+ end
78
+
58
79
  it 'fails to add host to a cluster' do
59
80
  cluster = described_class.new
60
81
  expect { cluster << 1 }.to raise_error(ArgumentError)
@@ -90,6 +111,58 @@ RSpec.describe Kanrisuru::Remote::Cluster do
90
111
  end.to raise_error(ArgumentError)
91
112
  end
92
113
 
114
+ it 'initializes cluster and is run in sequential mode by default' do
115
+ cluster = described_class.new
116
+ expect(cluster).to be_sequential
117
+ end
118
+
119
+ it 'sets cluster to parallel mode' do
120
+ cluster = described_class.new
121
+ cluster.parallel = true
122
+ expect(cluster).to be_parallel
123
+ end
124
+
125
+ it 'runs commands on a cluster sequentialy' do
126
+ cluster = described_class.new(host1, host2)
127
+
128
+ expect(cluster).to receive(:each_sequential)
129
+
130
+ command = Kanrisuru::Command.new('pwd')
131
+ cluster.execute(command)
132
+ end
133
+
134
+ it 'runs commands on a cluster in parallel across hosts' do
135
+ cluster = described_class.new(host1, host2)
136
+ cluster.parallel = true
137
+
138
+ expect(cluster).to receive(:each_parallel)
139
+
140
+ command = Kanrisuru::Command.new('pwd')
141
+ cluster.execute(command)
142
+ end
143
+
144
+ it 'gets cluster back from each method' do
145
+ cluster = described_class.new(host1, host2)
146
+
147
+ c = cluster.each { |h| h.su('root') }
148
+ expect(c).to be_instance_of(Kanrisuru::Remote::Cluster)
149
+ expect(c.object_id).to eq(cluster.object_id)
150
+ end
151
+
152
+ it 'gets results back from map method' do
153
+ cluster = described_class.new(host1, host2)
154
+
155
+ results = cluster.map do |host|
156
+ host.execute('hello')
157
+ end
158
+
159
+ expect(results).to be_instance_of(Array)
160
+ results.each do |result|
161
+ expect(result).to have_key(:host)
162
+ expect(result).to have_key(:result)
163
+ end
164
+ end
165
+
93
166
  it 'runs execute for a command on a cluster' do
94
167
  cluster = described_class.new
95
168
  cluster << host1
@@ -185,6 +258,19 @@ RSpec.describe Kanrisuru::Remote::Cluster do
185
258
  StubNetwork.unstub_command!(:realpath)
186
259
  end
187
260
 
261
+ it 'raises and catches exception in parallel mode' do
262
+ cluster = described_class.new(host1, host2, host3)
263
+ cluster.parallel = true
264
+
265
+ results = cluster.map do |host|
266
+ raise ArgumentError
267
+ end
268
+
269
+ results.each do |result|
270
+ expect(result[:result]).to be_instance_of(ArgumentError)
271
+ end
272
+ end
273
+
188
274
  it 'changes current working directory for each host in a cluster' do
189
275
  cluster = described_class.new(host1, host2)
190
276
 
@@ -8,6 +8,15 @@
8
8
  "kernel": "Linux",
9
9
  "home": "/home/ubuntu"
10
10
  },
11
+ {
12
+ "name": "proxy",
13
+ "port": 22,
14
+ "username": "ubuntu",
15
+ "ssh_key": "~/.ssh/id_rsa",
16
+ "hostname": "proxy-host",
17
+ "kernel": "Linux",
18
+ "home": "/home/ubuntu"
19
+ },
11
20
  {
12
21
  "name": "ubuntu",
13
22
  "port": 22,
@@ -4,6 +4,8 @@ require 'simplecov'
4
4
  require 'simplecov-cobertura'
5
5
 
6
6
  SimpleCov.start do
7
+ add_filter '/spec/'
8
+
7
9
  command_name "kanrisuru-tests#{ENV['TEST_ENV_NUMBER'] || ''}"
8
10
  merge_timeout 2400
9
11
 
@@ -20,6 +20,11 @@ class TestHosts
20
20
  hosts(name)
21
21
  end
22
22
 
23
+ def resolve(hostname)
24
+ s = Socket.getaddrinfo(hostname, nil)
25
+ s[0][2]
26
+ end
27
+
23
28
  def spec_dir(host_json)
24
29
  random_string = SecureRandom.hex
25
30
  "#{host_json['home']}/.kanrisuru_spec_files_#{random_string[0..5]}"
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+ require 'spec_helper'
5
+
6
+ RSpec.describe Kanrisuru::Remote::Cluster do
7
+ let(:host1) do
8
+ Kanrisuru::Remote::Host.new(
9
+ host: TestHosts.host('ubuntu')['hostname'],
10
+ username: TestHosts.host('ubuntu')['username'],
11
+ keys: [TestHosts.host('ubuntu')['ssh_key']]
12
+ )
13
+ end
14
+
15
+ let(:host2) do
16
+ Kanrisuru::Remote::Host.new(
17
+ host: TestHosts.host('debian')['hostname'],
18
+ username: TestHosts.host('debian')['username'],
19
+ keys: [TestHosts.host('debian')['ssh_key']]
20
+ )
21
+ end
22
+
23
+ let(:host3) do
24
+ Kanrisuru::Remote::Host.new(
25
+ host: TestHosts.host('centos')['hostname'],
26
+ username: TestHosts.host('centos')['username'],
27
+ keys: [TestHosts.host('centos')['ssh_key']]
28
+ )
29
+ end
30
+
31
+ let(:host4) do
32
+ Kanrisuru::Remote::Host.new(
33
+ host: TestHosts.host('opensuse')['hostname'],
34
+ username: TestHosts.host('opensuse')['username'],
35
+ keys: [TestHosts.host('opensuse')['ssh_key']]
36
+ )
37
+ end
38
+
39
+ it 'gets hostname for cluster' do
40
+ cluster = described_class.new(host1, host2, host3, host4)
41
+ expect(cluster.hostname).to match([
42
+ { host: 'ubuntu-host', result: 'ubuntu-host' },
43
+ { host: 'debian-host', result: 'debian-host' },
44
+ { host: 'centos-host', result: 'centos-host' },
45
+ { host: 'opensuse-host', result: 'opensuse-host' },
46
+ ])
47
+
48
+ cluster.disconnect
49
+ end
50
+
51
+ it 'can ping host cluster' do
52
+ cluster = described_class.new(host1, host2, host3, host4)
53
+ expect(cluster.ping?).to match([
54
+ { host: 'ubuntu-host', result: true },
55
+ { host: 'debian-host', result: true },
56
+ { host: 'centos-host', result: true },
57
+ { host: 'opensuse-host', result: true },
58
+ ])
59
+
60
+ cluster.disconnect
61
+ end
62
+
63
+ it 'should use specific number of threads as number of hosts' do
64
+ cluster = described_class.new(host1, host2, host3, host4)
65
+ cluster.parallel = true
66
+
67
+ ## Called x2 b/c two methods invoke the parallel runner
68
+ expect(Thread).to receive(:new).exactly(cluster.count * 2).times.and_call_original
69
+
70
+ cluster.hostname
71
+ cluster.disconnect
72
+ end
73
+
74
+ it 'should respect concurrency setting' do
75
+ concurrency = 2
76
+
77
+ cluster = described_class.new(host1, host2, host3, host4)
78
+ cluster.parallel = true
79
+ cluster.concurrency = concurrency
80
+
81
+ ## Called x2 b/c two methods invoke the parallel runner
82
+ expect(Thread).to receive(:new).exactly(concurrency * 2).times.and_call_original
83
+
84
+ cluster.hostname
85
+ cluster.disconnect
86
+ end
87
+
88
+ it 'saves time running cluster in parallel mode' do
89
+ cluster = described_class.new(host1, host2, host3, host4)
90
+ time1 = Benchmark.measure {
91
+ cluster.each do |host|
92
+ expect(host.pwd).to be_success
93
+ end
94
+ }
95
+
96
+ cluster.parallel = true
97
+ time2 = Benchmark.measure {
98
+ cluster.each do |host|
99
+ expect(host.pwd).to be_success
100
+ end
101
+ }
102
+
103
+ expect(time1.total).to be > time2.total
104
+ cluster.disconnect
105
+ end
106
+
107
+ it 'disconnects all hosts' do
108
+ cluster = described_class.new(host1, host2, host3, host4)
109
+ cluster.disconnect
110
+
111
+ cluster.each do |host|
112
+ expect(host.ssh).to be_closed
113
+ end
114
+ end
115
+ end
@@ -103,7 +103,7 @@ RSpec.shared_examples 'system' do |os_name, host_json, _spec_dir|
103
103
  it 'gets kernel sysctl info' do
104
104
  result = host.sysctl
105
105
  expect(result).to be_success
106
- expect(result).to respond_to :net
106
+ expect(result).to respond_to :net
107
107
  expect(result).to respond_to :kernel
108
108
  expect(result).to respond_to :dev
109
109
 
@@ -24,6 +24,45 @@ RSpec.shared_examples 'host' do |os_name, host_json, _spec_dir|
24
24
  expect(host.hostname).to eq(host_json['hostname'])
25
25
  end
26
26
 
27
+ it 'connects with proxy host' do
28
+ config = TestHosts.host('proxy')
29
+ proxy = Kanrisuru::Remote::Host.new(
30
+ host: config['hostname'],
31
+ username: config['username'],
32
+ keys: [config['ssh_key']]
33
+ )
34
+
35
+ expect(proxy).to be_ping
36
+
37
+ host = Kanrisuru::Remote::Host.new(
38
+ host: TestHosts.resolve(host_json['hostname']),
39
+ username: host_json['username'],
40
+ keys: [host_json['ssh_key']],
41
+ proxy: proxy
42
+ )
43
+
44
+ ## Test instantiation
45
+ expect(host.proxy).to be_instance_of(Net::SSH::Gateway)
46
+ expect(host.ssh).to be_instance_of(Net::SSH::Connection::Session)
47
+
48
+ ## Test basic commands
49
+ expect(host.hostname).to eq(host_json['hostname'])
50
+
51
+ host.cd('../')
52
+ expect(host.pwd.path).to eq('/home')
53
+
54
+ expect(host.whoami.user).to eq(host_json['username'])
55
+ host.su 'root'
56
+ expect(host.whoami.user).to eq('root')
57
+
58
+ ## Test download
59
+ src_path = '/etc/hosts'
60
+ result = host.download(src_path)
61
+ expect(result).to be_instance_of(String)
62
+ lines = result.split("\n")
63
+ expect(lines.length).to be >= 1
64
+ end
65
+
27
66
  it 'changes directories' do
28
67
  expect(host.pwd.path).to eq(host_json['home'])
29
68
 
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Kanrisuru::ProcessorCount do
6
+ it 'responds to methods' do
7
+ expect(described_class).to respond_to(:processor_count)
8
+ expect(described_class).to respond_to(:physical_processor_count)
9
+ end
10
+ end
@@ -26,11 +26,16 @@ RSpec.describe Kanrisuru::Remote::Cluster do
26
26
  expect(cluster).to respond_to(:execute)
27
27
  expect(cluster).to respond_to(:execute_shell)
28
28
  expect(cluster).to respond_to(:each)
29
+ expect(cluster).to respond_to(:map)
29
30
  expect(cluster).to respond_to(:hostname)
30
31
  expect(cluster).to respond_to(:ping?)
31
32
  expect(cluster).to respond_to(:su)
32
33
  expect(cluster).to respond_to(:chdir)
33
34
  expect(cluster).to respond_to(:cd)
34
35
  expect(cluster).to respond_to(:disconnect)
36
+ expect(cluster).to respond_to(:parallel)
37
+ expect(cluster).to respond_to(:concurrency)
38
+ expect(cluster).to respond_to(:parallel?)
39
+ expect(cluster).to respond_to(:sequential?)
35
40
  end
36
41
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Kanrisuru::Remote::Host do
4
+ before(:all) do
5
+ StubNetwork.stub!
6
+ end
7
+
8
+ after(:all) do
9
+ StubNetwork.unstub!
10
+ end
11
+
12
+ let(:host) do
13
+ described_class.new(
14
+ host: 'localhost',
15
+ username: 'ubuntu',
16
+ keys: ['id_rsa']
17
+ )
18
+ end
19
+
20
+ it 'responds to methods' do
21
+ expect(host).to respond_to(:remote_user)
22
+ expect(host).to respond_to(:hostname)
23
+ expect(host).to respond_to(:os)
24
+ expect(host).to respond_to(:env)
25
+ expect(host).to respond_to(:template)
26
+ expect(host).to respond_to(:fstab)
27
+ expect(host).to respond_to(:chdir)
28
+ expect(host).to respond_to(:cd)
29
+ expect(host).to respond_to(:cpu)
30
+ expect(host).to respond_to(:memory)
31
+ expect(host).to respond_to(:su)
32
+ expect(host).to respond_to(:file)
33
+ expect(host).to respond_to(:ssh)
34
+ expect(host).to respond_to(:proxy)
35
+ expect(host).to respond_to(:ping?)
36
+ expect(host).to respond_to(:execute)
37
+ expect(host).to respond_to(:execute_shell)
38
+ expect(host).to respond_to(:disconnect)
39
+ end
40
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kanrisuru
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.4
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Mammina
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-19 00:00:00.000000000 Z
11
+ date: 2022-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parallel_tests
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: '6.1'
139
+ - !ruby/object:Gem::Dependency
140
+ name: net-ssh-gateway
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '2.0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '2.0'
139
153
  description: " Kanrisuru helps manage remote servers with objected oriented ruby.
140
154
  \n Results come back as structured data, parsed, prepared and ready.\n"
141
155
  email: ryan@avamia.com
@@ -426,6 +440,7 @@ files:
426
440
  - lib/kanrisuru/os_package/collection.rb
427
441
  - lib/kanrisuru/os_package/define.rb
428
442
  - lib/kanrisuru/os_package/include.rb
443
+ - lib/kanrisuru/processor_count.rb
429
444
  - lib/kanrisuru/remote.rb
430
445
  - lib/kanrisuru/remote/cluster.rb
431
446
  - lib/kanrisuru/remote/cpu.rb
@@ -472,6 +487,7 @@ files:
472
487
  - spec/functional/core/user_spec.rb
473
488
  - spec/functional/core/yum_spec.rb
474
489
  - spec/functional/core/zypper_spec.rb
490
+ - spec/functional/processor_count_spec.rb
475
491
  - spec/functional/remote/cluster_spec.rb
476
492
  - spec/functional/remote/cpu_spec.rb
477
493
  - spec/functional/remote/env_spec.rb
@@ -588,7 +604,7 @@ files:
588
604
  - spec/integration/core/zypper/opensuse_spec.rb
589
605
  - spec/integration/core/zypper/sles_spec.rb
590
606
  - spec/integration/os_package_spec.rb
591
- - spec/integration/remote/cluster/ubuntu_spec.rb
607
+ - spec/integration/remote/cluster/cluster_spec.rb
592
608
  - spec/integration/remote/cpu/centos_spec.rb
593
609
  - spec/integration/remote/cpu/debian_spec.rb
594
610
  - spec/integration/remote/cpu/fedora_spec.rb
@@ -656,7 +672,6 @@ files:
656
672
  - spec/support/shared_examples/integration/core/user.rb
657
673
  - spec/support/shared_examples/integration/core/yum.rb
658
674
  - spec/support/shared_examples/integration/core/zypper.rb
659
- - spec/support/shared_examples/integration/remote/cluster.rb
660
675
  - spec/support/shared_examples/integration/remote/cpu.rb
661
676
  - spec/support/shared_examples/integration/remote/env_spec.rb
662
677
  - spec/support/shared_examples/integration/remote/fstab.rb
@@ -683,10 +698,12 @@ files:
683
698
  - spec/unit/core/zypper_spec.rb
684
699
  - spec/unit/kanrisuru_spec.rb
685
700
  - spec/unit/mode_spec.rb
701
+ - spec/unit/processor_count_spec.rb
686
702
  - spec/unit/remote/cluster_spec.rb
687
703
  - spec/unit/remote/cpu_spec.rb
688
704
  - spec/unit/remote/env_spec.rb
689
705
  - spec/unit/remote/fstab_spec.rb
706
+ - spec/unit/remote/host_spec.rb
690
707
  - spec/unit/template_spec.rb
691
708
  - spec/unit/util_spec.rb
692
709
  homepage: https://kanrisuru.com
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- TestHosts.each_os(only: %w[ubuntu]) do |os_name, host_json, spec_dir|
6
- RSpec.describe Kanrisuru::Core::Disk do
7
- include_examples 'disk', os_name, host_json, spec_dir
8
- end
9
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.shared_examples 'cluster' do |os_name, host_json, _spec_dir|
6
- context "with #{os_name}" do
7
- let(:host1) do
8
- Kanrisuru::Remote::Host.new(
9
- host: host_json['hostname'],
10
- username: host_json['username'],
11
- keys: [host_json['ssh_key']]
12
- )
13
- end
14
-
15
- let(:host2) do
16
- Kanrisuru::Remote::Host.new(
17
- host: 'localhost',
18
- username: 'ubuntu',
19
- keys: ['~/.ssh/id_rsa']
20
- )
21
- end
22
-
23
- it 'gets hostname for cluster' do
24
- cluster = described_class.new({
25
- host: 'localhost',
26
- username: 'ubuntu',
27
- keys: ['~/.ssh/id_rsa']
28
- }, {
29
- host: '127.0.0.1',
30
- username: 'ubuntu',
31
- keys: ['~/.ssh/id_rsa']
32
- })
33
-
34
- expect(cluster.hostname).to match([
35
- { host: 'localhost', result: 'ubuntu' },
36
- { host: '127.0.0.1', result: 'ubuntu' }
37
- ])
38
-
39
- cluster.disconnect
40
- end
41
-
42
- it 'can ping host cluster' do
43
- cluster = described_class.new({
44
- host: 'localhost',
45
- username: 'ubuntu',
46
- keys: ['~/.ssh/id_rsa']
47
- }, {
48
- host: '127.0.0.1',
49
- username: 'ubuntu',
50
- keys: ['~/.ssh/id_rsa']
51
- })
52
-
53
- expect(cluster.ping?).to match([
54
- { host: 'localhost', result: true },
55
- { host: '127.0.0.1', result: true }
56
- ])
57
-
58
- cluster.disconnect
59
- end
60
-
61
- it 'disconnects all hosts' do
62
- cluster = described_class.new(host1, host2)
63
- cluster.disconnect
64
-
65
- cluster.each do |host|
66
- expect(host.ssh.closed).to be_truthy
67
- end
68
- end
69
- end
70
- end