lib/hostsfile/manipulator.rb in hostsfile-0.0.1 vs lib/hostsfile/manipulator.rb in hostsfile-0.0.2

- old
+ new

@@ -1,28 +1,33 @@ # Copyright 2013-14, Tnarik Innael +# +# Heavily based on: # Copyright 2012-2013, Seth Vargo (from customink-webops/hostsfile/libraries/manipulator.rb) # Copyright 2012, CustomInk, LCC # require 'digest/sha2' module Hostsfile class Manipulator attr_reader :entries - # Create a new Manipulator object (aka an /etc/hosts manipulator). If a - # hostsfile is not found, a Exception is risen, causing - # the process to terminate on the node and the converge will fail. + # hostsfile is not found, a Exception is risen. + # Parameters are optional (see #hostsfile_path) # - # @param [Chef::node] node - # the current Chef node + # @param [String] path + # the file path for the host file + # @param [String] family + # the OS family ('windows' or anything else for POSIX support) + # @param [String] system_directory + # System directory for the 'windows' family (like C:\\Windows\\system32) # @return [Manipulator] - # a class designed to manipulate the node's /etc/hosts file + # a class designed to manipulate the /etc/hosts file def initialize(path = nil, family = nil, system_directory = nil) # Fail if no hostsfile is found - unless ::File.exists?(hostsfile_path) + unless ::File.exists?(hostsfile_path(path, family, system_directory)) raise "No hostsfile exists at '#{hostsfile_path}'!" end @entries = [] collect_and_flatten(::File.readlines(hostsfile_path)) @@ -111,34 +116,25 @@ end # Save the new hostsfile to the target machine. This method will only write the # hostsfile if the current version has changed. In other words, it is convergent. def save - entries = [] - entries << '#' - entries << '# This file is managed by Chef, using the hostsfile cookbook.' - entries << '# Editing this file by hand is highly discouraged!' - entries << '#' - entries << '# Comments containing an @ sign should not be modified or else' - entries << '# hostsfile will be unable to guarantee relative priority in' - entries << '# future Chef runs!' - entries << '#' - entries << '' - entries += unique_entries.map(&:to_line) - entries << '' - - contents = entries.join("\n") - contents_sha = Digest::SHA512.hexdigest(contents) - # Only write out the file if the contents have changed... - if contents_sha != current_sha - ::File.open(hostsfile_path, 'w') do |f| - f.write(contents) - end - end + ::File.open(hostsfile_path, 'w') do |f| + f.write(new_content) + end if content_changed? end + # Determine if the content of the hostfile has changed by comparing sha + # values of existing file and new content + # + # @return [Boolean] + def content_changed? + new_sha = Digest::SHA512.hexdigest(new_content) + new_sha != current_sha + end + # Find an entry by the given IP Address. # # @param [String] ip_address # the IP Address of the entry to find # @return [Entry, nil] @@ -150,110 +146,146 @@ end # Determine if the current hostsfile contains the given resource. This # is really just a proxy to {find_resource_by_ip_address} / # - # @param [Chef::Resource] resource - # + # @param [String] ip_address + # the IP Address of the entry to check # @return [Boolean] - def contains?(resource) - !!find_entry_by_ip_address(resource.ip_address) + def contains?(ip_address) + !!find_entry_by_ip_address(ip_address) end private - # The path to the current hostsfile. - # - # @return [String] - # the full path to the hostsfile, depending on the operating system - # can also be overriden in the attributes - def hostsfile_path (path = nil, family = nil, system_directory = nil) - return @hostsfile_path if @hostsfile_path - @hostsfile_path = path || case family - when 'windows' - "#{system_directory}\\drivers\\etc\\hosts" - else - '/etc/hosts' - end - end + # The path to the current hostsfile. + # If not path is provided, a default is guessed based on 'family' and 'system_directory' + # If path is provided, it takes priority + # + # @param [String] path + # the file path for the host file + # @param [String] family + # the OS family ('windows' or anything else for POSIX support) + # @param [String] system_directory + # System directory for the 'windows' family (default C:\\Windows\\system32) + # @return [String] + # the full path to the hostsfile, depending on the operating system + def hostsfile_path (path = nil, family = nil, system_directory = nil) + return @hostsfile_path if @hostsfile_path + @hostsfile_path = path || case family + when 'windows' + system_directory ||= File.join('C:','Windows','system32') + File.join("#{system_directory}", 'drivers', 'etc', 'hosts') + else + '/etc/hosts' + end + end - # The current sha of the system hostsfile. - # - # @return [String] - # the sha of the current hostsfile - def current_sha - @current_sha ||= Digest::SHA512.hexdigest(File.read(hostsfile_path)) - end + # The header of the new hostsfile + # + # @return [Array] + # an array of header comments + def hostsfile_header + lines = [] + lines << '#' + lines << '# This file is managed by the hostsfile gem.' + lines << '# Editing this file by hand is highly discouraged!' + lines << '#' + lines << '# Comments containing an @ sign should not be modified or else' + lines << '# hostsfile will be unable to guarantee relative priority in' + lines << '# future runs!' + lines << '#' + lines << '' + end - # Normalize the given list of elements into a single array with no nil - # values and no duplicate values. - # - # @param [Object] things - # - # @return [Array] - # a normalized array of things - def normalize(*things) - things.flatten.compact.uniq - end + # The content that will be written to the hostfile + # + # @return [String] + # the full contents of the hostfile to be written + def new_content + lines = hostsfile_header + lines += unique_entries.map(&:to_line) + lines << '' + lines.join("\n") + end - # This is a crazy way of ensuring unique objects in an array using a Hash. - # - # @return [Array] - # the sorted list of entires that are unique - def unique_entries - entries = Hash[*@entries.map { |entry| [entry.ip_address, entry] }.flatten].values - entries.sort_by { |e| [-e.priority.to_i, e.hostname.to_s] } - end + # The current sha of the system hostsfile. + # + # @return [String] + # the sha of the current hostsfile + def current_sha + @current_sha ||= Digest::SHA512.hexdigest(File.read(hostsfile_path)) + end - # Takes /etc/hosts file contents and builds a flattened entries - # array so that each IP address has only one line and multiple hostnames - # are flattened into a list of aliases. - # - # @param [Array] contents - # Array of lines from /etc/hosts file - def collect_and_flatten(contents) - contents.each do |line| - entry = ::Hostsfile::Entry.parse(line) - next if entry.nil? + # Normalize the given list of elements into a single array with no nil + # values and no duplicate values. + # + # @param [Object] things + # + # @return [Array] + # a normalized array of things + def normalize(*things) + things.flatten.compact.uniq + end - append( - ip_address: entry.ip_address, - hostname: entry.hostname, - aliases: entry.aliases, - comment: entry.comment, - priority: !entry.calculated_priority? && entry.priority, - ) - end + # This is a crazy way of ensuring unique objects in an array using a Hash. + # + # @return [Array] + # the sorted list of entires that are unique + def unique_entries + entries = Hash[*@entries.map { |entry| [entry.ip_address, entry] }.flatten].values + entries.sort_by { |e| [-e.priority.to_i, e.hostname.to_s] } + end + + # Takes /etc/hosts file contents and builds a flattened entries + # array so that each IP address has only one line and multiple hostnames + # are flattened into a list of aliases. + # + # @param [Array] contents + # Array of lines from /etc/hosts file + def collect_and_flatten(contents) + contents.each do |line| + entry = ::Hostsfile::Entry.parse(line) + next if entry.nil? + + append( + ip_address: entry.ip_address, + hostname: entry.hostname, + aliases: entry.aliases, + comment: entry.comment, + priority: !entry.calculated_priority? && entry.priority, + ) end + end - # Removes duplicate hostnames in other files ensuring they are unique - # - # @param [Entry] entry - # the entry to keep the hostname and aliases from - # - # @return [nil] - def remove_existing_hostnames(entry) - @entries.delete(entry) - changed_hostnames = [entry.hostname, entry.aliases].flatten.uniq + # Removes duplicate hostnames in other files ensuring they are unique + # + # @param [Entry] entry + # the entry to keep the hostname and aliases from + # + # @return [nil] + def remove_existing_hostnames(entry) + @entries.delete(entry) + changed_hostnames = [entry.hostname, entry.aliases].flatten.uniq - @entries = @entries.collect do |entry| - entry.hostname = nil if changed_hostnames.include?(entry.hostname) - entry.aliases = entry.aliases - changed_hostnames + @entries = @entries.collect do |entry| + entry.hostname = nil if changed_hostnames.include?(entry.hostname) + entry.aliases = entry.aliases - changed_hostnames - if entry.hostname.nil? - if entry.aliases.empty? - nil - else - entry.hostname = entry.aliases.shift - entry - end + if entry.hostname.nil? + if entry.aliases.empty? + nil else + entry.hostname = entry.aliases.shift entry end - end.compact + else + entry + end + end.compact - @entries << entry + @entries << entry - nil - end + nil + end end end \ No newline at end of file