#!/usr/bin/ruby require_relative 'objects/rcm_file' require_relative 'objects/rcm_package' require_relative 'objects/rcm_service' require_relative 'managers/rcm_package_manager' require_relative 'managers/rcm_file_manager' require_relative 'managers/rcm_service_manager' require_relative 'rcm_utils' require 'yaml' require 'logger' module RCM PACKAGES = 'packages'.freeze FILES = 'files'.freeze SERVICES = 'services'.freeze VALID_LOG_LEVELS = %w(fatal error info debug).freeze RCM_ENV = ENV.fetch('ENVIRONMENT', '') @wanted = { PACKAGES => {}, FILES => {}, SERVICES => {} } @got = { PACKAGES => {}, FILES => {}, SERVICES => {} } @logger = ::Logger.new(STDOUT) @pkg_mgr = ::RCM::Apt.new(@logger) @file_mgr = ::RCM::FileManager.new(@logger) @svc_mgr = ::RCM::ServiceManager.new(@logger) def self.configure_logger # Can be exposed as settings but was expanding the scope as this # object will need to be passed to all other objects @logger.level = ::Logger::DEBUG @logger.progname = 'rcm' end def self.parse_packages(markup) @logger.info("Environment = #{RCM_ENV}") @pkg_mgr.update unless RCM_ENV == 'dev' markup.each do |d| # ensure package version exists, if mentioned. version = d.fetch('version', '') desired_state = d.fetch('desired_state', ::RCM::Package::INSTALLED) valid_package_states = [::RCM::Package::INSTALLED, ::RCM::Package::REMOVED] if (valid_package_states & [desired_state]).empty? raise "Unknown desired_state: #{desired_state}. Valid states: #{valid_package_states.join(', ')}" end unless version.empty? available_versions = @pkg_mgr.get_versions(d['name']) if (available_versions & [version]).empty? raise "\n\n#{d['name']} = #{version} is not available.\nAvailable versions: #{available_versions.join(', ')}" end end p = RCM::Package.new(d['name'], version, desired_state) @wanted[PACKAGES][d['name']] = p end end def self.parse_files(markup) valid_file_states = [::RCM::File::PRESENT, ::RCM::File::ABSENT] markup.each do |d| raise "\n\nFile state can only be one of #{valid_file_states.join (', ')}" if (valid_file_states | [d['desired_state']]).empty? raise "\n\n'#{d['local_file']}' is not readable.\n\n" if d['desired_state'] == ::RCM::File::PRESENT && !::File.readable?(d['local_file']) f = RCM::File.new(d['path'], d['owner'], d['group'], d['mode'], d['local_file'], d['desired_state']) @wanted[FILES][d['path']] = f end end def self.parse_services(markup) pkgs = @wanted[PACKAGES] files = @wanted[FILES] file_dependencies = {} package_dependencies = {} markup.each do |d| dependencies = d.fetch('dependencies', {}) dependencies.each do |values| values.each do |dep_type, dep| case dep_type when PACKAGES dep.each do |pkg_definition| pkg_name = pkg_definition['name'] raise "\n\nPlease ensure #{pkg_name} is managed by this program before adding it as a dependency.\n\n" unless pkgs.key?(pkg_name) p = pkgs[pkg_name] package_dependencies[pkg_name] = p end when FILES dep.each do |file_defnition| path = file_defnition['path'] raise "\n\nPlease ensure #{path} is managed by this program before adding it as a dependency.\n\n" unless files.key?(path) f = files[path] file_dependencies[path] = f end end end end s = RCM::Service.new(d['name'], file_dependencies, package_dependencies) @wanted[SERVICES][d['name']] = s end end def self.whachuwant # or parse_config # Consume config from YAML, and convert it to usable objects in @wanted. config_file = RCM_ENV.empty? ? 'config.yaml' : "config_#{RCM_ENV}.yaml" raise "#{config_file} not found." unless ::File.readable?(config_file) config = YAML.load_file(config_file) config.each do |yaml_objects| yaml_objects.each do |collection, resource_definition| case collection when PACKAGES parse_packages(resource_definition) when FILES parse_files(resource_definition) when SERVICES parse_services(resource_definition) end end end end def self.whachugot # or get_current_state @got[PACKAGES] = @pkg_mgr.get_current_state(@wanted[PACKAGES]) @got[FILES] = @file_mgr.get_current_state(@wanted[FILES]) # @got[SERVICES] = @svc_mgr.get_current_state(@wanted[SERVICES]) end def self.converge_packages @wanted[PACKAGES].each do |pkg_name, pkg_obj| current_pkg = @got[PACKAGES][pkg_name] if current_pkg == pkg_obj @logger.info("#{pkg_name} is in expected state.") next end @logger.debug('Missing package:' + pkg_name) if current_pkg.version != pkg_obj.version && current_pkg.state == ::RCM::Package::INSTALLED # Wrong version is installed. Remove current version and install the correct one. # Not the best way to go about it... but we are not implementing a legit solution. @logger.debug("#{current_pkg.name}=#{current_pkg.version} is installed. Uninstalling first.") @pkg_mgr.remove(pkg_obj) @pkg_mgr.install(pkg_obj) elsif current_pkg.state == ::RCM::Package::REMOVED @pkg_mgr.install(pkg_obj) end end end def self.converge_files @wanted[FILES].each do |path, file_obj| file_on_fs = @got[FILES][path] if file_on_fs.state == ::RCM::File::PRESENT && file_obj.state == ::RCM::File::ABSENT @logger.info("#{path} is present on disk when it should not be. Removing...") @file_mgr.remove(file_obj) next end if file_on_fs == file_obj @logger.info("#{path} is in expected state (#{file_on_fs.state}).") next end @logger.info("#{path} is not present on disk.") status = @file_mgr.copy(file_obj) @file_mgr.apply_attributes(file_obj) if status == 0 end end def self.restart_services_if_needed @wanted[SERVICES].values.each do |svc_obj| @svc_mgr.restart(svc_obj) if @svc_mgr.dependencies_changed?(svc_obj) end end def main configure_logger whachuwant whachugot converge_packages converge_files restart_services_if_needed end module_function :main end RCM.main