pd-blender 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rubocop.yml +2 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE.txt +14 -0
  7. data/README.md +342 -0
  8. data/Rakefile +21 -0
  9. data/bin/blend +20 -0
  10. data/blender.gemspec +36 -0
  11. data/lib/blender.rb +67 -0
  12. data/lib/blender/cli.rb +71 -0
  13. data/lib/blender/configuration.rb +45 -0
  14. data/lib/blender/discovery.rb +41 -0
  15. data/lib/blender/drivers/base.rb +40 -0
  16. data/lib/blender/drivers/compound.rb +29 -0
  17. data/lib/blender/drivers/ruby.rb +55 -0
  18. data/lib/blender/drivers/shellout.rb +63 -0
  19. data/lib/blender/drivers/ssh.rb +93 -0
  20. data/lib/blender/drivers/ssh_multi.rb +102 -0
  21. data/lib/blender/event_dispatcher.rb +45 -0
  22. data/lib/blender/exceptions.rb +26 -0
  23. data/lib/blender/handlers/base.rb +39 -0
  24. data/lib/blender/handlers/doc.rb +73 -0
  25. data/lib/blender/job.rb +73 -0
  26. data/lib/blender/lock/flock.rb +64 -0
  27. data/lib/blender/log.rb +24 -0
  28. data/lib/blender/rspec.rb +68 -0
  29. data/lib/blender/rspec/stub_registry.rb +45 -0
  30. data/lib/blender/scheduled_job.rb +66 -0
  31. data/lib/blender/scheduler.rb +114 -0
  32. data/lib/blender/scheduler/dsl.rb +160 -0
  33. data/lib/blender/scheduling_strategies/base.rb +30 -0
  34. data/lib/blender/scheduling_strategies/default.rb +37 -0
  35. data/lib/blender/scheduling_strategies/per_host.rb +38 -0
  36. data/lib/blender/scheduling_strategies/per_task.rb +37 -0
  37. data/lib/blender/tasks/base.rb +72 -0
  38. data/lib/blender/tasks/ruby.rb +31 -0
  39. data/lib/blender/tasks/shell_out.rb +30 -0
  40. data/lib/blender/tasks/ssh.rb +25 -0
  41. data/lib/blender/timer.rb +54 -0
  42. data/lib/blender/utils/refinements.rb +45 -0
  43. data/lib/blender/utils/thread_pool.rb +54 -0
  44. data/lib/blender/utils/ui.rb +51 -0
  45. data/lib/blender/version.rb +20 -0
  46. data/spec/blender/blender_rspec.rb +31 -0
  47. data/spec/blender/discovery_spec.rb +16 -0
  48. data/spec/blender/drivers/ssh_multi_spec.rb +16 -0
  49. data/spec/blender/drivers/ssh_spec.rb +17 -0
  50. data/spec/blender/dsl_spec.rb +19 -0
  51. data/spec/blender/event_dispatcher_spec.rb +17 -0
  52. data/spec/blender/job_spec.rb +42 -0
  53. data/spec/blender/lock_spec.rb +129 -0
  54. data/spec/blender/scheduled_job_spec.rb +30 -0
  55. data/spec/blender/scheduler_spec.rb +140 -0
  56. data/spec/blender/scheduling_strategies/default_spec.rb +75 -0
  57. data/spec/blender/utils/refinements_spec.rb +16 -0
  58. data/spec/blender/utils/thread_pool_spec.rb +16 -0
  59. data/spec/blender_spec.rb +37 -0
  60. data/spec/data/example.rb +12 -0
  61. data/spec/spec_helper.rb +35 -0
  62. metadata +304 -0
@@ -0,0 +1,93 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2014 PagerDuty, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'net/ssh'
19
+ require 'blender/exceptions'
20
+ require 'blender/drivers/base'
21
+
22
+ module Blender
23
+ module Driver
24
+ class Ssh < Base
25
+
26
+ attr_reader :user
27
+
28
+ def initialize(config = {})
29
+ cfg = config.dup
30
+ @user = cfg.delete(:user) || ENV['USER']
31
+ super(cfg)
32
+ end
33
+
34
+ def execute(tasks, hosts)
35
+ Log.debug("SSH execution tasks [#{Array(tasks).size}]")
36
+ Log.debug("SSH on hosts [#{hosts.join(",")}]")
37
+ Array(hosts).each do |host|
38
+ session = ssh_session(host)
39
+ Array(tasks).each do |task|
40
+ cmd = run_command(task.command, session)
41
+ if cmd.exitstatus != 0 and !task.metadata[:ignore_failure]
42
+ raise ExecutionFailed, cmd.stderr
43
+ end
44
+ end
45
+ session.loop
46
+ end
47
+ end
48
+
49
+ def run_command(command, session)
50
+ password = config[:password]
51
+ command = fixup_sudo(command)
52
+ exit_status = 0
53
+ channel = session.open_channel do |ch|
54
+ ch.request_pty
55
+ ch.exec(command) do |ch, success|
56
+ unless success
57
+ Log.debug("Command not found:#{success.inspect}")
58
+ exit_status = -1
59
+ end
60
+ ch.on_data do |c, data|
61
+ stdout << data
62
+ if data =~ /^blender sudo password: /
63
+ c.send_data("#{password}\n")
64
+ end
65
+ end
66
+ ch.on_extended_data do |c, type, data|
67
+ stderr << data
68
+ end
69
+ ch.on_request "exit-status" do |ichannel, data|
70
+ l = data.read_long
71
+ exit_status = [exit_status, l].max
72
+ Log.debug("exit_status:#{exit_status} , data:#{l}")
73
+ end
74
+ end
75
+ Log.debug("Exit(#{exit_status}) Command: '#{command}'")
76
+ end
77
+ channel.wait
78
+ ExecOutput.new(exit_status, stdout, stderr)
79
+ end
80
+
81
+ private
82
+
83
+ def ssh_session(host)
84
+ Log.debug("Invoking ssh: #{user}@#{host}")
85
+ Net::SSH.start(host, user, config)
86
+ end
87
+
88
+ def fixup_sudo(command)
89
+ command.sub(/^sudo/, 'sudo -p \'blender sudo password: \'')
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,102 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2014 PagerDuty, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'net/ssh'
19
+ require 'blender/exceptions'
20
+ require 'blender/drivers/ssh'
21
+
22
+ module Blender
23
+ module Driver
24
+ class SshMulti < Ssh
25
+
26
+ def execute(tasks, hosts)
27
+ Log.debug("SSH execution tasks [#{tasks.size}]")
28
+ Log.debug("SSH on hosts [#{hosts.join("\n")}]")
29
+ session = ssh_multi_session(hosts)
30
+ Array(tasks).each do |task|
31
+ cmd = run_command(task.command, session)
32
+ if cmd.exitstatus != 0 and !task.metadata[:ignore_failure]
33
+ raise ExecutionFailed, cmd.stderr
34
+ end
35
+ end
36
+ session.loop
37
+ end
38
+
39
+ def run_command(command, session)
40
+ password = @config[:password]
41
+ command = fixup_sudo(command)
42
+ exit_status = 0
43
+ channel = session.open_channel do |ch|
44
+ ch.request_pty
45
+ ch.exec(command) do |ch, success|
46
+ unless success
47
+ Log.debug("Command not found:#{success.inspect}")
48
+ exit_status = -1
49
+ end
50
+ ch.on_data do |c, data|
51
+ stdout << data
52
+ if data =~ /^blender sudo password: /
53
+ c.send_data("#{password}\n")
54
+ end
55
+ end
56
+ ch.on_extended_data do |c, type, data|
57
+ stderr << data
58
+ end
59
+ ch.on_request "exit-status" do |ichannel, data|
60
+ l = data.read_long
61
+ exit_status = [exit_status, l].max
62
+ Log.debug("exit_status:#{exit_status} , data:#{l}")
63
+ end
64
+ end
65
+ Log.debug("Exit(#{exit_status}) Command: '#{command}'")
66
+ end
67
+ channel.wait
68
+ ExecOutput.new(exit_status, stdout, stderr)
69
+ end
70
+
71
+ def concurrency
72
+ @config[:concurrency]
73
+ end
74
+
75
+ private
76
+
77
+ def ssh_multi_session(hosts)
78
+ user = @config[:user] || ENV['USER']
79
+ ssh_config = { password: @config[:password]}
80
+ error_handler = lambda do |server|
81
+ if config[:ignore_on_failure]
82
+ $!.backtrace.each { |l| Blender::Log.debug(l) }
83
+ else
84
+ throw :go, :raise
85
+ end
86
+ end
87
+ s = Net::SSH::Multi.start(
88
+ concurrent_connections: concurrency,
89
+ on_error: error_handler
90
+ )
91
+ hosts.each do |h|
92
+ s.use(user + '@' + h)
93
+ end
94
+ s
95
+ end
96
+
97
+ def default_config
98
+ super.merge(concurrency: 5)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,45 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2014 PagerDuty, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'blender/log'
19
+ require 'blender/handlers/base'
20
+
21
+ module Blender
22
+ class EventDispatcher
23
+ attr_reader :handlers
24
+ def initialize
25
+ @handlers = []
26
+ end
27
+
28
+ def register(handler)
29
+ @handlers << handler
30
+ end
31
+
32
+ # Define a method that will be forwarded to all
33
+ def self.def_forwarding_method(method_name)
34
+ class_eval(<<-END_OF_METHOD, __FILE__, __LINE__)
35
+ def #{method_name}(*args)
36
+ @handlers.each {|s| s.#{method_name}(*args)}
37
+ end
38
+ END_OF_METHOD
39
+ end
40
+
41
+ (Handlers::Base.instance_methods - Object.instance_methods).each do |method_name|
42
+ def_forwarding_method(method_name)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,26 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2014 PagerDuty, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ module Blender
19
+ class ExecutionFailed < RuntimeError; end
20
+ class UnsupportedFeature < ArgumentError; end
21
+ class UnknownDriver < ArgumentError; end
22
+ class UnknownTask < ArgumentError; end
23
+ class UnknownSchedulingStrategy < ArgumentError; end
24
+ class MultipleDrivers < RuntimeError; end
25
+ class LockAcquisitionError < RuntimeError; end
26
+ end
@@ -0,0 +1,39 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2014 PagerDuty, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ module Blender
19
+ module Handlers
20
+ class Base
21
+ def run_started(scheduler)
22
+ end
23
+ def run_finished(scheduler)
24
+ end
25
+ def run_failed(scheduler, e)
26
+ end
27
+ def job_computation_started(strategy)
28
+ end
29
+ def job_computation_finished(strategy, jobs)
30
+ end
31
+ def job_started(job)
32
+ end
33
+ def job_finished(job)
34
+ end
35
+ def job_failed(job, error)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2014 PagerDuty, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'blender/utils/ui'
19
+ require 'blender/configuration'
20
+
21
+ module Blender
22
+ module Handlers
23
+ class Doc < Base
24
+
25
+ attr_reader :ui
26
+
27
+ def initialize
28
+ @ui = Blender::Utils::UI.new
29
+ end
30
+
31
+ def run_started(scheduler)
32
+ @start_time = Time.now
33
+ @task_id = 0
34
+ @job_id = 1
35
+ ui.puts_green("Run[#{scheduler.name}] started")
36
+ ui.puts_green('Running in No-Op mode, driver execution will be skipped') if Configuration[:noop]
37
+ end
38
+
39
+ def run_finished(scheduler)
40
+ delta = ( Time.now - @start_time)
41
+ ui.puts_green("Run finished (#{delta} s)")
42
+ end
43
+
44
+ def run_failed(scheduler, e)
45
+ delta = ( Time.now - @start_time)
46
+ ui.puts_red("Run failed (#{delta} s)")
47
+ ui.puts_red("Error :#{e.class} Message: #{e.message}")
48
+ ui.puts_red("Backtrace :#{e.backtrace.join("\n")}")
49
+ end
50
+
51
+ def job_started(job)
52
+ ui.puts(" #{job.to_s} started")
53
+ end
54
+
55
+ def job_finished(job)
56
+ end
57
+
58
+ def job_failed(job, e)
59
+ ui.puts_red(" #{job.to_s} failed")
60
+ end
61
+
62
+ def job_computation_started(strategy)
63
+ @compute_start_time = Time.now
64
+ @strategy = strategy.class.name.split('::').last
65
+ end
66
+
67
+ def job_computation_finished(scheduler, jobs)
68
+ delta = Time.now - @compute_start_time
69
+ ui.puts(" #{jobs.size} job(s) computed using '#{@strategy}' strategy")
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,73 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2014 PagerDuty, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'blender/exceptions'
19
+ require 'blender/log'
20
+ require 'blender/configuration'
21
+
22
+ module Blender
23
+ # A job represent encapsulates an array of tasks to be performed
24
+ # against an array of hosts. Jobs are created by scheduling strategies,
25
+ # and passed to underlying drivers for execution
26
+ # Tasks within a single job must has exactly same driver.
27
+ class Job
28
+
29
+ attr_reader :tasks, :hosts, :driver, :id
30
+
31
+ # creates a new job
32
+ # @param id [Fixnum] a numeric identifier
33
+ # @param driver [Blender::Driver::Base] a driver object
34
+ # @patam hosts [Array] list of target hosts
35
+ # @patam hosts [Array] list of tasks to be run against the hosts
36
+ def initialize(id, driver, tasks, hosts)
37
+ @id = id
38
+ @tasks = Array(tasks)
39
+ @hosts = Array(hosts)
40
+ @driver = driver
41
+ end
42
+
43
+ def run
44
+ driver.execute(tasks, hosts) unless Configuration[:noop]
45
+ end
46
+
47
+ def to_s
48
+ "Job #{id} [#{name}]"
49
+ end
50
+
51
+ # computes, momoize and return the name of the job
52
+ # name is used to summarize the job.
53
+ # @return [String]
54
+ def name
55
+ @name ||= compute_name
56
+ end
57
+
58
+ private
59
+ def compute_name
60
+ if tasks.size == 1
61
+ t_part = tasks.first.name
62
+ else
63
+ t_part = "#{tasks.size} tasks"
64
+ end
65
+ if hosts.size == 1
66
+ h_part = hosts.first
67
+ else
68
+ h_part = "#{hosts.size} members"
69
+ end
70
+ "#{t_part} on #{h_part}"
71
+ end
72
+ end
73
+ end