foreman_hooks 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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