pd-blender 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8b9983c59a3ba2c697b2d11c20cdb4d8a8a236f8
4
- data.tar.gz: 979315d46ed457760f89ff5692b475b69e94cfc5
3
+ metadata.gz: 4755355532133b48f4e4525b8a7671206318b470
4
+ data.tar.gz: 416b8fd419f08c1900b9f08cca300b7703d4ed68
5
5
  SHA512:
6
- metadata.gz: 5019047e53c66b01505a3a77c037f79fb395b342b5e3962dd33edb10e97803a20f0da5bc75211e65b64f9ac29211db6317af3a386ebc665f18a079e7f964375d
7
- data.tar.gz: 89107f6c920895540a00093cde7eb03df1e1a3f160935ebf75e1afb8d6a4eb4cb156427bd2cb78802ad3d5c74755e39413d76f3e5ef6ec6446ad9c0b7886c09c
6
+ metadata.gz: 7822a210909825f27e67a8887083ac2a3521ef3d465efcdcd3e45dd7b61da3551df7ad59fe480dcbe9875ee6bc77261d3ced3a157b744055d02c4e6c336b08e3
7
+ data.tar.gz: 07c515f06d1ae9d67360e264c95e2be947cac421ca9319fefdbb1c08f26258a468466ff7976fbb9fff7d3bdc5467e28447fdaa5834858116c7edb85457a4d8b6
@@ -1,9 +1,13 @@
1
+ language: ruby
2
+ cache:
3
+ directories:
4
+ - .bundle
1
5
  before_install:
2
6
  - bundle install --path .bundle
3
7
  rvm:
4
8
  - 1.9.3
5
- - 2.1.0
6
- - 2.1.2
9
+ - 2.1
10
+ - 2.2
7
11
  branches:
8
12
  only:
9
13
  - master
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  # Blender
3
3
 
4
4
  Blender is a modular remote command execution framework. Blender provides few basic
5
- primitives to automate cross server workflows. Workflows can be expressed in plain
5
+ primitives to automate across server workflows. Workflows can be expressed in plain
6
6
  ruby DSL and executed using the CLI.
7
7
 
8
8
  Following is an example of a simple blender script that will update the package
@@ -63,6 +63,17 @@ commands over ssh, serf handlers etc) which can ease automating large cluster
63
63
  maintenance, multi stage provisioning, establishing cross server feedback
64
64
  loops etc.
65
65
 
66
+ ## Installation
67
+
68
+ Blender is published as `pd-blender` in rubygems. And you can install it as:
69
+ ```sh
70
+ gem install pd-blender
71
+ ```
72
+ Or declare it as a dependency in your Gemfile, if you are using bundler.
73
+ ```ruby
74
+ gem 'pd-blender'
75
+ ```
76
+
66
77
  ## Concepts
67
78
 
68
79
  Blender is composed of two components:
@@ -71,7 +82,7 @@ Blender is composed of two components:
71
82
  script can have multiple tasks. Tasks are executed using drivers. Tasks can declare their
72
83
  target hosts.
73
84
 
74
- * **Scheduling stratgy** - Determines the order of task execution across the hosts.
85
+ * **Scheduling strategy** - Determines the order of task execution across the hosts.
75
86
  Every blender scripts has one and only one scheduling strategy. Scheduling strategies
76
87
  uses the task list as input and produces a list of jobs, to be executed using drivers.
77
88
 
@@ -115,6 +126,18 @@ can execute `ssh_task`s. Blender core ships with following tasks and drivers:
115
126
  end
116
127
  ```
117
128
 
129
+ - **scp_task**: download or upload files using scp
130
+
131
+ ```ruby
132
+ members ['host1', 'host2', 'host3']
133
+ scp_upload '/foo/bar' do
134
+ from '/path/to/remote/file'
135
+ end
136
+ scp_download '/foo/bar' do
137
+ to '/local/path'
138
+ end
139
+ ```
140
+
118
141
  As mentioned earlier tasks are executed using drivers. Tasks can declare their preferred driver or
119
142
  Blender will assign a driver to them automatically. Blender will reuse the global driver if its
120
143
  compatible, else it will create one. By default the ```global_driver``` is a ```shell_out``` driver.
@@ -125,9 +148,9 @@ specific to their own implementations.
125
148
 
126
149
  Scheduling strategies are the most crucial part of a blender script. They decide the
127
150
  order of command execution across distributed nodes in blender. Each blender script is
128
- invoked using one strategy. Consider them as a transformation, where the input is tasks and ouput is
151
+ invoked using one strategy. Consider them as a transformation, where the input is tasks and ouput is
129
152
  jobs. Tasks and job are pretty similar in their structures (both holds command and hosts),
130
- except a jobs can hold multiple tasks within them. We'll come to this later, but first, lets
153
+ except a jobs can hold multiple tasks within them. We'll come to this later, but first, let's
131
154
  see how the default strategy work.
132
155
 
133
156
  - **default strategy**: the default strategy takes the list of declared tasks (and associated members
@@ -275,7 +298,7 @@ Following are some examples:
275
298
  - **chef**: discover hosts using Chef search
276
299
 
277
300
  ```ruby
278
- require 'blender/dscoveries/chef'
301
+ require 'blender/discoveries/chef'
279
302
 
280
303
  ruby_task 'print host name' do
281
304
  execute do |host|
@@ -285,7 +308,7 @@ Following are some examples:
285
308
  end
286
309
  ```
287
310
 
288
- ## Invoking blender periodially with Rufus schedler
311
+ ## Invoking blender periodially with Rufus scheduler
289
312
 
290
313
  Blender is designed to be used as a standalone script that can be invoked on-demand or
291
314
  consumed as a library, i.e. workflows are written in plain Ruby objects and invoked
@@ -320,7 +343,18 @@ both per task level as well as globally.
320
343
  ## Event handlers
321
344
 
322
345
  Blender provides an event dispatchment facility (inspired from Chef), where arbitrary logic can
323
- be hooked into the event system (e.g. HipChat notification handlers, statsd handlers, etc) and blender will automatically invoke them during key events. As of now, events are available before and after run and per job execution. Event dispatch system is likely to get more elaborate and blender might have few common event handlers (metric, notifications etc) in near future.
346
+ be hooked into the event system (e.g. HipChat notification handlers, statsd handlers, etc) and blender
347
+ will automatically invoke them during key events. As of now, events are available before and after run
348
+ and per job execution. Event dispatch system is likely to get more elaborate and blender might have
349
+ few common event handlers (metric, notifications etc) in near future.
350
+
351
+
352
+ ## Ancillary projects
353
+
354
+ Blender has a few ancillary projects for integration with other systems, following are few of them:
355
+ - Zookeeper based locking for distributed blender deployments [blender-zk](https://github.com/PagerDuty/blender-zk)
356
+ - Serf based host discovery and command dispatch [blender-serf](https://github.com/PagerDuty/blender-serf)
357
+ - Chef based host discovery [blender-chef](https://github.com/PagerDuty/blender-chef)
324
358
 
325
359
  ## Supported ruby versions
326
360
 
data/Rakefile CHANGED
@@ -8,14 +8,16 @@ YARD::Rake::YardocTask.new do |t|
8
8
  end
9
9
 
10
10
  RSpec::Core::RakeTask.new(:spec) do |t|
11
- t.pattern = %w{spec/**/*_spec.rb}
11
+ t.pattern = %w(spec/**/*_spec.rb)
12
12
  end
13
13
 
14
14
  RSpec::Core::RakeTask.new(:rspec) do |t|
15
- t.pattern = %w{spec/**/*_rspec.rb}
15
+ t.pattern = %w(spec/**/*_rspec.rb)
16
16
  end
17
17
 
18
18
  RuboCop::RakeTask.new(:rubocop) do |t|
19
- t.patterns = %w{Rakefile Gemfile lib/**/*.rb}
19
+ t.patterns = %w(Rakefile Gemfile lib/**/*.rb)
20
20
  t.fail_on_error = true
21
21
  end
22
+
23
+ task default: [:spec, :rubocop]
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency 'mixlib-shellout'
25
25
  spec.add_dependency 'mixlib-log'
26
26
  spec.add_dependency 'net-ssh'
27
+ spec.add_dependency 'net-scp'
27
28
  spec.add_dependency 'rufus-scheduler'
28
29
 
29
30
  spec.add_development_dependency 'bundler'
@@ -29,13 +29,13 @@ module Blender
29
29
  # command and executed with local shellout driver
30
30
  #
31
31
  # @param name [String] Name of the run
32
+ # @param options[Hash] Additional options for scheduler
32
33
  #
33
34
  # @return [void]
34
- def self.blend(name, config_file = nil)
35
- scheduler = Scheduler.new(name)
36
- if config_file
37
- configure(config_file)
38
- end
35
+ def self.blend(name, options = {})
36
+ config_file = options.delete(:config_file)
37
+ scheduler = Scheduler.new(name, [], options)
38
+ configure(config_file) if config_file
39
39
  if block_given?
40
40
  yield scheduler
41
41
  else
@@ -47,12 +47,10 @@ module Blender
47
47
 
48
48
  def self.configure(file)
49
49
  data = JSON.parse(File.read(file))
50
- if data['log_level']
51
- Blender::Log.level = data['log_level'].to_sym
52
- end
53
- if data['log_file']
54
- Blender::Log.init(data['log_file'])
55
- end
50
+
51
+ Blender::Log.level = data['log_level'].to_sym if data['log_level']
52
+ Blender::Log.init(data['log_file']) if data['log_file']
53
+
56
54
  if data['load_paths']
57
55
  data['load_paths'].each do |path|
58
56
  $LOAD_PATH.unshift(path)
@@ -21,10 +21,11 @@ require 'blender/timer'
21
21
 
22
22
  module Blender
23
23
  class CLI < Thor
24
-
25
24
  def self.exit_on_failure?
26
25
  true
27
26
  end
27
+ stop_on_unknown_option! :from_file
28
+ check_unknown_options! except: :from_file
28
29
 
29
30
  default_command :from_file
30
31
  package_name 'Blender'
@@ -47,11 +48,22 @@ module Blender
47
48
  aliases: '-n',
48
49
  banner: 'No-op mode, run blender without executing jobs'
49
50
 
50
- def from_file
51
+ method_option :quiet,
52
+ default: false,
53
+ type: :boolean,
54
+ aliases: '-q',
55
+ banner: 'Quiet mode. Disable printing running job details'
56
+
57
+ def from_file(*args)
51
58
  Configuration[:noop] = options[:noop]
59
+ Configuration[:arguments] = args
52
60
  des = File.read(options[:file])
53
61
  $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(options[:file]), 'lib')))
54
- Blender.blend(options[:file], options[:config_file]) do |sch|
62
+ scheduler_options = {
63
+ config_file: options[:config_file],
64
+ no_doc: options[:quiet]
65
+ }
66
+ Blender.blend(options[:file], scheduler_options) do |sch|
55
67
  sch.instance_eval(des, __FILE__, __LINE__)
56
68
  end
57
69
  end
@@ -25,9 +25,8 @@ module Blender
25
25
  attr_reader :data, :mutex
26
26
 
27
27
  def initialize
28
- @data = Hash.new{|h,k| h[k] = Hash.new}
29
- @data[:noop] = false
30
28
  @mutex = Mutex.new
29
+ reset!
31
30
  end
32
31
 
33
32
  def self.[]=(key, value)
@@ -41,5 +40,17 @@ module Blender
41
40
  instance.data[key]
42
41
  end
43
42
  end
43
+
44
+ def self.reset!
45
+ instance.mutex.synchronize do
46
+ instance.reset!
47
+ end
48
+ end
49
+
50
+ def reset!
51
+ @data = Hash.new{|h, k| h[k] = Hash.new}
52
+ @data[:noop] = false
53
+ @data[:arguments] = []
54
+ end
44
55
  end
45
56
  end
@@ -20,7 +20,6 @@ require 'blender/drivers/base'
20
20
  module Blender
21
21
  module Driver
22
22
  class Ruby < Base
23
-
24
23
  def execute(tasks, hosts)
25
24
  tasks.each do |task|
26
25
  hosts.each do |host|
@@ -0,0 +1,85 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2015 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/scp'
19
+ require 'net/ssh'
20
+ require 'blender/exceptions'
21
+ require 'blender/drivers/base'
22
+
23
+ module Blender
24
+ module Driver
25
+ class Scp < Base
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("SCP execution tasks [#{Array(tasks).size}]")
36
+ Log.debug("SCP on hosts [#{hosts.join(",")}]")
37
+ Array(hosts).each do |host|
38
+ session = create_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
+ begin
51
+ case command.direction
52
+ when :upload
53
+ session.scp.upload!(command.source, command.target)
54
+ ExecOutput.new(0, '', '')
55
+ when :download
56
+ session.scp.download!(command.source, command.target)
57
+ ExecOutput.new(0, '', '')
58
+ else
59
+ ExecOutput.new(-1, '' , "Invalid direction. Can be either :upload or :download. Found:'#{command.direction}'")
60
+ end
61
+ rescue StandardError => e
62
+ ExecOutput.new(-1, stdout, e.message + e.backtrace.join("\n"))
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def create_session(host)
69
+ Log.debug("Invoking ssh: #{user}@#{host}")
70
+ Net::SSH.start(host, user, config)
71
+ end
72
+ end
73
+
74
+ class ScpUpload < Blender::Driver::Scp
75
+ end
76
+ class ScpDownload < Blender::Driver::Scp
77
+ def run_command(command, session)
78
+ begin
79
+ rescue StandardError => e
80
+ ExecOutput.new(-1, stdout, e.message + e.backtrace.join("\n"))
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -20,14 +20,11 @@ require 'mixlib/shellout'
20
20
  module Blender
21
21
  module Driver
22
22
  class ShellOut < Base
23
-
24
23
  def initialize(config = {})
25
24
  @options = {}
26
25
  cfg = config.dup
27
26
  [:user, :group, :cwd, :umask, :returns, :environment, :timeout].each do |key|
28
- if cfg.key?(key)
29
- @options[key] = cfg.delete(key)
30
- end
27
+ @options[key] = cfg.delete(key) if cfg.key?(key)
31
28
  end
32
29
  super(cfg)
33
30
  end
@@ -18,12 +18,13 @@
18
18
  require 'net/ssh'
19
19
  require 'blender/exceptions'
20
20
  require 'blender/drivers/base'
21
+ require 'blender/drivers/ssh_exec'
21
22
 
22
23
  module Blender
23
24
  module Driver
24
25
  class Ssh < Base
25
-
26
26
  attr_reader :user
27
+ include SSHExec
27
28
 
28
29
  def initialize(config = {})
29
30
  cfg = config.dup
@@ -35,7 +36,7 @@ module Blender
35
36
  Log.debug("SSH execution tasks [#{Array(tasks).size}]")
36
37
  Log.debug("SSH on hosts [#{hosts.join(",")}]")
37
38
  Array(hosts).each do |host|
38
- session = ssh_session(host)
39
+ session = create_session(host)
39
40
  Array(tasks).each do |task|
40
41
  cmd = run_command(task.command, session)
41
42
  if cmd.exitstatus != 0 and !task.metadata[:ignore_failure]
@@ -47,47 +48,16 @@ module Blender
47
48
  end
48
49
 
49
50
  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
51
+ exit_status = remote_exec(command, session)
78
52
  ExecOutput.new(exit_status, stdout, stderr)
79
53
  end
80
54
 
81
55
  private
82
56
 
83
- def ssh_session(host)
57
+ def create_session(host)
84
58
  Log.debug("Invoking ssh: #{user}@#{host}")
85
59
  Net::SSH.start(host, user, config)
86
60
  end
87
-
88
- def fixup_sudo(command)
89
- command.sub(/^sudo/, 'sudo -p \'blender sudo password: \'')
90
- end
91
61
  end
92
62
  end
93
63
  end
@@ -0,0 +1,54 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Author:: Smit Shah (<who828@gmail.com>)
4
+ # Copyright:: Copyright (c) 2014 PagerDuty, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'blender/log'
20
+ module Blender::Driver::SSHExec
21
+ def remote_exec(command, session)
22
+ password = config[:password]
23
+ command = fixup_sudo(command)
24
+ exit_status = 0
25
+ channel = session.open_channel do |ch|
26
+ ch.request_pty
27
+ ch.exec(command) do |ch, success|
28
+ unless success
29
+ Blender::Log.debug("Command not found:#{success.inspect}")
30
+ exit_status = -1
31
+ end
32
+ ch.on_data do |c, data|
33
+ stdout << data
34
+ c.send_data("#{password}\n") if data =~ /^blender sudo password: /
35
+ end
36
+ ch.on_extended_data do |c, type, data|
37
+ stderr << data
38
+ end
39
+ ch.on_request "exit-status" do |ichannel, data|
40
+ l = data.read_long
41
+ exit_status = [exit_status, l].max
42
+ Blender::Log.debug("exit_status:#{exit_status} , data:#{l}")
43
+ end
44
+ end
45
+ Blender::Log.debug("Exit(#{exit_status}) Command: '#{command}'")
46
+ end
47
+ channel.wait
48
+ exit_status
49
+ end
50
+
51
+ def fixup_sudo(command)
52
+ command.sub(/^sudo/, 'sudo -p \'blender sudo password: \'')
53
+ end
54
+ end
@@ -18,6 +18,7 @@
18
18
  require 'net/ssh'
19
19
  require 'blender/exceptions'
20
20
  require 'blender/drivers/ssh'
21
+ require 'blender/drivers/ssh_exec'
21
22
 
22
23
  module Blender
23
24
  module Driver
@@ -26,7 +27,7 @@ module Blender
26
27
  def execute(tasks, hosts)
27
28
  Log.debug("SSH execution tasks [#{tasks.size}]")
28
29
  Log.debug("SSH on hosts [#{hosts.join("\n")}]")
29
- session = ssh_multi_session(hosts)
30
+ session = create_session(hosts)
30
31
  Array(tasks).each do |task|
31
32
  cmd = run_command(task.command, session)
32
33
  if cmd.exitstatus != 0 and !task.metadata[:ignore_failure]
@@ -36,45 +37,13 @@ module Blender
36
37
  session.loop
37
38
  end
38
39
 
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
40
  def concurrency
72
41
  @config[:concurrency]
73
42
  end
74
43
 
75
44
  private
76
45
 
77
- def ssh_multi_session(hosts)
46
+ def create_session(hosts)
78
47
  user = @config[:user] || ENV['USER']
79
48
  ssh_config = { password: @config[:password]}
80
49
  error_handler = lambda do |server|
@@ -40,6 +40,6 @@ module Blender
40
40
 
41
41
  (Handlers::Base.instance_methods - Object.instance_methods).each do |method_name|
42
42
  def_forwarding_method(method_name)
43
- end
43
+ end
44
44
  end
45
45
  end
@@ -20,18 +20,25 @@ module Blender
20
20
  class Base
21
21
  def run_started(scheduler)
22
22
  end
23
+
23
24
  def run_finished(scheduler)
24
25
  end
26
+
25
27
  def run_failed(scheduler, e)
26
28
  end
29
+
27
30
  def job_computation_started(strategy)
28
31
  end
32
+
29
33
  def job_computation_finished(strategy, jobs)
30
34
  end
35
+
31
36
  def job_started(job)
32
37
  end
38
+
33
39
  def job_finished(job)
34
40
  end
41
+
35
42
  def job_failed(job, error)
36
43
  end
37
44
  end
@@ -21,7 +21,6 @@ require 'blender/configuration'
21
21
  module Blender
22
22
  module Handlers
23
23
  class Doc < Base
24
-
25
24
  attr_reader :ui
26
25
 
27
26
  def initialize
@@ -25,7 +25,6 @@ module Blender
25
25
  # and passed to underlying drivers for execution
26
26
  # Tasks within a single job must has exactly same driver.
27
27
  class Job
28
-
29
28
  attr_reader :tasks, :hosts, :driver, :id
30
29
 
31
30
  # creates a new job
@@ -21,12 +21,10 @@ require 'fcntl'
21
21
  module Blender
22
22
  module Lock
23
23
  class Flock
24
-
25
24
  def initialize(name, options)
26
25
  @path = options['path'] || File.join('/tmp', name)
27
26
  @timeout = options[:timeout] || 0
28
27
  @job_name = name
29
- @lock_fd = nil
30
28
  end
31
29
 
32
30
  def acquire
@@ -38,15 +36,15 @@ module Blender
38
36
  end
39
37
  else
40
38
  locked = @lock_fd.flock(File::LOCK_NB | File::LOCK_EX)
41
- raise LockAcquisitionError, 'Failed to lock file' if locked == false
39
+ raise LockAcquisitionError, "Failed to lock file '#{@path}'" if locked == false
42
40
  end
43
41
  @lock_fd.write({job: @job_name, pid: Process.pid }.inspect)
44
42
  end
45
43
 
46
44
  def release
47
- @lock_fd
48
45
  @lock_fd.flock(File::LOCK_UN)
49
46
  @lock_fd.close
47
+ File.delete(@path) if File.exists?(@path)
50
48
  end
51
49
 
52
50
  def with_lock
@@ -54,10 +52,8 @@ module Blender
54
52
  yield if block_given?
55
53
  rescue Timeout::Error => e
56
54
  raise LockAcquisitionError, 'Timeout while waiting for lock acquisition'
57
- rescue LockAcquisitionError => e
58
- raise e
59
55
  ensure
60
- release
56
+ release if @lock_fd
61
57
  end
62
58
  end
63
59
  end
@@ -25,6 +25,7 @@ module Blender
25
25
  @type = type
26
26
  @opts = opts
27
27
  end
28
+
28
29
  def and_return(value)
29
30
  @return_value = value
30
31
  end
@@ -35,6 +36,7 @@ module Blender
35
36
  def initialize
36
37
  @data = []
37
38
  end
39
+
38
40
  def self.add(type, opts)
39
41
  obj = SearchStub.new(type, opts)
40
42
  instance.data << obj
@@ -30,7 +30,6 @@ require 'blender/tasks/base'
30
30
 
31
31
  module Blender
32
32
  class Scheduler
33
-
34
33
  include SchedulerDSL
35
34
  include Lock
36
35
 
@@ -39,14 +38,16 @@ module Blender
39
38
  attr_reader :events, :tasks
40
39
  attr_reader :lock_properties
41
40
 
42
- def initialize(name, tasks = [], metadata = {})
41
+ def initialize(name, tasks = [], options = {})
43
42
  @name = name
44
43
  @tasks = tasks
45
- @metadata = default_metadata.merge(metadata)
46
44
  @events = Blender::EventDispatcher.new
47
- events.register(Blender::Handlers::Doc.new)
45
+ unless options.delete(:no_doc)
46
+ events.register(Blender::Handlers::Doc.new)
47
+ end
48
+ @metadata = default_metadata.merge(options)
48
49
  @scheduling_strategy = nil
49
- @lock_properties = {driver: 'flock', driver_options: {}}
50
+ @lock_properties = {driver: nil, driver_options: {}}
50
51
  end
51
52
 
52
53
  def run
@@ -21,12 +21,15 @@ require 'blender/tasks/base'
21
21
  require 'blender/tasks/ruby'
22
22
  require 'blender/tasks/ssh'
23
23
  require 'blender/tasks/shell_out'
24
+ require 'blender/tasks/scp'
24
25
  require 'highline'
25
26
  require 'blender/utils/refinements'
26
27
  require 'blender/drivers/ssh'
28
+ require 'blender/drivers/ssh'
27
29
  require 'blender/drivers/ssh_multi'
28
30
  require 'blender/drivers/shellout'
29
31
  require 'blender/drivers/ruby'
32
+ require 'blender/drivers/scp'
30
33
  require 'blender/discovery'
31
34
  require 'blender/handlers/base'
32
35
  require 'blender/lock/flock'
@@ -116,6 +119,20 @@ module Blender
116
119
  append_task(:ssh, task)
117
120
  end
118
121
 
122
+ def scp_upload(name, &block)
123
+ task = build_task(name, :scp)
124
+ task.instance_eval(&block) if block_given?
125
+ task.direction = :upload
126
+ append_task(:scp, task)
127
+ end
128
+
129
+ def scp_download(name, &block)
130
+ task = build_task(name, :scp)
131
+ task.instance_eval(&block) if block_given?
132
+ task.direction = :download
133
+ append_task(:scp, task)
134
+ end
135
+
119
136
  def strategy(strategy)
120
137
  klass_name = camelcase(strategy.to_s).to_sym
121
138
  begin
@@ -21,7 +21,6 @@ module Blender
21
21
  class Base
22
22
  include Blender::Discovery
23
23
 
24
- attr_reader :guards
25
24
  attr_reader :metadata
26
25
  attr_reader :name
27
26
  attr_reader :hosts
@@ -20,7 +20,6 @@ require 'blender/tasks/base'
20
20
  module Blender
21
21
  module Task
22
22
  class Ruby < Blender::Task::Base
23
-
24
23
  attr_reader :code_block
25
24
 
26
25
  def execute(&block)
@@ -0,0 +1,41 @@
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/tasks/ssh'
19
+ require 'forwardable'
20
+
21
+ module Blender
22
+ module Task
23
+ class Scp < Blender::Task::Base
24
+ extend Forwardable
25
+ def_delegators :@command, :direction, :direction=
26
+ def initialize(name, metadata = {})
27
+ super
28
+ @command = Struct.new(:direction, :source, :target).new
29
+ @command.target = name
30
+ @command.source = name
31
+ @direction = :upload
32
+ end
33
+ def from(source)
34
+ @command.source = source
35
+ end
36
+ def to(target)
37
+ @command.target = target
38
+ end
39
+ end
40
+ end
41
+ end
@@ -19,7 +19,7 @@ module Blender
19
19
  module Refinements
20
20
  def camelcase(string)
21
21
  str = string.dup
22
- str.gsub!(/[^A-Za-z0-9_]/,'_')
22
+ str.gsub!(/[^A-Za-z0-9_]/, '_')
23
23
  rname = nil
24
24
  regexp = %r{^(.+?)(_(.+))?$}
25
25
  mn = str.match(regexp)
@@ -20,7 +20,6 @@ require 'blender/log'
20
20
  module Blender
21
21
  module Utils
22
22
  class ThreadPool
23
-
24
23
  def initialize(size)
25
24
  @size = size
26
25
  @queue = Queue.new
@@ -24,6 +24,7 @@ module Blender
24
24
  @mutex = Mutex.new
25
25
  @highline = HighLine.new
26
26
  end
27
+
27
28
  def puts(string)
28
29
  @mutex.synchronize do
29
30
  $stdout.puts(string)
@@ -45,7 +46,6 @@ module Blender
45
46
  def color(string, *colors)
46
47
  @highline.color(string, *colors)
47
48
  end
48
-
49
49
  end
50
50
  end
51
51
  end
@@ -16,5 +16,5 @@
16
16
  # limitations under the License.
17
17
 
18
18
  module Blender
19
- VERSION = '0.0.1'
19
+ VERSION = '0.1.0'
20
20
  end
@@ -0,0 +1,34 @@
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 'spec_helper'
19
+
20
+ describe Blender::Configuration do
21
+ it 'should populate argments and noop key' do
22
+ expect(Blender::Configuration[:noop]).to be(false)
23
+ expect(Blender::Configuration[:argments]).to be_empty
24
+ end
25
+ it 'should set configs globally' do
26
+ Blender::Configuration[:x] = 1
27
+ expect(Blender::Configuration[:x]).to eq(1)
28
+ end
29
+ it 'should reset config' do
30
+ Blender::Configuration[:x] = 1
31
+ Blender::Configuration.reset!
32
+ expect(Blender::Configuration[:x]).to be_empty
33
+ end
34
+ end
@@ -16,4 +16,35 @@ describe '#dsl' do
16
16
  allow_any_instance_of(Blender::Driver::Ssh).to receive(:execute)
17
17
  scheduler.run
18
18
  end
19
+
20
+ context '#scp' do
21
+ it '#upload' do
22
+ session = double('Net::SSH::Session', loop: true)
23
+ scp = double('Net::SSH::Scp')
24
+ expect(Net::SSH).to receive(:start).with('host1', 'x', password: 'y').and_return(session)
25
+ expect(session).to receive(:scp).and_return(scp)
26
+ expect(scp).to receive(:upload!).with('/local/path', '/remote/path')
27
+ Blender.blend('test') do |sched|
28
+ sched.config(:scp, user: 'x', password: 'y')
29
+ sched.members(['host1'])
30
+ sched.scp_upload '/remote/path' do
31
+ from '/local/path'
32
+ end
33
+ end
34
+ end
35
+ it '#download' do
36
+ session = double('Net::SSH::Session', loop: true)
37
+ scp = double('Net::SSH::Scp')
38
+ expect(Net::SSH).to receive(:start).with('host1', 'x', password: 'y').and_return(session)
39
+ expect(session).to receive(:scp).and_return(scp)
40
+ expect(scp).to receive(:download!).with('/remote/path', '/local/path')
41
+ Blender.blend('test') do |sched|
42
+ sched.members(['host1'])
43
+ sched.config(:scp, user: 'x', password: 'y')
44
+ sched.scp_download '/remote/path' do
45
+ to '/local/path'
46
+ end
47
+ end
48
+ end
49
+ end
19
50
  end
@@ -1,72 +1,75 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Blender::Lock do
4
-
5
- it 'should not allow two blender run with same name to run at the same time' do
6
-
7
- pid1 = fork do
8
- Blender.blend('test-1') do |sched|
9
- sched.members(['localhost'])
10
- sched.ruby_task('date') do
11
- execute do
12
- sleep 5
13
- puts 'This will succeed'
4
+ context 'File based locking' do
5
+ it 'should not allow two blender run with same lockfile to run at the same time' do
6
+ pid1 = fork do
7
+ Blender.blend('test-1') do |sched|
8
+ sched.lock_options('flock')
9
+ sched.members(['localhost'])
10
+ sched.ruby_task('date') do
11
+ execute do
12
+ sleep 5
13
+ puts 'This will succeed'
14
+ end
14
15
  end
15
16
  end
16
17
  end
17
- end
18
18
 
19
- pid2 = fork do
20
- STDERR.reopen(File::NULL)
21
- Blender.blend('test-1') do |sched|
22
- sched.members(['localhost'])
23
- sched.ruby_task('date') do
24
- execute do
25
- puts 'This will fail'
19
+ pid2 = fork do
20
+ STDERR.reopen(File::NULL)
21
+ Blender.blend('test-1') do |sched|
22
+ sched.lock_options('flock')
23
+ sched.members(['localhost'])
24
+ sched.ruby_task('date') do
25
+ execute do
26
+ puts 'This will fail'
27
+ end
26
28
  end
27
29
  end
28
30
  end
31
+
32
+ status1 = Process.wait2 pid1
33
+ status2 = Process.wait2 pid2
34
+ expect(status1.last.exitstatus).to eq(0)
35
+ expect(status2.last.exitstatus).to_not eq(0)
29
36
  end
30
- status1 = Process.wait2 pid1
31
- status2 = Process.wait2 pid2
32
- expect(status1.last.exitstatus).to eq(0)
33
- expect(status2.last.exitstatus).to_not eq(0)
34
- end
35
37
 
36
- it 'should allow two blender run with different name to run at the same time' do
37
- pid1 = fork do
38
- Blender.blend('test-1') do |sched|
39
- sched.members(['localhost'])
40
- sched.ruby_task('date') do
41
- execute do
42
- sleep 5
43
- puts 'This will succeed'
38
+ it 'should allow two blender run with different lock file to run at the same time' do
39
+ pid1 = fork do
40
+ Blender.blend('test-1') do |sched|
41
+ sched.lock_options('flock')
42
+ sched.members(['localhost'])
43
+ sched.ruby_task('date') do
44
+ execute do
45
+ sleep 5
46
+ puts 'This will succeed'
47
+ end
44
48
  end
45
49
  end
46
50
  end
47
- end
48
51
 
49
- pid2 = fork do
50
- Blender.blend('test-2') do |sched|
51
- sched.members(['localhost'])
52
- sched.ruby_task('date') do
53
- execute do
54
- puts 'This will succeed'
52
+ pid2 = fork do
53
+ Blender.blend('test-2') do |sched|
54
+ sched.lock_options('flock')
55
+ sched.members(['localhost'])
56
+ sched.ruby_task('date') do
57
+ execute do
58
+ puts 'This will succeed'
59
+ end
55
60
  end
56
61
  end
57
62
  end
63
+ status1 = Process.wait2 pid1
64
+ status2 = Process.wait2 pid2
65
+ expect(status1.last.exitstatus).to eq(0)
66
+ expect(status2.last.exitstatus).to eq(0)
58
67
  end
59
- status1 = Process.wait2 pid1
60
- status2 = Process.wait2 pid2
61
- expect(status1.last.exitstatus).to eq(0)
62
- expect(status2.last.exitstatus).to eq(0)
63
- end
64
-
65
- context 'File based locking with timeout' do
66
68
 
67
69
  it 'should raise lock acquisition error when times out' do
68
70
  pid1 = fork do
69
71
  Blender.blend('test-1') do |sched|
72
+ sched.lock_options('flock')
70
73
  sched.members(['localhost'])
71
74
  sched.ruby_task('date') do
72
75
  execute do
@@ -98,6 +101,7 @@ describe Blender::Lock do
98
101
  it 'should not raise lock acquisition error when able to acquire lock within timeout period' do
99
102
  pid1 = fork do
100
103
  Blender.blend('test-1') do |sched|
104
+ sched.lock_options('flock')
101
105
  sched.members(['localhost'])
102
106
  sched.ruby_task('date') do
103
107
  execute do
@@ -112,7 +116,7 @@ describe Blender::Lock do
112
116
  STDERR.reopen(File::NULL)
113
117
  Blender.blend('test-1') do |sched|
114
118
  sched.members(['localhost'])
115
- sched.lock_options('flock', timeout: 3)
119
+ sched.lock_options('flock', timeout: 10)
116
120
  sched.ruby_task('date') do
117
121
  execute do
118
122
  puts 'This will fail'
@@ -123,7 +127,7 @@ describe Blender::Lock do
123
127
  status1 = Process.wait2 pid1
124
128
  status2 = Process.wait2 pid2
125
129
  expect(status1.last.exitstatus).to eq(0)
126
- expect(status2.last.exitstatus).to_not eq(0)
130
+ expect(status2.last.exitstatus).to eq(0)
127
131
  end
128
132
  end
129
133
  end
@@ -4,6 +4,14 @@ describe Blender::Scheduler do
4
4
  let(:scheduler) do
5
5
  described_class.new('test')
6
6
  end
7
+ describe '#no_doc' do
8
+ it 'should not use document handler if no_doc option is passed' do
9
+ expect(Blender::Handlers::Doc).to_not receive(:new)
10
+ task = Blender::Task::Ruby.new('test')
11
+ sched = described_class.new('no_doc', [] , no_doc: true)
12
+ sched.run
13
+ end
14
+ end
7
15
  describe '#DSL' do
8
16
  subject(:task){scheduler.tasks.first}
9
17
  it '#ask' do
@@ -16,6 +16,7 @@
16
16
  # limitations under the License.
17
17
 
18
18
  require 'spec_helper'
19
+ require 'blender/cli'
19
20
 
20
21
  describe Blender do
21
22
  describe '#blend' do
@@ -33,5 +34,15 @@ describe Blender do
33
34
  end
34
35
  expect(x).to eq(100)
35
36
  end
37
+ context 'CLI' do
38
+ it 'should store additional arguments in config' do
39
+ Blender::CLI.start(%w{-f spec/data/example.rb -x -y -z})
40
+ expect(Blender::Configuration[:arguments]).to eq(%w{-x -y -z})
41
+ end
42
+ it 'should store additional arguments in config' do
43
+ expect(Blender::Handlers::Doc).to_not receive(:new)
44
+ Blender::CLI.start(%w{-q -f spec/data/example.rb})
45
+ end
46
+ end
36
47
  end
37
48
  end
@@ -4,7 +4,6 @@ ruby_task 'task2' do
4
4
  execute do |h|
5
5
  puts 'HelloWorld'
6
6
  end
7
- driver_options(stdout: $stdout)
8
7
  end
9
8
  shell_task 'task3' do
10
9
  execute 'does not exist'
@@ -1,9 +1,3 @@
1
- require 'simplecov'
2
- SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
3
- SimpleCov.start do
4
- add_filter '/spec/'
5
- add_filter '.bundle'
6
- end
7
1
 
8
2
  require 'rspec'
9
3
  require 'rspec/mocks'
@@ -27,9 +21,5 @@ RSpec.configure do |config|
27
21
  config.mock_with :rspec do |mocks|
28
22
  mocks.verify_doubled_constant_names = true
29
23
  end
30
- config.before(:each) do
31
- doc = double(Blender::Handlers::Doc).as_null_object
32
- allow(Blender::Handlers::Doc).to receive(:new).and_return(doc)
33
- end
34
24
  config.backtrace_exclusion_patterns = []
35
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pd-blender
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ranjib Dey
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-10 00:00:00.000000000 Z
11
+ date: 2015-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: net-scp
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rufus-scheduler
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -216,8 +230,10 @@ files:
216
230
  - lib/blender/drivers/base.rb
217
231
  - lib/blender/drivers/compound.rb
218
232
  - lib/blender/drivers/ruby.rb
233
+ - lib/blender/drivers/scp.rb
219
234
  - lib/blender/drivers/shellout.rb
220
235
  - lib/blender/drivers/ssh.rb
236
+ - lib/blender/drivers/ssh_exec.rb
221
237
  - lib/blender/drivers/ssh_multi.rb
222
238
  - lib/blender/event_dispatcher.rb
223
239
  - lib/blender/exceptions.rb
@@ -237,6 +253,7 @@ files:
237
253
  - lib/blender/scheduling_strategies/per_task.rb
238
254
  - lib/blender/tasks/base.rb
239
255
  - lib/blender/tasks/ruby.rb
256
+ - lib/blender/tasks/scp.rb
240
257
  - lib/blender/tasks/shell_out.rb
241
258
  - lib/blender/tasks/ssh.rb
242
259
  - lib/blender/timer.rb
@@ -245,6 +262,7 @@ files:
245
262
  - lib/blender/utils/ui.rb
246
263
  - lib/blender/version.rb
247
264
  - spec/blender/blender_rspec.rb
265
+ - spec/blender/config_spec.rb
248
266
  - spec/blender/discovery_spec.rb
249
267
  - spec/blender/drivers/ssh_multi_spec.rb
250
268
  - spec/blender/drivers/ssh_spec.rb
@@ -286,6 +304,7 @@ specification_version: 4
286
304
  summary: A modular orchestration engine
287
305
  test_files:
288
306
  - spec/blender/blender_rspec.rb
307
+ - spec/blender/config_spec.rb
289
308
  - spec/blender/discovery_spec.rb
290
309
  - spec/blender/drivers/ssh_multi_spec.rb
291
310
  - spec/blender/drivers/ssh_spec.rb