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,64 @@
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 'timeout'
19
+ require 'fcntl'
20
+
21
+ module Blender
22
+ module Lock
23
+ class Flock
24
+
25
+ def initialize(name, options)
26
+ @path = options['path'] || File.join('/tmp', name)
27
+ @timeout = options[:timeout] || 0
28
+ @job_name = name
29
+ @lock_fd = nil
30
+ end
31
+
32
+ def acquire
33
+ @lock_fd = File.open(@path, File::CREAT|File::RDWR, 0644)
34
+ @lock_fd.fcntl( Fcntl::F_SETFD, @lock_fd.fcntl(Fcntl::F_GETFD, 0) | Fcntl::FD_CLOEXEC )
35
+ if @timeout > 0
36
+ Timeout.timeout(@timeout) do
37
+ @lock_fd.flock(File::LOCK_EX)
38
+ end
39
+ else
40
+ locked = @lock_fd.flock(File::LOCK_NB | File::LOCK_EX)
41
+ raise LockAcquisitionError, 'Failed to lock file' if locked == false
42
+ end
43
+ @lock_fd.write({job: @job_name, pid: Process.pid }.inspect)
44
+ end
45
+
46
+ def release
47
+ @lock_fd
48
+ @lock_fd.flock(File::LOCK_UN)
49
+ @lock_fd.close
50
+ end
51
+
52
+ def with_lock
53
+ acquire
54
+ yield if block_given?
55
+ rescue Timeout::Error => e
56
+ raise LockAcquisitionError, 'Timeout while waiting for lock acquisition'
57
+ rescue LockAcquisitionError => e
58
+ raise e
59
+ ensure
60
+ release
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
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 'mixlib/log'
19
+
20
+ module Blender
21
+ class Log
22
+ extend Mixlib::Log
23
+ end
24
+ end
@@ -0,0 +1,68 @@
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
+ begin
19
+ require 'rspec'
20
+ require 'rspec/mocks'
21
+ rescue LoadError
22
+ abort 'Blender::RSpec requires RSpec, RSpec::Mocks'
23
+ end
24
+
25
+ require 'blender'
26
+ require 'blender/rspec/stub_registry'
27
+
28
+ module Blender
29
+ module Discovery
30
+ alias_method :old_search, :search
31
+ def search(type, options = nil)
32
+ stub = Blender::RSpec::StubRegistry.instance.data.detect do |st|
33
+ st.type == type && st.opts == options
34
+ end
35
+ if stub
36
+ stub.return_value
37
+ else
38
+ old_search(type, options)
39
+ end
40
+ end
41
+ end
42
+ class Utils::UI
43
+ def puts(string)
44
+ end
45
+ end
46
+
47
+ module RSpec
48
+ extend self
49
+ include Blender::Utils::Refinements
50
+ def stub_search(type, options = nil)
51
+ StubRegistry.add(type, options)
52
+ end
53
+
54
+ def noop_scheduler_from_file(file)
55
+ Blender::Configuration[:noop] = true
56
+ des = File.read(file)
57
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(file), 'lib')))
58
+ Blender.blend(file) do |sch|
59
+ sch.lock_options(nil)
60
+ sch.instance_eval(des, __FILE__, __LINE__)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ RSpec.configure do |config|
67
+ config.include Blender::RSpec
68
+ 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 'singleton'
19
+
20
+ module Blender
21
+ module RSpec
22
+ class SearchStub
23
+ attr_reader :type, :opts, :return_value
24
+ def initialize(type, opts)
25
+ @type = type
26
+ @opts = opts
27
+ end
28
+ def and_return(value)
29
+ @return_value = value
30
+ end
31
+ end
32
+ class StubRegistry
33
+ include Singleton
34
+ attr_reader :data
35
+ def initialize
36
+ @data = []
37
+ end
38
+ def self.add(type, opts)
39
+ obj = SearchStub.new(type, opts)
40
+ instance.data << obj
41
+ obj
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,66 @@
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
+ # A scheduled job encapsulates a blender based job to be executed
20
+ # at certain interval. Job is specified as a file path, where
21
+ # the file contains job written in blender's DSL. Job interval can be
22
+ # specified either via +cron+ or +every+ method
23
+ #
24
+ # +Blender::Timer+ object uses +ScheduledJob+ and to execute the job
25
+ # and Rufus::Scheduler to schedule it
26
+ class ScheduledJob
27
+ attr_reader :schedule, :file
28
+ # create a new instance
29
+ # @param name [String] name of the job
30
+ def initialize(name)
31
+ @name = name
32
+ @file = name
33
+ end
34
+
35
+ # set the path of the file holding blender job
36
+ #
37
+ # @param file [String] path of the blender file
38
+ def blender_file(file)
39
+ @file = file
40
+ end
41
+
42
+ # set the job inteval via cron syntax. The value is passed as it is
43
+ # to rufus scheduler.
44
+ #
45
+ # @param line [String] job interval in cron syntax e.g (*/5 * * * *)
46
+ def cron(line)
47
+ @schedule = [ __method__, line]
48
+ end
49
+
50
+ # set the job inteval after every specified seconds
51
+ # to rufus scheduler.
52
+ #
53
+ # @param interval [Fixnum] job interval in seconds
54
+ def every(interval)
55
+ @schedule = [ __method__, interval]
56
+ end
57
+
58
+ # invoke a blender run based on the +blender_file+
59
+ def run
60
+ des = File.read(file)
61
+ Blender.blend(file) do |sch|
62
+ sch.instance_eval(des, __FILE__, __LINE__)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,114 @@
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/configuration'
20
+ require 'blender/utils/thread_pool'
21
+ require 'blender/exceptions'
22
+ require 'blender/scheduling_strategies/default'
23
+ require 'blender/scheduling_strategies/per_host'
24
+ require 'blender/scheduling_strategies/per_task'
25
+ require 'blender/utils/thread_pool'
26
+ require 'blender/scheduler/dsl'
27
+ require 'blender/event_dispatcher'
28
+ require 'blender/handlers/doc'
29
+ require 'blender/tasks/base'
30
+
31
+ module Blender
32
+ class Scheduler
33
+
34
+ include SchedulerDSL
35
+ include Lock
36
+
37
+ attr_reader :metadata, :name
38
+ attr_reader :scheduling_strategy
39
+ attr_reader :events, :tasks
40
+ attr_reader :lock_properties
41
+
42
+ def initialize(name, tasks = [], metadata = {})
43
+ @name = name
44
+ @tasks = tasks
45
+ @metadata = default_metadata.merge(metadata)
46
+ @events = Blender::EventDispatcher.new
47
+ events.register(Blender::Handlers::Doc.new)
48
+ @scheduling_strategy = nil
49
+ @lock_properties = {driver: 'flock', driver_options: {}}
50
+ end
51
+
52
+ def run
53
+ @scheduling_strategy ||= SchedulingStrategy::Default.new
54
+ events.run_started(self)
55
+ events.job_computation_started(scheduling_strategy)
56
+ jobs = scheduling_strategy.compute_jobs(@tasks)
57
+ events.job_computation_finished(self, jobs)
58
+ lock do
59
+ if metadata[:concurrency] > 1
60
+ concurrent_run(jobs)
61
+ else
62
+ serial_run(jobs)
63
+ end
64
+ events.run_finished(self)
65
+ jobs
66
+ end
67
+ rescue StandardError => e
68
+ events.run_failed(self, e)
69
+ raise e
70
+ end
71
+
72
+ def serial_run(jobs)
73
+ Log.debug('Invoking serial run')
74
+ jobs.each do |job|
75
+ run_job(job)
76
+ end
77
+ end
78
+
79
+ def concurrent_run(jobs)
80
+ c = metadata[:concurrency]
81
+ Log.debug("Invoking concurrent run with concurrency:#{c}")
82
+ pool = Utils::ThreadPool.new(c)
83
+ jobs.each do |job|
84
+ pool.add_job do
85
+ run_job(job)
86
+ end
87
+ end
88
+ pool.run_till_done
89
+ end
90
+
91
+ def run_job(job)
92
+ events.job_started(job)
93
+ Log.debug("Running job #{job.name}")
94
+ job.run
95
+ events.job_finished(job)
96
+ rescue StandardError => e
97
+ events.job_failed(job, e)
98
+ if metadata[:ignore_failure]
99
+ Log.warn("Exception: #{e.inspect} was suppressed, ignoring failure")
100
+ else
101
+ raise e
102
+ end
103
+ end
104
+
105
+ def default_metadata
106
+ {
107
+ ignore_failure: false,
108
+ concurrency: 0,
109
+ handlers: [],
110
+ members: []
111
+ }
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,160 @@
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/scheduling_strategies/default'
20
+ require 'blender/tasks/base'
21
+ require 'blender/tasks/ruby'
22
+ require 'blender/tasks/ssh'
23
+ require 'blender/tasks/shell_out'
24
+ require 'highline'
25
+ require 'blender/utils/refinements'
26
+ require 'blender/drivers/ssh'
27
+ require 'blender/drivers/ssh_multi'
28
+ require 'blender/drivers/shellout'
29
+ require 'blender/drivers/ruby'
30
+ require 'blender/discovery'
31
+ require 'blender/handlers/base'
32
+ require 'blender/lock/flock'
33
+
34
+ module Blender
35
+ module SchedulerDSL
36
+ include Blender::Utils::Refinements
37
+ include Blender::Discovery
38
+
39
+ def config(type, opts = {})
40
+ Blender::Configuration[type].merge!(opts)
41
+ end
42
+
43
+ alias :init :config
44
+
45
+ def log_level(level)
46
+ Blender::Log.level = level
47
+ end
48
+
49
+ def ask(msg, echo = false)
50
+ HighLine.new.ask(msg){|q| q.echo = echo}
51
+ end
52
+
53
+ def driver(type, opts = {})
54
+ klass_name = camelcase(type.to_s).to_sym
55
+ config = symbolize(opts.merge(events: events))
56
+ yield config if block_given?
57
+ begin
58
+ Blender::Driver.const_get(klass_name).new(config)
59
+ rescue NameError => e
60
+ raise UnknownDriver, e.message
61
+ end
62
+ end
63
+
64
+ def add_handler(handler)
65
+ events.register(handler)
66
+ end
67
+
68
+ alias :register_handler :add_handler
69
+
70
+ def on(event_type, &block)
71
+ add_handler(
72
+ Class.new(Handlers::Base) do
73
+ define_method(event_type) do |*args|
74
+ block.call(args)
75
+ end
76
+ end.new
77
+ )
78
+ end
79
+
80
+ def build_task(name, type)
81
+ task_klass = Blender::Task.const_get(camelcase(type.to_s).to_sym)
82
+ driver_klass = Blender::Driver.const_get(camelcase(type.to_s).to_sym)
83
+ task = task_klass.new(name)
84
+ task.members(metadata[:members]) unless metadata[:members].empty?
85
+ task
86
+ end
87
+
88
+ def append_task(type, task)
89
+ Log.debug("Appended task:#{task.name}")
90
+ klass = Blender::Driver.const_get(camelcase(type.to_s).to_sym)
91
+ if task.driver.nil?
92
+ opts = {}
93
+ opts.merge!(Blender::Configuration[type]) unless Blender::Configuration[type].empty?
94
+ opts.merge!(task.driver_opts)
95
+ task.use_driver(driver(type, opts))
96
+ end
97
+ @tasks << task
98
+ end
99
+
100
+ def shell_task(name, &block)
101
+ task = build_task(name, :shell_out)
102
+ task.members(['localhost'])
103
+ task.instance_eval(&block) if block_given?
104
+ append_task(:shell_out, task)
105
+ end
106
+
107
+ def ruby_task(name, &block)
108
+ task = build_task(name, :ruby)
109
+ task.instance_eval(&block) if block_given?
110
+ append_task(:ruby, task)
111
+ end
112
+
113
+ def ssh_task(name, &block)
114
+ task = build_task(name, :ssh)
115
+ task.instance_eval(&block) if block_given?
116
+ append_task(:ssh, task)
117
+ end
118
+
119
+ def strategy(strategy)
120
+ klass_name = camelcase(strategy.to_s).to_sym
121
+ begin
122
+ @scheduling_strategy = Blender::SchedulingStrategy.const_get(klass_name).new
123
+ @scheduling_strategy.freeze
124
+ rescue NameError => e
125
+ raise UnknownSchedulingStrategy, e.message
126
+ end
127
+ end
128
+
129
+ def concurrency(value)
130
+ @metadata[:concurrency] = value
131
+ end
132
+
133
+ def ignore_failure(value)
134
+ @metadata[:ignore_failure] = value
135
+ end
136
+
137
+ def members(hosts)
138
+ @metadata[:members] = hosts
139
+ end
140
+
141
+ def lock_options(driver, opts = {})
142
+ @lock_properties[:driver] = driver
143
+ @lock_properties[:driver_options].merge!(opts.dup)
144
+ end
145
+
146
+ def lock(opts = {})
147
+ options = lock_properties.dup.merge(opts)
148
+ if options[:driver]
149
+ lock_klass = Lock.const_get(camelcase(options[:driver]).to_sym)
150
+ lock_klass.new(name, options[:driver_options]).with_lock do
151
+ yield if block_given?
152
+ end
153
+ else
154
+ yield if block_given?
155
+ end
156
+ end
157
+
158
+ alias_method :task, :shell_task
159
+ end
160
+ end