From f6b57d7b92adf55a4e54e4549c8f2d1f2ef0ad7a Mon Sep 17 00:00:00 2001 From: bfulton Date: Tue, 19 Jun 2012 09:21:36 -0400 Subject: [PATCH 1/4] rough draft for systemd export support http://0pointer.de/blog/projects/systemd.html This adds support for exporting systemd targets and services. The structure is based on the existing upstart support. Quality is draft and expected to refine in the coming weeks. One Foremanism that is not respected by these export templates is the usual log output location, instead stdout and stderr go to syslog. --- data/export/systemd/master.target.erb | 1 + data/export/systemd/process.service.erb | 17 ++++ data/export/systemd/process_master.target.erb | 5 ++ lib/foreman/export.rb | 1 + lib/foreman/export/systemd.rb | 25 ++++++ man/foreman.1.ronn | 13 +++ spec/foreman/export/systemd_spec.rb | 87 +++++++++++++++++++ .../export/systemd/app-alpha-1.service | 16 ++++ .../export/systemd/app-alpha-2.service | 16 ++++ .../resources/export/systemd/app-alpha.target | 5 ++ .../export/systemd/app-bravo-1.service | 16 ++++ .../resources/export/systemd/app-bravo.target | 5 ++ spec/resources/export/systemd/app.target | 1 + 13 files changed, 208 insertions(+) create mode 100644 data/export/systemd/master.target.erb create mode 100644 data/export/systemd/process.service.erb create mode 100644 data/export/systemd/process_master.target.erb create mode 100644 lib/foreman/export/systemd.rb create mode 100644 spec/foreman/export/systemd_spec.rb create mode 100644 spec/resources/export/systemd/app-alpha-1.service create mode 100644 spec/resources/export/systemd/app-alpha-2.service create mode 100644 spec/resources/export/systemd/app-alpha.target create mode 100644 spec/resources/export/systemd/app-bravo-1.service create mode 100644 spec/resources/export/systemd/app-bravo.target create mode 100644 spec/resources/export/systemd/app.target diff --git a/data/export/systemd/master.target.erb b/data/export/systemd/master.target.erb new file mode 100644 index 0000000..3a8466d --- /dev/null +++ b/data/export/systemd/master.target.erb @@ -0,0 +1 @@ +[Unit] diff --git a/data/export/systemd/process.service.erb b/data/export/systemd/process.service.erb new file mode 100644 index 0000000..14e6bed --- /dev/null +++ b/data/export/systemd/process.service.erb @@ -0,0 +1,17 @@ +[Unit] +StopWhenUnneeded=true + +[Service] +User=<%= user %> +WorkingDirectory=<%= engine.root %> +Environment=PORT=<%= port %><% engine.env.each_pair do |var,env| %> +Environment=<%= var.upcase %>=<%= env %><% end %> +ExecStart=/bin/bash -lc '<%= process.command %>' +Restart=always +StandardInput=null +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=%n + +[Install] +WantedBy=<%= app %>-<%= name %>.target diff --git a/data/export/systemd/process_master.target.erb b/data/export/systemd/process_master.target.erb new file mode 100644 index 0000000..0a2f981 --- /dev/null +++ b/data/export/systemd/process_master.target.erb @@ -0,0 +1,5 @@ +[Unit] +StopWhenUnneeded=true + +[Install] +WantedBy=<%= app %>.target \ No newline at end of file diff --git a/lib/foreman/export.rb b/lib/foreman/export.rb index cc45be2..cdec407 100644 --- a/lib/foreman/export.rb +++ b/lib/foreman/export.rb @@ -32,3 +32,4 @@ require "foreman/export/bluepill" require "foreman/export/runit" require "foreman/export/supervisord" require "foreman/export/launchd" +require "foreman/export/systemd" diff --git a/lib/foreman/export/systemd.rb b/lib/foreman/export/systemd.rb new file mode 100644 index 0000000..a6f0381 --- /dev/null +++ b/lib/foreman/export/systemd.rb @@ -0,0 +1,25 @@ +require "erb" +require "foreman/export" + +class Foreman::Export::Systemd < Foreman::Export::Base + + def export + super + + Dir["#{location}/#{app}*.target"].concat(Dir["#{location}/#{app}*.service"]).each do |file| + clean file + end + + write_template "systemd/master.target.erb", "#{app}.target", binding + + engine.each_process do |name, process| + next if engine.formation[name] < 1 + write_template "systemd/process_master.target.erb", "#{app}-#{name}.target", binding + + 1.upto(engine.formation[name]) do |num| + port = engine.port_for(process, num) + write_template "systemd/process.service.erb", "#{app}-#{name}-#{num}.service", binding + end + end + end +end diff --git a/man/foreman.1.ronn b/man/foreman.1.ronn index e299f02..83c27a2 100644 --- a/man/foreman.1.ronn +++ b/man/foreman.1.ronn @@ -103,6 +103,8 @@ foreman currently supports the following output formats: * runit + * systemd + * upstart ## INITTAB EXPORT @@ -114,6 +116,17 @@ Will export a chunk of inittab-compatible configuration: EX02:4:respawn:/bin/su - example -c 'PORT=5100 bundle exec rake jobs:work >> /var/log/job-1.log 2>&1' # ----- end foreman example processes ----- +## SYSTEMD EXPORT + +Will create a series of systemd scripts in the location you specify. Scripts +will be structured to make the following commands valid: + + `systemctl start appname.target` + + `systemctl stop appname-processname.target` + + `systemctl restart appname-processname-3.service` + ## UPSTART EXPORT Will create a series of upstart scripts in the location you specify. Scripts diff --git a/spec/foreman/export/systemd_spec.rb b/spec/foreman/export/systemd_spec.rb new file mode 100644 index 0000000..2ef1870 --- /dev/null +++ b/spec/foreman/export/systemd_spec.rb @@ -0,0 +1,87 @@ +require "spec_helper" +require "foreman/engine" +require "foreman/export/systemd" +require "tmpdir" + +describe Foreman::Export::Systemd, :fakefs do + let(:procfile) { write_procfile("/tmp/app/Procfile") } + let(:formation) { nil } + let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } + let(:options) { Hash.new } + let(:systemd) { Foreman::Export::Systemd.new("/tmp/init", engine, options) } + + before(:each) { load_export_templates_into_fakefs("systemd") } + before(:each) { stub(systemd).say } + + it "exports to the filesystem" do + systemd.export + + File.read("/tmp/init/app.target").should == example_export_file("systemd/app.target") + File.read("/tmp/init/app-alpha.target").should == example_export_file("systemd/app-alpha.target") + File.read("/tmp/init/app-alpha-1.service").should == example_export_file("systemd/app-alpha-1.service") + File.read("/tmp/init/app-bravo.target").should == example_export_file("systemd/app-bravo.target") + File.read("/tmp/init/app-bravo-1.service").should == example_export_file("systemd/app-bravo-1.service") + end + + it "cleans up if exporting into an existing dir" do + mock(FileUtils).rm("/tmp/init/app.target") + mock(FileUtils).rm("/tmp/init/app-alpha.target") + mock(FileUtils).rm("/tmp/init/app-alpha-1.service") + mock(FileUtils).rm("/tmp/init/app-bravo.target") + mock(FileUtils).rm("/tmp/init/app-bravo-1.service") + + systemd.export + systemd.export + end + + it "includes environment variables" do + engine.env['KEY'] = 'some "value"' + systemd.export + File.read("/tmp/init/app-alpha-1.service").should =~ /KEY=some "value"$/ + end + + context "with a formation" do + let(:formation) { "alpha=2" } + + it "exports to the filesystem with concurrency" do + systemd.export + + File.read("/tmp/init/app.target").should == example_export_file("systemd/app.target") + File.read("/tmp/init/app-alpha.target").should == example_export_file("systemd/app-alpha.target") + File.read("/tmp/init/app-alpha-1.service").should == example_export_file("systemd/app-alpha-1.service") + File.read("/tmp/init/app-alpha-2.service").should == example_export_file("systemd/app-alpha-2.service") + File.exists?("/tmp/init/app-bravo-1.service").should == false + end + end + + context "with alternate templates" do + let(:template) { "/tmp/alternate" } + let(:options) { { :app => "app", :template => template } } + + before do + FileUtils.mkdir_p template + File.open("#{template}/master.target.erb", "w") { |f| f.puts "alternate_template" } + end + + it "can export with alternate template files" do + systemd.export + File.read("/tmp/init/app.target").should == "alternate_template\n" + end + end + + context "with alternate templates from home dir" do + + before do + FileUtils.mkdir_p File.expand_path("~/.foreman/templates/systemd") + File.open(File.expand_path("~/.foreman/templates/systemd/master.target.erb"), "w") do |file| + file.puts "default_alternate_template" + end + end + + it "can export with alternate template files" do + systemd.export + File.read("/tmp/init/app.target").should == "default_alternate_template\n" + end + end + +end diff --git a/spec/resources/export/systemd/app-alpha-1.service b/spec/resources/export/systemd/app-alpha-1.service new file mode 100644 index 0000000..175bc17 --- /dev/null +++ b/spec/resources/export/systemd/app-alpha-1.service @@ -0,0 +1,16 @@ +[Unit] +StopWhenUnneeded=true + +[Service] +User=app +WorkingDirectory=/tmp/app +Environment=PORT=5000 +ExecStart=/bin/bash -lc './alpha' +Restart=always +StandardInput=null +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=%n + +[Install] +WantedBy=app-alpha.target diff --git a/spec/resources/export/systemd/app-alpha-2.service b/spec/resources/export/systemd/app-alpha-2.service new file mode 100644 index 0000000..7c30c31 --- /dev/null +++ b/spec/resources/export/systemd/app-alpha-2.service @@ -0,0 +1,16 @@ +[Unit] +StopWhenUnneeded=true + +[Service] +User=app +WorkingDirectory=/tmp/app +Environment=PORT=5001 +ExecStart=/bin/bash -lc './alpha' +Restart=always +StandardInput=null +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=%n + +[Install] +WantedBy=app-alpha.target diff --git a/spec/resources/export/systemd/app-alpha.target b/spec/resources/export/systemd/app-alpha.target new file mode 100644 index 0000000..ec16afa --- /dev/null +++ b/spec/resources/export/systemd/app-alpha.target @@ -0,0 +1,5 @@ +[Unit] +StopWhenUnneeded=true + +[Install] +WantedBy=app.target diff --git a/spec/resources/export/systemd/app-bravo-1.service b/spec/resources/export/systemd/app-bravo-1.service new file mode 100644 index 0000000..1975015 --- /dev/null +++ b/spec/resources/export/systemd/app-bravo-1.service @@ -0,0 +1,16 @@ +[Unit] +StopWhenUnneeded=true + +[Service] +User=app +WorkingDirectory=/tmp/app +Environment=PORT=5100 +ExecStart=/bin/bash -lc './bravo' +Restart=always +StandardInput=null +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=%n + +[Install] +WantedBy=app-bravo.target diff --git a/spec/resources/export/systemd/app-bravo.target b/spec/resources/export/systemd/app-bravo.target new file mode 100644 index 0000000..ec16afa --- /dev/null +++ b/spec/resources/export/systemd/app-bravo.target @@ -0,0 +1,5 @@ +[Unit] +StopWhenUnneeded=true + +[Install] +WantedBy=app.target diff --git a/spec/resources/export/systemd/app.target b/spec/resources/export/systemd/app.target new file mode 100644 index 0000000..3a8466d --- /dev/null +++ b/spec/resources/export/systemd/app.target @@ -0,0 +1 @@ +[Unit] From d3571977181177fdd57efbe98099174959d08fa6 Mon Sep 17 00:00:00 2001 From: Bright Fulton Date: Wed, 12 Dec 2012 14:57:11 -0500 Subject: [PATCH 2/4] better default for things which intentionally daemonize child processes, the default KillMode is control-group which survives daemonization --- data/export/systemd/process.service.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/data/export/systemd/process.service.erb b/data/export/systemd/process.service.erb index 14e6bed..e51d8c0 100644 --- a/data/export/systemd/process.service.erb +++ b/data/export/systemd/process.service.erb @@ -12,6 +12,7 @@ StandardInput=null StandardOutput=syslog StandardError=syslog SyslogIdentifier=%n +KillMode=process [Install] WantedBy=<%= app %>-<%= name %>.target From 669a920c1eb0cbe7c292d08adadfefb1921c75f7 Mon Sep 17 00:00:00 2001 From: Bright Fulton Date: Thu, 13 Dec 2012 20:53:06 -0500 Subject: [PATCH 3/4] fix spec after d4ab495 --- spec/resources/export/systemd/app-alpha-1.service | 1 + spec/resources/export/systemd/app-alpha-2.service | 1 + spec/resources/export/systemd/app-bravo-1.service | 1 + 3 files changed, 3 insertions(+) diff --git a/spec/resources/export/systemd/app-alpha-1.service b/spec/resources/export/systemd/app-alpha-1.service index 175bc17..9e3a0a9 100644 --- a/spec/resources/export/systemd/app-alpha-1.service +++ b/spec/resources/export/systemd/app-alpha-1.service @@ -11,6 +11,7 @@ StandardInput=null StandardOutput=syslog StandardError=syslog SyslogIdentifier=%n +KillMode=process [Install] WantedBy=app-alpha.target diff --git a/spec/resources/export/systemd/app-alpha-2.service b/spec/resources/export/systemd/app-alpha-2.service index 7c30c31..f1620b7 100644 --- a/spec/resources/export/systemd/app-alpha-2.service +++ b/spec/resources/export/systemd/app-alpha-2.service @@ -11,6 +11,7 @@ StandardInput=null StandardOutput=syslog StandardError=syslog SyslogIdentifier=%n +KillMode=process [Install] WantedBy=app-alpha.target diff --git a/spec/resources/export/systemd/app-bravo-1.service b/spec/resources/export/systemd/app-bravo-1.service index 1975015..3e3617a 100644 --- a/spec/resources/export/systemd/app-bravo-1.service +++ b/spec/resources/export/systemd/app-bravo-1.service @@ -11,6 +11,7 @@ StandardInput=null StandardOutput=syslog StandardError=syslog SyslogIdentifier=%n +KillMode=process [Install] WantedBy=app-bravo.target From 7d77d8ff1a24f7ad6bc93a29da807a77a6d75ecc Mon Sep 17 00:00:00 2001 From: Bright Fulton Date: Sat, 20 Apr 2013 13:48:27 -0400 Subject: [PATCH 4/4] updated systemd export spec after rebasing included 5ef8bbdb --- spec/foreman/export/systemd_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/foreman/export/systemd_spec.rb b/spec/foreman/export/systemd_spec.rb index 2ef1870..6733a97 100644 --- a/spec/foreman/export/systemd_spec.rb +++ b/spec/foreman/export/systemd_spec.rb @@ -29,6 +29,10 @@ describe Foreman::Export::Systemd, :fakefs do mock(FileUtils).rm("/tmp/init/app-alpha-1.service") mock(FileUtils).rm("/tmp/init/app-bravo.target") mock(FileUtils).rm("/tmp/init/app-bravo-1.service") + mock(FileUtils).rm("/tmp/init/app-foo-bar.target") + mock(FileUtils).rm("/tmp/init/app-foo-bar-1.service") + mock(FileUtils).rm("/tmp/init/app-foo_bar.target") + mock(FileUtils).rm("/tmp/init/app-foo_bar-1.service") systemd.export systemd.export