From 665b96c0445c2a87f1cc69fab056f6a12ad3f10d Mon Sep 17 00:00:00 2001 From: Shawn Dahlen Date: Wed, 27 Mar 2013 15:29:21 -0400 Subject: [PATCH] Proof-of-concept support for managing /etc/hosts. Hooks into the up and destroy commands to manage changes to the /etc/hosts file on active machines. --- .gitignore | 4 ++ Gemfile | 7 ++ LICENSE.txt | 22 +++++++ README.md | 42 ++++++++++++ bin/build.sh | 5 ++ bin/test.sh | 12 ++++ lib/vagrant-hostmanager.rb | 12 ++++ .../action/update_hosts_file.rb | 65 +++++++++++++++++++ lib/vagrant-hostmanager/errors.rb | 6 ++ lib/vagrant-hostmanager/helpers/translator.rb | 20 ++++++ lib/vagrant-hostmanager/plugin.rb | 64 ++++++++++++++++++ lib/vagrant-hostmanager/version.rb | 5 ++ locales/en.yml | 5 ++ test/Vagrantfile | 17 +++++ vagrant-hostmanager.gemspec | 17 +++++ 15 files changed, 303 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100755 bin/build.sh create mode 100755 bin/test.sh create mode 100644 lib/vagrant-hostmanager.rb create mode 100644 lib/vagrant-hostmanager/action/update_hosts_file.rb create mode 100644 lib/vagrant-hostmanager/errors.rb create mode 100644 lib/vagrant-hostmanager/helpers/translator.rb create mode 100644 lib/vagrant-hostmanager/plugin.rb create mode 100644 lib/vagrant-hostmanager/version.rb create mode 100644 locales/en.yml create mode 100644 test/Vagrantfile create mode 100644 vagrant-hostmanager.gemspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c916bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.gem +pkg +Gemfile.lock +test/.vagrant diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..5346732 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gemspec + +group :development do + gem 'vagrant', github: 'mitchellh/vagrant', tag: 'v1.1.2' +end diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..326b88b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2013 Shawn Dahlen + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7093713 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +Vagrant Host Manager +==================== +`vagrant-hostmanager` is a Vagrant 1.1+ plugin that manages the `/etc/hosts` +file on guest machines. Its goal is to enable resolution of multi-machine +environments deployed with a cloud provider where IP addresses are not known +in advance. + +Status +------ +The current implementation is a proof-of-concept supporting the larger +objective of using Vagrant as a cloud management interface for development +and production environments. + +The plugin has been tested with Vagrant 1.1.4. + +Installation +------------ +Install the plugin following the typical Vagrant 1.1 procedure: + + vagrant plugin install vagrant-hostmanager + +Usage +----- +The plugin hooks into the `vagrant up` and `vagrant destroy` commands +automatically updating the `/etc/hosts` file on each active machine that +is using the same provider. + +A machine's IP address is defined by either the static IP for a private +network configuration or by the SSH host configuration. + +A machine's host name is defined by `config.vm.hostname`. If this is not +set, it falls back to the symbol defining the machine in the Vagrantfile. + +Contribute +---------- +Contributions are welcome. + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/bin/build.sh b/bin/build.sh new file mode 100755 index 0000000..ff9a0cf --- /dev/null +++ b/bin/build.sh @@ -0,0 +1,5 @@ +rm *.gem +gem uninstall -a vagrant-hostmanager +gem build *.gemspec +gem install *.gem +vagrant plugin install vagrant-hostmanager diff --git a/bin/test.sh b/bin/test.sh new file mode 100755 index 0000000..d7552f8 --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,12 @@ +cd test +vagrant up +echo "[server1] /etc/hosts file:" +vagrant ssh server1 -c 'cat /etc/hosts' +echo "[server2] /etc/hosts file:" +vagrant ssh server2 -c 'cat /etc/hosts' +vagrant destroy server1 -f +echo "[server2] /etc/hosts file:" +vagrant ssh server2 -c 'cat /etc/hosts' +vagrant destroy server2 -f +cd .. + diff --git a/lib/vagrant-hostmanager.rb b/lib/vagrant-hostmanager.rb new file mode 100644 index 0000000..4ef7a9f --- /dev/null +++ b/lib/vagrant-hostmanager.rb @@ -0,0 +1,12 @@ +require 'vagrant' +require 'vagrant-hostmanager/plugin' +require 'vagrant-hostmanager/version' +require 'vagrant-hostmanager/errors' + +module VagrantPlugins + module HostManager + def self.source_root + @source_root ||= Pathname.new(File.expand_path('../../', __FILE__)) + end + end +end diff --git a/lib/vagrant-hostmanager/action/update_hosts_file.rb b/lib/vagrant-hostmanager/action/update_hosts_file.rb new file mode 100644 index 0000000..593f0ed --- /dev/null +++ b/lib/vagrant-hostmanager/action/update_hosts_file.rb @@ -0,0 +1,65 @@ +module VagrantPlugins + module HostManager + module Action + class UpdateHostsFile + def initialize(app, env) + @app, @env = app, env + @translator = Helpers::Translator.new('action.update_hosts_file') + @logger = + Log4r::Logger.new('vagrant_hostmanager::action::update') + end + + def call(env) + global_env = env[:machine].env + current_provider = env[:machine].provider_name + + # build a list of host entries based on active machines that + # are using the same provider as the current one + matching_machines = [] + entries = {} + entries['127.0.0.1'] = 'localhost' + global_env.active_machines.each do |name, provider| + if provider == current_provider + machine = global_env.machine(name, provider) + host = machine.config.vm.hostname || name + entries[get_ip_address(machine)] = host + matching_machines << machine + end + end + + # generate hosts file + path = env[:tmp_path].join('hosts') + File.open(path, 'w') do |file| + entries.each_pair do |ip, host| + @logger.info "Adding /etc/hosts entry: #{ip} #{host}" + file << "#{ip}\t#{host}\n" + end + end + + # copy the hosts file to each matching machine + # TODO append hostname to loopback address + matching_machines.each do |machine| + env[:ui].info @translator.t('update', { :name => machine.name }) + machine.communicate.upload(path, '/tmp/hosts') + machine.communicate.sudo("mv /tmp/hosts /etc/hosts") + end + + @app.call(env) + end + + protected + + def get_ip_address(machine) + ip = nil + machine.config.vm.networks.each do |network| + key, options = network[0], network[1] + ip = options[:ip] if key == :private_network + next if ip + end + + ip || machine.ssh_info[:host] + end + end + end + end +end diff --git a/lib/vagrant-hostmanager/errors.rb b/lib/vagrant-hostmanager/errors.rb new file mode 100644 index 0000000..36695db --- /dev/null +++ b/lib/vagrant-hostmanager/errors.rb @@ -0,0 +1,6 @@ +module VagrantPlugins + module HostManager + module Errors + end + end +end diff --git a/lib/vagrant-hostmanager/helpers/translator.rb b/lib/vagrant-hostmanager/helpers/translator.rb new file mode 100644 index 0000000..5a2e75d --- /dev/null +++ b/lib/vagrant-hostmanager/helpers/translator.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module HostManager + module Helpers + class Translator + def self.plugin_namespace=(val) + @@plugin_namespace = val + end + + def initialize(namespace) + @namespace = namespace + end + + def t(keys, opts = {}) + value = I18n.t("#{@@plugin_namespace}.#{@namespace}.#{keys}", opts) + opts[:progress] == false ? value : value + "..." + end + end + end + end +end diff --git a/lib/vagrant-hostmanager/plugin.rb b/lib/vagrant-hostmanager/plugin.rb new file mode 100644 index 0000000..ea09565 --- /dev/null +++ b/lib/vagrant-hostmanager/plugin.rb @@ -0,0 +1,64 @@ +require 'vagrant-hostmanager/helpers/translator' +require 'vagrant-hostmanager/action/update_hosts_file' + +module VagrantPlugins + module HostManager + class Plugin < Vagrant.plugin('2') + name 'HostManager' + description <<-DESC + This plugin manages the /etc/hosts file for guest machines. A entry is + created for each active machine using the hostname attribute. + DESC + + action_hook(:hostmanager_up, :machine_action_up) do |hook| + setup_i18n + setup_logging + + # TODO use hook.append when defect is fixed within vagrant + hook.after(ProviderVirtualBox::Action::Boot, Action::UpdateHostsFile) + end + + action_hook(:hostmanger_destroy, :machine_action_destroy) do |hook| + setup_i18n + setup_logging + + # TODO use hook.append when defect is fixed within vagrant + hook.after( + ProviderVirtualBox::Action::DestroyUnusedNetworkInterfaces, + Action::UpdateHostsFile) + end + + def self.setup_i18n + I18n.load_path << File.expand_path('locales/en.yml', HostManager.source_root) + I18n.reload! + Helpers::Translator.plugin_namespace = 'vagrant_hostmanager' + end + + def self.setup_logging + level = nil + begin + level = Log4r.const_get(ENV["VAGRANT_LOG"].upcase) + rescue NameError + # This means that the logging constant wasn't found, + # which is fine. We just keep `level` as `nil`. But + # we tell the user. + level = nil + end + + # Some constants, such as "true" resolve to booleans, so the + # above error checking doesn't catch it. This will check to make + # sure that the log level is an integer, as Log4r requires. + level = nil if !level.is_a?(Integer) + + # Set the logging level on all "vagrant" namespaced + # logs as long as we have a valid level. + if level + logger = Log4r::Logger.new("vagrant_hostmanager") + logger.outputters = Log4r::Outputter.stderr + logger.level = level + logger = nil + end + end + end + end +end diff --git a/lib/vagrant-hostmanager/version.rb b/lib/vagrant-hostmanager/version.rb new file mode 100644 index 0000000..293a107 --- /dev/null +++ b/lib/vagrant-hostmanager/version.rb @@ -0,0 +1,5 @@ +module VagrantPlugins + module HostManager + VERSION = '0.0.1' + end +end diff --git a/locales/en.yml b/locales/en.yml new file mode 100644 index 0000000..99c6e6c --- /dev/null +++ b/locales/en.yml @@ -0,0 +1,5 @@ +en: + vagrant_hostmanager: + action: + update_hosts_file: + update: "Updating /etc/hosts file on %{name}" diff --git a/test/Vagrantfile b/test/Vagrantfile new file mode 100644 index 0000000..85a2322 --- /dev/null +++ b/test/Vagrantfile @@ -0,0 +1,17 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure('2') do |config| + config.vm.box = 'precise64-chef11.2' + config.vm.box_url = 'https://opscode-vm.s3.amazonaws.com/vagrant/opscode_ubuntu-12.04_chef-11.2.0.box' + + config.vm.define :server1 do |server| + server.vm.hostname = 'fry' + server.vm.network :private_network, :ip => '10.0.5.2' + end + + config.vm.define :server2 do |server| + server.vm.hostname = 'bender' + server.vm.network :private_network, :ip => '10.0.5.3' + end +end diff --git a/vagrant-hostmanager.gemspec b/vagrant-hostmanager.gemspec new file mode 100644 index 0000000..40131ef --- /dev/null +++ b/vagrant-hostmanager.gemspec @@ -0,0 +1,17 @@ +# -*- encoding: utf-8 -*- +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'vagrant-hostmanager/version' + +Gem::Specification.new do |gem| + gem.name = 'vagrant-hostmanager' + gem.version = VagrantPlugins::HostManager::VERSION + gem.authors = ['Shawn Dahlen'] + gem.email = ['shawn@dahlen.me'] + gem.description = %q{A Vagrant plugin that manages the /etc/hosts file within a multi-machine environment} + gem.summary = gem.description + + gem.files = `git ls-files`.split($/) + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.require_paths = ['lib'] +end