pleaserun 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.
- data/.gitignore +4 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +73 -0
- data/Guardfile +17 -0
- data/Makefile +50 -0
- data/README.md +98 -0
- data/bin/pleaserun +7 -0
- data/examples/runit.rb +16 -0
- data/lib/pleaserun/cli.rb +241 -0
- data/lib/pleaserun/configurable.rb +143 -0
- data/lib/pleaserun/detector.rb +58 -0
- data/lib/pleaserun/mustache_methods.rb +41 -0
- data/lib/pleaserun/namespace.rb +3 -0
- data/lib/pleaserun/platform/base.rb +144 -0
- data/lib/pleaserun/platform/launchd.rb +27 -0
- data/lib/pleaserun/platform/runit.rb +18 -0
- data/lib/pleaserun/platform/systemd.rb +24 -0
- data/lib/pleaserun/platform/sysv.rb +12 -0
- data/lib/pleaserun/platform/upstart.rb +11 -0
- data/pleaserun.gemspec +27 -0
- data/spec/pleaserun/configurable_spec.rb +215 -0
- data/spec/pleaserun/mustache_methods_spec.rb +46 -0
- data/spec/pleaserun/platform/base_spec.rb +27 -0
- data/spec/pleaserun/platform/launchd_spec.rb +93 -0
- data/spec/pleaserun/platform/systemd_spec.rb +119 -0
- data/spec/pleaserun/platform/sysv_spec.rb +133 -0
- data/spec/pleaserun/platform/upstart_spec.rb +117 -0
- data/spec/testenv.rb +69 -0
- data/templates/launchd/10.9/program.plist +47 -0
- data/templates/runit/log +4 -0
- data/templates/runit/run +17 -0
- data/templates/systemd/default/prestart.sh +2 -0
- data/templates/systemd/default/program.service +17 -0
- data/templates/sysv/lsb-3.1/default +0 -0
- data/templates/sysv/lsb-3.1/init.d +141 -0
- data/templates/upstart/1.5/init.conf +41 -0
- data/templates/upstart/1.5/init.d.sh +4 -0
- data/test.rb +33 -0
- data/test/helpers.rb +20 -0
- data/test/test.rb +60 -0
- data/test/vagrant/Vagrantfile +40 -0
- data/test/vagrant/fedora-18/Vagrantfile +28 -0
- data/test/vagrant/fedora-18/provision.sh +10 -0
- metadata +187 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
require "pleaserun/namespace"
|
2
|
+
require "insist"
|
3
|
+
|
4
|
+
# A mixin class that provides 'attribute' to a class.
|
5
|
+
# The main use for such attributes is to provide validation for mutators.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# class Person
|
10
|
+
# include PleaseRun::Configurable
|
11
|
+
#
|
12
|
+
# attribute :greeting, "A simple greeting" do |greeting|
|
13
|
+
# # do any validation here.
|
14
|
+
# raise "Greeting must be a string!" unless greeting.is_a?(String)
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# person = Person.new
|
19
|
+
# person.greeting = 1234 # Fails!
|
20
|
+
# person.greeting = "Hello, world!"
|
21
|
+
#
|
22
|
+
# puts person.greeting
|
23
|
+
# # "Hello, world!"
|
24
|
+
module PleaseRun::Configurable
|
25
|
+
class ConfigurationError < ::StandardError; end
|
26
|
+
class ValidationError < ConfigurationError; end
|
27
|
+
|
28
|
+
def self.included(klass)
|
29
|
+
klass.extend(ClassMixin)
|
30
|
+
|
31
|
+
m = respond_to?(:initialize) ? method(:initialize) : nil
|
32
|
+
define_method(:initialize) do |*args, &block|
|
33
|
+
m.call(*args, &block) if m
|
34
|
+
configurable_setup
|
35
|
+
end
|
36
|
+
end # def self.included
|
37
|
+
|
38
|
+
def configurable_setup
|
39
|
+
@attributes = {}
|
40
|
+
self.class.ancestors.each do |ancestor|
|
41
|
+
next unless ancestor.include?(PleaseRun::Configurable)
|
42
|
+
ancestor.attributes.each do |facet|
|
43
|
+
@attributes[facet.name] = facet.clone
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end # def configurable_setup
|
47
|
+
|
48
|
+
module ClassMixin
|
49
|
+
# Define an attribute on this class.
|
50
|
+
def attribute(name, description, options={}, &validator)
|
51
|
+
facet = Facet.new(name, description, options, &validator)
|
52
|
+
attributes << facet
|
53
|
+
|
54
|
+
# define '<name>' and '<name>=' methods
|
55
|
+
define_method(name.to_sym) do
|
56
|
+
# object instance, not class ivar
|
57
|
+
@attributes[name.to_sym].value
|
58
|
+
end
|
59
|
+
|
60
|
+
define_method("#{name}=".to_sym) do |value|
|
61
|
+
# object instance, not class ivar
|
62
|
+
@attributes[name.to_sym].value = value
|
63
|
+
end
|
64
|
+
|
65
|
+
define_method("#{name}?".to_sym) do
|
66
|
+
return @attributes[name.to_sym].set?
|
67
|
+
end
|
68
|
+
end # def attribute
|
69
|
+
|
70
|
+
def attributes
|
71
|
+
return @attributes ||= []
|
72
|
+
end
|
73
|
+
|
74
|
+
def all_attributes
|
75
|
+
return ancestors.select { |a| a.respond_to?(:attributes) }.collect{ |a| a.attributes }.flatten
|
76
|
+
end # def attributes
|
77
|
+
end # def ClassMixin
|
78
|
+
|
79
|
+
class FacetBuilder
|
80
|
+
def initialize(facet, &block)
|
81
|
+
@facet = facet
|
82
|
+
instance_eval(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate(&block)
|
86
|
+
@facet.validator = block
|
87
|
+
end
|
88
|
+
def munge(&block)
|
89
|
+
@facet.munger = block
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Facet
|
94
|
+
attr_reader :name
|
95
|
+
attr_reader :description
|
96
|
+
|
97
|
+
def initialize(name, description, options={}, &validator)
|
98
|
+
insist { name }.is_a?(Symbol)
|
99
|
+
insist { description }.is_a?(String)
|
100
|
+
insist { options }.is_a?(Hash)
|
101
|
+
|
102
|
+
@name = name
|
103
|
+
@description = description
|
104
|
+
@options = options
|
105
|
+
|
106
|
+
FacetBuilder.new(self, &validator) if block_given?
|
107
|
+
|
108
|
+
if @options[:default]
|
109
|
+
validate(@options[:default])
|
110
|
+
end
|
111
|
+
end # def initialize
|
112
|
+
|
113
|
+
def validator=(callback)
|
114
|
+
@validator = callback
|
115
|
+
end # def validator=
|
116
|
+
|
117
|
+
def munger=(callback)
|
118
|
+
@munger = callback
|
119
|
+
end # def munger=
|
120
|
+
|
121
|
+
def value=(v)
|
122
|
+
v = @munger.call(v) if @munger
|
123
|
+
validate(v)
|
124
|
+
@value = v
|
125
|
+
end # def value=
|
126
|
+
|
127
|
+
def validate(v)
|
128
|
+
return @validator.call(v) if @validator
|
129
|
+
rescue => e
|
130
|
+
raise ValidationError, "Invalid value '#{v.inspect}' for attribute '#{name}' (#{e})"
|
131
|
+
end # def validate
|
132
|
+
|
133
|
+
def value
|
134
|
+
return @value if @value
|
135
|
+
return @options[:default] if @options.include?(:default)
|
136
|
+
return nil
|
137
|
+
end # def value
|
138
|
+
|
139
|
+
def set?
|
140
|
+
return !@value.nil?
|
141
|
+
end # def set?
|
142
|
+
end # class Facet
|
143
|
+
end # module PleaseRun::Configurable
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "cabin"
|
2
|
+
class PleaseRun::Detector
|
3
|
+
class UnknownSystem < StandardError; end
|
4
|
+
|
5
|
+
MAPPING = {
|
6
|
+
["ubuntu", "12.04"] => ["upstart", "1.5"],
|
7
|
+
["ubuntu", "12.10"] => ["upstart", "1.5"],
|
8
|
+
["ubuntu", "13.04"] => ["upstart", "1.5"],
|
9
|
+
["ubuntu", "13.10"] => ["upstart", "1.5"],
|
10
|
+
["debian", "7"] => [ "sysv", "lsb-3.1"],
|
11
|
+
["debian", "6"] => [ "sysv", "lsb-3.1"],
|
12
|
+
["fedora", "18"] => [ "systemd", "default"],
|
13
|
+
["fedora", "19"] => [ "systemd", "default"],
|
14
|
+
["fedora", "20"] => [ "systemd", "default"]
|
15
|
+
}
|
16
|
+
|
17
|
+
def self.detect
|
18
|
+
@logger ||= Cabin::Channel.get
|
19
|
+
begin
|
20
|
+
platform, version = detect_ohai
|
21
|
+
rescue LoadError => e
|
22
|
+
@logger.debug("Failed to load ohai", :exception => e)
|
23
|
+
begin
|
24
|
+
platform, version = detect_facter
|
25
|
+
rescue LoadError
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
raise UnknownSystem if platform.nil? || version.nil?
|
30
|
+
system = MAPPING[ [platform, version] ]
|
31
|
+
raise UnknownSystem if system.nil?
|
32
|
+
return system
|
33
|
+
end # def detect
|
34
|
+
|
35
|
+
def self.detect_ohai
|
36
|
+
require "ohai/system"
|
37
|
+
ohai = Ohai::System.new
|
38
|
+
# TODO(sissel): Loading all plugins takes a long time (seconds).
|
39
|
+
# TODO(sissel): Figure out how to load just the platform plugin correctly.
|
40
|
+
ohai.all_plugins
|
41
|
+
|
42
|
+
platform = ohai["platform"]
|
43
|
+
platform_version = ohai["platform_version"]
|
44
|
+
version = case platform
|
45
|
+
# Take '6.0.8' and make it just '6' since debian never makes major
|
46
|
+
# changes in a minor release
|
47
|
+
when "debian" ; platform_version[/^[0-9]+/]
|
48
|
+
else ; platform_version
|
49
|
+
end # case platform
|
50
|
+
|
51
|
+
return platform, version
|
52
|
+
end # def detect_ohai
|
53
|
+
|
54
|
+
def self.detect_facter
|
55
|
+
require "facter"
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "pleaserun/namespace"
|
2
|
+
require "shellwords" # stdlib
|
3
|
+
require "mustache" # gem 'mustache'
|
4
|
+
|
5
|
+
module PleaseRun::MustacheMethods
|
6
|
+
def escaped_args
|
7
|
+
return if args.nil?
|
8
|
+
return Shellwords.shellescape(Shellwords.shelljoin(args))
|
9
|
+
end # def escaped_args
|
10
|
+
|
11
|
+
def escaped(str)
|
12
|
+
return Shellwords.shellescape(Mustache.render(str, self))
|
13
|
+
end # def escaped
|
14
|
+
|
15
|
+
def shell_args
|
16
|
+
return if args.nil?
|
17
|
+
return args.collect { |a| shell_quote(a) }.join(" ")
|
18
|
+
end # def shell_args
|
19
|
+
|
20
|
+
def shell_quote(str)
|
21
|
+
# interpreted from POSIX 1003.1 2004 section 2.2.3 (Double-Quotes)
|
22
|
+
|
23
|
+
# $ is has meaning, escape it.
|
24
|
+
value = str.gsub(/(?<![\\])\$/, "\\$")
|
25
|
+
# ` is has meaning, escape it.
|
26
|
+
value = value.gsub(/`/) { "\\`" }
|
27
|
+
|
28
|
+
# Backslash means escape a literal unless followed by one of $`"\
|
29
|
+
value = value.gsub(/\\[^$`"\\]/) { |v| "\\#{v}" }
|
30
|
+
|
31
|
+
return "\"" + value + "\""
|
32
|
+
end # def shell_quote
|
33
|
+
|
34
|
+
def shell_continuation(str)
|
35
|
+
return render(str).split("\n").reject { |l| l =~ /^\s*$/ }.join(" \\\n")
|
36
|
+
end # def shell_continuation
|
37
|
+
|
38
|
+
def quoted(str)
|
39
|
+
return shell_quote(render(str))
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require "pleaserun/namespace"
|
2
|
+
require "pleaserun/configurable"
|
3
|
+
require "pleaserun/mustache_methods"
|
4
|
+
|
5
|
+
require "insist" # gem 'insist'
|
6
|
+
|
7
|
+
class PleaseRun::Platform::Base
|
8
|
+
include PleaseRun::Configurable
|
9
|
+
include PleaseRun::MustacheMethods
|
10
|
+
|
11
|
+
class InvalidTemplate < ::StandardError; end
|
12
|
+
|
13
|
+
attribute :name, "The name of this program." do
|
14
|
+
validate do |name|
|
15
|
+
insist { name.is_a?(String) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attribute :program, "The program to execute. This can be a full path, like " \
|
20
|
+
"/usr/bin/cat, or a shorter name like 'cat' if you wish to search $PATH." do
|
21
|
+
validate do |program|
|
22
|
+
insist { program.is_a?(String) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attribute :args, "The arguments to pass to the program." do
|
27
|
+
validate do |args|
|
28
|
+
insist { args }.is_a?(Array)
|
29
|
+
args.each { |a| insist { a }.is_a?(String) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attribute :user, "The user to use for executing this program.",
|
34
|
+
:default => "root" do
|
35
|
+
validate do |user|
|
36
|
+
insist { user }.is_a?(String)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
attribute :group, "The group to use for executing this program.",
|
41
|
+
:default => "root" do
|
42
|
+
validate do |group|
|
43
|
+
insist { group }.is_a?(String)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attribute :target_version, "The version of this runner platform to target." do
|
48
|
+
validate do |version|
|
49
|
+
insist { version.is_a?(String) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
attribute :description, "The human-readable description of your program",
|
54
|
+
:default => "no description given" do
|
55
|
+
validate do |description|
|
56
|
+
insist { description }.is_a?(String)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# TODO(sissel): Should this be a numeric, not a string?
|
61
|
+
attribute :umask, "The umask to apply to this program",
|
62
|
+
:default => "022" do
|
63
|
+
validate do |umask|
|
64
|
+
insist { umask }.is_a?(String)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
attribute :chroot, "The directory to chroot to", :default => "/" do
|
69
|
+
validate do |chroot|
|
70
|
+
insist { chroot }.is_a?(String)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
attribute :chdir, "The directory to chdir to before running" do
|
75
|
+
validate do |chdir|
|
76
|
+
insist { chdir }.is_a?(String)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attribute :nice, "The nice level to add to this program before running" do
|
81
|
+
validate do |nice|
|
82
|
+
insist { nice }.is_a?(Fixnum)
|
83
|
+
end
|
84
|
+
munge do |nice|
|
85
|
+
next nice.to_i
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
attribute :prestart, "A command to execute before starting and restarting. A failure of this command will cause the start/restart to abort. This is useful for health checks, config tests, or similar operations."
|
90
|
+
|
91
|
+
def initialize(target_version)
|
92
|
+
configurable_setup
|
93
|
+
self.target_version = target_version
|
94
|
+
end # def initialize
|
95
|
+
|
96
|
+
# Get the platform name for this class.
|
97
|
+
# The platform name is simply the lowercased class name, but this can be overridden by subclasses (but don't, because that makes things confusing!)
|
98
|
+
def platform
|
99
|
+
return self.class.name.split(/::/)[-1].downcase
|
100
|
+
end # def platform
|
101
|
+
|
102
|
+
# Get the template path for this platform.
|
103
|
+
def template_path
|
104
|
+
return File.join("templates", platform)
|
105
|
+
end # def template_path
|
106
|
+
|
107
|
+
def render_template(name)
|
108
|
+
possibilities = [
|
109
|
+
File.join(template_path, target_version, name),
|
110
|
+
File.join(template_path, "default", name),
|
111
|
+
File.join(template_path, name)
|
112
|
+
]
|
113
|
+
|
114
|
+
possibilities.each do |path|
|
115
|
+
next if !(File.readable?(path) && File.file?(path))
|
116
|
+
return render(File.read(path))
|
117
|
+
end
|
118
|
+
|
119
|
+
raise InvalidTemplate, "Could not find template file for '#{name}'. Tried all of these: #{possibilities.inspect}"
|
120
|
+
end # def render_template
|
121
|
+
|
122
|
+
# Render a text input through Mustache based on this object.
|
123
|
+
def render(text)
|
124
|
+
return Mustache.render(text, self)
|
125
|
+
end # def render
|
126
|
+
|
127
|
+
# Get a safe-ish filename.
|
128
|
+
#
|
129
|
+
# This renders `str` through Mustache and replaces spaces with underscores.
|
130
|
+
def safe_filename(str)
|
131
|
+
return render(str).gsub(" ","_")
|
132
|
+
end # def safe_filename
|
133
|
+
|
134
|
+
# The default install_actions is none.
|
135
|
+
#
|
136
|
+
# Subclasses which need installation actions should implement this method.
|
137
|
+
# This method will return an Array of String commands to execute in order
|
138
|
+
# to install this given runner.
|
139
|
+
#
|
140
|
+
# For examples, see launchd and systemd platforms.
|
141
|
+
def install_actions
|
142
|
+
return []
|
143
|
+
end # def install_actions
|
144
|
+
end # class PleaseRun::Base
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "pleaserun/platform/base"
|
2
|
+
require "pleaserun/namespace"
|
3
|
+
|
4
|
+
class PleaseRun::Platform::LaunchD < PleaseRun::Platform::Base
|
5
|
+
# Returns the file path to write this launchd config
|
6
|
+
def daemons_path
|
7
|
+
# Quoting launchctl(1):
|
8
|
+
# "/Library/LaunchDaemons System wide daemons provided by the administrator."
|
9
|
+
return safe_filename("/Library/LaunchDaemons/{{ name }}.plist")
|
10
|
+
end # def daemons_path
|
11
|
+
|
12
|
+
def files
|
13
|
+
return Enumerator::Generator.new do |out|
|
14
|
+
out.yield [ daemons_path, render_template("program.plist") ]
|
15
|
+
end
|
16
|
+
end # def files
|
17
|
+
|
18
|
+
def install_actions
|
19
|
+
return [ "launchctl load #{daemons_path}", ]
|
20
|
+
end # def install_actions
|
21
|
+
|
22
|
+
def xml_args
|
23
|
+
return if args.nil?
|
24
|
+
return args.collect { |a| "<string>#{a}</string>" }.join("\n ")
|
25
|
+
end # def xml_args
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'pleaserun/namespace'
|
2
|
+
require "pleaserun/platform/base"
|
3
|
+
|
4
|
+
class PleaseRun::Platform::Runit < PleaseRun::Platform::Base
|
5
|
+
attribute :service_path, "The path runit service directory",
|
6
|
+
:default => "/service" do |path|
|
7
|
+
validate do
|
8
|
+
insist { path }.is_a?(String)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def files
|
13
|
+
return Enumerator::Generator.new do |enum|
|
14
|
+
enum.yield [ safe_filename("{{ service_path }}/{{ name }}/run"), render_template('run') ]
|
15
|
+
enum.yield [ safe_filename("{{ service_path }}/{{ name}}/log/run"), render_template('log') ]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'pleaserun/namespace'
|
2
|
+
require "pleaserun/platform/base"
|
3
|
+
|
4
|
+
class PleaseRun::Platform::Systemd < PleaseRun::Platform::Base
|
5
|
+
def files
|
6
|
+
begin
|
7
|
+
# TODO(sissel): Make it easy for subclasses to extend validation on attributes.
|
8
|
+
insist { program } =~ /^\//
|
9
|
+
rescue Insist::Failure
|
10
|
+
raise PleaseRun::Configurable::ValidationError, "In systemd, the program must be a full path. You gave '#{program}'."
|
11
|
+
end
|
12
|
+
|
13
|
+
return Enumerator::Generator.new do |enum|
|
14
|
+
enum.yield [ safe_filename("/lib/systemd/system/{{{ name }}}.service"), render_template("program.service") ]
|
15
|
+
if prestart
|
16
|
+
enum.yield [ safe_filename("/lib/systemd/system/{{{ name }}}-prestart.sh"), render_template("prestart.sh"), 0755 ]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end # def files
|
20
|
+
|
21
|
+
def install_actions
|
22
|
+
return [ "systemctl --system daemon-reload" ]
|
23
|
+
end
|
24
|
+
end # class PleaseRun::Platform::Systemd
|