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.
Files changed (44) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +13 -0
  3. data/Gemfile.lock +73 -0
  4. data/Guardfile +17 -0
  5. data/Makefile +50 -0
  6. data/README.md +98 -0
  7. data/bin/pleaserun +7 -0
  8. data/examples/runit.rb +16 -0
  9. data/lib/pleaserun/cli.rb +241 -0
  10. data/lib/pleaserun/configurable.rb +143 -0
  11. data/lib/pleaserun/detector.rb +58 -0
  12. data/lib/pleaserun/mustache_methods.rb +41 -0
  13. data/lib/pleaserun/namespace.rb +3 -0
  14. data/lib/pleaserun/platform/base.rb +144 -0
  15. data/lib/pleaserun/platform/launchd.rb +27 -0
  16. data/lib/pleaserun/platform/runit.rb +18 -0
  17. data/lib/pleaserun/platform/systemd.rb +24 -0
  18. data/lib/pleaserun/platform/sysv.rb +12 -0
  19. data/lib/pleaserun/platform/upstart.rb +11 -0
  20. data/pleaserun.gemspec +27 -0
  21. data/spec/pleaserun/configurable_spec.rb +215 -0
  22. data/spec/pleaserun/mustache_methods_spec.rb +46 -0
  23. data/spec/pleaserun/platform/base_spec.rb +27 -0
  24. data/spec/pleaserun/platform/launchd_spec.rb +93 -0
  25. data/spec/pleaserun/platform/systemd_spec.rb +119 -0
  26. data/spec/pleaserun/platform/sysv_spec.rb +133 -0
  27. data/spec/pleaserun/platform/upstart_spec.rb +117 -0
  28. data/spec/testenv.rb +69 -0
  29. data/templates/launchd/10.9/program.plist +47 -0
  30. data/templates/runit/log +4 -0
  31. data/templates/runit/run +17 -0
  32. data/templates/systemd/default/prestart.sh +2 -0
  33. data/templates/systemd/default/program.service +17 -0
  34. data/templates/sysv/lsb-3.1/default +0 -0
  35. data/templates/sysv/lsb-3.1/init.d +141 -0
  36. data/templates/upstart/1.5/init.conf +41 -0
  37. data/templates/upstart/1.5/init.d.sh +4 -0
  38. data/test.rb +33 -0
  39. data/test/helpers.rb +20 -0
  40. data/test/test.rb +60 -0
  41. data/test/vagrant/Vagrantfile +40 -0
  42. data/test/vagrant/fedora-18/Vagrantfile +28 -0
  43. data/test/vagrant/fedora-18/provision.sh +10 -0
  44. 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,3 @@
1
+ module PleaseRun
2
+ module Platform; end
3
+ 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