Class: ServiceInstaller

Inherits:
Object
  • Object
show all
Defined in:
lib/service_installer.rb

Overview

Generates and installs the systemd unit for the collector.

The unit is generated rather than shipped because the correct paths are only knowable on the target host: under rvm/rbenv/asdf the executable does not live in /usr/local/bin, and systemd's minimal PATH does not include the gem bin directory that passenger-status is installed into.

Defined Under Namespace

Classes: Error

Constant Summary collapse

UNIT_NAME =
'passenger-datadog.service'
UNIT_DIR =
'/etc/systemd/system'
BASE_PATH =

systemd starts services with a minimal PATH; these are its usual defaults, which we extend with the directories the resolved binaries actually live in.

%w[/usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin].freeze
UNIT_TEMPLATE =
<<~UNIT
  # Generated by `passenger-datadog install-service` on this host.
  # Paths are resolved for this machine's Ruby; regenerate after a Ruby upgrade.
  [Unit]
  Description=Send Passenger stats to Datadog
  After=network-online.target
  Wants=network-online.target

  [Service]
  Type=notify
  NotifyAccess=main
  ExecStart=%<exec_start>s
  Environment=PATH=%<path>s
  # systemd inherits no gem environment, so pin the one this Ruby is using.
  Environment=GEM_HOME=%<gem_home>s
  Environment=GEM_PATH=%<gem_path>s
  Restart=on-failure
  RestartSec=5s
  # Passenger recommends running passenger-status as root so it can see every
  # Passenger instance on the host.
  User=%<user>s
  Group=%<group>s

  [Install]
  WantedBy=multi-user.target
UNIT

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user: 'root', group: 'root', unit_dir: UNIT_DIR, executable: nil, passenger_status: nil, search_paths: nil, env: ENV) ⇒ ServiceInstaller

Returns a new instance of ServiceInstaller.



50
51
52
53
54
55
56
57
58
59
# File 'lib/service_installer.rb', line 50

def initialize(user: 'root', group: 'root', unit_dir: UNIT_DIR, executable: nil,
               passenger_status: nil, search_paths: nil, env: ENV)
  @user = user
  @group = group
  @unit_dir = unit_dir
  @executable = executable
  @passenger_status = passenger_status
  @search_paths = search_paths
  @env = env
end

Instance Attribute Details

#group ⇒ Object (readonly)

Returns the value of attribute group.



48
49
50
# File 'lib/service_installer.rb', line 48

def group
  @group
end

#unit_dir ⇒ Object (readonly)

Returns the value of attribute unit_dir.



48
49
50
# File 'lib/service_installer.rb', line 48

def unit_dir
  @unit_dir
end

#user ⇒ Object (readonly)

Returns the value of attribute user.



48
49
50
# File 'lib/service_installer.rb', line 48

def user
  @user
end

Instance Method Details

#exec_start ⇒ Object

Invoke Ruby explicitly so the unit does not depend on the shebang line or on systemd's PATH resolving the executable.



67
68
69
# File 'lib/service_installer.rb', line 67

def exec_start
  "#{RbConfig.ruby} #{executable}"
end

#executable ⇒ Object



71
72
73
# File 'lib/service_installer.rb', line 71

def executable
  @executable ||= File.expand_path($PROGRAM_NAME)
end

#gem_home ⇒ Object

Where this Ruby installs and looks up gems. Under rvm/rbenv these are set by shell integration that systemd never runs, so they have to be baked in.



96
97
98
# File 'lib/service_installer.rb', line 96

def gem_home
  Gem.dir
end

#gem_path ⇒ Object



100
101
102
# File 'lib/service_installer.rb', line 100

def gem_path
  Gem.path.uniq.join(File::PATH_SEPARATOR)
end

#install(force: false) ⇒ Object

Writes the unit and reloads systemd. Returns the path written.

Raises:



110
111
112
113
114
115
116
117
118
# File 'lib/service_installer.rb', line 110

def install(force: false)
  require_root
  raise Error, "#{unit_path} already exists; pass --force to overwrite" if File.exist?(unit_path) && !force

  File.write(unit_path, unit)
  File.chmod(0o644, unit_path)
  daemon_reload
  unit_path
end

#passenger_status ⇒ Object



82
83
84
85
# File 'lib/service_installer.rb', line 82

def passenger_status
  @passenger_status ||= which('passenger-status') ||
                        raise(Error, 'passenger-status not found; is the passenger gem installed for this ruby?')
end

#search_paths ⇒ Object

passenger is a runtime dependency, so passenger-status is installed alongside this executable. Looking there first means the command still works under sudo, whose secure_path strips rvm/rbenv directories from PATH.



90
91
92
# File 'lib/service_installer.rb', line 90

def search_paths
  @search_paths ||= [File.dirname(executable), Gem.bindir] + @env.fetch('PATH', '').split(File::PATH_SEPARATOR)
end

#service_path ⇒ Object

Everything systemd would normally have, plus the directories holding the resolved Ruby and passenger-status, so the backtick call in the collector can find passenger-status.



78
79
80
# File 'lib/service_installer.rb', line 78

def service_path
  ([File.dirname(passenger_status), File.dirname(RbConfig.ruby)] + BASE_PATH).uniq.join(':')
end

#uninstall ⇒ Object



120
121
122
123
124
125
126
127
128
# File 'lib/service_installer.rb', line 120

def uninstall
  require_root
  return nil unless File.exist?(unit_path)

  system('systemctl', 'disable', '--now', UNIT_NAME, out: File::NULL, err: File::NULL)
  File.delete(unit_path)
  daemon_reload
  unit_path
end

#unit ⇒ Object



104
105
106
107
# File 'lib/service_installer.rb', line 104

def unit
  format(UNIT_TEMPLATE, exec_start: exec_start, path: service_path, gem_home: gem_home,
                        gem_path: gem_path, user: user, group: group)
end

#unit_path ⇒ Object



61
62
63
# File 'lib/service_installer.rb', line 61

def unit_path
  File.join(unit_dir, UNIT_NAME)
end