├── .gitignore ├── README.md ├── Rakefile ├── bin └── hostess ├── hostess.gemspec ├── lib ├── hostess.rb └── hostess │ ├── options.rb │ └── virtual_host.rb └── test ├── hostess_integration_test.rb ├── hostess_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | rdoc 3 | *.gem 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hostess 2 | 3 | A simple tool for adding local directories as virtual hosts in a local apache installation. It probably only works well on a Mac, but we're scratching our own itch here. 4 | 5 | ## Usage 6 | 7 | $ hostess create mysite.local /Users/myuser/Sites/mysite 8 | 9 | This will create a new virtual host in your Apache configuration, setup your Mac's DNS to respond to that domain name, and restart Apache to make the new virtual host live. 10 | 11 | $ hostess help 12 | Usage: 13 | hostess create domain directory - create a new virtual host 14 | hostess delete domain - delete a virtual host 15 | hostess list - list hostess virtual hosts 16 | hostess help - this info 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "rubygems/package_task" 3 | require 'rdoc/task' 4 | 5 | require "rake/testtask" 6 | Rake::TestTask.new do |t| 7 | t.libs << "test" 8 | t.test_files = FileList["test/**/*_test.rb"] 9 | t.verbose = true 10 | end 11 | 12 | task :default => ["test"] 13 | 14 | # This builds the actual gem. For details of what all these options 15 | # mean, and other ones you can add, check the documentation here: 16 | # 17 | # http://rubygems.org/read/chapter/20 18 | # 19 | spec = Gem::Specification.new do |s| 20 | 21 | # Change these as appropriate 22 | s.name = "hostess" 23 | s.version = "0.1.8" 24 | s.summary = "Manage simple apache virtual hosts" 25 | s.author = "Chris Roos, James Adam" 26 | s.email = "chris@chrisroos.co.uk" 27 | s.homepage = "http://chrisroos.co.uk" 28 | 29 | s.has_rdoc = false 30 | 31 | # You should probably have a README of some kind. Change the filename 32 | # as appropriate 33 | s.extra_rdoc_files = %w(README.md) 34 | s.rdoc_options = %w(--main README.md) 35 | 36 | s.require_paths = ['bin', 'lib'] 37 | 38 | # Add any extra files to include in the gem (like your README) 39 | s.files = %w(README.md Rakefile) + Dir.glob("{bin}/**/*") + Dir.glob("{lib}/**/*") 40 | s.executables = FileList["bin/**"].map { |f| File.basename(f) } 41 | 42 | # If you want to depend on other gems, add them here, along with any 43 | # relevant versions 44 | # s.add_dependency("some_other_gem", "~> 0.1.0") 45 | 46 | # If your tests use any gems, include them here 47 | s.add_development_dependency("mocha") 48 | 49 | # If you want to publish automatically to rubyforge, you'll may need 50 | # to tweak this, and the publishing task below too. 51 | s.rubyforge_project = "hostess" 52 | end 53 | 54 | # This task actually builds the gem. We also regenerate a static 55 | # .gemspec file, which is useful if something (i.e. GitHub) will 56 | # be automatically building a gem for this project. If you're not 57 | # using GitHub, edit as appropriate. 58 | Gem::PackageTask.new(spec) do |pkg| 59 | pkg.gem_spec = spec 60 | 61 | # Generate the gemspec file for github. 62 | file = File.dirname(__FILE__) + "/#{spec.name}.gemspec" 63 | File.open(file, "w") {|f| f << spec.to_ruby } 64 | end 65 | 66 | # Generate documentation 67 | Rake::RDocTask.new do |rd| 68 | 69 | rd.rdoc_files.include("lib/**/*.rb") 70 | rd.rdoc_dir = "rdoc" 71 | end 72 | 73 | desc 'Clear out RDoc and generated packages' 74 | task :clean => [:clobber_rdoc, :clobber_package] do 75 | rm "#{spec.name}.gemspec" 76 | end 77 | 78 | # If you want to publish to RubyForge automatically, here's a simple 79 | # task to help do that. If you don't, just get rid of this. 80 | # Be sure to set up your Rubyforge account details with the Rubyforge 81 | # gem; you'll need to run `rubyforge setup` and `rubyforge config` at 82 | # the very least. 83 | begin 84 | require "rake/contrib/sshpublisher" 85 | namespace :rubyforge do 86 | 87 | desc "Release gem and RDoc documentation to RubyForge" 88 | task :release => ["rubyforge:release:gem", "rubyforge:release:docs"] 89 | 90 | namespace :release do 91 | desc "Release a new version of this gem" 92 | task :gem => [:package] do 93 | require 'rubyforge' 94 | rubyforge = RubyForge.new 95 | rubyforge.configure 96 | rubyforge.login 97 | rubyforge.userconfig['release_notes'] = spec.summary 98 | path_to_gem = File.join(File.dirname(__FILE__), "pkg", "#{spec.name}-#{spec.version}.gem") 99 | puts "Publishing #{spec.name}-#{spec.version.to_s} to Rubyforge..." 100 | rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version.to_s, path_to_gem) 101 | end 102 | 103 | desc "Publish RDoc to RubyForge." 104 | task :docs => [:rdoc] do 105 | config = YAML.load( 106 | File.read(File.expand_path('~/.rubyforge/user-config.yml')) 107 | ) 108 | 109 | host = "#{config['username']}@rubyforge.org" 110 | remote_dir = "/var/www/gforge-projects/hostess/" # Should be the same as the rubyforge project name 111 | local_dir = 'rdoc' 112 | 113 | Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload 114 | end 115 | end 116 | end 117 | rescue LoadError 118 | puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured." 119 | end 120 | -------------------------------------------------------------------------------- /bin/hostess: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | require 'hostess' 4 | 5 | options = Hostess::Options.new(*ARGV) 6 | options.display_banner_and_return unless options.valid? 7 | 8 | Hostess::VirtualHost.new(options).execute! -------------------------------------------------------------------------------- /hostess.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "hostess" 5 | s.version = "0.1.8" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Chris Roos, James Adam"] 9 | s.date = "2012-10-27" 10 | s.email = "chris@chrisroos.co.uk" 11 | s.executables = ["hostess"] 12 | s.extra_rdoc_files = ["README.md"] 13 | s.files = ["README.md", "Rakefile", "bin/hostess", "lib/hostess", "lib/hostess/options.rb", "lib/hostess/virtual_host.rb", "lib/hostess.rb"] 14 | s.homepage = "http://chrisroos.co.uk" 15 | s.rdoc_options = ["--main", "README.md"] 16 | s.require_paths = ["bin", "lib"] 17 | s.rubyforge_project = "hostess" 18 | s.rubygems_version = "1.8.23" 19 | s.summary = "Manage simple apache virtual hosts" 20 | 21 | if s.respond_to? :specification_version then 22 | s.specification_version = 3 23 | 24 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 25 | s.add_development_dependency(%q, [">= 0"]) 26 | else 27 | s.add_dependency(%q, [">= 0"]) 28 | end 29 | else 30 | s.add_dependency(%q, [">= 0"]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/hostess.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require 'fileutils' 3 | require 'pathname' 4 | require 'tempfile' 5 | 6 | module Hostess 7 | 8 | autoload :Options, 'hostess/options' 9 | autoload :VirtualHost, 'hostess/virtual_host' 10 | 11 | class << self 12 | 13 | attr_writer :apache_config_dir, :apache_log_dir 14 | 15 | def script_name 16 | 'hostess' 17 | end 18 | 19 | def apache_config_dir 20 | @apache_config_dir || File.join('/', 'etc', 'apache2') 21 | end 22 | 23 | def apache_config 24 | File.join(apache_config_dir, 'httpd.conf') 25 | end 26 | 27 | def vhosts_dir 28 | File.join(apache_config_dir, "#{script_name}_vhosts") 29 | end 30 | 31 | def apache_log_dir 32 | @apache_log_dir || File.join('/', 'var', 'log', 'apache2') 33 | end 34 | 35 | def vhosts_log_dir 36 | File.join(apache_log_dir, "#{script_name}_vhosts") 37 | end 38 | 39 | def disable_sudo! 40 | @disable_sudo = true 41 | end 42 | 43 | def use_sudo? 44 | @disable_sudo ? false : true 45 | end 46 | 47 | end 48 | 49 | end -------------------------------------------------------------------------------- /lib/hostess/options.rb: -------------------------------------------------------------------------------- 1 | module Hostess 2 | class Options 3 | attr_reader :action, :domain, :directory 4 | def initialize(action=nil, domain=nil, directory=nil) 5 | @action, @domain, @directory = action, domain, directory 6 | end 7 | def directory 8 | File.expand_path(@directory) if @directory 9 | end 10 | def display_banner_and_return 11 | puts banner 12 | exit 13 | end 14 | def valid? 15 | valid_create? or valid_delete? or valid_list? or valid_help? 16 | end 17 | private 18 | def valid_create? 19 | @action == 'create' and domain and directory 20 | end 21 | def valid_delete? 22 | @action == 'delete' and domain 23 | end 24 | def valid_list? 25 | @action == 'list' 26 | end 27 | def valid_help? 28 | @action == 'help' 29 | end 30 | def banner 31 | <> #{hosts_filename}" 43 | end 44 | end 45 | def delete_dns_entry 46 | if dscl_works? 47 | run "dscl localhost -delete /Local/Default/Hosts/#{@options.domain}" 48 | else 49 | run "perl -pi -e 's/127.0.0.1 #{@options.domain}\\n//g' #{hosts_filename}" 50 | end 51 | end 52 | def create_vhost 53 | tempfile = Tempfile.new('vhost') 54 | tempfile.puts(vhost_config) 55 | tempfile.close 56 | run "mv #{tempfile.path} #{config_filename}" 57 | end 58 | def delete_vhost 59 | run "rm #{config_filename}" 60 | end 61 | def apache_log_directory 62 | File.join(Hostess.vhosts_log_dir, @options.domain) 63 | end 64 | def create_apache_log_directory 65 | run "mkdir -p #{apache_log_directory}" 66 | end 67 | def delete_apache_log_directory 68 | run "rm -r #{apache_log_directory}" 69 | end 70 | def vhost_config 71 | domain, directory = @options.domain, @options.directory 72 | template = <<-EOT 73 | 74 | ServerName <%= domain %> 75 | DocumentRoot "<%= directory %>" 76 | "> 77 | Options FollowSymLinks 78 | AllowOverride All 79 | allow from all 80 | 81 | 82 | ErrorDocument 403 /404.html 83 | Order allow,deny 84 | Deny from all 85 | Satisfy All 86 | 87 | ErrorLog <%= File.join(apache_log_directory, 'error_log') %> 88 | CustomLog <%= File.join(apache_log_directory, 'access_log') %> common 89 | #RewriteLogLevel 3 90 | RewriteLog <%= File.join(apache_log_directory, 'rewrite_log') %> 91 | 92 | EOT 93 | ERB.new(template).result(binding) 94 | end 95 | def config_filename 96 | File.join(Hostess.vhosts_dir, "#{@options.domain}.conf") 97 | end 98 | def setup_apache_config 99 | unless File.read(Hostess.apache_config).include?("Include #{File.join(Hostess.vhosts_dir, '*.conf')}") 100 | run "echo '' >> #{Hostess.apache_config}" 101 | run "echo '' >> #{Hostess.apache_config}" 102 | run "echo '# Line added by #{Hostess.script_name}' >> #{Hostess.apache_config}" 103 | run "echo 'NameVirtualHost *:80' >> #{Hostess.apache_config}" 104 | run "echo 'Include #{File.join(Hostess.vhosts_dir, '*.conf')}' >> #{Hostess.apache_config}" 105 | end 106 | end 107 | def create_vhost_directory 108 | run "mkdir -p #{Hostess.vhosts_dir}" 109 | end 110 | def restart_apache 111 | run "apachectl restart" 112 | end 113 | def run(cmd) 114 | cmd = sudo(cmd) if Hostess.use_sudo? 115 | puts cmd if @debug 116 | system cmd 117 | end 118 | def sudo(cmd) 119 | "sudo -s \"#{cmd}\"" 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/hostess_integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class HostessIntegrationTest < Test::Unit::TestCase 4 | 5 | def test_creating_a_virtual_host 6 | Hostess.apache_config_dir = File.join(Dir.tmpdir, 'hostess', 'apache_config') 7 | Hostess.apache_log_dir = File.join(Dir.tmpdir, 'hostess', 'apache_logs') 8 | Hostess.disable_sudo! 9 | 10 | create_hostess_directories_and_apache_config 11 | 12 | domain, path_to_site = 'example.local', '/path/to/site' 13 | options = Hostess::Options.new('create', domain, path_to_site) 14 | virtual_host = Hostess::VirtualHost.new(options) 15 | 16 | ensure_we_setup_the_local_dns_entry(virtual_host) 17 | ensure_we_restart_apache(virtual_host) 18 | 19 | virtual_host.execute! 20 | 21 | assert_that_we_have_updated_the_apache_config_file 22 | assert_that_we_have_created_the_hostess_vhosts_directory 23 | assert_that_we_have_created_the_hostess_log_directory 24 | assert_that_we_have_created_the_vhost(domain, path_to_site) 25 | end 26 | 27 | private 28 | 29 | def create_hostess_directories_and_apache_config 30 | # Remove our test directories and apache config 31 | FileUtils.rm_f(Hostess.apache_config_dir) 32 | FileUtils.rm_f(Hostess.apache_log_dir) 33 | FileUtils.rm_f(Hostess.apache_config) 34 | 35 | # Create our test subdirectories and apache config 36 | FileUtils.mkdir_p(Hostess.apache_config_dir) 37 | FileUtils.mkdir_p(Hostess.apache_log_dir) 38 | FileUtils.touch(Hostess.apache_config) 39 | end 40 | 41 | def ensure_we_setup_the_local_dns_entry(virtual_host) 42 | virtual_host.expects(:create_dns_entry) 43 | end 44 | 45 | def ensure_we_restart_apache(virtual_host) 46 | virtual_host.expects(:restart_apache) 47 | end 48 | 49 | def assert_that_we_have_updated_the_apache_config_file 50 | expected_apache_config_file_content = <<-EOS 51 | 52 | 53 | # Line added by #{Hostess.script_name} 54 | NameVirtualHost *:80 55 | Include #{File.join(Hostess.vhosts_dir, '*.conf')} 56 | EOS 57 | assert_equal expected_apache_config_file_content, File.read(Hostess.apache_config) 58 | end 59 | 60 | def assert_that_we_have_created_the_hostess_vhosts_directory 61 | assert File.directory?(Hostess.vhosts_dir) 62 | end 63 | 64 | def assert_that_we_have_created_the_hostess_log_directory 65 | assert File.directory?(Hostess.vhosts_log_dir) 66 | end 67 | 68 | def assert_that_we_have_created_the_vhost(domain, path_to_site) 69 | expected_vhost_content = <<-EOS 70 | 71 | ServerName #{domain} 72 | DocumentRoot "#{path_to_site}" 73 | 74 | Options FollowSymLinks 75 | AllowOverride All 76 | allow from all 77 | 78 | 79 | ErrorDocument 403 /404.html 80 | Order allow,deny 81 | Deny from all 82 | Satisfy All 83 | 84 | ErrorLog #{File.join(Hostess.vhosts_log_dir, domain, 'error_log')} 85 | CustomLog #{File.join(Hostess.vhosts_log_dir, domain, 'access_log')} common 86 | #RewriteLogLevel 3 87 | RewriteLog #{File.join(Hostess.vhosts_log_dir, domain, 'rewrite_log')} 88 | 89 | EOS 90 | assert_equal expected_vhost_content, File.read(File.join(Hostess.vhosts_dir, "#{domain}.conf")) 91 | end 92 | 93 | end -------------------------------------------------------------------------------- /test/hostess_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class HostessTest < Test::Unit::TestCase 4 | 5 | def test_should_set_a_default_for_the_apache_config_directory 6 | Hostess.apache_config_dir = nil 7 | 8 | assert_equal '/etc/apache2', Hostess.apache_config_dir 9 | end 10 | 11 | def test_should_allow_us_to_override_the_default_apache_config_directory 12 | Hostess.apache_config_dir = '/path/to/apache' 13 | 14 | assert_equal '/path/to/apache', Hostess.apache_config_dir 15 | end 16 | 17 | def test_should_return_the_path_to_the_apache_config_file 18 | Hostess.apache_config_dir = '/path/to/apache' 19 | 20 | assert_equal '/path/to/apache/httpd.conf', Hostess.apache_config 21 | end 22 | 23 | def test_should_return_the_path_to_vhosts_directory 24 | Hostess.apache_config_dir = '/path/to/apache' 25 | 26 | assert_equal '/path/to/apache/hostess_vhosts', Hostess.vhosts_dir 27 | end 28 | 29 | def test_should_set_a_default_for_the_apache_log_directory 30 | Hostess.apache_log_dir = nil 31 | 32 | assert_equal '/var/log/apache2', Hostess.apache_log_dir 33 | end 34 | 35 | def test_should_allow_us_to_override_the_default_apache_log_directory 36 | Hostess.apache_log_dir = '/path/to/apache/logs' 37 | 38 | assert_equal '/path/to/apache/logs', Hostess.apache_log_dir 39 | end 40 | 41 | def test_should_return_the_path_to_the_vhosts_log_directory 42 | Hostess.apache_log_dir = '/path/to/apache/logs' 43 | 44 | assert_equal '/path/to/apache/logs/hostess_vhosts', Hostess.vhosts_log_dir 45 | end 46 | 47 | def test_should_use_sudo_by_default 48 | # Have to ensure that we haven't disabled sudo 49 | Hostess.instance_variable_set("@disable_sudo", false) 50 | 51 | assert_equal true, Hostess.use_sudo? 52 | end 53 | 54 | def test_should_allow_us_to_disable_sudo 55 | Hostess.disable_sudo! 56 | 57 | assert_equal false, Hostess.use_sudo? 58 | end 59 | 60 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'mocha' 3 | 4 | require 'hostess' 5 | 6 | require 'tmpdir' --------------------------------------------------------------------------------