$OpenBSD: patch-lib_puppet_provider_service_openbsd_rb,v 1.28 2015/08/13 10:46:31 jasper Exp $

* From 93f6754f9c886922d39e938251d4885c1ebb5a29 Mon Sep 17 00:00:00 2001
  From: Jasper Lievisse Adriaanse <jasper@humppa.nl>
  Date: Tue, 26 Aug 2014 08:52:12 +0200
  Subject: [PATCH] (PUP-3144) Rewrite OpenBSD service provider with rcctl(8)

* From 00322051f892916a58eaf138e2757a4828c7688d Mon Sep 17 00:00:00 2001
  From: Jasper Lievisse Adriaanse <jasper@humppa.nl>
  Date: Thu, 4 Dec 2014 09:17:14 +0100
  Subject: [PATCH] (PUP-3736) Restart a service when the flags are modified

* From 55ef06ea1baff10ab518c93e3649f5f27c365055 Mon Sep 17 00:00:00 2001
  From: Jasper Lievisse Adriaanse <jasper@humppa.nl>
  Date: Fri, 23 Jan 2015 08:30:06 +0100
  Subject: [PATCH] (PUP-3939) Sync with upstream OpenBSD rcctl(1) changes

* enhance initial service provisioning with regard to service_flags

* print a warning when rcctl failed to run, instead of having the user guess
  at Ruby errors

--- lib/puppet/provider/service/openbsd.rb.orig	Thu Aug  6 18:17:42 2015
+++ lib/puppet/provider/service/openbsd.rb	Thu Aug 13 10:22:56 2015
@@ -2,341 +2,111 @@ Puppet::Type.type(:service).provide :openbsd, :parent 
 
   desc "Provider for OpenBSD's rc.d daemon control scripts"
 
+  commands :rcctl => '/usr/sbin/rcctl'
+
   confine :operatingsystem => :openbsd
   defaultfor :operatingsystem => :openbsd
   has_feature :flaggable
 
-  def self.rcconf()        '/etc/rc.conf' end
-  def self.rcconf_local()  '/etc/rc.conf.local' end
-
-  def self.defpath
-    ["/etc/rc.d"]
+  def startcmd
+    if @resource[:flags] and flags != @resource[:flags]
+      # Unfortunately, the startcmd gets called, before
+      # the service is enabled (in case its supposed to be enabled).
+      # Setting flags via rcctl is only possible, when the service is enabled
+      # In case the service is not to be enabled, it will be automatically
+      # disabled later in the same puppet run.
+      self.enable
+      self.flags = @resource[:flags]
+    end
+    [command(:rcctl), '-f', :start, @resource[:name]]
   end
 
-  def startcmd
-    [self.initscript, "-f", :start]
+  def stopcmd
+    [command(:rcctl), :stop, @resource[:name]]
   end
 
   def restartcmd
-    (@resource[:hasrestart] == :true) && [self.initscript, "-f", :restart]
+    (@resource[:hasrestart] == :true) && [command(:rcctl), '-f', :restart, @resource[:name]]
   end
 
   def statuscmd
-    [self.initscript, :check]
+    [command(:rcctl), :check, @resource[:name]]
   end
 
-  # Fetch the state of all service resources
-  def self.prefetch(resources)
-    services = instances
-    resources.keys.each do |name|
-      if provider = services.find { |svc| svc.name == name }
-        resources[name].provider = provider
-      end
-    end
-  end
-
-  # Return the list of rc scripts
   # @api private
-  def self.rclist
-    unless @rclist
-      Puppet.debug "Building list of rc scripts"
-      @rclist = []
-      defpath.each do |p|
-        Dir.glob(p + '/*').each do |item|
-          @rclist << item if File.executable?(item)
-        end
-      end
-    end
-    @rclist
-  end
+  # When storing the name, take into account not everything has
+  # '_flags', like 'multicast_host' and 'pf'.
+  def self.instances
+    instances = []
 
-  # Return a hash where the keys are the rc script names as symbols with flags
-  # as values
-  # @api private
-  def self.rcflags
-    unless @flag_hash
-      Puppet.debug "Retrieving flags for all discovered services"
+    begin
+      execpipe([command(:rcctl), :ls, :all]) do |all_output|
+        all_output.each_line do |svc|         
+          execpipe([command(:rcctl), :get, svc.chomp, :flags]) do |flags_output|
+            flags_output.each_line do |flags|
+              attributes_hash = {
+                :name      => svc,
+                :flags     => flags,
+                :hasstatus => true,
+                :provider  => :openbsd,
+              }
 
-      Puppet.debug "Reading the contents of the rc conf files"
-
-      if File.exists?(rcconf_local())
-        rcconf_local_contents = File.readlines(rcconf_local())
-      else
-        rcconf_local_contents = []
-      end
-
-      if File.exists?(rcconf())
-        rcconf_contents = File.readlines(rcconf())
-      else
-        rcconf_contents = []
-      end
-
-      @flag_hash = {}
-      rclist().each do |rcitem|
-
-        rcname = rcitem.split('/').last
-
-        if flagline = rcconf_local_contents.find {|l| l =~ /^#{rcname}_flags/ }
-          flag = parse_rc_line(flagline)
-          @flag_hash[rcname.to_sym] ||= flag
-        end
-
-        # For the defaults, if the flags are set to 'NO', we skip setting the
-        # flag here, since it will already be disabled, and this makes the
-        # output of `puppet resource service` a bit more correct.
-        if flagline = rcconf_contents.find {|l| l =~ /^#{rcname}_flags/ }
-          flag = parse_rc_line(flagline)
-          unless flag == "NO"
-            @flag_hash[rcname.to_sym] ||= flag
+              instances << new(attributes_hash)
+            end
           end
         end
-        @flag_hash[rcname.to_sym] ||= nil
       end
+      instances
+    rescue Puppet::ExecutionFailure
+      Puppet.warning("Failed to execute rcctl")
+      return nil
     end
-    @flag_hash
   end
 
-  # @api private
-  def self.parse_rc_line(rc_line)
-    rc_line.sub!(/\s*#(.*)$/,'')
-    regex = /\w+_flags=(.*)/
-    rc_line.match(regex)[1].gsub(/^"/,'').gsub(/"$/,'')
-  end
-
-  # Read the rc.conf* files and determine the value of the flags
-  # @api private
-  def self.get_flags(rcname)
-    rcflags()
-    @flag_hash[rcname.to_sym]
-  end
-
-  def self.instances
-    instances = []
-    defpath.each do |path|
-      unless File.directory?(path)
-        Puppet.debug "Service path #{path} does not exist"
-        next
-      end
-
-      rclist().each do |d|
-        instances << new(
-          :name      => File.basename(d),
-          :path      => path,
-          :flags     => get_flags(File.basename(d)),
-          :hasstatus => true
-        )
-      end
-    end
-    instances
-  end
-
-  # @api private
-  def rcvar_name
-    self.name + '_flags'
-  end
-
-  # @api private
-  def read_rcconf_local_text()
-    if File.exists?(self.class.rcconf_local())
-      File.read(self.class.rcconf_local())
-    else
-      []
-    end
-  end
-
-  # @api private
-  def load_rcconf_local_array
-    if File.exists?(self.class.rcconf_local())
-      File.readlines(self.class.rcconf_local()).map {|l|
-        l.chomp!
-      }
-    else
-      []
-    end
-  end
-
-  # @api private
-  def write_rc_contents(file, text)
-    Puppet::Util.replace_file(file, 0644) do |f|
-      f.write(text)
-    end
-  end
-
-  # @api private
-  def set_content_flags(content,flags)
-    unless content.is_a? Array
-      debug "content must be an array at flags"
-      return ""
-    else
-      content.reject! {|l| l.nil? }
-    end
-
-    if flags.nil? or flags.size == 0
-      if in_base?
-        append = resource[:name] + '_flags=""'
-      end
-    else
-      append = resource[:name] + '_flags="' + flags + '"'
-    end
-
-    if content.find {|l| l =~ /#{resource[:name]}_flags/ }.nil?
-      content << append
-    else
-      content.map {|l| l.gsub!(/^#{resource[:name]}_flags="(.*)?"(.*)?$/, append) }
-    end
-    content
-  end
-
-  # @api private
-  def remove_content_flags(content)
-    content.reject {|l| l =~ /#{resource[:name]}_flags/ }
-  end
-
-  # return an array of the currently enabled pkg_scripts
-  # @api private
-  def pkg_scripts
-    current = load_rcconf_local_array()
-    if scripts = current.find{|l| l =~ /^pkg_scripts/ }
-      if match = scripts.match(/^pkg_scripts="(.*)?"(.*)?$/)
-        match[1].split(' ')
-      else
-        []
-      end
-    else
-      []
-    end
-  end
-
-  # return the array with the current resource added
-  # @api private
-  def pkg_scripts_append
-    [pkg_scripts(), resource[:name]].flatten.uniq
-  end
-
-  # return the array without the current resource
-  # @api private
-  def pkg_scripts_remove
-    pkg_scripts().reject {|s| s == resource[:name] }
-  end
-
-  # Modify the content array to contain the requsted pkg_scripts line and retun
-  # the resulting array
-  # @api private
-  def set_content_scripts(content,scripts)
-    unless content.is_a? Array
-      debug "content must be an array at scripts"
-      return ""
-    else
-      content.reject! {|l| l.nil? }
-    end
-
-    scripts_line = 'pkg_scripts="' + scripts.join(' ') + '"'
-
-    if content.find {|l| l =~ /^pkg_scripts/ }.nil?
-      content << scripts_line
-    else
-      # Replace the found pkg_scripts line with our own
-      content.each_with_index {|l,i|
-        if l =~ /^pkg_scripts/
-          content[i] = scripts_line
-        end
-      }
-    end
-    content
-  end
-
-  # Determine if the rc script is included in base
-  # @api private
-  def in_base?
-    script = File.readlines(self.class.rcconf).find {|s| s =~ /^#{rcvar_name}/ }
-    !script.nil?
-  end
-
-  # @api private
-  def default_disabled?
-    line = File.readlines(self.class.rcconf).find {|l| l =~ /#{rcvar_name}/ }
-    self.class.parse_rc_line(line) == 'NO'
-  end
-
   def enabled?
-    if in_base?
-      if (@property_hash[:flags].nil? or @property_hash[:flags] == 'NO')
-        :false
-      else
-        :true
-      end
+    output = Puppet::Util::Execution.execute([command(:rcctl), "get", @resource[:name], "status"],
+                     :failonfail => false, :combine => false, :squelch => false)
+    if output.exitstatus == 0
+      self.debug("Is enabled")
+      return :true
     else
-      if (pkg_scripts().include?(@property_hash[:name]))
-        :true
-      else
-        :false
-      end
+      self.debug("Is disabled")
+      return :false
     end
   end
 
   def enable
-    self.debug("Enabling #{self.name}")
+    self.debug("Enabling")
+    rcctl(:enable, @resource[:name])
+    if @resource[:flags]
+      rcctl(:set, @resource[:name], :flags, @resource[:flags])
+    end
   end
 
-  # We should also check for default state
   def disable
-    self.debug("Disabling #{self.name}")
+    self.debug("Disabling")
+    rcctl(:disable, @resource[:name])
   end
 
-  def flags
-    @property_hash[:flags]
+  def running?
+    output = execute([command(:rcctl), "check", @resource[:name]],
+                     :failonfail => false, :combine => false, :squelch => false).chomp
+    return true if output.match(/\(ok\)/)
   end
 
-  def flags=(value)
-    @property_hash[:flags] = value
+  # Uses the wrapper to prevent failure when the service is not running;
+  # rcctl(8) return non-zero in that case.
+  def flags
+    output = execute([command(:rcctl), "get", @resource[:name], "flags"],
+                     :failonfail => false, :combine => false, :squelch => false).chomp
+    self.debug("Flags are: \"#{output}\"")
+    output
   end
 
-  def flush
-    debug "Flusing resource for #{self.name}"
-
-    # Here we load the contents of the rc.conf.local file into the contents
-    # variable, modify it if needed, and then compare that to the original.  If
-    # they are different, we write it out.
-
-    original = load_rcconf_local_array()
-    content = original
-
-    debug @property_hash.inspect
-
-    if resource[:enable] == :true
-      #set_flags(resource[:flags])
-      content = set_content_flags(content, resource[:flags])
-
-      # We need only add append the resource name to the pkg_scripts if the
-      # package is not found in the base system.
-
-      if not in_base?
-        content = set_content_scripts(content,pkg_scripts_append())
-      end
-    elsif resource[:enable] == :false
-
-      # By virtue of being excluded from the base system, all packages are
-      # disabled by default and need not be set in the rc.conf.local at all.
-
-      if not in_base?
-        content = remove_content_flags(content)
-        content = set_content_scripts(content,pkg_scripts_remove())
-      else
-        if default_disabled?
-          content = remove_content_flags(content)
-        else
-          content = set_content_flags(content, "NO")
-        end
-      end
-    end
-
-    # Make sure to append a newline to the end of the file
-    unless content[-1] == ""
-      content << ""
-    end
-    output = content.join("\n")
-
-    # Write the contents only if necessary, and only once
-    write_rc_contents(self.class.rcconf_local(), output)
+  def flags=(value)
+    self.debug("Changing flags from #{flags} to #{value}")
+    rcctl(:set, @resource[:name], :flags, value)
+    # If the service is already running, force a restart as the flags have been changed.
+    rcctl(:restart, @resource[:name]) if running?
   end
 end
