From f6b57d7b92adf55a4e54e4549c8f2d1f2ef0ad7a Mon Sep 17 00:00:00 2001 From: bfulton Date: Tue, 19 Jun 2012 09:21:36 -0400 Subject: [PATCH] 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]