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]