the_runner 0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c174cbb0b588944e2b98a96456888524377408f3
4
+ data.tar.gz: 4eec30802e33873199b4a9e91ddf9e7adb4c68b9
5
+ SHA512:
6
+ metadata.gz: 28463bca208e35d65aada635106110c63200e35f58b25c4795fec1ac760cd9b1ffe248b25fca2c63943157483d7a4b30b2c67187d505593597c65eff9548cd08
7
+ data.tar.gz: b6ad5aed807acae81355452f7f7c65e38ba7ffa113667537ced5d1adbaab375e1c21bb894ac4bc2c18da838f060f81429838bdcf2e1a9aafa2beb7dab147b3d5
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2016 Leszek Stachowski <leszek@stachowski.me>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,138 @@
1
+ # The Runner
2
+
3
+ ## Disclaimer
4
+
5
+ This version is just a rough proof of concept. There is basically no error handling, no argument type checking, no docs,
6
+ no tests. I just wanted to have my own solution to run commands across multiple servers (ideally in parallel) and that's how
7
+ this project started. If you somehow come across the library and use it, don't hesitate to open bug / improvement tickets or
8
+ send me a PR.
9
+
10
+ ## Overview
11
+
12
+ The Runner is a ruby library that provides a solution for running ssh commands across multiple servers. It's supposed to be
13
+ highly extendable and configurable. It lets you also run commands in parallel.
14
+
15
+ ## Installation
16
+
17
+ gem install the_runner
18
+
19
+ ## Usage
20
+
21
+ ### Direct
22
+
23
+ This direct usage example you can try using `irb` or by embedding it into ruby file.
24
+
25
+ ```ruby
26
+ require 'the_runner'
27
+
28
+ the_runner = TheRunner.new
29
+ the_runner['default'] = TheRunner::Context.new do |context|
30
+ context.add_server('host.example.com')
31
+ context.command = 'echo "Hello World!"'
32
+ end
33
+ the_runner.run('default')
34
+ ```
35
+
36
+ ### Therunnerfile
37
+
38
+ The Runner supports so called `Therunnerfile`. Below you can find contents of `Therunnerfile` which is part of the library for
39
+ demonstration purposes. It contains almost all possible configuration options out there.
40
+
41
+ ```ruby
42
+
43
+ TheRunner.context("example-ls") do |context|
44
+ # Adding single server
45
+ context.add_server('host.example.com')
46
+ # Command to run on specified server
47
+ context.command = 'ls -al'
48
+ end
49
+
50
+ TheRunner.context("example-ls-multiple") do |context|
51
+ context.add_server('host.example.com')
52
+ # Adding another server, command will be executed sequentially
53
+ context.add_server('another.example.com')
54
+ context.command = 'ls -al'
55
+ end
56
+
57
+ TheRunner.context("example-ls-multiple-add-servers") do |context|
58
+ # Instead of calling add_server twice, you can call add_servers once with array of servers
59
+ context.add_servers(['host.example.com', 'another.example.com'])
60
+ context.command = 'ls -al'
61
+ end
62
+
63
+ TheRunner.context("example-server-config") do |context|
64
+ # Both add_servers and add_server will yield a server config if you provide a block. In this
65
+ # case both servers will share the same config.
66
+ context.add_servers(['host.example.com', 'another.example.com']) { |config|
67
+ config.runner_options = {
68
+ # Will disable key checks while connecting to server (default: false) by
69
+ # prepending '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
70
+ :disable_key_checking => true,
71
+ # Will use this username while connecting to server (default: nil, no username
72
+ # would be used)
73
+ :user => 'root',
74
+ # Will enable ssh debugging by adding '-vvv' to the command
75
+ :debug => true,
76
+ }
77
+ }
78
+ context.command = 'ls -al'
79
+ end
80
+
81
+ TheRunner.context("example-mutiple-configs") do |context|
82
+ # We specify one host with specific config
83
+ context.add_server('host.example.com') { |config|
84
+ config.runner_options = {
85
+ :user => 'root',
86
+ }
87
+ }
88
+
89
+ # Another host with different config
90
+ context.add_server('another.example.com') { |config|
91
+ config.runner_options = {
92
+ :debug => true,
93
+ }
94
+ }
95
+
96
+ # And yet another host without any config (which means: default one)
97
+ context.add_server('yet-another.example.com')
98
+ context.command = 'ls -al'
99
+ end
100
+
101
+ TheRunner.context("example-parallel") do |context|
102
+ context.add_server('host.example.com') { |config|
103
+ config.runner_options = {
104
+ :user => 'root',
105
+ }
106
+ }
107
+ context.add_server('another.example.com') { |config|
108
+ config.runner_options = {
109
+ :debug => true,
110
+ }
111
+ }
112
+ context.add_server('yet-another.example.com')
113
+ context.command = 'ls -al'
114
+ # Parallel option means that the specified command will be executed in parallel
115
+ # (in multiple threads) instead of sequentially.
116
+ context.config.parallel = true
117
+ end
118
+
119
+ # Default context
120
+ TheRunner.context do |context|
121
+ context.add_server('host.example.com')
122
+ context.command = 'uptime'
123
+ end
124
+ ```
125
+
126
+ And run command in following format:
127
+
128
+ the_runner <context name>
129
+
130
+ for example:
131
+
132
+ the_runner example-ls
133
+
134
+ inside directory containing the file to execute The Runner in "example-ls" context from the example `Therunnerfile`.
135
+
136
+ Things to remember:
137
+ * If you specify only one context, it will be by executed always by default, no matter what you specify on command line.
138
+ * If you don't specify <context name> on command line, "default" will be used if provided.
@@ -0,0 +1,81 @@
1
+ TheRunner.context("example-ls") do |context|
2
+ # Adding single server
3
+ context.add_server('host.example.com')
4
+ # Command to run on specified server
5
+ context.command = 'ls -al'
6
+ end
7
+
8
+ TheRunner.context("example-ls-multiple") do |context|
9
+ context.add_server('host.example.com')
10
+ # Adding another server, command will be executed sequentially
11
+ context.add_server('another.example.com')
12
+ context.command = 'ls -al'
13
+ end
14
+
15
+ TheRunner.context("example-ls-multiple-add-servers") do |context|
16
+ # Instead of calling add_server twice, you can call add_servers once with array of servers
17
+ context.add_servers(['host.example.com', 'another.example.com'])
18
+ context.command = 'ls -al'
19
+ end
20
+
21
+ TheRunner.context("example-server-config") do |context|
22
+ # Both add_servers and add_server will yield a server config if you provide a block. In this
23
+ # case both servers will share the same config.
24
+ context.add_servers(['host.example.com', 'another.example.com']) { |config|
25
+ config.runner_options = {
26
+ # Will disable key checks while connecting to server (default: false) by
27
+ # prepending '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
28
+ :disable_key_checking => true,
29
+ # Will use this username while connecting to server (default: nil, no username
30
+ # would be used)
31
+ :user => 'root',
32
+ # Will enable ssh debugging by adding '-vvv' to the command
33
+ :debug => true,
34
+ }
35
+ }
36
+ context.command = 'ls -al'
37
+ end
38
+
39
+ TheRunner.context("example-mutiple-configs") do |context|
40
+ # We specify one host with specific config
41
+ context.add_server('host.example.com') { |config|
42
+ config.runner_options = {
43
+ :user => 'root',
44
+ }
45
+ }
46
+
47
+ # Another host with different config
48
+ context.add_server('another.example.com') { |config|
49
+ config.runner_options = {
50
+ :debug => true,
51
+ }
52
+ }
53
+
54
+ # And yet another host without any config (which means: default one)
55
+ context.add_server('yet-another.example.com')
56
+ context.command = 'ls -al'
57
+ end
58
+
59
+ TheRunner.context("example-parallel") do |context|
60
+ context.add_server('host.example.com') { |config|
61
+ config.runner_options = {
62
+ :user => 'root',
63
+ }
64
+ }
65
+ context.add_server('another.example.com') { |config|
66
+ config.runner_options = {
67
+ :debug => true,
68
+ }
69
+ }
70
+ context.add_server('yet-another.example.com')
71
+ context.command = 'ls -al'
72
+ # Parallel option means that the specified command will be executed in parallel
73
+ # (in multiple threads) instead of sequentially.
74
+ context.config.parallel = true
75
+ end
76
+
77
+ # Default context
78
+ TheRunner.context do |context|
79
+ context.add_server('host.example.com')
80
+ context.command = 'uptime'
81
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if File.exists?('Therunnerfile')
4
+ require 'the_runner'
5
+ require 'the_runner/therunnerfile/handler'
6
+ load './Therunnerfile'
7
+ # TODO check if ARGV[0] exists and if not - display usage help
8
+ TheRunner::Therunnerfile::Handler.new.handle(ARGV[0])
9
+ else
10
+ # TODO dedicated error
11
+ raise StandardError.new('No Therunnerfile found.')
12
+ end
@@ -0,0 +1,51 @@
1
+ require 'the_runner/command'
2
+ require 'the_runner/server'
3
+ require 'the_runner/server/config'
4
+ require 'the_runner/context'
5
+ require 'the_runner/context/config'
6
+ require 'the_runner/runner/base'
7
+ require 'the_runner/runner/ssh'
8
+
9
+ class TheRunner
10
+ def initialize
11
+ @contexts = {}
12
+ end
13
+
14
+ # TODO we don't always need the key, mostly when we want to run a single context
15
+ def []=(key, context)
16
+ @contexts[key] = context
17
+ end
18
+
19
+ # TODO make constant for 'default', it's used in multiple places
20
+ def run(key = 'default')
21
+ if @contexts.count == 1
22
+ run_context(@contexts.first[1])
23
+ else
24
+ # TODO check if the key exists
25
+ run_context(@contexts[key])
26
+ end
27
+ end
28
+
29
+ protected
30
+ def run_context(context)
31
+ if context.config.parallel?
32
+ threads = []
33
+ context.servers.each { |server|
34
+ threads << Thread.new {
35
+ run_command_from_context_on_server(context, server)
36
+ }
37
+ }
38
+ threads.each do |thread|
39
+ thread.join
40
+ end
41
+ else
42
+ context.servers.each do |server|
43
+ run_command_from_context_on_server(context, server)
44
+ end
45
+ end
46
+ end
47
+
48
+ def run_command_from_context_on_server(context, server)
49
+ context.get_runner_for(server).run(Command.new(context.command))
50
+ end
51
+ end
@@ -0,0 +1,11 @@
1
+ class TheRunner
2
+ class Command
3
+ def initialize(command)
4
+ @command = command
5
+ end
6
+
7
+ def to_s
8
+ @command
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,48 @@
1
+ require 'logger'
2
+
3
+ class TheRunner
4
+ class Context
5
+ attr_accessor :command, :config, :servers
6
+ def initialize(logger = nil)
7
+ @servers = []
8
+ @command = ''
9
+ @server_configs = {}
10
+ @config = TheRunner::Context::Config.new
11
+ @logger = Logger.new(STDOUT) if logger.nil?
12
+
13
+ yield self if block_given?
14
+ end
15
+ def add_server(server)
16
+ server = Server.new(server) unless server.is_a?(Server)
17
+ @servers << server
18
+
19
+ if block_given?
20
+ config = Config.new
21
+ yield config
22
+
23
+ @server_configs[server] = config
24
+ end
25
+ end
26
+ def add_servers(servers)
27
+ servers.each do |server|
28
+ if block_given?
29
+ add_server(server) { |config|
30
+ yield config
31
+ }
32
+ else
33
+ add_server(server)
34
+ end
35
+ end
36
+ end
37
+ def get_runner_for(server)
38
+ if has_specific_config_for?(server)
39
+ @config.runner.new(server, @server_configs[server].runner_options, @logger)
40
+ else
41
+ @config.runner.new(server, @config.runner_options, @logger)
42
+ end
43
+ end
44
+ def has_specific_config_for?(server)
45
+ @server_configs.has_key?(server)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ class TheRunner
2
+ class Context
3
+ # TODO Not quite sure about that inheritance
4
+ class Config < TheRunner::Server::Config
5
+ attr_accessor :parallel
6
+ attr_reader :runner
7
+ def initialize
8
+ super
9
+ @parallel = false
10
+ @runner = TheRunner::Runner::SSH
11
+ end
12
+ def runner=(runner)
13
+ # TODO Check if class exists or if it's already a the_runner
14
+ @runner = runner
15
+ end
16
+ def parallel?
17
+ @parallel
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ class TheRunner
2
+ class Runner
3
+ class Base
4
+ @@default_options = {}
5
+
6
+ # TODO Not quite sure about logger injection here.
7
+ def initialize(server, options = {}, logger = nil)
8
+ @server, @options = server, @@default_options.merge(options)
9
+ @logger = logger
10
+ end
11
+ def log(message)
12
+ # TODO Of course there should be more levels of log than just debug.
13
+ @logger.debug(sprintf('[%s] %s', @server.hostname, message)) unless @logger.nil?
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,48 @@
1
+ require 'open3'
2
+
3
+ class TheRunner
4
+ class Runner
5
+ class SSH < Base
6
+ @@default_options = {
7
+ debug: false,
8
+ disable_key_checking: false,
9
+ user: nil,
10
+ }
11
+ def run(command)
12
+ system_args = build_system_args(command)
13
+ command = system_args.join(' ')
14
+ log(sprintf('Running [%s]', command))
15
+
16
+ Open3.popen2e(command) { |stdin, output|
17
+ output.each_line { |line|
18
+ log(line.chomp)
19
+ }
20
+ }
21
+ end
22
+ protected
23
+ def user
24
+ if @options[:user].nil?
25
+ args << @server.hostname
26
+ else
27
+ args << "#{@options[:user]}@#{@server.hostname}"
28
+ end
29
+ end
30
+ def build_system_args(command)
31
+ args = []
32
+ args << 'ssh'
33
+ if @options[:debug]
34
+ args << '-vvv'
35
+ end
36
+ if @options[:disable_key_checking]
37
+ args << '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
38
+ end
39
+ if @options[:user].nil?
40
+ args << @server.hostname
41
+ else
42
+ args << "#{@options[:user]}@#{@server.hostname}"
43
+ end
44
+ args << command.to_s
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,8 @@
1
+ class TheRunner
2
+ class Server
3
+ attr_reader :hostname
4
+ def initialize(hostname)
5
+ @hostname = hostname
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ class TheRunner
2
+ class Server
3
+ class Config
4
+ attr_accessor :runner_options
5
+ def initialize
6
+ @runner_options = {}
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ class TheRunner
2
+ def self.context(key = 'default')
3
+ context = TheRunner::Context.new
4
+ yield context
5
+ TheRunner::Therunnerfile::Handler.add_context(key, context)
6
+ end
7
+ class Therunnerfile
8
+ class Handler
9
+ @@contexts = {}
10
+ def self.add_context(key, context)
11
+ @@contexts[key] = context
12
+ end
13
+ def handle(key)
14
+ key = 'default' if key.nil?
15
+ the_runner = TheRunner.new
16
+ @@contexts.each { |key, value|
17
+ the_runner[key] = value
18
+ }
19
+ the_runner.run(key)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.authors = ['Leszek Stachowski']
3
+ spec.description = %q{Library for running commands on multiple servers.}
4
+ spec.email = 'leszek@stachowski.me'
5
+ spec.executables = %w(the_runner)
6
+ spec.files = %w(LICENSE.md README.md Therunnerfile the_runner.gemspec)
7
+ spec.files += Dir.glob("bin/**/*")
8
+ spec.files += Dir.glob("lib/**/*.rb")
9
+ spec.homepage = 'https://github.com/shazarre/the_runner'
10
+ spec.licenses = ['MIT']
11
+ spec.name = 'the_runner'
12
+ spec.require_paths = ['lib']
13
+ spec.required_rubygems_version = '>= 1.3.6'
14
+ spec.summary = spec.description
15
+ spec.version = '0.1'
16
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: the_runner
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Leszek Stachowski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-08-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Library for running commands on multiple servers.
14
+ email: leszek@stachowski.me
15
+ executables:
16
+ - the_runner
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.md
21
+ - README.md
22
+ - Therunnerfile
23
+ - bin/the_runner
24
+ - lib/the_runner.rb
25
+ - lib/the_runner/command.rb
26
+ - lib/the_runner/context.rb
27
+ - lib/the_runner/context/config.rb
28
+ - lib/the_runner/runner/base.rb
29
+ - lib/the_runner/runner/ssh.rb
30
+ - lib/the_runner/server.rb
31
+ - lib/the_runner/server/config.rb
32
+ - lib/the_runner/therunnerfile/handler.rb
33
+ - the_runner.gemspec
34
+ homepage: https://github.com/shazarre/the_runner
35
+ licenses:
36
+ - MIT
37
+ metadata: {}
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 1.3.6
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 2.4.5.1
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Library for running commands on multiple servers.
58
+ test_files: []