Skip to content

Instantly share code, notes, and snippets.

@andkirby
Last active October 18, 2016 13:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andkirby/dd66d8fadcd8050b381a1cfd2a5f801e to your computer and use it in GitHub Desktop.
Save andkirby/dd66d8fadcd8050b381a1cfd2a5f801e to your computer and use it in GitHub Desktop.
This file can help to set forwarded ports in VirtualBox VM. It solves the problem described in the issue https://github.com/mitchellh/vagrant/issues/7905
require 'yaml'
require 'digest/md5'
##
# Module VBoxDirectPorts
# It add ports forwarding in VirtualBox VM
# It can generate ports based upon hostname
#
# Configuration files.
# (.dist) means there is should be distributive file by default with default configuration
#
# - pf-config.yml(.dist)
# This file contains custom user configuration. E.g.:
# my.hostname.cc:
# - host_ip: 127.0.0.1
# guest_ip: 0.0.0.0
# host: 80
# guest: 80
#
# - pf-hosts
# This file contains user hostnames.
# System will generate guest ports and host IP addresses for them in
# file pf-config-autogenerated.yml.
# Default guest IP address is 0.0.0.0
#
# - pf-host-ports(.dist)
# Ports list which will be used on the host side during autogeneration.
# Guest ports will be generated automatically.
#
# Guest ports generation algorithm in
# Vagrant::VBoxDirectPorts::host_ports
# Host IP addresses generation algorithm in
# Vagrant::VBoxDirectPorts::host_ip
#
# Using in Vagrantfile:
# # Set up port forwarding in non-provisioning mode
# # http://stackoverflow.com/questions/24855635/check-if-vagrant-provisioning-has-been-done
# if File.file?('.vagrant/machines/default/virtualbox/action_provision')
# require_relative 'vbox_direct_ports.rb'
#
# # Enable ports output
# # Vagrant::VBoxDirectPorts::verbosity = true
# # Generate always new file pf-config-autogenerated.yml (any vagrant command here)
# # Vagrant::VBoxDirectPorts::always_generate_config = true
# # Set direct_vm_update to update port forwarding list with using Vagrant native functionality
# # Vagrant::VBoxDirectPorts::direct_vm_update = false
#
# # Make ports forwarding
# Vagrant::VBoxDirectPorts::forward_ports config
# end
#
# GitHub Gist: https://gist.github.com/andkirby/dd66d8fadcd8050b381a1cfd2a5f801e
#
module Vagrant
module VBoxDirectPorts
module_function
# region Properties
##
# Enable verbosity
#
@@verbosity = false
##
# Generate config always in file pf-config-autogenerated.yml
#
@@always_generate_config = false
##
# Status of updating port forwarding list in VirtualBox VM directly
#
@@direct_update = true
##
# Set status of output
#
def verbosity=(flag)
@@verbosity = flag
end
##
# Set status of output
#
def always_generate_config=(flag)
@@always_generate_config = flag
end
##
# Status of updating port forwarding list in VirtualBox VM directly
# If FALSE it will use Vagrant functionality
#
def direct_vm_update=(flag)
@@direct_update = flag
end
# endregion
# region Main methods
##
# Set up port forwarding from configuration file
#
# pf-config.yml or pf-config.yml.dist
#
def forward_ports(config)
forward_hosts_ports config
forward_custom_ports config
end
##
# Forward ports by user config file
#
def forward_custom_ports(config)
update_hosts_file load_config(port_forward_custom_file)
config_forward_ports config, file: port_forward_custom_file
self
end
##
# Forward host ports from file pf-hosts (or pf-hosts.dist)
#
def forward_hosts_ports(config)
if @@always_generate_config === false && File.exist?(autogenerated_config_file)
return config_forward_ports config, file: autogenerated_config_file
end
return self until File.readable? current_dir + '/pf-hosts'
generated_config = {}
(File.read(current_dir + '/pf-hosts') + $/).each_line do |line|
hostname = line.strip!
# Ignore comments, empty strings
next if hostname == nil || hostname.chars[0] == '#'
generated_config[hostname] = auto_forward_ports_config(hostname: hostname, config: config)
end
# Generate ports forwarding from written file
config_forward_ports config, file: write_autogenerated_config(generated_config)
update_hosts_file generated_config
self
end
# endregion
protected
module_function
##
# It will return path to pf-config.yml if exist
# or pf-config.yml.dist instead
#
def port_forward_custom_file
path = current_dir
path + '/pf-config.yml' +
(File.exist?(path + '/pf-config.yml') ? '' : '.dist')
end
# region Ports Auto-generation
##
# Write config into YAML file
#
def write_autogenerated_config(ports_config)
file = autogenerated_config_file
fw = File.open(file, 'w')
fw.write(ports_config.to_yaml)
fw.close
file
end
def autogenerated_config_file
current_dir + '/pf-config-autogenerated.yml'
end
##
# Generate configuration automatically with guest ports and host IP address for a hostname
#
def auto_forward_ports_config(hostname:, config:)
configs = []
ip_host = host_ip(hostname)
host_ports(hostname).each { |host_port, guest_port|
configs << {
host: host_port, guest: guest_port,
host_ip: ip_host, guest_ip: default_guest_ip
}
}
configs
end
##
# Get hostname ports map which should be forwarded from a host OS to a guest OS
#
def host_ports(hostname)
base_port = guest_base_port(hostname).to_i
ports_map = {}
path = current_dir
file = path + '/pf-host-ports' + (File.exist?(path + '/pf-host-ports') ? '' : '.dist')
index = 0
File.open(file, 'r') do |f|
f.each_line do |port|
# Ignore comments, empty strings
next if port == nil || port.chars[0] == '#'
ports_map[port.to_i] = base_port + port.to_i
index += 1
end
end
ports_map
end
##
# Generate guest base port based upon a hostname
#
# NOTE: This algorithm must reflected in centos7-nginx-php7/create-container.sh file
# or used the same container ports
#
def guest_base_port(hostname)
hash = Digest::SHA1.hexdigest hostname
1024 + hash.chars[0].ord + hash.chars[1].ord + hash.chars[2].ord + hash.chars[3].ord +
hash.chars[4].ord + hash.chars[5].ord
end
##
# Generate local IP address based upon a hostname
#
def host_ip(hostname)
hash = Digest::SHA1.hexdigest hostname
'127.' + (hash_symbol_int(hash.chars[0]) + hash_symbol_int(hash.chars[1])).to_s + '.' +
(hash_symbol_int(hash.chars[2]).ord + hash_symbol_int(hash.chars[3])).to_s + '.' +
(hash_symbol_int(hash.chars[4]).ord + hash_symbol_int(hash.chars[5])).to_s
end
# endregion
# region Apply ports forwarding by YAML file
##
# Forward ports by config file
#
def config_forward_ports(config, file:)
puts "Reading #{file}..." if @@verbosity
load_config(file).each { | group_key, group_net_conf |
puts "===== #{group_key.green} =====" if @@verbosity
puts 'Ports forwarding.' if @@verbosity
group_net_conf.each { |net_conf|
# convert keys to hash keys
net_conf_hashed = Hash.new
net_conf.each { |key, value| net_conf_hashed[key.to_sym] = value }
puts "#{net_conf_hashed[:host_ip]}:#{net_conf_hashed[:host]} => " +
"#{net_conf_hashed[:guest_ip]}:#{net_conf_hashed[:guest]}" if @@verbosity
if @@direct_update
##
# Due to reason there is no ability to use the same ports Vagrant command "config.vm.network" cannot be used
# https://github.com/mitchellh/vagrant/issues/7905
#
add_vbox_nat_port_forwarding(config, net_conf_hashed)
else
# Add port forwarding via Vagrant
config.vm.network 'forwarded_port', net_conf_hashed
end
}
puts '===============' if @@verbosity
}
end
##
# Make direct changes in VM
# It works with NAT only
#
def add_vbox_nat_port_forwarding(config, host:, guest:, host_ip: '', guest_ip: '')
config.vm.provider :virtualbox do |vb|
value = "tcp,#{host_ip},#{host},#{guest_ip},#{guest}"
value = Digest::SHA1.hexdigest(value) + ',' + value # add unique ID
vb.customize ['modifyvm', :id, '--natpf1', value]
end
end
##
# Load YAML configuration
#
def load_config(file)
YAML.load_file(file) || {}
end
# endregion
# region Common
##
# Get current directory path
#
def current_dir
File.dirname(File.expand_path(__FILE__))
end
##
# Default guest IP address
#
def default_guest_ip
'0.0.0.0'
end
##
# It will make numbers lower
# because there is the range 0..9 and a..f
# The smallest number is 48 ("0".ord)
#
def hash_symbol_int(char)
char.ord - 47 # 47 means "0".ord - 1
end
##
# Added records in hosts file
#
def update_hosts_file(config)
until File.writable? hosts_file
puts "Cannot update etc/hosts file. It's not writable".red if @@verbosity
return
end
hosts_content = File.read(hosts_file).strip
updated = false
config.each { |hostname, item|
next if item[0][:host_ip].nil? && item[0]['host_ip'].nil?
host_ip = item[0][:host_ip] || item[0]['host_ip']
# 20 symbols space for IP address
entry = host_ip + ' ' * (20 - host_ip.length) + hostname
# ignore exist entries
if hosts_content.scan(/^#{host_ip}[ \t]+#{hostname}$/).empty?
if hosts_content.scan(/^(\d{1,3}\.?){4}[ \t]+#{hostname}$/).empty?
hosts_content += $/ + entry
else
hosts_content = hosts_content.gsub!(/^(\d{1,3}\.?){4}[ \t]+#{hostname}$/, entry)
end
updated = true
end
}
# Apply changed hosts file content
if updated
puts 'etc/hosts file has been updated.'.yellow
fw = File.open(hosts_file, 'w')
fw.write hosts_content + $/
fw.close
end
end
##
# Get path to hosts file
#
def hosts_file
ENV['OS'] == 'Windows_NT' ? 'c:/Windows/System32/drivers/etc/hosts' : '/etc/hosts'
end
#endregion
end
end
# String colorization
class String
# colorization
def colorize(color_code)
"\e[#{color_code}m#{self}\e[0m"
end
def red
colorize(31)
end
def green
colorize(32)
end
def yellow
colorize(33)
end
def blue
colorize(34)
end
def pink
colorize(35)
end
def light_blue
colorize(36)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment