From 2a87c63a9cde7289b950a694b9a902e80f134add Mon Sep 17 00:00:00 2001 From: Tyler Flint Date: Sat, 10 May 2014 22:14:58 -0600 Subject: [PATCH] imported lib and resources from core gem; created bin script --- .gitignore | 22 +++++ Gemfile | 4 + LICENSE.txt | 22 +++++ README.md | 31 ++++++- Rakefile | 2 + bin/hooky | 44 +++++++++ hooky.gemspec | 28 ++++++ lib/hooky.rb | 20 ++++ lib/hooky/converginator.rb | 118 ++++++++++++++++++++++++ lib/hooky/db.rb | 37 ++++++++ lib/hooky/dsl.rb | 40 ++++++++ lib/hooky/error.rb | 6 ++ lib/hooky/exit.rb | 22 +++++ lib/hooky/helpers.rb | 112 +++++++++++++++++++++++ lib/hooky/hook.rb | 36 ++++++++ lib/hooky/registry.rb | 53 +++++++++++ lib/hooky/resource.rb | 18 ++++ lib/hooky/resource/base.rb | 95 +++++++++++++++++++ lib/hooky/resource/cron.rb | 26 ++++++ lib/hooky/resource/directory.rb | 67 ++++++++++++++ lib/hooky/resource/execute.rb | 157 ++++++++++++++++++++++++++++++++ lib/hooky/resource/file.rb | 71 +++++++++++++++ lib/hooky/resource/hook_file.rb | 80 ++++++++++++++++ lib/hooky/resource/link.rb | 61 +++++++++++++ lib/hooky/resource/mount.rb | 77 ++++++++++++++++ lib/hooky/resource/rsync.rb | 67 ++++++++++++++ lib/hooky/resource/scp.rb | 87 ++++++++++++++++++ lib/hooky/resource/service.rb | 60 ++++++++++++ lib/hooky/resource/template.rb | 87 ++++++++++++++++++ lib/hooky/resource/warning.rb | 87 ++++++++++++++++++ lib/hooky/resource/zfs.rb | 98 ++++++++++++++++++++ lib/hooky/resources.rb | 12 +++ lib/hooky/version.rb | 3 + 33 files changed, 1748 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 LICENSE.txt create mode 100644 Rakefile create mode 100755 bin/hooky create mode 100644 hooky.gemspec create mode 100644 lib/hooky.rb create mode 100644 lib/hooky/converginator.rb create mode 100644 lib/hooky/db.rb create mode 100644 lib/hooky/dsl.rb create mode 100644 lib/hooky/error.rb create mode 100644 lib/hooky/exit.rb create mode 100644 lib/hooky/helpers.rb create mode 100644 lib/hooky/hook.rb create mode 100644 lib/hooky/registry.rb create mode 100644 lib/hooky/resource.rb create mode 100644 lib/hooky/resource/base.rb create mode 100644 lib/hooky/resource/cron.rb create mode 100644 lib/hooky/resource/directory.rb create mode 100644 lib/hooky/resource/execute.rb create mode 100644 lib/hooky/resource/file.rb create mode 100644 lib/hooky/resource/hook_file.rb create mode 100644 lib/hooky/resource/link.rb create mode 100644 lib/hooky/resource/mount.rb create mode 100644 lib/hooky/resource/rsync.rb create mode 100644 lib/hooky/resource/scp.rb create mode 100644 lib/hooky/resource/service.rb create mode 100644 lib/hooky/resource/template.rb create mode 100644 lib/hooky/resource/warning.rb create mode 100644 lib/hooky/resource/zfs.rb create mode 100644 lib/hooky/resources.rb create mode 100644 lib/hooky/version.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31cafb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp +*.bundle +*.so +*.o +*.a +mkmf.log diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..c24f7ad --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in hooky.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..5a1d3a4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2014 Tyler Flint + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 6a82308..d9a3a04 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# hooky -a ruby framework to provide hooky scripts with re-usable components and resources via an elegant dsl +# Hooky + +TODO: Write a gem description + +## Installation + +Add this line to your application's Gemfile: + + gem 'hooky' + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install hooky + +## Usage + +TODO: Write usage instructions here + +## Contributing + +1. Fork it ( https://github.com/[my-github-username]/hooky/fork ) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..809eb56 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +require "bundler/gem_tasks" + diff --git a/bin/hooky b/bin/hooky new file mode 100755 index 0000000..a69e15a --- /dev/null +++ b/bin/hooky @@ -0,0 +1,44 @@ +#!/usr/bin/env ruby + +HOOKS_DIR = "#{Dir.pwd}/tmp" + +hook = ARGV.shift +mod = nil +script = nil + +if not hook + $stderr.puts "hook is required" + exit 1 +end + +hook.match '(.+)\[(.+)\]' do |m| + mod = m[1] + script = m[2] +end + +if not (mod and script) + $stderr.puts "invalid hook format, expecting: php[default-configure]" + exit 1 +end + +if not File.exists? "#{HOOKS_DIR}/#{mod}/hooks/#{script}" + $stderr.puts "hook: #{hook} does not exist" +end + +# TODO: find a way to conditionally trigger this if dev only +lib = File.expand_path('../../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) + +require 'hooky' + +include Hooky::Hook # payload helpers +include Hooky::DSL # awesome resource library + +set :hook_root, "#{HOOKS_DIR}/#{mod}" + +# require hook libs +Dir.glob("#{HOOKS_DIR}/#{mod}/lib/*.rb").sort.each do |file| + require file +end + +load "#{HOOKS_DIR}/#{mod}/hooks/#{script}" diff --git a/hooky.gemspec b/hooky.gemspec new file mode 100644 index 0000000..798a812 --- /dev/null +++ b/hooky.gemspec @@ -0,0 +1,28 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'hooky/version' + +Gem::Specification.new do |spec| + spec.name = "hooky" + spec.version = Hooky::VERSION + spec.authors = ["Tyler Flint"] + spec.email = ["tyler@pagodabox.com"] + spec.summary = %q{Hooky is the framework to provide hooky scripts with re-usable components and resources via an elegant dsl.} + spec.description = %q{The core framework to provide hooky scripts with re-usable components.} + spec.homepage = "" + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0") + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ["lib"] + + spec.add_dependency 'tilt' + spec.add_dependency 'erubis' + spec.add_dependency 'oj' + spec.add_dependency 'multi_json', '>= 1.3' + + spec.add_development_dependency "bundler", "~> 1.6" + spec.add_development_dependency "rake" +end diff --git a/lib/hooky.rb b/lib/hooky.rb new file mode 100644 index 0000000..ebdde4a --- /dev/null +++ b/lib/hooky.rb @@ -0,0 +1,20 @@ +require 'hooky/converginator' +require 'hooky/db' +require 'hooky/dsl' +require 'hooky/error' +require 'hooky/exit' +require 'hooky/helpers' +require 'hooky/hook' +require 'hooky/registry' +require 'hooky/resource' +require "hooky/version" + +module Hooky + extend self + + def resources + @resources ||= Hooky::Registry.new + end +end + +require 'hooky/resources' \ No newline at end of file diff --git a/lib/hooky/converginator.rb b/lib/hooky/converginator.rb new file mode 100644 index 0000000..0f710f3 --- /dev/null +++ b/lib/hooky/converginator.rb @@ -0,0 +1,118 @@ +module Hooky + class Converginator + + def initialize(map, list) + @map = map + @list = list + end + + def converge! + output = {} + @map.each do |key, template| + if @list.key? key + output[key] = converge_value template, @list[key] + else + output[key] = template[:default] + end + end + output + end + + def converge_value(template, value) + if valid? template, value + value + else + template[:default] + end + end + + def valid?(template, value) + valid_type?(template, value) and valid_value?(template, value) + end + + def valid_type?(template, value) + case template[:type] + when :array + valid_array? template, value + when :byte + valid_byte? value + when :file + valid_file? value + when :folder + valid_folder? value + when :hash + valid_hash? value + when :integer + valid_integer? value + when :on_off + valid_on_off? value + when :string + valid_string? value + end + end + + def valid_value?(template, value) + + return true if not template.key? :from + + if template[:type] == :array + !( value.map {|element| template[:from].include? element} ).include? false + else + template[:from].include? value + end + end + + def valid_string?(element) + element.is_a? String + end + + def valid_array?(template, value) + + return false if not value.is_a? Array + + return true if not template.key? :of + + case template[:of] + when :byte + !( value.map {|element| valid_byte? element} ).include? false + when :file + !( value.map {|element| valid_file? element} ).include? false + when :folder + !( value.map {|element| valid_folder? element} ).include? false + when :integer + !( value.map {|element| valid_integer? element} ).include? false + when :on_off + !( value.map {|element| valid_on_off? element} ).include? false + when :string + !( value.map {|element| valid_string? element} ).include? false + else + true + end + end + + def valid_hash?(value) + value.is_a? Hash + end + + def valid_integer?(value) + value.is_a? Integer || (value.to_i.to_s == value.to_s) + end + + def valid_file?(value) + value =~ /^\/?[^\/]+(\/[^\/]+)*$/ + end + + def valid_folder?(value) + value =~ /^\/?[^\/]+(\/[^\/]+)*\/?$/ + end + + def valid_on_off?(value) + ['true', 'false', 'On', 'on', 'Off', 'off', '1', '0'].include? value.to_s + end + + def valid_byte?(value) + value.to_s =~ /^\d+[BbKkMmGgTt]?$/ + end + + end +end \ No newline at end of file diff --git a/lib/hooky/db.rb b/lib/hooky/db.rb new file mode 100644 index 0000000..8b1df2d --- /dev/null +++ b/lib/hooky/db.rb @@ -0,0 +1,37 @@ +require 'oj' +require 'multi_json' +require 'fileutils' + +module Hooky + class DB + + DEFAULT_PATH = '/var/db/hooky/db.json' + + def initialize(path=nil) + @path = path || DEFAULT_PATH + end + + def fetch(key) + data[key] + end + + def put(key, value) + data[key] = value + save + end + + def load + ::MultiJson.load(::File.read(@path)) rescue {} + end + + def save + ::FileUtils.mkdir_p(File.dirname(@path)) + ::File.write(@path, ::MultiJson.dump(data)) + end + + def data + @data ||= load + end + + end +end \ No newline at end of file diff --git a/lib/hooky/dsl.rb b/lib/hooky/dsl.rb new file mode 100644 index 0000000..97108b5 --- /dev/null +++ b/lib/hooky/dsl.rb @@ -0,0 +1,40 @@ +module Hooky + module DSL + + def dict + @dict ||= {} + end + + def set(key, value) + dict[key] = value + end + + def get(key) + dict[key] + end + + def method_missing(method_symbol, *args, &block) + resource_klass = Hooky.resources.get(method_symbol) + if resource_klass + resource = resource_klass.new(*args) + resource.dict = dict + resource.instance_eval(&block) if block_given? + if resource.can_run? + actions = resource.action + if actions.length > 1 + res = {} + actions.each do |action| + res[action] = resource.run action + end + res + else + resource.run actions.first + end + end + else + super + end + end + + end +end \ No newline at end of file diff --git a/lib/hooky/error.rb b/lib/hooky/error.rb new file mode 100644 index 0000000..d160d11 --- /dev/null +++ b/lib/hooky/error.rb @@ -0,0 +1,6 @@ +module Hooky + module Error + class UnexpectedExit < StandardError; end + class UnknownAction < StandardError; end + end +end \ No newline at end of file diff --git a/lib/hooky/exit.rb b/lib/hooky/exit.rb new file mode 100644 index 0000000..4a13026 --- /dev/null +++ b/lib/hooky/exit.rb @@ -0,0 +1,22 @@ +module Hooky + module Exit + SUCCESS = 0 + ERROR = 1 + ERROR_RETRY = 2 + WAIT_2 = 11 + WAIT_4 = 12 + WAIT_8 = 13 + WAIT_16 = 14 + WAIT_32 = 15 + WAIT_64 = 16 + WAIT_128 = 17 + WAIT_256 = 18 + WAIT_512 = 19 + WAIT_1024 = 20 + ABORT = 21 + ABORT_MSG = 22 + SUCCESS_FLAG = 31 + SUCCESS_MSG = 32 + WTF = 255 + end +end \ No newline at end of file diff --git a/lib/hooky/helpers.rb b/lib/hooky/helpers.rb new file mode 100644 index 0000000..c85fa20 --- /dev/null +++ b/lib/hooky/helpers.rb @@ -0,0 +1,112 @@ +module Hooky + module Helpers + + def is_new?(payload) + payload[:member][:service_uid] == payload[:new_member][:service_uid] + end + + def get_tunnel_port(default_port, payload) + if is_new?(payload) + @port ||= payload[:old_member][:tunnels].find {|s| s[:to_zone_port] == default_port }[:from_zone_port] + else + @port ||= payload[:new_member][:tunnels].find {|s| s[:to_zone_port] == default_port }[:from_zone_port] + end + end + + BOB_SUCCESSFUL_POST = Proc.new do |result| + if result =~ /\{"success":true\}/ + next true + else + next false + end + end + + def sanitize_network_dirs(payload) + net_dirs = ensure_format(payload) + net_dirs.each do |component, dirs| + net_dirs[component] = clean_writables(dirs) + end + return net_dirs + end + + def ensure_format(payload) + net_dirs = payload[:boxfile][:network_dirs] || payload[:boxfile][:shared_writable_dirs] + if net_dirs.kind_of?(Hash) + return net_dirs + elsif net_dirs.nil? or net_dirs.empty? + return {} + else + return {payload[:storage].keys.first => net_dirs} + end + end + + def clean_writables(dirs) + dirs = dirs.map(&:to_s) + dirs = remove_empty(dirs) + dirs = filter_offensive(dirs) + dirs = strip_leading_slash(dirs) + dirs = strip_trailing_slash(dirs) + dirs = remove_nested(dirs) + return dirs + end + + def remove_empty(dirs) + dirs.inject([]) do |res, elem| + res << elem if elem && elem != "" + res + end + end + + def filter_offensive(dirs) + dirs.inject([]) do |res, elem| + if elem[0] != '.' + # ensure not going up a directory + unless elem =~ /\*|\.?\.\// + res << elem + end + end + res + end + end + + def strip_leading_slash(dirs) + dirs.inject([]) do |res, elem| + if elem[0] == '/' + elem.slice!(0) + end + res << elem + end + end + + def strip_trailing_slash(dirs) + dirs.inject([]) do |res, elem| + if elem[-1] == '/' + elem.slice!(-1) + end + res << elem + end + end + + # this removes nested mounts like: + # tmp/ + # tmp/cache/ + # tmp/assets/ + # + # and keeps tmp/ + def remove_nested(dirs) + dirs.sort! + dirs.inject([]) do |res, elem| + overlap = false + # now make sure parents dont contain children + res.each do |parent| + if elem =~ /^#{parent}\// + overlap = true + end + end + res << elem if not overlap + res + end + end + + end +end \ No newline at end of file diff --git a/lib/hooky/hook.rb b/lib/hooky/hook.rb new file mode 100644 index 0000000..ace0a40 --- /dev/null +++ b/lib/hooky/hook.rb @@ -0,0 +1,36 @@ +require 'oj' +require 'multi_json' + +module Hooky + module Hook + + def payload + @payload ||= parse_payload + end + + def parse_payload + if not ARGV.empty? + MultiJson.load ARGV.first, symbolize_keys: true + else + {} + end + end + + def converge(map, list) + Converginator.new(map, list).converge! + end + + def registry(key, value=nil) + unless value.nil? + db.put(key, value) + else + db.fetch(key) + end + end + + def db + @db ||= Hooky::DB.new + end + + end +end \ No newline at end of file diff --git a/lib/hooky/registry.rb b/lib/hooky/registry.rb new file mode 100644 index 0000000..f138aec --- /dev/null +++ b/lib/hooky/registry.rb @@ -0,0 +1,53 @@ +module Hooky + # Register components in a single location that can be queried. + # + # This allows certain components (such as guest systems, configuration + # pieces, etc.) to be registered and queried. + class Registry + def initialize + @actions = {} + @results_cache = {} + end + + # Register a callable by key. + # + # The callable should be given in a block which will be lazily evaluated + # when the action is needed. + # + # If an action by the given name already exists then it will be + # overwritten. + def register(key, value=nil, &block) + @results_cache.delete key + block = lambda { value } unless value.nil? + @actions[key] = block + end + + # Get an action by the given key. + # + # This will evaluate the block given to `register` and return the resulting + # action stack. + def get(key) + return nil unless @actions.has_key?(key) + return @results_cache[key] if @results_cache.has_key?(key) + @results_cache[key] = @actions[key].call + end + alias :[] :get + + # Iterate over the keyspace. + def each(&block) + @actions.each do |key, _| + yield key, get(key) + end + end + + # Converts this registry to a hash + def to_hash + result = {} + self.each do |key, value| + result[key] = value + end + + result + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource.rb b/lib/hooky/resource.rb new file mode 100644 index 0000000..a8038d7 --- /dev/null +++ b/lib/hooky/resource.rb @@ -0,0 +1,18 @@ +require 'hooky/resource/base' +require 'hooky/resource/directory' +require 'hooky/resource/execute' +require 'hooky/resource/file' +require 'hooky/resource/hook_file' +require 'hooky/resource/link' +require 'hooky/resource/mount' +require 'hooky/resource/rsync' +require 'hooky/resource/scp' +require 'hooky/resource/service' +require 'hooky/resource/template' +require 'hooky/resource/zfs' + +module Hooky + module Resource + + end +end \ No newline at end of file diff --git a/lib/hooky/resource/base.rb b/lib/hooky/resource/base.rb new file mode 100644 index 0000000..2a3d681 --- /dev/null +++ b/lib/hooky/resource/base.rb @@ -0,0 +1,95 @@ +module Hooky + module Resource + class Base + + class << self + + def field(key) + define_method key do |*args| + if data = args[0] + instance_variable_set("@#{key}", data) + else + instance_variable_get("@#{key}") + end + end + end + + def actions(*actions) + if actions.any? + @actions = *actions + else + @actions + end + end + + def default_action(action=nil) + if action + @default_action = action + else + @default_action || :run + end + end + + end + + attr_accessor :dict + + field :name + + def initialize(name) + name(name) + end + + def run(action); end + + def can_run? + only_if_res = true + not_if_res = false + + if only_if and only_if.respond_to? :call + only_if_res = only_if.call + end + + if not_if and not_if.respond_to? :call + not_if_res = not_if.call + end + + only_if_res and not not_if_res + end + + def action(*actions) + if actions.any? + actions.each do |action| + if not self.class.actions.include? action + raise Hooky::Error::UnknownAction, "unknown action '#{action}'" + end + end + @actions = *actions + else + @actions || [default_action] + end + end + + def default_action + self.class.default_action + end + + def not_if(&block) + if block_given? + @not_if = block + else + @not_if + end + end + + def only_if(&block) + if block_given? + @only_if = block + else + @only_if + end + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/cron.rb b/lib/hooky/resource/cron.rb new file mode 100644 index 0000000..5aa5dc7 --- /dev/null +++ b/lib/hooky/resource/cron.rb @@ -0,0 +1,26 @@ +module Hooky + module Resource + class Cron < Execute + + def initialize(name) + super + timeout 60 + cwd '/data' + end + + protected + + def run! + begin + Timeout::timeout(timeout) do + f = IO.popen("#{cmd} || exit 0", :err=>[:child, :out]) + puts f.readline while true + end + rescue Timeout::Error + $stderr.puts 'Timed out running cron! Consider using a worker.' + end + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/directory.rb b/lib/hooky/resource/directory.rb new file mode 100644 index 0000000..b91260e --- /dev/null +++ b/lib/hooky/resource/directory.rb @@ -0,0 +1,67 @@ +module Hooky + module Resource + class Directory < Base + + field :path + field :recursive + field :mode + field :owner + field :group + + actions :create, :delete + default_action :create + + def initialize(name) + path name unless path + super + end + + def run(action) + case action + when :create + create! + chown! + chmod! + when :delete + delete! + end + end + + protected + + def create! + return if ::File.exists? path + cmd = "mkdir #{"-p " if recursive}#{path}" + `#{cmd}` + code = $?.exitstatus + if code != 0 + raise Hooky::Error::UnexpectedExit, "#{cmd} failed with exit code '#{code}'" + end + end + + def delete! + return if not ::File.exists? path + cmd = "rm -rf #{path}" + `#{cmd}` + code = $?.exitstatus + if code != 0 + raise Hooky::Error::UnexpectedExit, "#{cmd} failed with exit code '#{code}'" + end + end + + def chown! + return unless owner or group + if ::File.exists? path + `chown #{(group.nil?) ? owner : "#{owner}:#{group}"} #{path}` + end + end + + def chmod! + if ::File.exists? path and mode + ::File.chmod(mode, path) + end + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/execute.rb b/lib/hooky/resource/execute.rb new file mode 100644 index 0000000..9deaab7 --- /dev/null +++ b/lib/hooky/resource/execute.rb @@ -0,0 +1,157 @@ +require 'timeout' + +module Hooky + module Resource + class Execute < Base + + field :command + field :cwd + field :environment + field :user + field :path + field :returns + field :timeout + field :stream + field :stream_prefix + field :validator + + actions :run + default_action :run + + def initialize(name) + command name unless command + timeout 3600 + returns 0 + super + end + + def run(action) + case action + when :run + if stream + stream! + else + run! + end + end + end + + protected + + def validate!(res) + if validator.is_a? Proc + if validator.call(res) + res + else + raise "ERROR: execute resource \"#{name}\" failed validation!" + end + else + res + end + end + + def run! + Timeout::timeout(timeout) do + res = `#{cmd}` + code = $?.exitstatus + unexpected_exit(code) unless code == returns + return validate!(res) + end + end + + def stream! + STDOUT.sync = STDERR.sync = true + STDOUT.print stream_prefix if stream_prefix + + result = "" + + ::IO.popen(cmd, :err=>[:child, :out]) do |out| + eof = false + until eof do + begin + if stream_prefix + @chunck = out.readpartial(4096).gsub("\n", "\n#{stream_prefix}") + STDOUT.print @chunck + else + @chunck = out.readpartial(4096) + STDOUT.print @chunck + end + rescue EOFError + eof = true + end + result << @chunck.to_s + end + end + + if @chunck =~ /\n#{stream_prefix}$/ + STDOUT.print "\b" * stream_prefix.length + else + STDOUT.print "\n" + end + + code = $?.exitstatus + unexpected_exit(code) unless code == returns + + return validate!(result) + end + + def cmd + com = command + + if environment + com = "#{env}#{com}" + end + + if path + com = "PATH=\"#{path}\" #{com}" + end + + if cwd + com = "cd #{cwd}; #{com}" + end + + if user + com = su(user, com) + end + + com + end + + # strategy: + # 1- escape the escapes + # 2- escape quotes + # 3- escape dollar signs + def escape(cmd) + cmd.gsub!(/\\/, "\\\\\\") + cmd.gsub!(/"/, "\\\"") + cmd.gsub!(/\$/, "\\$") + cmd + end + + def su(user, cmd) + "su - #{user} -c \"#{escape(cmd)}\"" + end + + def env + vars = environment || {} + env = '' + vars.each do |key, val| + env += " " if not env == '' + env += env_string(key, val) + end + (env == '')? env : "#{env}" + end + + def env_string(key, val) + key = key.to_s if not key.is_a? String + val = val.to_s if not val.is_a? String + %Q{export #{key.upcase}="#{escape(val)}";} + end + + def unexpected_exit(res) + raise Hooky::Error::UnexpectedExit, "'#{name}' exited with #{res}, expected #{returns}" + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/file.rb b/lib/hooky/resource/file.rb new file mode 100644 index 0000000..8554a87 --- /dev/null +++ b/lib/hooky/resource/file.rb @@ -0,0 +1,71 @@ +module Hooky + module Resource + class File < Base + + field :path + field :content + field :mode + field :owner + field :group + + actions :create, :create_if_missing, :delete, :touch + default_action :create + + def initialize(name) + path name unless path + super + end + + def run(action) + case action + when :create + create! + chown! + chmod! + when :create_if_missing + create_if_missing! + chown! + chmod! + when :delete + delete! + when :touch + touch! + end + end + + protected + + def create! + ::File.write path, (content || "") + end + + def create_if_missing! + if not ::File.exists? path + create! + end + end + + def delete! + ::File.delete path + end + + def chown! + return unless owner or group + if ::File.exists? path + `chown #{(group.nil?) ? owner : "#{owner}:#{group}"} #{path}` + end + end + + def chmod! + if ::File.exists? path and mode + ::File.chmod(mode, path) + end + end + + def touch! + `touch -c #{path}` + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/hook_file.rb b/lib/hooky/resource/hook_file.rb new file mode 100644 index 0000000..8ec378e --- /dev/null +++ b/lib/hooky/resource/hook_file.rb @@ -0,0 +1,80 @@ +module Hooky + module Resource + class HookFile < Base + + field :path + field :source + field :mode + field :owner + field :group + + actions :create, :create_if_missing, :delete, :touch + default_action :create + + def initialize(name) + path name unless path + source ::File.basename(name) + super + end + + def run(action) + case action + when :create + create! + chown! + chmod! + when :create_if_missing + create_if_missing! + chown! + chmod! + when :delete + delete! + when :touch + touch! + end + end + + protected + + def create! + ::File.write path, render + end + + def create_if_missing! + if not ::File.exists? path + create! + end + end + + def delete! + ::File.delete path + end + + def chown! + return unless owner or group + `chown #{(group.nil?) ? owner : "#{owner}:#{group}"} #{path}` + end + + def chmod! + ::File.chmod(mode, path) if mode + end + + def touch! + `touch -c #{path}` + end + + def render + ::File.read("#{file_dir}/#{source}") + end + + def file_dir + "#{hook_root}/files" + end + + def hook_root + dict[:hook_root] + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/link.rb b/lib/hooky/resource/link.rb new file mode 100644 index 0000000..9bfea2c --- /dev/null +++ b/lib/hooky/resource/link.rb @@ -0,0 +1,61 @@ +module Hooky + module Resource + class Link < Base + + field :owner + field :group + field :link_type + field :target_file + field :to + + actions :create, :delete + default_action :create + + def initialize(name) + target_file name unless target_file + link_type :symbolic + super + end + + def run(action) + case action + when :create + create! + chown! + when :delete + delete! + end + end + + protected + + def create! + args = ['f'] + args << 's' if link_type == :symbolic + cmd = "ln -#{args.join} #{to} #{target_file}" + `#{cmd}` + code = $?.exitstatus + if code != 0 + raise Hooky::Error::UnexpectedExit, "#{cmd} failed with exit code '#{code}'" + end + end + + def delete! + cmd = "rm -f #{target_file}" + `#{cmd}` + code = $?.exitstatus + if code != 0 + raise Hooky::Error::UnexpectedExit, "#{cmd} failed with exit code '#{code}'" + end + end + + def chown! + return unless owner or group + if ::File.exists? target_file + `chown #{(group.nil?) ? owner : "#{owner}:#{group}"} #{target_file}` + end + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/mount.rb b/lib/hooky/resource/mount.rb new file mode 100644 index 0000000..52d7299 --- /dev/null +++ b/lib/hooky/resource/mount.rb @@ -0,0 +1,77 @@ +module Hooky + module Resource + class Mount < Base + + field :device + # field :device_type + # field :dump + field :fstype + field :mount_point + field :options + field :pass + field :supports + + actions :mount, :umount, :remount, :enable, :disable + default_action :mount + + def initialize(name) + mount_point(name) unless mount_point + pass('-') unless pass + super + end + + def run(action) + case action + when :mount + mount! + when :umount + umount! + when :remount + umount! + mount! + when :enable + disable! + enable! + when :disable + disable! + end + end + + protected + + def mount! + ::FileUtils.mkdir_p(mount_point) + run_command! "mount -O -F #{fstype} -o retry=5,timeo=300 #{options!(as_arg=true)} #{device} #{mount_point}" + end + + def umount! + run_command! "umount #{mount_point}" + end + + def enable! + entry = "#{device}\t#{device =~ /^\/dev/ ? device : "-"}\t#{mount_point}\t#{fstype}\t#{pass}\tyes\t#{options!}" + `echo "#{entry}" >> /etc/vfstab` + end + + def disable! + `egrep -v "#{device}.*#{mount_point}" /etc/vfstab > /tmp/vfstab.tmp; mv -f /tmp/vfstab.tmp /etc/vfstab` + end + + def options!(as_arg=false) + options = self.options.kind_of?(Array) ? self.options.join(',') : self.options + if as_arg + options ? (return "-o #{options}") : (return "") + end + options != "" ? (return "#{options}") : (return "-") + end + + def run_command!(cmd, expect_code=0) + `#{cmd}` + code = $?.exitstatus + if code != expect_code + raise Hooky::Error::UnexpectedExit, "#{cmd} failed with exit code '#{code}'" + end + end + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/rsync.rb b/lib/hooky/resource/rsync.rb new file mode 100644 index 0000000..a01c4c2 --- /dev/null +++ b/lib/hooky/resource/rsync.rb @@ -0,0 +1,67 @@ +module Hooky + module Resource + class Rsync < Base + + field :source + field :destination + field :wrapper + field :archive + field :recursive + field :checksum + field :compress + + actions :sync + default_action :sync + + def initialize(name) + source name unless source + super + end + + def run(action) + case action + when :sync + sync! + end + end + + def sync! + run_command! "rsync -q#{archive!}#{recursive!}#{checksum!}#{compress!} #{wrapper!} #{source} #{destination}" + end + + def archive! + (return "a") if archive + "" + end + + def recursive! + (return "r") if archive + "" + end + + def checksum! + (return "c") if archive + "" + end + + def compress! + (return "z") if archive + "" + end + + def wrapper! + (return "-e '#{wrapper}'") if wrapper + "" + end + + def run_command!(cmd, expect_code=0) + `#{cmd}` + code = $?.exitstatus + if code != expect_code + raise Hooky::Error::UnexpectedExit, "#{cmd} failed with exit code '#{code}'" + end + end + + end + end +end diff --git a/lib/hooky/resource/scp.rb b/lib/hooky/resource/scp.rb new file mode 100644 index 0000000..6f5ec13 --- /dev/null +++ b/lib/hooky/resource/scp.rb @@ -0,0 +1,87 @@ +module Hooky + module Resource + class Scp < Base + + field :source + field :destination + field :port + field :recursive + field :config + field :cipher + field :identity + field :ssh_options + field :compression + field :preserve + + actions :copy + default_action :copy + + def initialize(name) + source(name) unless source + preserve(true) unless preserve + recursive(true) unless recursive + super + end + + def run(action) + case action + when :copy + copy! + end + end + + def copy! + run_command!("scp -q#{preserve!}#{recursive!}B#{compression!} #{config!} #{port!} #{cipher!} #{identity!} #{ssh_options!} #{source} #{destination}") + end + + def cipher! + (return "-c #{cipher}") if cipher + "" + end + + def compression! + (return "C") if compression + "" + end + + def config! + (return "-F #{config}") if config + "" + end + + def identity! + (return "-i #{identity}") if identity + "" + end + + def port! + (return "-P #{port}") if port + "" + end + + def preserve! + (return "p") if preserve + "" + end + + def recursive! + (return "r") if recursive + "" + end + + def ssh_options! + (return "-o #{ssh_options}") if ssh_options + "" + end + + def run_command!(cmd, expect_code=0) + `#{cmd}` + code = $?.exitstatus + if code != expect_code + raise Hooky::Error::UnexpectedExit, "#{cmd} failed with exit code '#{code}'" + end + end + + end + end +end diff --git a/lib/hooky/resource/service.rb b/lib/hooky/resource/service.rb new file mode 100644 index 0000000..8e86e6b --- /dev/null +++ b/lib/hooky/resource/service.rb @@ -0,0 +1,60 @@ +module Hooky + module Resource + class Service < Base + + field :recursive + field :service_name + + actions :enable, :disable, :start, :stop, :restart, :reload + default_action :enable + + def initialize(name) + service_name(name) unless service_name + end + + def run(action) + case action + when :enable + enable! + when :disable + disable! + when :start + enable! + when :stop + disable! + when :restart + restart! + when :reload + reload! + end + end + + protected + + def enable! + run_command! "svcadm enable #{"-r" if recursive} #{service_name}" + end + + def disable! + run_command! "svcadm disable #{service_name}" + end + + def restart! + run_command! "svcadm restart #{service_name}" + end + + def reload! + run_command! "svcadm refresh #{service_name}" + end + + def run_command!(cmd, expect_code=0) + `#{cmd}` + code = $?.exitstatus + if code != expect_code + raise Hooky::Error::UnexpectedExit, "#{cmd} failed with exit code '#{code}'" + end + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/template.rb b/lib/hooky/resource/template.rb new file mode 100644 index 0000000..929dde8 --- /dev/null +++ b/lib/hooky/resource/template.rb @@ -0,0 +1,87 @@ +require 'tilt' +require 'erubis' +require 'erb' +require 'oj' +require 'multi_json' + +module Hooky + module Resource + class Template < Base + + field :path + field :source + field :variables + field :mode + field :owner + field :group + + actions :create, :create_if_missing, :delete, :touch + default_action :create + + def initialize(name) + path name unless path + source "#{::File.basename(name)}.erb" + super + end + + def run(action) + case action + when :create + create! + chown! + chmod! + when :create_if_missing + create_if_missing! + chown! + chmod! + when :delete + delete! + when :touch + touch! + end + end + + protected + + def create! + ::File.write path, render + end + + def create_if_missing! + if not ::File.exists? path + create! + end + end + + def delete! + ::File.delete path + end + + def chown! + return unless owner or group + `chown #{(group.nil?) ? owner : "#{owner}:#{group}"} #{path}` + end + + def chmod! + ::File.chmod(mode, path) if mode + end + + def touch! + `touch -c #{path}` + end + + def render + Tilt.new("#{template_dir}/#{source}").render(self, variables) + end + + def template_dir + "#{hook_root}/templates" + end + + def hook_root + dict[:hook_root] + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/warning.rb b/lib/hooky/resource/warning.rb new file mode 100644 index 0000000..c949a7e --- /dev/null +++ b/lib/hooky/resource/warning.rb @@ -0,0 +1,87 @@ +module Hooky + module Resource + class Warning < Base + + field :source + field :content + field :stream + + actions :warn + default_action :warn + + def initialize(name) + source(name) unless source or content + stream :stdout unless stream + + @default_header = "\u25BC\u25BC\u25BC\u25BC :: WARNING :: \u25BC\u25BC\u25BC\u25BC" + @block_width = @default_header.length + end + + def run(action) + case action + when :warn + warn! + end + end + + protected + + def gem + dict[:template_gem] + end + + def gem_spec + Gem::Specification.find_by_name(gem) + end + + def gem_root + gem_spec.gem_dir + end + + def content! + output_string ||= content or ::File.open("#{gem_root}/messages/#{source}").read + return output_string + end + + def header! + + header = @default_header + padding = "\u25BC" + + longest_line = content!.split.sort_by {|x| x.length}.last + + if longest_line.length > header.length + + difference = longest_line.length - header.length + padding *= (difference.to_f / 2).ceil + + header = padding + header + padding + end + + @block_width = header.length + + puts header + end + + def footer! + footer = "\u25B2" * @block_width + puts footer + end + + def warn! + + header! + + case stream + when :stdout + puts content! + when :stderr + $stderr.puts content! + end + + footer! + end + + end + end +end \ No newline at end of file diff --git a/lib/hooky/resource/zfs.rb b/lib/hooky/resource/zfs.rb new file mode 100644 index 0000000..8798eb6 --- /dev/null +++ b/lib/hooky/resource/zfs.rb @@ -0,0 +1,98 @@ +module Hooky + module Resource + class Zfs < Base + + field :snapshot + field :dataset + field :destination + field :source + field :ssh_host + field :validator + + actions :send, :receive, :transmit, :snapshot, :destroy, :rollback, :clone + default_action :send + + def initialize(name) + snapshot(name) unless snapshot + super + end + + def run(action) + case action + when :send + send! + when :receive + receive! + when :transmit + transmit! + when :snapshot + snapshot! + when :destroy + destroy! + when :rollback + rollback! + when :clone + clone! + end + end + + def send! + run_command! "zfs send #{snapshot} | #{destination}" + end + + def receive! + run_command! "#{source.to_s.strip} | zfs receive -F #{dataset}" + end + + def transmit! + if ssh_host + run_command! "zfs send #{snapshot} | ssh -o stricthostkeychecking=no #{ssh_host} zfs receive -F #{dataset}" + else + run_command! "zfs send #{snapshot} | zfs receive -F #{dataset}" + end + end + + def snapshot! + destroy! + run_command! "zfs snapshot #{snapshot}" + end + + def destroy! + `zfs list -t snapshot | grep #{snapshot}` + if $?.exitstatus == 0 + run_command! "zfs destroy #{snapshot}" + end + end + + def rollback! + run_command! "zfs rollback -r #{snapshot}" + end + + def clone! + run_command! "zfs clone #{snapshot} #{dataset}" + end + + def validate!(res) + if validator.is_a? Proc + if validator.call(res) + res + else + raise "ERROR: execute resource \"#{name}\" failed validation!" + end + else + res + end + end + + def run_command!(cmd, expect_code=0) + res = `#{cmd}` + code = $?.exitstatus + if code != expect_code + raise Hooky::Error::UnexpectedExit, "#{cmd} failed with exit code '#{code}'" + end + return validate!(res) + end + + end + end +end diff --git a/lib/hooky/resources.rb b/lib/hooky/resources.rb new file mode 100644 index 0000000..fb51449 --- /dev/null +++ b/lib/hooky/resources.rb @@ -0,0 +1,12 @@ +Hooky.resources.register(:directory) { Hooky::Resource::Directory } +Hooky.resources.register(:execute) { Hooky::Resource::Execute } +Hooky.resources.register(:file) { Hooky::Resource::File } +Hooky.resources.register(:hook_file) { Hooky::Resource::HookFile } +Hooky.resources.register(:hooky_file) { Hooky::Resource::HookFile } +Hooky.resources.register(:link) { Hooky::Resource::Link } +Hooky.resources.register(:mount) { Hooky::Resource::Mount } +Hooky.resources.register(:rsync) { Hooky::Resource::Rsync } +Hooky.resources.register(:scp) { Hooky::Resource::Scp } +Hooky.resources.register(:service) { Hooky::Resource::Service } +Hooky.resources.register(:template) { Hooky::Resource::Template } +Hooky.resources.register(:zfs) { Hooky::Resource::Zfs } diff --git a/lib/hooky/version.rb b/lib/hooky/version.rb new file mode 100644 index 0000000..d05a4a7 --- /dev/null +++ b/lib/hooky/version.rb @@ -0,0 +1,3 @@ +module Hooky + VERSION = "0.0.1" +end