cookbooks/fb_systemd/libraries/systemd_helpers.rb (96 lines of code) (raw):
# Copyright (c) 2017-present, Facebook, Inc.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'iniparse'
require 'shellwords'
module FB
class Systemd
def self.condition_user_online(user)
# see https://www.freedesktop.org/wiki/Software/systemd/NetworkTarget/
user_has_session = self.condition_user_session(user)
return {
'After' => 'network-online.target',
'Wants' => 'network-online.target',
}.merge(user_has_session)
end
def self.condition_user_session(user)
# this should hopefully be implemented upstream at some point, maybe as
# ConditionUserSession
if user.is_a?(Integer)
uid = user
else
# throws ArgumentError if the username is invalid
user_entry = ::Etc.getpwnam(user)
uid = user_entry.uid
end
return { 'ConditionPathExists' => "/run/user/#{uid}/bus" }
end
def self.path_to_unit(path, unit_type)
cmd = [
'/bin/systemd-escape',
'--path',
"--suffix=#{unit_type}",
path,
]
return Mixlib::ShellOut.new(cmd).run_command.stdout.chomp
end
def self.sanitize(name)
name.gsub(/[^[a-zA-Z0-9]]/, '_')
end
# this is based on
# https://github.com/chef/chef/blob/61a8aa44ac33fc3bbeb21fa33acf919a97272eb7/lib/chef/resource/systemd_unit.rb#L66-L83
def self.to_ini(content)
append_sections = ''
case content
when Hash
IniParse.gen do |doc|
content.each_pair do |sect, opts|
case opts
when Hash
doc.section(sect) do |section|
opts.each_pair do |opt, val|
[val].flatten.each do |v|
section.option(opt, v)
end
end
end
when Array
opts.each do |o|
append_sections << "\n" << IniParse.gen do |d|
d.section(sect) do |section|
o.each_pair do |opt, val|
[val].flatten.each do |v|
section.option(opt, v)
end
end
end
end.to_s
end
end
end
end.to_s + append_sections
else
IniParse.parse(content.to_s).to_s
end
end
def self.merge_unit(default_systemd_settings, systemd_overrides)
merged = {
'Service' => {},
'Unit' => {},
'Install' => {},
}
default_systemd_settings.each do |k, v|
merged[k] = v.clone
end
if systemd_overrides
['Service', 'Unit', 'Install'].each do |stanza|
systemd_overrides[stanza]&.each do |k, override|
default = merged[stanza][k]
# If either value is a list, append them together
list = override.is_a?(Array) || default.is_a?(Array)
if list
merged[stanza][k] = Array(default) + Array(override)
else
# Override
merged[stanza][k] = override
end
end
end
end
# Remove empty keys in the systemd_overrides
merged.reject! { |_k, v| v.empty? }
# Sort the stanzas so reordering the keys doesn't alter the
# returned hash structure
['Service', 'Unit', 'Install'].each do |stanza|
merged[stanza] = merged[stanza].sort.to_h if merged[stanza]
end
merged
end
end
end