doable 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.
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: