grifork 0.3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 39cd6c990f5c21321b737f9c12c7651ff843a7a2
4
+ data.tar.gz: 5a02758138e9f35fada790135e896d7bcea1a249
5
+ SHA512:
6
+ metadata.gz: 5c87db79f590d4356a7e4c6225cd8f546895e0ec3ebcbba02c45bfe6e742006d2726ff523c22a00ff19aa3d747a0416adf576c0b3bdf1ef9c65c8460f2f28c07
7
+ data.tar.gz: 6dfbb038e924db9aa075c4ea212d15c41d32ec6c4ccbe4143dbcb89488a3b6c185178c69b93a74d659c851965233e0c18e560b0c4a59fc76547fc6d1a503825e
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ *.swp
10
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ rvm:
2
+ - 2.0.0
3
+ - 2.1.8
4
+ - 2.2.0
5
+
6
+ install:
7
+ - gem install bundler -v 1.12.5
8
+ - bundle _1.12.5_ install
9
+
10
+ script:
11
+ - bundle _1.12.5_ exec rake spec
12
+
13
+ notifications:
14
+ slack:
15
+ rooms:
16
+ secure: FahF4aDqUG6GNTurc3lYmaudpB25dgEQmd2HCXMU5o9MHsMa5UMNX1yP5vykhW+n9zGBq5yJKOXZeVlJsVSFGuPOjrFXUqbc75RO1sjov0r7vPb59mDRTOTpiodBwMBwdrATokBNnzZGujN8C5LoLpFXlPehCvoEfZfFS5tuCq9CE9UOXdapVArBwM/UkBaLgkSLi+JHb3fYKcZXsHkZ6SB1RdJKONbbHFW+7TuuWagXjEE80oLHKMClLedUXhg5uC9f39aqUar5PawPjR3r6pMMXtBsbeZylszL6Xzx2Bt6TRB5cE+479zpfbvre8NvbZ4HJDZgHto+hUo9RyyzxxJU6QIdweU4H0LiKBtFeSqpTcWTf/8h9OL6I7TviGyQ1PdX/jWhoJwfhGhy1yXE7ZLNI6kczb9DHDdMK+aUOvuFiPBWyUrPSsqfDm0jRAWfiUqzdxrn8B7N3C8W99u6gYkhC/Zy8j7mLjH8NnAPcBS8cd/hH9qWtJ8v9Fr3dhEPsKjiGU61cVk2OFOxJ2YXttNe3iqOV3hKS86632jkIEICllT4ZYNClNi0znS7J6kuQYYwDdaqSYY+jTFIAyeV4+toHGNcTg9fb5s81XwfKgg5E8LwiWIWFr+/HMsh0RkaaIaQInsLTIBTnARUwM+BYWoaZ/FGrCjFH8mExBI/F0M=
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ ## 0.3.0 (2016/10/4)
2
+
3
+ Release as a RubyGem.
4
+
5
+ ## 0.2.0 (2016/10/2)
6
+
7
+ Feature:
8
+
9
+ - Implement "grifork" mode to exec "grifork" command in parallel and recursively
10
+ among target hosts #1
11
+
12
+ ## 0.1.0 (2016/9/28)
13
+
14
+ Initial release.
15
+
16
+ Feature:
17
+
18
+ - Implement "standalone" mode to run defined tasks for target hosts
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in grifork.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 IKEDA Kiyoshi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ [![Build Status](https://travis-ci.org/key-amb/grifork.svg?branch=master)](https://travis-ci.org/key-amb/grifork)
2
+
3
+ # grifork
4
+
5
+ Fast propagative task runner for systems which consist of a lot of servers.
6
+
7
+ # Concept
8
+
9
+ **Grifork** runs defined tasks on the system in a way like tree's branching.
10
+ Give **grifork** a list of hosts, then it creates a tree graph internally, and runs
11
+ tasks in a top-down way.
12
+
13
+ **Grifork** has two modes to work:
14
+
15
+ 1. **Standalone** mode. This requires **grifork** program only on the root server
16
+ in the tree graph: i.e. the server which invokes tasks.
17
+ 1. **Grifork** mode. On the other hand, this requires **grifork** program to work
18
+ on every node in the graph.
19
+
20
+ Take a look at each mechanism.
21
+
22
+ ## Standalone Mode
23
+
24
+ The image below illustrates a 3-depth tree of 13 nodes including root node.
25
+
26
+ ![standalone mode](https://raw.githubusercontent.com/key-amb/grifork/resource/images/standalone_mode2.png)
27
+
28
+ We are running a task to copy a file tree to every host.
29
+ 1st stage is completed: to copy them to nodes in 1st generation.
30
+
31
+ Now at 2nd stage, root node logins each of its children by _ssh_ and kicks _rsync_
32
+ program there, in order to copy the file tree from 1st to 2nd generation.
33
+
34
+ NOTE:
35
+
36
+ - Max concurrency of running task in **standalone** mode is the number of nodes
37
+ at the generation which holds max.
38
+ This is the last genaration or the genartion before the last.
39
+
40
+ ## Grifork Mode
41
+
42
+ The image below is similar to previous situation except that this is in **grifork** mode.
43
+
44
+ ![grifork mode](https://raw.githubusercontent.com/key-amb/grifork/resource/images/grifork_mode2.png)
45
+
46
+ In this mode, parent nodes in the graph invokes _grifork_ command on every child
47
+ node via _ssh_, giving the graph tree which descends from each child node.
48
+
49
+ # System Requirements
50
+
51
+ - Ruby v2
52
+ - ssh, rsync
53
+
54
+ # Installation
55
+
56
+ ```sh
57
+ git clone https://github.com/key-amb/grifork.git
58
+ cd grifork
59
+ bundle
60
+ ```
61
+
62
+ # Usage
63
+
64
+ ```sh
65
+ edit Griforkfile
66
+ ./bin/grifork [--[f]ile path/to/Griforkfile]
67
+ ```
68
+
69
+ See [example](example) directory for sample of Griforkfiles.
70
+
71
+ # Authors
72
+
73
+ IKEDA Kiyoshi <yasutake.kiyoshi@gmail.com>
74
+
75
+ # License
76
+
77
+ The MIT License (MIT)
78
+
79
+ Copyright (c) 2016 IKEDA Kiyoshi
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc 'Open a pry session preloaded with the library'
4
+ task :console do
5
+ sh 'pry --gem'
6
+ end
7
+ task :c => :console
8
+
9
+ require 'rspec/core'
10
+ require 'rspec/core/rake_task'
11
+ RSpec::Core::RakeTask.new(:spec) do |spec|
12
+ spec.pattern = FileList['spec/**/*_spec.rb']
13
+ end
14
+ task :default => :spec
15
+
16
+ namespace :git do
17
+ desc 'Bump git tag and release to origin'
18
+ task :bump do
19
+ require_relative 'lib/grifork'
20
+ version = Grifork::VERSION
21
+ sh "git commit -m #{version}"
22
+ sh "git tag -a v#{version} -m #{version}"
23
+ sh "git push origin master"
24
+ sh "git push origin v#{version}"
25
+ end
26
+ end
data/bin/grifork ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/grifork'
4
+
5
+ Grifork::CLI.new.run(ARGV)
@@ -0,0 +1,33 @@
1
+ # Configuration
2
+ #mode :grifork
3
+ branches 4
4
+ log file: 'tmp/grifork.log', level: 'debug'
5
+
6
+ # Setting to exec grifork on remote
7
+ # Implies to set mode as :grifork
8
+ grifork do
9
+ user 'someone' # ssh user to exec grifork on remote
10
+ chdir '/path/to/your-app'
11
+ tmpdir '/path/to/tmpdir'
12
+ exec '/path/to/grifork'
13
+ end
14
+
15
+ # Define hosts as array
16
+ hosts ['web1.internal', 'web2.internal', '192.168.10.1', '192.168.10.2']
17
+
18
+ # Define task run on localhost
19
+ local do
20
+ sh :echo, %W(LOCAL: #{src} => #{dst})
21
+ ssh dst, :mkdir, %W(-p /path/to/dest), user: 'someone'
22
+ rsync '/path/to/src/', '/path/to/dest/'
23
+ end
24
+
25
+ # Define task run on remote hosts
26
+ # NOTE: This task is run as "local" task on remote
27
+ # different from "remote" task in :standalone mode
28
+ remote do
29
+ sh :echo, %W(REMOTE: #{src} => #{dst})
30
+ ssh dst, :mkdir, %W(-p /path/to/dest), user: 'someone'
31
+ rsync '/path/to/src/', '/path/to/dest/'
32
+ end
33
+
@@ -0,0 +1,22 @@
1
+ # Configuration
2
+ #mode :standalone # Default mode
3
+ branches 4
4
+ log file: 'tmp/grifork.log', level: 'debug'
5
+
6
+ # Define hosts as array
7
+ hosts ['web1.internal', 'web2.internal', '192.168.10.1', '192.168.10.2']
8
+
9
+ # Define task run on localhost
10
+ local do
11
+ sh :echo, %W(LOCAL: #{src} => #{dst})
12
+ ssh dst, :mkdir, %W(-p /path/to/dest), user: 'someone'
13
+ rsync '/path/to/src/', '/path/to/dest/'
14
+ end
15
+
16
+ # Define task run on remote hosts
17
+ remote do
18
+ sh :echo, %W(REMOTE: #{src} => #{dst})
19
+ ssh dst, :mkdir, %W(-p /path/to/dest)
20
+ rsync_remote '/path/to/src/', '/path/to/dest/', user: 'someone'
21
+ end
22
+
data/grifork.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'grifork/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "grifork"
8
+ spec.version = Grifork::VERSION
9
+ spec.authors = ["key-amb"]
10
+ spec.email = ["yasutake.kiyoshi@gmail.com"]
11
+
12
+ spec.summary = %q{Fast Propagative Task Runner}
13
+ spec.description = %q{Fast propagative task runner for systems which consist of a lot of servers.}
14
+ spec.homepage = "https://github.com/key-amb/grifork"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "bin"
19
+ spec.executables = ["grifork"]
20
+ spec.require_paths = ["lib"]
21
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.0")
22
+
23
+ spec.add_runtime_dependency "net-ssh", "~> 3.0"
24
+ spec.add_runtime_dependency "parallel", "~> 1.9"
25
+ spec.add_runtime_dependency "stdlogger", "~> 0.3.0"
26
+
27
+ spec.add_development_dependency "pry", "~> 0.10"
28
+ spec.add_development_dependency "bundler", "~> 1.12"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec", "~> 3.0"
31
+ end
@@ -0,0 +1,55 @@
1
+ class Grifork::CLI
2
+ def run(argv)
3
+ OptionParser.new do |opt|
4
+ opt.on('-f', '--file Griforkfile') { |f| @taskfile = f }
5
+ opt.on('-o', '--override-by FILE') { |f| @override_file = f }
6
+ opt.on('-r', '--on-remote') { @on_remote = true }
7
+ opt.on('-v', '--version') { @version = true }
8
+ opt.parse!(argv)
9
+ end
10
+ if @version
11
+ puts Grifork::VERSION
12
+ exit
13
+ end
14
+
15
+ config = load_taskfiles.freeze
16
+ Grifork.configure!(config)
17
+
18
+ graph = Grifork::Graph.new(config.hosts)
19
+
20
+ if @on_remote
21
+ puts "Start on remote. Hosts: #{config.hosts}"
22
+ end
23
+
24
+ case config.mode
25
+ when :standalone
26
+ graph.launch_tasks
27
+ when :grifork
28
+ graph.grifork
29
+ else
30
+ # Never comes here
31
+ raise "Unexpected mode! #{config.mode}"
32
+ end
33
+
34
+ if @on_remote
35
+ puts "End on remote."
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def load_taskfiles
42
+ puts "Load settings from #{taskfile}"
43
+ dsl = Grifork::DSL.load_file(taskfile, on_remote: @on_remote)
44
+ if @override_file
45
+ dsl.load_and_merge_config_by!(@override_file)
46
+ end
47
+ config = dsl.to_config
48
+ config.griforkfile = taskfile
49
+ config
50
+ end
51
+
52
+ def taskfile
53
+ @taskfile || ENV['GRIFORKFILE'] || Grifork::DEFAULT_TASKFILE
54
+ end
55
+ end
@@ -0,0 +1,53 @@
1
+ class Grifork::Config
2
+ attr_reader :branches, :hosts, :log, :local_task, :remote_task, :grifork
3
+ attr_accessor :griforkfile
4
+
5
+ def initialize(args)
6
+ args.each do |key, val|
7
+ instance_variable_set("@#{key}", val)
8
+ end
9
+ end
10
+
11
+ def mode
12
+ @mode || :standalone
13
+ end
14
+
15
+ class Log
16
+ attr :file, :level
17
+
18
+ def initialize(args)
19
+ @file = args[:file]
20
+ @level = args[:level] || 'info'
21
+ end
22
+ end
23
+
24
+ class Grifork
25
+ attr :dir, :cmd, :login
26
+
27
+ def initialize(&config)
28
+ instance_eval(&config)
29
+ end
30
+
31
+ def workdir
32
+ @tmpdir || Dir.tmpdir
33
+ end
34
+
35
+ private
36
+
37
+ def user(login)
38
+ @login = login
39
+ end
40
+
41
+ def chdir(path)
42
+ @dir = path
43
+ end
44
+
45
+ def exec(cmd)
46
+ @cmd = cmd
47
+ end
48
+
49
+ def tmpdir(path)
50
+ @tmpdir = path
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,81 @@
1
+ class Grifork::DSL
2
+ attr :config
3
+
4
+ class LoadError < StandardError; end
5
+
6
+ # Load DSL file to object
7
+ # @param path [String]
8
+ # @param on_remote [Boolean] whether process is invoked by remote host in :grifork mode or not
9
+ def self.load_file(path, on_remote: false)
10
+ content = File.binread(path)
11
+ dsl = new(on_remote)
12
+ dsl.instance_eval(content)
13
+ dsl
14
+ end
15
+
16
+ def initialize(on_remote)
17
+ @config = {}
18
+ @on_remote = on_remote
19
+ end
20
+
21
+ def to_config
22
+ Grifork::Config.new(@config)
23
+ end
24
+
25
+ # Load another DSL file and merge its config
26
+ def load_and_merge_config_by!(path)
27
+ content = File.binread(path)
28
+ other = self.class.new(@on_remote)
29
+ other.instance_eval(content)
30
+ @config.merge!(other.config)
31
+ end
32
+
33
+ def mode(m)
34
+ unless Grifork::MODES.has_key?(m)
35
+ raise LoadError, "Undefined mode! #{m}"
36
+ end
37
+ config_set(:mode, m)
38
+ end
39
+
40
+ def grifork(&command)
41
+ if @config[:mode] == :standalone
42
+ raise LoadError, "Can't configure grifork in standalone mode"
43
+ end
44
+ @config[:mode] = :grifork
45
+ config_set(:grifork, Grifork::Config::Grifork.new(&command))
46
+ end
47
+
48
+ def branches(num)
49
+ config_set(:branches, num)
50
+ end
51
+
52
+ def log(args)
53
+ config_set(:log, Grifork::Config::Log.new(args))
54
+ end
55
+
56
+ def hosts(list)
57
+ config_set(:hosts, list)
58
+ end
59
+
60
+ def local(&task)
61
+ return if @on_remote
62
+ config_set(:local_task, Grifork::Executor::Task.new(:local, &task))
63
+ end
64
+
65
+ def remote(&task)
66
+ if @on_remote
67
+ config_set(:local_task, Grifork::Executor::Task.new(:local, &task))
68
+ else
69
+ config_set(:remote_task, Grifork::Executor::Task.new(:remote, &task))
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def config_set(key, value)
76
+ if @config[key]
77
+ raise LoadError, %(Config "#{key}" is already defined!)
78
+ end
79
+ @config[key] = value
80
+ end
81
+ end
@@ -0,0 +1,26 @@
1
+ class Grifork::Executor::Grifork
2
+ include Grifork::Executable
3
+ attr :config
4
+
5
+ # @param cfg [Grifork::Config::Grifork] configured by DSL#grifork
6
+ def initialize(cfg)
7
+ @config = cfg
8
+ end
9
+
10
+ # Run grifork command on remote node:
11
+ # 1. Create Griforkfile and copy it to remote
12
+ # 2. ssh remote host and exec grifork
13
+ def run(node)
14
+ c = config
15
+ ssh node.host, %(test -d "#{c.workdir}" || mkdir -p "#{c.workdir}"), user: c.login
16
+ sh :rsync, ['-avzc', Grifork.config.griforkfile, "#{node.host}:#{c.workdir}/Griforkfile"]
17
+ hostsfile = Tempfile.create('Griforkfile.hosts')
18
+ hostsfile.write(<<-EOS)
19
+ hosts #{node.all_descendant_nodes.map(&:host)}
20
+ EOS
21
+ hostsfile.flush
22
+ sh :rsync, ['-avzc', hostsfile.path, "#{node.host}:#{c.workdir}/Griforkfile.hosts"]
23
+ hostsfile.close
24
+ ssh node.host, %(cd #{c.dir} && #{c.cmd} --file #{c.workdir}/Griforkfile --override-by #{c.workdir}/Griforkfile.hosts --on-remote), [], user: c.login
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ class Grifork::Executor::Task
2
+ include Grifork::Executable
3
+ attr :src, :dst
4
+
5
+ def initialize(type, &task)
6
+ @type = type
7
+ @task = task
8
+ end
9
+
10
+ def run(src, dst)
11
+ @src = src
12
+ @dst = dst
13
+ instance_eval(&@task)
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ module Grifork::Executor; end
@@ -0,0 +1,68 @@
1
+ class Grifork::Graph::Node
2
+ include Grifork::Configured
3
+ attr :index, :level, :number, :children, :parent, :host
4
+
5
+ class << self
6
+ attr :count
7
+ def add
8
+ @count ||= 0
9
+ @count += 1
10
+ end
11
+ end
12
+
13
+ def initialize(host, parent: nil)
14
+ @host = host
15
+ @children = []
16
+ if parent
17
+ @parent = parent
18
+ @level = parent.level + 1
19
+ @number = parent.children.size
20
+ else
21
+ @level = 0
22
+ @number = 0
23
+ end
24
+ @index = self.class.count || 0
25
+ self.class.add
26
+ end
27
+
28
+ def to_s
29
+ "<#{index}:#{id}>"
30
+ end
31
+
32
+ def id
33
+ @id ||= -> {
34
+ if parent
35
+ "#{parent.id}-#{level}.#{number}"
36
+ else
37
+ "#{level}.#{number}"
38
+ end
39
+ }.call
40
+ end
41
+
42
+ def add_child(child)
43
+ unless acceptable?
44
+ raise "Unacceptable!"
45
+ end
46
+ @children << child
47
+ end
48
+
49
+ def local?
50
+ parent ? false : true
51
+ end
52
+
53
+ def acceptable?
54
+ children.size < config.branches
55
+ end
56
+
57
+ def all_descendant_nodes
58
+ @descendants = @children
59
+ @descendables = @children.select { |c| c.children.size > 0 }
60
+ while @descendables.size > 0
61
+ child = @descendables.shift
62
+ @descendants.concat(child.children)
63
+ new_descendables = child.children { |n| n.children.size > 0 }
64
+ @descendables.concat(new_descendables)
65
+ end
66
+ @descendants
67
+ end
68
+ end
@@ -0,0 +1,94 @@
1
+ class Grifork::Graph
2
+ include Grifork::Configured
3
+ include Grifork::Loggable
4
+ attr :root, :nodes, :depth
5
+
6
+ def initialize(hosts = [])
7
+ @root = Node.new('localhost')
8
+ @depth = @root.level
9
+ @nodes = 1
10
+ @acceptable_nodes = [@root]
11
+ hosts.each do |host|
12
+ self.add_node_by_host(host)
13
+ end
14
+ end
15
+
16
+ def add_node_by_host(host)
17
+ parent = @acceptable_nodes.first
18
+ node = Node.new(host, parent: parent)
19
+ parent.add_child(node)
20
+ unless parent.acceptable?
21
+ @acceptable_nodes.shift
22
+ end
23
+ @last = node
24
+ @depth = node.level
25
+ @nodes += 1
26
+ @acceptable_nodes << node
27
+ parent
28
+ end
29
+
30
+ # Launch local and remote tasks through whole graph
31
+ def launch_tasks
32
+ # level = 1
33
+ Parallel.map(root.children, in_threads: root.children.size) do |node|
34
+ logger.info("Run locally. localhost => #{node.host}")
35
+ config.local_task.run(root.host, node.host)
36
+ end
37
+ # level in (2..depth)
38
+ fork_remote_tasks(root.children)
39
+ end
40
+
41
+ # Run grifork command on child nodes recursively
42
+ def grifork
43
+ if root.children.size.zero?
44
+ logger.debug("#{root} Reached leaf. Nothing to do.")
45
+ return
46
+ end
47
+ Parallel.map(root.children, in_processes: root.children.size) do |child|
48
+ logger.info("Run locally. localhost => #{child.host}")
49
+ config.local_task.run(root.host, child.host)
50
+ Grifork::Executor::Grifork.new(config.grifork).run(child)
51
+ end
52
+ end
53
+
54
+ # Print graph structure for debug usage
55
+ def print(node = root)
56
+ puts %( ) * node.level + "#{node}"
57
+ node.children.each do |child|
58
+ print(child)
59
+ end
60
+ true
61
+ end
62
+
63
+ private
64
+
65
+ # Launch remote tasks recursively
66
+ def fork_remote_tasks(parents)
67
+ families = []
68
+ next_generation = []
69
+ parents.each do |parent|
70
+ if parent.children.size.zero?
71
+ logger.debug("#{parent} Reached leaf. Nothing to do.")
72
+ next
73
+ end
74
+ parent.children.each do |child|
75
+ families << [parent, child]
76
+ next_generation << child
77
+ end
78
+ end
79
+
80
+ if families.size.zero?
81
+ logger.info("Reached bottom of the tree. Nothing to do.")
82
+ return
83
+ end
84
+
85
+ Parallel.map(families, in_threads: families.size) do |family|
86
+ parent = family[0]
87
+ child = family[1]
88
+ logger.info("Run remote [#{parent.level}]. #{parent.host} => #{child.host}")
89
+ config.remote_task.run(parent.host, child.host)
90
+ end
91
+
92
+ fork_remote_tasks(next_generation)
93
+ end
94
+ end
@@ -0,0 +1,21 @@
1
+ class Grifork::Logger
2
+ def self.create
3
+ c = Grifork.config
4
+ if c.log.file
5
+ logger = StdLogger.create c.log.file
6
+ else
7
+ logger = StdLogger.create
8
+ end
9
+ logger.level = log_level(c.log.level)
10
+ logger
11
+ end
12
+
13
+ def self.log_level(arg_level = 'info')
14
+ level = arg_level.upcase
15
+ if ::Logger.const_defined?(level)
16
+ ::Logger.const_get(level)
17
+ else
18
+ ::Logger::INFO
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Grifork::Configured
2
+ def config
3
+ Grifork.config
4
+ end
5
+ end
@@ -0,0 +1,59 @@
1
+ module Grifork::Executable
2
+ include Grifork::Loggable
3
+
4
+ class CommandFailure < StandardError; end
5
+ class SSHCommandFailure < StandardError; end
6
+
7
+ def sh(cmd, args = [])
8
+ logger.info("#sh start - #{cmd} #{args}")
9
+ stat = Open3.popen3(cmd.to_s, *args) do |stdin, stdout, stderr, wait_thr|
10
+ stdin.close
11
+ stdout.each { |l| logger.info("#sh [out] #{l.chomp}") }
12
+ stderr.each { |l| logger.warn("#sh [err] #{l.chomp}") }
13
+ wait_thr.value
14
+ end
15
+
16
+ unless stat.success?
17
+ raise CommandFailure, "Failed to exec command! #{cmd} #{args}"
18
+ end
19
+ end
20
+
21
+ def ssh(host, cmd, args = [], user: nil)
22
+ command = "#{cmd} #{args.shelljoin}"
23
+ logger.info("#ssh start - to: #{host}, command: #{cmd} #{args}")
24
+ ssh_args = [host]
25
+ ssh_args << user if user
26
+ Net::SSH.start(*ssh_args) do |ssh|
27
+ channel = ssh.open_channel do |ch|
28
+ ch.exec(command) do |ch, success|
29
+ unless success
30
+ raise SSHCommandFailure, "Failed to exec ssh command! on: #{host} command: #{cmd} #{args}"
31
+ end
32
+
33
+ ch.on_data do |c, d|
34
+ d.each_line { |l| logger.info("#ssh @#{host} [out] #{l.chomp}") }
35
+ end
36
+ ch.on_extended_data do |c, t, d|
37
+ d.each_line { |l| logger.warn("#ssh @#{host} [err] #{l.chomp}") }
38
+ end
39
+ ch.on_close { logger.debug("#ssh @#{host} end.") }
40
+ end
41
+ end
42
+ channel.wait
43
+ end
44
+ end
45
+
46
+ def rsync(from, to = nil)
47
+ to ||= from
48
+ sh :rsync, [*rsync_opts, from, "#{dst}:#{to}"]
49
+ end
50
+
51
+ def rsync_remote(from, to = nil, user: nil)
52
+ to ||= from
53
+ ssh src, :rsync, [*rsync_opts, from, "#{dst}:#{to}"], user: user
54
+ end
55
+
56
+ def rsync_opts
57
+ %w(-avzc --delete)
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ module Grifork::Loggable
2
+ def logger
3
+ Grifork.logger
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ class Grifork
2
+ VERSION = '0.3.0'
3
+ end
data/lib/grifork.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'open3'
2
+ require 'optparse'
3
+ require 'ostruct'
4
+ require 'shellwords'
5
+ require 'tempfile'
6
+ require 'net/ssh'
7
+ require 'parallel'
8
+ require 'stdlogger'
9
+
10
+ class Grifork
11
+ require_relative 'grifork/config'
12
+ require_relative 'grifork/mixin/configured'
13
+ require_relative 'grifork/logger'
14
+ require_relative 'grifork/mixin/loggable'
15
+ require_relative 'grifork/graph'
16
+ require_relative 'grifork/graph/node'
17
+ require_relative 'grifork/executor'
18
+ require_relative 'grifork/mixin/executable'
19
+ require_relative 'grifork/executor/grifork'
20
+ require_relative 'grifork/executor/task'
21
+ require_relative 'grifork/dsl'
22
+ require_relative 'grifork/cli'
23
+ require_relative 'grifork/version'
24
+
25
+ DEFAULT_TASKFILE = 'Griforkfile'
26
+
27
+ MODES = {
28
+ standalone: 1,
29
+ grifork: 2,
30
+ }.freeze
31
+
32
+ class << self
33
+ attr :config
34
+
35
+ def configure!(config)
36
+ @config = config
37
+ end
38
+
39
+ def logger
40
+ @logger ||= -> { Grifork::Logger.create }.call
41
+ end
42
+ end
43
+ end
data/tmp/.keep ADDED
File without changes
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: grifork
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - key-amb
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ssh
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parallel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.9'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: stdlogger
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.3.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.3.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.12'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.12'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ description: Fast propagative task runner for systems which consist of a lot of servers.
112
+ email:
113
+ - yasutake.kiyoshi@gmail.com
114
+ executables:
115
+ - grifork
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".gitignore"
120
+ - ".rspec"
121
+ - ".travis.yml"
122
+ - CHANGELOG.md
123
+ - Gemfile
124
+ - LICENSE
125
+ - README.md
126
+ - Rakefile
127
+ - bin/grifork
128
+ - example/Griforkfile.grifork
129
+ - example/Griforkfile.standalone
130
+ - grifork.gemspec
131
+ - lib/grifork.rb
132
+ - lib/grifork/cli.rb
133
+ - lib/grifork/config.rb
134
+ - lib/grifork/dsl.rb
135
+ - lib/grifork/executor.rb
136
+ - lib/grifork/executor/grifork.rb
137
+ - lib/grifork/executor/task.rb
138
+ - lib/grifork/graph.rb
139
+ - lib/grifork/graph/node.rb
140
+ - lib/grifork/logger.rb
141
+ - lib/grifork/mixin/configured.rb
142
+ - lib/grifork/mixin/executable.rb
143
+ - lib/grifork/mixin/loggable.rb
144
+ - lib/grifork/version.rb
145
+ - tmp/.keep
146
+ homepage: https://github.com/key-amb/grifork
147
+ licenses:
148
+ - MIT
149
+ metadata: {}
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '2.0'
159
+ required_rubygems_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubyforge_project:
166
+ rubygems_version: 2.6.6
167
+ signing_key:
168
+ specification_version: 4
169
+ summary: Fast Propagative Task Runner
170
+ test_files: []