require 'active_support/core_ext/hash' require 'active_support/core_ext/string' require 'logger' require 'yaml' module ConfigCurator # Manages collections of units. # @example Load a list of units and install them # Collection.new(manifest_path: 'manifest.yml').install class Collection # Supported unit types. UNIT_TYPES = %i(unit component config_file symlink) # The possible attributes specific to each unit type. # This should not include generic attributes # such as {Unit#source} and {Unit#destination}. UNIT_ATTRIBUTES = { unit: %i(hosts packages), component: %i(hosts packages fmode dmode owner group), config_file: %i(hosts packages fmode owner group), symlink: %i(hosts packages) } attr_accessor :logger, :manifest, :units def initialize(manifest_path: nil, logger: nil) self.logger = logger unless logger.nil? load_manifest manifest_path unless manifest_path.nil? end # Logger instance to use. # @return [Logger] logger instance def logger @logger ||= Logger.new($stdout).tap do |log| log.progname = self.class.name end end # Loads the manifest from file. # @param file [Hash] the yaml file to load # @return [Hash] the loaded manifest def load_manifest(file) self.manifest = ActiveSupport::HashWithIndifferentAccess.new YAML.load_file(file) end # Unit objects defined by the manifest and organized by type. # @return [Hash] keys are pluralized unit types from {UNIT_TYPES} def units @units ||= {}.tap do |u| UNIT_TYPES.each do |type| k = type.to_s.pluralize.to_sym u[k] = [] next unless manifest next if manifest[k].nil? manifest[k].each { |v| u[k] << create_unit(type, attributes: v) } end end end # Installs all units from the manifest. # @return [Boolean, nil] if units were installed or nil if fails mid-install def install return false unless install? quiet: !(logger.level == Logger::DEBUG) UNIT_TYPES.each do |type| units[type.to_s.pluralize.to_sym].each do |unit| return nil unless install_unit(unit, type) end end true end # Checks all units in the manifest for any detectable install issues. # @param quiet [Boolean] suppress some {#logger} output # @return [Boolean] if units can be installed def install?(quiet: false) result = true UNIT_TYPES.each do |type| units[type.to_s.pluralize.to_sym].each do |unit| result = install_unit?(unit, type, quiet) ? result : false end end result end # Creates a new unit object for the collection. # @param type [Symbol] a unit type in {UNIT_TYPES} # @param attributes [Hash] attributes for the unit from {UNIT_ATTRIBUTES} # @return [Unit] the unit object of the appropriate subclass def create_unit(type, attributes: {}) "#{self.class.name.split('::').first}::#{type.to_s.camelize}".constantize .new(options: unit_options, logger: logger).tap do |unit| {src: :source, dst: :destination}.each do |k, v| unit.send "#{v}=".to_sym, attributes[k] unless attributes[k].nil? end UNIT_ATTRIBUTES[type].each do |v| unit.send "#{v}=".to_sym, defaults[v] unless defaults[v].nil? unit.send "#{v}=".to_sym, attributes[v] unless attributes[v].nil? end end end private # Formats a unit type for display. # @param type [Symbol] the unit type # @return [String] formatted unit type def type_name(type) type.to_s.humanize capitalize: false end # Hash of any defaults given in the manifest. # @return [Hash] the defaults def defaults return {} unless manifest manifest[:defaults].nil? ? {} : manifest[:defaults] end # Load basic unit options from the manifest. # @return [Hash] the options def unit_options options = {} return options unless manifest %i(root package_tool).each do |k| options[k] = manifest[k] unless manifest[k].nil? end options end # Installs a unit. # @param unit [Unit] the unit to install # @param type [Symbol] the unit type # @param quiet [Boolean] suppress some {#logger} output # @return [Boolean] if unit was installed def install_unit(unit, type, quiet = false) unit.install logger.info do "Installed #{type_name(type)}: #{unit.source} ⇨ #{unit.destination_path}" end unless quiet return true rescue Unit::InstallFailed => e logger.fatal { "Halting install! Install attempt failed for #{type_name(type)}: #{e}" } return false end # Checks if a unit can be installed. # @param unit [Unit] the unit to check # @param type [Symbol] the unit type # @param quiet [Boolean] suppress some {#logger} output # @return [Boolean] if unit can be installed def install_unit?(unit, type, quiet = false) unit.install? logger.info do "Testing install for #{type_name(type)}:" \ " #{unit.source} ⇨ #{unit.destination_path}" end unless quiet return true rescue Unit::InstallFailed => e logger.error { "Cannot install #{type_name(type)}: #{e}" } return false end end end