pd-blender 0.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.
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