Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominic Cleal committed Mar 24, 2013
0 parents commit a89da9b
Show file tree
Hide file tree
Showing 14 changed files with 1,018 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
.rvmrc
Gemfile.lock
test/foreman_app
.bundle
6 changes: 6 additions & 0 deletions .travis.yml
@@ -0,0 +1,6 @@
language: ruby
rvm:
- "1.8.7"
- "1.9.3"
before_install: rake test:foreman_prepare
script: rake test
15 changes: 15 additions & 0 deletions Gemfile
@@ -0,0 +1,15 @@
source "http://rubygems.org"

FOREMAN_GEMFILE=File.expand_path('../test/foreman_app/Gemfile', __FILE__)
unless File.exist?(FOREMAN_GEMFILE)
puts <<MESSAGE
Foreman source code is not present. To get the latest version, run:
rake test:foreman_prepare
and try again.
MESSAGE

else
self.instance_eval(Bundler.read_file(FOREMAN_GEMFILE))
end
619 changes: 619 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions README.md
@@ -0,0 +1,93 @@
# foreman_hooks

Allows you to trigger scripts and commands on the Foreman server at any point
in an object's lifecycle in Foreman. This lets you run a script when a host
is created, or finishes provisioning etc.

It observes every object in Foreman and exposes the Rails callbacks by running
scripts within its hooks directory.

# Installation:

Include in your `~foreman/bundler.d/foreman_hooks.rb`

gem 'foreman_hooks'

Or from git:

gem 'foreman_hooks', :git => "https://github.com/domcleal/foreman_hooks.git"

Regenerate Gemfile.lock:

cd ~foreman && sudo -u foreman bundle install

To upgrade to newest version of the plugin:

cd ~foreman && sudo -u foreman bundle update foreman_hooks

# Usage

Hooks are stored in `/usr/share/foreman/config/hooks` (`~foreman/config/hooks`)
with a subdirectory for the object, then a subdirectory for the event name.
Each file within the directory is executed in alphabetical order.

Examples:

~foreman/config/hooks/smart_proxy/after_create/01_email_operations.sh
~foreman/config/hooks/host/before_provision/50_do_something.sh
~foreman/config/hooks/host/managed/after_destroy/15_cleanup_database.sh

Note that in Foreman 1.1, hosts are just named `Host` so hooks go in a `host/`
directory, while in Foreman 1.2 they're `Host::Base` and `Host::Managed`, so
the hook directory becomes `host/base/` and `host/managed/` respectively.

## Objects / Models

Every object (or model in Rails terms) in Foreman can have hooks. Check
`~foreman/app/models` for the full list, but these are the interesting ones:

* `host` (Foreman 1.1), `host/managed` (Foreman 1.2)
* `host/discovered` (Foreman 1.2)
* `report`

## Events

These are the most interesting events that Rails provides and this plugin
exposes:

* `after_create`
* `after_destroy`

Every event has a "before" and "after" hook. For the full list, see the
Constants section at the bottom of the
[ActiveRecord::Callbacks](http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html)
documentation.

The host object has two special callbacks in Foreman 1.1 that you can use:

* `host/after_build` triggers when a host is put into Build mode(??)
* `host/before_provision` triggers... (??)

## Execution of hooks

Hooks are executed in the context of the Foreman server, so usually under the
`foreman` user. One argument is provided, which is the string representation
of the object that was hooked, e.g. the hostname for a host. No other data
about the object is currently made available.

# Copyright

Copyright (c) 2012-2013 Red Hat Inc.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
111 changes: 111 additions & 0 deletions Rakefile
@@ -0,0 +1,111 @@
# encoding: utf-8

require 'rubygems'
require 'rake'
require 'fileutils'

task :default => :test

ENGINE_DIR = File.expand_path('..', __FILE__)
FOREMAN_DIR = 'test/foreman_app'

namespace :test do
desc "Download latest foreman devel source and install dependencies"
task :foreman_prepare do
foreman_repo = 'https://github.com/theforeman/foreman.git'
foreman_gemfile = File.join(FOREMAN_DIR, "Gemfile")
unless File.exists?(foreman_gemfile)
puts "Foreman source code is not present at #{FOREMAN_DIR}"
puts "Downloading latest Foreman development branch into #{FOREMAN_DIR}..."
FileUtils.mkdir_p(FOREMAN_DIR)

unless system("git clone #{foreman_repo} #{FOREMAN_DIR}")
puts "Error while getting latest Foreman code from #{foreman_repo} into #{FOREMAN_DIR}"
fail
end
end

gemfile_content = File.read(foreman_gemfile)
unless gemfile_content.include?('FOREMAN_GEMFILE')
puts 'Preparing Gemfile'
gemfile_content.gsub!('__FILE__', 'FOREMAN_GEMFILE')
gemfile_content.insert(0, "FOREMAN_GEMFILE = __FILE__ unless defined? FOREMAN_GEMFILE\n")
File.open(foreman_gemfile, 'w') { |f| f << gemfile_content }
end

settings_file = "#{FOREMAN_DIR}/config/settings.yaml"
unless File.exists?(settings_file)
puts 'Preparing settings file'
FileUtils.copy("#{settings_file}.example", settings_file)
settings_content = File.read(settings_file)
settings_content.sub!('organizations_enabled: false', 'organizations_enabled: true')
settings_content << ":puppetgem: true\n"
File.open(settings_file, 'w') { |f| f << settings_content }
end

db_file = "#{FOREMAN_DIR}/config/database.yml"
unless File.exists?(db_file)
puts 'Preparing database file'
FileUtils.copy("#{db_file}.example", db_file)
end

["#{ENGINE_DIR}/.bundle/config", "#{FOREMAN_DIR}/.bundle/config"].each do |bundle_file|
unless File.exists?(bundle_file)
FileUtils.mkdir_p(File.dirname(bundle_file))
puts 'Preparing bundler configuration'
File.open(bundle_file, 'w') { |f| f << <<FILE }
---
BUNDLE_WITHOUT: console:development:fog:jsonp:libvirt:mysql:mysql2:ovirt:postgresql:vmware
FILE
end
end

local_gemfile = "#{FOREMAN_DIR}/bundler.d/Gemfile.local.rb"
unless File.exist?(local_gemfile)
File.open(local_gemfile, 'w') { |f| f << <<GEMFILE }
gem "puppet"
gem "facter"
GEMFILE
end

puts 'Installing dependencies...'
unless system('bundle install')
fail
end
end

task :db_prepare do
unless File.exists?(FOREMAN_DIR)
puts <<MESSAGE
Foreman source code not prepared. Run
rake test:foreman_prepare
to download foreman source and its dependencies
MESSAGE
fail
end

# once we are Ruby19 only, switch to block variant of cd
pwd = FileUtils.pwd
FileUtils.cd(FOREMAN_DIR)
unless system('rake db:test:prepare RAILS_ENV=test')
puts "Migrating database first"
system('rake db:migrate db:schema:dump db:test:prepare RAILS_ENV=test') || fail
end
FileUtils.cd(pwd)
end

task :set_loadpath do
%w[lib test].each do |dir|
$:.unshift(File.expand_path(dir, ENGINE_DIR))
end
end

task :all => [:db_prepare, :set_loadpath] do
Dir.glob('test/**/*_test.rb') { |f| require f.sub('test/','') unless f.include? '/foreman_app/' }
end

end

task :test => 'test:all'
3 changes: 3 additions & 0 deletions TODO
@@ -0,0 +1,3 @@
* pass more data into hooks
* JSON dump of the model via stdin + utility shell script
* tie into orchestration
Binary file added foreman_hooks-0.1.0.gem
Binary file not shown.
22 changes: 22 additions & 0 deletions foreman_hooks.gemspec
@@ -0,0 +1,22 @@
Gem::Specification.new do |s|
s.name = "foreman_hooks"

s.version = "0.1.0"
s.date = "2013-03-23"

s.summary = "Run custom hook scripts on Foreman events"
s.description = "Plugin engine for Foreman that enables running custom hook scripts on Foreman events"
s.homepage = "http://github.com/domcleal/foreman_hooks"
s.licenses = ["GPL-3"]
s.require_paths = ["lib"]

s.authors = ["Dominic Cleal"]
s.email = "dcleal@redhat.com"

s.extra_rdoc_files = [
"LICENSE",
"README.md",
"TODO"
]
s.files = `git ls-files`.split("\n")
end
3 changes: 3 additions & 0 deletions lib/foreman_hooks.rb
@@ -0,0 +1,3 @@
module ForemanHooks
require 'foreman_hooks/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
end
13 changes: 13 additions & 0 deletions lib/foreman_hooks/engine.rb
@@ -0,0 +1,13 @@
require 'foreman_hooks'
require 'foreman_hooks/hooks_observer'

module ForemanHooks
class Engine < ::Rails::Engine
config.to_prepare do
ForemanHooks::HooksObserver.observed_classes.each do |klass|
klass.observers << ForemanHooks::HooksObserver
klass.instantiate_observers
end
end
end
end
92 changes: 92 additions & 0 deletions lib/foreman_hooks/hooks_observer.rb
@@ -0,0 +1,92 @@
module ForemanHooks
class HooksObserver < ActiveRecord::Observer
def self.logger
Rails.logger
end

def self.hooks_root
File.join(Rails.application.root, 'config', 'hooks')
end

# Find all executable hook files under $hook_root/model_name/event_name/
def self.search_hooks
hooks = {}
Dir.glob(File.join(hooks_root, '**', '*')) do |filename|
next if filename.end_with? '~'
next if filename.end_with? '.bak'
next if File.directory? filename
next unless File.executable? filename

relative = filename[hooks_root.size..-1]
next unless relative =~ %r{^/(.+)/([^/]+)/([^/]+)$}
klass = $1.camelize.constantize
event = $2
script_name = $3
hooks[klass] ||= {}
hooks[klass][event] ||= []
hooks[klass][event] << filename
logger.debug "Found hook to #{klass.to_s}##{event}, filename #{script_name}"
end
hooks
end

# {ModelClass => {'event_name' => ['/path/to/01.sh', '/path/to/02.sh']}}
def self.hooks
unless @hooks
@hooks = search_hooks
@hooks.each do |klass,events|
events.each do |event,hooks|
logger.info "Finished adding #{hooks.size} hooks to #{Host::Base.to_s}##{event}"
hooks.sort!
end
end
end
@hooks
end

# ['event1', 'event2']
def self.events
@events = hooks.values.map(&:keys).flatten.uniq.map(&:to_sym) unless @events
@events
end

# Override ActiveRecord::Observer
def self.observed_classes
hooks.keys
end

def respond_to?(method)
return true if super
self.class.events.include? method
end

def method_missing(event, *args)
obj = args.first
logger.debug "Observed #{event} hook on #{obj}"

return unless hooks = self.class.hooks[obj.class]
return unless hooks = hooks[event.to_s]
return if hooks.empty?

logger.debug "Running #{hooks.size} hooks for #{obj.class.to_s}##{event}"
hooks.each { |filename| exec_hook(filename, obj.to_s) }
end

def exec_hook(*args)
logger.debug "Running hook: #{args.join(' ')}"
success = if defined? Bundler && Bundler.responds_to(:with_clean_env)
Bundler.with_clean_env { system(*args) }
else
system(*args)
end

unless success
logger.warn "Hook failure running `#{args.join(' ')}`: #{$?}"
end
end

def logger
Rails.logger
end
end
end

0 comments on commit a89da9b

Please sign in to comment.