Vendor the external cookbooks
Knife-Zero doesn't include Berkshelf support, so vendoring everything in the repo is convenient again
This commit is contained in:
73
cookbooks/hostsfile/CHANGELOG.md
Normal file
73
cookbooks/hostsfile/CHANGELOG.md
Normal file
@@ -0,0 +1,73 @@
|
||||
hostsfile Cookbook CHANGELOG
|
||||
=======================
|
||||
This file is used to list changes made in each version of the hostsfile cookbook.
|
||||
|
||||
|
||||
v2.4.5 (2014-06-24)
|
||||
-------------------
|
||||
- Fix notifications and why-run mode
|
||||
|
||||
|
||||
v2.4.4 (2014-02-25)
|
||||
-------------------
|
||||
- Bump Berkshelf version
|
||||
- Remove scope pieces from IPv6 addresses
|
||||
|
||||
|
||||
v2.4.3 (2014-02-01)
|
||||
-------------------
|
||||
|
||||
- Package custom ChefSpec matchers
|
||||
- Update testing harness
|
||||
- Avoid using `Chef::Application.fatal!`
|
||||
- Use Chef::Resource::File for atomic updates
|
||||
|
||||
|
||||
v2.4.2
|
||||
------
|
||||
- Fix Travis CI integration
|
||||
- Remove newline characters
|
||||
- Allow specifying a custom hostsfile path
|
||||
|
||||
|
||||
v2.4.1
|
||||
------
|
||||
- Force a new upload to the community site
|
||||
|
||||
|
||||
v2.4.0
|
||||
------
|
||||
- Convert everything to Ruby 1.9 syntax because I'm tired of people removing trailing commas despite the **massive** warning in the README: ([#29](https://github.com/customink-webops/hostsfile/issues/29), [#30](https://github.com/customink-webops/hostsfile/issues/30), [#32](https://github.com/customink-webops/hostsfile/issues/32), [#33](https://github.com/customink-webops/hostsfile/issues/33), [#34](https://github.com/customink-webops/hostsfile/issues/34), [#35](https://github.com/customink-webops/hostsfile/issues/35), [#36](https://github.com/customink-webops/hostsfile/issues/36), [#38](https://github.com/customink-webops/hostsfile/issues/38), [#39](https://github.com/customink-webops/hostsfile/issues/39))
|
||||
- Update to the latest and greatest testing gems and practices
|
||||
- Remove strainer in favor of a purer solution
|
||||
- Update `.gitignore` to ignore additional files
|
||||
- Add more platforms to the `.kitchen.yml`
|
||||
- Use `converge_by` and support whyruny mode
|
||||
|
||||
v2.0.0
|
||||
------
|
||||
- Completely manage the hostsfile, ensuring no duplicate entries
|
||||
|
||||
v1.0.2
|
||||
------
|
||||
- Support Windows (thanks @igantt-daptiv)
|
||||
- Specs + Travis support
|
||||
- Throw fatal error if hostsfile does not exist (@jkerzner)
|
||||
- Write priorities in hostsfile so they are read on subsequent Chef runs
|
||||
|
||||
v0.2.0
|
||||
------
|
||||
- Updated README to require Ruby 1.9
|
||||
- Allow hypens in hostnames
|
||||
- Ensure newline at end of file
|
||||
- Allow priority ordering in hostsfile
|
||||
|
||||
v0.1.1
|
||||
------
|
||||
- Fixed issue #1
|
||||
- Better unique object filtering
|
||||
- Better handing of aliases
|
||||
|
||||
v0.1.0
|
||||
------
|
||||
- Initial release
|
||||
234
cookbooks/hostsfile/README.md
Normal file
234
cookbooks/hostsfile/README.md
Normal file
@@ -0,0 +1,234 @@
|
||||
hostsfile LWRP
|
||||
==============
|
||||
[](https://travis-ci.org/customink-webops/hostsfile)
|
||||
|
||||
`hostsfile` provides an LWRP for managing your `/etc/hosts` (or Windows equivalent) file using Chef.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Chef 11 or higher
|
||||
- **Ruby 1.9.3 or higher**
|
||||
|
||||
**Please stop opening Pull Requests to restore Ruby 1.8 support!** Any of the `1.x.y` series of this cookbook will work with Chef 10 and Ruby 1.8. You can use Opscode's [Omnibus installer](http://www.opscode.com/blog/2012/06/29/omnibus-chef-packaging/) to install Ruby 1.9+ and Seth Chisamore's [Vagrant Omnibus plugin](https://github.com/schisamo/vagrant-omnibus) to get Ruby 1.9+ on your Vagrant box.
|
||||
|
||||
|
||||
Attributes
|
||||
----------
|
||||
<table>
|
||||
<tr>
|
||||
<th>Attribute</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ip_address</td>
|
||||
<td>(name attribute) the IP address for the entry</td>
|
||||
<td><tt>1.2.3.4</tt></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>hostname</td>
|
||||
<td>(required) the hostname associated with the entry</td>
|
||||
<td><tt>example.com</tt></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>unique</td>
|
||||
<td>remove any existing entries that have the same <tt>hostname</tt></td>
|
||||
<td><tt>true</tt></td>
|
||||
<td><tt>false</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>aliases</td>
|
||||
<td>array of aliases for the entry</td>
|
||||
<td><tt>['www.example.com']</tt></td>
|
||||
<td><tt>[]</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>comment</td>
|
||||
<td>a comment to append to the end of the entry</td>
|
||||
<td><tt>'interal DNS server'</tt></td>
|
||||
<td><tt>nil</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>priority</td>
|
||||
<td>the relative position of this entry</td>
|
||||
<td><tt>20</tt></td>
|
||||
<td>(varies, see **Priorities** section)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
Actions
|
||||
-------
|
||||
**Please note**: In `v0.1.2`, specifying a hostname or alias that existed in another automatically removed that hostname from the other entry before. In `v2.1.0`, the `unique` option was added to give the user case-by-case control of this behavior. For example, given an `/etc/hosts` file that contains:
|
||||
|
||||
1.2.3.4 example.com www.example.com
|
||||
|
||||
when the Chef recipe below is converged:
|
||||
|
||||
```ruby
|
||||
hostsfile_entry '2.3.4.5' do
|
||||
hostname 'www.example.com'
|
||||
unique true
|
||||
end
|
||||
```
|
||||
|
||||
then the `/etc/hosts` file will look like this:
|
||||
|
||||
1.2.3.4 example.com
|
||||
2.3.4.5 www.example.com
|
||||
|
||||
Not specifying the `unique` parameter will result in duplicate hostsfile entries.
|
||||
|
||||
#### `create`
|
||||
Creates a new hosts file entry. If an entry already exists, it will be overwritten by this one.
|
||||
|
||||
```ruby
|
||||
hostsfile_entry '1.2.3.4' do
|
||||
hostname 'example.com'
|
||||
action :create
|
||||
end
|
||||
```
|
||||
|
||||
This will create an entry like this:
|
||||
|
||||
1.2.3.4 example.com
|
||||
|
||||
#### `create_if_missing`
|
||||
Create a new hosts file entry, only if one does not already exist for the given IP address. If one exists, this does nothing.
|
||||
|
||||
```ruby
|
||||
hostsfile_entry '1.2.3.4' do
|
||||
hostname 'example.com'
|
||||
action :create_if_missing
|
||||
end
|
||||
```
|
||||
|
||||
#### `append`
|
||||
Append a hostname or alias to an existing record. If the given IP address doesn't already exist in the hostsfile, this method behaves the same as create. Otherwise, it will append the additional hostname and aliases to the existing entry.
|
||||
|
||||
1.2.3.4 example.com www.example.com # Created by Chef
|
||||
|
||||
```ruby
|
||||
hostsfile_entry '1.2.3.4' do
|
||||
hostname 'www2.example.com'
|
||||
aliases ['foo.com', 'foobar.com']
|
||||
comment 'Append by Recipe X'
|
||||
action :append
|
||||
end
|
||||
```
|
||||
|
||||
would yield:
|
||||
|
||||
1.2.3.4 example.com www.example.com www2.example.com foo.com foobar.com # Created by Chef, Appended by Recipe X
|
||||
|
||||
|
||||
#### `update`
|
||||
Updates the given hosts file entry. Does nothing if the entry does not exist.
|
||||
|
||||
```ruby
|
||||
hostsfile_entry '1.2.3.4' do
|
||||
hostname 'example.com'
|
||||
comment 'Update by Chef'
|
||||
action :update
|
||||
end
|
||||
```
|
||||
|
||||
This will create an entry like this:
|
||||
|
||||
1.2.3.4 example # Updated by Chef
|
||||
|
||||
#### `remove`
|
||||
Removes an entry from the hosts file. Does nothing if the entry does not
|
||||
exist.
|
||||
|
||||
```ruby
|
||||
hostsfile_entry '1.2.3.4' do
|
||||
action :remove
|
||||
end
|
||||
```
|
||||
|
||||
This will remove the entry for `1.2.3.4`.
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
If you're using [Berkshelf](http://berkshelf.com/), just add `hostsfile` to your `Berksfile`:
|
||||
|
||||
```ruby
|
||||
cookbook 'hostsfile'
|
||||
```
|
||||
|
||||
Otherwise, install the cookbook from the community site:
|
||||
|
||||
knife cookbook site install hostsfile
|
||||
|
||||
Have any other cookbooks *depend* on hostsfile by editing editing the `metadata.rb` for your cookbook.
|
||||
|
||||
```ruby
|
||||
# metadata.rb
|
||||
depends 'hostsfile'
|
||||
```
|
||||
|
||||
Note that you can specify a custom path to your hosts file in the `['hostsfile']['path']` node attribute. Otherwise, it defaults to sensible paths depending on your OS.
|
||||
|
||||
### Testing
|
||||
If you are using [ChefSpec](https://github.com/sethvargo/chefspec) to unit test a cookbook that implements the `hostsfile_entry` LWRP, this cookbook packages customer matchers that you can use in your unit tests:
|
||||
|
||||
- `append_hostsfile_entry`
|
||||
- `create_hostsfile_entry`
|
||||
- `create_hostsfile_entry_if_missing`
|
||||
- `remove_hostsfile_entry`
|
||||
- `update_hostsfile_entry`
|
||||
|
||||
For example:
|
||||
|
||||
```ruby
|
||||
it 'creates a hostsfile entry for the DNS server' do
|
||||
expect(chef_run).to create_hostsfile_entry('1.2.3.4')
|
||||
.with_hostname('dns.example.com')
|
||||
end
|
||||
```
|
||||
|
||||
Priority
|
||||
--------
|
||||
Priority is a relatively new addition to the cookbook. It gives you the ability to (somewhat) specify the relative order of entries. By default, the priority is calculated for you as follows:
|
||||
|
||||
1. Local, loopback
|
||||
2. IPV4
|
||||
3. IPV6
|
||||
|
||||
However, you can override it using the `priority` option.
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
1. Fork the project
|
||||
2. Create a feature branch corresponding to you change
|
||||
3. Commit and test thoroughly
|
||||
4. Create a Pull Request on github
|
||||
|
||||
|
||||
License & Authors
|
||||
-----------------
|
||||
- Author:: Seth Vargo (sethvargo@gmail.com)
|
||||
|
||||
```text
|
||||
Copyright 2012-2013, Seth Vargo
|
||||
Copyright 2012, CustomInk, LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
22
cookbooks/hostsfile/attributes/default.rb
Normal file
22
cookbooks/hostsfile/attributes/default.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# Author:: Seth Vargo <sethvargo@gmail.com>
|
||||
# Cookbook:: hostsfile
|
||||
# Attribute:: default
|
||||
#
|
||||
# Copyright 2012-2013, Seth Vargo
|
||||
# Copyright 2012, CustomInk, LCC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
default['hostsfile']['path'] = nil
|
||||
183
cookbooks/hostsfile/libraries/entry.rb
Normal file
183
cookbooks/hostsfile/libraries/entry.rb
Normal file
@@ -0,0 +1,183 @@
|
||||
#
|
||||
# Author:: Seth Vargo <sethvargo@gmail.com>
|
||||
# Cookbook:: hostsfile
|
||||
# Library:: entry
|
||||
#
|
||||
# Copyright 2012-2013, Seth Vargo
|
||||
# Copyright 2012, CustomInk, LCC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
require 'ipaddr'
|
||||
|
||||
# An object representation of a single line in a hostsfile.
|
||||
#
|
||||
# @author Seth Vargo <sethvargo@gmail.com>
|
||||
class Entry
|
||||
class << self
|
||||
# Creates a new Hostsfile::Entry object by parsing a text line. The
|
||||
# `line` attribute will be in the following format:
|
||||
#
|
||||
# 1.2.3.4 hostname [alias[, alias[, alias]]] [# comment [@priority]]
|
||||
#
|
||||
# @param [String] line
|
||||
# the line to parse
|
||||
# @return [Entry]
|
||||
# a new entry object
|
||||
def parse(line)
|
||||
entry, comment = extract_comment(line)
|
||||
comment, priority = extract_priority(comment)
|
||||
entries = extract_entries(entry)
|
||||
|
||||
# Return nil if the line is empty
|
||||
return nil if entries.nil? || entries.empty?
|
||||
|
||||
# If /etc/hosts has a broken content we throw a descriptive exception
|
||||
if entries[0].nil?
|
||||
raise ArgumentError, "/etc/hosts has a line without IP address: #{line}"
|
||||
end
|
||||
if entries[1].nil?
|
||||
raise ArgumentError, "/etc/hosts has a line without hostname: #{line}"
|
||||
end
|
||||
|
||||
return self.new(
|
||||
ip_address: entries[0],
|
||||
hostname: entries[1],
|
||||
aliases: entries[2..-1],
|
||||
comment: comment,
|
||||
priority: priority,
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_comment(line)
|
||||
return nil if presence(line).nil?
|
||||
line.split('#', 2).collect { |part| presence(part) }
|
||||
end
|
||||
|
||||
def extract_priority(comment)
|
||||
return nil if comment.nil?
|
||||
|
||||
if comment.include?('@')
|
||||
comment.split('@', 2).collect { |part| presence(part) }
|
||||
else
|
||||
[comment, nil]
|
||||
end
|
||||
end
|
||||
|
||||
def extract_entries(entry)
|
||||
return nil if entry.nil?
|
||||
entry.split(/\s+/).collect { |entry| presence(entry) }.compact
|
||||
end
|
||||
|
||||
def presence(string)
|
||||
return nil if string.nil?
|
||||
return nil if string.strip.empty?
|
||||
string.strip
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
attr_accessor :ip_address, :hostname, :aliases, :comment, :priority
|
||||
|
||||
# Creates a new entry from the given options.
|
||||
#
|
||||
# @param [Hash] options
|
||||
# a list of options to create the entry with
|
||||
# @option options [String] :ip_address
|
||||
# the IP Address for this entry
|
||||
# @option options [String] :hostname
|
||||
# the hostname for this entry
|
||||
# @option options [String, Array<String>] :aliases
|
||||
# a alias or array of aliases for this entry
|
||||
# @option options[String] :comment
|
||||
# an optional comment for this entry
|
||||
# @option options [Fixnum] :priority
|
||||
# the relative priority of this entry (compared to others)
|
||||
#
|
||||
# @raise [ArgumentError]
|
||||
# if neither :ip_address nor :hostname are supplied
|
||||
def initialize(options = {})
|
||||
if options[:ip_address].nil? || options[:hostname].nil?
|
||||
raise ArgumentError, ':ip_address and :hostname are both required options'
|
||||
end
|
||||
|
||||
@ip_address = IPAddr.new(remove_ip_scope(options[:ip_address]))
|
||||
@hostname = options[:hostname]
|
||||
@aliases = [options[:aliases]].flatten.compact
|
||||
@comment = options[:comment]
|
||||
@priority = options[:priority] || calculated_priority
|
||||
end
|
||||
|
||||
# Set a the new priority for an entry.
|
||||
#
|
||||
# @param [Fixnum] new_priority
|
||||
# the new priority to set
|
||||
def priority=(new_priority)
|
||||
@calculated_priority = false
|
||||
@priority = new_priority
|
||||
end
|
||||
|
||||
# The line representation of this entry.
|
||||
#
|
||||
# @return [String]
|
||||
# the string representation of this entry
|
||||
def to_line
|
||||
hosts = [hostname, aliases].flatten.join(' ')
|
||||
|
||||
comments = "# #{comment.to_s}".strip
|
||||
comments << " @#{priority}" unless priority.nil? || @calculated_priority
|
||||
comments = comments.strip
|
||||
comments = nil if comments == '#'
|
||||
|
||||
[ip_address, hosts, comments].compact.join("\t").strip
|
||||
end
|
||||
|
||||
# Returns true if priority is calculated
|
||||
#
|
||||
# @return [Boolean]
|
||||
# true if priority is calculated and false otherwise
|
||||
def calculated_priority?
|
||||
@calculated_priority
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Calculates the relative priority of this entry.
|
||||
#
|
||||
# @return [Fixnum]
|
||||
# the relative priority of this item
|
||||
def calculated_priority
|
||||
@calculated_priority = true
|
||||
|
||||
return 81 if ip_address == IPAddr.new('127.0.0.1')
|
||||
return 80 if IPAddr.new('127.0.0.0/8').include?(ip_address) # local
|
||||
return 60 if ip_address.ipv4? # ipv4
|
||||
return 20 if ip_address.ipv6? # ipv6
|
||||
return 00
|
||||
end
|
||||
|
||||
# Removes the scopes pieces of the address, because reasons.
|
||||
#
|
||||
# @see https://bugs.ruby-lang.org/issues/8464
|
||||
# @see https://github.com/customink-webops/hostsfile/issues/51
|
||||
#
|
||||
# @return [String, nil]
|
||||
#
|
||||
def remove_ip_scope(address)
|
||||
return nil if address.nil?
|
||||
address.to_s.sub(/%.*/, '')
|
||||
end
|
||||
end
|
||||
293
cookbooks/hostsfile/libraries/manipulator.rb
Normal file
293
cookbooks/hostsfile/libraries/manipulator.rb
Normal file
@@ -0,0 +1,293 @@
|
||||
#
|
||||
# Author:: Seth Vargo <sethvargo@gmail.com>
|
||||
# Cookbook:: hostsfile
|
||||
# Library:: manipulator
|
||||
#
|
||||
# Copyright 2012-2013, Seth Vargo
|
||||
# Copyright 2012, CustomInk, LCC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
require 'chef/application'
|
||||
require 'digest/sha2'
|
||||
|
||||
class Manipulator
|
||||
attr_reader :node
|
||||
attr_reader :entries
|
||||
|
||||
# Create a new Manipulator object (aka an /etc/hosts manipulator). If a
|
||||
# hostsfile is not found, an exception is raised.
|
||||
#
|
||||
# @param [Chef::node] node
|
||||
# the current Chef node
|
||||
# @return [Manipulator]
|
||||
# a class designed to manipulate the node's /etc/hosts file
|
||||
def initialize(node)
|
||||
@node = node
|
||||
|
||||
# Fail if no hostsfile is found
|
||||
unless ::File.exists?(hostsfile_path)
|
||||
raise RuntimeError, "No hostsfile exists at `#{hostsfile_path}'!"
|
||||
end
|
||||
|
||||
@entries = []
|
||||
collect_and_flatten(::File.readlines(hostsfile_path))
|
||||
end
|
||||
|
||||
# Return a list of all IP Addresses for this hostsfile.
|
||||
#
|
||||
# @return [Array<IPAddr>]
|
||||
# the list of IP Addresses
|
||||
def ip_addresses
|
||||
@entries.collect do |entry|
|
||||
entry.ip_address
|
||||
end.compact || []
|
||||
end
|
||||
|
||||
# Add a new record to the hostsfile.
|
||||
#
|
||||
# @param [Hash] options
|
||||
# a list of options to create the entry with
|
||||
# @option options [String] :ip_address
|
||||
# the IP Address for this entry
|
||||
# @option options [String] :hostname
|
||||
# the hostname for this entry
|
||||
# @option options [String, Array<String>] :aliases
|
||||
# a alias or array of aliases for this entry
|
||||
# @option options[String] :comment
|
||||
# an optional comment for this entry
|
||||
# @option options [Fixnum] :priority
|
||||
# the relative priority of this entry (compared to others)
|
||||
def add(options = {})
|
||||
entry = Entry.new(
|
||||
ip_address: options[:ip_address],
|
||||
hostname: options[:hostname],
|
||||
aliases: options[:aliases],
|
||||
comment: options[:comment],
|
||||
priority: options[:priority],
|
||||
)
|
||||
|
||||
@entries << entry
|
||||
remove_existing_hostnames(entry) if options[:unique]
|
||||
end
|
||||
|
||||
# Update an existing entry. This method will do nothing if the entry
|
||||
# does not exist.
|
||||
#
|
||||
# @param (see #add)
|
||||
def update(options = {})
|
||||
if entry = find_entry_by_ip_address(options[:ip_address])
|
||||
entry.hostname = options[:hostname]
|
||||
entry.aliases = options[:aliases]
|
||||
entry.comment = options[:comment]
|
||||
entry.priority = options[:priority]
|
||||
|
||||
remove_existing_hostnames(entry) if options[:unique]
|
||||
end
|
||||
end
|
||||
|
||||
# Append content to an existing entry. This method will add a new entry
|
||||
# if one does not already exist.
|
||||
#
|
||||
# @param (see #add)
|
||||
def append(options = {})
|
||||
if entry = find_entry_by_ip_address(options[:ip_address])
|
||||
hosts = normalize(entry.hostname, entry.aliases, options[:hostname], options[:aliases])
|
||||
entry.hostname = hosts.shift
|
||||
entry.aliases = hosts
|
||||
|
||||
unless entry.comment && options[:comment] && entry.comment.include?(options[:comment])
|
||||
entry.comment = normalize(entry.comment, options[:comment]).join(', ')
|
||||
end
|
||||
|
||||
remove_existing_hostnames(entry) if options[:unique]
|
||||
else
|
||||
add(options)
|
||||
end
|
||||
end
|
||||
|
||||
# Remove an entry by it's IP Address
|
||||
#
|
||||
# @param [String] ip_address
|
||||
# the IP Address of the entry to remove
|
||||
def remove(ip_address)
|
||||
if entry = find_entry_by_ip_address(ip_address)
|
||||
@entries.delete(entry)
|
||||
end
|
||||
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
|
||||
file = Chef::Resource::File.new(hostsfile_path, node.run_context)
|
||||
file.content(new_content)
|
||||
file.run_action(:create)
|
||||
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]
|
||||
# the corresponding entry object, or nil if it does not exist
|
||||
def find_entry_by_ip_address(ip_address)
|
||||
@entries.find do |entry|
|
||||
!entry.ip_address.nil? && entry.ip_address == ip_address
|
||||
end
|
||||
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
|
||||
#
|
||||
# @return [Boolean]
|
||||
def contains?(resource)
|
||||
!!find_entry_by_ip_address(resource.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 node attributes
|
||||
def hostsfile_path
|
||||
return @hostsfile_path if @hostsfile_path
|
||||
@hostsfile_path = node['hostsfile']['path'] || case node['platform_family']
|
||||
when 'windows'
|
||||
"#{node['kernel']['os_info']['system_directory']}\\drivers\\etc\\hosts"
|
||||
else
|
||||
'/etc/hosts'
|
||||
end
|
||||
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 Chef, using the hostsfile cookbook.'
|
||||
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 Chef runs!'
|
||||
lines << '#'
|
||||
lines << ''
|
||||
end
|
||||
|
||||
# The content that will be written to the hostfile
|
||||
#
|
||||
# @return [String]
|
||||
# the full contents of the hostfile to be written
|
||||
def new_content
|
||||
entries = hostsfile_header
|
||||
entries += unique_entries.map(&:to_line)
|
||||
entries << ''
|
||||
entries.join("\n")
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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 = 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
|
||||
|
||||
@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
|
||||
else
|
||||
entry
|
||||
end
|
||||
end.compact
|
||||
|
||||
@entries << entry
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
21
cookbooks/hostsfile/libraries/matchers.rb
Normal file
21
cookbooks/hostsfile/libraries/matchers.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
if defined?(ChefSpec)
|
||||
def append_hostsfile_entry(resource_name)
|
||||
ChefSpec::Matchers::ResourceMatcher.new(:hostsfile_entry, :append, resource_name)
|
||||
end
|
||||
|
||||
def create_hostsfile_entry(resource_name)
|
||||
ChefSpec::Matchers::ResourceMatcher.new(:hostsfile_entry, :create, resource_name)
|
||||
end
|
||||
|
||||
def create_hostsfile_entry_if_missing(resource_name)
|
||||
ChefSpec::Matchers::ResourceMatcher.new(:hostsfile_entry, :create_if_missing, resource_name)
|
||||
end
|
||||
|
||||
def remove_hostsfile_entry(resource_name)
|
||||
ChefSpec::Matchers::ResourceMatcher.new(:hostsfile_entry, :remove, resource_name)
|
||||
end
|
||||
|
||||
def update_hostsfile_entry(resource_name)
|
||||
ChefSpec::Matchers::ResourceMatcher.new(:hostsfile_entry, :update, resource_name)
|
||||
end
|
||||
end
|
||||
29
cookbooks/hostsfile/metadata.json
Normal file
29
cookbooks/hostsfile/metadata.json
Normal file
File diff suppressed because one or more lines are too long
7
cookbooks/hostsfile/metadata.rb
Normal file
7
cookbooks/hostsfile/metadata.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
name 'hostsfile'
|
||||
maintainer 'Seth Vargo'
|
||||
maintainer_email 'sethvargo@gmail.com'
|
||||
license 'Apache 2.0'
|
||||
description 'Provides an LWRP for managing the /etc/hosts file'
|
||||
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
|
||||
version '2.4.5'
|
||||
138
cookbooks/hostsfile/providers/entry.rb
Normal file
138
cookbooks/hostsfile/providers/entry.rb
Normal file
@@ -0,0 +1,138 @@
|
||||
#
|
||||
# Author:: Seth Vargo <sethvargo@gmail.com>
|
||||
# Cookbook:: hostsfile
|
||||
# Provider:: entry
|
||||
#
|
||||
# Copyright 2012-2013, Seth Vargo
|
||||
# Copyright 2012, CustomInk, LCC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# Support whyrun
|
||||
def whyrun_supported?
|
||||
true
|
||||
end
|
||||
|
||||
# Creates a new hosts file entry. If an entry already exists, it will be
|
||||
# overwritten by this one.
|
||||
action :create do
|
||||
if hostsfile.contains?(new_resource)
|
||||
Chef::Log.debug "#{new_resource} already exists - overwriting."
|
||||
end
|
||||
|
||||
hostsfile.add(
|
||||
ip_address: new_resource.ip_address,
|
||||
hostname: new_resource.hostname,
|
||||
aliases: new_resource.aliases,
|
||||
comment: new_resource.comment,
|
||||
priority: new_resource.priority,
|
||||
unique: new_resource.unique,
|
||||
)
|
||||
|
||||
if hostsfile.content_changed?
|
||||
converge_by("Create #{new_resource}") { hostsfile.save }
|
||||
else
|
||||
Chef::Log.info "#{new_resource} content already matches - nothing to do."
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new hosts file entry, only if one does not already exist for
|
||||
# the given IP address. If one exists, this does nothing.
|
||||
action :create_if_missing do
|
||||
if hostsfile.contains?(new_resource)
|
||||
Chef::Log.info "#{new_resource} already exists - skipping create_if_missing."
|
||||
else
|
||||
converge_by("Create #{new_resource} if missing") do
|
||||
hostsfile.add(
|
||||
ip_address: new_resource.ip_address,
|
||||
hostname: new_resource.hostname,
|
||||
aliases: new_resource.aliases,
|
||||
comment: new_resource.comment,
|
||||
priority: new_resource.priority,
|
||||
unique: new_resource.unique,
|
||||
)
|
||||
hostsfile.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Appends the given data to an existing entry. If an entry does not exist,
|
||||
# one will be created
|
||||
action :append do
|
||||
unless hostsfile.contains?(new_resource)
|
||||
Chef::Log.info "#{new_resource} does not exist - creating instead."
|
||||
end
|
||||
|
||||
hostsfile.append(
|
||||
ip_address: new_resource.ip_address,
|
||||
hostname: new_resource.hostname,
|
||||
aliases: new_resource.aliases,
|
||||
comment: new_resource.comment,
|
||||
priority: new_resource.priority,
|
||||
unique: new_resource.unique,
|
||||
)
|
||||
|
||||
if hostsfile.content_changed?
|
||||
converge_by("Append #{new_resource}") { hostsfile.save }
|
||||
else
|
||||
Chef::Log.info "#{new_resource} content already matches - nothing to do."
|
||||
end
|
||||
end
|
||||
|
||||
# Updates the given hosts file entry. Does nothing if the entry does not
|
||||
# exist.
|
||||
action :update do
|
||||
if hostsfile.contains?(new_resource)
|
||||
|
||||
hostsfile.update(
|
||||
ip_address: new_resource.ip_address,
|
||||
hostname: new_resource.hostname,
|
||||
aliases: new_resource.aliases,
|
||||
comment: new_resource.comment,
|
||||
priority: new_resource.priority,
|
||||
unique: new_resource.unique,
|
||||
)
|
||||
|
||||
if hostsfile.content_changed?
|
||||
converge_by("Update #{new_resource}") { hostsfile.save }
|
||||
else
|
||||
Chef::Log.info "#{new_resource} content already matches - nothing to do."
|
||||
end
|
||||
else
|
||||
Chef::Log.info "#{new_resource} does not exist - skipping update."
|
||||
end
|
||||
end
|
||||
|
||||
# Removes an entry from the hosts file. Does nothing if the entry does
|
||||
# not exist.
|
||||
action :remove do
|
||||
if hostsfile.contains?(new_resource)
|
||||
converge_by("Remove #{new_resource}") do
|
||||
hostsfile.remove(new_resource.ip_address)
|
||||
hostsfile.save
|
||||
end
|
||||
else
|
||||
Chef::Log.info "#{new_resource} does not exist - skipping remove."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# The hostsfile object
|
||||
#
|
||||
# @return [Manipulator]
|
||||
# the manipulator for this hostsfile
|
||||
def hostsfile
|
||||
@hostsfile ||= Manipulator.new(node)
|
||||
end
|
||||
36
cookbooks/hostsfile/resources/entry.rb
Normal file
36
cookbooks/hostsfile/resources/entry.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
#
|
||||
# Author:: Seth Vargo <sethvargo@gmail.com>
|
||||
# Cookbook:: hostsfile
|
||||
# Resource:: entry
|
||||
#
|
||||
# Copyright 2012-2013, Seth Vargo
|
||||
# Copyright 2012, CustomInk, LCC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# List of all actions supported by the provider
|
||||
actions :create, :create_if_missing, :append, :update, :remove
|
||||
|
||||
# Make create the default action
|
||||
default_action :create
|
||||
|
||||
# Required attributes
|
||||
attribute :ip_address, kind_of: String, name_attribute: true
|
||||
attribute :hostname, kind_of: String
|
||||
|
||||
# Optional attributes
|
||||
attribute :aliases, kind_of: Array
|
||||
attribute :comment, kind_of: String
|
||||
attribute :priority, kind_of: Fixnum
|
||||
attribute :unique, kind_of: [TrueClass, FalseClass]
|
||||
Reference in New Issue
Block a user