ptomulik/puppet-portsxutil

View on GitHub
lib/puppet/util/ptomulik/packagex/portsx/options.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'puppet/util/ptomulik/packagex'
require 'fileutils'

module Puppet::Util::PTomulik::Packagex::Portsx
  # Maintain FreeBSD ports options  (normally settable with `make search`).
  #
  # This is just a hash with simple validation and few additional methods for
  # loading, parsing and saving ports options to a file.
  #
  # ### Options as a validating hash
  #
  # The hash accepts Strings or Symbols as keys. When entering the hash, keys
  # are validated against a regexp accepting only well-formed identifiers
  # (`/^[a-zA-Z_]\w*$/`). The values must be either `true`/`false` boolean
  # values, `:on`/`:off` symbols or `'on'`/`'off'` strings. If validation
  # fails an exception is raised. The class uses
  # [vash](https://github.com/ptomulik/puppet-vash) module to enforce
  # validation and munging, and it is vash who defines the exceptions being
  # raised.
  #
  # Keys and values are internally munged, such that all input keys get
  # converted to symbols and input values to booleans. For example:
  #
  #     require 'puppet/util/ptomulik/packagex/portsx/options'
  #     opts = Puppet::Util::PTomulik::Packagex::Portsx::Options.new
  #     opts['FOO'] = 'off'
  #     puts opts.inspect
  #
  # would print `{:FOO=>false}`.
  #
  # ### Loading and saving options
  #
  # Options can be loaded from FreeBSD port options files or extracted from a
  # string. They may be further converted to a string or saved to a file. The
  # class provides following methods for that:
  # 
  # - {load} - load options from files,
  # - {parse} - extract options from a string,
  # - {#generate} - store options to a string,
  # - {#save} - save options to a file.
  #
  class Options

    # Module from ptomulik/vash puppet plugin
    require 'puppet/util/ptomulik/vash/contained'
    include Puppet::Util::PTomulik::Vash::Contained

    # Is x valid as option name?
    #
    # @param x an input value to be checked
    # @return [Boolean] `true` if `x` is a valid option name, `false` if not.
    def self.option_name?(x)
      x = x.to_s if x.is_a?(Symbol)
      x.is_a?(String) and x =~ /^[a-zA-Z_]\w*$/
    end

    # Is x valid as option value?
    #
    # @param x an input value to be checked
    # @return [Boolean] `true` if `x` is a valid option value, `false` if not
    #
    def self.option_value?(x)
      ['on', 'off', :on, :off, true, false].include?(x)
    end

    # Convert valid option names to symbols
    #
    # @param name [String|Symbol] input name to be munged
    # @return [Symbol] the `name` converted to Symbol
    #
    def self.munge_option_name(name)
      # note, on 1.8 Symbol has no :intern method
      name.is_a?(String) ? name.intern : name
    end

    # Convert valid option values (strings, symbols) to boolean values
    #
    # @param value [String|Symbol|Boolean] input value to be munged,
    # @return [Boolean] `true` if the `value` is `true`, `:on`, or `on`;
    #   otherwise `false` 
    #
    def self.munge_option_value(value)
      case value
      when 'on', :on, true; true
      else; false
      end
    end

    # --
    # Overriden methods from Vash::Contained
    # ++
    
    # Required by Vash to have key validation in place.
    def vash_valid_key?(x);     self.class.option_name?(x);          end
    # Required by Vash to have value validation in place.
    def vash_valid_value?(x);   self.class.option_value?(x);         end
    # Required by Vash to have key munging in place.
    def vash_munge_key(key);    self.class.munge_option_name(key);   end
    # Required by Vash to have value munging in place.
    def vash_munge_value(val);  self.class.munge_option_value(val);  end
    # Used by Vash as a key name.
    def vash_key_name(*args);   'option name';                       end
    # Used by Vash as to generate exceptions for invalid option values.
    def vash_value_exception(val,*args)
      name = vash_value_name(val,*args)
      msg  = "invalid value #{val.inspect}"
      msg += " at position  #{args[0].inspect}" unless args[0].nil?
      msg += " for option #{args[1].to_s}" unless args.length < 2
      [Puppet::Util::PTomulik::Vash::InvalidValueError, msg]
    end


    # Parse string for options.
    #
    # @param string [String] a content of options file to be scanned for
    #   options,
    # @return [Puppet::Util::PTomulik::Packagex::Portsx::Options] new instance
    #   of Options.
    def self.parse(string)
      opt_re = /^\s*OPTIONS_FILE_((?:UN)?SET)\s*\+=(\w+)\s*$/
      Options[ string.scan(opt_re).map{|pair| [pair[1], pair[0]=='SET']} ]
    end

    # Read options from options files. Missing files from __files__ list are
    # ignored by default.
    #
    # @param files [String|Array] file name (or array of file names) to be
    #   scanned for ports options, the files get loaded in order specified in
    #   __files__ array; options found in later files overwrite the earlier
    #   options,
    # @param params [Hash] additional parameters to alter method behavior
    # @option params :all [Boolean] load options from all files listed in
    #   __files__ (don't skip missing files), if a file is missing the method
    #   will fail with an exception instead of silently ignoring missing files,
    def self.load(files,params={})
      files = [files] unless files.is_a?(Array)
      # concatenate all files in order ...
      contents = []
      files.each do|file|
        next if (not File.exists?(file)) and not params[:all]
        msg = "Reading port options from '#{file}'"
        respond_to?(:debug) ? debug(msg) : Puppet.debug(msg)
        contents << File.read(file)
      end
      parse(contents.join("\n"))
    end

    self::PKG_ARGS_MAX = 60

    # Query pkgng for package options. 
    #
    # This method executes 
    #
    #     pkg query "#{key} %Ok %Ov" ...
    #
    # to extract package options for (a list of) installed package(s). See
    # pkg-query(8) for query formats used by `pkg query`.
    #
    # @param key [String] determines what will be used as keys in the returned
    #   hash; example values are `'%n'` - return *pkgnames* in keys, `'%o'`
    #   return *pkgorigins* in keys, 
    # @param packages [Array] list of packages to be queried; if not given,
    #   query all the installed packages,
    # @param params [Hash] additional parameters to alter method's behavior,
    # @option params :execpipe [Method] handle to a method which provides
    #   `execpipe` functionality, should have same interface as
    #   `Puppet::Util::Execution#execpipe`,
    # @option params :pkg [String] absolute path to the `pkg` command,
    # @return [Hash] a hash in form `{'package'=>{'OPTION'=>value,...}, ... }`,
    #   what is put in keys (`'package'` in the above example) depends on the
    #   __key__ argument,
    #
    def self.query_pkgng(key,packages=nil,params={})
      options = {}
      if packages
        packages.each_slice(self::PKG_ARGS_MAX) do |slice|
          query_pkgng_1(key,slice,params) { |hash| options.merge!(hash) }
        end
      else
        query_pkgng_1(key,[],params) {|hash| options.merge!(hash) }
      end
      options
    end

    # @api private
    def self.query_pkgng_1(key,slice,params)
      pkg = params[:pkg] || 'pkg'
      cmd = [pkg, 'query', "'#{key} %Ok %Ov'"] + slice
      execpipe = params[:execpipe] || Puppet::Util::Execution.method(:execpipe)
      options = {}
      execpipe.call(cmd) do |pipe|
        pipe.each_line do |line|
          origin, option, value = line.strip.split
          options[origin] ||= new
          options[origin][option] = value
        end
      end
      yield options
    end
    private_class_method :query_pkgng_1

    # Write to a string all the options in form suitable to be saved as an
    # options file. This is symmetric to what {parse} does.
    #
    # @param params [Hash] hash of parameters to alter method's behavior
    # @option params :pkgname [String] package name to which the options apply,
    #   by convention it should be a *pkgname* of the given package.
    # @return [String] the generated content as string.
    #
    def generate(params)
      content  = "# This file is auto-generated by puppet\n"
      if params[:pkgname]
        content += "# Options for #{params[:pkgname]}\n"
        content += "_OPTIONS_READ=#{params[:pkgname]}\n"
      end
      keys.sort.each do |k|
        v = self[k]
        content += "OPTIONS_FILE_#{v ? '':'UN'}SET+=#{k}\n"
      end
      content
    end

    # Save package options to options file.
    #
    # @param [String] file path to options' file.
    # @param [Hash] params additional parameters to function
    #
    # @option params :pkgname [String] package name to which the options apply,
    #   by convention it should be a *pkgname* of the given package,
    # @option params :mkdir_p [Boolean] create directories recursively if they
    #   don't exist; if `false`, only last level subdirectory is allowed to be
    #   created.
    # @note by default we do not allow to create directories recursivelly;
    #       we assume, that '/var/db/ports' already exists and user saves
    #       its options to '/var/db/ports/my_port/options';
    #
    def save(file,params={})
      dir = File.dirname(file)
      if not File.exists?(dir)
        msg = "Creating directory #{dir}"
        respond_to?(:debug) ? debug(msg) : Puppet.debug(msg)
        params[:mkdir_p] ?  FileUtils.mkdir_p(dir) : Dir.mkdir(dir)
      end
      msg = params[:pkgname] ?
        "Saving options for '#{params[:pkgname]}' port to file '#{file}'" :
        "Saving port options to file '#{file}'"
      respond_to?(:debug) ? debug(msg) : Puppet.debug(msg)
      File.write(file,generate(params))
    end

  end
end