File: systemd.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (231 lines) | stat: -rw-r--r-- 8,520 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# Manage systemd services using systemctl

require_relative '../../../puppet/file_system'

Puppet::Type.type(:service).provide :systemd, :parent => :base do
  desc "Manages `systemd` services using `systemctl`.

  Because `systemd` defaults to assuming the `.service` unit type, the suffix
  may be omitted.  Other unit types (such as `.path`) may be managed by
  providing the proper suffix."

  commands :systemctl => "systemctl"

  confine :true => Puppet::FileSystem.exist?('/proc/1/comm') && Puppet::FileSystem.read('/proc/1/comm').include?('systemd')

  defaultfor :osfamily => [:archlinux]
  defaultfor :osfamily => :redhat, :operatingsystemmajrelease => ["7", "8", "9"]
  defaultfor :osfamily => :redhat, :operatingsystem => :fedora
  defaultfor :osfamily => :suse
  defaultfor :osfamily => :coreos
  defaultfor :operatingsystem => :amazon, :operatingsystemmajrelease => ["2"]
  defaultfor :operatingsystem => :debian
  notdefaultfor :operatingsystem => :debian, :operatingsystemmajrelease => ["5", "6", "7"] # These are using the "debian" method
  defaultfor :operatingsystem => :LinuxMint
  notdefaultfor :operatingsystem => :LinuxMint, :operatingsystemmajrelease => ["10", "11", "12", "13", "14", "15", "16", "17"] # These are using upstart
  defaultfor :operatingsystem => :ubuntu
  notdefaultfor :operatingsystem => :ubuntu, :operatingsystemmajrelease => ["10.04", "12.04", "14.04", "14.10"] # These are using upstart
  defaultfor :operatingsystem => :cumuluslinux, :operatingsystemmajrelease => ["3", "4"]

  def self.instances
    i = []
    output = systemctl('list-unit-files', '--type', 'service', '--full', '--all',  '--no-pager')
    output.scan(/^(\S+)\s+(disabled|enabled|masked|indirect|bad|static)\s*([^-]\S+)?\s*$/i).each do |m|
      Puppet.debug("#{m[0]} marked as bad by `systemctl`. It is recommended to be further checked.") if m[1] == "bad"
      i << new(:name => m[0])
    end
    return i
  rescue Puppet::ExecutionFailure
    return []
  end

  # Static services cannot be enabled or disabled manually. Indirect services 
  # should not be enabled or disabled due to limitations in systemd (see 
  # https://github.com/systemd/systemd/issues/6681).
  def enabled_insync?(current)
    case cached_enabled?[:output]
    when 'static'
      # masking static services is OK, but enabling/disabling them is not
      if @resource[:enable] == :mask
        current == @resource[:enable]
      else
        Puppet.debug("Unable to enable or disable static service #{@resource[:name]}")
        return true
      end
    when 'indirect'
      Puppet.debug("Service #{@resource[:name]} is in 'indirect' state and cannot be enabled/disabled")
      return true
    else
      current == @resource[:enable]
    end
  end

  # This helper ensures that the enable state cache is always reset
  # after a systemctl enable operation. A particular service state is not guaranteed
  # after such an operation, so the cache must be emptied to prevent inconsistencies
  # in the provider's believed state of the service and the actual state.
  # @param action [String,Symbol] One of 'enable', 'disable', 'mask' or 'unmask'
  def systemctl_change_enable(action)
    output = systemctl(action, '--', @resource[:name])
  rescue
    raise Puppet::Error, "Could not #{action} #{self.name}: #{output}", $!.backtrace
  ensure
    @cached_enabled = nil
  end

  def disable
    systemctl_change_enable(:disable)
  end

  def get_start_link_count
    # Start links don't include '.service'. Just search for the service name.
    if @resource[:name] =~ /\.service/
      link_name = @resource[:name].split('.')[0]
    else
      link_name = @resource[:name]
    end

    Dir.glob("/etc/rc*.d/S??#{link_name}").length
  end

  def cached_enabled?
    return @cached_enabled if @cached_enabled
    cmd = [command(:systemctl), 'is-enabled', '--', @resource[:name]]
    result = execute(cmd, :failonfail => false)
    @cached_enabled = { output: result.chomp, exitcode: result.exitstatus }
  end

  def enabled?
    output = cached_enabled?[:output]
    code = cached_enabled?[:exitcode]

    # The masked state is equivalent to the disabled state in terms of
    # comparison so we only care to check if it is masked if we want to keep
    # it masked.
    #
    # We only return :mask if we're trying to mask the service. This prevents
    # flapping when simply trying to disable a masked service.
    return :mask if (@resource[:enable] == :mask) && (output == 'masked')

    # The indirect state indicates that the unit is not enabled.
    return :false if output == 'indirect'
    return :true if (code == 0)
    if (output.empty?) && (code > 0) && (Puppet.runtime[:facter].value(:osfamily).casecmp('debian').zero?)
      ret = debian_enabled?
      return ret if ret
    end

    return :false
  end

  # This method is required for Debian systems due to the way the SysVInit-Systemd
  # compatibility layer works. When we are trying to manage a service which does not
  # have a Systemd unit file, we need to go through the old init script to determine
  # whether it is enabled or not. See PUP-5016 for more details.
  #
  def debian_enabled?
    status = execute(["/usr/sbin/invoke-rc.d", "--quiet", "--query", @resource[:name], "start"], :failonfail => false)
    if [104, 106].include?(status.exitstatus)
      return :true
    elsif [101, 105].include?(status.exitstatus)
      # 101 is action not allowed, which means we have to do the check manually.
      # 105 is unknown, which generally means the initscript does not support query
      # The debian policy states that the initscript should support methods of query
      # For those that do not, perform the checks manually
      # http://www.debian.org/doc/debian-policy/ch-opersys.html
      if get_start_link_count >= 4
        return :true
      else
        return :false
      end
    else
      return :false
    end
  end

  # Define the daemon_reload? function to check if the unit is requiring to trigger a "systemctl daemon-reload"
  # If the unit file is flagged with NeedDaemonReload=yes, then a systemd daemon-reload will be run.
  # If multiple unit files have been updated, the first one flagged will trigger the daemon-reload for all of them.
  # The others will be then flagged with NeedDaemonReload=no. So the command will run only once in a puppet run.
  # This function is called only on start & restart unit options.
  # Reference: (PUP-3483) Systemd provider doesn't scan for changed units
  def daemon_reload?
    cmd = [command(:systemctl), 'show', '--property=NeedDaemonReload', '--', @resource[:name]]
    daemon_reload = execute(cmd, :failonfail => false).strip.split('=').last
    if daemon_reload == 'yes'
      daemon_reload_cmd = [command(:systemctl), 'daemon-reload']
      execute(daemon_reload_cmd, :failonfail => false)
    end
  end

  def enable
    self.unmask
    systemctl_change_enable(:enable)
  end

  def mask
    disable if exist?
    systemctl_change_enable(:mask)
  end

  def exist?
    result = execute([command(:systemctl), 'cat', '--', @resource[:name]], :failonfail => false)
    result.exitstatus == 0
  end

  def unmask
    systemctl_change_enable(:unmask)
  end

  def restartcmd
    [command(:systemctl), "restart", '--', @resource[:name]]
  end

  def startcmd
    self.unmask
    [command(:systemctl), "start", '--', @resource[:name]]
  end

  def stopcmd
    [command(:systemctl), "stop", '--', @resource[:name]]
  end

  def statuscmd
    [command(:systemctl), "is-active", '--', @resource[:name]]
  end

  def restart
    begin
      daemon_reload?
      super
    rescue Puppet::Error => e
      raise Puppet::Error.new(prepare_error_message(@resource[:name], 'restart', e))
    end
  end

  def start
    begin
      daemon_reload?
      super
    rescue Puppet::Error => e
      raise Puppet::Error.new(prepare_error_message(@resource[:name], 'start', e))
    end
  end

  def stop
    begin
      super
    rescue Puppet::Error => e
      raise Puppet::Error.new(prepare_error_message(@resource[:name], 'stop', e))
    end
  end

  def prepare_error_message(name, action, exception)
    error_return = "Systemd #{action} for #{name} failed!\n"
    journalctl_command = "journalctl -n 50 --since '5 minutes ago' -u #{name} --no-pager"
    Puppet.debug("Running journalctl command to get logs for systemd #{action} failure: #{journalctl_command}")
    journalctl_output = execute(journalctl_command)
    error_return << "journalctl log for #{name}:\n#{journalctl_output}"
  end
end