doable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 56e965ddda372a9869ce205446246564c6ccff61
4
+ data.tar.gz: 2b7a330755f47d29ab09ea8534573b5091ce277a
5
+ SHA512:
6
+ metadata.gz: c0f5a9c98bf55b5f0a6e68283605df03ee2c643f43b395d5ec25f387934289fab656128bb828b13cec3c6ed0d07486187110fd40d970b6bab6daaaae542cb1ce
7
+ data.tar.gz: 0475b2cc4859475763911c159eec725881b9a7297e045b2657d96bb111af45e8011ef1223aa2c1ef57d2cee859799d2b069ea60bdb66e52e4fbb1274c0ec5795
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jonathan Gnagy <jonathan.gnagy@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,34 @@
1
+ module Doable
2
+ module Exceptions
3
+ module FrameworkExceptions
4
+ class GenericFrameworkError < StandardError
5
+ end
6
+
7
+ class InvalidInput < GenericFrameworkError
8
+ end
9
+
10
+ class NotApplicable < InvalidInput
11
+ end
12
+
13
+ class InvalidAction < GenericFrameworkError
14
+ end
15
+
16
+ class MissingDefinitionsDirectory < GenericFrameworkError
17
+ end
18
+
19
+ class MissingParameter < InvalidInput
20
+ end
21
+
22
+ # This exception should be used by those rare features that aren't cross-platform
23
+ class UnsupportedPlatformFeature < GenericFrameworkError
24
+ end
25
+
26
+ # !These exceptions should never be caught by anything!
27
+ class SkipStep < StandardError
28
+ end
29
+
30
+ class RolledBack < StandardError
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ module Doable
2
+ module Helpers
3
+ module FrameworkHelpers
4
+ # raises a special skip exception and __must__ only be run in job steps
5
+ # @raise [SkipStep] Do __not__ handle this exception!
6
+ def skip(message = "Skipping Step")
7
+ raise SkipStep, message
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+ module Doable
2
+ module Helpers
3
+ module LoggingHelpers
4
+ # Create a mutex to manage logging to STDOUT
5
+ LOGGING_MUTEX = Mutex.new
6
+ LOGGING_MUTEX.freeze
7
+
8
+ # Applies a color (with optional addition settings) to some text
9
+ # @return [String]
10
+ # @param text [String] The String to be colorized
11
+ # @param color [Symbol] The color to use. Must be one of [ :gray, :red, :green, :yellow, :blue, :magenta, :cyan, :white ]
12
+ def colorize(text, color, options = {})
13
+ background = options[:background] || options[:bg] || false
14
+ style = options[:style].to_sym if options[:style]
15
+ offsets = [ :gray, :red, :green, :yellow, :blue, :magenta, :cyan, :white ]
16
+ styles = [ :normal, :bold, :dark, :italic, :underline, :xx, :xx, :underline, :xx, :strikethrough ]
17
+ start = background ? 40 : 30
18
+ color_code = start + (offsets.index(color) || 8)
19
+ style_code = styles.index(style) || 0
20
+ "\e[#{style_code};#{color_code}m#{text}\e[0m"
21
+ end
22
+
23
+ # All logging or writing to STDOUT should happen here (to be threadsafe)
24
+ def log(text, level = :info)
25
+ level = level.to_sym
26
+
27
+ color, options = case level
28
+ when :info
29
+ [:white, {}]
30
+ when :warn
31
+ [:yellow, {}]
32
+ when :error
33
+ [:red, {:bg => true}]
34
+ when :success
35
+ [:green, {}]
36
+ else
37
+ [:gray, {}]
38
+ end
39
+
40
+ LOGGING_MUTEX.synchronize {
41
+ puts "[#{Time.now.strftime('%Y/%m/%d %H:%M:%S')}] #{colorize(text, color, options)}"
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,15 @@
1
+ module Doable
2
+ module Helpers
3
+ module PasswordHelpers
4
+ # Generates a password
5
+ # @param length [Fixnum] Length of the password
6
+ # @param options [Hash] Options hash for specifying password details
7
+ # @return [String]
8
+ def generate_password(length = 8, options = {})
9
+ options[:characters] ||= [*(2..9), *('a'..'z'), *('A'..'Z')] - %w(i l O)
10
+
11
+ (1..length).collect{|a| options[:characters][rand(options[:characters].size)] }.join
12
+ end # generate_password()
13
+ end
14
+ end
15
+ end
data/lib/doable/job.rb ADDED
@@ -0,0 +1,224 @@
1
+ module Doable
2
+ # The Job class is responsible for describing the process of running some set of steps.
3
+ # It utilizes a very specific DSL for defining what steps need executing, along with their order. It
4
+ # can also describe how to recover when things break and provides hooks and triggers to make more flexible
5
+ # scripts for varying environments.
6
+ class Job
7
+ include Helpers::FrameworkHelpers
8
+ include Helpers::LoggingHelpers
9
+ attr_reader :steps, :hooks, :handlers, :threads
10
+
11
+ def self.plan(&block)
12
+ self.new(&block)
13
+ end
14
+
15
+ def initialize
16
+ @hooks = {}
17
+ @steps = []
18
+ @handlers = {}
19
+ @threads = []
20
+ yield self
21
+ end
22
+
23
+ # Registers a hook action to be performed when the hook is triggered
24
+ # @param hook [Symbol] Name of the hook to register the action with
25
+ # @param options [Hash]
26
+ # @param block [Proc]
27
+ # @return [Step]
28
+ def on(hook, options = {}, &block)
29
+ @hooks[hook] ||= []
30
+ @hooks[hook] << Step.new(self, options, &block)
31
+ end # on()
32
+
33
+ # Adds a step to the queue
34
+ # @param options [Hash]
35
+ # @param block [Proc]
36
+ # @return [Step]
37
+ def step(options = {}, &block)
38
+ @steps << Step.new(self, options, &block)
39
+ end # step()
40
+
41
+ # Registers an action to be performed before normal step execution
42
+ # @param options [Hash]
43
+ # @param block [Proc]
44
+ # @return [Step]
45
+ def before(options = {}, &block)
46
+ on(:before, options, &block)
47
+ end # before()
48
+
49
+ # Registers an action to be performed after normal execution completes
50
+ # @param options [Hash]
51
+ # @param block [Proc]
52
+ # @return [Boolean]
53
+ def after(options = {}, &block)
54
+ on(:after, options, &block)
55
+ end # after()
56
+
57
+ # Add a step to the queue, but first wrap it in a begin..rescue
58
+ # WARNING! Exception handlers are __not__ used with these steps, as they never actually raise exceptions
59
+ # @param options [Hash]
60
+ # @param block [Proc]
61
+ # @return [Boolean]
62
+ def attempt(options = {}, &block)
63
+ @steps << Step.new(self, options) do
64
+ begin
65
+ block.call
66
+ rescue SkipStep => e
67
+ raise e # We'll rescue this somewhere higher up the stack
68
+ rescue => e
69
+ log "Ignoring Exception in attempted step: #{colorize("#{e.class}: (#{e.message})", :red)}"
70
+ end
71
+ end
72
+ return true
73
+ end # attempt()
74
+
75
+ # Allow running steps in the background
76
+ # @param options [Hash]
77
+ # @param block [Proc]
78
+ # @return [Step]
79
+ def background(options = {}, &block)
80
+ @steps << Step.new(self, options) do
81
+ @threads << Thread.new { block.call }
82
+ end
83
+ end # background()
84
+
85
+ # Check if background steps are running
86
+ # @return [Boolean]
87
+ def multitasking?
88
+ return @threads.collect {|t| t if t.alive? }.compact.empty? ? false : true
89
+ end # multitasking?()
90
+
91
+ # Trigger a rollback of the entire Job, based on calls to #rollback!() on each eligible Step
92
+ def rollback!
93
+ log "Rolling Back...", :warn
94
+ @hooks[:after].reverse.each {|s| s.rollback! if s.rollbackable? }
95
+ @steps.reverse.each {|s| s.rollback! if s.rollbackable? }
96
+ @hooks[:before].reverse.each {|s| s.rollback! if s.rollbackable? }
97
+ log "Rollback complete!", :warn
98
+ raise RolledBack
99
+ end # rollback!()
100
+
101
+ # Returns the binding context of the Job
102
+ # @return [Binding]
103
+ def context
104
+ binding
105
+ end # context()
106
+
107
+ # Register a handler for named exception
108
+ # @param exception [String,StandardError] Exception to register handler for
109
+ # @param block [Proc]
110
+ def handle(exception, &block)
111
+ @handlers[exception] = Step.new(self, &block)
112
+ end # handle()
113
+
114
+ # This triggers a block associated with a hook
115
+ # @param hook [Symbol] Hook to trigger
116
+ def trigger(hook)
117
+ @hooks[hook].each_with_index do |step, index|
118
+ begin
119
+ step.call
120
+ rescue SkipStep => e
121
+ step.skip
122
+ log e.message, :warn
123
+ rescue => e
124
+ if @handlers[e.message]
125
+ log "Handling #{e.class}: (#{e.message})", :warn
126
+ @handlers[e.message].call(e, step)
127
+ step.handled unless step.status == :skipped # Don't mark the step as "handled" if it was skipped
128
+ elsif @handlers[e.class]
129
+ log "Handling #{e.class}: (#{e.message})", :warn
130
+ @handlers[e.class].call(e, step)
131
+ step.handled unless step.status == :skipped # Don't mark the step as "handled" if it was skipped
132
+ else
133
+ # Check the ancestry of the exception to see if any lower level Exception classes are caught
134
+ e.class.ancestors[1..-4].each do |ancestor|
135
+ if @handlers[ancestor]
136
+ log "Handling #{e.class}: (#{e.message}) via handler for #{ancestor}", :warn
137
+ @handlers[ancestor].call(e, step)
138
+ step.handled unless step.status == :skipped # Don't mark the step as "handled" if it was skipped
139
+ end # if @@handlers[ancestor]
140
+ end
141
+
142
+ unless step.successful?
143
+ message = "\n\nUnhandled Exception in #{colorize("hooks##{hook}[#{index}]", :yellow)}: #{colorize("#{e.class}: (#{e.message})", :red)}\n\n"
144
+ if @config.auto_rollback
145
+ log message
146
+ rollback!
147
+ else
148
+ raise message
149
+ end
150
+ end # unless
151
+ end
152
+ end # begin()
153
+ end if @hooks[hook] # each_with_index()
154
+ end # trigger()
155
+
156
+ # Here we actually trigger the execution of a Job
157
+ def run
158
+ #merge_config FILE_CONFIG
159
+ #merge_config CLI_CONFIG
160
+ ## Run our defaults Proc to merge in any default configs
161
+ #@defaults.call(@@config)
162
+
163
+ # before hooks
164
+ trigger(:before)
165
+
166
+ # Actual installer steps
167
+ @steps.each_with_index do |step, index|
168
+ begin
169
+ step.call
170
+ rescue SkipStep => e
171
+ step.skip
172
+ log e.message, :warn
173
+ rescue => e
174
+ if @handlers[e.message]
175
+ log "Handling #{e.class}: (#{e.message})", :warn
176
+ @handlers[e.message].call(e, step)
177
+ step.handled
178
+ elsif @handlers[e.class]
179
+ log "Handling #{e.class}: (#{e.message})", :warn
180
+ @handlers[e.class].call(e, step)
181
+ step.handled
182
+ else
183
+ # Check the ancestry of the exception to see if any lower level Exception classes are caught
184
+ e.class.ancestors[1..-4].each do |ancestor|
185
+ if @handlers[ancestor]
186
+ log "Handling #{e.class}: (#{e.message}) via handler for #{ancestor}", :warn
187
+ @handlers[ancestor].call(e, step)
188
+ step.handled
189
+ end # if @handlers[ancestor]
190
+ end
191
+
192
+ unless step.successful?
193
+ message = "\n\nUnhandled Exception in #{colorize("steps[#{index}]", :yellow)}: #{colorize("#{e.class}: (#{e.message})", :red)}\n\n"
194
+ #if @config.auto_rollback
195
+ # log message
196
+ # rollback!
197
+ #else
198
+ raise message
199
+ #end
200
+ end # unless
201
+ end # if @handlers...
202
+ end # rescue
203
+ end # @steps.each_with_index
204
+
205
+ # after hooks
206
+ trigger(:after)
207
+
208
+ # bring together all background threads
209
+ unless @threads.empty?
210
+ log "Cleaning up background tasks..."
211
+ @threads.each do |t|
212
+ begin
213
+ t.join
214
+ rescue => e
215
+ # We don't really need to do anything here,
216
+ # we've already handled or died from aborted Threads
217
+ end
218
+ end
219
+ end
220
+
221
+ log "All Job steps completed successfully!", :success # This should only happen if everything goes well
222
+ end # run()
223
+ end
224
+ end
@@ -0,0 +1,75 @@
1
+ module Doable
2
+ # This class contains the actual work to be done
3
+ class Step
4
+ attr_reader :success, :status, :task, :name
5
+
6
+ # @param options [Hash]
7
+ # @param block [Proc]
8
+ def initialize(context, options = {}, &block)
9
+ @context = context
10
+ @success = false
11
+ @status = :unattempted
12
+ @task = block
13
+ @name = options.key?(:name) ? options[:key] : block.to_s
14
+ @rollback = nil
15
+ end
16
+
17
+ # Call our step, passing any arguments to the block (task) itself
18
+ def call(*args)
19
+ @status = :failed # first assume our attempt fails
20
+ @context.instance_exec(*args, &@task) # try to execute the Step's task
21
+ @success = true # we'll only get here if no exceptions were raised
22
+ @status = :completed # if we're here, the Step is complete
23
+ return self
24
+ end # call()
25
+
26
+ # Set the Step's status to 'handled'
27
+ def handled
28
+ @status = :handled
29
+ @success = true # Might want to change this to false...
30
+ end # handled
31
+
32
+ # Boolean way of requesting success
33
+ # @return [Boolean]
34
+ def successful?
35
+ return @success
36
+ end
37
+
38
+ # Wrapper around call used for retrying a step. Recommended approach to retrying as this may be
39
+ # enhanced in the future.
40
+ def retry(*args)
41
+ call(*args)
42
+ end
43
+
44
+ # allow skipping steps (needs to be called externally)
45
+ def skip
46
+ @status = :skipped
47
+ @success = false
48
+ end
49
+
50
+ # Sets up a block to call when rolling back this step
51
+ # @return [Boolean]
52
+ def rollback(&block)
53
+ @rollback = block
54
+ return true
55
+ end
56
+
57
+ # Query a step to see if it can be rolled back
58
+ # @return [Boolean]
59
+ def rollbackable?
60
+ if @rollback and [:completed, :handled, :failed].include?(@status)
61
+ return true
62
+ else
63
+ return false
64
+ end
65
+ end
66
+
67
+ # Actually rollback this step
68
+ def rollback!
69
+ @rollback.call
70
+ @status = :rolledback
71
+ @success = false
72
+ end
73
+
74
+ end # class Step
75
+ end
data/lib/doable.rb ADDED
@@ -0,0 +1,17 @@
1
+ # Standard Library includes
2
+ require 'digest'
3
+ require 'base64'
4
+ require 'fileutils'
5
+ require 'tempfile'
6
+ require 'ostruct'
7
+ require 'erb'
8
+ require 'thread'
9
+ require 'pathname'
10
+ require 'yaml'
11
+ # Framework requirements
12
+ require 'doable/exceptions/framework_exceptions'
13
+ include Doable::Exceptions::FrameworkExceptions
14
+ require 'doable/helpers/logging_helpers'
15
+ require 'doable/helpers/framework_helpers'
16
+ require 'doable/step'
17
+ require 'doable/job'
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: doable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Gnagy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-29 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A framework for automating tasks with ease
14
+ email: jonathan.gnagy@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - lib/doable.rb
21
+ - lib/doable/exceptions/framework_exceptions.rb
22
+ - lib/doable/helpers/framework_helpers.rb
23
+ - lib/doable/helpers/logging_helpers.rb
24
+ - lib/doable/helpers/password_helpers.rb
25
+ - lib/doable/job.rb
26
+ - lib/doable/step.rb
27
+ homepage:
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.2.0
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Ruby Doable Framework
51
+ test_files: []
52
+ has_rdoc: