vagrant/plugins/providers/docker/action/prepare_networks.rb

370 lines
15 KiB
Ruby

require 'ipaddr'
require 'log4r'
require 'vagrant/util/scoped_hash_override'
module VagrantPlugins
module DockerProvider
module Action
class PrepareNetworks
include Vagrant::Util::ScopedHashOverride
@@lock = Mutex.new
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new('vagrant::plugins::docker::preparenetworks')
end
# Generate CLI arguments for creating the docker network.
#
# @param [Hash] options Options from the network config
# @returns[Array<String>] Network create arguments
def generate_create_cli_arguments(options)
options.map do |key, value|
# If value is false, option is not set
next if value.to_s == "false"
# If value is true, consider feature flag with no value
opt = value.to_s == "true" ? [] : [value]
opt.unshift("--#{key.to_s.tr("_", "-")}")
end.flatten.compact
end
# @return [Array<Socket::Ifaddr>] interface list
def list_interfaces
Socket.getifaddrs.find_all do |i|
!i.addr.nil? && i.addr.ip? && !i.addr.ipv4_loopback? &&
!i.addr.ipv6_loopback? && !i.addr.ipv6_linklocal?
end
end
# Validates that a network name exists. If it does not
# exist, an exception is raised.
#
# @param [String] network_name Name of existing network
# @param [Hash] env Local call env
# @return [Boolean]
def validate_network_name!(network_name, env)
if !env[:machine].provider.driver.existing_named_network?(network_name)
raise Errors::NetworkNameUndefined,
network_name: network_name
end
true
end
# Validates that the provided options are compatible with a
# pre-existing network. Raises exceptions on invalid configurations
#
# @param [String] network_name Name of the network
# @param [Hash] root_options Root networking options
# @param [Hash] network_options Docker scoped networking options
# @param [Driver] driver Docker driver
# @return [Boolean]
def validate_network_configuration!(network_name, root_options, network_options, driver)
if root_options[:ip] &&
driver.network_containing_address(root_options[:ip]) != network_name
raise Errors::NetworkAddressInvalid,
address: root_options[:ip],
network_name: network_name
end
if network_options[:subnet] &&
driver.network_containing_address(network_options[:subnet]) != network_name
raise Errors::NetworkSubnetInvalid,
subnet: network_options[:subnet],
network_name: network_name
end
true
end
# Generate configuration for private network
#
# @param [Hash] root_options Root networking options
# @param [Hash] net_options Docker scoped networking options
# @param [Hash] env Local call env
# @return [String, Hash] Network name and updated network_options
def process_private_network(root_options, network_options, env)
if root_options[:name] && validate_network_name!(root_options[:name], env)
network_name = root_options[:name]
end
if root_options[:type].to_s == "dhcp"
if !root_options[:ip] && !root_options[:subnet]
network_name = "vagrant_network" if !network_name
return [network_name, network_options]
end
if root_options[:subnet]
addr = IPAddr.new(root_options[:subnet])
root_options[:netmask] = addr.prefix
end
end
if root_options[:ip]
addr = IPAddr.new(root_options[:ip])
elsif addr.nil?
raise Errors::NetworkIPAddressRequired
end
# If address is ipv6, enable ipv6 support
network_options[:ipv6] = addr.ipv6?
# If no mask is provided, attempt to locate any existing
# network which contains the assigned IP address
if !root_options[:netmask] && !network_name
network_name = env[:machine].provider.driver.
network_containing_address(root_options[:ip])
# When no existing network is found, we are creating
# a new network. Since no mask was provided, default
# to /24 for ipv4 and /64 for ipv6
if !network_name
root_options[:netmask] = addr.ipv4? ? 24 : 64
end
end
# With no network name, process options to find or determine
# name for new network
if !network_name
if !root_options[:subnet]
# Only generate a subnet if not given one
subnet = IPAddr.new("#{addr}/#{root_options[:netmask]}")
network = "#{subnet}/#{root_options[:netmask]}"
else
network = root_options[:subnet]
end
network_options[:subnet] = network
existing_network = env[:machine].provider.driver.
network_defined?(network)
if !existing_network
network_name = "vagrant_network_#{network}"
else
if !existing_network.to_s.start_with?("vagrant_network")
env[:ui].warn(I18n.t("docker_provider.subnet_exists",
network_name: existing_network,
subnet: network))
end
network_name = existing_network
end
end
[network_name, network_options]
end
# Generate configuration for public network
#
# TODO: When the Vagrant installer upgrades to Ruby 2.5.x,
# remove all instances of the roundabout way of determining a prefix
# and instead just use the built-in `.prefix` method
#
# @param [Hash] root_options Root networking options
# @param [Hash] net_options Docker scoped networking options
# @param [Hash] env Local call env
# @return [String, Hash] Network name and updated network_options
def process_public_network(root_options, net_options, env)
if root_options[:name] && validate_network_name!(root_options[:name], env)
network_name = root_options[:name]
end
if !network_name
valid_interfaces = list_interfaces
if valid_interfaces.empty?
raise Errors::NetworkNoInterfaces
elsif valid_interfaces.size == 1
bridge_interface = valid_interfaces.first
elsif i = valid_interfaces.detect{|i| Array(root_options[:bridge]).include?(i.name) }
bridge_interface = i
end
if !bridge_interface
env[:ui].info(I18n.t("vagrant.actions.vm.bridged_networking.available"),
prefix: false)
valid_interfaces.each_with_index do |int, i|
env[:ui].info("#{i + 1}) #{int.name}", prefix: false)
end
env[:ui].info(I18n.t(
"vagrant.actions.vm.bridged_networking.choice_help") + "\n",
prefix: false
)
end
while !bridge_interface
choice = env[:ui].ask(
I18n.t("vagrant.actions.vm.bridged_networking.select_interface") + " ",
prefix: false)
bridge_interface = valid_interfaces[choice.to_i - 1]
end
base_opts = Vagrant::Util::HashWithIndifferentAccess.new
base_opts[:opt] = "parent=#{bridge_interface.name}"
subnet = IPAddr.new(bridge_interface.addr.ip_address <<
"/" << bridge_interface.netmask.ip_unpack.first)
netmask = bridge_interface.netmask.ip_unpack.first
prefix = IPAddr.new("255.255.255.255/#{netmask}").to_i.to_s(2).count("1")
base_opts[:subnet] = "#{subnet}/#{prefix}"
subnet_addr = IPAddr.new(base_opts[:subnet])
base_opts[:driver] = "macvlan"
base_opts[:gateway] = subnet_addr.succ.to_s
base_opts[:ipv6] = subnet_addr.ipv6?
network_options = base_opts.merge(net_options)
# Check if network already exists for this subnet
network_name = env[:machine].provider.driver.
network_containing_address(network_options[:gateway])
if !network_name
network_name = "vagrant_network_public_#{bridge_interface.name}"
end
# If the network doesn't already exist, gather available address range
# within subnet which docker can provide addressing
if !env[:machine].provider.driver.existing_named_network?(network_name)
if !net_options[:gateway]
network_options[:gateway] = request_public_gateway(
network_options, bridge_interface.name, env)
end
network_options[:ip_range] = request_public_iprange(
network_options, bridge_interface, env)
end
end
[network_name, network_options]
end
# Request the gateway address for the public network
#
# @param [Hash] network_options Docker scoped networking options
# @param [String] interface The bridge interface used
# @param [Hash] env Local call env
# @return [String] Gateway address
def request_public_gateway(network_options, interface, env)
subnet = IPAddr.new(network_options[:subnet])
gateway = nil
while !gateway
gateway = env[:ui].ask(I18n.t(
"docker_provider.network_bridge_gateway_request",
interface: interface,
default_gateway: network_options[:gateway]) + " ",
prefix: false
).strip
if gateway.empty?
gateway = network_options[:gateway]
end
begin
gateway = IPAddr.new(gateway)
if !subnet.include?(gateway)
env[:ui].warn(I18n.t("docker_provider.network_bridge_gateway_outofbounds",
gateway: gateway,
subnet: network_options[:subnet]) + "\n", prefix: false)
end
rescue IPAddr::InvalidAddressError
env[:ui].warn(I18n.t("docker_provider.network_bridge_gateway_invalid",
gateway: gateway) + "\n", prefix: false)
gateway = nil
end
end
gateway.to_s
end
# Request the IP range allowed for use by docker when creating a new
# public network
#
# TODO: When the Vagrant installer upgrades to Ruby 2.5.x,
# remove all instances of the roundabout way of determining a prefix
# and instead just use the built-in `.prefix` method
#
# @param [Hash] network_options Docker scoped networking options
# @param [Socket::Ifaddr] interface The bridge interface used
# @param [Hash] env Local call env
# @return [String] Address range
def request_public_iprange(network_options, interface, env)
return network_options[:ip_range] if network_options[:ip_range]
subnet = IPAddr.new(network_options[:subnet])
env[:ui].info(I18n.t(
"docker_provider.network_bridge_iprange_info") + "\n",
prefix: false
)
range = nil
while !range
range = env[:ui].ask(I18n.t(
"docker_provider.network_bridge_iprange_request",
interface: interface.name,
default_range: network_options[:subnet]) + " ",
prefix: false
).strip
if range.empty?
range = network_options[:subnet]
end
begin
range = IPAddr.new(range)
if !subnet.include?(range)
netmask = interface.netmask.ip_unpack.first
prefix = IPAddr.new("255.255.255.255/#{netmask}").to_i.to_s(2).count("1")
env[:ui].warn(I18n.t(
"docker_provider.network_bridge_iprange_outofbounds",
subnet: network_options[:subnet],
range: "#{range}/#{prefix}"
) + "\n", prefix: false)
range = nil
end
rescue IPAddr::InvalidAddressError
env[:ui].warn(I18n.t(
"docker_provider.network_bridge_iprange_invalid",
range: range) + "\n", prefix: false)
range = nil
end
end
netmask = interface.netmask.ip_unpack.first
prefix = IPAddr.new("255.255.255.255/#{netmask}").to_i.to_s(2).count("1")
"#{range}/#{prefix}"
end
# Execute the action
def call(env)
# If we are using a host VM, then don't worry about it
machine = env[:machine]
if machine.provider.host_vm?
@logger.debug("Not setting up networks because docker host_vm is in use")
return @app.call(env)
end
connections = {}
@@lock.synchronize do
machine.env.lock("docker-network-create", retry: true) do
env[:ui].info(I18n.t("docker_provider.network_create"))
machine.config.vm.networks.each_with_index do |net_info, net_idx|
type, options = net_info
network_options = scoped_hash_override(options, :docker_network)
network_options.delete_if{|k,_| options.key?(k)}
case type
when :public_network
network_name, network_options = process_public_network(
options, network_options, env)
when :private_network
network_name, network_options = process_private_network(
options, network_options, env)
else
next # unsupported type so ignore
end
if !network_name
raise Errors::NetworkInvalidOption, container: machine.name
end
if !machine.provider.driver.existing_named_network?(network_name)
@logger.debug("Creating network #{network_name}")
cli_opts = generate_create_cli_arguments(network_options)
machine.provider.driver.create_network(network_name, cli_opts)
else
@logger.debug("Network #{network_name} already created")
validate_network_configuration!(network_name, options, network_options, machine.provider.driver)
end
connections[net_idx] = network_name
end
end
end
env[:docker_connects] = connections
@app.call(env)
end
end
end
end
end