├── .gitignore ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── Makefile ├── README.md ├── Rakefile ├── lib ├── vagrant-dnsmasq.rb └── vagrant-dnsmasq │ ├── actions.rb │ ├── config.rb │ ├── includes │ ├── DnsmasqConf.class.rb │ ├── Domain.class.rb │ ├── Ip.class.rb │ ├── Resolver.class.rb │ └── helper.rb │ └── version.rb ├── test ├── Dnsmasq.class.spec.rb ├── Domain.class.spec.rb ├── Ip.class.spec.rb ├── Resolver.class.spec.rb ├── Vagrantfile └── helper.spec.rb └── vagrant-dnsmasq.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | # YARD artifacts 16 | .yardoc 17 | _yardoc 18 | doc/ 19 | 20 | .DS_Store 21 | .vagrant -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.3 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | script: "bundle exec rspec test/*.spec.rb" 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in vagrant-dnsmasq.gemspec 4 | 5 | group :development do 6 | gem 'vagrant', :git => 'git://github.com/mitchellh/vagrant.git', :tag => 'v1.2.2' 7 | end 8 | 9 | gemspec 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/mitchellh/vagrant.git 3 | revision: 7e400d00a3c5a0fdf2809c8b5001a035415a607b 4 | tag: v1.2.2 5 | specs: 6 | vagrant (1.2.2) 7 | childprocess (~> 0.3.7) 8 | erubis (~> 2.7.0) 9 | i18n (~> 0.6.0) 10 | json (>= 1.5.1, < 1.8.0) 11 | log4r (~> 1.1.9) 12 | net-scp (~> 1.1.0) 13 | net-ssh (~> 2.6.6) 14 | 15 | PATH 16 | remote: . 17 | specs: 18 | vagrant-dnsmasq (0.1.1) 19 | ffi (~> 1.9.5) 20 | 21 | GEM 22 | remote: https://rubygems.org/ 23 | specs: 24 | childprocess (0.3.9) 25 | ffi (~> 1.0, >= 1.0.11) 26 | diff-lcs (1.2.5) 27 | erubis (2.7.0) 28 | ffi (1.9.5) 29 | i18n (0.6.11) 30 | json (1.7.7) 31 | log4r (1.1.10) 32 | net-scp (1.1.2) 33 | net-ssh (>= 2.6.5) 34 | net-ssh (2.6.8) 35 | rake (10.3.2) 36 | rspec (3.1.0) 37 | rspec-core (~> 3.1.0) 38 | rspec-expectations (~> 3.1.0) 39 | rspec-mocks (~> 3.1.0) 40 | rspec-core (3.1.5) 41 | rspec-support (~> 3.1.0) 42 | rspec-expectations (3.1.2) 43 | diff-lcs (>= 1.2.0, < 2.0) 44 | rspec-support (~> 3.1.0) 45 | rspec-mocks (3.1.2) 46 | rspec-support (~> 3.1.0) 47 | rspec-support (3.1.1) 48 | 49 | PLATFORMS 50 | ruby 51 | 52 | DEPENDENCIES 53 | bundler (~> 1.3) 54 | rake 55 | rspec 56 | vagrant! 57 | vagrant-dnsmasq! 58 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Matthias Kadenbach 2 | https://github.com/mattes/vagrant-dnsmasq 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | refresh: 2 | rake build && vagrant plugin uninstall vagrant-dnsmasq && vagrant plugin install pkg/vagrant-dnsmasq-0.0.*.gem 3 | 4 | test-and-destroy: 5 | make refresh && make up-and-destroy 6 | 7 | test: 8 | make refresh && make up 9 | 10 | up: 11 | cd test && vagrant up 12 | 13 | debug-up: 14 | cd test && VAGRANT_LOG=INFO vagrant up 15 | 16 | destroy: 17 | cd test && vagrant destroy -f 18 | 19 | reload: 20 | cd test && vagrant reload 21 | 22 | up-and-destroy: 23 | make up; make destroy 24 | 25 | 26 | .PHONY: refresh up debug-up destroy reload up-and-destroy test-and-destroy test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vagrant-dnsmasq [![Build Status](https://travis-ci.org/mattes/vagrant-dnsmasq.png?branch=master)](https://travis-ci.org/mattes/vagrant-dnsmasq) 2 | 3 | A Dnsmasq Vagrant plugin that manages the dnsmasq.conf file and /etc/resolver directory on your host system. 4 | 5 | ## Prerequisites 6 | * [Vagrant](http://www.vagrantup.com) 7 | * [Dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) 8 | 9 | Easily install dnsmasq with [brew](http://mxcl.github.io/homebrew/) under Mac OS: 10 | ```brew install dnsmasq``` 11 | 12 | 13 | ## Installation 14 | ``` 15 | vagrant plugin install vagrant-dnsmasq 16 | ``` 17 | 18 | ## Usage 19 | in your Vagrantfile 20 | ```ruby 21 | # set domain ending (required) 22 | # adding this line enables dnsmasq handling 23 | config.dnsmasq.domain = '.dev' 24 | 25 | 26 | # optional configuration ... 27 | 28 | # this plugin runs 'hostname -I' on the guest machine to obtain 29 | # the guest ip address. you can overwrite this behaviour. 30 | # config.dnsmasq.ip = '192.168.59.100' 31 | 32 | # config.dnsmasq.ip = proc do |guest_machine| 33 | # guest_machine.communicate.sudo("command to obtain ip somehow") do |type, data| 34 | # # return something like '192.168.59.100' or ['192.168.59.100', '192.168.59.103'] 35 | # end 36 | # end 37 | 38 | # this will prompt you during 'vagrant up' to choose an IP 39 | # config.dnsmasq.ip = ['192.168.59.100', '192.168.59.103'] 40 | 41 | # overwrite default location for /etc/resolver directory 42 | # config.dnsmasq.resolver = '/etc/resolver' 43 | 44 | # 'vagrant destroy' does not delete /etc/resolver nameserver file, defaults to false 45 | # config.dnsmasq.keep_resolver_on_destroy = true 46 | 47 | # overwrite default location for /etc/dnsmasq.conf 48 | brew_prefix = `brew --prefix`.strip 49 | config.dnsmasq.dnsmasqconf = brew_prefix + '/etc/dnsmasq.conf' 50 | 51 | # command for reloading dnsmasq after config changes 52 | config.dnsmasq.reload_command = 'sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist; sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist' 53 | 54 | # disable dnsmasq handling 55 | # config.dnsmasq.disable = true 56 | ``` 57 | 58 | ## Uninstall 59 | ``` 60 | vagrant plugin uninstall vagrant-dnsmasq 61 | ``` 62 | 63 | Verify ```/etc/resolver``` and ```$(brew --prefix)/etc/dnsmasq.conf```. 64 | 65 | 66 | ## Alternatives 67 | 68 | __`/etc/resolver` approach__ 69 | * [vagrant-dns](https://github.com/BerlinVagrant/vagrant-dns) (using [rubydns](http://www.codeotaku.com/projects/rubydns/index.en)) 70 | 71 | __`/etc/hosts` approach__ 72 | * [vagrant-hostmaster](https://github.com/mosaicxm/vagrant-hostmaster) 73 | * [vagrant-hostmanager](https://github.com/smdahlen/vagrant-hostmanager) 74 | * [vagrant-hostsupdater](https://github.com/cogitatio/vagrant-hostsupdater) -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | Bundler::GemHelper.install_tasks 4 | 5 | task :default do 6 | system 'rake -T' 7 | end 8 | 9 | desc "run tests" 10 | task :test do 11 | system 'rspec test/*.spec.rb' 12 | end 13 | 14 | desc "tag this version and rake release" 15 | task :publish do 16 | lib = File.expand_path('../lib', __FILE__) 17 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 18 | require 'vagrant-dnsmasq/version' 19 | 20 | puts "VERSION: #{Vagrant::Dnsmasq::VERSION}" 21 | 22 | system "git add ." 23 | system "git commit -a" 24 | system "git push" 25 | 26 | system "git tag -a v#{Vagrant::Dnsmasq::VERSION}" 27 | system "git push origin v#{Vagrant::Dnsmasq::VERSION}" 28 | 29 | system 'rake release' 30 | end -------------------------------------------------------------------------------- /lib/vagrant-dnsmasq.rb: -------------------------------------------------------------------------------- 1 | module Vagrant 2 | module Dnsmasq 3 | class Plugin < Vagrant.plugin("2") 4 | name "vagrant-dnsmasq" 5 | description "A Dnsmasq Vagrant plugin that manages the dnsmasq.conf file and /etc/resolver directory on your host system." 6 | 7 | lib_path = Pathname.new(File.expand_path("../vagrant-dnsmasq", __FILE__)) 8 | require lib_path.join("actions") 9 | 10 | inc_path = Pathname.new(File.expand_path("../vagrant-dnsmasq/includes", __FILE__)) 11 | require inc_path.join("Domain.class.rb") 12 | require inc_path.join("Ip.class.rb") 13 | require inc_path.join("DnsmasqConf.class.rb") 14 | require inc_path.join("Resolver.class.rb") 15 | require inc_path.join("helper.rb") 16 | 17 | config "dnsmasq" do 18 | require lib_path.join("config") 19 | Config 20 | end 21 | 22 | action_hook(:dnsmasq, :machine_action_up) do |hook| 23 | hook.append(Vagrant::Action::Up) 24 | end 25 | 26 | action_hook(:dnsmasq, :machine_action_destroy) do |hook| 27 | hook.append(Vagrant::Action::Destroy) 28 | end 29 | 30 | end 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /lib/vagrant-dnsmasq/actions.rb: -------------------------------------------------------------------------------- 1 | module Vagrant 2 | module Action 3 | 4 | class Up 5 | def initialize(app, env) 6 | @app = app 7 | @machine = env[:machine] 8 | @ips = nil 9 | end 10 | 11 | def call(env) 12 | if @machine.config.dnsmasq.enabled? 13 | env[:ui].info "Dnsmasq handler actived" 14 | 15 | @ip = @machine.config.dnsmasq.ip 16 | 17 | # is a proc? 18 | if @ip.is_a? Proc 19 | ips = @ip.call(@machine) 20 | ips = [ips] unless ips.is_a? Array 21 | ips.map!{|ip| begin Ip.new(ip) rescue nil end}.compact! # dismiss invalid ips 22 | @ip = ips 23 | end 24 | 25 | if @ip.is_a?(Array) && @ip.count > 0 26 | # @ip is an array with domain instances ... 27 | 28 | if @ip.count > 1 29 | # prompt: choose ip 30 | ask = true 31 | while(ask) 32 | env[:ui].info "Dnsmasq handler asks: Which IP would you like to use?" 33 | i = 0 34 | @ip.each do |ip| 35 | i += 1 36 | env[:ui].info "(#{i}) #{ip.v4}" 37 | end 38 | env[:ui].info "Please type number [1-#{i}]: " 39 | answer = $stdin.gets.strip.to_i - 1 40 | use_ip = @ip.at(answer) 41 | ask = false unless use_ip.nil? 42 | end 43 | 44 | else 45 | use_ip = @ip[0] 46 | end 47 | 48 | # use ip to update dnsmasq.conf and /etc/resolver 49 | 50 | # update dnsmasq.conf 51 | dnsmasq = DnsmasqConf.new(@machine.config.dnsmasq.dnsmasqconf, @machine.config.dnsmasq.reload_command) 52 | if dnsmasq.includes?(@machine.config.dnsmasq.domain) 53 | dnsmasq.update(@machine.config.dnsmasq.domain, use_ip) 54 | else 55 | dnsmasq.insert(@machine.config.dnsmasq.domain, use_ip) 56 | end 57 | 58 | # update /etc/resolver 59 | resolver = Resolver.new(@machine.config.dnsmasq.resolver, true) # true for sudo 60 | resolver.insert(@machine.config.dnsmasq.domain, Ip.new('127.0.0.1')) 61 | 62 | env[:ui].success "Dnsmasq handler set IP '#{use_ip}' for domain '#{@machine.config.dnsmasq.domain.dotted}'" 63 | 64 | else 65 | env[:ui].warn "Dnsmasq handler was not able to determine an IP address" 66 | end 67 | 68 | @app.call(env) 69 | end 70 | end 71 | end 72 | 73 | 74 | class Destroy 75 | def initialize(app, env) 76 | @app = app 77 | @machine = env[:machine] 78 | end 79 | 80 | def call(env) 81 | if @machine.config.dnsmasq.enabled? 82 | 83 | # remove records from dnsmasq.conf and /etc/resolver 84 | 85 | # update dnsmasq.conf 86 | dnsmasq = DnsmasqConf.new(@machine.config.dnsmasq.dnsmasqconf, @machine.config.dnsmasq.reload_command) 87 | dnsmasq.delete(@machine.config.dnsmasq.domain) 88 | 89 | # update /etc/resolver 90 | unless @machine.config.dnsmasq.keep_resolver_on_destroy 91 | resolver = Resolver.new(@machine.config.dnsmasq.resolver, true) # true for sudo 92 | resolver.delete(@machine.config.dnsmasq.domain) 93 | end 94 | 95 | env[:ui].success "Dnsmasq handler removed domain '#{@machine.config.dnsmasq.domain}'" 96 | 97 | @app.call(env) 98 | end 99 | end 100 | end 101 | 102 | end 103 | end -------------------------------------------------------------------------------- /lib/vagrant-dnsmasq/config.rb: -------------------------------------------------------------------------------- 1 | module Vagrant 2 | module Dnsmasq 3 | class Config < Vagrant.plugin("2", :config) 4 | 5 | attr_accessor :domain 6 | attr_accessor :ip 7 | attr_accessor :resolver 8 | attr_accessor :keep_resolver_on_destroy 9 | attr_accessor :dnsmasqconf 10 | attr_accessor :reload_command 11 | attr_accessor :disable 12 | 13 | def initialize 14 | @domain = UNSET_VALUE 15 | @ip = UNSET_VALUE 16 | @resolver = UNSET_VALUE 17 | @keep_resolver_on_destroy = UNSET_VALUE 18 | @dnsmasqconf = UNSET_VALUE 19 | @reload_command = UNSET_VALUE 20 | @disable = UNSET_VALUE 21 | end 22 | 23 | def finalize! 24 | 25 | if @domain == UNSET_VALUE 26 | @domain = nil 27 | elsif !@domain.is_a?(Domain) 28 | @domain = Domain.new @domain; 29 | end 30 | 31 | @keep_resolver_on_destroy = false if @keep_resolver_on_destroy == UNSET_VALUE 32 | @resolver = '/etc/resolver' if @resolver == UNSET_VALUE 33 | @dnsmasqconf = "/etc/dnsmasq.conf" if @dnsmasqconf == UNSET_VALUE 34 | @reload_command = nil if @reload_command == UNSET_VALUE 35 | @disable = false if @disable == UNSET_VALUE 36 | 37 | # default way to obtain ip address 38 | if @ip == UNSET_VALUE 39 | @ip = proc do |guest_machine| 40 | ips = nil 41 | guest_machine.communicate.sudo("hostname -I") do |type, data| 42 | ips = data.scan /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ 43 | end 44 | ips 45 | end 46 | end 47 | 48 | end 49 | 50 | def enabled? 51 | not @disable and not @domain.nil? 52 | end 53 | 54 | def validate(machine) 55 | return unless enabled? 56 | 57 | errors = [] 58 | 59 | # verify @disable 60 | if @disable != true and @disable != false then errors << 'invalid disable setting' end 61 | 62 | # verify domain 63 | begin @domain = Domain.new @domain; rescue => e; errors << e.message end 64 | 65 | # verify ip 66 | if @ip.is_a? Array 67 | @ip.map!{|ip| begin Ip.new(ip); rescue => e; errors << e.message end} 68 | 69 | elsif @ip.is_a? String 70 | begin @ip = Ip.new(@ip); rescue => e; errors << e.message end 71 | @ip = [@ip] 72 | 73 | elsif @ip.is_a? Proc 74 | # okay, there is nothing to verify at the moment 75 | else 76 | @ip = nil 77 | end 78 | 79 | # verify resolver 80 | if @resolver 81 | errors << "directory '#{@resolver}' does not exist" unless Dir.exists? @resolver 82 | end 83 | 84 | # verify dnsmasqconf 85 | if @dnsmasqconf 86 | errors << "file '#{@dnsmasqconf}' does not exist" unless File.exists? @dnsmasqconf 87 | end 88 | 89 | return { 'Dnsmasq configuration' => errors } 90 | end 91 | 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/vagrant-dnsmasq/includes/DnsmasqConf.class.rb: -------------------------------------------------------------------------------- 1 | class DnsmasqConf 2 | 3 | attr_reader :filename 4 | attr_reader :reload_command 5 | 6 | def initialize(filename, reload_command) 7 | raise ArgumentError, 'wrong filename' if filename.blank? 8 | raise IOError unless File.exists? filename 9 | @filename = filename 10 | @reload_command = reload_command 11 | end 12 | 13 | def reload 14 | if @reload_command 15 | begin 16 | # reload dnsmasq config if command specified 17 | puts "You might be asked for your password to restart the dnsmasq daemon." 18 | system @reload_command 19 | rescue => e 20 | # hmm ... i dont care 21 | end 22 | end 23 | end 24 | 25 | def insert(domain, ip) 26 | raise ArgumentError, 'invalid domain instance' unless domain.is_a? Domain 27 | raise ArgumentError, 'invalid ip instance' unless ip.is_a? Ip 28 | 29 | File.open(@filename, 'a') { |file| file.write "\naddress=/#{domain.dotted}/#{ip.v4}" } 30 | reload 31 | end 32 | 33 | def update(domain, ip) 34 | raise ArgumentError, 'invalid domain instance' unless domain.is_a? Domain 35 | raise ArgumentError, 'invalid ip instance' unless ip.is_a? Ip 36 | 37 | File.write(@filename, File.read(@filename).gsub(/(address=\/#{domain.dotted}\/).*/, "\\1#{ip.v4}")) 38 | reload 39 | end 40 | 41 | def delete(domain) 42 | raise ArgumentError, 'invalid domain instance' unless domain.is_a? Domain 43 | 44 | delete_line_from_file(@filename, Regexp.new("address=/\.#{domain.name}")) 45 | reload 46 | end 47 | 48 | def includes?(domain) 49 | raise ArgumentError, 'invalid domain instance' unless domain.is_a? Domain 50 | 51 | File.open(@filename, "r").each_line do |l| 52 | return true if Regexp.new("address=/\.#{domain.name}").match(l.strip) 53 | end 54 | return false 55 | end 56 | 57 | end -------------------------------------------------------------------------------- /lib/vagrant-dnsmasq/includes/Domain.class.rb: -------------------------------------------------------------------------------- 1 | class Domain 2 | 3 | MATCH = /^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*$/ 4 | 5 | def initialize(name) 6 | @name = nil 7 | 8 | if name.is_a? Domain 9 | name = name.dotted 10 | end 11 | 12 | raise ArgumentError, "no domain name given" if name.blank? 13 | 14 | # parse domain name ... 15 | name = name.to_s 16 | name = name[1..-1] if name.start_with? '.' 17 | name = name.downcase 18 | raise ArgumentError, "Domain '#{name}' must match #{MATCH}" unless Domain::valid?(name) 19 | @name = name # without leading . 20 | end 21 | 22 | def self.valid?(name) 23 | if not name.blank? and Domain::MATCH.match(name.downcase) then true else false end 24 | end 25 | 26 | def dotted 27 | '.' + @name 28 | end 29 | 30 | def name 31 | @name 32 | end 33 | 34 | def to_s 35 | dotted 36 | end 37 | 38 | end -------------------------------------------------------------------------------- /lib/vagrant-dnsmasq/includes/Ip.class.rb: -------------------------------------------------------------------------------- 1 | class Ip 2 | 3 | MATCH_IP4 = /^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$/ 4 | 5 | def initialize(ipv4) 6 | 7 | if ipv4.is_a? Ip 8 | ipv4 = ipv4.v4 9 | end 10 | 11 | raise ArgumentError, "IPv4 '#{ipv4}' must match #{MATCH_IP4}" unless Ip::ipv4_valid?(ipv4) 12 | 13 | @ipv4 = ipv4 14 | end 15 | 16 | def self.ipv4_valid?(ipv4) 17 | if not ipv4.blank? and Ip::MATCH_IP4.match(ipv4) then true else false end 18 | end 19 | 20 | def v4 21 | @ipv4 22 | end 23 | 24 | def to_s 25 | v4 26 | end 27 | 28 | end -------------------------------------------------------------------------------- /lib/vagrant-dnsmasq/includes/Resolver.class.rb: -------------------------------------------------------------------------------- 1 | class Resolver 2 | 3 | attr_reader :dirname 4 | 5 | def initialize(dirname, sudo = false) 6 | raise ArgumentError, 'wrong dirname' if dirname.blank? 7 | raise IOError unless Dir.exists? dirname 8 | @dirname = dirname 9 | @sudo = sudo 10 | end 11 | 12 | def self.flush_cache! 13 | system 'sudo dscacheutil -flushcache' if @sudo 14 | end 15 | 16 | def list 17 | Dir["#{@dirname}/*"].map{|dir| '.' + File.basename(dir)} 18 | end 19 | 20 | def insert(domain, ip) 21 | raise ArgumentError, 'invalid domain instance' unless domain.is_a? Domain 22 | raise ArgumentError, 'invalid ip instance' unless ip.is_a? Ip 23 | 24 | unless includes?(domain, ip) 25 | delete(domain) 26 | 27 | puts "You may be asked for your password to insert #{@dirname}/#{domain.name} (ip: #{ip})" if @sudo 28 | system("#{'sudo' if @sudo} sh -c \"echo 'nameserver #{ip.v4}' >> #{@dirname}/#{domain.name}\"") 29 | Resolver::flush_cache! 30 | end 31 | end 32 | 33 | def delete(domain) 34 | raise ArgumentError, 'invalid domain instance' unless domain.is_a? Domain 35 | 36 | if includes? domain 37 | puts "You may be asked for your password to delete #{@dirname}/#{domain.name}" if @sudo 38 | system("#{'sudo' if @sudo} rm -rf #{@dirname}/#{domain.name}") 39 | Resolver::flush_cache! 40 | end 41 | end 42 | 43 | def includes?(domain, with_ip = nil) 44 | raise ArgumentError, 'invalid domain instance' unless domain.is_a? Domain 45 | 46 | unless with_ip.nil? 47 | raise ArgumentError, 'invalid ip instance' unless with_ip.is_a? Ip 48 | File.exists?("#{@dirname}/#{domain.name}") && IO.read("#{@dirname}/#{domain.name}").strip == "nameserver #{with_ip.v4}" 49 | else 50 | File.exists? "#{@dirname}/#{domain.name}" 51 | end 52 | end 53 | 54 | end -------------------------------------------------------------------------------- /lib/vagrant-dnsmasq/includes/helper.rb: -------------------------------------------------------------------------------- 1 | def delete_line_from_file(filename, regex) 2 | # create empty file string 3 | tmp_file = '' 4 | 5 | # iterate over every line and skip lines that match 6 | File.open(filename, "r").each_line do |l| 7 | tmp_file += l unless regex.match(l.strip) 8 | end 9 | 10 | # write tmp file without matching lines 11 | File.open(filename, 'w') { |file| file.write tmp_file } 12 | 13 | # clear memory 14 | tmp_file = nil 15 | end 16 | 17 | class Object 18 | def blank? 19 | respond_to?(:empty?) ? empty? : !self 20 | end 21 | end -------------------------------------------------------------------------------- /lib/vagrant-dnsmasq/version.rb: -------------------------------------------------------------------------------- 1 | module Vagrant 2 | module Dnsmasq 3 | VERSION = "0.1.1" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/Dnsmasq.class.spec.rb: -------------------------------------------------------------------------------- 1 | require './lib/vagrant-dnsmasq/includes/DnsmasqConf.class.rb' 2 | require './lib/vagrant-dnsmasq/includes/Domain.class.rb' 3 | require './lib/vagrant-dnsmasq/includes/Ip.class.rb' 4 | 5 | random = (0...50).map{ ('a'..'z').to_a[rand(26)] }.join 6 | 7 | describe "At first" do 8 | it "should fail if there is no conf file" do 9 | expect { 10 | DnsmasqConf.new("/tmp/some-non-existing-file-#{random}-h25hch345b3k5", nil) 11 | }.to raise_error IOError 12 | end 13 | end 14 | 15 | 16 | describe DnsmasqConf do 17 | before(:each) do 18 | system "touch /tmp/dnsmasq-conf-#{random}" 19 | @dnsm = DnsmasqConf.new("/tmp/dnsmasq-conf-#{random}", nil) 20 | @domain = Domain.new('.foobar') 21 | @ip = Ip.new('10.10.10.10') 22 | @new_ip = Ip.new('10.10.10.11') 23 | end 24 | 25 | after(:each) do 26 | system("rm /tmp/dnsmasq-conf-#{random}") if File.exists? "/tmp/dnsmasq-conf-#{random}" 27 | end 28 | 29 | it "should insert a domain" do 30 | @dnsm.insert(@domain, @ip) 31 | @dnsm.includes?(@domain).should eq(true) 32 | end 33 | 34 | it "should update a domain" do 35 | @dnsm.insert(@domain, @ip) 36 | @dnsm.update(@domain, @new_ip) 37 | domain_exists = begin 38 | found = false 39 | File.open(@dnsm.filename, "r").each_line do |l| 40 | found = true if Regexp.new("address=/\.#{@domain.name}/#{@new_ip.v4}").match(l.strip) 41 | end 42 | found 43 | end 44 | domain_exists.should eq(true) 45 | end 46 | 47 | it "should return false if no domain is found" do 48 | @dnsm.includes?(Domain.new('.doesntexist')).should eq(false) 49 | end 50 | 51 | it "should delete a domain" do 52 | @dnsm.includes?(@domain).should eq(false) 53 | @dnsm.insert(@domain, @ip) 54 | @dnsm.includes?(@domain).should eq(true) 55 | @dnsm.delete(@domain) 56 | @dnsm.includes?(@domain).should eq(false) 57 | end 58 | 59 | it "#insert should raise if domain is nil" do 60 | expect { 61 | @dnsm.insert(nil, nil) 62 | }.to raise_error ArgumentError 63 | end 64 | 65 | it "#delete should raise if domain is nil" do 66 | expect { 67 | @dnsm.delete(nil) 68 | }.to raise_error ArgumentError 69 | end 70 | 71 | it "#includes? should raise if domain is nil" do 72 | expect { 73 | @dnsm.includes?(nil) 74 | }.to raise_error ArgumentError 75 | end 76 | 77 | end -------------------------------------------------------------------------------- /test/Domain.class.spec.rb: -------------------------------------------------------------------------------- 1 | require './lib/vagrant-dnsmasq/includes/Domain.class.rb' 2 | 3 | describe Domain do 4 | 5 | it "should return normal domain" do 6 | Domain.new('foobar').name.should eq('foobar') 7 | Domain.new('.foobar').name.should eq('foobar') 8 | Domain.new('foo.bar').name.should eq('foo.bar') 9 | Domain.new('.foo.bar').name.should eq('foo.bar') 10 | Domain.new('foo8ar').name.should eq('foo8ar') 11 | Domain.new('.foo8ar').name.should eq('foo8ar') 12 | Domain.new('foo.8ar').name.should eq('foo.8ar') 13 | Domain.new('.foo.8ar').name.should eq('foo.8ar') 14 | Domain.new('foo-bar').name.should eq('foo-bar') 15 | end 16 | 17 | it "should return dotted domain" do 18 | Domain.new('foobar').dotted.should eq('.foobar') 19 | Domain.new('.foobar').dotted.should eq('.foobar') 20 | Domain.new('foo.bar').dotted.should eq('.foo.bar') 21 | Domain.new('.foo.bar').dotted.should eq('.foo.bar') 22 | Domain.new('foo8ar').dotted.should eq('.foo8ar') 23 | Domain.new('.foo8ar').dotted.should eq('.foo8ar') 24 | Domain.new('foo.8ar').dotted.should eq('.foo.8ar') 25 | Domain.new('.foo.8ar').dotted.should eq('.foo.8ar') 26 | Domain.new('foo-bar').dotted.should eq('.foo-bar') 27 | end 28 | 29 | it "should return valid domain if domain is passed" do 30 | Domain.new(Domain.new('foobar')).name.should eq('foobar') 31 | Domain.new(Domain.new('.foobar')).name.should eq('foobar') 32 | Domain.new(Domain.new('foobar')).dotted.should eq('.foobar') 33 | Domain.new(Domain.new('.foobar')).dotted.should eq('.foobar') 34 | Domain.new(Domain.new('foo.bar')).name.should eq('foo.bar') 35 | Domain.new(Domain.new('.foo.bar')).name.should eq('foo.bar') 36 | Domain.new(Domain.new('foo.bar')).dotted.should eq('.foo.bar') 37 | Domain.new(Domain.new('.foo.bar')).dotted.should eq('.foo.bar') 38 | Domain.new(Domain.new('foo8ar')).name.should eq('foo8ar') 39 | Domain.new(Domain.new('.foo8ar')).name.should eq('foo8ar') 40 | Domain.new(Domain.new('foo8ar')).dotted.should eq('.foo8ar') 41 | Domain.new(Domain.new('.foo8ar')).dotted.should eq('.foo8ar') 42 | Domain.new(Domain.new('foo.8ar')).name.should eq('foo.8ar') 43 | Domain.new(Domain.new('.foo.8ar')).name.should eq('foo.8ar') 44 | Domain.new(Domain.new('foo.8ar')).dotted.should eq('.foo.8ar') 45 | Domain.new(Domain.new('.foo.8ar')).dotted.should eq('.foo.8ar') 46 | Domain.new(Domain.new('foo-bar')).name.should eq('foo-bar') 47 | end 48 | 49 | it "should make the domain lowercase" do 50 | Domain.new('FOOBAR').name.should eq('foobar') 51 | end 52 | 53 | it "should fail for invalid domains" do 54 | expect { 55 | Domain.new('foo*') 56 | }.to raise_error ArgumentError 57 | 58 | expect { 59 | Domain.new('.') 60 | }.to raise_error ArgumentError 61 | 62 | expect { 63 | Domain.new('foo..bar') 64 | }.to raise_error ArgumentError 65 | 66 | expect { 67 | Domain.new('foo_') 68 | }.to raise_error ArgumentError 69 | 70 | expect { 71 | Domain.new('foo.') 72 | }.to raise_error ArgumentError 73 | 74 | expect { 75 | Domain.new('-foo') 76 | }.to raise_error ArgumentError 77 | 78 | expect { 79 | Domain.new('foo-') 80 | }.to raise_error ArgumentError 81 | 82 | expect { 83 | Domain.new('') 84 | }.to raise_error ArgumentError 85 | 86 | expect { 87 | Domain.new(nil) 88 | }.to raise_error ArgumentError 89 | end 90 | 91 | it "#valid? should fail for invalid domains" do 92 | Domain::valid?('foo*').should eq(false) 93 | Domain::valid?('.').should eq(false) 94 | Domain::valid?('foo..bar').should eq(false) 95 | Domain::valid?('foo_').should eq(false) 96 | Domain::valid?('foo.').should eq(false) 97 | Domain::valid?('-foo').should eq(false) 98 | Domain::valid?('foo-').should eq(false) 99 | Domain::valid?('').should eq(false) 100 | Domain::valid?(nil).should eq(false) 101 | end 102 | 103 | end -------------------------------------------------------------------------------- /test/Ip.class.spec.rb: -------------------------------------------------------------------------------- 1 | require './lib/vagrant-dnsmasq/includes/Ip.class.rb' 2 | 3 | describe Ip do 4 | 5 | it "should return ipv4" do 6 | Ip.new('10.10.10.10').v4.should eq('10.10.10.10') 7 | end 8 | 9 | it "should return ipv4 if ip is passed" do 10 | Ip.new(Ip.new('10.10.10.10')).v4.should eq('10.10.10.10') 11 | end 12 | 13 | it "should fail for invalid ips" do 14 | expect { 15 | Ip.new('10.10.10') 16 | }.to raise_error ArgumentError 17 | 18 | expect { 19 | Ip.new('300.0.0.1') 20 | }.to raise_error ArgumentError 21 | 22 | expect { 23 | Ip.new('10-10-10-10') 24 | }.to raise_error ArgumentError 25 | 26 | expect { 27 | Ip.new('') 28 | }.to raise_error ArgumentError 29 | 30 | expect { 31 | Ip.new(nil) 32 | }.to raise_error ArgumentError 33 | end 34 | 35 | it "#valid? should fail for invalid ips" do 36 | Ip::ipv4_valid?('10.10.10').should eq(false) 37 | Ip::ipv4_valid?('300.0.0.1').should eq(false) 38 | Ip::ipv4_valid?('10-10-10-10').should eq(false) 39 | Ip::ipv4_valid?('').should eq(false) 40 | Ip::ipv4_valid?(nil).should eq(false) 41 | end 42 | 43 | end -------------------------------------------------------------------------------- /test/Resolver.class.spec.rb: -------------------------------------------------------------------------------- 1 | require './lib/vagrant-dnsmasq/includes/Resolver.class.rb' 2 | require './lib/vagrant-dnsmasq/includes/Domain.class.rb' 3 | require './lib/vagrant-dnsmasq/includes/Ip.class.rb' 4 | 5 | random = (0...50).map{ ('a'..'z').to_a[rand(26)] }.join 6 | 7 | describe "At first" do 8 | it "should fail if there is no dir" do 9 | expect { 10 | Resolver.new("/tmp/some-non-existing-dir-#{random}-j2335n3kjfs3ap") 11 | }.to raise_error IOError 12 | end 13 | end 14 | 15 | describe Resolver do 16 | before(:each) do 17 | system("mkdir -p /tmp/resolver-dir-#{random}") 18 | @res = Resolver.new("/tmp/resolver-dir-#{random}") 19 | @domain = Domain.new('.foobar') 20 | @ip = Ip.new('10.10.10.10') 21 | end 22 | 23 | after(:each) do 24 | system("rm -R /tmp/resolver-dir-#{random}") if File.exists? "/tmp/resolver-dir-#{random}" 25 | end 26 | 27 | it "should insert a domain" do 28 | @res.insert(@domain, @ip) 29 | @res.includes?(@domain).should eq(true) 30 | end 31 | 32 | it "should return false if no domain is found" do 33 | @res.includes?(Domain.new('.doesntexist')).should eq(false) 34 | end 35 | 36 | it "should delete a domain" do 37 | @res.includes?(@domain).should eq(false) 38 | @res.insert(@domain, @ip) 39 | @res.includes?(@domain).should eq(true) 40 | @res.delete(@domain) 41 | @res.includes?(@domain).should eq(false) 42 | end 43 | 44 | it "#includes? should verify content of domain file" do 45 | @res.includes?(@domain).should eq(false) 46 | @res.includes?(@domain, @ip).should eq(false) 47 | 48 | @res.insert(@domain, @ip) 49 | 50 | @res.includes?(@domain).should eq(true) 51 | @res.includes?(@domain, @ip).should eq(true) 52 | @res.includes?(@domain, Ip.new('10.10.10.11')).should eq(false) 53 | end 54 | 55 | it "#insert should raise if domain is nil" do 56 | expect { 57 | @res.insert(nil, nil) 58 | }.to raise_error ArgumentError 59 | end 60 | 61 | it "#delete should raise if domain is nil" do 62 | expect { 63 | @res.delete(nil) 64 | }.to raise_error ArgumentError 65 | end 66 | 67 | it "#includes? should raise if domain is nil" do 68 | expect { 69 | @res.includes?(nil) 70 | }.to raise_error ArgumentError 71 | end 72 | 73 | end -------------------------------------------------------------------------------- /test/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure('2') do |config| 5 | config.vm.box = 'precise64' 6 | config.vm.box_url = "http://files.vagrantup.com/precise64.box" 7 | 8 | # dnsmasq works with both, private network as well as public network 9 | # config.vm.network :private_network, ip: "192.168.59.100" 10 | config.vm.network :public_network 11 | 12 | 13 | # dnsmasq ... 14 | 15 | # set domain ending (required) 16 | # adding this line enables dnsmasq handling 17 | config.dnsmasq.domain = '.dnsmasqplugintest' 18 | 19 | # optional configuration ... 20 | 21 | # this plugin runs 'hostname -I' on the guest machine to obtain 22 | # the guest ip address. you can overwrite this behaviour. 23 | # config.dnsmasq.ip = '192.168.59.109' 24 | 25 | # config.dnsmasq.ip = proc do |guest_machine| 26 | # guest_machine.communicate.sudo("command to obtain ip somehow") do |type, data| 27 | # # return something like '192.168.59.100' or ['192.168.59.100', '192.168.59.103'] 28 | # end 29 | # end 30 | 31 | # this will prompt you during 'vagrant up' to choose an IP 32 | # config.dnsmasq.ip = ['192.168.59.100', '192.168.59.103'] 33 | 34 | # overwrite default location for /etc/resolver directory 35 | # config.dnsmasq.resolver = '/etc/resolver' 36 | 37 | # 'vagrant destroy' does not delete /etc/resolver nameserver file, defaults to false 38 | # config.dnsmasq.keep_resolver_on_destroy = true 39 | 40 | # overwrite default location for /etc/dnsmasq.conf 41 | brew_prefix = `brew --prefix`.strip 42 | config.dnsmasq.dnsmasqconf = brew_prefix + '/etc/dnsmasq.conf' 43 | 44 | # specify command to reload dnsmasq 45 | config.dnsmasq.reload_command = 'sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist; sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist' 46 | 47 | # disable dnsmasq handling 48 | # config.dnsmasq.disable = true 49 | 50 | end -------------------------------------------------------------------------------- /test/helper.spec.rb: -------------------------------------------------------------------------------- 1 | require './lib/vagrant-dnsmasq/includes/helper.rb' 2 | 3 | random = (0...50).map{ ('a'..'z').to_a[rand(26)] }.join 4 | 5 | 6 | describe "Helper" do 7 | 8 | it "#delete_line_from_file should delete a line from a file" do 9 | tmp_file = "/tmp/helper-delete-line-from-file-test-#{random}" 10 | system "echo \"line 1\n another line\n\na line after an empty line \nline5\" > #{tmp_file}" 11 | delete_line_from_file(tmp_file, Regexp.new("an")) 12 | IO.read(tmp_file).should eq("line 1\n\nline5\n") 13 | system("rm #{tmp_file}") if File.exists? tmp_file 14 | end 15 | 16 | end 17 | 18 | describe Object do 19 | it "#blank? should work" do 20 | "".blank?.should eq(true) 21 | nil.blank?.should eq(true) 22 | [].blank?.should eq(true) 23 | false.blank?.should eq(true) 24 | 25 | 0.blank?.should eq(false) 26 | 27 | 'foo'.blank?.should eq(false) 28 | [1].blank?.should eq(false) 29 | true.blank?.should eq(false) 30 | 1.blank?.should eq(false) 31 | -1.blank?.should eq(false) 32 | 33 | end 34 | end -------------------------------------------------------------------------------- /vagrant-dnsmasq.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'vagrant-dnsmasq/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "vagrant-dnsmasq" 8 | spec.version = Vagrant::Dnsmasq::VERSION 9 | spec.authors = ["mattes"] 10 | spec.email = ["matthias.kadenbach@gmail.com"] 11 | spec.description = "A Dnsmasq Vagrant plugin that manages the dnsmasq.conf file and /etc/resolver directory on your host system." 12 | spec.summary = spec.description 13 | spec.homepage = "https://github.com/mattes/vagrant-dnsmasq" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.3" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "rspec" 24 | 25 | spec.add_runtime_dependency "ffi", "~> 1.9.5" 26 | end 27 | --------------------------------------------------------------------------------