foreman_hooks 0.1.0 → 0.2.0

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.
data/README.md CHANGED
@@ -33,9 +33,9 @@ Each file within the directory is executed in alphabetical order.
33
33
 
34
34
  Examples:
35
35
 
36
+ ~foreman/config/hooks/host/create/50_register_system.sh
37
+ ~foreman/config/hooks/host/destroy/15_cleanup_database.sh
36
38
  ~foreman/config/hooks/smart_proxy/after_create/01_email_operations.sh
37
- ~foreman/config/hooks/host/before_provision/50_do_something.sh
38
- ~foreman/config/hooks/host/managed/after_destroy/15_cleanup_database.sh
39
39
 
40
40
  Note that in Foreman 1.1, hosts are just named `Host` so hooks go in a `host/`
41
41
  directory, while in Foreman 1.2 they're `Host::Base` and `Host::Managed`, so
@@ -50,7 +50,20 @@ Every object (or model in Rails terms) in Foreman can have hooks. Check
50
50
  * `host/discovered` (Foreman 1.2)
51
51
  * `report`
52
52
 
53
- ## Events
53
+ ## Orchestration events
54
+
55
+ Foreman supports orchestration tasks for hosts and NICs (each network
56
+ interface) which happen when the object is created, updated and destroyed.
57
+ These tasks are shown to the user in the UI and if they fail, will
58
+ automatically trigger a rollback of the action.
59
+
60
+ To add hooks to these, use these event names:
61
+
62
+ * `create`
63
+ * `update`
64
+ * `destroy`
65
+
66
+ ## Rails events
54
67
 
55
68
  These are the most interesting events that Rails provides and this plugin
56
69
  exposes:
@@ -71,9 +84,21 @@ The host object has two special callbacks in Foreman 1.1 that you can use:
71
84
  ## Execution of hooks
72
85
 
73
86
  Hooks are executed in the context of the Foreman server, so usually under the
74
- `foreman` user. One argument is provided, which is the string representation
75
- of the object that was hooked, e.g. the hostname for a host. No other data
76
- about the object is currently made available.
87
+ `foreman` user.
88
+
89
+ The first argument is always the event name, enabling scripts to be symlinked
90
+ into multiple event directories. The second argument is the string
91
+ representation of the object that was hooked, e.g. the hostname for a host.
92
+ No other data about the object is currently made available.
93
+
94
+ Every hook within the event directory is executed in alphabetical order. For
95
+ orchestration hooks, an integer prefix in the hook filename will be used as
96
+ the priority value, so influences where it's done in relation to DNS, DHCP, VM
97
+ creation and other tasks.
98
+
99
+ If a hook fails (non-zero return code), the event is logged.
100
+ For orchestration events, a failure will halt the action and rollback will
101
+ occur. For Rails events, execution of other hooks will continue.
77
102
 
78
103
  # Copyright
79
104
 
data/TODO CHANGED
@@ -1,3 +1,2 @@
1
1
  * pass more data into hooks
2
2
  * JSON dump of the model via stdin + utility shell script
3
- * tie into orchestration
@@ -1,8 +1,8 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "foreman_hooks"
3
3
 
4
- s.version = "0.1.0"
5
- s.date = "2013-03-23"
4
+ s.version = "0.2.0"
5
+ s.date = "2013-03-24"
6
6
 
7
7
  s.summary = "Run custom hook scripts on Foreman events"
8
8
  s.description = "Plugin engine for Foreman that enables running custom hook scripts on Foreman events"
@@ -1,13 +1,24 @@
1
1
  require 'foreman_hooks'
2
2
  require 'foreman_hooks/hooks_observer'
3
+ require 'foreman_hooks/orchestration_hook'
3
4
 
4
5
  module ForemanHooks
5
6
  class Engine < ::Rails::Engine
6
7
  config.to_prepare do
8
+ # Register an observer to all classes with hooks present
7
9
  ForemanHooks::HooksObserver.observed_classes.each do |klass|
8
10
  klass.observers << ForemanHooks::HooksObserver
9
11
  klass.instantiate_observers
10
12
  end
13
+
14
+ # Find any orchestration related hooks and register in those classes
15
+ ForemanHooks::HooksObserver.hooks.each do |klass,events|
16
+ orchestrate = false
17
+ events.keys.each do |event|
18
+ orchestrate = true if ['create', 'update', 'destroy'].include? event
19
+ end
20
+ klass.send(:include, ForemanHooks::OrchestrationHook) if orchestrate
21
+ end
11
22
  end
12
23
  end
13
24
  end
@@ -1,54 +1,8 @@
1
+ require 'foreman_hooks/util'
2
+
1
3
  module ForemanHooks
2
4
  class HooksObserver < ActiveRecord::Observer
3
- def self.logger
4
- Rails.logger
5
- end
6
-
7
- def self.hooks_root
8
- File.join(Rails.application.root, 'config', 'hooks')
9
- end
10
-
11
- # Find all executable hook files under $hook_root/model_name/event_name/
12
- def self.search_hooks
13
- hooks = {}
14
- Dir.glob(File.join(hooks_root, '**', '*')) do |filename|
15
- next if filename.end_with? '~'
16
- next if filename.end_with? '.bak'
17
- next if File.directory? filename
18
- next unless File.executable? filename
19
-
20
- relative = filename[hooks_root.size..-1]
21
- next unless relative =~ %r{^/(.+)/([^/]+)/([^/]+)$}
22
- klass = $1.camelize.constantize
23
- event = $2
24
- script_name = $3
25
- hooks[klass] ||= {}
26
- hooks[klass][event] ||= []
27
- hooks[klass][event] << filename
28
- logger.debug "Found hook to #{klass.to_s}##{event}, filename #{script_name}"
29
- end
30
- hooks
31
- end
32
-
33
- # {ModelClass => {'event_name' => ['/path/to/01.sh', '/path/to/02.sh']}}
34
- def self.hooks
35
- unless @hooks
36
- @hooks = search_hooks
37
- @hooks.each do |klass,events|
38
- events.each do |event,hooks|
39
- logger.info "Finished adding #{hooks.size} hooks to #{Host::Base.to_s}##{event}"
40
- hooks.sort!
41
- end
42
- end
43
- end
44
- @hooks
45
- end
46
-
47
- # ['event1', 'event2']
48
- def self.events
49
- @events = hooks.values.map(&:keys).flatten.uniq.map(&:to_sym) unless @events
50
- @events
51
- end
5
+ include ForemanHooks::Util
52
6
 
53
7
  # Override ActiveRecord::Observer
54
8
  def self.observed_classes
@@ -63,30 +17,12 @@ module ForemanHooks
63
17
  def method_missing(event, *args)
64
18
  obj = args.first
65
19
  logger.debug "Observed #{event} hook on #{obj}"
66
-
67
- return unless hooks = self.class.hooks[obj.class]
68
- return unless hooks = hooks[event.to_s]
69
- return if hooks.empty?
20
+ return unless hooks = find_hooks(obj.class, event)
70
21
 
71
22
  logger.debug "Running #{hooks.size} hooks for #{obj.class.to_s}##{event}"
72
- hooks.each { |filename| exec_hook(filename, obj.to_s) }
23
+ hooks.each { |filename| exec_hook(filename, event.to_s, obj.to_s) }
73
24
  end
74
25
 
75
- def exec_hook(*args)
76
- logger.debug "Running hook: #{args.join(' ')}"
77
- success = if defined? Bundler && Bundler.responds_to(:with_clean_env)
78
- Bundler.with_clean_env { system(*args) }
79
- else
80
- system(*args)
81
- end
82
-
83
- unless success
84
- logger.warn "Hook failure running `#{args.join(' ')}`: #{$?}"
85
- end
86
- end
87
-
88
- def logger
89
- Rails.logger
90
- end
26
+ def logger; Rails.logger; end
91
27
  end
92
28
  end
@@ -0,0 +1,63 @@
1
+ require 'foreman_hooks/util'
2
+
3
+ module ForemanHooks::OrchestrationHook
4
+ extend ActiveSupport::Concern
5
+ include ForemanHooks::Util
6
+
7
+ included do
8
+ after_validation :queue_hooks_validate
9
+ before_destroy :queue_hooks_destroy
10
+ end
11
+
12
+ def queue_hooks_validate
13
+ return unless errors.empty?
14
+ queue_hooks(new_record? ? 'create' : 'update')
15
+ end
16
+
17
+ def queue_hooks_destroy
18
+ return unless errors.empty?
19
+ queue_hooks('destroy')
20
+ end
21
+
22
+ def queue_hooks(event)
23
+ logger.debug "Observed #{event} hook on #{self}"
24
+ unless is_a? Orchestration
25
+ logger.warn "#{self.class.to_s} doesn't support orchestration, can't run orchestration hooks: use Rails events instead"
26
+ end
27
+
28
+ return unless hooks = find_hooks(self.class, event)
29
+ logger.debug "Queueing #{hooks.size} hooks for #{self.class.to_s}##{event}"
30
+
31
+ counter = 0
32
+ hooks.each do |filename|
33
+ basename = File.basename(filename)
34
+ priority = basename =~ /^(\d+)/ ? $1 : 10000 + (counter += 1)
35
+ logger.debug "Queuing hook #{basename} for #{self.class.to_s}##{event} at priority #{priority}"
36
+ queue.create(:name => "Hook: #{basename}", :priority => priority,
37
+ :action => [HookRunner.new(filename, self, event.to_s),
38
+ event.to_s == 'destroy' ? :hook_execute_del : :hook_execute_set])
39
+ end
40
+ end
41
+
42
+ # Orchestration runs methods against an object, so generate a runner for each
43
+ # hook that will need executing
44
+ class HookRunner
45
+ def initialize(filename, obj, event)
46
+ @filename = filename
47
+ @obj = obj
48
+ @event = event
49
+ end
50
+
51
+ def args
52
+ [@obj.to_s]
53
+ end
54
+
55
+ def hook_execute_set
56
+ @obj.exec_hook(@filename, @event, *args)
57
+ end
58
+
59
+ def hook_execute_del
60
+ @obj.exec_hook(@filename, 'destroy', *args)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,76 @@
1
+ module ForemanHooks::Util
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ class_eval do
6
+ def self.hooks_root
7
+ File.join(Rails.application.root, 'config', 'hooks')
8
+ end
9
+
10
+ # Find all executable hook files under $hook_root/model_name/event_name/
11
+ def self.discover_hooks
12
+ hooks = {}
13
+ Dir.glob(File.join(hooks_root, '**', '*')) do |filename|
14
+ next if filename.end_with? '~'
15
+ next if filename.end_with? '.bak'
16
+ next if File.directory? filename
17
+ next unless File.executable? filename
18
+
19
+ relative = filename[hooks_root.size..-1]
20
+ next unless relative =~ %r{^/(.+)/([^/]+)/([^/]+)$}
21
+ klass = $1.camelize.constantize
22
+ event = $2
23
+ script_name = $3
24
+ hooks[klass] ||= {}
25
+ hooks[klass][event] ||= []
26
+ hooks[klass][event] << filename
27
+ logger.debug "Found hook to #{klass.to_s}##{event}, filename #{script_name}"
28
+ end
29
+ hooks
30
+ end
31
+
32
+ # {ModelClass => {'event_name' => ['/path/to/01.sh', '/path/to/02.sh']}}
33
+ def self.hooks
34
+ unless @hooks
35
+ @hooks = discover_hooks
36
+ @hooks.each do |klass,events|
37
+ events.each do |event,hooks|
38
+ logger.info "Finished registering #{hooks.size} hooks for #{Host::Base.to_s}##{event}"
39
+ hooks.sort!
40
+ end
41
+ end
42
+ end
43
+ @hooks
44
+ end
45
+
46
+ # ['event1', 'event2']
47
+ def self.events
48
+ @events = hooks.values.map(&:keys).flatten.uniq.map(&:to_sym) unless @events
49
+ @events
50
+ end
51
+
52
+ def self.logger; Rails.logger; end
53
+ end
54
+ end
55
+
56
+ def find_hooks(klass, event)
57
+ return unless filtered = self.class.hooks[klass]
58
+ return unless filtered = filtered[event.to_s]
59
+ return if filtered.empty?
60
+ filtered
61
+ end
62
+
63
+ def exec_hook(*args)
64
+ logger.debug "Running hook: #{args.join(' ')}"
65
+ success = if defined? Bundler && Bundler.responds_to(:with_clean_env)
66
+ Bundler.with_clean_env { system(*args) }
67
+ else
68
+ system(*args)
69
+ end
70
+
71
+ unless success
72
+ logger.warn "Hook failure running `#{args.join(' ')}`: #{$?}"
73
+ end
74
+ success
75
+ end
76
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_hooks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-23 00:00:00.000000000 Z
12
+ date: 2013-03-24 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Plugin engine for Foreman that enables running custom hook scripts on
15
15
  Foreman events
@@ -32,6 +32,8 @@ files:
32
32
  - lib/foreman_hooks.rb
33
33
  - lib/foreman_hooks/engine.rb
34
34
  - lib/foreman_hooks/hooks_observer.rb
35
+ - lib/foreman_hooks/orchestration_hook.rb
36
+ - lib/foreman_hooks/util.rb
35
37
  - test/test_helper.rb
36
38
  - test/unit/host_observer_test.rb
37
39
  homepage: http://github.com/domcleal/foreman_hooks