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
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .rspec_system/
2
+ .*.sw*
3
+ .vagrant
4
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+ gem "clamp"
3
+ gem "cabin"
4
+ gem "stud"
5
+ gem "mustache"
6
+ gem "insist"
7
+
8
+ # For testing
9
+ gem "rspec"
10
+ gem "guard"
11
+ gem "guard-rspec"
12
+ gem "net-ssh"
13
+ gem "peach"
data/Gemfile.lock ADDED
@@ -0,0 +1,73 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ atomic (1.1.15)
5
+ avl_tree (1.1.3)
6
+ cabin (0.6.1)
7
+ celluloid (0.15.2)
8
+ timers (~> 1.1.0)
9
+ clamp (0.6.3)
10
+ coderay (1.1.0)
11
+ diff-lcs (1.2.5)
12
+ ffi (1.9.3)
13
+ formatador (0.2.4)
14
+ guard (2.2.5)
15
+ formatador (>= 0.2.4)
16
+ listen (~> 2.1)
17
+ lumberjack (~> 1.0)
18
+ pry (>= 0.9.12)
19
+ thor (>= 0.18.1)
20
+ guard-rspec (4.0.4)
21
+ guard (>= 2.1.1)
22
+ rspec (~> 2.14)
23
+ hitimes (1.2.1)
24
+ insist (1.0.0)
25
+ listen (2.4.0)
26
+ celluloid (>= 0.15.2)
27
+ rb-fsevent (>= 0.9.3)
28
+ rb-inotify (>= 0.9)
29
+ lumberjack (1.0.4)
30
+ method_source (0.8.2)
31
+ metriks (0.9.9.6)
32
+ atomic (~> 1.0)
33
+ avl_tree (~> 1.1.2)
34
+ hitimes (~> 1.1)
35
+ mustache (0.99.5)
36
+ net-ssh (2.8.0)
37
+ peach (0.5.1)
38
+ pry (0.9.12.4)
39
+ coderay (~> 1.0)
40
+ method_source (~> 0.8)
41
+ slop (~> 3.4)
42
+ rb-fsevent (0.9.4)
43
+ rb-inotify (0.9.3)
44
+ ffi (>= 0.5.0)
45
+ rspec (2.14.1)
46
+ rspec-core (~> 2.14.0)
47
+ rspec-expectations (~> 2.14.0)
48
+ rspec-mocks (~> 2.14.0)
49
+ rspec-core (2.14.7)
50
+ rspec-expectations (2.14.4)
51
+ diff-lcs (>= 1.1.3, < 2.0)
52
+ rspec-mocks (2.14.4)
53
+ slop (3.4.7)
54
+ stud (0.0.17)
55
+ ffi
56
+ metriks
57
+ thor (0.18.1)
58
+ timers (1.1.0)
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ cabin
65
+ clamp
66
+ guard
67
+ guard-rspec
68
+ insist
69
+ mustache
70
+ net-ssh
71
+ peach
72
+ rspec
73
+ stud
data/Guardfile ADDED
@@ -0,0 +1,17 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ notification :tmux,
5
+ display_message: true,
6
+ timeout: 5, # in seconds
7
+ default_message_format: '%s >> %s',
8
+ success: 'green',
9
+ default_message_color: 'black',
10
+ line_separator: ' > ', # since we are single line we need a separator
11
+ color_location: 'status-left-bg' # to customize which tmux element will change color
12
+
13
+ guard :rspec, cmd: 'rspec --color --order rand:$RANDOM' do
14
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
15
+ watch(%r{^spec/.*\.rb$}) { |m| m[0] }
16
+ end
17
+
data/Makefile ADDED
@@ -0,0 +1,50 @@
1
+ GEMSPEC=$(shell ls *.gemspec)
2
+ VERSION=$(shell awk -F\" '/spec.version/ { print $$2 }' $(GEMSPEC))
3
+ NAME=$(shell awk -F\" '/spec.name/ { print $$2 }' $(GEMSPEC))
4
+ GEM=$(NAME)-$(VERSION).gem
5
+
6
+ .PHONY: test
7
+ test:
8
+ sh notify-failure.sh ruby test/all.rb
9
+
10
+ .PHONY: testloop
11
+ testloop:
12
+ while true; do \
13
+ $(MAKE) test; \
14
+ $(MAKE) wait-for-changes; \
15
+ done
16
+
17
+ .PHONY: serve-coverage
18
+ serve-coverage:
19
+ cd coverage; python -mSimpleHTTPServer
20
+
21
+ .PHONY: wait-for-changes
22
+ wait-for-changes:
23
+ -inotifywait --exclude '\.swp' -e modify $$(find $(DIRS) -name '*.rb'; find $(DIRS) -type d)
24
+
25
+ .PHONY: package
26
+ package: | $(GEM)
27
+
28
+ .PHONY: gem
29
+ gem: $(GEM)
30
+
31
+ $(GEM):
32
+ gem build $(GEMSPEC)
33
+
34
+ .PHONY: test-package
35
+ test-package: $(GEM)
36
+ # Sometimes 'gem build' makes a faulty gem.
37
+ gem unpack $(GEM)
38
+ rm -rf $(NAME)-$(VERSION)/
39
+
40
+ .PHONY: publish
41
+ publish: test-package
42
+ gem push $(GEM)
43
+
44
+ .PHONY: install
45
+ install: $(GEM)
46
+ gem install $(GEM)
47
+
48
+ .PHONY: clean
49
+ clean:
50
+ -rm -rf .yardoc $(GEM) $(NAME)-$(VERSION)/
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # Please, Run!
2
+
3
+ Pleaserun is a tool to generate startup scripts for the wasteland of sorrow
4
+ that is process launchers.
5
+
6
+ Ideally, you should be able to specify a configuration of how to run a given
7
+ service command (like apache, syslog-ng, whatever), and this tool should
8
+ be able to spit out a script or config file for your target platform.
9
+
10
+ ## Installation
11
+
12
+ ```
13
+ gem install pleaserun
14
+ ```
15
+
16
+ ## Your First Process
17
+
18
+ First, we need a program to run!
19
+
20
+ ### Example: redis
21
+
22
+ For no particular reason, this example will choose redis to run. The idea is to
23
+ simulate the same workflow you would normally go through in production: acquire
24
+ software, deploy it, run it. Pleaserun helps you with the 'run it' part, but
25
+ first let's get redis and build it.
26
+
27
+ ```
28
+ wget http://download.redis.io/releases/redis-2.8.6.tar.gz
29
+ tar -zxf redis-2.8.6.tar.gz
30
+ cd redis-2.8.6
31
+ make -j4
32
+ make install PREFIX=/tmp/redis
33
+ ```
34
+
35
+ Assuming the above succeeds (it did for me!), we now have redis installed to `/tmp/redis`:
36
+
37
+ ```
38
+ % ls /tmp/redis/bin
39
+ redis-benchmark redis-check-aof redis-check-dump redis-cli redis-server
40
+ ```
41
+
42
+ You might be thinking - why /tmp? This is just a demo! That's why! :)
43
+
44
+ ### Generate a runner
45
+
46
+ What platform are you on? Do you know the best way to run a server process? I
47
+ can never remember.
48
+
49
+ Luckily, pleaserun remembers.
50
+
51
+ ```
52
+ # Run as root so pleaserun has permissions to write to
53
+ # any files required to install this as a service!
54
+ % sudo pleaserun --install /tmp/redis/bin/redis-server
55
+ No platform selected. Autodetecting... {:platform=>"upstart", :version=>"1.5", :level=>:warn}
56
+ No name given, setting reasonable default {:name=>"redis-server", :level=>:warn}
57
+ Writing file {:destination=>"/etc/init/redis-server.conf"}
58
+ Writing file {:destination=>"/etc/init.d/redis-server"}
59
+ ```
60
+
61
+ Note: The `--install` flag above tells pleaserun to install it on this current system. The
62
+ default behavior without this flag is to install it in a temp directory so you can copy
63
+ it elsewhere if desired.
64
+
65
+ Now what? You can see above it automatically detected that "Upstart 1.5" was
66
+ the right process runner to target. Let's try using it!
67
+
68
+ ```
69
+ % status redis-server
70
+ redis-server stop/waiting
71
+
72
+ % sudo start redis-server
73
+ redis-server start/running, process 395
74
+
75
+ % status redis-server
76
+ redis-server start/running, process 395
77
+
78
+ % ps -fwwp 395
79
+ UID PID PPID C STIME TTY TIME CMD
80
+ root 395 1 0 06:27 ? 00:00:00 /tmp/redis/bin/redis-server *:6379
81
+
82
+ # Is it running? Let's check with redis-cli
83
+ % redis-cli
84
+ 127.0.0.1:6379> ping
85
+ PONG
86
+
87
+ % sudo stop redis-server
88
+ redis-server stop/waiting
89
+ ```
90
+
91
+ Bam. Pretty easy, right? Let's recap!
92
+
93
+ ### Recap
94
+
95
+ * You ran `pleaserun --install /tmp/redis/bin/redis-server`
96
+ * Pleaserun detected the platform as Upstart 1.5
97
+ * You didn't have to write an init script.
98
+ * You didn't have to know how to write an Upstart config.
data/bin/pleaserun ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ libdir = File.expand_path("../lib", File.dirname(__FILE__))
4
+ $: << libdir if File.exists?(File.join(libdir, "pleaserun", "cli.rb"))
5
+ require "pleaserun/cli"
6
+
7
+ exit(PleaseRun::CLI.run || 0)
data/examples/runit.rb ADDED
@@ -0,0 +1,16 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'pleaserun/runit'
4
+
5
+ pr = PleaseRun::RunIt.new('debian-7.0')
6
+ pr.name = 'foo'
7
+ pr.user = 'nobody'
8
+ pr.command = '/bin/true'
9
+ pr.args = []
10
+
11
+ pr.files.each do |path, content|
12
+ puts path => content.bytes.size
13
+ puts "#{path}:"
14
+ puts content.gsub(/^/, ' ')
15
+ end
16
+
@@ -0,0 +1,241 @@
1
+
2
+ require "pleaserun/namespace"
3
+ require "clamp"
4
+ require "cabin"
5
+ require "stud/temporary"
6
+
7
+ require "pleaserun/platform/base"
8
+
9
+ class PleaseRun::CLI < Clamp::Command
10
+ class Error < StandardError; end
11
+ class ConfigurationError < Error; end
12
+ class PlatformLoadError < Error; end
13
+ class FileWritingFailure < Error; end
14
+
15
+ option ["-p", "--platform"], "PLATFORM", "The name of the platform to target, such as sysv, upstart, etc"
16
+ option ["-v", "--version"], "VERSION", "The version of the platform to target, such as 'lsb-3.1' for sysv or '1.5' for upstart",
17
+ :default => "default", :attribute_name => :target_version
18
+
19
+ option "--log", "LOGFILE", "The path to use for writing pleaserun logs."
20
+ option "--json", :flag, "Output a result in JSON. Intended to be consumed by other programs. This will emit the file contents and install actions as a JSON object."
21
+
22
+ option "--install", :flag, "Install the program on this system. This will write files to the correct location and execute any actions to make the program available to the system."
23
+
24
+ option "--verbose", :flag, "More verbose logging"
25
+ option "--debug", :flag, "Debug-level logging"
26
+ option "--quiet", :flag, "Only errors or worse will be logged"
27
+
28
+ PleaseRun::Platform::Base.attributes.each do |facet|
29
+ # Skip program and args which we don't want to make into flags.
30
+ next if [:program, :args, :target_version].include?(facet.name)
31
+
32
+ # Turn the attribute name into a flag.
33
+ option "--#{facet.name}", facet.name.to_s.upcase, facet.description,
34
+ :attribute_name => facet.name
35
+ end
36
+
37
+ # TODO(sissel): Make options based on other platforms
38
+
39
+ # Make program and args attributes into parameters
40
+ base = PleaseRun::Platform::Base
41
+
42
+ # Load the 'program' attribute from the Base class and use it as the first
43
+ # cli parameter.
44
+ program = base.attributes.find { |f| f.name == :program }
45
+ raise "Something is wrong; Base missing 'program' attribute" if program.nil?
46
+ parameter "PROGRAM", program.description, :attribute_name => program.name
47
+
48
+ # Load the 'args' attribute from the Base class
49
+ # and use it as the remaining arguments setting
50
+ args = base.attributes.find { |f| f.name == :args }
51
+ raise "Something is wrong; Base missing 'args' attribute" if program.nil?
52
+
53
+ parameter "[ARGS] ...", args.description, :attribute_name => args.name
54
+
55
+ def help
56
+ return <<-HELP
57
+ Welcome to pleaserun!
58
+
59
+ This program aims to help you generate 'init' scripts/configs for various
60
+ platforms. The simplest example takes only the command you wish to run.
61
+ For example, let's run elasticsearch:
62
+
63
+ % pleaserun /opt/elasticsearch/bin/elasticsearch
64
+
65
+ The above will automatically detect what platform you are running on
66
+ and try to use the most sensible init system. For Ubuntu, this means
67
+ Upstart. For Debian, this means sysv init scripts. For Fedora, this
68
+ means systemd.
69
+
70
+ You can the running environment and settings for your runner with various
71
+ flags. By way of example, let's make our elasticsearch service run as the
72
+ 'elasticsearch' user!
73
+
74
+ % pleaserun --user elasticsearch /opt/elasticsearch/bin/elasticsearch
75
+
76
+ If you don't want the platform autodetected, you can always specify the
77
+ exact process launcher to target:
78
+
79
+ # Generate a sysv (/etc/init.d) script for LSB 3.1 (Debian uses this)
80
+ % pleaserun -p sysv -v lsb-3.1 /opt/elasticsearch/bin/elasticsearch
81
+
82
+ Let's do another example. How about running nagios in systemd, but we
83
+ want to abort if the nagios config is invalid?
84
+
85
+ % pleaserun -t systemd \
86
+ --prestart "/usr/sbin/nagios -v /etc/nagios/nagios.cfg" \
87
+ /usr/sbin/nagios /etc/nagios/nagios.cfg
88
+
89
+ The above makes 'nagios -v ...' run before any start/restart attempts
90
+ are made. If it fails, nagios will not start. Yay!
91
+
92
+ #{super}
93
+ HELP
94
+ end
95
+
96
+ def execute
97
+ setup_logger
98
+
99
+ # Provide any dynamic defaults if necessary
100
+ if platform.nil?
101
+ require "pleaserun/detector"
102
+ self.platform, self.target_version = PleaseRun::Detector.detect
103
+ @logger.warn("No platform selected. Autodetecting...", :platform => platform, :version => target_version)
104
+ end
105
+
106
+ if name.nil?
107
+ self.name = File.basename(program)
108
+ @logger.warn("No name given, setting reasonable default", :name => self.name)
109
+ end
110
+
111
+ # Load the platform implementation
112
+ platform_klass = load_platform(platform)
113
+ runner = platform_klass.new(target_version)
114
+
115
+ platform_klass.all_attributes.each do |facet|
116
+ # Get the value of this attribute
117
+ # The idea here is to translate CLI options to runner settings
118
+ value = send(facet.name)
119
+ next if value.nil?
120
+ @logger.debug("Setting runner attribute", :name => facet.name, :value => value)
121
+
122
+ # Set the value in the runner we've selected
123
+ # This is akin to `obj.someattribute = value`
124
+ runner.send("#{facet.name}=", value)
125
+ end
126
+
127
+ if json?
128
+ return run_json(runner)
129
+ else
130
+ return run_human(runner)
131
+ end
132
+ return 0
133
+ rescue Error => e
134
+ @logger.error("An error occurred: #{e}")
135
+ return 1
136
+ end # def execute
137
+
138
+ def run_json(runner)
139
+ require "json"
140
+
141
+ result = {}
142
+ result["files"] = []
143
+ runner.files.each do |path, content, perms|
144
+ result["files"] << {
145
+ "path" => path,
146
+ "content" => content,
147
+ "perms" => perms
148
+ }
149
+ end
150
+
151
+ result["install_actions"] = runner.install_actions
152
+
153
+ puts JSON.dump(result)
154
+ return 0
155
+ end # def run_json
156
+
157
+ def run_human(runner)
158
+ tmp = Stud::Temporary.directory
159
+ errors = []
160
+
161
+ runner.files.each do |path, content, perms|
162
+ #perms ||= (0666 ^ File.umask)
163
+ fullpath = install? ? path : File.join(tmp, path)
164
+ success = write(fullpath, content, perms)
165
+ errors << fullpath unless success
166
+ end
167
+
168
+ if errors.any?
169
+ raise FileWritingFailure, "Errors occurred while writing files"
170
+ end
171
+
172
+ # TODO(sissel): Refactor this to be less lines of code or put into methods.
173
+ if runner.install_actions.any?
174
+ if install?
175
+ runner.install_actions.each do |action|
176
+ @logger.info("Running install action", :action => action)
177
+ system(action)
178
+ if !$?.success?
179
+ @logger.warn("Install action failed", :action => action, :code => $?.exitstatus)
180
+ end
181
+ end # each install action
182
+ else
183
+ path = File.join(tmp, "install_actions.sh")
184
+ @logger.log("Writing install actions. You will want to run this script to properly activate your service on the target host", :path => path)
185
+ File.open(path, "w") do |fd|
186
+ runner.install_actions.each do |action|
187
+ fd.puts(action)
188
+ end
189
+ end
190
+ end
191
+ end # if runner.install_actions.any?
192
+ end # def run_human
193
+
194
+ def write(fullpath, content, perms)
195
+ @logger.log("Writing file", :destination => fullpath)
196
+ FileUtils.mkdir_p(File.dirname(fullpath))
197
+ File.write(fullpath, content)
198
+ @logger.debug("Setting permissions", :destination => fullpath, :perms => perms)
199
+ File.chmod(perms, fullpath) if perms
200
+ return true
201
+ rescue Errno::EACCES
202
+ @logger.error("Access denied in writing a file. Maybe we need to be root?", :path => fullpath)
203
+ return false
204
+ end
205
+
206
+ def setup_logger
207
+ @logger = Cabin::Channel.new
208
+ if quiet?
209
+ @logger.level = :error
210
+ elsif verbose?
211
+ @logger.level = :info
212
+ elsif debug?
213
+ @logger.level = :debug
214
+ else
215
+ @logger.level = :warn
216
+ end
217
+
218
+ if log
219
+ logfile = File.new(logfile, "a")
220
+ @logger.subscribe(logfile)
221
+ STDERR.puts "Sending all logs to #{log}" if STDERR.tty?
222
+ else
223
+ @logger.subscribe(STDERR)
224
+ end
225
+ end # def setup_logger
226
+
227
+ def load_platform(v)
228
+ @logger.debug("Loading platform", :platform => v)
229
+ platform_lib = "pleaserun/platform/#{v}"
230
+ require(platform_lib)
231
+
232
+ const = PleaseRun::Platform.constants.find { |c| c.to_s.downcase == v.downcase }
233
+ if const.nil?
234
+ raise PlatformLoadError, "Could not find platform named '#{v}' after loading library '#{platform_lib}'. This is probably a bug."
235
+ end
236
+
237
+ return PleaseRun::Platform.const_get(const)
238
+ rescue LoadError => e
239
+ raise PlatformLoadError, "Failed to find or load platform '#{v}'. This could be a typo or a bug. If it helps, the error is: #{e}"
240
+ end # def load_platform
241
+ end # class PleaseRun::CLI