Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
test:
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.0"
bundler-cache: true

- name: Install podman
run: |
sudo apt-get update
sudo apt-get install -y podman

- name: Install dependencies
run: bundle install

- name: Run tests
run: bundle exec rspec
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Foreman export scripts for user-level systemd on Ubuntu 16.04+
# Foreman export scripts for user-level systemd

```ruby
# Gemfile
Expand All @@ -13,9 +13,29 @@ bundle exec foreman export systemd-user --app <app-name>

Note that this may break from foreman's protocol a bit, because it starts the processes after export. It does this by running the following:
```
systemctl --user daemon-reload
loginctl enable-linger
systemctl --user enable <app-name>.target
systemctl --user restart <app-name>.target
```
After forgetting to run these steps enough times, I just decided to bake it into the export.

## Including extra systemd files

Use `--include-dir` to copy additional systemd files (drop-in overrides, extra units, timers) after generating the main units:

```
bundle exec foreman export systemd-user --app <app-name> --include-dir Procfile.systemd
```

Example directory structure:
```
Procfile.systemd/
<app-name>-web@.service.d/
override.conf # Drop-in override for the web service
<app-name>-restart.service # Extra standalone unit
<app-name>-restart.timer # Timer (will be enabled and started automatically)
```

Any `.timer` files in the root of the include directory will be enabled with `--now`.

5 changes: 5 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
#!/usr/bin/env rake
require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

task default: :spec
14 changes: 14 additions & 0 deletions bin/foreman
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env ruby

require "rubygems"
require "foreman/cli"
require "foreman-export-systemd_user"

# Add --include-dir option to foreman export command
Foreman::CLI.class_eval do
option = Thor::Option.new(:include_dir, type: :string, desc: "Directory of additional systemd files to copy")
export_command = commands["export"]
export_command.options[:include_dir] = option
end

Foreman::CLI.start
10 changes: 10 additions & 0 deletions bin/setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail

echo "==> Installing gem dependencies..."
bundle install

echo "==> Building test container image..."
podman build -t foreman-export-systemd-user-test -f spec/Dockerfile.systemd spec/

echo "==> Setup complete! Run 'bundle exec rake spec' to run tests."
8 changes: 5 additions & 3 deletions foreman-export-systemd_user.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ Gem::Specification.new do |s|
s.authors = ["Micah Geisel"]
s.email = ["micah@botandrose.com"]
s.homepage = "http://github.com/botandrose/foreman-export-systemd_user"
s.summary = "Upstart user-level export scripts for systemd on Ubuntu 16.04+"
s.description = "Upstart user-level export scripts for systemd on Ubuntu 16.04+"
s.summary = "Foreman export scripts for user-level systemd"
s.description = "Foreman export scripts for user-level systemd"

s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]

s.add_runtime_dependency "foreman"
s.add_runtime_dependency "foreman", ">= 0.90.0"

s.add_development_dependency "rspec"
end
61 changes: 46 additions & 15 deletions lib/foreman/export/systemd_user.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
require "erb"
require "foreman/export"

File.instance_eval { def exists?(p); exist?(p); end } # restore old method removed in Ruby 3.2, because foreman 0.87.2 uses it, and appears abandoned, and I don't want to maintain a fork.

class Foreman::Export::SystemdUser < Foreman::Export::Base
def initialize location, engine, options={}
super
# what a pain in the ass
# template is obviously not intended to be overriden
unless @options.has_key?(:template)
template = File.expand_path("../../../../data/export/systemd_user", __FILE__)
@options = { template: template }.merge(@options).freeze
end
TEMPLATE_DIR = File.expand_path("../../../../data/export/systemd_user", __FILE__)

def initialize(location, engine, options = {})
options = options.dup
options[:template] ||= TEMPLATE_DIR
super(location, engine, options)
end

def app
Expand All @@ -24,7 +20,15 @@ def location

def export
super
clean_old_units
write_units
install_include_dir
configure_systemd
end

private

def clean_old_units
Dir["#{location}/#{app}*.target"]
.concat(Dir["#{location}/#{app}*.service"])
.concat(Dir["#{location}/#{app}*.target.wants/#{app}*.service"])
Expand All @@ -35,7 +39,9 @@ def export
Dir["#{location}/#{app}*.target.wants"].each do |file|
clean_dir file
end
end

def write_units
process_master_names = []

engine.each_process do |name, process|
Expand All @@ -55,17 +61,42 @@ def export
end

write_template "systemd_user/master.target.erb", "#{app}.target", binding
end

def run_command command
puts command
raise unless system(command)
end

def include_dir
dir = options[:include_dir]
return unless dir

raise "include_dir '#{dir}' is not a directory" unless File.directory?(dir)
dir
end

def install_include_dir
if include_dir
run_command "cp -r #{include_dir}/. #{location}/"
end
end

def configure_systemd
run_command "systemctl --user daemon-reload"
run_command "test -f /var/lib/systemd/linger/$USER || loginctl enable-linger"
run_command "systemctl --user enable #{app}.target"
run_command "systemctl --user restart #{app}.target"
enable_timers
end

private

def run_command command
puts command
raise unless system(command)
def enable_timers
if include_dir
Dir.glob("#{include_dir}/*.timer").each do |timer|
timer_name = File.basename(timer)
run_command "systemctl --user enable --now #{timer_name}"
end
end
end
end

38 changes: 38 additions & 0 deletions spec/Dockerfile.systemd
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y \
systemd \
systemd-sysv \
dbus \
dbus-user-session \
ruby \
ruby-bundler \
git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Create test user
RUN useradd -m -s /bin/bash testuser \
&& mkdir -p /home/testuser/.config/systemd/user \
&& chown -R testuser:testuser /home/testuser

# Enable linger for testuser so user services start at boot
RUN mkdir -p /var/lib/systemd/linger \
&& touch /var/lib/systemd/linger/testuser

# Remove unnecessary systemd services that won't work in container
RUN rm -f /lib/systemd/system/multi-user.target.wants/* \
/etc/systemd/system/*.wants/* \
/lib/systemd/system/local-fs.target.wants/* \
/lib/systemd/system/sockets.target.wants/*udev* \
/lib/systemd/system/sockets.target.wants/*initctl* \
/lib/systemd/system/sysinit.target.wants/systemd-tmpfiles-setup* \
/lib/systemd/system/systemd-update-utmp*

VOLUME ["/sys/fs/cgroup"]

STOPSIGNAL SIGRTMIN+3

CMD ["/sbin/init"]
Loading