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] 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] 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 idx = valid_interfaces.detect{|i| Array(root_options[:bridge]).include?(i.name) } bridge_interface = idx 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