pleaserun 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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